Files
gochat/docs/requirements/M03-conversation-and-message.md
T
rogee 0dabb8cfa5 docs: 整理文档目录结构 — 清理过时文档、归集功能子目录、统一命名规范
清理:
- 删除 34 份过时文档(gap reports/QA临时报告/验收报告/阶段性文档)
- 删除 docs/.hermes/skills 第三方 skills 副本(16 文件)
- 删除 skills-lock.json

目录归集:
- 根目录仅保留 README.md 索引
- product/ — 产品与架构设计(PRD + ARCHITECTURE + P2设计文档 + AI/企业路线图)
- tracking/ — Chatwoot parity 开发跟踪
- requirements/ — M01-M12 模块需求
- plans/ — 历史实现计划
- parity/ — 路由 parity 与前端契约
- qa/ — QA 报告与测试计划
- ops/ — 运维部署

命名规范:
- 全小写 kebab-case,禁止全大写文件名
- product/tracking/ops 用 NN- 序号前缀
- requirements 用 MNN- 两位零填充模块号
- plans/qa 用 YYYY-MM-DD- 日期前缀
- requirements M1-M9 零填充为 M01-M09(修复字典序)

同步更新:
- backend/cmd/route_parity/main.go 路径默认值
- backend/scripts/parity_frontend_smoke.sh 报告路径
- 所有 docs 内部交叉引用
- .gitignore 排除编译产物 (backend/gochat, backend/route_parity)
- 新增迁移 000052/000053
- 前端 WS 相关修改
2026-07-09 14:53:27 +08:00

41 KiB
Raw Blame History

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} — 获取对话详情
  • 涉及的数据模型:
    • Conversationconversations 表)核心字段:
      • id, display_idaccount 级自增编号), 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_attributesjsonb, custom_attributesjsonb, cached_label_listtext
      • agent_last_seen_at, assignee_last_seen_at, contact_last_seen_at, waiting_since, last_activity_at, first_reply_created_at
    • status enumopen(0), resolved(1), pending(2), snoozed(3)
    • priority enumlow(0), medium(1), high(2), urgent(3)
  • 涉及的业务逻辑:
    • ConversationBuilderapp/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 }(可选,不传则 toggleopen ↔ resolved
  • 状态流转规则:
    • open → resolved:坐席/自动化解决对话
    • resolved → open:客户新消息 / 坐席重新打开
    • pending → openBot handoffbot_handoff! 方法)
    • open/snoozed → snoozed:设置 snoozed_until 时间
    • snoozed → open:当 snoozed_until 到期或手动取消
    • toggle_status:若无参数则 open ↔ resolved(若 pending/snoozed 则 → open
    • AgentBot 可执行 pending → openbot 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-resolveAccount 设置 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_deletionCONVERSATION_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 — 取消静音
  • 涉及的业务逻辑:
    • ConversationMuteHelpersmute! → resolved! + contact.update(blocked: true) + 创建 activity messageunmute! → contact.update(blocked: false) + 创建 activity message

对话邮件转录

  • 涉及的API端点:
    • POST /api/v1/accounts/{account_id}/conversations/{display_id}/transcript — 发送对话转录邮件
    • 请求参数:{ email }
  • 涉及的业务逻辑:
    • 企业版 feature gateaccount.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_typeme(我的), unassigned(未分配), all(全部)
      • statusopen, 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 — 先按优先级降序再按创建时间升序(复合排序)
  • 涉及的业务逻辑:
    • ConversationFinderapp/finders/conversation_finder.rb):
      • 权限过滤:非 administrator 只能看自己 accessible inboxes 的对话
      • 分步处理:set_upfind_all_conversationsfilter_by_statusfilter_by_teamfilter_by_labelsfilter_by_queryfilter_by_assignee_type
      • 返回 { conversations: [...], count: { mine_count, assigned_count, unassigned_count, all_count } }
    • Conversations::FilterServiceapp/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::PermissionFilterServiceadministrator 可看全部,其他角色仅能看自己所属 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_listtext)— 缓存标签列表,逗号分隔
    • Conversation include Labelable — 使用 ActsAsTaggableOn 的标签能力
  • 涉及的业务逻辑:
    • LabelConcernapp/controllers/concerns/label_concern.rb):
      • createmodel.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 enumlow(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
        }
      }
      
  • 涉及的数据模型:
    • Messagemessages 表)核心字段:
      • id, content, content_type, message_type, private, status, sender_id, sender_type
      • conversation_id, account_id, inbox_id, source_id
      • content_attributesjson, additional_attributesjsonb, external_source_idsjsonb
      • processed_message_contenttext, sentimentjsonb
    • message_type enumincoming(0), outgoing(1), activity(2), template(3)
    • content_type enumtext(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 enumsent(0), delivered(1), read(2), failed(3)
    • content_attributes stored accessorssubmitted_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 accessorsslackprefix: external_source_id_slack
  • 涉及的业务逻辑:
    • Messages::MessageBuilderapp/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_sinceincoming 消息时)
      • 更新 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::StatusUpdateServicesent → 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
  • 涉及的业务逻辑:
    • MessageFinderapp/finders/message_finder.rb):
      • 默认最新 20 条(messages_latest
      • before 参数:加载更早的消息(ID < before20 条)
      • after 参数:加载更新的消息(ID > after,最多 100 条)
      • before + after:加载区间内消息(最多 1000 条)
      • filter_internal_messages:过滤掉 private 和 activity 消息
      • eager loadincludes(: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[] 参数附带
  • 涉及的数据模型:
    • Attachmentattachments 表)核心字段:
      • id, message_id, account_id, file_type, extension, external_url, fallback_title
      • coordinates_lat, coordinates_long(地理位置)
      • metajsonb
    • file_type enumimage(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 flagconversation_unread_counts
  • 涉及的数据模型/缓存:
    • Redis 缓存结构(Conversations::UnreadCounts::Store):
      • 按 Inbox + Label + Team 维度存储未读对话 membership
      • base cache:所有 open 对话的 membershipinbox_id, label_ids, team_id
      • assignment cache:加上 assignee_id 维度(支持 mine_count 精确计算)
    • TTLbase 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_message24h 窗口内)
    • 消息窗口检测:Conversations::MessageWindowServiceWhatsApp 24h

Facebook SendOnFacebookService

  • 涉及的文件:app/services/facebook/send_on_facebook_service.rb
  • 特殊逻辑:
    • 文本消息和附件消息分开发送(FB API 限制)
    • 使用 Facebook::Messenger::Bot.deliver 发送
    • 错误处理:FacebookErrorStatusUpdateService('failed')
    • 支持 messaging_type/tagHUMAN_AGENT 等)

