生成模型: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 并不是一个独立的聊天服务——它本质上是 Gateway WebSocket 协议的一个客户端,与 Telegram、Discord 等渠道处于同一层次。区别在于:
特征
WebChat
Telegram/Discord 等
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
中止操作调用 chat.abort RPC:
renderChat 函数组合了整个聊天界面的布局:
聊天线程和侧边栏之间通过可拖拽分隔器 <resizable-divider> 分割,比例保存在 splitRatio 中(默认 0.6,范围 0.4-0.7)。侧边栏用于展示工具调用的输出内容——点击聊天中的工具卡片即可在侧边栏中查看完整输出。
聊天线程内的渲染使用 Lit 的 repeat() 指令进行高效列表更新——相比简单的 map(),repeat() 基于 key 进行 DOM 复用,避免在消息追加时重新创建所有已有消息的 DOM 节点。
当助手正在回复时,UI 会根据 chatStream 的状态显示不同的指示器:
"正在思考"指示器是三个跳动的点(CSS 动画),给用户一个视觉反馈表明助手正在处理。一旦第一个 delta 到达,指示器被替换为实际的流式文本。
WebChat 是 Gateway WebSocket 协议的一个客户端,以 operator 角色连接,拥有完整管理权限;会话通过 sessionKey 标识和切换。
事件驱动更新:消息发送后通过 chat 事件接收流式 delta、最终结果、中止和错误通知,delta 采用全量替换模式防止乱序。
消息发送三层架构:UI 层处理命令识别和消息队列、控制层执行乐观更新和工具流重置、网络层发起 chat.send RPC 调用。
消息渲染管线:原始消息 → 规范化(统一格式)→ 分组(连续同角色合并)→ Lit 模板渲染(Markdown + 头像 + 工具卡片)。
消息队列允许用户在助手回复过程中继续输入,队列消息在当前轮次完成后自动发送。
图片附件通过剪贴板粘贴添加,以 Base64 Data URL 形式暂存,发送时转换为 API 格式。