Files
gochat/docs/requirements/M3-conversation-and-message.md
T
2026-06-04 15:44:48 +08:00

776 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M3 对话与消息 — Chatwoot 功能梳理文档
> 基于 Chatwoot 源码深度阅读 + CodeGraph 上下文梳理
> 生成日期:2026-05-22
> 参照仓库:chatwoot-reference
---
## 1. 对话 CRUD + 状态流转
### 对话创建
- 功能描述:对话(Conversation)是 Chatwoot 的核心实体,代表客户与团队之间的一次完整交互线程。创建时自动关联 Account、Inbox、Contact、ContactInbox。
- 用户操作流程:
1. 客户通过渠道(WhatsApp/Facebook/Telegram 等)发消息,系统自动创建对话
2. 坐席可通过 API 手动创建对话(`POST /api/v1/accounts/{account_id}/conversations`),同时可附带第一条消息
3. Campaign 发起时批量创建对话
4. 对话创建后触发 `CONVERSATION_CREATED` 事件,经 Dispatcher 通知所有 Listener
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations` — 手动创建对话
- 请求参数:`{ inbox_id, contact_id, source_id, additional_attributes, custom_attributes, status, assignee_id, team_id, snoozed_until, message: { content, content_type, attachments } }`
- `GET /api/v1/accounts/{account_id}/conversations/{display_id}` — 获取对话详情
- 涉及的数据模型:
- **Conversation**(`conversations` 表)核心字段:
- `id, display_id`(account 级自增编号), `uuid`(全局唯一), `status`, `priority`, `snoozed_until`
- `account_id, inbox_id, contact_id, contact_inbox_id, assignee_id, assignee_agent_bot_id, team_id, campaign_id, sla_policy_id`
- `additional_attributes`(jsonb), `custom_attributes`(jsonb), `cached_label_list`(text)
- `agent_last_seen_at, assignee_last_seen_at, contact_last_seen_at, waiting_since, last_activity_at, first_reply_created_at`
- `status` enum:`open(0), resolved(1), pending(2), snoozed(3)`
- `priority` enum:`low(0), medium(1), high(2), urgent(3)`
- 涉及的业务逻辑:
- **ConversationBuilder**(`app/builders/conversation_builder.rb`):
- 若 Inbox 设置 `lock_to_single_conversation`,查找该 ContactInbox 的最近对话复用
- 否则创建新对话(未解决时才建新对话的逻辑在 IncomingMessageService 中)
- `before_create :determine_conversation_status`:根据 Contact 是否 blocked(→ resolved)、是否 Campaign(→ pending)、是否 Bot Inbox(→ pending)决定初始状态
- `before_validation :reset_agent_bot_when_assignee_present`:如 assignee_id 存在则清空 assignee_agent_bot_id
- 涉及的自动化/规则/事件:
- `after_create_commit :notify_conversation_creation` → Dispatcher `CONVERSATION_CREATED`
- `after_create_commit :load_attributes_created_by_db_triggers` → 从 DB 读取 display_id(由 DB trigger 生成)
- `before_destroy :set_unread_count_deletion_data` → 收集未读计数删除所需数据
- `after_destroy_commit :notify_conversation_deletion` → Dispatcher `CONVERSATION_DELETED`
### 对话更新
- 功能描述:支持修改对话的各种属性,包括状态变更、标签、自定义属性、团队/坐席分配、优先级等。
- 涉及的API端点:
- `PATCH /api/v1/accounts/{account_id}/conversations/{display_id}` — 更新对话属性
- 请求参数:`{ status, assignee_id, team_id, priority, labels, custom_attributes, additional_attributes, snoozed_until }`
- 涉及的业务逻辑:
- `after_update_commit :execute_after_update_commit_callbacks`:触发一系列更新后处理
- `handle_resolved_status_change` → resolved 时清空 `waiting_since`
- `notify_status_change` → 根据状态变化派发不同事件
- `create_activity` → 创建 Activity Message(如状态变更记录)
- `notify_conversation_updation` → 仅当变更字段在 `list_of_keys` 中时派发 `CONVERSATION_UPDATED`
- `list_of_keys` 允许触发更新事件的字段:`team_id, assignee_id, assignee_agent_bot_id, status, snoozed_until, custom_attributes, label_list, waiting_since, first_reply_created_at, priority`
- 涉及的自动化/规则/事件:
- `ASSIGNEE_CHANGED` → 当 assignee_id 变化时
- `TEAM_CHANGED` → 当 team_id 变化时
- `CONVERSATION_STATUS_CHANGED` / `CONVERSATION_OPENED` / `CONVERSATION_RESOLVED`
- `CONVERSATION_UPDATED` → 其他允许字段变化时
### 对话状态流转
- 功能描述:对话有四种状态可流转,状态切换触发不同事件和自动化。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/toggle_status` — 切换对话状态
- 请求参数:`{ status }`(可选,不传则 toggle:open ↔ resolved)
- 状态流转规则:
- **open → resolved**:坐席/自动化解决对话
- **resolved → open**:客户新消息 / 坐席重新打开
- **pending → open**:Bot handoff(`bot_handoff!` 方法)
- **open/snoozed → snoozed**:设置 `snoozed_until` 时间
- **snoozed → open**:当 `snoozed_until` 到期或手动取消
- `toggle_status`:若无参数则 open ↔ resolved(若 pending/snoozed 则 → open)
- AgentBot 可执行 `pending → open`(bot handoff)
- Agent 切换为 open 时自动 assign 给自己
- 涉及的业务逻辑:
- `before_save :ensure_snooze_until_reset` → 非 snoozed 状态时清空 snoozed_until
- `handle_resolved_status_change` → resolved 时 `update_column(:waiting_since, nil)`
- AutoAssignmentHandler:状态变 open 时触发自动分配
- Auto-resolve:Account 设置 `auto_resolve_after` 分钟后自动 resolve
### 对话删除
- 涉及的API端点:
- `DELETE /api/v1/accounts/{account_id}/conversations/{display_id}` — 删除对话(需要 administrator 权限)
- 涉及的业务逻辑:
- `before_destroy :set_unread_count_deletion_data` → 保留 account_id, inbox_id, assignee_id 等用于清理 Redis 未读计数
- `after_destroy_commit :notify_conversation_deletion` → `CONVERSATION_DELETED`
- 关联级联删除:messages → destroy_async, conversation_participants → destroy_async, notifications → destroy_async
### 对话静音/取消静音
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/mute` — 静音
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/unmute` — 取消静音
- 涉及的业务逻辑:
- **ConversationMuteHelpers**:mute! → resolved! + contact.update(blocked: true) + 创建 activity message;unmute! → contact.update(blocked: false) + 创建 activity message
### 对话邮件转录
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/transcript` — 发送对话转录邮件
- 请求参数:`{ email }`
- 涉及的业务逻辑:
- 企业版 feature gate:`account.email_transcript_enabled?`
- 速率限制:`account.within_email_rate_limit?`
- 异步发送:`ConversationReplyMailer.conversation_transcript.deliver_later`
---
## 2. 对话搜索 + 过滤 + 排序
### 对话列表与搜索
- 功能描述:提供对话列表视图,支持按状态/分配/标签/团队等多维度过滤、全文搜索和排序。
- 涉及的API端点:
- `GET /api/v1/accounts/{account_id}/conversations` — 列表(带过滤参数)
- `GET /api/v1/accounts/{account_id}/conversations/meta` — 仅返回计数信息(mine_count, unassigned_count, all_count)
- `GET /api/v1/accounts/{account_id}/conversations/search` — 搜索(q 参数)
- `POST /api/v1/accounts/{account_id}/conversations/filter` — 自定义过滤器(高级查询)
- 涉及的数据模型/查询参数:
- ConversationFinder 支持的参数:
- `inbox_id` — 按收件箱过滤
- `assignee_type` — `me`(我的), `unassigned`(未分配), `all`(全部)
- `status` — `open`, `resolved`, `pending`, `snoozed`(默认 open)
- `team_id` — 按团队过滤
- `labels` — 按标签过滤
- `q` — 搜索关键词(全文搜索 content + additional_attributes)
- `sort_by` — 排序选项
- 排序选项(ConversationFinder::SORT_OPTIONS):
- `last_activity_at_asc/desc` — 最后活动时间升/降序
- `created_at_asc/desc` — 创建时间升/降序
- `priority_asc/desc` — 优先级升/降序
- `waiting_since_asc/desc` — 等待时间升/降序
- `priority_desc_created_at_asc` — 先按优先级降序再按创建时间升序(复合排序)
- 涉及的业务逻辑:
- **ConversationFinder**(`app/finders/conversation_finder.rb`):
- 权限过滤:非 administrator 只能看自己 accessible inboxes 的对话
- 分步处理:`set_up` → `find_all_conversations` → `filter_by_status` → `filter_by_team` → `filter_by_labels` → `filter_by_query` → `filter_by_assignee_type`
- 返回 `{ conversations: [...], count: { mine_count, assigned_count, unassigned_count, all_count } }`
- **Conversations::FilterService**(`app/services/conversations/filter_service.rb`)继承 FilterService:
- 支持 filter_keys.yml 中定义的所有过滤字段
- 支持操作符:`equal_to, not_equal_to, contains, does_not_contain, is_present, is_not_present, is_greater_than, is_less_than, days_before`
- 先经 PermissionFilterService 限制可访问的 Inbox
- 结果按 `sort_on_last_activity_at` 分页排序
### 权限过滤
- 功能描述:根据用户角色限制可见的对话范围。
- 涉及的业务逻辑:
- **Conversations::PermissionFilterService**:administrator 可看全部,其他角色仅能看自己所属 Inbox 的对话
- 非管理员:`conversations.where(inbox: user.inboxes.where(account_id: account.id))`
---
## 3. 标签 + 优先级
### 标签管理
- 功能描述:对话支持标签(Labels)系统,用于分类和过滤。标签基于 ActsAsTaggableOn,有 cached_label_list 缓存字段。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/labels` — 设置标签
- `GET /api/v1/accounts/{account_id}/conversations/{display_id}/labels` — 获取标签列表
- 请求/响应:`{ labels: ["support", "priority"] }`
- 涉及的数据模型:
- Conversation `cached_label_list`(text)— 缓存标签列表,逗号分隔
- Conversation `include Labelable` — 使用 ActsAsTaggableOn 的标签能力
- 涉及的业务逻辑:
- **LabelConcern**(`app/controllers/concerns/label_concern.rb`):
- `create` → `model.update_labels(permitted_params[:labels])`
- `index` → 返回 `model.label_list`
- 标签变化会触发 `CONVERSATION_UPDATED`(因 label_list 在 `list_of_keys` 中)
- 标签变化也会触发 UnreadCounts 的 Refresher(因标签影响分组计数)
### 优先级管理
- 功能描述:对话可设置优先级(low/medium/high/urgent),用于排序和工作量管理。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/toggle_priority` — 设置优先级
- 请求参数:`{ priority }`(`low`, `medium`, `high`, `urgent`)
- 涉及的数据模型:
- `priority` enum:`low(0), medium(1), high(2), urgent(3)`
- 涉及的业务逻辑:
- `toggle_priority(priority)` → 设置 priority 值并保存
- 优先级变化触发 `CONVERSATION_UPDATED`
---
## 4. 消息收发 + 类型
### 消息创建(坐席/API 发送)
- 功能描述:坐席或 API 端在对话中创建消息,支持文本、模板、私有备注、附件等多种类型。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/messages` — 创建消息
- 请求参数:
```
{
content, // 消息内容
message_type, // outgoing/template (默认 outgoing)
private, // 是否私有备注 (默认 false)
content_type, // text/input_text/input_email/input_select/cards/form/article 等
content_attributes: { // 扩展属性
in_reply_to, // 回复指定消息 ID
items, // 选择项 (input_select/cards)
submitted_values, // 提交值 (form)
deleted // 标记删除
},
attachments: [ // 附件列表(最多 15 个)
{ file, content_type, file_type }
],
template_params: { // WhatsApp 模板参数
name, category, language, namespace, processed_params
}
}
```
- 涉及的数据模型:
- **Message**(`messages` 表)核心字段:
- `id, content, content_type, message_type, private, status, sender_id, sender_type`
- `conversation_id, account_id, inbox_id, source_id`
- `content_attributes`(json), `additional_attributes`(jsonb), `external_source_ids`(jsonb)
- `processed_message_content`(text), `sentiment`(jsonb)
- `message_type` enum:`incoming(0), outgoing(1), activity(2), template(3)`
- `content_type` enum:`text(0), input_text(1), input_textarea(2), input_email(3), input_select(4), cards(5), form(6), article(7), incoming_email(8), input_csat(9), integrations(10), sticker(11), voice_call(12)`
- `status` enum:`sent(0), delivered(1), read(2), failed(3)`
- `content_attributes` stored accessors:`submitted_email, items, submitted_values, email, in_reply_to, deleted, external_created_at, story_sender, story_id, external_error, translations, in_reply_to_external_id, is_unsupported, data`
- `external_source_ids` stored accessors:`slack`(prefix: `external_source_id_slack`)
- 涉及的业务逻辑:
- **Messages::MessageBuilder**(`app/builders/messages/message_builder.rb`):
- 构建 Message 对象(`conversation.messages.build(message_params)`)
- 处理附件(`process_attachments`)
- 处理邮件相关(`process_emails` / `process_email_content`)
- 保存后触发 `after_create_commit` 回调链
- 消息创建后触发的事件:
- `after_create_commit :execute_after_create_commit_callbacks`
- 调度 SendReplyJob → 调用对应渠道的 SendOnChannelService 发送外部消息
- 调度 MentionService → 处理 @mention
- 调度 NewMessageNotificationService → 通知 assignee 和参与用户
- Dispatcher 派发 `MESSAGE_CREATED` 事件
- 对话更新 `last_activity_at`
- 更新 `waiting_since`(incoming 消息时)
- 更新 `first_reply_created_at`(第一条 outgoing 消息时)
### 消息更新/删除
- 涉及的API端点:
- `PATCH /api/v1/accounts/{account_id}/conversations/{display_id}/messages/{id}` — 更新消息状态(仅 API Inbox)
- `DELETE /api/v1/accounts/{account_id}/conversations/{display_id}/messages/{id}` — 删除消息(软删除)
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/messages/{id}/retry` — 重试失败消息
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/messages/{id}/translate` — 翻译消息(Google Translate)
- 消息删除逻辑:
- 软删除:`content = I18n.t('conversations.messages.deleted'), content_type = :text, content_attributes = { deleted: true }`
- 级联删除所有 attachments
- 消息状态更新逻辑:
- **Messages::StatusUpdateService**:`sent → delivered → read`(正向),可 `→ failed`(任意状态)
- 不允许 `read → delivered`(反向降级)
- failed 时记录 `external_error`
- 消息重试逻辑:
- 状态重置为 `sent`,清空 `content_attributes`,重新执行 `SendReplyJob`
- 消息翻译逻辑:
- 使用 `Integrations::GoogleTranslate::ProcessorService`
- 翻译结果存入 `content_attributes[:translations]`(按目标语言 key 存储)
### 消息查找/分页
- 功能描述:获取对话中的消息列表,支持分页加载、过滤内部消息。
- 涉及的API端点:
- `GET /api/v1/accounts/{account_id}/conversations/{display_id}/messages` — 消息列表
- 参数:`after, before, filter_internal_messages`
- 涉及的业务逻辑:
- **MessageFinder**(`app/finders/message_finder.rb`):
- 默认最新 20 条(`messages_latest`)
- `before` 参数:加载更早的消息(ID < before,20 条)
- `after` 参数:加载更新的消息(ID > after,最多 100 条)
- `before + after`:加载区间内消息(最多 1000 条)
- `filter_internal_messages`:过滤掉 private 和 activity 消息
- eager load:`includes(:attachments, :sender, sender: { avatar_attachment: [:blob] })`
### 消息类型详解
| 类型 | 值 | 说明 |
|------|------|------|
| incoming | 0 | 客户发来的消息 |
| outgoing | 1 | 坐席/系统发出的消息 |
| activity | 2 | 系统活动消息(状态变更、分配变更等,自动生成) |
| template | 3 | WhatsApp 模板消息(24h 窗口外使用) |
| 内容类型 | 值 | 说明 |
|----------|------|------|
| text | 0 | 纯文本 |
| input_text | 1 | Bot 文本输入 |
| input_textarea | 2 | Bot 多行文本输入 |
| input_email | 3 | Bot 邮箱输入 |
| input_select | 4 | Bot 选项选择 |
| cards | 5 | 卡片式消息 |
| form | 6 | 表单消息 |
| article | 7 | 知识库文章 |
| incoming_email | 8 | 来信邮件 |
| input_csat | 9 | CSAT 评分 |
| integrations | 10 | 集成消息 |
| sticker | 11 | 贴纸(WhatsApp/Line) |
| voice_call | 12 | 语音通话气泡(企业版) |
---
## 5. 附件上传
### 附件上传流程
- 功能描述:消息可附带最多 15 个附件,支持多种文件类型。通过 ActiveStorage 直接上传机制支持大文件。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/direct_uploads` — ActiveStorage 直传(大文件)
- 消息创建时通过 `attachments[]` 参数附带
- 涉及的数据模型:
- **Attachment**(`attachments` 表)核心字段:
- `id, message_id, account_id, file_type, extension, external_url, fallback_title`
- `coordinates_lat, coordinates_long`(地理位置)
- `meta`(jsonb)
- `file_type` enum:`image(0), audio(1), video(2), file(3), location(4), fallback(5), share(6), story_mention(7), contact(8), ig_reel(9), ig_post(10), ig_story(11), embed(12)`
- ActiveStorage `has_one_attached :file`
- 涉及的业务逻辑:
- 文件类型白名单(`ACCEPTABLE_FILE_TYPES`):CSV/Plain/PDF/Word/Excel/PowerPoint/Zip/7z/RAR/Tar 等
- 通用文件处理:`application/octet-stream` + 指定扩展名
- `before_save :set_extension` → 从文件名提取扩展名
- `validate :acceptable_file` → 校验文件大小和类型
- `Message::NUMBER_OF_PERMITTED_ATTACHMENTS = 15`
- `before_add :validate_attachments_limit` → 防止超过 15 个附件
- DirectUploadsController:继承 `ActiveStorage::DirectUploadsController`,增加 account + conversation 校验
---
## 6. 已读回执
### 坐席已读回执
- 功能描述:坐席查看对话后更新 `agent_last_seen_at` / `assignee_last_seen_at`,用于计算未读消息数。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/update_last_seen` — 更新已读时间
- 涉及的业务逻辑:
- 区分 assignee vs 非 assignee:
- assignee → 更新 `assignee_last_seen_at`
- 非 assignee → 更新 `agent_last_seen_at`
- 节流策略:无未读消息时每小时最多更新一次(减少 DB 写压力)
- 有未读消息时立即更新
- 同时调用 `Notification::MarkConversationReadService` 清除该对话的通知
- 触发 `CONVERSATION_READ` 事件(ActionCable 实时推送)
### 未读消息计数
- 功能描述:实时计算各维度(Inbox/Label/Team/Assignee)的未读消息数,使用 Redis 缓存优化性能。
- 涉及的API端点:
- `GET /api/v1/accounts/{account_id}/conversations/unread_counts` — 未读计数
- 需要 feature flag:`conversation_unread_counts`
- 涉及的数据模型/缓存:
- Redis 缓存结构(`Conversations::UnreadCounts::Store`):
- 按 Inbox + Label + Team 维度存储未读对话 membership
- base cache:所有 open 对话的 membership(inbox_id, label_ids, team_id)
- assignment cache:加上 assignee_id 维度(支持 mine_count 精确计算)
- TTL:base 24h, set 25h
- 涉及的业务逻辑:
- **Conversations::UnreadCounts::Builder**:批量扫描 open 对话,写入 Redis memberships
- **Conversations::UnreadCounts::Counter**:
- 权限模式检查(manage_all / unassigned_manage / participating_manage / none)
- 返回 `{ inboxes: {...}, labels: {...}, teams: {...} }` 各维度的未读数
- **Conversations::UnreadCounts::Refresher**:对话属性变化时增量更新 Redis 缓存
- **Conversations::UnreadCounts::Notifier**:缓存变化后派发 `CONVERSATION_UNREAD_COUNT_CHANGED` 事件
- **Conversations::UnreadCounts::Listener**:监听 message_created / status_changed / assignee_changed / team_changed / label_changed 事件 → 触发 Refresher
- 构建锁机制:`Redis::LockManager` 确保同一 account 只有一个 Builder 在运行,等待超时 30s
### 客户已读回执(渠道侧)
- 功能描述:部分渠道(WhatsApp/Facebook)支持客户侧的已读回执,通过 `Messages::StatusUpdateService` 更新消息状态为 `read`。
- 涉及的业务逻辑:
- WhatsApp:通过 webhook status callback 更新消息 `read` 状态
- Facebook/Messenger:通过 messaging_seen 事件更新
- `Messages::StatusUpdateService`:验证状态转换合法性(不允许 `read → delivered`)
---
## 7. IncomingMessageService 各渠道
### 通用模式
- 功能描述:各渠道的 IncomingMessageService 职责相似:解析渠道 webhook → 查找/创建 Contact → 查找/创建 Conversation → 创建 Message → 附加附件。
- 涉及的通用逻辑:
- `ContactInboxWithContactBuilder` → 根据 source_id 查找/创建 Contact + ContactInbox
- 对话查找/创建逻辑:
- `lock_to_single_conversation` → 查找最近对话(无论状态)
- 否则 → 查找未解决对话,若都已解决则创建新对话
- 消息创建:`conversation.messages.build/build.create!`
- 附件处理:`attach_files` → 下载渠道侧的媒体文件,创建 Attachment
### WhatsApp IncomingMessageService
- 涉及的文件:`app/services/whatsapp/incoming_message_base_service.rb`, `incoming_message_service.rb`
- 特殊逻辑:
- 支持 status callbacks(消息状态回执:delivered/read/failed)
- 支持 outgoing echo(坐席在 WhatsApp App 直接回复时,系统识别为 echo 而不重复创建)
- Redis 去重:`lock_message_source_id!`(SET NX)防止同消息并发重复处理
- 支持 reaction/ephemeral/unsupported 类型过滤
- 支持 sticker、location、contact、document、image、video、audio 等多种消息类型
- 联系人 blocked 时跳过(除非 outgoing echo)
- 使用 `Whatsapp::IncomingMessageServiceHelpers` 提供通用方法
### Telegram IncomingMessageService
- 涉及的文件:`app/services/telegram/incoming_message_service.rb`
- 特殊逻辑:
- 支持私聊消息(`private_message?`),不支持群聊
- 支持 text/photo/document/video/audio/voice/sticker/location/animation 等类型
- Bot token 验证:通过 inbox.channel.token 验证消息来源
- 更新联系人头像(`update_contact_avatar`)
- 支持 in_reply_to(回复特定消息)
### SMS IncomingMessageService
- 涉及的文件:`app/services/sms/incoming_message_service.rb`
- 特殊逻辑:
- 使用 `TelephoneNumber` 解析/格式化电话号码
- 通过 `params[:from]` 作为 source_id 查找 Contact
- 附件处理(MMS 图片等)
### Twilio IncomingMessageService
- 涉及的文件:`app/services/twilio/incoming_message_service.rb`
- 特殊逻辑:
- 支持两种模式:MessagingServiceSid 或 AccountSid+PhoneNumber 查找 channel
- 支持 WhatsApp 和 SMS(通过 `WhatsappIdentifierHelper` 区分)
- 支持地理位置附件(`attach_location if location_message?`)
- BSUID-only payload 处理(WhatsApp)
### Line IncomingMessageService
- 涉及的文件:`app/services/line/incoming_message_service.rb`
- 特殊逻辑:
- 批量处理 events 数组
- 支持 text/image/video/audio/sticker/location 等类型
- 贴纸特殊处理:使用 LINE_STICKER_IMAGE_URL 模板构建贴纸图片 URL
- 通过 LINE SDK 获取联系人信息
### Facebook/Messenger IncomingMessageService
- 涉及的文件:`app/builders/messages/facebook/message_builder.rb`, `app/builders/messages/messenger/message_builder.rb`
- 特殊逻辑:
- 通过 Facebook Graph API webhook 接收
- 支持 text/attachment/reaction 等类型
- 使用 FbObject 查找/创建 Contact
### Instagram IncomingMessageService
- 涉及的文件:`app/builders/messages/instagram/message_builder.rb`, `app/builders/messages/instagram/messenger/message_builder.rb`
- 特殊逻辑:
- Instagram Messaging API 接收
- 支持 story_mention/ig_reel/ig_post/ig_story 等专属类型
- 不支持 ephemeral messages
---
## 8. OutgoingMessageService 各渠道
### 通用模式(Base::SendOnChannelService)
- 涉及的文件:`app/services/base/send_on_channel_service.rb`
- 功能描述:所有渠道的发送服务继承此基类,统一校验和流程。
- 通用逻辑:
- `validate_target_channel` → 校验消息的 inbox channel 类型是否匹配
- `outgoing_message?` → 仅 outgoing/template 消息才发送
- `invalid_message?` → 过滤 private notes、source_id 已存在的 echo 消息、voice_call 气泡
- `perform_reply` → 子类实现具体发送逻辑
- 发送成功后 `message.update!(source_id: ...)` 记录渠道侧 ID
- 发送失败时 `Messages::StatusUpdateService.new(message, 'failed', error).perform`
### WhatsApp SendOnWhatsappService
- 涉及的文件:`app/services/whatsapp/send_on_whatsapp_service.rb`
- 特殊逻辑:
- 判断是否需发送模板消息:`template_params` 存在 或 超过 24h messaging window
- 模板消息:`Whatsapp::TemplateProcessorService` 处理参数 → `channel.send_template`
- Session 消息:`channel.send_message`(24h 窗口内)
- 消息窗口检测:`Conversations::MessageWindowService`(WhatsApp 24h)
### Facebook SendOnFacebookService
- 涉及的文件:`app/services/facebook/send_on_facebook_service.rb`
- 特殊逻辑:
- 文本消息和附件消息分开发送(FB API 限制)
- 使用 `Facebook::Messenger::Bot.deliver` 发送
- 错误处理:`FacebookError` → `StatusUpdateService('failed')`
- 支持 messaging_type/tag(HUMAN_AGENT 等)
### Instagram SendOnInstagramService
- 涉及的文件:`app/services/instagram/send_on_instagram_service.rb`(继承 `Instagram::BaseSendService`)
- 特殊逻辑:
- 使用 Instagram Messaging API v22.0
- 支持 HUMAN_AGENT tag(24h 窗口外)
- 支持 text/image/video 等附件
### Telegram SendOnTelegramService
- 涉及的文件:`app/services/telegram/send_on_telegram_service.rb`
- 特殊逻辑:
- 使用 Telegram Bot API `send_message_on_telegram`
- 附件:通过 `Telegram::SendAttachmentsService` 单独处理
### Line SendOnLineService
- 涉及的文件:`app/services/line/send_on_line_service.rb`
- 特殊逻辑:
- 使用 LINE Messaging API `push_message`
- 支持 text + image/video 组合发送
- 支持 input_select 类型消息(QuickReply)
- 发送成功 → `StatusUpdateService('delivered')`
- 发送失败 → `StatusUpdateService('failed', error)`
### SMS SendOnSmsService
- 涉及的文件:`app/services/sms/send_on_sms_service.rb`
- 特殊逻辑:
- `channel.send_message(phone_number, message)` 发送 SMS
- 简单直接,无特殊格式处理
### Twilio SendOnTwilioService
- 涉及的文件:`app/services/twilio/send_on_twilio_service.rb`
- 特殊逻辑:
- 支持模板消息(`content_sid` + `content_variables`)
- WhatsApp 模板:`send_template_message`
- SMS:`channel.send_message(**message_params)`
- 错误处理:`Twilio::REST::TwilioError/RestError` → `StatusUpdateService('failed')`
- 支持 CSAT 模板消息发送
### Email SendOnEmailService
- 涉及的文件:`app/services/email/send_on_email_service.rb`
- 特殊逻辑:
- 使用 `ConversationReplyMailer.email_reply` 发送邮件回复
- 仅发送 `email_notifiable_message?` 的消息
- 发送成功后记录 `source_id = reply_mail.message_id`
- 失败 → `StatusUpdateService('failed')`
### 消息发送窗口(MessageWindowService)
- 涉及的文件:`app/services/conversations/message_window_service.rb`
- 功能描述:判断当前对话是否在渠道允许的回复窗口内,影响是否只能发模板消息。
- 各渠道窗口规则:
- **WhatsApp**:24 小时窗口
- **Facebook/Messenger**:根据 `ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` 配置(7 天或无限制)
- **Instagram**:根据 `ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT` 配置
- **TikTok**:7 天窗口
- **Twilio WhatsApp**:24 小时窗口
- **API Inbox**:根据 `agent_reply_time_window` 配置(小时为单位)
- **其他渠道**:无窗口限制(可随时回复)
---
## 9. ConversationParticipant(参与者)
### 参与者管理
- 功能描述:对话参与者(ConversationParticipant)表示除 assignee 外也关注该对话的坐席,他们可以收到通知、查看对话。
- 涉及的API端点:
- `GET /api/v1/accounts/{account_id}/conversations/{display_id}/participants` — 查看参与者
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/participants` — 添加参与者
- `PATCH /api/v1/accounts/{account_id}/conversations/{display_id}/participants` — 更新参与者列表(增量添加/移除)
- `DELETE /api/v1/accounts/{account_id}/conversations/{display_id}/participants` — 移除参与者
- 参数:`{ user_ids: [...] }`
- 涉及的数据模型:
- **ConversationParticipant**(`conversation_participants` 表):
- `id, conversation_id, user_id, account_id`
- 唯一约束:`user_id + conversation_id`
- 关联:`belongs_to :conversation, :user, :account`
- 涉及的业务逻辑:
- `validate :ensure_inbox_access` → 参与者必须是该 Inbox 的 assignable_agents
- `before_validation :ensure_account_id` → 自动从 conversation 获取 account_id
- 添加参与者时使用 `find_or_create_by` 防重复
- 更新时计算差集:`participants_to_be_added_ids` = 新列表 - 当前列表;`participants_to_be_removed_ids` = 当前列表 - 新列表
### 参与者自动添加
- 功能描述:系统在特定场景下自动将坐席添加为参与者。
- 涉及的业务逻辑:
- **ParticipationListener**:
- `assignee_changed` → 自动将新 assignee 添加为 participant
- **Messages::MentionService**:
- 私有备注中 @mention 用户/团队 → 自动将被提及用户添加为 participant
- `add_mentioned_users_as_participants(validated_mentioned_ids)`
- 支持 `mention://user/{id}/{name}` 和 `mention://team/{id}/{name}` 格式
- 团队 mention 会展开为所有团队成员
---
## 10. Dispatcher + Listener 事件系统
### Dispatcher 事件分发
- 功能描述:Chatwoot 使用事件驱动架构,所有对话/消息的变更通过 Dispatcher 分发给各 Listener 处理。
- 涉及的文件:
- `app/dispatchers/dispatcher.rb` — 单例入口
- `app/dispatchers/sync_dispatcher.rb` — 同步分发(立即执行)
- `app/dispatchers/async_dispatcher.rb` — 异步分发(通过 EventDispatcherJob)
- 事件分发流程:
1. 业务代码调用 `Dispatcher.dispatch(event_name, timestamp, data)`
2. Dispatcher 同时调用 SyncDispatcher 和 AsyncDispatcher
3. SyncDispatcher → 立即调用 `publish(event.method_name, event_object)`
4. AsyncDispatcher → `EventDispatcherJob.perform_later` → 异步执行 `publish`
- 涉及的事件类型(`Events::Types` 模块中定义):
- 对话事件:`CONVERSATION_CREATED, CONVERSATION_UPDATED, CONVERSATION_STATUS_CHANGED, CONVERSATION_OPENED, CONVERSATION_RESOLVED, CONVERSATION_READ, CONVERSATION_DELETED, CONVERSATION_BOT_HANDOFF, CONVERSATION_TYPING_ON, CONVERSATION_TYPING_OFF, CONVERSATION_UNREAD_COUNT_CHANGED`
- 分配事件:`ASSIGNEE_CHANGED, TEAM_CHANGED`
- 消息事件:`MESSAGE_CREATED, MESSAGE_UPDATED, FIRST_REPLY_CREATED`
- 通知事件:`NOTIFICATION_CREATED, NOTIFICATION_UPDATED, NOTIFICATION_DELETED`
### SyncDispatcher Listener
- **ActionCableListener**:实时 WebSocket 推送
- `message_created/updated` → 推送给 inbox members + contact(通过 pubsub_token)
- `conversation_created/read/status_changed/updated` → 推送给 inbox members
- `first_reply_created` → 推送给 inbox members
- `conversation_typing_on/off` → 推送给 inbox members
- **AgentBotListener**:通知 AgentBot webhook
- `conversation_resolved/opened/status_changed/updated` → 发送 webhook 给关联的 agent bots
- `message_created` → 发送 webhook 给关联的 agent bots
### AsyncDispatcher Listener
- **AutomationRuleListener**:触发自动化规则
- `conversation_created/updated/opened/resolved` → 匹配自动化规则条件 → 执行动作
- `message_created` → 匹配消息触发规则
- **CampaignListener**:Campaign 相关事件处理
- **CsatSurveyListener**:CSAT 满意度调查触发
- **HookListener**:渠道 webhook 处理
- **InstallationWebhookListener**:安装级别 webhook 发送
- **NotificationListener**:通知创建
- `conversation_created/bot_handoff` → 为 inbox members 创建 notification
- `assignee_changed` → 为新 assignee 创建 notification
- `message_created` → 为 assignee 和 participants 创建 notification
- **ParticipationListener**:自动添加参与者(见 §9)
- **Conversations::UnreadCounts::Listener**:未读计数增量刷新(见 §6)
- **ReportingEventListener**:报表数据采集
- **WebhookListener**:Account 级 webhook 发送
- `conversation_status_changed/updated/created` → 发送 webhook payload
- `message_created/updated` → 仅 `webhook_sendable?` 的消息发送
---
## 11. Copilot 消息(企业版)
### CopilotThread
- 功能描述:Copilot(AI 助手)对话线程,用于坐席与 AI 之间的内部交互。与客户对话独立。
- 涉及的数据模型:
- **CopilotThread**(`copilot_threads` 表,企业版):
- `id, title, account_id, user_id, assistant_id`
- 关联:`belongs_to :user, :account, :assistant(Captain::Assistant)`
- `has_many :copilot_messages, dependent: :destroy_async`
- 涉及的业务逻辑:
- `previous_history` → 返回 thread 中所有 user + assistant 消息作为 LLM 对话历史
- `push_event_data` → ActionCable 实时推送数据
### CopilotMessage
- 功能描述:Copilot 线程中的单条消息,支持用户提问和 AI 回复。
- 涉及的数据模型:
- **CopilotMessage**(`copilot_messages` 表,企业版):
- `id, message(jsonb), message_type, account_id, copilot_thread_id`
- `message_type` enum:`user(0), assistant(1), assistant_thinking(2)`
- `message` jsonb → 存储 `{ content: "..." }` 结构
- 涉及的业务逻辑:
- `enqueue_response_job(conversation_id, user_id)` → `Captain::Copilot::ResponseJob.perform_later`
- `after_create_commit :broadcast_message` → ActionCable 推送给 user
- `validate :validate_message_attributes` → 校验 message 字段结构
- `ensure_account` → 从 copilot_thread 获取 account_id
---
## 12. Call 语音通话(企业版)
### Call 模型
- 功能描述:语音通话记录,与 Conversation + Message 关联。支持 Twilio 和 WhatsApp 通话。
- 涉及的数据模型:
- **Call**(`calls` 表,企业版):
- `id, direction, provider, status, duration_seconds, end_reason, transcript, provider_call_id`
- `account_id, inbox_id, conversation_id, contact_id, message_id, accepted_by_agent_id`
- `meta`(jsonb)→ `conference_sid, twilio_conference_sid, recording_sid, parent_call_sid, initiated_at, ended_at`
- `started_at`(datetime)
- `direction` enum:`incoming(0), outgoing(1)`
- `provider` enum:`twilio(0), whatsapp(1)`
- `status`:`ringing, in_progress, completed, no_answer, failed`(非 enum,string)
- `TERMINAL_STATUSES = %w[completed no_answer failed]`
- 涉及的业务逻辑:
- `Call.active` → 非终态通话
- `default_conference_sid` → `conf_account_{account_id}_call_{id}`
- `recording_url` → 通话录音下载链接(ActiveStorage)
- `has_one_attached :recording` → 通话录音文件
- `push_event_data` → 前端语音气泡展示数据
- `from_number/to_number` → 根据方向确定来电/去电号码
- STUN/ICE 服务器配置:`ENV['VOICE_CALL_STUN_URLS']` 或默认 Google STUN
- 消息中的 Call:
- Message `content_type = :voice_call` → 消息显示为语音通话气泡
- Message `content_attributes[:data]` → 存储通话元数据
- Call `belongs_to :message, optional: true`
---
## 13. 其他辅助功能
### Draft Message(草稿消息)
- 功能描述:坐席在对话中输入但未发送的内容保存为草稿,存储在 Redis 中。
- 涉及的API端点:
- `GET /api/v1/accounts/{account_id}/conversations/{display_id}/draft_messages` — 获取草稿
- `PATCH /api/v1/accounts/{account_id}/conversations/{display_id}/draft_messages` — 保存草稿
- `DELETE /api/v1/accounts/{account_id}/conversations/{display_id}/draft_messages` — 清除草稿
- 涉及的业务逻辑:
- Redis key:`Redis::Alfred::CONVERSATION_DRAFT_MESSAGE`(格式含 conversation.id)
- 返回 `{ has_draft: true/false, message: ... }`
### Typing Status(输入状态指示)
- 功能描述:坐席输入消息时向其他坐席和客户实时展示"正在输入"状态。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/toggle_typing_status`
- 参数:`{ typing_status: "on/off", is_private: bool }`
- 涉及的业务逻辑:
- **Conversations::TypingStatusManager**:
- `trigger_typing_event(CONVERSATION_TYPING_ON/OFF, is_private)`
→ Dispatcher 分发 → ActionCableListener 实时推送
### 坐席分配(Assignments)
- 功能描述:为对话分配坐席或 AgentBot,支持手动和自动分配。
- 涉及的API端点:
- `POST /api/v1/accounts/{account_id}/conversations/{display_id}/assignments`
- 参数:`{ assignee_id, assignee_type: "User" | "AgentBot", team_id }`
- 涉及的业务逻辑:
- **Conversations::AssignmentService**:
- `assignee_type == 'AgentBot'` → 分配 AgentBot(清空 assignee_id)
- 否则 → 分配 User(清空 assignee_agent_bot_id)
- **AssignmentHandler** concern:
- `before_save :ensure_assignee_is_from_team` → 团队变更时验证/重新分配
- `after_commit :notify_assignment_change` → 派发 ASSIGNEE_CHANGED / TEAM_CHANGED
- `after_commit :process_assignment_changes` → 创建 activity message
- **AutoAssignmentHandler** concern:
- `after_save :run_auto_assignment` → open 状态变化时触发自动分配
- V2 模式:`AutoAssignment::AssignmentJob.enqueue_for_inbox` 批量分配
- V1 模式:`AutoAssignment::AgentAssignmentService` 单次分配
### Message Mention(@提及)
- 涉及的文件:`app/services/messages/mention_service.rb`
- 功能描述:私有备注中通过 `@mention` 提及用户或团队,自动发送通知和添加为参与者。
- 提及格式:
- 用户:`(mention://user/{id}/{name})`
- 团队:`(mention://team/{id}/{name})`
- 业务流程:
1. MentionService 扫描消息 content 中的 mention 模式
2. 验证被提及用户是否属于该 Inbox
3. 团队 mention 展开为团队成员
4. 添加为 ConversationParticipant
5. 为每个被提及用户创建 Notification
### 附件列表视图
- 涉及的API端点:
- `GET /api/v1/accounts/{account_id}/conversations/{display_id}/attachments` — 对话的所有附件
- 参数:`{ page }`(分页,每页 100)
- 涉及的业务逻辑:
- 包含附件关联的消息和发送者信息
- 按 `created_at: :desc` 排序
---
## 附录:关键数据关系图
```
Account
└── Inbox ──────── Channel (各渠道实现)
└── Conversation ── Contact ── ContactInbox
│ ├── Assignee (User/AgentBot)
│ ├── Team
│ ├── Campaign (可选)
│ ├── SLAPolicy (可选)
│ ├── ConversationParticipant ── User
│ ├── Message ── Attachment ── ActiveStorage::file
│ │ ├── Sender (User/Contact/AgentBot, polymorphic)
│ │ ├── CsatSurveyResponse
│ │ └── Call (企业版, content_type=voice_call)
│ ├── CsatSurveyResponse
│ ├── Notification (as primary_actor)
│ └── ReportingEvent
CopilotThread (企业版) ── CopilotMessage
└── User, Account, Captain::Assistant
```
## 附录:核心事件流转图
```
IncomingMessageService各渠道
→ ContactInboxWithContactBuilder (查找/创建 Contact)
→ set_conversation (查找/创建 Conversation)
→ message.create!
→ Dispatcher: MESSAGE_CREATED
→ SyncDispatcher → ActionCableListener (实时推送)
→ AgentBotListener (Bot webhook)
→ AsyncDispatcher → AutomationRuleListener (自动化规则)
→ NotificationListener (通知)
→ ParticipationListener (参与者)
→ UnreadCounts::Listener (未读计数)
→ WebhookListener (Account webhook)
→ ReportingEventListener (报表)
坐席发送消息 (MessagesController#create)
→ Messages::MessageBuilder
→ message.save!
→ SendReplyJob → Base::SendOnChannelService → 各渠道 SendOn*Service
→ Dispatcher: MESSAGE_CREATED → (同上 Listener)
对话状态变更
→ Conversation#save
→ Dispatcher: CONVERSATION_STATUS_CHANGED / ASSIGNEE_CHANGED / TEAM_CHANGED
→ 各 Listener 处理
```