Instagram SendOnInstagramService

  • 涉及的文件:app/services/instagram/send_on_instagram_service.rb(继承 Instagram::BaseSendService
  • 特殊逻辑:
    • 使用 Instagram Messaging API v22.0
    • 支持 HUMAN_AGENT tag24h 窗口外)
    • 支持 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
    • SMSchannel.send_message(**message_params)
    • 错误处理:Twilio::REST::TwilioError/RestErrorStatusUpdateService('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
  • 功能描述:判断当前对话是否在渠道允许的回复窗口内,影响是否只能发模板消息。
  • 各渠道窗口规则:
    • WhatsApp24 小时窗口
    • Facebook/Messenger:根据 ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT 配置(7 天或无限制)
    • Instagram:根据 ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT 配置
    • TikTok7 天窗口
    • Twilio WhatsApp24 小时窗口
    • 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: [...] }
  • 涉及的数据模型:
    • ConversationParticipantconversation_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 → 匹配消息触发规则
  • CampaignListenerCampaign 相关事件处理
  • CsatSurveyListenerCSAT 满意度调查触发
  • 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:报表数据采集
  • WebhookListenerAccount 级 webhook 发送
    • conversation_status_changed/updated/created → 发送 webhook payload
    • message_created/updated → 仅 webhook_sendable? 的消息发送

11. Copilot 消息(企业版)

CopilotThread

  • 功能描述:Copilot(AI 助手)对话线程,用于坐席与 AI 之间的内部交互。与客户对话独立。
  • 涉及的数据模型:
    • CopilotThreadcopilot_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 回复。
  • 涉及的数据模型:
    • CopilotMessagecopilot_messages 表,企业版):
      • id, message(jsonb), message_type, account_id, copilot_thread_id
    • message_type enumuser(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 通话。
  • 涉及的数据模型:
    • Callcalls 表,企业版):
      • 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
      • metajsonb)→ conference_sid, twilio_conference_sid, recording_sid, parent_call_sid, initiated_at, ended_at
      • started_atdatetime
    • direction enumincoming(0), outgoing(1)
    • provider enumtwilio(0), whatsapp(1)
    • statusringing, in_progress, completed, no_answer, failed(非 enumstring
    • TERMINAL_STATUSES = %w[completed no_answer failed]
  • 涉及的业务逻辑:
    • Call.active → 非终态通话
    • default_conference_sidconf_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 keyRedis::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 处理