# 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 处理 ```