本文回答什么问题:WebSocket 通道和 Telegram 通道有什么不同?为什么 WebUI 用 WebSocket?多路复用怎么实现?断线重连怎么处理?
目标读者 :想深入 WebUI 实现 / 自定义 WebSocket 客户端的开发者
预计阅读时间 :14 分钟
源码版本 :GitHub HKUDS/nanobot main 分支主线代码(仓库相对路径)
WebSocket 通道(nanobot/channels/websocket/)是 nanobot 最特殊的通道------它是 WebUI 与 AgentLoop 的桥梁,所有 WebUI 用户共享同一通道。
1. 整体定位:为什么 WebUI 用 WebSocket
WebUI 是浏览器,与 gateway 长跑进程通信只能用:
- HTTP 轮询:延迟高 + 服务器负载重
- WebSocket:双向实时 + 一次连接复用
WebSocket 通道 + FastAPI /ws 端点让浏览器享受毫秒级响应。
核心要点速查(建议收藏)
- 核心文件 :
nanobot/channels/websocket/runtime.py(约 250 行)+nanobot/web/server.py(/ws端点 ~50 行) - WebSocket 端点
ws://host:port/ws(FastAPIWebSocketroute) - 多路复用 单 gateway 支持 N 个 WebSocket 连接(每个浏览器一个)
- 断线重连 客户端自动(浏览器内置),服务端不主动断
- 媒体接入 图片 / 音频作为 base64 inline
2. WebSocket 通道实现
python
# nanobot/channels/websocket/runtime.py
class WebSocketChannel(BaseChannel):
name = "websocket"
display_name = "WebUI"
def __init__(self, config: Config, bus: MessageBus):
super().__init__(config, bus)
self._connections: dict[str, WebSocket] = {} # sender_id -> ws
async def start(self) -> None:
"""WebSocket 通道的 start 由 FastAPI /ws 端点触发,不主动启动。"""
pass # 由 gateway 启动 HTTP server
async def register_connection(self, sender_id: str, ws: WebSocket):
"""新连接注册到 self._connections。"""
self._connections[sender_id] = ws
async def handle_message(self, sender_id: str, data: dict):
"""从 WebSocket 收消息,转 InboundMessage。"""
await self._bus.publish_inbound(InboundMessage(
channel="websocket",
sender_id=sender_id,
chat_id=data["chat_id"],
content=data["content"],
media=data.get("media", []),
))
async def send(self, msg: OutboundMessage) -> None:
"""发到指定 sender_id 的 WebSocket。"""
ws = self._connections.get(msg.sender_id) # 注意:OutboundMessage 需要 sender_id 字段
if ws is None:
return
await ws.send_json({
"content": msg.content,
"event": msg.event.type if msg.event else None,
"event_data": msg.event.__dict__ if msg.event else {},
})
async def stop(self) -> None:
for ws in self._connections.values():
await ws.close()
关键差异 vs Telegram:
- start() 不主动------HTTP server 启动后由 FastAPI 端点触发
- 多连接 ---
self._connections字典,每个 WebSocket 一个 - media --- WebUI 直接传 base64,不需要 download
3. FastAPI /ws 端点
python
# nanobot/web/server.py
@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
await websocket.accept()
sender_id = websocket.headers.get("x-client-id", "anonymous")
channel = websocket.app.state.ws_channel
# 注册到通道
await channel.register_connection(sender_id, websocket)
try:
# 处理客户端消息
async for data in websocket.iter_json():
await channel.handle_message(sender_id, data)
except WebSocketDisconnect:
# 客户端断开
await channel.unregister_connection(sender_id)
4. 多路复用时序
渲染错误: Mermaid 渲染失败: Parse error on line 14: ...h_inbound Bus->>Loop: consume_inboun ----------------------^ Expecting '+', '-', '()', 'ACTOR', got 'loop'
5. WebSocket 协议(客户端 ↔ 服务端)
客户端 → 服务端
json
{
"type": "message",
"content": "你好",
"chat_id": "user1",
"media": []
}
服务端 → 客户端(文本)
json
{
"content": "你好!",
"event": null
}
服务端 → 客户端(流式 delta)
json
{
"content": "你好",
"event": "stream_delta",
"event_data": {"content": "你好"}
}
服务端 → 客户端(回合结束)
json
{
"content": "",
"event": "turn_end",
"event_data": {"latency_ms": 1234}
}
6. 实战:WebSocket 客户端示例
javascript
// webui/src/connection.ts
const ws = new WebSocket(`ws://${host}:${port}/ws`);
ws.onopen = () => {
ws.send(JSON.stringify({
type: "message",
content: "你好",
chat_id: "user1",
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === "stream_delta") {
// 流式:编辑原消息
updateMessage(data.event_data.content);
} else {
// 最终回复
showMessage(data.content);
}
};
7. 实战:断线重连
javascript
// webui/src/connection.ts
function connect() {
ws = new WebSocket(url);
ws.onclose = () => {
// 自动重连(指数退避)
setTimeout(connect, Math.min(30000, 1000 * 2 ** reconnectCount));
reconnectCount++;
};
}
服务端侧 :channel._connections[sender_id] 在 WebSocketDisconnect 时自动移除------下次客户端重连会重新注册。
8. 4 个核心决策
决策 1 · 为什么多路复用(每客户端一连接)而不是广播?
WebUI 是个人化 UI------每用户独立 session / 历史 / 上下文。广播会让"用户 A 看到用户 B 的回复"。
决策 2 · 为什么 WebSocket 通道不 start()?
HTTP server 启动后,FastAPI 端点 accept() 才算"通道启动"------start() 等待 accept() 而非主动 start。
决策 3 · 为什么 media 直接传 base64?
浏览器访问 WebUI 的图像无需 download 服务器------base64 inline 直接渲染。
决策 4 · 为什么 WebSocket 通道不用 pairing?
WebUI 在 localhost,只有本机用户能访问------pairing 机制对 WebUI 多余。
9. 4 个常见误区
误区 1 · WebSocket 断线会丢消息?
A :(未投递的 outbound 找不到对应 sender_id)。建议 WebUI 重连时拉 get_recent 历史补齐。
误区 2 · WebSocket 性能瓶颈?
A:单 gateway 100+ 并发 WebSocket 没问题;FastAPI + uvicorn 单进程 ~500 连接。
误区 3 · 流式 delta 顺序乱?
A :send_json 按 publish 顺序;但不同 sender_id 并发可能交错------前端按 chat_id 过滤。
误区 4 · WebSocket 通道支持跨域?
A :。WebUI 在 localhost OK;远程需 app.add_middleware(CORSMiddleware, ...)。
10. 小结
- WebSocket 通道 WebUI 与 gateway 的桥梁
- 多路复用 每客户端独立连接
- 流式
StreamDeltaEvent实时编辑 - 断线重连 客户端自动
本文要点速查
- WebSocket 通道 见 §2
- FastAPI /ws 端点 见 §3
- 协议 见 §5
- 下一步:第 25 章《Feishu / Weixin 复杂通道解析》------ 复杂通道的 5 个挑战
按角色推荐
- 聊天通道开发者:必读(WebSocket 通道模式必读)
- 前端开发者:必读(WebUI 怎么连 gateway)
- 系统架构师:必读
- LLM Agent 开发者:选读
- Tool / MCP 工具开发者:选读
下一步
- 第 25 章《Feishu / Weixin 复杂通道解析》 ------ 复杂通道的 5 个挑战(主题群"聊天通道",第 4 周)
tags :#nanobot #AI Agent #LLM #Python #源码解析 #WebSocket #WebUI