add plan
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user