OpenClaw 节点方法调用:跨设备能力调用实战

目录

    • 摘要
    • [一、背景:为什么需要节点方法调用 🤔](#一、背景:为什么需要节点方法调用 🤔)
    • [二、方法调用基础:invoke 工具使用 🛠️](#二、方法调用基础:invoke 工具使用 🛠️)
      • [2.1 核心概念](#2.1 核心概念)
      • [2.2 CLI 方式调用](#2.2 CLI 方式调用)
      • [2.3 工具层调用(Agent 上下文)](#2.3 工具层调用(Agent 上下文))
      • [2.4 节点选择策略](#2.4 节点选择策略)
    • [三、参数传递机制 📦](#三、参数传递机制 📦)
      • [3.1 输入参数(Input Parameters)](#3.1 输入参数(Input Parameters))
      • [3.2 输出参数(Output/Response)](#3.2 输出参数(Output/Response))
      • [3.3 大型数据传输](#3.3 大型数据传输)
      • [3.4 参数校验与类型安全](#3.4 参数校验与类型安全)
    • [四、回调与事件处理 🔄](#四、回调与事件处理 🔄)
      • [4.1 异步回调模式](#4.1 异步回调模式)
      • [4.2 事件订阅](#4.2 事件订阅)
      • [4.3 常见事件类型](#4.3 常见事件类型)
      • [4.4 事件节流与过滤](#4.4 事件节流与过滤)
    • [五、错误处理:调用失败与重试 ⚠️](#五、错误处理:调用失败与重试 ⚠️)
      • [5.1 错误分类](#5.1 错误分类)
      • [5.2 标准错误码](#5.2 标准错误码)
      • [5.3 重试策略](#5.3 重试策略)
      • [5.4 超时配置](#5.4 超时配置)
    • [六、实战案例 1:跨设备数据同步 🔄](#六、实战案例 1:跨设备数据同步 🔄)
      • [6.1 场景描述](#6.1 场景描述)
      • [6.2 实现代码](#6.2 实现代码)
    • [七、实战案例 2:远程函数执行 🖥️](#七、实战案例 2:远程函数执行 🖥️)
      • [7.1 场景描述](#7.1 场景描述)
      • [7.2 实现代码](#7.2 实现代码)
      • [7.3 审批与安全](#7.3 审批与安全)
    • [八、实战案例 3:设备间消息传递 📨](#八、实战案例 3:设备间消息传递 📨)
      • [8.1 场景描述](#8.1 场景描述)
      • [8.2 架构设计](#8.2 架构设计)
      • [8.3 实现代码](#8.3 实现代码)
    • [九、进阶话题 🚀](#九、进阶话题 🚀)
      • [9.1 连接生命周期管理](#9.1 连接生命周期管理)
      • [9.2 能力协商(Capability Negotiation)](#9.2 能力协商(Capability Negotiation))
      • [9.3 并发控制](#9.3 并发控制)
      • [9.4 性能优化建议](#9.4 性能优化建议)
    • [十、安全考量 🔒](#十、安全考量 🔒)
      • [10.1 端到端信任链](#10.1 端到端信任链)
      • [10.2 权限最小化原则](#10.2 权限最小化原则)
      • [10.3 Token 轮换与安全](#10.3 Token 轮换与安全)
    • [十一、调试与排错 🔍](#十一、调试与排错 🔍)
      • [11.1 常用诊断命令](#11.1 常用诊断命令)
      • [11.2 常见问题速查表](#11.2 常见问题速查表)
    • [十二、总结 📝](#十二、总结 📝)
    • 参考资料

摘要

OpenClaw 的节点(Node)机制是其多设备协作的核心能力之一。通过节点方法调用(node.invoke),Gateway 可以将指令转发到配对的 iOS、Android、macOS 或无头节点上执行,实现跨设备的摄像头拍摄、Canvas 渲染、Shell 命令运行等操作。本文从方法调用基础入手,系统讲解 invoke 工具的使用方式、参数传递机制、回调与事件处理流程,并结合跨设备数据同步、远程函数执行、设备间消息传递三大实战案例,帮助开发者全面掌握 OpenClaw 节点调用的设计与实现。文章同时覆盖错误处理、重试策略和事件订阅等进阶话题,助你在生产环境中构建稳定的跨设备协作方案。


一、背景:为什么需要节点方法调用 🤔

在传统的 AI Agent 架构中,模型与工具运行在同一台主机上。但随着应用场景的扩展,我们越来越多地需要:

  • 在手机上拍照并发送给云端 Agent 分析
  • 在 Mac 上执行编译命令,而 Gateway 跑在远程服务器
  • 在平板上渲染 Canvas 画面,让 Agent 获取视觉上下文
  • 在多台设备间同步状态,实现真正的分布式 Agent 网络

OpenClaw 通过 Node + Gateway 架构解决了这个问题。Gateway 是控制平面,Node 是能力宿主。方法调用就是两者之间的通信桥梁。
#mermaid-svg-ZtFPSlUwBv5us94w{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZtFPSlUwBv5us94w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZtFPSlUwBv5us94w .error-icon{fill:#552222;}#mermaid-svg-ZtFPSlUwBv5us94w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZtFPSlUwBv5us94w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZtFPSlUwBv5us94w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZtFPSlUwBv5us94w .marker.cross{stroke:#333333;}#mermaid-svg-ZtFPSlUwBv5us94w svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZtFPSlUwBv5us94w p{margin:0;}#mermaid-svg-ZtFPSlUwBv5us94w .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZtFPSlUwBv5us94w .cluster-label text{fill:#333;}#mermaid-svg-ZtFPSlUwBv5us94w .cluster-label span{color:#333;}#mermaid-svg-ZtFPSlUwBv5us94w .cluster-label span p{background-color:transparent;}#mermaid-svg-ZtFPSlUwBv5us94w .label text,#mermaid-svg-ZtFPSlUwBv5us94w span{fill:#333;color:#333;}#mermaid-svg-ZtFPSlUwBv5us94w .node rect,#mermaid-svg-ZtFPSlUwBv5us94w .node circle,#mermaid-svg-ZtFPSlUwBv5us94w .node ellipse,#mermaid-svg-ZtFPSlUwBv5us94w .node polygon,#mermaid-svg-ZtFPSlUwBv5us94w .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZtFPSlUwBv5us94w .rough-node .label text,#mermaid-svg-ZtFPSlUwBv5us94w .node .label text,#mermaid-svg-ZtFPSlUwBv5us94w .image-shape .label,#mermaid-svg-ZtFPSlUwBv5us94w .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZtFPSlUwBv5us94w .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZtFPSlUwBv5us94w .rough-node .label,#mermaid-svg-ZtFPSlUwBv5us94w .node .label,#mermaid-svg-ZtFPSlUwBv5us94w .image-shape .label,#mermaid-svg-ZtFPSlUwBv5us94w .icon-shape .label{text-align:center;}#mermaid-svg-ZtFPSlUwBv5us94w .node.clickable{cursor:pointer;}#mermaid-svg-ZtFPSlUwBv5us94w .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZtFPSlUwBv5us94w .arrowheadPath{fill:#333333;}#mermaid-svg-ZtFPSlUwBv5us94w .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZtFPSlUwBv5us94w .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZtFPSlUwBv5us94w .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZtFPSlUwBv5us94w .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZtFPSlUwBv5us94w .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZtFPSlUwBv5us94w .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZtFPSlUwBv5us94w .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZtFPSlUwBv5us94w .cluster text{fill:#333;}#mermaid-svg-ZtFPSlUwBv5us94w .cluster span{color:#333;}#mermaid-svg-ZtFPSlUwBv5us94w div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZtFPSlUwBv5us94w .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZtFPSlUwBv5us94w rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZtFPSlUwBv5us94w .icon-shape,#mermaid-svg-ZtFPSlUwBv5us94w .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZtFPSlUwBv5us94w .icon-shape p,#mermaid-svg-ZtFPSlUwBv5us94w .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZtFPSlUwBv5us94w .icon-shape .label rect,#mermaid-svg-ZtFPSlUwBv5us94w .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZtFPSlUwBv5us94w .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZtFPSlUwBv5us94w .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZtFPSlUwBv5us94w :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Nodes(能力宿主)
Gateway(控制平面)
node.invoke
node.invoke
node.invoke
node.invoke
AI Agent
Session Manager
Node Router
iOS Node

camera/canvas/location
Android Node

camera/canvas/voice
macOS Node

system.run/canvas
Headless Node

system.run/exec


二、方法调用基础:invoke 工具使用 🛠️

2.1 核心概念

node.invoke 是 OpenClaw 节点通信的底层 RPC 机制。它基于 Gateway WebSocket 协议,采用请求-响应模式,每条消息都是 JSON 帧。

协议帧格式:

字段 类型 说明
type string 帧类型:req(请求)/ res(响应)/ event(事件)
id string 请求 ID,用于匹配响应
method string 调用方法名,如 canvas.evalcamera.snap
params object 调用参数
ok boolean 响应是否成功(仅 res 帧)
payload any 成功时的返回数据(仅 res 帧)
error object 失败时的错误信息(仅 res 帧)

2.2 CLI 方式调用

最直接的方式是通过 CLI 调用节点方法:

bash 复制代码
# 查看所有在线节点
openclaw nodes status

# 调用节点的 canvas.eval 方法
openclaw nodes invoke \
  --node "my-iphone" \
  --command canvas.eval \
  --params '{"javaScript": "document.title"}'

# 调用节点的 camera.snap 方法
openclaw nodes invoke \
  --node "my-android" \
  --command camera.snap \
  --params '{"facing": "back", "quality": 0.8}'

# 调用节点的 system.run 方法(远程执行命令)
openclaw nodes invoke \
  --node "build-mac" \
  --command system.run \
  --params '{"cmd": "git status", "cwd": "/project/repo"}'

💡 解读: 上面三条命令分别展示了三种典型的节点调用场景------浏览器上下文获取、设备硬件操作、远程命令执行。--node 参数接受节点 ID、名称或 IP 地址,--command 是方法全名(命名空间.方法),--params 是 JSON 格式的参数对象。

2.3 工具层调用(Agent 上下文)

在 Agent 的工具上下文中,exec 工具可以通过 host=node 参数将命令路由到指定节点执行:

json 复制代码
{
  "tool": "exec",
  "params": {
    "command": "npm test",
    "host": "node",
    "node": "build-mac",
    "security": "allowlist"
  }
}

这比直接使用 node.invoke 更自然,因为它复用了 Agent 已经熟悉的 exec 工具接口,只是将执行目标从本地切换到远程节点。

2.4 节点选择策略

当 Gateway 连接了多个节点时,方法调用需要明确目标节点。OpenClaw 支持多种节点定位方式:

定位方式 示例 优先级 说明
精确 ID --node "n_abc123" 最高 节点的唯一标识符
显示名称 --node "Build Node" 通过 --display-name 设置的友好名称
IP 地址 --node "192.168.1.50" 适合内网固定 IP 场景
默认节点 配置 tools.exec.node 全局或会话级默认

最佳实践: 在生产环境中,推荐使用显示名称而非 ID,因为名称更可读且在节点重配后保持稳定。可以通过 openclaw nodes rename 修改节点名称。


三、参数传递机制 📦

3.1 输入参数(Input Parameters)

节点方法的输入参数通过 params 字段传递,是一个 JSON 对象。不同命令有不同的参数规范:

json 复制代码
// canvas.eval --- 在节点浏览器中执行 JavaScript
{
  "javaScript": "document.querySelector('.title').textContent",
  "timeoutMs": 5000
}

// camera.snap --- 拍摄照片
{
  "facing": "back",        // "front" | "back"
  "quality": 0.8,          // 0.0 ~ 1.0
  "flash": "auto",         // "on" | "off" | "auto"
  "metadata": true         // 是否附带 EXIF 信息
}

// system.run --- 远程执行命令
{
  "cmd": "python3 train.py --epochs 100",
  "cwd": "/home/user/project",
  "env": { "CUDA_VISIBLE_DEVICES": "0" },
  "timeout": 300
}

💡 解读: 参数结构体设计遵循"必要参数精简、可选参数丰富"的原则。canvas.eval 只要求 javaScript 一个必填参数;camera.snapfacingquality 都有合理默认值;system.run 支持环境变量注入和超时控制,满足远程执行的安全需求。

3.2 输出参数(Output/Response)

节点方法的响应同样遵循标准帧格式。成功时 ok: true,数据在 payload 中;失败时 ok: false,错误在 error 中:

json 复制代码
// 成功响应 --- canvas.eval
{
  "type": "res",
  "id": "req_7f3a",
  "ok": true,
  "payload": {
    "result": "My Page Title",
    "type": "string"
  }
}

// 失败响应 --- camera.snap(权限被拒)
{
  "type": "res",
  "id": "req_8b2c",
  "ok": false,
  "error": {
    "code": "PERMISSION_DENIED",
    "message": "Camera access denied by user",
    "details": {
      "permission": "camera.capture",
      "required": true
    }
  }
}

3.3 大型数据传输

当返回数据较大(如截图、录屏文件)时,OpenClaw 采用分块传输 + 引用模式:
Node Gateway Node Gateway #mermaid-svg-geI3E0w6JPmRcEOx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-geI3E0w6JPmRcEOx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-geI3E0w6JPmRcEOx .error-icon{fill:#552222;}#mermaid-svg-geI3E0w6JPmRcEOx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-geI3E0w6JPmRcEOx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-geI3E0w6JPmRcEOx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-geI3E0w6JPmRcEOx .marker.cross{stroke:#333333;}#mermaid-svg-geI3E0w6JPmRcEOx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-geI3E0w6JPmRcEOx p{margin:0;}#mermaid-svg-geI3E0w6JPmRcEOx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-geI3E0w6JPmRcEOx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-geI3E0w6JPmRcEOx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-geI3E0w6JPmRcEOx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-geI3E0w6JPmRcEOx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-geI3E0w6JPmRcEOx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-geI3E0w6JPmRcEOx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-geI3E0w6JPmRcEOx .sequenceNumber{fill:white;}#mermaid-svg-geI3E0w6JPmRcEOx #sequencenumber{fill:#333;}#mermaid-svg-geI3E0w6JPmRcEOx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-geI3E0w6JPmRcEOx .messageText{fill:#333;stroke:none;}#mermaid-svg-geI3E0w6JPmRcEOx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-geI3E0w6JPmRcEOx .labelText,#mermaid-svg-geI3E0w6JPmRcEOx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-geI3E0w6JPmRcEOx .loopText,#mermaid-svg-geI3E0w6JPmRcEOx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-geI3E0w6JPmRcEOx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-geI3E0w6JPmRcEOx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-geI3E0w6JPmRcEOx .noteText,#mermaid-svg-geI3E0w6JPmRcEOx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-geI3E0w6JPmRcEOx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-geI3E0w6JPmRcEOx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-geI3E0w6JPmRcEOx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-geI3E0w6JPmRcEOx .actorPopupMenu{position:absolute;}#mermaid-svg-geI3E0w6JPmRcEOx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-geI3E0w6JPmRcEOx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-geI3E0w6JPmRcEOx .actor-man circle,#mermaid-svg-geI3E0w6JPmRcEOx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-geI3E0w6JPmRcEOx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} node.invoke camera.snap 拍照并生成临时文件 res { payload: { ref: "media://snap_abc", size: 2048576, mimeType: "image/jpeg" } } 将 media ref 绑定到 session 附件 Agent 接收到 image attachment

节点不会在 JSON 帧中直接内联二进制数据,而是返回一个 media:// 引用。Gateway 负责将引用解析为实际的媒体附件,附加到当前 Agent 会话中。这种设计保证了协议帧的轻量性,同时支持任意大小的媒体传输。

3.4 参数校验与类型安全

Gateway 在转发调用前会对参数进行基本校验:

校验项 说明 失败行为
必填参数缺失 canvas.eval 缺少 javaScript 返回 INVALID_PARAMS 错误
参数类型错误 quality 传入字符串而非数字 返回 INVALID_PARAMS 错误
参数值越界 quality 超出 0~1 范围 返回 INVALID_PARAMS 错误
节点不支持该方法 如对 iOS 节点调用 system.run 返回 METHOD_NOT_FOUND 错误
权限不足 如调用 system.run 但只有 operator.write 权限 返回 PERMISSION_DENIED 错误

四、回调与事件处理 🔄

4.1 异步回调模式

部分节点方法是异步的,执行时间可能较长(如视频录制、大文件传输)。OpenClaw 的回调机制基于事件帧(type: "event"),节点在方法执行过程中可以主动推送事件到 Gateway:
Node Gateway Agent Node Gateway Agent #mermaid-svg-jOdm2DgX5QZkCYxH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jOdm2DgX5QZkCYxH .error-icon{fill:#552222;}#mermaid-svg-jOdm2DgX5QZkCYxH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jOdm2DgX5QZkCYxH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jOdm2DgX5QZkCYxH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jOdm2DgX5QZkCYxH .marker.cross{stroke:#333333;}#mermaid-svg-jOdm2DgX5QZkCYxH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jOdm2DgX5QZkCYxH p{margin:0;}#mermaid-svg-jOdm2DgX5QZkCYxH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jOdm2DgX5QZkCYxH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jOdm2DgX5QZkCYxH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jOdm2DgX5QZkCYxH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jOdm2DgX5QZkCYxH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jOdm2DgX5QZkCYxH .sequenceNumber{fill:white;}#mermaid-svg-jOdm2DgX5QZkCYxH #sequencenumber{fill:#333;}#mermaid-svg-jOdm2DgX5QZkCYxH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jOdm2DgX5QZkCYxH .messageText{fill:#333;stroke:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jOdm2DgX5QZkCYxH .labelText,#mermaid-svg-jOdm2DgX5QZkCYxH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .loopText,#mermaid-svg-jOdm2DgX5QZkCYxH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jOdm2DgX5QZkCYxH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jOdm2DgX5QZkCYxH .noteText,#mermaid-svg-jOdm2DgX5QZkCYxH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jOdm2DgX5QZkCYxH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jOdm2DgX5QZkCYxH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jOdm2DgX5QZkCYxH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jOdm2DgX5QZkCYxH .actorPopupMenu{position:absolute;}#mermaid-svg-jOdm2DgX5QZkCYxH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jOdm2DgX5QZkCYxH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jOdm2DgX5QZkCYxH .actor-man circle,#mermaid-svg-jOdm2DgX5QZkCYxH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jOdm2DgX5QZkCYxH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 录制进行中... 调用 screen.record node.invoke screen.record { duration: 30 } res { ok: true, payload: { recordingId: "rec_1" } } event "recording.progress" { recordingId: "rec_1", elapsed: 10, total: 30 } event "recording.progress" { recordingId: "rec_1", elapsed: 20, total: 30 } event "recording.complete" { recordingId: "rec_1", ref: "media://rec_1" } 推送完成事件 + 媒体附件

4.2 事件订阅

Gateway 可以订阅节点的特定事件类型,实现设备状态变化的实时感知:

json 复制代码
// 订阅节点位置变化事件
{
  "type": "req",
  "id": "sub_001",
  "method": "node.subscribe",
  "params": {
    "node": "my-iphone",
    "events": ["location.changed", "battery.changed"],
    "options": {
      "throttleMs": 5000,
      "includeInitial": true
    }
  }
}

事件帧结构:

json 复制代码
{
  "type": "event",
  "event": "location.changed",
  "payload": {
    "latitude": 39.9042,
    "longitude": 116.4074,
    "accuracy": 10.0,
    "timestamp": 1737264000000
  },
  "seq": 42,
  "stateVersion": 7
}

seq 是单调递增的事件序列号,stateVersion 是节点状态版本,用于乐观并发控制和事件去重。

4.3 常见事件类型

事件名 触发条件 Payload 关键字段
location.changed GPS 位置更新 latitude, longitude, accuracy
battery.changed 电池状态变化 level, charging, lowPower
network.changed 网络连接变化 type, connected, ssid
recording.progress 录制进度 recordingId, elapsed, total
recording.complete 录制完成 recordingId, ref
command.stdout 远程命令输出 execId, data, stream
command.exit 远程命令退出 execId, exitCode, signal

4.4 事件节流与过滤

对于高频事件(如位置更新、命令输出),OpenClaw 提供了节流和过滤机制:

  • throttleMs:最小事件间隔,避免洪泛。如位置事件每 5 秒最多推送一次。
  • debounceMs:事件停止后延迟触发。适合"用户停止移动后才上报最终位置"的场景。
  • filter:基于条件表达式过滤。如只上报电量低于 20% 的电池事件。
json 复制代码
{
  "method": "node.subscribe",
  "params": {
    "node": "my-iphone",
    "events": ["battery.changed"],
    "options": {
      "filter": "payload.level < 20",
      "throttleMs": 60000
    }
  }
}

💡 解读: 事件订阅不是"全量推送"。通过合理配置 throttleMsfilter,可以在保持实时性的同时大幅降低网络和计算开销。生产环境中建议对高频事件始终设置节流阈值。


五、错误处理:调用失败与重试 ⚠️

5.1 错误分类

节点调用错误按来源可分为三类:
#mermaid-svg-1hyamrCxfI94yUsp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1hyamrCxfI94yUsp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1hyamrCxfI94yUsp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1hyamrCxfI94yUsp .error-icon{fill:#552222;}#mermaid-svg-1hyamrCxfI94yUsp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1hyamrCxfI94yUsp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1hyamrCxfI94yUsp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1hyamrCxfI94yUsp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1hyamrCxfI94yUsp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1hyamrCxfI94yUsp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1hyamrCxfI94yUsp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1hyamrCxfI94yUsp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1hyamrCxfI94yUsp .marker.cross{stroke:#333333;}#mermaid-svg-1hyamrCxfI94yUsp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1hyamrCxfI94yUsp p{margin:0;}#mermaid-svg-1hyamrCxfI94yUsp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1hyamrCxfI94yUsp .cluster-label text{fill:#333;}#mermaid-svg-1hyamrCxfI94yUsp .cluster-label span{color:#333;}#mermaid-svg-1hyamrCxfI94yUsp .cluster-label span p{background-color:transparent;}#mermaid-svg-1hyamrCxfI94yUsp .label text,#mermaid-svg-1hyamrCxfI94yUsp span{fill:#333;color:#333;}#mermaid-svg-1hyamrCxfI94yUsp .node rect,#mermaid-svg-1hyamrCxfI94yUsp .node circle,#mermaid-svg-1hyamrCxfI94yUsp .node ellipse,#mermaid-svg-1hyamrCxfI94yUsp .node polygon,#mermaid-svg-1hyamrCxfI94yUsp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1hyamrCxfI94yUsp .rough-node .label text,#mermaid-svg-1hyamrCxfI94yUsp .node .label text,#mermaid-svg-1hyamrCxfI94yUsp .image-shape .label,#mermaid-svg-1hyamrCxfI94yUsp .icon-shape .label{text-anchor:middle;}#mermaid-svg-1hyamrCxfI94yUsp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1hyamrCxfI94yUsp .rough-node .label,#mermaid-svg-1hyamrCxfI94yUsp .node .label,#mermaid-svg-1hyamrCxfI94yUsp .image-shape .label,#mermaid-svg-1hyamrCxfI94yUsp .icon-shape .label{text-align:center;}#mermaid-svg-1hyamrCxfI94yUsp .node.clickable{cursor:pointer;}#mermaid-svg-1hyamrCxfI94yUsp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1hyamrCxfI94yUsp .arrowheadPath{fill:#333333;}#mermaid-svg-1hyamrCxfI94yUsp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1hyamrCxfI94yUsp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1hyamrCxfI94yUsp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1hyamrCxfI94yUsp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1hyamrCxfI94yUsp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1hyamrCxfI94yUsp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1hyamrCxfI94yUsp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1hyamrCxfI94yUsp .cluster text{fill:#333;}#mermaid-svg-1hyamrCxfI94yUsp .cluster span{color:#333;}#mermaid-svg-1hyamrCxfI94yUsp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1hyamrCxfI94yUsp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1hyamrCxfI94yUsp rect.text{fill:none;stroke-width:0;}#mermaid-svg-1hyamrCxfI94yUsp .icon-shape,#mermaid-svg-1hyamrCxfI94yUsp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1hyamrCxfI94yUsp .icon-shape p,#mermaid-svg-1hyamrCxfI94yUsp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1hyamrCxfI94yUsp .icon-shape .label rect,#mermaid-svg-1hyamrCxfI94yUsp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1hyamrCxfI94yUsp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1hyamrCxfI94yUsp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1hyamrCxfI94yUsp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用错误
Gateway 端错误
传输层错误
Node 端错误
节点不在线
方法不存在
参数校验失败
权限不足
连接超时
帧过大
WebSocket 断开
执行超时
资源不可用
运行时异常
系统限制

5.2 标准错误码

错误码 类别 说明 可重试
NODE_NOT_FOUND Gateway 节点不在线或未配对 ✅ 节点上线后
METHOD_NOT_FOUND Gateway 节点不支持该方法
INVALID_PARAMS Gateway 参数校验失败
PERMISSION_DENIED Gateway 权限不足 ❌(需授权)
TIMEOUT 传输/Node 调用超时 ✅(带退避)
CONNECTION_LOST 传输 WebSocket 断开 ✅ 重连后
EXEC_FAILED Node 远程执行失败 ⚠️ 视具体错误
RESOURCE_BUSY Node 资源被占用 ✅(带退避)

5.3 重试策略

OpenClaw 内置了指数退避重试机制,开发者也可自定义策略:

javascript 复制代码
// 自定义重试策略示例
class NodeInvokeRetry {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3;
    this.baseDelay = options.baseDelay || 1000;   // 1s
    this.maxDelay = options.maxDelay || 30000;     // 30s
    this.jitter = options.jitter || true;          // 启用抖动
  }

  async invoke(node, command, params) {
    let lastError;
    
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        const result = await this.rawInvoke(node, command, params);
        return result;
      } catch (error) {
        lastError = error;
        
        // 不可重试的错误立即抛出
        if (!this.isRetryable(error)) {
          throw error;
        }
        
        // 最后一次尝试不再等待
        if (attempt === this.maxRetries) break;
        
        // 计算退避延迟
        const delay = this.calculateDelay(attempt);
        console.warn(
          `节点调用失败 (尝试 ${attempt + 1}/${this.maxRetries}), ` +
          `${delay}ms 后重试...`, error
        );
        await this.sleep(delay);
      }
    }
    
    throw lastError;
  }

  calculateDelay(attempt) {
    const exponential = this.baseDelay * Math.pow(2, attempt);
    const capped = Math.min(exponential, this.maxDelay);
    if (this.jitter) {
      return capped * (0.5 + Math.random() * 0.5);
    }
    return capped;
  }

  isRetryable(error) {
    const retryableCodes = [
      'TIMEOUT', 'CONNECTION_LOST', 'RESOURCE_BUSY', 'NODE_NOT_FOUND'
    ];
    return retryableCodes.includes(error.code);
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

💡 解读: 重试策略的关键在于区分可重试和不可重试错误。NODE_NOT_FOUND 是可重试的(节点可能暂时离线),但 PERMISSION_DENIED 不应该重试(除非用户重新授权)。指数退避 + 随机抖动可以避免"惊群效应",防止多个客户端在同一时刻重试导致节点过载。

5.4 超时配置

超时是多层次的,每一层都有独立配置:

层次 配置项 默认值 说明
WebSocket 帧级 policy.maxPayload 26214400 (25MB) 单帧最大载荷
调用级 params.timeoutMs 方法相关 单次调用超时
会话级 tools.exec.timeout 无限制 会话全局超时
节点级 节点心跳 15s 节点在线判定

六、实战案例 1:跨设备数据同步 🔄

6.1 场景描述

假设我们有一个多设备协作场景:用户在手机上拍摄文档照片,Gateway 上的 Agent 调用 OCR 服务提取文字,然后将结果同步到 Mac 节点上的本地数据库。
#mermaid-svg-1Ag7qeuQtMHD6q9F{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1Ag7qeuQtMHD6q9F .error-icon{fill:#552222;}#mermaid-svg-1Ag7qeuQtMHD6q9F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1Ag7qeuQtMHD6q9F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .marker.cross{stroke:#333333;}#mermaid-svg-1Ag7qeuQtMHD6q9F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1Ag7qeuQtMHD6q9F p{margin:0;}#mermaid-svg-1Ag7qeuQtMHD6q9F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .cluster-label text{fill:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .cluster-label span{color:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .cluster-label span p{background-color:transparent;}#mermaid-svg-1Ag7qeuQtMHD6q9F .label text,#mermaid-svg-1Ag7qeuQtMHD6q9F span{fill:#333;color:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .node rect,#mermaid-svg-1Ag7qeuQtMHD6q9F .node circle,#mermaid-svg-1Ag7qeuQtMHD6q9F .node ellipse,#mermaid-svg-1Ag7qeuQtMHD6q9F .node polygon,#mermaid-svg-1Ag7qeuQtMHD6q9F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .rough-node .label text,#mermaid-svg-1Ag7qeuQtMHD6q9F .node .label text,#mermaid-svg-1Ag7qeuQtMHD6q9F .image-shape .label,#mermaid-svg-1Ag7qeuQtMHD6q9F .icon-shape .label{text-anchor:middle;}#mermaid-svg-1Ag7qeuQtMHD6q9F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .rough-node .label,#mermaid-svg-1Ag7qeuQtMHD6q9F .node .label,#mermaid-svg-1Ag7qeuQtMHD6q9F .image-shape .label,#mermaid-svg-1Ag7qeuQtMHD6q9F .icon-shape .label{text-align:center;}#mermaid-svg-1Ag7qeuQtMHD6q9F .node.clickable{cursor:pointer;}#mermaid-svg-1Ag7qeuQtMHD6q9F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .arrowheadPath{fill:#333333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1Ag7qeuQtMHD6q9F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1Ag7qeuQtMHD6q9F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1Ag7qeuQtMHD6q9F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1Ag7qeuQtMHD6q9F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .cluster text{fill:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F .cluster span{color:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1Ag7qeuQtMHD6q9F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1Ag7qeuQtMHD6q9F rect.text{fill:none;stroke-width:0;}#mermaid-svg-1Ag7qeuQtMHD6q9F .icon-shape,#mermaid-svg-1Ag7qeuQtMHD6q9F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1Ag7qeuQtMHD6q9F .icon-shape p,#mermaid-svg-1Ag7qeuQtMHD6q9F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1Ag7qeuQtMHD6q9F .icon-shape .label rect,#mermaid-svg-1Ag7qeuQtMHD6q9F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1Ag7qeuQtMHD6q9F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1Ag7qeuQtMHD6q9F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1Ag7qeuQtMHD6q9F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 图片
文字结果
确认
通知
📱 iOS Node

camera.snap
🖥️ Gateway

OCR Agent
💻 macOS Node

system.run + DB写入

6.2 实现代码

javascript 复制代码
// 跨设备数据同步:拍照 → OCR → 写入远程数据库
async function syncDocumentCapture(iosNode, macNode, ocrService) {
  // 第一步:在 iOS 节点拍照
  console.log(`📷 在 ${iosNode} 上拍摄文档...`);
  const snapResult = await invokeNode(iosNode, 'camera.snap', {
    facing: 'back',
    quality: 0.9,
    flash: 'auto',
    metadata: true
  });

  if (!snapResult.ok) {
    throw new Error(`拍照失败: ${snapResult.error.message}`);
  }

  // 第二步:将图片发送给 OCR Agent
  console.log('🔍 提取文档文字...');
  const ocrResult = await ocrService.extract(snapResult.payload.ref, {
    language: 'chi_sim+eng',
    outputFormat: 'text'
  });

  // 第三步:在 Mac 节点写入数据库
  console.log(`💾 在 ${macNode} 上写入数据库...`);
  const dbResult = await invokeNode(macNode, 'system.run', {
    cmd: `python3 -c "
import sqlite3, json, sys
data = json.loads(sys.argv[1])
conn = sqlite3.connect('/data/documents.db')
conn.execute(
  'INSERT INTO docs (content, source, created) VALUES (?, ?, datetime(\"now\"))',
  (data['text'], data['source'])
)
conn.commit()
print(json.dumps({'rows_affected': conn.total_changes}))
conn.close()
" '${JSON.stringify({
      text: ocrResult.text,
      source: `camera://${iosNode}/${snapResult.payload.ref}`
    })}'"`,
    cwd: '/home/user/scripts',
    timeout: 30
  });

  // 第四步:通知 iOS 节点同步完成
  if (dbResult.ok) {
    console.log('✅ 数据同步完成');
    await invokeNode(iosNode, 'notifications.send', {
      title: '同步完成',
      body: `已识别 ${ocrResult.text.length} 个字符`,
      sound: 'default'
    });
  }

  return {
    captured: snapResult.payload,
    extracted: ocrResult,
    stored: dbResult.payload
  };
}

💡 解读: 这个案例展示了节点调用的组合模式------多个节点的顺序调用构成一个完整的业务流程。关键点在于:每一步都有明确的错误处理(拍照失败则终止);跨节点传递的数据通过 Gateway 中转;最后一步使用 notifications.send 通知用户,形成闭环。生产环境中还需考虑部分失败的补偿机制(如数据库写入失败时是否重试 OCR)。


七、实战案例 2:远程函数执行 🖥️

7.1 场景描述

AI Agent 需要在远程构建节点上执行编译、测试、部署等命令,但 Gateway 本身运行在云端沙箱中,无法直接访问项目文件。通过 system.run 方法,可以将命令路由到正确的节点执行。

7.2 实现代码

javascript 复制代码
// 远程函数执行:在指定节点上运行构建流水线
class RemoteBuildPipeline {
  constructor(gateway, nodeManager) {
    this.gateway = gateway;
    this.nodeManager = nodeManager;
  }

  async execute(buildConfig) {
    const node = buildConfig.node || 'build-default';
    
    // 验证节点在线
    const status = await this.nodeManager.getStatus(node);
    if (status !== 'online') {
      throw new Error(`构建节点 ${node} 不在线 (状态: ${status})`);
    }

    // 阶段1: 拉取最新代码
    const pullResult = await this.invokeWithOutput(
      node, 'system.run',
      { cmd: `cd ${buildConfig.repo} && git pull origin ${buildConfig.branch}` },
      { label: 'git-pull' }
    );
    this.assertSuccess(pullResult, '代码拉取失败');

    // 阶段2: 安装依赖
    const installResult = await this.invokeWithOutput(
      node, 'system.run',
      { cmd: `cd ${buildConfig.repo} && npm ci`, timeout: 120 },
      { label: 'npm-install' }
    );
    this.assertSuccess(installResult, '依赖安装失败');

    // 阶段3: 运行测试
    const testResult = await this.invokeWithOutput(
      node, 'system.run',
      { cmd: `cd ${buildConfig.repo} && npm test`, timeout: 300 },
      { label: 'npm-test', allowFailure: true }
    );

    // 阶段4: 构建
    const buildResult = await this.invokeWithOutput(
      node, 'system.run',
      { cmd: `cd ${buildConfig.repo} && npm run build`, timeout: 180 },
      { label: 'npm-build' }
    );
    this.assertSuccess(buildResult, '构建失败');

    return {
      success: testResult.ok && buildResult.ok,
      pull: pullResult.payload,
      install: installResult.payload,
      test: testResult,
      build: buildResult.payload
    };
  }

  async invokeWithOutput(node, command, params, options = {}) {
    // 订阅命令输出事件
    const subscription = await this.gateway.subscribe(
      node, 'command.stdout',
      (event) => {
        if (options.label) {
          process.stdout.write(`[${options.label}] ${event.payload.data}`);
        }
      }
    );

    try {
      const result = await this.gateway.invokeNode(node, command, params);
      if (!result.ok && !options.allowFailure) {
        return result;
      }
      return result;
    } finally {
      await subscription.unsubscribe();
    }
  }

  assertSuccess(result, message) {
    if (!result.ok) {
      throw new Error(`${message}: ${result.error?.message || '未知错误'}`);
    }
  }
}

💡 解读: 远程构建流水线的核心挑战是实时输出超时控制 。通过订阅 command.stdout 事件,Agent 可以实时看到构建进度;通过每个阶段设置合理的 timeout,防止某个阶段卡死导致整个流水线超时。allowFailure 选项允许测试阶段失败后继续构建,这是 CI/CD 的常见需求。

7.3 审批与安全

system.run 是高危操作,OpenClaw 通过审批机制保障安全:

bash 复制代码
# 在节点上添加允许列表条目
openclaw approvals allowlist add --node build-mac "/usr/bin/git"
openclaw approvals allowlist add --node build-mac "/usr/local/bin/npm"
openclaw approvals allowlist add --node build-mac "/usr/local/bin/node"

# 查看当前审批规则
cat ~/.openclaw/exec-approvals.json

审批规则存储在节点本地的 ~/.openclaw/exec-approvals.json 中,Gateway 无法远程修改------这是安全设计的一部分。


八、实战案例 3:设备间消息传递 📨

8.1 场景描述

多个节点之间需要传递消息,但它们不直接通信------所有消息都通过 Gateway 路由。典型场景包括:手机节点发送控制指令到 Mac 节点、多节点间协调任务状态、设备间共享剪贴板等。

8.2 架构设计

#mermaid-svg-whZxrqi7QC2UsWOi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-whZxrqi7QC2UsWOi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-whZxrqi7QC2UsWOi .error-icon{fill:#552222;}#mermaid-svg-whZxrqi7QC2UsWOi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-whZxrqi7QC2UsWOi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-whZxrqi7QC2UsWOi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-whZxrqi7QC2UsWOi .marker.cross{stroke:#333333;}#mermaid-svg-whZxrqi7QC2UsWOi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-whZxrqi7QC2UsWOi p{margin:0;}#mermaid-svg-whZxrqi7QC2UsWOi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-whZxrqi7QC2UsWOi .cluster-label text{fill:#333;}#mermaid-svg-whZxrqi7QC2UsWOi .cluster-label span{color:#333;}#mermaid-svg-whZxrqi7QC2UsWOi .cluster-label span p{background-color:transparent;}#mermaid-svg-whZxrqi7QC2UsWOi .label text,#mermaid-svg-whZxrqi7QC2UsWOi span{fill:#333;color:#333;}#mermaid-svg-whZxrqi7QC2UsWOi .node rect,#mermaid-svg-whZxrqi7QC2UsWOi .node circle,#mermaid-svg-whZxrqi7QC2UsWOi .node ellipse,#mermaid-svg-whZxrqi7QC2UsWOi .node polygon,#mermaid-svg-whZxrqi7QC2UsWOi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-whZxrqi7QC2UsWOi .rough-node .label text,#mermaid-svg-whZxrqi7QC2UsWOi .node .label text,#mermaid-svg-whZxrqi7QC2UsWOi .image-shape .label,#mermaid-svg-whZxrqi7QC2UsWOi .icon-shape .label{text-anchor:middle;}#mermaid-svg-whZxrqi7QC2UsWOi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-whZxrqi7QC2UsWOi .rough-node .label,#mermaid-svg-whZxrqi7QC2UsWOi .node .label,#mermaid-svg-whZxrqi7QC2UsWOi .image-shape .label,#mermaid-svg-whZxrqi7QC2UsWOi .icon-shape .label{text-align:center;}#mermaid-svg-whZxrqi7QC2UsWOi .node.clickable{cursor:pointer;}#mermaid-svg-whZxrqi7QC2UsWOi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-whZxrqi7QC2UsWOi .arrowheadPath{fill:#333333;}#mermaid-svg-whZxrqi7QC2UsWOi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-whZxrqi7QC2UsWOi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-whZxrqi7QC2UsWOi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whZxrqi7QC2UsWOi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-whZxrqi7QC2UsWOi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whZxrqi7QC2UsWOi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-whZxrqi7QC2UsWOi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-whZxrqi7QC2UsWOi .cluster text{fill:#333;}#mermaid-svg-whZxrqi7QC2UsWOi .cluster span{color:#333;}#mermaid-svg-whZxrqi7QC2UsWOi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-whZxrqi7QC2UsWOi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-whZxrqi7QC2UsWOi rect.text{fill:none;stroke-width:0;}#mermaid-svg-whZxrqi7QC2UsWOi .icon-shape,#mermaid-svg-whZxrqi7QC2UsWOi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whZxrqi7QC2UsWOi .icon-shape p,#mermaid-svg-whZxrqi7QC2UsWOi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-whZxrqi7QC2UsWOi .icon-shape .label rect,#mermaid-svg-whZxrqi7QC2UsWOi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whZxrqi7QC2UsWOi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-whZxrqi7QC2UsWOi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-whZxrqi7QC2UsWOi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 节点集群
发布指令
分发任务
分发任务
上报结果
上报结果
汇总通知
📱 iOS Node

controller
💻 macOS Node

worker-1
💻 macOS Node

worker-2
🖥️ Gateway

消息路由 + 状态存储

8.3 实现代码

javascript 复制代码
// 设备间消息传递:基于 Gateway 的消息总线
class DeviceMessageBus {
  constructor(gateway) {
    this.gateway = gateway;
    this.handlers = new Map();
    this.subscriptions = [];
  }

  // 注册消息处理器
  on(messageType, handler) {
    if (!this.handlers.has(messageType)) {
      this.handlers.set(messageType, []);
    }
    this.handlers.get(messageType).push(handler);
  }

  // 向指定节点发送消息
  async send(targetNode, messageType, payload, options = {}) {
    const result = await this.gateway.invokeNode(targetNode, 'notifications.send', {
      title: `[msg] ${messageType}`,
      body: JSON.stringify(payload),
      category: 'device-message',
      sound: options.silent ? undefined : 'default'
    });

    // 同时在节点上执行关联动作
    if (options.action) {
      await this.gateway.invokeNode(targetNode, options.action.command, options.action.params);
    }

    return result;
  }

  // 广播消息到所有在线节点
  async broadcast(messageType, payload, options = {}) {
    const nodes = await this.gateway.listNodes({ status: 'online' });
    const results = await Promise.allSettled(
      nodes.map(node => this.send(node.id, messageType, payload, options))
    );

    const succeeded = results.filter(r => r.status === 'fulfilled').length;
    const failed = results.filter(r => r.status === 'rejected').length;

    return {
      total: nodes.length,
      succeeded,
      failed,
      details: results
    };
  }

  // 请求-响应模式:发送消息并等待回复
  async request(targetNode, messageType, payload, timeoutMs = 30000) {
    const replyType = `${messageType}.reply`;
    const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2)}`;

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.off(replyType, handler);
        reject(new Error(`请求超时: ${messageType} (${timeoutMs}ms)`));
      }, timeoutMs);

      const handler = (event) => {
        if (event.payload.requestId === requestId) {
          clearTimeout(timer);
          this.off(replyType, handler);
          resolve(event.payload);
        }
      };

      this.on(replyType, handler);
      this.send(targetNode, messageType, { ...payload, requestId });
    });
  }

  off(messageType, handler) {
    const handlers = this.handlers.get(messageType) || [];
    const index = handlers.indexOf(handler);
    if (index !== -1) handlers.splice(index, 1);
  }
}

💡 解读: 设备间消息传递的核心挑战是没有直接通道 ------所有通信必须经过 Gateway。DeviceMessageBus 封装了三种通信模式:点对点发送(send)、广播(broadcast)、请求-响应(request)。其中 request 模式最复杂,需要通过 requestId 关联请求和响应,并设置超时防止永久等待。这是构建多设备协作应用的基础设施。


九、进阶话题 🚀

9.1 连接生命周期管理

节点的连接不是永久的,网络波动、设备休眠、应用切换都可能导致断连。OpenClaw 的处理流程如下:
#mermaid-svg-qvZPe8SRpOJPWKbK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qvZPe8SRpOJPWKbK .error-icon{fill:#552222;}#mermaid-svg-qvZPe8SRpOJPWKbK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qvZPe8SRpOJPWKbK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qvZPe8SRpOJPWKbK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qvZPe8SRpOJPWKbK .marker.cross{stroke:#333333;}#mermaid-svg-qvZPe8SRpOJPWKbK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qvZPe8SRpOJPWKbK p{margin:0;}#mermaid-svg-qvZPe8SRpOJPWKbK defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-qvZPe8SRpOJPWKbK g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-qvZPe8SRpOJPWKbK g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-qvZPe8SRpOJPWKbK g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-qvZPe8SRpOJPWKbK g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-qvZPe8SRpOJPWKbK g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-qvZPe8SRpOJPWKbK .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-qvZPe8SRpOJPWKbK .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-qvZPe8SRpOJPWKbK .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-qvZPe8SRpOJPWKbK .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qvZPe8SRpOJPWKbK .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-qvZPe8SRpOJPWKbK .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-qvZPe8SRpOJPWKbK .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-qvZPe8SRpOJPWKbK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qvZPe8SRpOJPWKbK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qvZPe8SRpOJPWKbK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qvZPe8SRpOJPWKbK .edgeLabel .label text{fill:#333;}#mermaid-svg-qvZPe8SRpOJPWKbK .label div .edgeLabel{color:#333;}#mermaid-svg-qvZPe8SRpOJPWKbK .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-qvZPe8SRpOJPWKbK .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-qvZPe8SRpOJPWKbK .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-qvZPe8SRpOJPWKbK .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-qvZPe8SRpOJPWKbK .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-qvZPe8SRpOJPWKbK .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qvZPe8SRpOJPWKbK .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qvZPe8SRpOJPWKbK #statediagram-barbEnd{fill:#333333;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qvZPe8SRpOJPWKbK .cluster-label,#mermaid-svg-qvZPe8SRpOJPWKbK .nodeLabel{color:#131300;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-qvZPe8SRpOJPWKbK .note-edge{stroke-dasharray:5;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-note text{fill:black;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram-note .nodeLabel{color:black;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagram .edgeLabel{color:red;}#mermaid-svg-qvZPe8SRpOJPWKbK #dependencyStart,#mermaid-svg-qvZPe8SRpOJPWKbK #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-qvZPe8SRpOJPWKbK .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qvZPe8SRpOJPWKbK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动连接
发送 connect 帧
管理员批准
管理员拒绝
握手完成
WebSocket 断开
重连成功
重连耗尽
节点主动断开
Offline
Connecting
PendingApproval
Paired
Rejected
Online
Reconnecting

Gateway 侧的节点状态可通过 CLI 实时查看:

bash 复制代码
# 查看节点状态(含连接状态和最后心跳)
openclaw nodes status

# 查看节点详细信息(配对记录、能力列表、权限)
openclaw nodes describe --node "my-iphone"

9.2 能力协商(Capability Negotiation)

节点在连接握手时声明自己支持的能力(caps)和命令(commands),Gateway 据此判断是否可以路由特定调用:

json 复制代码
// iOS 节点连接声明
{
  "role": "node",
  "caps": ["camera", "canvas", "screen", "location", "voice"],
  "commands": ["camera.snap", "canvas.navigate", "canvas.eval", "screen.record", "location.get"]
}

// macOS 节点连接声明
{
  "role": "node",
  "caps": ["canvas", "system"],
  "commands": ["canvas.navigate", "canvas.eval", "system.run", "system.which"]
}

如果 Agent 尝试在 macOS 节点上调用 camera.snap,Gateway 会直接返回 METHOD_NOT_FOUND 错误,不会将请求转发到节点。

9.3 并发控制

同一个节点同时接收多个调用时,需要考虑并发策略:

策略 适用场景 实现方式
串行队列 摄像头操作(同一时刻只能拍一张) 节点内部排队
并行执行 互不影响的独立命令 默认行为
互斥锁 文件写入(防止写冲突) 应用层实现
取消-替换 用户快速连续操作 取消前一次调用

9.4 性能优化建议

  1. 减少帧大小 :避免在参数中传递大块数据,使用 media:// 引用替代
  2. 批量调用 :多个独立调用可以并行发出,用 Promise.all 等待
  3. 事件节流 :高频事件始终设置 throttleMs
  4. 连接复用:保持 WebSocket 长连接,避免频繁握手
  5. 就近路由:Gateway 和节点在同一内网时,延迟更低

十、安全考量 🔒

10.1 端到端信任链

#mermaid-svg-9IsVTxEvOADHNG2u{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9IsVTxEvOADHNG2u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9IsVTxEvOADHNG2u .error-icon{fill:#552222;}#mermaid-svg-9IsVTxEvOADHNG2u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9IsVTxEvOADHNG2u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9IsVTxEvOADHNG2u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9IsVTxEvOADHNG2u .marker.cross{stroke:#333333;}#mermaid-svg-9IsVTxEvOADHNG2u svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9IsVTxEvOADHNG2u p{margin:0;}#mermaid-svg-9IsVTxEvOADHNG2u .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9IsVTxEvOADHNG2u .cluster-label text{fill:#333;}#mermaid-svg-9IsVTxEvOADHNG2u .cluster-label span{color:#333;}#mermaid-svg-9IsVTxEvOADHNG2u .cluster-label span p{background-color:transparent;}#mermaid-svg-9IsVTxEvOADHNG2u .label text,#mermaid-svg-9IsVTxEvOADHNG2u span{fill:#333;color:#333;}#mermaid-svg-9IsVTxEvOADHNG2u .node rect,#mermaid-svg-9IsVTxEvOADHNG2u .node circle,#mermaid-svg-9IsVTxEvOADHNG2u .node ellipse,#mermaid-svg-9IsVTxEvOADHNG2u .node polygon,#mermaid-svg-9IsVTxEvOADHNG2u .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9IsVTxEvOADHNG2u .rough-node .label text,#mermaid-svg-9IsVTxEvOADHNG2u .node .label text,#mermaid-svg-9IsVTxEvOADHNG2u .image-shape .label,#mermaid-svg-9IsVTxEvOADHNG2u .icon-shape .label{text-anchor:middle;}#mermaid-svg-9IsVTxEvOADHNG2u .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9IsVTxEvOADHNG2u .rough-node .label,#mermaid-svg-9IsVTxEvOADHNG2u .node .label,#mermaid-svg-9IsVTxEvOADHNG2u .image-shape .label,#mermaid-svg-9IsVTxEvOADHNG2u .icon-shape .label{text-align:center;}#mermaid-svg-9IsVTxEvOADHNG2u .node.clickable{cursor:pointer;}#mermaid-svg-9IsVTxEvOADHNG2u .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9IsVTxEvOADHNG2u .arrowheadPath{fill:#333333;}#mermaid-svg-9IsVTxEvOADHNG2u .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9IsVTxEvOADHNG2u .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9IsVTxEvOADHNG2u .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9IsVTxEvOADHNG2u .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9IsVTxEvOADHNG2u .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9IsVTxEvOADHNG2u .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9IsVTxEvOADHNG2u .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9IsVTxEvOADHNG2u .cluster text{fill:#333;}#mermaid-svg-9IsVTxEvOADHNG2u .cluster span{color:#333;}#mermaid-svg-9IsVTxEvOADHNG2u div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9IsVTxEvOADHNG2u .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9IsVTxEvOADHNG2u rect.text{fill:none;stroke-width:0;}#mermaid-svg-9IsVTxEvOADHNG2u .icon-shape,#mermaid-svg-9IsVTxEvOADHNG2u .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9IsVTxEvOADHNG2u .icon-shape p,#mermaid-svg-9IsVTxEvOADHNG2u .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9IsVTxEvOADHNG2u .icon-shape .label rect,#mermaid-svg-9IsVTxEvOADHNG2u .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9IsVTxEvOADHNG2u .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9IsVTxEvOADHNG2u .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9IsVTxEvOADHNG2u :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 设备身份

device.id + publicKey
配对审批

operator.pairing
Token 签发

deviceToken
作用域绑定

scopes
命令白名单

exec-approvals.json
执行时校验

allowlist/full

整个信任链从设备身份开始,经过配对审批、Token 签发、作用域绑定、命令白名单,最终到执行时校验,每一层都是独立的安全防线。

10.2 权限最小化原则

节点配对时,Gateway 根据节点声明的命令自动确定所需权限:

  • 仅声明非执行命令 (如 camera.*canvas.*):授予 operator.pairing + operator.write
  • 声明执行命令system.run 等):额外授予 operator.admin

这意味着一个只用于拍照的 iOS 节点不会获得执行任意命令的权限。

10.3 Token 轮换与安全

  • 设备 Token 在配对记录内自动轮换,不影响审批状态
  • 配对记录是"权限合约",Token 轮换不能升级到未授权的权限
  • 敏感操作(system.run)需要节点本地审批,Gateway 无法绕过

十一、调试与排错 🔍

11.1 常用诊断命令

bash 复制代码
# 1. 检查节点连接状态
openclaw nodes status

# 2. 查看节点详细能力
openclaw nodes describe --node "my-iphone"

# 3. 测试节点连通性
openclaw nodes invoke --node "my-iphone" --command canvas.eval --params '{"javaScript":"1+1"}'

# 4. 查看设备配对请求
openclaw devices list

# 5. 查看审批规则(在节点机器上)
cat ~/.openclaw/exec-approvals.json

# 6. 查看网关日志
openclaw gateway logs --filter node

11.2 常见问题速查表

问题 可能原因 解决方法
NODE_NOT_FOUND 节点未启动或网络不通 检查 openclaw nodes status
PERMISSION_DENIED 配对未审批或权限不足 openclaw devices list + approve
调用无响应 WebSocket 断开或节点休眠 检查节点心跳和网关日志
RESOURCE_BUSY 摄像头/麦克风被其他应用占用 关闭占用应用后重试
参数被拒 参数格式或类型错误 对照方法文档检查参数
媒体引用无效 引用过期或节点已断连 重新发起调用获取新引用

十二、总结 📝

OpenClaw 的节点方法调用是构建跨设备 AI Agent 的核心基础设施。通过本文的系统性讲解,我们掌握了以下要点:

  1. node.invoke 是底层 RPC 机制,CLI 和工具层都基于它构建
  2. 参数传递 遵循 JSON 帧 + 媒体引用模式,支持任意大小的数据传输
  3. 回调与事件 实现异步进度感知和状态变化监听,节流机制保障性能
  4. 错误处理 区分可重试/不可重试错误,指数退避 + 抖动是推荐的重试策略
  5. 三大实战案例 演示了数据同步、远程执行、消息传递的真实应用模式
  6. 安全机制 从设备身份到执行审批形成完整信任链

参考资料

https://docs.openclaw.ai/nodes

https://docs.openclaw.ai

相关推荐
bleuesprit1 小时前
DeerFlow 2.0 Lead Agent 中间件分析
ai
一切皆是因缘际会1 小时前
隐层表征解构:LLM感知式幻觉稀疏成因
算法·数学建模·ai·架构
iotxiaohu1 小时前
一图认识 —— 互斥锁
c语言·ai·信号量
DS随心转APP1 小时前
Claude 导出对话多方案横向测评来袭,借助 AI 导出鸭对比各类导出工具优劣,筛选最优处理办法
人工智能·ai·chatgpt·deepseek·ai导出鸭
尘埃落定wf1 小时前
LangGraph 与 Human-in-the-Loop 实战指南
ai·langragh
岳小哥AI2 小时前
一文读懂AI应用技术:自然语言处理、语音识别/合成、可解释AI
ai·ai基础
DS随心转插件2 小时前
Kimi 转 pdf 怎么压缩但清晰?AI 导出鸭一站式优化,压缩文件同时留存原版高清内容
人工智能·ai·pdf·豆包·deepseek·ai导出鸭
钱多多_qdd2 小时前
claude code(十):【企业级应用实战1】:章节介绍与前言
ai·claude
xiezhr2 小时前
Hermes官方桌面版发布了
人工智能·ai·agent·codex·hermes