776 lines
41 KiB
Markdown
776 lines
41 KiB
Markdown
# 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 处理
|
||
``` |