From 8fed449b9545af4b3a063910efd7672908a007ba Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 9 Jul 2026 15:43:15 +0800 Subject: [PATCH] add plan --- ...-09-brainstorming-fake-message-platform.md | 782 ++++++++++++++++++ 1 file changed, 782 insertions(+) create mode 100644 docs/plans/2026-07-09-brainstorming-fake-message-platform.md diff --git a/docs/plans/2026-07-09-brainstorming-fake-message-platform.md b/docs/plans/2026-07-09-brainstorming-fake-message-platform.md new file mode 100644 index 00000000..c24ac7fd --- /dev/null +++ b/docs/plans/2026-07-09-brainstorming-fake-message-platform.md @@ -0,0 +1,782 @@ +# Brainstorming — FakeMessagePlatform 设计分析 + +> 日期:2026-07-09 +> 目标:为 GoChat 开发一个可用于自动化对接测试的 FakeMessagePlatform,覆盖消息动态发送/回复、客服上下线、聊天窗口关闭等场景。 +> 前置条件:已完前后端 28 页功能测试(Round 4),WebSocket 认证修复已验证,但消息收发平台与多客服登录消息分配尚未对接实际平台验证。 + +--- + +## 1. 问题背景 + +### 1.1 测试现状 + +Round 4 QA 验证了: +- 28 页全量功能页面无新 console error +- WebSocket 认证端到端修复(access-token cookie → WS URL 拼接) +- 实时消息推送(agent 发消息 → 前端即时显示) + +**但以下场景未被实际验证**: +- 外部渠道 → GoChat:客户通过外部消息平台发消息,GoChat webhook 接收并创建会话 +- GoChat → 外部渠道:客服回复后,消息通过渠道 provider 发回外部平台 +- 多客服同时在线的消息分配(assignment policy) +- 客服上下线状态切换对消息路由的影响 +- 聊天窗口关闭/重开对话的连续性 +- 打字状态指示(typing indicator)跨渠道传递 +- 消息送达/已读回执(delivery/read receipt) + +### 1.2 为什么需要 FakeMessagePlatform + +GoChat 支持 13 种渠道类型(web_widget/telegram/facebook/instagram/whatsapp/email/twilio_sms/twilio_whatsapp/line/slack/api/tiktok/microsoft),但: +- 对接真实平台需要 API Key / OAuth Token / 企业认证,测试环境不可获取 +- 真实平台有 rate limit、审核流程、费用,不适合自动化压测 +- 真实平台的 webhook 回调不可控(无法模拟"客户发消息→等3秒→客服回复→客户关闭窗口"这种时序场景) + +**FakeMessagePlatform 的定位**:一个可编程的、运行在本地 HTTP 端口上的"假"消息平台,它: +1. 模拟外部渠道的角色(客户、客服、系统) +2. 通过 HTTP API 触发对 GoChat webhook 的回调 +3. 接收 GoChat outbound 发来的消息并暴露给测试断言 +4. 支持时序编排(先发消息、等 N 秒、再关闭窗口) +5. 当前仅实现 `fake` 渠道,但架构预留 `qq/weixin/shangwutong/douyin/xiaohongshu` 等国内平台扩展 + +--- + +## 2. GoChat 消息事件类型梳理 + +### 2.1 内部事件类型(event.go) + +GoChat 定义了 6 大类 32 种 EventType: + +| 类别 | 事件 | 说明 | +|------|------|------| +| **Message** | message.incoming | 外部渠道入站消息 | +| | message.outgoing | 客服出站消息 | +| | message.created | 消息创建完成 | +| | message.updated | 消息更新 | +| | message.deleted | 消息删除 | +| | message.status_updated | 消息状态更新(送达/已读) | +| **Conversation** | conversation.created | 会话创建 | +| | conversation.updated | 会话更新 | +| | conversation.resolved | 会话已解决 | +| | conversation.opened | 会话重新打开 | +| | conversation.assigned | 会话已分配 | +| | conversation.unassigned | 会话取消分配 | +| | conversation.deleted | 会话删除 | +| | conversation.muted | 会话静音 | +| | conversation.unmuted | 会话取消静音 | +| | conversation.priority_updated | 优先级更新 | +| | conversation.labels_updated | 标签更新 | +| | conversation.typing | 打字中 | +| | conversation.typing_on | 打字开始 | +| | conversation.typing_off | 打字结束 | +| **Contact** | contact.created | 联系人创建 | +| | contact.updated | 联系人更新 | +| | contact.deleted | 联系人删除 | +| | contact.merged | 联系人合并 | +| **Channel/Inbox** | channel.connected | 渠道连接成功 | +| | channel.disconnected | 渠道断开 | +| | channel.reauthorized | 渠道重新授权 | +| | inbox.created | 收件箱创建 | +| | inbox.updated | 收件箱更新 | +| | inbox.deleted | 收件箱删除 | +| **Agent** | agent.added | 客服加入 | +| | agent.removed | 客服移除 | +| | agent.online | 客服上线 | +| | agent.offline | 客服下线 | +| **Typing** | typing.start | 打字开始 | +| | typing.stop | 打字停止 | + +### 2.2 消息内容类型(ContentType) + +| 类型 | 标识 | 用途 | +|------|------|------| +| 文本 | text | 纯文本消息 | +| 图片 | image | 图片附件 | +| 文件 | file | 通用文件 | +| 音频 | audio | 语音消息 | +| 视频 | video | 视频消息 | +| 位置 | location | 地理位置 | +| 邮件 | email | 邮件渠道消息 | +| 模板 | template | WhatsApp 模板消息 | + +### 2.3 发送者类型(SenderType) + +| 类型 | 说明 | +|------|------| +| contact | 客户/访客 | +| agent | 客服/坐席 | +| system | 系统消息(活动日志等) | + +### 2.4 渠道能力声明(ChannelCapabilities) + +每个 provider 声明其支持的能力,FakeMessagePlatform 需覆盖的核心能力: +- supports_attachments(附件收发) +- supports_typing_indicator(打字指示) +- supports_delivery_status(送达回执) +- supports_replies(消息回复引用) +- supports_emoji_reactions(表情回应) + +### 2.5 Webhook 路由模式 + +GoChat 统一 webhook 入口:`/webhooks/{channel_type}/{identifier}` + +当前已注册的路由: +- `/webhooks/facebook/:page_id` (GET 验证 + POST 消息) +- `/webhooks/telegram/:bot_token` (POST) +- `/webhooks/whatsapp/:phone_number` (GET 验证 + POST 消息) +- `/webhooks/tiktok/:business_id` (GET 验证 + POST 消息) +- `/webhooks/line/:line_channel_id` (POST) +- `/webhooks/sms/:phone_number` (POST, Twilio) +- `/webhooks/twitter` (GET + POST) +- `/twilio/callback` (POST, Twilio SMS) +- `/twilio/delivery_status` (POST) + +FakeMessagePlatform 需注册路由:`/webhooks/fake/:inbox_identifier` (GET 验证 + POST 消息) + +### 2.6 渠道消息流(Broker → Pipeline → Dispatcher) + +``` +外部平台 → HTTP POST /webhooks/fake/:identifier + → WebhookHandler.HandleWebhook + → provider.ValidateWebhookRequest (签名/Token 验证) + → provider.ProcessIncoming (raw JSON → IncomingMessage) + → broker.HandleIncoming + → IncomingMessageProcessor.Process + → ValidateConfig + → ResolveContact (创建/查找 Contact) + → ResolveConversation (创建/查找 Conversation) + → PersistMessage (写入 DB) + → PublishEvent (EventBus → message.created) + → Dispatcher.Dispatch + → WebhookListener (转发到外部 webhook URL) + → NotificationListener (推送通知给客服) + → ChannelStatusListener (渠道健康跟踪) +``` + +出站流程: +``` +客服在前端发消息 → API → MessageService.Create + → broker.HandleOutgoing + → OutgoingMessageProcessor.Process + → provider.SendMessage (调用外部平台 API) + → 更新 message.SourceID + → PublishEvent (message.outgoing) +``` + +--- + +## 3. FakeMessagePlatform 架构设计 + +### 3.1 设计原则 + +1. **独立进程,可独立启停** — FakeMessagePlatform 是一个独立的 Node.js/TypeScript HTTP 服务,不嵌入 GoChat 后端 +2. **HTTP API 驱动** — 所有操作(发消息、上下线、关窗口)通过 REST API 触发,方便自动化测试脚本调用 +3. **真实模拟 GoChat webhook 契约** — FakeMessagePlatform 作为"外部平台"角色,向 GoChat 的 `/webhooks/fake/:identifier` 发送 HTTP POST,完全模拟真实渠道的行为 +4. **接收 GoChat outbound 回调** — FakeMessagePlatform 暴露一个回调接收端点,GoChat 的 fake provider 的 SendMessage 方法将消息 POST 到这个端点 +5. **状态可观测** — 所有发送/接收的消息、会话状态、客服在线状态都存在内存中,通过 API 可查询,供测试断言 +6. **时序编排能力** — 提供"脚本化"API,支持 `发送消息 → 等待 → 验证回复 → 关闭窗口`的编排 +7. **当前仅实现 fake,架构预留国内平台** — 目录结构 `channels/{fake,qq,weixin,shangwutong,douyin,xiaohongshu}`,fake 先行 + +### 3.2 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 测试编排脚本 (test runner) │ +│ (Jest/Vitest/Playwright 或纯 shell + curl) │ +└──────────┬──────────────────────────────┬───────────────────────┘ + │ HTTP API │ HTTP API + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ FakeMessagePlatform│ │ GoChat Backend │ +│ (Node.js HTTP :9100)│ │ (Go :3000) │ +│ │ │ │ +│ ┌─── REST API ───┐ │ │ ┌─ Webhook Handler ─┐│ +│ │ POST /send │─┼──POST──▶│ │ /webhooks/fake/:id ││ +│ │ POST /reply │ │ │ │ → ProcessIncoming││ +│ │ POST /close │ │ │ │ → Broker ││ +│ │ POST /typing │ │ │ │ → Dispatcher ││ +│ │ POST /online │ │ │ └────────────────────┘│ +│ │ POST /offline │ │ │ │ +│ │ GET /messages │ │ │ ┌─ Outbound ────────┐│ +│ │ GET /status │ │◀─POST───┼─│ provider.SendMessage│ +│ └────────────────┘ │ │ │ → POST to Fake ││ +│ │ │ └────────────────────┘│ +│ ┌─ State Store ──┐ │ │ │ +│ │ messages[] │ │ │ │ +│ │ sessions[] │ │ │ │ +│ │ agents[] │ │ │ │ +│ └────────────────┘ │ │ │ +└─────────────────────┘ └─────────────────────┘ +``` + +### 3.3 目录结构 + +``` +channels/ # 仓库根新增目录 +├── fake/ # FakeMessagePlatform — 当前实现 +│ ├── package.json # 独立 package(pnpm workspace 成员) +│ ├── tsconfig.json +│ ├── src/ +│ │ ├── server.ts # Express/Fastify HTTP 服务器 +│ │ ├── api/ # REST API 路由 +│ │ │ ├── send.ts # 发送消息到 GoChat webhook +│ │ │ ├── reply.ts # 回复消息 +│ │ │ ├── close.ts # 关闭聊天窗口 +│ │ │ ├── typing.ts # 打字状态 +│ │ │ ├── online.ts # 客服上下线 +│ │ │ └── query.ts # 查询消息/状态 +│ │ ├── client/ # GoChat webhook 客户端 +│ │ │ └── gochat-client.ts # 封装对 GoChat /webhooks/fake 的调用 +│ │ ├── store/ # 内存状态存储 +│ │ │ └── memory-store.ts +│ │ ├── types.ts # 类型定义 +│ │ └── index.ts # 入口 +│ ├── README.md +│ └── tests/ # 自身单元测试 +├── qq/ # 预留(未实现) +│ └── README.md # 仅说明文档 +├── weixin/ # 预留 +│ └── README.md +├── shangwutong/ # 预留 +│ └── README.md +├── douyin/ # 预留 +│ └── README.md +└── xiaohongshu/ # 预留 + └── README.md +``` + +### 3.4 Fake Provider(GoChat 后端侧) + +在 GoChat 后端新增 `fake` 渠道类型和 provider: + +**文件位置**:`backend/internal/channel/provider/fake.go` + +**ChannelType 常量**: +```go +ChannelFake ChannelType = "fake" +``` + +**FakeProvider 实现**: +- `Type()` → "fake" +- `Name()` → "Fake Message Platform" +- `Description()` → "Test channel for automated integration testing" +- `ConfigSchema()` → 需要 `webhook_url`(FakeMessagePlatform 回调端点 URL)+ `identifier` +- `ValidateConfig()` → 验证 webhook_url 非空 +- `DefaultConfig()` → webhook_url: "", identifier: "" +- `OnCreate()` → 无外部资源需要创建 +- `OnDestroy()` → 无外部资源需要清理 +- `ProcessIncoming()` → 解析 FakeMessagePlatform 发来的 JSON payload → IncomingMessage +- `ValidateWebhookRequest()` → 简单 token 验证(X-Fake-Token header 匹配 config 中的 token) +- `SendMessage()` → 将出站消息 POST 到 FakeMessagePlatform 的 `/receive` 端点 +- `GetContactProfile()` → 返回 FakeMessagePlatform 模拟的联系人信息 +- `Capabilities()` → 全能力开启(attachments + typing + delivery + replies + reactions) + +**Webhook 路由注册**(router.go): +``` +POST /webhooks/fake/:identifier → FakeProvider.ProcessIncoming +GET /webhooks/fake/:identifier → 简单验证返回 challenge +``` + +### 3.5 FakeMessagePlatform REST API 设计 + +#### 3.5.1 发送消息(模拟客户发消息) + +``` +POST /api/send +{ + "inbox_identifier": "fake_inbox_1", + "sender_id": "customer_001", + "sender_name": "测试客户", + "content": "你好,我需要帮助", + "content_type": "text", + "attachments": [] +} + +→ FakeMessagePlatform 向 GoChat POST /webhooks/fake/fake_inbox_1 + { + "event": "message.incoming", + "message_id": "fake_msg_001", + "sender_id": "customer_001", + "sender_name": "测试客户", + "content": "你好,我需要帮助", + "content_type": "text", + "timestamp": 1720000000 + } +``` + +#### 3.5.2 回复消息(模拟客户回复客服) + +``` +POST /api/reply +{ + "inbox_identifier": "fake_inbox_1", + "sender_id": "customer_001", + "reply_to_id": "msg_from_agent_123", + "content": "谢谢,问题解决了", + "content_type": "text" +} +``` + +#### 3.5.3 关闭聊天窗口 + +``` +POST /api/close +{ + "inbox_identifier": "fake_inbox_1", + "conversation_id": "external_conv_001", + "sender_id": "customer_001" +} + +→ FakeMessagePlatform 向 GoChat 发送 session_end 事件 + { + "event": "session.end", + "conversation_id": "external_conv_001", + "sender_id": "customer_001", + "timestamp": 1720000000 + } +``` + +#### 3.5.4 打字状态 + +``` +POST /api/typing +{ + "inbox_identifier": "fake_inbox_1", + "sender_id": "customer_001", + "typing": true +} +``` + +#### 3.5.5 客服上下线(模拟多客服场景) + +``` +POST /api/agent/online +{ + "agent_id": "agent_001", + "agent_name": "客服小王" +} + +POST /api/agent/offline +{ + "agent_id": "agent_001" +} +``` + +注意:这里的"客服上下线"是模拟 FakeMessagePlatform 侧观察到的 GoChat 客服状态变化。实际的 agent online/offline 是 GoChat 内部事件,FakeMessagePlatform 通过 GoChat 的 webhook subscription 或 WebSocket 来接收这些事件,并通过 API 暴露给测试脚本查询。 + +#### 3.5.6 查询接口 + +``` +GET /api/messages?inbox_identifier=fake_inbox_1 +→ 返回 FakeMessagePlatform 收到的所有出站消息(GoChat 发来的) + +GET /api/status +→ 返回平台状态:在线客服列表、活跃会话、消息计数 + +GET /api/messages/:id +→ 查询单条消息详情 +``` + +#### 3.5.7 接收 GoChat 出站消息 + +``` +POST /receive +→ GoChat FakeProvider.SendMessage 将消息 POST 到这里 +Body: +{ + "message_id": 123, + "conversation_id": 456, + "content": "您好,有什么可以帮您?", + "content_type": "text", + "sender": { + "id": 1, + "name": "客服小王", + "type": "agent" + } +} + +FakeMessagePlatform 将消息存入内存 store,供 /api/messages 查询 +``` + +### 3.6 FakeMessagePlatform 与 GoChat 的对接流程 + +#### 3.6.1 初始化对接 + +1. 启动 FakeMessagePlatform(`pnpm fake:start`,默认端口 9100) +2. GoChat 后端注册 FakeProvider(init() 自动注册) +3. 在 GoChat 中创建 fake 渠道的 Inbox(通过 API 或 seed): + ``` + POST /api/v1/accounts/1/inboxes + { + "name": "Fake Test Inbox", + "channel": { + "type": "fake", + "webhook_url": "http://127.0.0.1:9100/receive", + "identifier": "fake_inbox_1", + "token": "test_token_123" + } + } + ``` +4. FakeMessagePlatform 配置 GoChat webhook URL(环境变量或 API): + ``` + GOCHAT_WEBHOOK_URL=http://127.0.0.1:3000/webhooks/fake/fake_inbox_1 + GOCHAT_FAKE_TOKEN=test_token_123 + ``` + +#### 3.6.2 消息收发测试流程 + +``` +测试脚本 FakeMsgPlatform GoChat + │ │ │ + │── POST /api/send ──────────────▶│ │ + │ │── POST /webhooks/fake ─▶│ + │ │ │── ProcessIncoming + │ │ │── Broker.HandleIncoming + │ │ │── Create Contact + Conv + │ │ │── PersistMessage + │ │ │── Dispatch message.created + │ │◀── HTTP 200 ───────────│ + │ │ │ + │ │ │── 客服在前端看到消息 + │ │ │── 客服回复消息 + │ │ │── Broker.HandleOutgoing + │ │◀── POST /receive ──────│── FakeProvider.SendMessage + │ │── 存入 memory store │ + │ │ │ + │── GET /api/messages ───────────▶│ │ + │◀── 返回出站消息列表 ───────────│ │ + │ │ │ + │── 断言:收到客服回复 "您好..." ──│ │ +``` + +### 3.7 消息事件类型映射(国内平台参考) + +FakeMessagePlatform 当前只实现 fake 渠道,但为后续对接国内平台做事件类型参考: + +| GoChat 事件 | Fake 事件 | 微信对应 | 商务通对应 | 抖音对应 | +|-------------|-----------|---------|-----------|---------| +| message.incoming | message.incoming | 文本/图片/语音消息 | 客户发起对话 | 私信消息 | +| message.outgoing | message.outgoing | 公众号回复 | 客服回复 | 创作者回复 | +| conversation.typing_on | typing.start | — | 正在输入 | — | +| conversation.typing_off | typing.stop | — | 停止输入 | — | +| conversation.resolved | session.end | 客户关闭会话 | 客服结束对话 | 会话超时 | +| agent.online | agent.online | — | 客服上班签到 | — | +| agent.offline | agent.offline | — | 客服下班签退 | — | +| message.status_updated | delivery.read | 已读 | 已读回执 | — | + +### 3.8 pnpm 集成 + +在根 `package.json` 添加脚本: +```json +{ + "scripts": { + "fake:start": "cd channels/fake && tsx src/index.ts", + "fake:dev": "cd channels/fake && tsx watch src/index.ts", + "fake:test": "cd channels/fake && vitest run" + } +} +``` + +在 `pnpm-workspace.yaml` 添加: +```yaml +packages: + - frontend + - channels/fake +``` + +### 3.9 技术选型 + +| 组件 | 选型 | 理由 | +|------|------|------| +| 语言 | TypeScript | 与前端工具链一致,类型安全 | +| HTTP 框架 | Express.js | 生态成熟,团队熟悉 | +| 运行时 | tsx (esbuild) | 零配置 TS 执行,比 ts-node 快 | +| 测试 | vitest | 与前端一致,零配置 | +| HTTP 客户端 | undici/fetch | Node 20+ 内置 fetch | +| 状态存储 | 内存 Map | 测试工具不需要持久化 | + +--- + +## 4. 关键设计决策 + +### 4.1 为什么 FakeMessagePlatform 是独立进程而非 GoChat 内嵌 mock + +- **隔离性**:FakeMessagePlatform 模拟的是"外部平台",独立进程更真实地模拟 HTTP 交互 +- **可观测性**:独立进程有自己的 API 和日志,测试脚本可直接查询状态 +- **不污染后端代码**:GoChat 后端只需要新增一个 FakeProvider(遵循渠道插件接口),不需要引入测试 mock 逻辑 +- **可复用性**:FakeMessagePlatform 可以被任何测试框架调用(Playwright、curl、shell 脚本) + +### 4.2 为什么用 TypeScript 而非 Go + +- **快速迭代**:测试工具不需要生产级性能,TS 开发效率更高 +- **与前端工具链统一**:vitest/tsx/esbuild 已在前端使用,无额外学习成本 +- **JSON 处理更自然**:FakeMessagePlatform 的核心工作是 JSON payload 构造和 HTTP 收发,TS 的 JSON 处理比 Go 更简洁 + +### 4.3 为什么 FakeProvider 需要注册到 GoChat 的 ChannelType 枚举 + +GoChat 的 ChannelType 是 string 枚举,FakeProvider 通过 init() 自动注册到全局 ChannelRegistry。这意味着: +- 前端可以像创建 Telegram/WhatsApp 渠道一样创建 fake 渠道 Inbox +- Webhook 路由自动注册 `/webhooks/fake/:identifier` +- Broker/Dispatcher/Pipeline 对 fake 渠道完全透明,走相同的消息处理管道 +- 这才是真正的"端到端"测试——从 webhook 到 DB 到 WebSocket 全链路验证 + +### 4.4 国内平台预留目录的策略 + +`channels/{qq,weixin,shangwutong,douyin,xiaohongshu}` 当前只放 README.md 说明文档,不实现代码。原因: +- 这些平台需要真实 API 对接,当前无需求也无凭证 +- 目录预留让未来扩展时路径已就位,不需要重新规划 +- 每个平台的消息事件类型差异较大(微信公众号 vs 微信小程序 vs 企业微信),需要各自独立的 provider 实现 + +--- + +## 5. 开放问题 + +1. **FakeMessagePlatform 是否需要模拟 OAuth 流程?** — 当前设计不需要,fake 渠道用简单 token 验证。如果后续要测试 OAuth 渠道的 reauthorization 流程,可以扩展。 +2. **多 Inbox 场景**:FakeMessagePlatform 需要支持同时模拟多个 fake inbox(多渠道实例),当前设计通过 `inbox_identifier` 区分,已支持。 +3. **与 GoChat WebSocket 的集成**:FakeMessagePlatform 当前只走 HTTP webhook。如果需要验证实时推送(客服收到消息的 WS 通知),测试脚本可以直接连 GoChat 的 WebSocket 验证,不需要 FakeMessagePlatform 介入。 +4. **消息附件测试**:FakeMessagePlatform 可以生成临时文件 URL 模拟附件,但需要确保 GoChat 能下载该 URL(同网络环境即可)。 + +--- + +## 6. 结论 + +FakeMessagePlatform 的核心价值在于:它让 GoChat 的渠道消息流从"对接真实平台才能测"变成"本地启动即可全链路验证"。通过实现一个符合 ChannelProvider 接口的 FakeProvider + 一个可编程的外部平台模拟器,我们可以自动化验证: + +- 消息收发全链路(webhook → broker → pipeline → dispatcher → WS 推送) +- 多客服上下线消息分配 +- 打字状态、送达回执 +- 聊天窗口关闭/重开 +- 会话创建/分配/解决流程 + +架构上,FakeProvider 是 GoChat 渠道插件体系的又一个实现,证明"渠道即插件"设计的有效性;FakeMessagePlatform 是测试基础设施的一部分,与 parity_frontend_smoke.sh 形成互补。 + +--- + +## 7. 基于代码事实验证的 Review 结论 + +> 以下结论均通过读取仓库实际代码验证,非假设。 + +### 7.1 验证通过的设计决策 + +| 验证项 | 代码依据 | 结论 | +|--------|----------|------| +| ChannelProvider 接口方法完整性 | `backend/internal/channel/provider.go:38-104` — 12 个必需方法 | FakeProvider 覆盖全部接口,与 web_widget.go 一致 | +| provider init() 自动注册 | `bootstrap.go:22` 直接 import channelprovider 包 | fake.go 的 init() 会随包加载自动执行 channel.MustRegister() | +| IncomingPersister API | `incoming_persister.go:60-82` — NewIncomingPersister(db, dispatcher...) + PersistIncoming(ctx, inbox, msg) | 签名与计划代码一致 | +| router_test.go 路由断言 | `router_test.go:51-104` — routes map + t.Fatalf | 添加 fake 路由到 expected 列表即可 | +| concurrently 3 色参数 | 实测 `npx concurrently -c blue,green,magenta` 成功 | dev:all 脚本可行 | +| bootstrap search indexer 装配 | `bootstrap.go:712-717` — 所有 webhook handler 链式调用 WithSearchIndexer | 分两步创建 handler + indexer | + +### 7.2 已修正的错误 + +**错误 1:model.Message 没有 User 字段** +- 验证:`backend/internal/model/message.go:8-29` — Message 只有 `SenderID *uint`、`SenderType string`,无 User 关联 +- 修正:SendMessage 改用 `*message.SenderID`(nil 检查)+ `message.SenderType` 判断 agent 身份 + +**错误 2:lookupInbox 引用不存在的 ChannelAPI 查询** +- 验证:`backend/internal/model/channel/` 无 fake.go 模型;`Inbox.ChannelConfig` 是 TEXT 列非 JSONB +- 修正:改为 `WHERE channel_type = 'fake'` 查全部 fake inbox,Go 层面解析 ChannelConfig JSON 过滤 identifier。兼容 SQLite 测试模式 + +**错误 3:fake_webhook.go 多导入 channelmodel** +- 修正:移除不再需要的 `channelmodel` import + +### 7.3 已知限制(非阻塞) + +**限制 1:createMessage 硬编码 SenderType** +- 验证:`incoming_persister.go:544-557` — SenderType 固定为 SenderTypeContact,忽略 IncomingMessage.SenderType +- 影响:session.end 事件设置 SenderSystem 不生效,消息仍以 contact + incoming 创建 +- 应对:session.end 的 content 设为 "[session ended]",测试脚本通过内容识别 + +**限制 2:InboxChannelType 枚举无 Channel::Fake** +- 验证:`backend/internal/model/enums.go:125-140` — 无 Channel::Fake +- 但 Inbox.ChannelType 存的是 snake_case 字符串("telegram" 等),不是枚举值 +- 结论:用 "fake" 即可,不需要加枚举常量 + +**限制 3:inbox_service.go 白名单不包含 fake** +- 验证:`backend/internal/service/inbox_service.go:172-176` — validChannelTypes 白名单无 "fake" +- 修正:需要在白名单中添加 "fake",并在 buildInitialInboxChannelConfig 中添加 fake case + +--- + +## 8. 实施计划 + +> 以下为 bite-sized task 列表,每个 task 包含精确文件路径和验证步骤。 + +### Task 1: 注册 ChannelFake 类型常量 + +**文件:** `backend/internal/channel/provider.go:15-29` + +在 ChannelType 常量块末尾添加 `ChannelFake ChannelType = "fake"`。 + +验证:`cd backend && go build ./internal/channel/...` + +### Task 2: 实现 FakeProvider + +**文件:** Create `backend/internal/channel/provider/fake.go` + +实现完整 ChannelProvider 接口(12 方法),参照 `web_widget.go` 结构。关键实现要点: + +- ProcessIncoming:解析 FakeIncomingPayload JSON → IncomingMessage +- SendMessage:将出站消息 POST 到 config.webhook_url(FakeMessagePlatform /receive 端点) + - 注意:model.Message 无 User 字段,用 `*message.SenderID` + `message.SenderType` +- ValidateWebhookRequest:X-Fake-Token header 匹配 config.token +- Capabilities:全能力开启 +- init():channel.MustRegister(NewFakeProvider()) + +验证:`cd backend && go build ./internal/channel/provider/...` + +### Task 3: 实现 FakeWebhookHandler(Gin 适配器) + +**文件:** Create `backend/internal/handler/webhook/fake_webhook.go` + +参照 `telegram_webhook.go` 实现。关键: + +- lookupInbox:`WHERE channel_type = 'fake'` 查全部 → Go 层面解析 ChannelConfig JSON 过滤 identifier(不查 ChannelAPI 表,不需要 channelmodel import) +- HandleFakeWebhook:读 body → lookupInbox → 验证 X-Fake-Token → ProcessIncoming → PersistIncoming +- HandleFakeWebhookVerification:GET 验证,echo challenge +- WithWorkerPool / WithSearchIndexer 链式方法 + +验证:`cd backend && go build ./internal/handler/webhook/...` + +### Task 4: 后端 inbox_service 支持 fake 渠道创建 + +**文件:** Modify `backend/internal/service/inbox_service.go` + +两处修改: +1. `validChannelTypes` 白名单(line 172-176):添加 `"fake": true` +2. `buildInitialInboxChannelConfig` switch(line 494):添加 fake case,从 channel map 中提取 identifier/webhook_url/token + +验证:`cd backend && go build ./internal/service/...` + +### Task 5: Handlers 结构体 + 路由注册 + +**文件:** +- Modify `backend/internal/router/router.go:38-103`(Handlers 结构体加 FakeWebhook 字段) +- Modify `backend/internal/router/router.go:354-484`(webhook 路由组加 fake 路由) +- Modify `backend/internal/router/router_test.go:51-98`(expected 列表加 fake 路由) + +路由注册: +```go +if handlers.FakeWebhook != nil { + fakeGroup := webhookGroup.Group("/fake") + fakeGroup.GET("/:identifier", handlers.FakeWebhook.HandleFakeWebhookVerification) + fakeGroup.POST("/:identifier", handlers.FakeWebhook.HandleFakeWebhook) +} +``` + +router_test.go expected 添加:`"GET /webhooks/fake/:identifier"`, `"POST /webhooks/fake/:identifier"` + +验证:`cd backend && go build ./internal/router/... && go test -run TestRegisterRoutes ./internal/router/... -v` + +### Task 6: Bootstrap 装配 + +**文件:** Modify `backend/internal/app/bootstrap.go` + +两步: +1. webhook handler 创建区域(~line 403-430):创建 fakeWebhookHandler + WithWorkerPool +2. search indexer 区域(~line 712-717):fakeWebhookHandler.WithSearchIndexer(searchIndexer) +3. Handlers 结构体初始化(~line 844):加 `FakeWebhook: fakeWebhookHandler` + +验证:`cd backend && go build ./...` + +### Task 7: 前端渠道选择器添加 fake 选项 + +**文件:** Modify `frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue` + +在 channelList computed 的 channels 数组中,在 voice 之前添加: +```js +{ + key: 'fake', + title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.FAKE.TITLE'), + description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.FAKE.DESCRIPTION'), + icon: 'i-woot-api', // 复用 API 图标,或用其他合适图标 +}, +``` + +### Task 8: 前端渠道工厂注册 fake 组件 + +**文件:** +- Create `frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Fake.vue` +- Modify `frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelFactory.vue` + +Fake.vue 参照 Api.vue 实现,表单字段:channelName(必填)、identifier(必填)、webhookUrl(FakeMessagePlatform /receive URL)、token(可选)。 +提交时 dispatch `inboxes/createChannel`,channel.type = 'fake'。 + +ChannelFactory.vue:import Fake 组件,channelViewList 添加 `fake: Fake`。 + +### Task 9: 前端 i18n 添加 fake 渠道翻译 + +**文件:** +- Modify `frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json` +- Modify `frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json` + +zh_CN 添加: +- `AUTH.CHANNEL.FAKE`: `{ "TITLE": "Fake 测试平台", "DESCRIPTION": "创建用于自动化测试的 Fake 消息渠道" }` +- `FAKE_CHANNEL`: `{ "TITLE": "Fake 测试频道", "DESC": "...", "CHANNEL_NAME": {...}, "IDENTIFIER": {...}, "WEBHOOK_URL": {...}, "TOKEN": {...}, "SUBMIT_BUTTON": "创建 Fake 频道", "API": { "ERROR_MESSAGE": "..." } }` + +en 添加对应英文翻译。 + +### Task 10: 创建 FakeMessagePlatform 项目 + +**文件:** +- Create `channels/fake/package.json`(@gochat/fake-platform, express/tsx/vitest) +- Create `channels/fake/tsconfig.json` +- Create `channels/fake/src/types.ts` +- Create `channels/fake/src/store/memory-store.ts` +- Create `channels/fake/src/client/gochat-client.ts` +- Create `channels/fake/src/server.ts`(Express HTTP 服务器,REST API: /api/send /api/reply /api/close /api/typing /api/agent/online /api/agent/offline /api/messages /api/status /api/reset /receive /health) +- Create `channels/fake/src/index.ts`(入口,读环境变量配置) +- Create `channels/fake/README.md` +- Modify `pnpm-workspace.yaml`(添加 channels/fake) +- Modify `package.json`(添加 fake:start fake:dev fake:test dev:all 脚本) + +验证:`pnpm install && pnpm fake:start`,curl /health 返回 200 + +### Task 11: 国内平台预留目录 + +**文件:** +- Create `channels/README.md`(目录说明) +- Create `channels/qq/README.md` +- Create `channels/weixin/README.md` +- Create `channels/shangwutong/README.md` +- Create `channels/douyin/README.md` +- Create `channels/xiaohongshu/README.md` + +### Task 12: 编写测试 + +**文件:** +- Create `backend/internal/channel/provider/fake_test.go`(Go 单元测试:Type/ValidateConfig/ProcessIncoming/ValidateWebhookRequest/Capabilities) +- Create `channels/fake/tests/integration.test.ts`(TS 测试:/health /api/send 校验 /api/agent/online + /api/status /api/reset) + +验证:`cd backend && GOCHAT_TEST_DB=sqlite go test ./internal/channel/provider/... -v -run Fake` + `cd channels/fake && pnpm test` + +### Task 13: 全量验证 + +```bash +cd backend && go build ./... && go vet ./... +cd backend && GOCHAT_TEST_DB=sqlite go test ./internal/channel/... ./internal/handler/webhook/... ./internal/router/... ./internal/service/... -v +pnpm fake:start # Terminal 2 +# 端到端:创建 fake inbox → POST /api/send → 验证 GoChat 收到消息 → 客服回复 → GET /api/messages 验证出站 +``` + +--- + +## 9. 文件清单 + +| 操作 | 文件 | Task | +|------|------|------| +| Modify | `backend/internal/channel/provider.go` | 1 | +| Create | `backend/internal/channel/provider/fake.go` | 2 | +| Create | `backend/internal/handler/webhook/fake_webhook.go` | 3 | +| Modify | `backend/internal/service/inbox_service.go` | 4 | +| Modify | `backend/internal/router/router.go` | 5 | +| Modify | `backend/internal/router/router_test.go` | 5 | +| Modify | `backend/internal/app/bootstrap.go` | 6 | +| Modify | `frontend/.../inbox/ChannelList.vue` | 7 | +| Create | `frontend/.../inbox/channels/Fake.vue` | 8 | +| Modify | `frontend/.../inbox/ChannelFactory.vue` | 8 | +| Modify | `frontend/.../i18n/locale/zh_CN/inboxMgmt.json` | 9 | +| Modify | `frontend/.../i18n/locale/en/inboxMgmt.json` | 9 | +| Create | `channels/fake/` 全套 TS 文件 | 10 | +| Modify | `pnpm-workspace.yaml` + `package.json` | 10 | +| Create | `channels/{qq,weixin,shangwutong,douyin,xiaohongshu}/README.md` | 11 | +| Create | `backend/.../fake_test.go` + `channels/fake/tests/` | 12 |