目录
-
- 摘要
- [一、背景:为什么需要节点方法调用 🤔](#一、背景:为什么需要节点方法调用 🤔)
- [二、方法调用基础: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.eval、camera.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.snap 的 facing 和 quality 都有合理默认值;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
}
}
}
💡 解读: 事件订阅不是"全量推送"。通过合理配置 throttleMs 和 filter,可以在保持实时性的同时大幅降低网络和计算开销。生产环境中建议对高频事件始终设置节流阈值。
五、错误处理:调用失败与重试 ⚠️
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 性能优化建议
- 减少帧大小 :避免在参数中传递大块数据,使用
media://引用替代 - 批量调用 :多个独立调用可以并行发出,用
Promise.all等待 - 事件节流 :高频事件始终设置
throttleMs - 连接复用:保持 WebSocket 长连接,避免频繁握手
- 就近路由: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 的核心基础设施。通过本文的系统性讲解,我们掌握了以下要点:
node.invoke是底层 RPC 机制,CLI 和工具层都基于它构建- 参数传递 遵循 JSON 帧 + 媒体引用模式,支持任意大小的数据传输
- 回调与事件 实现异步进度感知和状态变化监听,节流机制保障性能
- 错误处理 区分可重试/不可重试错误,指数退避 + 抖动是推荐的重试策略
- 三大实战案例 演示了数据同步、远程执行、消息传递的真实应用模式
- 安全机制 从设备身份到执行审批形成完整信任链