WebChat 架构(V2)— 与 Telegram 共享会话
最后更新:2026-03-11 状态:已上线(V2 重写)
0. 核心目标
| # | 目标 | 含义 |
|---|---|---|
| 1 | WebChat 和 Telegram 共享同一个会话 | 用户在 TG 聊了一半,切到 WebChat 继续聊,上下文无缝衔接。反之亦然。 |
| 2 | WebChat 和 Telegram 聊天效果一模一样 | 工具调用、长期记忆、文件读写、代码执行、流式输出——TG 能做的,WebChat 全部能做。 |
推论:WebChat 不是一个独立的 LLM 前端,它是 OpenClaw 的第二个消息通道,和 Telegram 插件平级。
1. 架构总览
┌─────────────┐ ┌─────────────┐
│ Telegram │ │ WebChat │
│ (手机/桌面) │ │ (浏览器) │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ TG Bot API │ │ Portal API (Go) │
│ (OC 内置插件) │ │ WebSocket Handler│
└──────┬──────┘ └───────┬──────────┘
│ │
│ ┌─────────────────┘
│ │ 同一个 HTTP API
▼ ▼
┌─────────────────────────────────┐
│ OpenClaw Gateway │
│ agent:main:main (唯一会话) │
│ ┌───────────────────────────┐ │
│ │ 工具调用 / 记忆 / 执行 │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
关键:TG 插件和 WebChat 中继都往同一个 agent session 发消息,OC 不区分消息来源是 TG 还是 WebChat。
2. 为什么不能前端直连 OC Gateway?
| 问题 | 说明 |
|---|---|
| OC Gateway WS 不是公共 API | 它是给 OC 内部 agent session 用的,协议不兼容前端 JSON |
| 多实例动态路由 | 每个 OC 实例在不同服务器不同端口,Nginx 无法静态配置 |
| 鉴权耦合 | 让 OC 理解 Portal JWT 会污染 Gateway 职责 |
| 会话共享 | 前端直连无法和 TG 共享同一个 agent session |
结论:Portal API 作为 WebChat Channel Adapter,翻译前端 WS 消息 → OC Gateway HTTP API。
3. 详细交互时序
[前端 WebChat] [Portal API] [OC Gateway] [Telegram]
│ │ │ │
│ 1. POST /session/create │ │
├──────────────────────►│ │ │
│ │ 2. 查 DB: instance → │ │
│ │ gateway_url + token │ │
│ 3. 返回 ws_url │ │ │
◄───────────────────────┤ │ │
│ │ │ │
│ 4. WSS 连接 Portal │ │ │
├──────────────────────►│ │ │
│ │ 5. JWT 验证 │ │
│ 6. 连接建立 ✅ │ │ │
│ │ │ │
│ 7. 发送消息 │ │ │
├──────────────────────►│ │ │
│ │ 8. POST /api/v1/message │ │
│ │ (OC Gateway HTTP API)│ │
│ ├────────────────────────►│ │
│ │ │ 9. agent 处理 │
│ │ │ (和 TG 共享) │
│ │ 10. SSE/轮询获取响应 │ │
│ ◄─────────────────────────┤ │
│ 11. 流式返回 │ │ │
◄───────────────────────┤ │ │
│ │ │ │
│ │ │ 12. TG 用户发消息 │
│ │ ◄────────────────────┤
│ │ │ 13. 同一 session │
│ │ │ 处理并回复 TG │
│ │ ├────────────────────►
步骤 8-10 是核心:Portal 把 WebChat 消息"翻译"成 OC Gateway 能理解的格式,就像 Telegram 插件把 TG 消息翻译给 OC 一样。
4. Portal API 接口
4.1 创建 WebSocket 会话凭证
POST /api/v1/me/webchat/session/create
Authorization: Bearer <portal_jwt>
Request:
{
"instance_id": "oc-xxx-yyy"
}
Response:
{
"ws_url": "wss://api.sayclaw.ai/ws/webchat",
"ephemeral_token": "eyJhbGciOiJIUzI1Ni...",
"session_key": "ws_16a4f2b98e",
"instance_id": "oc-xxx-yyy",
"expires_in": 600
}
4.2 刷新凭证
POST /api/v1/me/webchat/session/refresh
Authorization: Bearer <portal_jwt>
Request:
{
"instance_id": "oc-xxx-yyy",
"session_key": "ws_16a4f2b98e"
}
4.3 WebSocket 端点(Portal 直接承载)
WSS wss://api.sayclaw.ai/ws/webchat?token=<ephemeral_token>
Portal API 自己作为 WebSocket 服务端:
- 握手时验证
token(JWT) - 建立连接后维持双向通信
- 前端发消息 → Portal 转发给 OC Gateway
- OC 响应 → Portal 流式推回前端
5. 消息协议(前端 ↔ Portal WS)
5.1 前端 → Portal
{
"type": "message",
"data": {
"text": "帮我查一下昨天注册的用户数",
"attachments": []
}
}
{
"type": "ping"
}
5.2 Portal → 前端
{ "type": "status", "data": { "state": "thinking", "text": "正在思考..." } }
{ "type": "status", "data": { "state": "tool_call", "text": "正在查询数据库..." } }
{ "type": "chunk", "data": { "text": "昨天共有 " } }
{
"type": "done",
"data": {
"full_text": "昨天共有 150 名新注册用户。",
"usage": { "total_tokens": 120 }
}
}
{ "type": "pong" }
{ "type": "error", "data": { "message": "会话已过期,请刷新" } }
6. Portal ↔ OC Gateway 通信
Portal 需要和 OC Gateway 交互的两个能力:
6.1 发送消息
调用 OC Gateway 的 RPC/HTTP API 将用户消息注入 agent session:
POST http://<gateway_host>:<gateway_port>/api/v1/message
Authorization: Bearer <gateway_auth_token>
{
"agentId": "main",
"message": "帮我查一下昨天注册的用户数",
"sender": {
"id": "webchat:<user_id>",
"name": "<user_display_name>"
},
"channel": "webchat",
"sessionKey": "agent:main:main"
}
注意:
sessionKey固定为agent:main:main,这就是 TG 也在用的那个主会话。消息进入后,OC 不关心来源是 TG 还是 WebChat。
6.2 获取响应
两种方案(取决于 OC Gateway 支持哪个):
方案 A:SSE 流式
GET http://<gateway_host>:<gateway_port>/api/v1/message/stream?sessionKey=agent:main:main
Authorization: Bearer <gateway_auth_token>
Portal 持续读取 SSE 事件,转发给前端 WS。
方案 B:轮询
GET http://<gateway_host>:<gateway_port>/api/v1/message/poll?sessionKey=agent:main:main&after=<last_msg_id>
Authorization: Bearer <gateway_auth_token>
Portal 定期轮询新消息,转发给前端 WS。
6.3 OC Gateway 认证
每个 OC 实例的 Gateway 都有 auth token(存在 openclaw.json → gateway.auth.token)。Portal 通过 DB 查出实例的 gateway_url + gateway_auth_token,作为内部通信凭证。
7. 会话共享保证
| 场景 | 行为 |
|---|---|
| 用户在 TG 发了消息 | OC agent:main:main 处理,回复到 TG |
| 用户在 WebChat 发了消息 | Portal 转发到同一个 agent:main:main,OC 处理,Portal 拿到结果推给 WebChat |
| 用户在 TG 聊了半小时再开 WebChat | WebChat 能看到 OC 的记忆(MEMORY.md),上下文无缝 |
| 两端同时发消息 | OC 的消息队列串行处理,不会冲突 |
WebChat 消息持久化:
- 前端在收发消息后,调用 Portal API 的
POST /me/chat/save端点,将聊天记录写入sayclaw_portal.chat_messages表。- 页面加载时优先从 Portal API
/me/chat/history获取持久化的聊天记录,fallback 到 OC Gateway 的chat.history事件。- 这种设计允许我们在
chat_messages中关联 OneAPI 的 LLM 调用日志,展示 token 消耗和费用。
8. 需要回答的技术问题
在动手写代码之前,需要先确认 OC Gateway 的能力:
| # | 问题 | 调研方法 |
|---|---|---|
| 1 | OC Gateway 是否有 HTTP API 可以注入消息? | 查 OC docs /opt/homebrew/lib/node_modules/openclaw/docs/gateway/ |
| 2 | 注入的消息能否进入 agent:main:main 的同一个 session? | 本地测试 |
| 3 | 响应能否流式获取(SSE/WS/轮询)? | 查 Gateway API |
| 4 | Gateway auth token 格式和传递方式? | 查 openclaw.json → gateway.auth |
这些问题必须先搞清楚,否则写出来的代码只是猜测。我可以立即去翻 OC 源码和文档来回答这些。
9. 回复路由:WebChat 消息回 WebChat,TG 消息回 TG
共享会话不等于回复串台。需要保证:
- TG 发的消息 → OC 回复到 TG
- WebChat 发的消息 → OC 回复到 WebChat(经 Portal 中继)
实现方式:Portal 注入消息时标记 channel: "webchat",OC 根据 channel 路由回复。
如果 OC 不支持按 channel 路由(即所有回复都发到 TG),则 Portal 需要:
- 注入消息后,监听 OC 的输出
- 拦截 OC 的回复(不让它发到 TG)
- 自己推给 WebChat 前端
这是一个关键设计决策点,取决于 OC 的回复路由机制。
10. 开发计划
| 阶段 | 内容 | 前置条件 |
|---|---|---|
| Phase 0 | 调研 OC Gateway API:消息注入、响应获取、回复路由 | 无 |
| Phase 1 | Portal API:WebSocket handler + OC Gateway 适配层 | Phase 0 结论 |
| Phase 2 | 前端 WebChat:WS 连接 + 消息渲染(status/chunk/done) | Phase 1 API 就绪 |
| Phase 3 | 聊天记录展示(从 OC session 读历史) | Phase 1 |
| Phase 4 | 回复路由(确保 WebChat 和 TG 不串台) | Phase 0 结论 |
11. 与 V1 方案的区别
| 维度 | V1(旧方案) | V2(本方案) |
|---|---|---|
| 前端连接目标 | 直连 OC Gateway(经 Nginx) | 连接 Portal API(Portal 中继) |
| 鉴权 | Nginx auth_request + Portal 内部接口 | Portal WS handler 直接验 JWT |
| 会话 | 独立 WebChat session | 共享 TG 的 agent:main:main |
| 消息能力 | 受限于 OC WS 协议兼容性 | 和 TG 完全一致(因为走同一个入口) |
| 动态路由 | Nginx 需要静态配置每个实例 | Portal 查 DB 动态路由 |
| 依赖 | 需要 Nginx auth_request 配置 | 不需要额外 Nginx 配置 |
12. 执行记录
| 日期 | 变更 | Commit |
|---|---|---|
| 2026-03-07 | V2 方案设计稿 | — |
| 2026-03-08 | ✅ 直连 Gateway 方案实现上线。portal-api 增加 X-Backend 响应头;Nginx 动态路由;前端 WS 聊天替换旧 iframe;jp-2 全部 10 个实例开启 trusted-proxy + allowedOrigins | — |
| 2026-03-09 | ⚠️ 设计稿同步导致 Chat 回退到 HTTP POST /me/chat(portal-api 中继),不经过 OC Gateway | e717fb41 |
| 2026-03-09 | ❌ 第一次修复:改用 portal-api WS 桥接(简化协议 type:message/status/chunk/done),仍非直连 Gateway | 77037999 |
| 2026-03-09 | ✅ 第二次修复:实现完整 OC Gateway 协议(connect.challenge → connect → sessions.list → chat.history → chat.send → chat events),与 TG 共享 agent:main:main 会话 | 626aa9ea |
| 2026-03-10 | ✅ 第三次修复:将消息发送的 deliver 参数从 false 修改为 true,实现 Web 端发送的消息同步推送至用户绑定的 Telegram Bot;实现 saveChatMsg 消息持久化 | 4c9eabe |
| 2026-03-11 | ✅ 移动端同步:移动端 Chat 页面同步 Web 端完整功能,包括 WebSocket 直连、deliver=true 消息互通、saveChatMsg 持久化等 | 7b0eee5 |
回退根因分析
2026-03-09 设计稿全面同步时,portal/chat/index.html 和 portal/m/chat/index.html 是新建文件。开发者从旧的 portal/m/chat.html(使用 HTTP POST API)复制了聊天逻辑,而非从已有的 chat.html(根目录,使用 WS 协议)或文档中获取正确的 Gateway 协议实现。
教训:
- 前端聊天页面修改前,必须参照本文档的协议规范
POST /me/chat是已废弃的中继路径,新页面禁止使用- 正确路径:
/me/webchat/session/create→ WebSocket → OC Gateway