27.3 WebChat 实现

生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~160k tokens,输出 ~7k tokens(本节)


上一节剖析了控制台 UI 的整体架构。本节聚焦 WebChat——控制台中的聊天界面,它是用户直接与 AI 助手对话的主要入口。我们将分析 WebChat 作为 Gateway WebSocket 客户端的工作方式,以及聊天历史加载与消息发送的完整流程。

27.3.1 WebChat 作为 Gateway WS 客户端

WebChat 的定位

WebChat 并不是一个独立的聊天服务——它本质上是 Gateway WebSocket 协议的一个客户端,与 Telegram、Discord 等渠道处于同一层次。区别在于:

特征
WebChat
Telegram/Discord 等

连接方式

浏览器 WebSocket

服务端长连接/Webhook

客户端模式

mode: "webchat"

mode: "channel"

角色

role: "operator"

role: "channel"

认证

Ed25519 设备签名 + Token

API Token/OAuth

会话选择

用户可自由切换 sessionKey

由平台消息自动路由

WebChat 在连接 Gateway 时声明自己为 openclaw-control-ui 客户端,模式为 webchat,角色为 operator

// ui/src/ui/gateway.ts(连接参数)
const params = {
  minProtocol: 3,
  maxProtocol: 3,
  client: {
    id: "openclaw-control-ui",
    mode: "webchat",
    platform: navigator.platform ?? "web",
  },
  role: "operator",
  scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
};

operator 角色拥有完整的管理权限,包括管理员操作、执行审批、设备配对等。这意味着 WebChat 不仅能聊天,还能通过聊天命令(如 /new/stop)控制会话生命周期。

会话键(Session Key)

WebChat 的核心概念是 sessionKey——它决定了用户当前对话的上下文。sessionKey 是一个字符串标识符,例如 "main""agent:researcher:main" 等。用户可以在 UI 中自由切换会话。

切换 sessionKey 时,UI 会清空当前的聊天草稿、流式内容、运行状态和工具流,然后重新加载对应会话的历史消息和头像:

事件驱动的聊天更新

发送消息后,UI 不会同步等待完整的回复。而是通过 Gateway 推送的 chat 事件实时更新界面:

事件状态机的转换:

delta 事件的处理有一个巧妙的设计——它不是追加文本,而是替换为更长的文本:

这种"全量替换"模式避免了流式传输中 delta 包乱序导致的文本错乱问题。

跨运行事件过滤

一个细节是 handleChatEvent 会检查事件的 runId 是否匹配当前运行:

当一个代理在后台运行完成后通过 announce 发送消息时,当前 WebChat 会收到 final 事件但 runId 不匹配。此时 UI 不更新流式状态,但仍然触发聊天历史刷新——确保用户能看到新消息。

27.3.2 聊天历史加载与消息发送

历史加载

聊天历史通过 chat.history RPC 方法从 Gateway 获取:

返回的 thinkingLevel 用于确定当前会话是否启用了推理/思考模式——如果 reasoningLevel 不是 "off",UI 会显示 thinking 相关的 token 输出。

消息渲染管线

加载的原始消息经过一条 规范化 → 分组 → 渲染 的管线:

消息规范化是关键一步。Gateway 返回的消息可能有多种格式(来自不同渠道或 API 版本):

消息分组将连续同角色的消息合并为一个 MessageGroup,渲染时共享头像和时间戳:

消息发送流程

发送一条消息的完整流程涉及三个层次:

第一层:命令识别与队列

消息队列机制让用户可以在助手回复过程中继续输入。队列中的消息在当前轮次完成后自动逐条发送:

衍生解释 — 乐观更新(Optimistic Update)

在网络应用中,"乐观更新"是指在服务端确认之前就先更新 UI。用户发送消息时,消息立即显示在聊天列表中(而非等待 Gateway 响应),如果发送失败再回滚。这让界面感觉更流畅,消除了网络延迟带来的"卡顿感"。

第二层:乐观更新与 RPC 调用

关键参数解析:

  • deliver: false——WebChat 的消息只在 Gateway 内部处理,不投递到其他渠道

  • idempotencyKey: runId——如果网络抖动导致请求重传,Gateway 可以用此键去重

图片附件

WebChat 支持通过粘贴(Ctrl+V)添加图片附件:

图片以 Data URL(Base64 编码)形式暂存在客户端。发送时转换为 API 格式:

中止与命令

用户可以通过多种方式中止正在进行的对话:

方式
触发

输入 stop/abort/esc/wait/exit

isChatStopCommand 识别

输入 /stop

命令前缀

点击 "Stop" 按钮

canAbort && chatRunId

中止操作调用 chat.abort RPC:

聊天视图布局

renderChat 函数组合了整个聊天界面的布局:

聊天线程和侧边栏之间通过可拖拽分隔器 <resizable-divider> 分割,比例保存在 splitRatio 中(默认 0.6,范围 0.4-0.7)。侧边栏用于展示工具调用的输出内容——点击聊天中的工具卡片即可在侧边栏中查看完整输出。

聊天线程内的渲染使用 Lit 的 repeat() 指令进行高效列表更新——相比简单的 map()repeat() 基于 key 进行 DOM 复用,避免在消息追加时重新创建所有已有消息的 DOM 节点。

流式输出展示

当助手正在回复时,UI 会根据 chatStream 的状态显示不同的指示器:

"正在思考"指示器是三个跳动的点(CSS 动画),给用户一个视觉反馈表明助手正在处理。一旦第一个 delta 到达,指示器被替换为实际的流式文本。


本节小结

  1. WebChat 是 Gateway WebSocket 协议的一个客户端,以 operator 角色连接,拥有完整管理权限;会话通过 sessionKey 标识和切换。

  2. 事件驱动更新:消息发送后通过 chat 事件接收流式 delta、最终结果、中止和错误通知,delta 采用全量替换模式防止乱序。

  3. 消息发送三层架构:UI 层处理命令识别和消息队列、控制层执行乐观更新和工具流重置、网络层发起 chat.send RPC 调用。

  4. 消息渲染管线:原始消息 → 规范化(统一格式)→ 分组(连续同角色合并)→ Lit 模板渲染(Markdown + 头像 + 工具卡片)。

  5. 消息队列允许用户在助手回复过程中继续输入,队列消息在当前轮次完成后自动发送。

  6. 图片附件通过剪贴板粘贴添加,以 Base64 Data URL 形式暂存,发送时转换为 API 格式。

Last updated