跳到主要内容

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.jsongateway.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.jsongateway.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.challengeconnectsessions.listchat.historychat.sendchat 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.htmlportal/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