清理: - 删除 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 相关修改
41 KiB
41 KiB
M3 对话与消息 — Chatwoot 功能梳理文档
基于 Chatwoot 源码深度阅读 + CodeGraph 上下文梳理
生成日期:2026-05-22
参照仓库:chatwoot-reference
1. 对话 CRUD + 状态流转
对话创建
- 功能描述:对话(Conversation)是 Chatwoot 的核心实体,代表客户与团队之间的一次完整交互线程。创建时自动关联 Account、Inbox、Contact、ContactInbox。
- 用户操作流程:
- 客户通过渠道(WhatsApp/Facebook/Telegram 等)发消息,系统自动创建对话
- 坐席可通过 API 手动创建对话(
POST /api/v1/accounts/{account_id}/conversations),同时可附带第一条消息 - Campaign 发起时批量创建对话
- 对话创建后触发
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_untilaccount_id, inbox_id, contact_id, contact_inbox_id, assignee_id, assignee_agent_bot_id, team_id, campaign_id, sla_policy_idadditional_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
statusenum:open(0), resolved(1), pending(2), snoozed(3)priorityenum:low(0), medium(1), high(2), urgent(3)
- Conversation(
- 涉及的业务逻辑:
- ConversationBuilder(
app/builders/conversation_builder.rb):- 若 Inbox 设置
lock_to_single_conversation,查找该 ContactInbox 的最近对话复用 - 否则创建新对话(未解决时才建新对话的逻辑在 IncomingMessageService 中)
- 若 Inbox 设置
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
- ConversationBuilder(
- 涉及的自动化/规则/事件:
after_create_commit :notify_conversation_creation→ DispatcherCONVERSATION_CREATEDafter_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→ DispatcherCONVERSATION_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_sincenotify_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_RESOLVEDCONVERSATION_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_untilhandle_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
- 企业版 feature gate:
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 支持的参数:
- 排序选项(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分页排序
- ConversationFinder(
权限过滤
- 功能描述:根据用户角色限制可见的对话范围。
- 涉及的业务逻辑:
- 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 的标签能力
- Conversation
- 涉及的业务逻辑:
- 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(因标签影响分组计数)
- LabelConcern(
优先级管理
- 功能描述:对话可设置优先级(low/medium/high/urgent),用于排序和工作量管理。
- 涉及的API端点:
POST /api/v1/accounts/{account_id}/conversations/{display_id}/toggle_priority— 设置优先级- 请求参数:
{ priority }(low,medium,high,urgent)
- 涉及的数据模型:
priorityenum: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_typeconversation_id, account_id, inbox_id, source_idcontent_attributes(json),additional_attributes(jsonb),external_source_ids(jsonb)processed_message_content(text),sentiment(jsonb)
message_typeenum:incoming(0), outgoing(1), activity(2), template(3)content_typeenum: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)statusenum:sent(0), delivered(1), read(2), failed(3)content_attributesstored 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, dataexternal_source_idsstored accessors:slack(prefix:external_source_id_slack)
- Message(
- 涉及的业务逻辑:
- Messages::MessageBuilder(
app/builders/messages/message_builder.rb):- 构建 Message 对象(
conversation.messages.build(message_params)) - 处理附件(
process_attachments) - 处理邮件相关(
process_emails/process_email_content) - 保存后触发
after_create_commit回调链
- 构建 Message 对象(
- 消息创建后触发的事件:
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 消息时)
- Messages::MessageBuilder(
消息更新/删除
- 涉及的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
- Messages::StatusUpdateService:
- 消息重试逻辑:
- 状态重置为
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] })
- 默认最新 20 条(
- MessageFinder(
消息类型详解
| 类型 | 值 | 说明 |
|---|---|---|
| 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_titlecoordinates_lat, coordinates_long(地理位置)meta(jsonb)
file_typeenum: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
- Attachment(
- 涉及的业务逻辑:
- 文件类型白名单(
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 = 15before_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
- assignee → 更新
- 节流策略:无未读消息时每小时最多更新一次(减少 DB 写压力)
- 有未读消息时立即更新
- 同时调用
Notification::MarkConversationReadService清除该对话的通知 - 触发
CONVERSATION_READ事件(ActionCable 实时推送)
- 区分 assignee vs 非 assignee:
未读消息计数
- 功能描述:实时计算各维度(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
- Redis 缓存结构(
- 涉及的业务逻辑:
- 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)
- WhatsApp:通过 webhook status callback 更新消息
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单独处理
- 使用 Telegram Bot API
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)
- 使用 LINE Messaging API
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
- ConversationParticipant(
- 涉及的业务逻辑:
validate :ensure_inbox_access→ 参与者必须是该 Inbox 的 assignable_agentsbefore_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 会展开为所有团队成员
- ParticipationListener:
10. Dispatcher + Listener 事件系统
Dispatcher 事件分发
- 功能描述:Chatwoot 使用事件驱动架构,所有对话/消息的变更通过 Dispatcher 分发给各 Listener 处理。
- 涉及的文件:
app/dispatchers/dispatcher.rb— 单例入口app/dispatchers/sync_dispatcher.rb— 同步分发(立即执行)app/dispatchers/async_dispatcher.rb— 异步分发(通过 EventDispatcherJob)
- 事件分发流程:
- 业务代码调用
Dispatcher.dispatch(event_name, timestamp, data) - Dispatcher 同时调用 SyncDispatcher 和 AsyncDispatcher
- SyncDispatcher → 立即调用
publish(event.method_name, event_object) - 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 membersfirst_reply_created→ 推送给 inbox membersconversation_typing_on/off→ 推送给 inbox members
- AgentBotListener:通知 AgentBot webhook
conversation_resolved/opened/status_changed/updated→ 发送 webhook 给关联的 agent botsmessage_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 创建 notificationassignee_changed→ 为新 assignee 创建 notificationmessage_created→ 为 assignee 和 participants 创建 notification
- ParticipationListener:自动添加参与者(见 §9)
- Conversations::UnreadCounts::Listener:未读计数增量刷新(见 §6)
- ReportingEventListener:报表数据采集
- WebhookListener:Account 级 webhook 发送
conversation_status_changed/updated/created→ 发送 webhook payloadmessage_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
- CopilotThread(
- 涉及的业务逻辑:
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_typeenum:user(0), assistant(1), assistant_thinking(2)messagejsonb → 存储{ content: "..." }结构
- CopilotMessage(
- 涉及的业务逻辑:
enqueue_response_job(conversation_id, user_id)→Captain::Copilot::ResponseJob.perform_laterafter_create_commit :broadcast_message→ ActionCable 推送给 uservalidate :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_idaccount_id, inbox_id, conversation_id, contact_id, message_id, accepted_by_agent_idmeta(jsonb)→conference_sid, twilio_conference_sid, recording_sid, parent_call_sid, initiated_at, ended_atstarted_at(datetime)
directionenum:incoming(0), outgoing(1)providerenum:twilio(0), whatsapp(1)status:ringing, in_progress, completed, no_answer, failed(非 enum,string)TERMINAL_STATUSES = %w[completed no_answer failed]
- Call(
- 涉及的业务逻辑:
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
- Message
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: ... }
- Redis key:
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 实时推送
- Conversations::TypingStatusManager:
坐席分配(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_CHANGEDafter_commit :process_assignment_changes→ 创建 activity message
- AutoAssignmentHandler concern:
after_save :run_auto_assignment→ open 状态变化时触发自动分配- V2 模式:
AutoAssignment::AssignmentJob.enqueue_for_inbox批量分配 - V1 模式:
AutoAssignment::AgentAssignmentService单次分配
- Conversations::AssignmentService:
Message Mention(@提及)
- 涉及的文件:
app/services/messages/mention_service.rb - 功能描述:私有备注中通过
@mention提及用户或团队,自动发送通知和添加为参与者。 - 提及格式:
- 用户:
(mention://user/{id}/{name}) - 团队:
(mention://team/{id}/{name})
- 用户:
- 业务流程:
- MentionService 扫描消息 content 中的 mention 模式
- 验证被提及用户是否属于该 Inbox
- 团队 mention 展开为团队成员
- 添加为 ConversationParticipant
- 为每个被提及用户创建 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 处理