Files
gochat/docs/architecture/message-lifecycle-and-ai-flow.md
T
2026-08-24 00:14:21 +08:00

564 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GoChat 消息生命周期与 AI 自动回复流程
> 本文档描述从渠道消息入站、人工/AI 回复、消息打标签到 AI 自动触发的完整运行流程。
> 所有文件路径相对于仓库根目录,行号截至 2026-07-30。
---
## 一、整体架构概览
```
┌─────────────────────────────────────────────────────────────────────┐
│ 外部渠道 │
│ Facebook │ Instagram │ TikTok │ Telegram │ LINE │ Email │ Web Widget │ API
└─────┬──────────┬──────────┬──────────┬─────────┬───────┬──────┬──────┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Webhook 路由层 (router.go) │
│ /webhooks/facebook /webhooks/instagram /webhooks/tiktok ... │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Channel Provider 层 (插件接口) │
│ FacebookProvider InstagramProvider TikTokProvider TelegramProvider│
│ IncomingMessage() 解析渠道原始 payload → channel.IncomingMessage │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ IncomingPersister (持久化层) │
│ 1. resolveOrCreateContactInbox — 查找/创建 Contact + ContactInbox │
│ 2. resolveOrCreateConversation — 查找/创建 Conversation │
│ 3. createMessage — 持久化 Message │
│ 4. dispatchIncomingEvents — 广播 channel events │
└──────────────────────────────┬──────────────────────────────────────┘
│ DispatchAsync
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Dispatcher (事件分发总线) │
│ channel.Dispatcher → 广播 ChannelEvent 给所有注册的 EventListener │
│ │
│ 注册的 Listener: │
│ ├── AutoReplyListener — AI 自动回复 │
│ ├── AutomationRuleListener— 自动化规则(标签/分配/状态变更) │
│ ├── ActionCableListener — WebSocket 推送到前端 │
│ ├── NotificationListener — 创建通知 │
│ ├── AgentBotRuleListener — Agent Bot 规则 │
│ └── AutoAssignmentListener— 自动分配 │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 二、入站消息流(Inbound)
### 2.1 Webhook 路由注册
路由在 `backend/internal/router/router.go` 中注册:
- **Facebook**: `POST /webhooks/facebook` → `FacebookWebhookHandler.HandleWebhook`
- **Instagram**: `POST /webhooks/instagram` → `InstagramWebhookHandler`
- **TikTok**: `POST /webhooks/tiktok` → `TikTokWebhookHandler`
- **Telegram**: `POST /webhooks/telegram/:inbox_id` → `TelegramWebhookHandler`
- **LINE**: `POST /webhooks/line/:inbox_id` → `LineWebhookHandler`
- **Email**: IMAP 轮询 + ActionMailbox 端点
- **Web Widget**: WebSocket 实时消息(不走 webhook)
- **API Channel**: `POST /webhooks/api/:inbox_id` → 外部系统主动推送
### 2.2 Webhook Handler → Provider → IncomingMessage
以 Facebook 为例:
```
FacebookWebhookHandler.HandleWebhook (handler/webhook/facebook_webhook.go)
→ 解析 Facebook webhook payload (entry[].messaging[])
→ 遍历每条 messaging event
→ 调用 FacebookProvider.IncomingMessage() (channel/facebook/provider.go)
→ 解析 sender_id, recipient_id, message text, attachments
→ 返回 channel.IncomingMessage{
SourceID: "facebook_msg_xxx",
ConversationID: senderPSID,
Content: messageText,
ContentType: channel.ContentText,
Attachments: []Attachment{...},
ChannelType: channel.ChannelFacebook,
}
```
其他渠道同理,每个 Provider 实现 `IncomingMessage()` 方法将渠道原始数据归一化为 `channel.IncomingMessage` 结构。
### 2.3 IncomingPersister 持久化
`backend/internal/handler/webhook/incoming_persister.go:74`
```
IncomingPersister.PersistIncoming(ctx, inbox, msg)
│
├── 若有 WorkerPool → enqueueIncomingMessagePersist (异步队列)
│ └── providerIncomingMessagePersistJob (incoming_persister_jobs.go:24)
│ └── performPersistIncoming (异步执行)
│
└── performPersistIncoming(ctx, inbox, msg) — 同步路径
│
├── 1. resolveOrCreateContactInbox (L451)
│ ├── 按 inbox_id + source_id 查找 ContactInbox
│ ├── 找到 → 返回关联的 Contact(更新 name 等)
│ └── 未找到 → 创建 Contact + ContactInbox
│
├── 2. resolveOrCreateConversation (L504)
│ ├── 查找 account_id+inbox_id+contact_id+status=open 的最近会话
│ ├── 找到 → 更新 last_activity_at, last_message_at
│ └── 未找到 → 创建新 Conversation (status=open)
│
├── 3. createMessage (L545)
│ ├── 构建 model.Message{SenderType:"Contact", MessageType:"incoming"}
│ ├── 处理 in_reply_to(按 source_id 查找被回复的消息)
│ ├── 处理 attachments
│ └── messageRepo.Create() 持久化到 messages 表
│
└── 4. dispatchIncomingEvents (L383)
├── EventContactCreated (仅新联系人)
├── EventConversationCreated + EventConversationOpened (仅新会话)
├── EventConversationUpdated (已有会话)
├── EventMessageCreated
└── EventMessageIncoming
└── dispatcher.DispatchAsync() → 广播给所有 Listener
```
### 2.4 WebSocket 实时推送
```
ActionCableListener.OnEvent (internal/wsevent/bridge_listener.go:51)
├── 接收 EventMessageCreated / EventMessageIncoming
├── 构建 WebSocket payload (message JSON + conversation meta)
└── wsHub.Broadcast() → 推送到该 account 所有在线 agent 前端
└── 前端 Vue SPA WebSocket 连接接收 → Vuex/Pinia store 更新 → UI 刷新
```
#### Durable realtime 投递语义
配置 WorkerPool 后,`EventPublisher` 先把 account 与 pubsub token 目标分别写入
`background_jobs`。在当前 Redis relay 配置下,account target job 发布到 account
room,由 `BroadcastRelay` 转发到 Hub,并由执行 job 的实例写 account SSE;pubsub
token target job 只发布到 token room,经 relay 转发到 Hub,不写 SSE。并非每个
job 都同时直发 Redis、Hub 和 SSE。
幂等键避免同一条 `message.created`/目标重复入队,job claim 避免并发执行同一条
记录;它们不覆盖发布副作用与 job 完成状态之间的崩溃窗口:
1. job 已完成 Redis 发布,并可能已完成本地 SSE 投递;
2. 进程在将 `background_jobs.status` 更新为 `completed` 前退出;
3. stale-job 恢复将该记录重新置为可执行并再次发布。
因此,对外契约是 **durable at-least-once publication**,不是 exactly-once
delivery。进程恢复后,在线 WebSocket/SSE 消费者可能再次收到同一事件;account
与 pubsub token 是独立 job,重试和到达顺序也彼此独立。Redis Pub/Sub、Hub 和
SSE 不保存离线或慢消费者的确认状态,所以该契约保证发布尝试可恢复,不保证每个
客户端至少接收一次。
该保证从 job 成功写入 `background_jobs` 开始。事务内入队失败会使业务事务回滚;
非事务入队失败则没有 durable job,因而不保证该事件会被发布。每个 target job 的
`MaxAttempts=3`;达到上限(或遇到 permanent error)进入 `dead` 后不会再被自动
重试,也不再保证发布,消费者仍需通过 REST reconciliation 恢复持久化状态。
消费者必须把 realtime 事件作为可重放通知处理:
- `message.created` 使用订阅作用域、事件名和 payload 的稳定 message `id` 去重或
upsert;不得使用未出现在 wire payload 中的 background job ID。
- `*.updated` 按资源 `id` 幂等 upsert,并在 payload 提供 `updated_at`/版本时拒绝
旧更新;不要仅按资源 ID 永久丢弃后续合法更新。
- 重连或发现序列缺口时,以 REST API 返回的持久化资源为准。会触发声音、通知、
SDK callback 或其他非幂等副作用的外部消费者必须在执行副作用前自行去重。
当前风险可接受:复用的 dashboard 会按 message ID 替换重复消息,并按
conversation `updated_at` 忽略旧更新;widget 消息仓库也按 message ID upsert。
这与上游 Chatwoot 的异步 `ActionCableBroadcastJob` 行为一致。只有当业务要求
跨进程崩溃的 exactly-once 副作用时,才应另行引入稳定 transport event ID 与
消费端 inbox/ack;当前协议不承诺该能力。
### 2.5 通知创建
```
NotificationListener.OnEvent
├── 接收 EventMessageIncoming
├── 检查 inbox 的通知设置
├── 创建 Notification 记录 (notification 表)
├── 推送 Push notification (PushDeliveryService)
│ └── SendPushNotification (push_delivery_service.go:63)
└── 发送邮件通知(如配置)
```
---
## 三、出站回复流(Outbound)
### 3.1 前端发起
```
Agent 在 ReplyBox 输入消息 → 点击发送
│
├── frontend/app/javascript/dashboard/api/inbox/message.js:56
│ MessageApi.create({ conversationId, message, ... })
│ → POST /api/v1/accounts/:id/conversations/:conversation_id/messages
│
└── 前端 Vuex store: conversations/actions.js
createPendingMessageAndSend → MessageApi.create()
```
### 3.2 后端处理
```
MessageHandler.Create (handler/api/v1/message_handler.go)
→ 解析请求 body (content, private, files, echo_id, ...)
→ 调用 MessageService.Create()
│
│ backend/internal/service/message_service.go
│
├── 构建 model.Message{MessageType:"outgoing", SenderType:"User"}
├── messageRepo.Create() — 持久化消息
├── 更新 conversation.last_activity_at, last_message_at
├── dispatchMessageEvent(EventMessageCreated + EventMessageOutgoing)
│ └── dispatcher.Dispatch() → 广播给 Listener
│ ├── ActionCableListener → WebSocket 推送 (agent 端即时看到)
│ ├── AutomationRuleListener → 触发自动化规则
│ └── 其他 Listener
│
└── 投递到渠道 (MessageDeliveryWorker)
│
├── 若有 WorkerPool → EnqueueSendReply (异步队列)
│ └── message_delivery_worker.go: CreateOutgoing → SendReply
│
└── SendReply(messageID)
├── 加载 message + conversation + inbox + contact
├── 根据 inbox.ChannelType 获取对应 Provider
├── 调用 Provider.SendMessage(ctx, inbox, message, contact)
│ ├── Facebook: POST graph.facebook.com/me/messages
│ ├── Instagram: POST graph.facebook.com/me/messages (IGID)
│ ├── TikTok: POST business-api.tiktok.com/...
│ ├── Telegram: POST api.telegram.org/bot.../sendMessage
│ ├── Email: SMTP 发送
│ ├── Web Widget: WebSocket 推送到 widget 端
│ └── API: POST 到 inbox.webhook_url
├── 更新 message.Status = "sent" / "delivered" / "failed"
└── dispatchMessageEvent(EventMessageStatusUpdated)
```
### 3.3 消息状态回执
渠道 webhook 回调消息已读/已送达状态:
```
FacebookWebhookHandler → 解析 delivery/read event
→ IncomingPersister.dispatchMessageStatusEvent()
→ 更新 message.Status
→ dispatcher.Dispatch(EventMessageStatusUpdated)
→ ActionCableListener → WebSocket 推送状态更新到前端
```
---
## 四、标签系统
### 4.1 数据模型(双路径并存)
GoChat 标签系统存在两条并行路径:
**路径 A — Tag 关联表(结构化)**
- **Tag** (`backend/internal/model/tag.go:12`): 表名 `tags`,字段 `ID, AccountID, Name(账号内唯一), Color, Description, ShowOnSidebar`
- 唯一索引:`idx_tag_account_name`(AccountID + Name)
- **ConversationLabel** (`backend/internal/model/conversation_label.go:11`): 表名 `conversation_labels`,字段 `ID, ConversationID, TagID, AccountID`
- 唯一索引:`idx_conv_label_tag`(ConversationID + TagID),防止重复关联
- Repository: `TagRepo` (`repository/tag_repo.go`) — `DeleteWithAssociations` 删除 Tag 时同步清理关联行和 `conversations.labels` 文本字段
**路径 B — Conversation.Labels 文本字段(遗留)**
- `Conversation.Labels string` (`model/conversation.go:30`) — 逗号分隔字符串,如 `"billing,urgent"`
- 辅助函数: `mergeConversationLabels()` / `splitConversationLabels()` (`conversation_maintenance_worker.go:726,755`)
- `TagRepo.RenameConversationLabelText` (`repository/tag_repo.go:70`) — Tag 重命名时同步更新 `conversations.labels` 中的文本
两条路径在自动化规则和手动操作中同时维护。
### 4.2 标签 CRUD API
```
LabelHandler (handler/api/v1/label_handler.go)
├── GET /api/v1/accounts/:id/labels — 列出标签
├── POST /api/v1/accounts/:id/labels — 创建标签
├── PATCH /api/v1/accounts/:id/labels/:id — 更新标签
└── DELETE /api/v1/accounts/:id/labels/:id — 删除标签
│
└── LabelService (service/label_service.go)
└── LabelRepo (repository/label_repo.go)
```
### 4.3 会话标签操作
```
ConversationHandler (handler/api/v1/conversation_handler.go)
├── POST /conversations/:id/labels — 给会话打标签
└── POST /conversations/:id/labels — 移除标签
│
└── ConversationService.ToggleLabels()
├── 更新 conversation.labels 字段 (mergeConversationLabels)
├── dispatchConversationEventWithData(EventConversationLabelsUpdated)
└── ActionCableListener → WebSocket 推送标签变更到前端
```
### 4.4 自动化规则中的标签
```
AutomationRule (model/automation_rule.go)
└── AutomationAction.ActionType = "add_label"
└── ActionParams = {"labels": ["billing", "urgent"]}
AutomationRuleListener.OnEvent (automation/listener.go)
├── 接收 EventMessageCreated / EventConversationCreated
├── ConditionsFilterService 检查规则条件是否匹配
├── 匹配 → ActionService 执行动作
│ └── add_label → 更新 conversation.labels
└── dispatch EventConversationLabelsUpdated
```
### 4.5 前端标签管理
```
LabelBox.vue (dashboard/routes/dashboard/conversation/labels/LabelBox.vue)
├── 显示当前会话所有标签
├── 添加/移除标签 → API 调用
└── useConversationLabels composable
```
---
## 五、AI 自动回复
### 5.1 架构概览
```
入站消息 EventMessageCreated
│
▼
┌────────────────────────────────┐
│ AutoReplyListener │
│ (service/auto_reply_listener.go)│
└───────────────┬────────────────┘
│
┌─────────▼──────────┐
│ EvaluateRules() │
│ (auto_reply_rule │
│ _service.go) │
└─────────┬──────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
Static Mode LLM Mode Mixed Mode
(固定文本回复) (LLM 生成回复) (LLM + 固定文本)
│ │ │
│ ┌──────▼──────┐ │
│ │ LLM Provider │ │
│ │ (RAG 知识库) │ │
│ └──────┬──────┘ │
│ │ │
└───────────────┼───────────────┘
▼
MessageService.Create()
(outgoing message)
│
▼
MessageDeliveryWorker
→ 渠道发送
```
### 5.2 AutoReplyListener 触发链路
`backend/internal/service/auto_reply_listener.go`
```
AutoReplyListener.OnEvent(ctx, event) (L62)
│
├── 过滤:仅处理 EventMessageCreated / EventMessageIncoming
├── 过滤:仅处理 sender_type = "Contact" 的入站消息
├── 加载 Conversation(检查是否已 resolved → 跳过)
├── fetchRecentMessages — 取最近 10 条消息作为上下文 (L205)
│
├── 构建 AutoReplyEvaluationContext {
│ AccountID, InboxID, ConversationID,
│ MessageContent, PreviousMessages,
│ ConversationStatus, Language
│ }
│
├── AutoReplyRuleService.EvaluateRules(ctx, evalCtx) (auto_reply_rule_service.go)
│ ├── 按 inbox_id 查找 active 状态的 auto-reply rules
│ ├── 逐条评估 conditions(关键词匹配 / 正则 / 消息长度 / 时间窗口)
│ ├── 按 priority 排序,返回第一个匹配的 rule
│ └── 根据 rule.Mode 生成回复内容:
│ ├── Static: 直接返回 rule.ResponseText
│ ├── LLM: 调用 LLM Provider 生成回复(可带 RAG 检索)
│ └── Mixed: LLM 生成 + 固定文本拼接
│
├── 检查 OneTimeOnly — 该规则是否已对此会话触发过 (L225)
│
├── 若有 DelaySeconds → 异步 goroutine 延迟发送 (L137)
│
└── sendAutoReply(ctx, event, conversation, result) (L151)
├── 解析 inbox 关联的 AgentBot 作为 sender
├── 构建 CreateMessageRequest{
│ Content: replyContent,
│ MessageType: "outgoing",
│ SenderType: "agent_bot",
│ }
└── MessageService.Create() → 消息持久化 + 事件分发
└── → MessageDeliveryWorker → 渠道发送
```
### 5.3 Auto-Reply Rule 数据模型
`backend/internal/model/captain_auto_reply_rule.go`
```
CaptainAutoReplyRule {
AccountID
AssistantID — 关联 Captain Assistant
InboxID — 绑定到特定 inbox
Name, Description
Status — draft / active / archived
Mode — static / llm / mixed
Priority — 数字越小优先级越高
Conditions — JSON: 关键词/正则/消息属性条件
ResponseText — static/mixed 模式的固定回复文本
LLMPromptOverride — LLM 模式的自定义 prompt
DelaySeconds — 延迟发送秒数
OneTimeOnly — 是否只触发一次
}
```
### 5.4 Captain Assistant
Captain 是 GoChat 的 AI 助手框架,提供 LLM 能力:
```
CaptainAssistant (model/captain_assistant.go)
├── 关联一个 LLM Provider (OpenAI / DeepSeek / 自定义)
├── 关联 RAG 文档库 (CaptainDocument)
├── 关联 Scenario(场景化 prompt 模板)
└── 关联 Auto-Reply Rules
CaptainAssistantHandler (handler/api/v1/captain_assistant_handler.go)
├── CRUD assistant
├── 测试 assistant (发送测试消息)
└── 生成回复 (直接调用 LLM)
CaptainConversationHandler (handler/api/v1/captain_conversation_handler.go)
├── AI 参与会话 — Captain 读取会话上下文 + RAG 检索 → 生成建议回复
└── 也可直接发送 AI 回复到会话
```
### 5.5 Copilot(Agent 辅助)
Copilot 是给人工 agent 用的 AI 辅助工具,不会自动发送回复:
```
CopilotHandler (handler/api/v1/copilot_handler.go)
├── POST /copilot/suggest — 根据会话上下文生成建议回复
├── POST /copilot/rephrase — 改写 agent 输入的文本
└── POST /copilot/summarize — 总结会话
CopilotContainer.vue (前端侧边栏)
├── Agent 在侧边栏与 Copilot 交互
├── Copilot 调用 LLM + RAG 生成建议
└── Agent 确认后手动发送(不自动发送)
```
### 5.6 RAG 知识检索
```
RAGHandler (handler/api/v1/rag_handler.go)
├── 上传文档 → 向量化 → 存入 pgvector
├── 检索:query 向量化 → pgvector 相似度搜索 → 返回 top-k 文档片段
└── 在 LLM 调用时注入检索到的上下文
流程:
用户消息 → 向量化 → pgvector 检索相关文档 → 注入 LLM prompt → 生成回复
```
---
## 六、完整消息生命周期
```
客户 GoChat 后端 Agent 前端
│ │ │
│ 1. 发送消息 │ │
│ ──────────────────────► │ │
│ 2. Webhook 接收 │
│ 3. Provider 解析 │
│ 4. IncomingPersister 持久化 │
│ 5. Dispatcher 广播事件 │
│ │ 6. WebSocket 推送 │
│ │ ────────────────────────────► │
│ │ 7. Agent 看到消息 │
│ │ │
│ 8. AutoReplyListener 检查 │
│ 9. 匹配规则 → AI 生成回复 │
│ 10. MessageService.Create() │
│ 11. MessageDeliveryWorker │
│ 12. 渠道发送回复 │ │
│ ◄────────────────────── │ │
│ │ 13. WebSocket 推送 AI 回复 │
│ │ ────────────────────────────► │
│ │ 14. Agent 看到 AI 回复 │
│ │ │
│ │ 15. (可选) Agent 手动回复 │
│ │ ◄──────────────────────────── │
│ 16. MessageService.Create() │
│ 17. Provider.SendMessage() │
│ 18. 渠道发送回复 │ │
│ ◄────────────────────── │ │
│ │ │
│ │ 19. AutomationRuleListener │
│ │ → 匹配规则 → 自动打标签 │
│ │ → 自动分配 agent │
│ │ → 自动变更状态 │
```
---
## 七、关键文件索引
| 组件 | 文件路径 | 关键函数/行号 |
|---|---|---|
| Webhook 路由 | `backend/internal/router/router.go` | webhook 路由注册 |
| Facebook Webhook | `backend/internal/handler/webhook/facebook_webhook.go` | `HandleWebhook` |
| TikTok Webhook | `backend/internal/handler/webhook/tiktok_webhook.go` | `HandleWebhook` |
| LINE Webhook | `backend/internal/handler/webhook/line_webhook.go` | `HandleWebhook` |
| IncomingPersister | `backend/internal/handler/webhook/incoming_persister.go` | `PersistIncoming` L74 |
| 持久化 Job | `backend/internal/handler/webhook/incoming_persister_jobs.go` | `providerIncomingMessagePersistJob` L24 |
| Dispatcher | `backend/internal/channel/dispatcher.go` | `Dispatch` L85, `DispatchAsync` L110 |
| Channel Event | `backend/internal/channel/event.go` | `ChannelEvent` 结构体, EventType 常量 |
| Facebook Provider | `backend/internal/channel/facebook/provider.go` | `IncomingMessage`, `SendMessage` L352 |
| Instagram Provider | `backend/internal/channel/facebook/instagram_provider.go` | `IncomingMessage`, `SendMessage` L364 |
| Message Service | `backend/internal/service/message_service.go` | `Create`, `dispatchMessageEvent` L68 |
| Message Handler | `backend/internal/handler/api/v1/message_handler.go` | `Create` |
| Message Delivery Worker | `backend/internal/service/message_delivery_worker.go` | `EnqueueSendReply`, `SendReply` |
| AutoReply Listener | `backend/internal/service/auto_reply_listener.go` | `OnEvent` L62, `sendAutoReply` L151 |
| AutoReply Rule Service | `backend/internal/service/auto_reply_rule_service.go` | `EvaluateRules` |
| AutoReply Rule Model | `backend/internal/model/captain_auto_reply_rule.go` | `CaptainAutoReplyRule` |
| Captain Assistant Handler | `backend/internal/handler/api/v1/captain_assistant_handler.go` | CRUD + 测试 |
| Captain Conversation Handler | `backend/internal/handler/api/v1/captain_conversation_handler.go` | AI 会话参与 |
| Copilot Handler | `backend/internal/handler/api/v1/copilot_handler.go` | suggest/rephrase/summarize |
| RAG Handler | `backend/internal/handler/api/v1/rag_handler.go` | 文档上传/检索 |
| Automation Rule Listener | `backend/internal/automation/listener.go` | `OnEvent` |
| Automation Action | `backend/internal/model/automation_action.go` | `AutomationAction` L10 |
| Tag Model | `backend/internal/model/tag.go` | `Tag` L12 |
| ConversationLabel Model | `backend/internal/model/conversation_label.go` | `ConversationLabel` L11 |
| Tag Repo | `backend/internal/repository/tag_repo.go` | `DeleteWithAssociations` L54, `RenameConversationLabelText` L70 |
| ConversationLabel Repo | `backend/internal/repository/conversation_label_repo.go` | CRUD |
| WebSocket Hub | `backend/internal/ws/hub.go` | `Broadcast` |
| WS Event Bridge | `backend/internal/wsevent/bridge_listener.go` | `ActionCableListener.OnEvent` L51 |
| Inbox Serializer | `backend/internal/handler/api/v1/inbox_serializer.go` | `serializeInbox` L21 |
| Notification Service | `backend/internal/service/notification_delivery_service.go` | 通知投递 |
| Push Delivery | `backend/internal/service/push_delivery_service.go` | `SendPushNotification` L63 |
| 前端 Message API | `frontend/app/javascript/dashboard/api/inbox/message.js` | `create` L56 |
| 前端 ReplyBox | `frontend/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue` | 消息输入框 |
| 前端 Copilot | `frontend/app/javascript/dashboard/components/copilot/CopilotContainer.vue` | Copilot 侧边栏 |