跳到主要内容

WebChat 架构(V2)— 与 Telegram 共享会话

最后更新:2026-03-11 状态:已上线(V2 重写)


0. 核心目标​

#目标含义
1WebChat 和 Telegram 共享同一个会话用户在 TG 聊了一半,切到 WebChat 继续聊,上下文无缝衔接。反之亦然。
2WebChat 和 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 聊了半小时再开 WebChatWebChat 能看到 OC 的记忆(MEMORY.md),上下文无缝
两端同时发消息OC 的消息队列串行处理,不会冲突

WebChat 消息持久化:

  1. 前端在收发消息后,调用 Portal API 的 POST /me/chat/save 端点,将聊天记录写入 sayclaw_portal.chat_messages 表。
  2. 页面加载时优先从 Portal API /me/chat/history 获取持久化的聊天记录,fallback 到 OC Gateway 的 chat.history 事件。
  3. 这种设计允许我们在 chat_messages 中关联 OneAPI 的 LLM 调用日志,展示 token 消耗和费用。

8. 需要回答的技术问题​

在动手写代码之前,需要先确认 OC Gateway 的能力:

#问题调研方法
1OC Gateway 是否有 HTTP API 可以注入消息?查 OC docs /opt/homebrew/lib/node_modules/openclaw/docs/gateway/
2注入的消息能否进入 agent:main:main 的同一个 session?本地测试
3响应能否流式获取(SSE/WS/轮询)?查 Gateway API
4Gateway 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 需要:

  1. 注入消息后,监听 OC 的输出
  2. 拦截 OC 的回复(不让它发到 TG)
  3. 自己推给 WebChat 前端

这是一个关键设计决策点,取决于 OC 的回复路由机制。


10. 开发计划​

阶段内容前置条件
Phase 0调研 OC Gateway API:消息注入、响应获取、回复路由无
Phase 1Portal 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-07V2 方案设计稿—
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 Gatewaye717fb41
2026-03-09❌ 第一次修复:改用 portal-api WS 桥接(简化协议 type:message/status/chunk/done),仍非直连 Gateway77037999
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 协议实现。

教训:

  1. 前端聊天页面修改前,必须参照本文档的协议规范
  2. POST /me/chat 是已废弃的中继路径,新页面禁止使用
  3. 正确路径:/me/webchat/session/create → WebSocket → OC Gateway