让多端同步看到 OpenClaw tool 事件:两种无需大改源码的实现方案

近期在围绕鼎道 PSUIP 等核心产品研发时,我们一直通过 OpenClaw 提升研发效率和产品体验,其灵活的事件分发能力为开发提供了关键支撑。但在实际落地适配中,常会观察到下面这种现象:

  • 某条 WS 连接发起一条消息后,这条连接自己能收到 stream: "tool" 的事件
  • 另一个独立的 WS 连接,即使连的是同一个 gateway、同一个 session,也可能收不到这条 tool 事件
  • 但这个独立 WS 连接通常仍然能收到:
    • assistant
    • lifecycle
    • chat
    • agent

这会让人误以为:

  • 是前端渲染出了问题
  • 是当前连接 gateway 的 WS 客户端没有正确解析 tool
  • 或者不同 WS 连接应该天然共享同一条 run 的所有事件

这一问题直接影响前端调试需要攻克的核心卡点。本文结合实际研发场景,深度解析该现象的底层逻辑,并给出适配我们业务场景的解决方案。

核心结论:OpenClaw 事件分发的默认语义

结合鼎道的研发场景拆解,我先明确核心结论:OpenClaw 后端对不同事件的分发策略存在本质差异,这也是导致上述问题的核心原因,而非鼎道产品层的代码问题:

  • 普通消息事件,例如 assistantlifecyclechat
    • 走广播逻辑
  • tool 事件
    • 走定向分发逻辑
    • 默认只发给"发起这次 run 的那条 WS 连接"

所以:

  • A 连接发起 chat.send
    • A 能收到这次 run 的 tool
    • B 通常收不到
  • B 连接发起 chat.send
    • B 能收到这次 run 的 tool
    • A 通常收不到

这并非异常,而是 OpenClaw 的默认设计语义,也是我们适配鼎道产品时需要重点关注的底层逻辑。

根本原因:从源码到鼎道场景的落地分析

tool 事件不走广播逻辑

OpenClaw 在处理 agent 事件时,会先判断事件类型是否为 tool,并分流处理:

ts 复制代码
const isToolEvent = evt.stream === "tool";

然后按两种路径处理:

  • 如果是 tool 事件,调用 broadcastToConnIds(...) 定向发送;
  • 如果不是 tool 事件,调用 broadcast(...) 广播。

这意味着,在 DingOS 和 PSUIP 的研发场景中,tool 是"连接级定向消息",而其他事件是"会话级广播消息",二者的分发范围天然不同。

tool 仅发给已注册的连接

OpenClaw 在处理 chat.send(对应 DingOS 智能助手的交互触发、PSUIP 的前端指令提交)时,会将发起请求的 connId 注册为本次 runIdtool 事件接收方,注册条件为:

  • 当前连接存在有效 connId
  • 连接在 connect.caps 中声明了 tool-events

满足条件时执行:

ts 复制代码
registerToolEventRecipient(runId, connId);

后续该 runId 产生的 tool 事件,仅会发送给这个注册的 connId------这也是鼎道不同研发页面收不到跨连接 tool 事件的核心原因。

tool 只发给已注册的 recipient

OpenClaw 在处理 chat.send 启动一次 run 时,把当前发起请求的 connId 注册到本次 runId 的 tool 事件接收方,注册条件为:。

  • 当前连接存在有效 connId
  • 连接在 connect.caps 里声明了 tool-events

只有满足这两个条件,后端才会执行:

ts 复制代码
registerToolEventRecipient(runId, connId)

后续该 runId 产生的 tool 事件,仅会发送给这个注册的 connId,这也是鼎道不同研发页面收不到跨连接 tool 事件的核心原因。

新 WS 连接会生成新 connId

在 OpenClaw 网关源码中,每次新建 WS 连接都会生成全新的 connId(UUID):

ts 复制代码
const connId = randomUUID();

这意味着,即便 DingOS 控制台和 PSUIP 调试页面使用相同的 gatewayUrltokendevice.idclient.id,只要是两次独立的 WS 连接,就会生成不同的 connId。例如:

  • DingOS 控制台的 WS 连接:connId=fd95xxxx
  • PSUIP 调试页面的 WS 连接:connId=abf396xxxx

