WebSocket 与 WebUI

对应源码

主题源码
WebSocket Channel 与协议nanobot/channels/websocket.py
WebUI 会话协调nanobot/session/webui_turns.py
WebUI 后端路由与运行数据nanobot/webui/
前端客户端与消息渲染webui/src/lib/nanobot-client.ts、webui/src/hooks/useNanobotStream.ts、webui/src/components/
WebSocket/WebUI 测试tests/channels/test_websocket_*.py、tests/webui/

学习目标

读完后,你应当能够:

  • 解释 WebUI 为什么依赖 WebSocket Channel 和 Gateway;
  • 区分 WebSocket 消息帧与 Runtime Event;
  • 理解多 chat multiplex、stream segment 和认证边界;
  • 找到 UI 状态协调的 Python 侧入口。

前置知识

  • 理解 ChannelManager;
  • 理解 RuntimeEventBus;
  • 知道 WebSocket 是双向长连接。

1. WebUI 不是 Gateway health 页面

默认端口:

表面默认端口
WebSocket Channel / WebUI8765
Gateway health18790
OpenAI-compatible API8900

WebUI 静态资源由 WebSocket Channel 相关 HTTP 服务提供。18790 主要是 Gateway health,不是浏览器工作台。

2. 结构

flowchart TD
    UI["React/TypeScript WebUI"] --> HTTP["静态资源与 WebUI HTTP API"]
    UI <--> WS["WebSocket Channel"]
    WS <--> BUS["MessageBus"]
    BUS <--> AL["AgentLoop"]
    AL --> RE["RuntimeEventBus"]
    RE --> WTC["WebuiTurnCoordinator"]
    WTC --> BUS
    HTTP --> GS["Gateway Services"]
    GS --> SM["Session / Cron / Skills / Settings"]

WebUI 同时使用:

  • WebSocket 对话和实时事件;
  • HTTP routes 管理设置、媒体、Skills、Apps、Cron 等;
  • Session 和 workspace metadata。

3. WebSocket 连接

连接 URL:

ws://<host>:<port><path>?client_id=<id>&token=<token>

认证可能使用:

  • static token;
  • 短期 issued token;
  • tokenIssueSecret;
  • websocketRequiresToken。

allowFrom 仍控制 client ID。

对外绑定 0.0.0.0 时,Gateway 会要求 token 或 token issue secret,避免无认证暴露。

4. 多 Chat Multiplex

一个 WebSocket connection 可以订阅多个 chat ID。客户端 envelope 包括:

  • new_chat
  • attach
  • message

服务端确认:

  • attached

这意味着:

  • connection ID 不等于 chat ID;
  • 一个浏览器连接可以承载多个 Session;
  • page reload 后可以重新 attach;
  • stream buffer 必须按 chat ID 和 stream ID 区分。

5. 主要服务端事件

事件作用
ready连接已建立
message完整回复
delta文本增量
stream_end当前文本 segment 结束
reasoning_delta推理增量
reasoning_end推理 segment 结束
runtime_model_updated运行模型发生变化
goal_state 等协调事件UI 状态同步
error软协议错误,连接可保持

具体协议字段以 官方 WebSocket 文档 和 nanobot/channels/websocket.py 为准。

6. Stream ID

同一个 chat 内可以出现多个 stream segment,例如:

模型先输出
  → 调用 Tool
  → 继续输出

因此 UI 不能只按 chat ID 把所有 delta 永久拼接。_stream_id 用于区分 segment,stream_end 关闭当前 segment。

7. Runtime Event 与 WebSocket Frame

RuntimeEventBus 发布通用 Python 事件:

  • Turn started;
  • run status;
  • completed;
  • goal changed;
  • runtime model changed。

WebuiTurnCoordinator 或 WebSocket adapter 将其转换为 UI 所需 frame。

Runtime Event:核心内部语义
WebSocket Frame:客户端 wire protocol
React state:最终展示状态

三层应保持分离。

8. WebUI Python 侧服务

nanobot/webui/ 包含:

  • settings routes/API;
  • Skills API;
  • MCP preset;
  • CLI Apps;
  • media;
  • file preview;
  • session list/index;
  • automations;
  • gateway token;
  • workspace;
  • token usage;
  • thread disk/display state。

这些不是全都经过 AgentLoop。例如修改设置或列出 Skills 是管理操作,不是用户 Agent Turn。

9. React 前端

源码位于 webui/。只有在修改浏览器展示时才需要深入:

  • Vite;
  • React;
  • TypeScript;
  • WebSocket client;
  • 状态和组件。

排查顺序:

  1. 核心 Turn 是否完成;
  2. Runtime Event 是否发布;
  3. WebSocket frame 是否送达;
  4. 浏览器是否接收;
  5. React 是否正确更新。

不要从 CSS 或组件开始猜后端状态。

10. WebUI 能力不等于安全授权

WebUI 可以选择 workspace 和本地 access mode,但不能绕过:

  • Gateway 配置;
  • Tool workspace restriction;
  • shell sandbox;
  • 操作系统权限;
  • Provider 权限;
  • SSRF;
  • token/allowFrom。

UI 控件是能力选择表面,不是新的安全根。

源码与测试

  • nanobot/channels/websocket.py
  • nanobot/session/webui_turns.py
  • nanobot/webui/
  • webui/
  • tests/channels/test_websocket_integration.py
  • tests/channels/test_websocket_http_routes.py
  • tests/channels/test_websocket_envelope_media.py
  • tests/webui/

自检

  1. 为什么 WebUI 在 8765,而 health 在 18790?
  2. connection ID、client ID 和 chat ID 有什么区别?
  3. Runtime Event 为什么不能直接当成 WebSocket JSON?
  4. 一个 chat 为什么需要多个 stream ID?
  5. WebUI access mode 为什么不能绕过 Tool 安全边界?

下一步

阅读 Provider 解析与 Fallback。