二者属于完全独立的连接,自然无法共享定向分发的 tool 事件。

研发场景下的源码链路参考

结合 OpenClaw 的实际适配,我梳理了与该问题直接相关的核心源码位置,便于定位和调整:

chat.send 时注册 tool 接收方

文件:src/gateway/server-methods/chat.ts

关键逻辑:

ts 复制代码
const connId = typeof client?.connId === "string" ? client.connId : undefined;
const wantsToolEvents = hasGatewayClientCap(
  client?.connect?.caps,
  GATEWAY_CLIENT_CAPS.TOOL_EVENTS,
);
if (connId && wantsToolEvents) {
  context.registerToolEventRecipient(runId, connId);
}

在 DingOS 场景中,这段逻辑决定了"哪条连接发起智能助手交互,哪条连接就会被注册为 tool 接收方"。

tool 与普通消息分开发送

文件:src/gateway/server-chat.ts

关键逻辑:

ts 复制代码
const isToolEvent = evt.stream === "tool";
if (isToolEvent) {
  const recipients = toolEventRecipients.get(evt.runId);
  if (recipients && recipients.size > 0) {
    broadcastToConnIds("agent", toolPayload, recipients);
  }
} else {
  broadcast("agent", agentPayload);
}

这也是鼎道多端场景下,tool 事件仅单端可见的核心代码逻辑。

定向发送的核心实现

文件:src/gateway/server-broadcast.ts

关键逻辑:

ts 复制代码
const broadcastToConnIds = (event, payload, connIds, opts) => {
  if (connIds.size === 0) {
    return;
  }
  broadcastInternal(event, payload, opts, connIds);
};

该方法仅向指定 connIds 发送消息,而非所有在线客户端,直接导致多端无法共享 tool 事件。

为什么普通消息在多端能正常同步?

在 调试中,stream=assistantstream=lifecycleevent chat 等普通消息能正常同步,核心原因是这些事件走广播路径,不受 connId 限制。这也解释了研发过程中"文本回复多端可见,但工具调用仅单端可见"的现象------并非我们前端代码问题,而是 OpenClaw 后端分发策略的差异。

在研发场景下的日志排查方法

在调试相关功能时,可通过以下日志特征定位 tool 事件问题:

判断 tool 是否定向发送

日志中若出现:

text 复制代码
stream=tool ... targeted clients=2 targets=1

说明当前网关下有 2 条在线连接,但该 tool 仅定向发送给 1 条,符合默认逻辑。

定位发起 run 的连接

日志中若出现:

text 复制代码
[ws] ⇄ res ✓ chat.send ... conn=fd95...

说明本次 chat.sendfd95... 连接发起,后续 tool 仅会发送给该连接。

确认前端连接的 connId

日志中若出现:

text 复制代码
[ws] webchat connected conn=abf396...

说明当前调试页面的连接 ID 为 abf396...,若本次 run 由 fd95... 发起,则调试页面仅能收到广播事件,无法收到 tool

研发过程中的常见误区(鼎道场景适配)

误区一:同一 sessionKey 应共享 tool 事件

实际:OpenClaw 中 toolrunId -> connId 绑定分发,而非仅按 sessionKey 广播,需针对性调整。

误区二:相同 token/device/client 参数复用 connId

实际:connId 是连接级 UUID,每次新建连接都会重新生成,即便 调试页面参数完全一致,也会生成不同 connId

误区三:前端未显示 tool 就是渲染 Bug

实际:更常见的原因是后端仅将 tool 发送给发起 run 的连接,当前前端并非该连接,因此天然收不到。

公司业务场景下的解决方案

针对公司业务场景需求,我们梳理了三种适配方案,兼顾"无源码修改""快速魔改""长期适配"的不同诉求:

无源码修改:利用 session.tool 订阅能力

若不想修改 OpenClaw 核心源码,可借助 OpenClaw 源码层面的 session.tool 事件能力(非官方文档明确特性),适配 DingOS/PSUIP 的多端观测需求:

  1. WS 连接成功后,客户端主动调用订阅指令:
json 复制代码
{
  "method": "sessions.subscribe",
  "params": {}
}
  1. 前端监听 session.tool 事件(而非原生 tool 事件)。

该方案已在鼎道研发环境验证有效:调试页面通过订阅 session.tool,可同步接收 run 产生的工具生命周期事件,无需修改后端源码,快速解决联调过程中的信息断层问题。

快速魔改:调整 tool 事件分发逻辑

可直接修改 OpenClaw 安装包中的事件分发逻辑,让 tool 既定向发送又广播:

js 复制代码
if (isToolEvent) {
  if ((typeof evt.data?.phase === "string" ? evt.data.phase : "") === "start" && isControlUiVisible && sessionKey && !isAborted) {
    flushBufferedChatDeltaIfNeeded(sessionKey, clientRunId, evt.runId, evt.seq);
  }
  const recipients = toolEventRecipients.get(evt.runId);
  if (recipients && recipients.size > 0) {
    broadcastToConnIds("agent", sessionKey ? {
      ...toolPayload,
      ...buildSessionEventSnapshot(sessionKey)
    } : toolPayload, recipients);
  }
  // 新增广播逻辑,适配鼎道多端场景
  broadcast("agent", toolPayload);
}

该修改已在鼎道测试环境验证:

  • 原始逻辑下,只有发起当前 run 的连接能收到 tool
  • 打开 broadcast("agent", toolPayload) 后,其他 WS 客户端也能同步收到 tool

这说明当前问题并不是客户端无法解析 tool,而是后端默认没有把 tool 做广播。

总结

在鼎道各项产品的研发过程中,"不同 WS 连接无法共享 OpenClaw tool 事件"的问题,核心根源是 OpenClaw 对 tool 事件的定向分发设计------tool 仅发送给发起 run 的连接,且每次新 WS 连接都会生成新 connId

我们研发团队通过深入解析 OpenClaw 源码,结合自身业务场景,梳理出"无源码修改""快速魔改"等解决方案,既解决了当下多端调试的痛点,也为后续产品迭代提供了技术支撑。未来,鼎道智联将持续深耕智能交互、前端一体化等领域,结合 OpenClaw 等优秀开源技术的适配经验,不断优化 DingOS、PSUIP 等产品的研发效率与用户体验,同时积极回馈开源社区,推动技术生态的共同发展。

相关推荐
前端技术官4 小时前
OpenClaw 跟病毒的区别是什么?
ai agent·ai安全·openclaw·devops风险·chmod 777
sunywz7 小时前
【从零搭建物联网智能充电桩系统】4、Netty WebSocket 服务:小程序实时推送背后的长连接管理
物联网·websocket·小程序
AC赳赳老秦1 天前
OpenClaw 采集效率优化:并发控制、增量采集策略与服务器友好型设计
大数据·运维·服务器·人工智能·信息可视化·deepseek·openclaw
七夜zippoe1 天前
OpenClaw 错误处理实战:优雅降级、重试机制与熔断器设计
错误处理·重试机制·openclaw·优雅降级·熔断器
贾天佑忆月 迷失的昵2 天前
客户端与服务器持续同步解析(轮询,comet,WebSocket)
运维·服务器·websocket
李昊哲小课3 天前
fastapi sse websocket 奶茶店实时订单看板
人工智能·python·websocket·网络协议·fastapi·sse
撞强3 天前
OpenClaw 自定义Model Provider 的一键配置脚本
模型·maas·openclaw·小龙虾
李昊哲小课3 天前
fastapi sse websocket 智能家居实时控制台
python·websocket·智能家居·fastapi·sse
花生了什么事o3 天前
WebSocket 与 SSE:实时通信方案的选择
网络·websocket·网络协议
paopaokaka_luck3 天前
基于springboot3+vue3的乡村医生诊疗管理系统(AI助手、协同过滤算法、webSocket实时聊天、Echarts图形化分析)
前端·网络·人工智能·spring boot·websocket·网络协议·echarts