OpenClaw 深度解析(六):节点、Canvas 与子 Agent

场景:AI 助手的"边界问题"

把 OpenClaw 当作个人 AI 助手使用一段时间后,会遇到几个让单进程模型力不从心的场景:

  1. 远程执行:你想让 AI 帮你在家里的 Linux 服务器上跑一段脚本,但 OpenClaw Gateway 运行在 Mac 上------AI 怎么触达那台服务器的 Shell?
  2. 手机上的交互 UI:你想在手机上看到 AI 生成的实时仪表盘,并且能点击按钮触发下一步操作------AI 怎么向移动端 WebView 推送 UI,WebView 里的点击又怎么反馈给 AI?
  3. 并行任务:你让 AI 帮你整理 1000 封邮件------用一个 Agent 串行处理太慢了,能不能派出多个 AI 同时干?

这三个场景分别对应 OpenClaw 的三个核心扩展机制:Node Host(节点主机)Canvas + A2UI子 Agent(Sub-agent)


一、Node Host:让 AI 触达远程机器

问题:Gateway 和执行目标不在同一台机器

Gateway 负责对话管理和 Agent 执行,但 system.run(执行 Shell 命令)这类工具需要在目标机器上运行------可能是远程服务器、NAS、Raspberry Pi,或者同一台 Mac 上受限环境的另一个进程。

Node 是解决这个问题的抽象:一个独立进程,连接到 Gateway,响应执行请求。Node 通过标准的 GatewayClient(WebSocket)注册自己:

typescript 复制代码
// src/node-host/runner.ts
const client = new GatewayClient({
  url: `wss://${gatewayHost}:${gatewayPort}`,
  instanceId: nodeId,         // 节点唯一标识(如机器 hostname)
  clientName: "node-host",
  role: "node",               // 区别于 "agent"、"cli" 角色
  caps: ["system", "browser"], // 声明此节点支持的能力
  commands: NODE_SYSTEM_RUN_COMMANDS,  // 支持的命令列表
  onEvent: (evt) => {
    if (evt.event !== "node.invoke.request") return;
    const payload = coerceNodeInvokePayload(evt.payload);
    void handleInvoke(payload, client, skillBins);  // 处理执行请求
  },
});
client.start();

数据流:

arduino 复制代码
Agent 调用 system.run 工具
  → Gateway 路由到目标 nodeId 的 WebSocket 连接
  → 发送 node.invoke.request 事件
  → Node 执行命令 (spawn子进程)
  → node.invoke.result 返回结果
  → Gateway 把结果交给 Agent

执行安全:三级权限模型

Node 不是无限制的 Shell 执行器。exec-approvals.ts 实现了一个三级安全模型:

typescript 复制代码
type ExecSecurity = "deny" | "allowlist" | "full";
  • deny:拒绝所有命令执行
  • allowlist (默认):只允许 exec-approvals.json 白名单中的命令
  • full:允许所有命令(高信任环境下使用)

白名单文件用哈希防止竞态修改------读取和更新都需要传入 baseHash,若文件已被其他进程改动,返回 "INVALID_REQUEST: exec approvals changed; reload and retry"。这防止了 TOCTOU 攻击。

还有一个特殊路径:在 macOS 上,preferMacAppExecHost 为 true 时,执行请求会优先通过 macOS 应用的 Exec Host(Unix socket),而不是直接 spawn。这是因为 macOS 沙盒限制了某些路径的访问权限,通过应用层代理绕过这个限制。

Node 的输出上限

命令的输出被硬性截断:

typescript 复制代码
const OUTPUT_CAP = 200_000;      // 累计输出上限(字节)
const OUTPUT_EVENT_TAIL = 20_000; // 单次事件的输出尾部(字节)

超出上限的内容被丢弃,结果中 truncated: true。这保证了大量输出(如日志文件)不会撑爆 Agent 的上下文窗口。


二、Canvas:在手机上渲染 AI 生成的 UI

问题:AI 的回复只能是文字吗?

AI 擅长生成代码,能不能生成 HTML 仪表盘、然后在用户手机上直接显示,并支持交互?

Canvas 是 Gateway 内置的一个轻量 HTTP 服务器,挂载在 /__openclaw__/canvas,专门伺服 AI 生成的 HTML/JS/CSS 文件:

typescript 复制代码
// 目录结构
~/.openclaw/canvas/
  index.html   ← AI 生成后写到这里
  app.js
  style.css

Gateway 启动时,createCanvasHostHandler 会:

  1. ~/.openclaw/canvas/ 创建默认的 index.html(如果不存在)
  2. chokidar 监听目录变化
  3. 文件变化时通过 WebSocket(/__openclaw__/ws)推送 "reload" 给所有已连接的客户端

被注入的实时重载脚本:

typescript 复制代码
// 每个 HTML 页面末尾都会自动注入
const ws = new WebSocket("wss://host/__openclaw__/ws");
ws.onmessage = (ev) => {
  if (String(ev.data || "") === "reload") location.reload();
};

这意味着:AI 重写 index.html → chokidar 检测到文件变化 → WebSocket 广播 "reload" → 用户手机上的 WebView 自动刷新。

文件系统边界

文件服务有安全约束。resolveFileWithinRootopen(fd) + realpath 验证每个请求的文件路径都在 Canvas 根目录内------这与上一篇 Plugin SDK 里的 openBoundaryFileSync 是同一类防护,都是防止路径遍历攻击。


三、A2UI:WebView 与 Agent 的双向通信

问题:Canvas 页面里的按钮点击怎么回传给 Agent?

Canvas 里的 HTML 页面可以显示数据,但它本身没有办法与 OpenClaw 通信------它是运行在 iOS/Android WebView 里的孤立页面。

A2UI(Agent-to-UI) 解决了这个方向问题:提供一套跨平台的 JavaScript 桥接 API,让 WebView 里的代码可以触发 OpenClaw Agent 的动作。

A2UI 包(src/canvas-host/a2ui/)通过 /__openclaw__/a2ui/ 伺服,并在每个 Canvas 页面的 HTML 里自动注入一段引导脚本:

typescript 复制代码
// 被注入到每个 Canvas HTML 页面的桥接脚本(简化)
function postToNode(payload) {
  const raw = typeof payload === "string" ? payload : JSON.stringify(payload);

  // iOS 桥接
  const iosHandler = globalThis.webkit?.messageHandlers?.openclawCanvasA2UIAction;
  if (iosHandler?.postMessage) {
    iosHandler.postMessage(raw);
    return true;
  }

  // Android 桥接
  const androidHandler = globalThis.openclawCanvasA2UIAction;
  if (androidHandler?.postMessage) {
    androidHandler.postMessage(raw);
    return true;
  }

  return false;
}

// 给 Canvas 页面的公开 API
globalThis.openclawSendUserAction = (userAction) => {
  const id = userAction.id || crypto.randomUUID();
  return postToNode({ userAction: { ...userAction, id } });
};

Canvas 页面里的代码可以这样触发动作:

javascript 复制代码
// 用户点击了"执行备份"按钮
window.openclawSendUserAction({
  name: "run_backup",
  surfaceId: "main",
  sourceComponentId: "backup.button",
  context: { target: "nas-01", compress: true }
});

这条消息经由:

csharp 复制代码
Canvas JS → native MessageHandler(iOS/Android)
  → OpenClaw Node Host 的 node.event
  → Gateway
  → 对应的 Agent 会话(作为用户输入)
  → Agent 决定下一步行动

动作结果回传

Agent 处理完动作后,可以通过 window.dispatchEvent(new CustomEvent("openclaw:a2ui-action-status", { detail: { id, ok, error } })) 通知 Canvas 页面操作已完成。这构成了一个完整的请求-响应循环。


四、ACP:标准化的 Agent 互操作协议

问题:外部工具如何调用 OpenClaw?

@agentclientprotocol/sdk 是 Agent Client Protocol 的实现,OpenClaw 通过它暴露一个标准接口,让任何兼容 ACP 的工具都能与 OpenClaw 的 Agent 会话交互。

typescript 复制代码
// src/acp/server.ts
export async function serveAcpGateway(opts: AcpServerOptions): Promise<void> {
  // 监听本地端口,将 ACP 请求翻译成 Gateway 操作
  const agent = new AcpGatewayAgent(gateway);
  // 每个 ACP 会话映射到一个 OpenClaw 会话(sessionKey)
}

ACP 会话有两种模式:

typescript 复制代码
export const ACP_SPAWN_MODES = ["run", "session"] as const;
// "run"     → 一次性任务:完成后关闭会话
// "session" → 持久会话:完成后保留,后续请求继续使用同一上下文

这让 CI/CD 系统、IDE 插件、或其他 AI 工具可以把 OpenClaw 当作一个可编程的 AI 后端调用,而不需要理解 OpenClaw 内部的协议细节。


五、子 Agent:并行任务分解

问题:串行处理大任务太慢

一个复杂任务(整理 1000 封邮件,分析 50 个代码文件)用单个 Agent 串行处理太慢。子 Agent 机制让 Agent 可以派生(spawn)独立的子 Agent 并行执行子任务。

typescript 复制代码
// Agent 使用 sessions.spawn 工具派生子 Agent
export async function spawnSubagentDirect(
  params: SpawnSubagentParams,
  ctx: SpawnSubagentContext,
): Promise<SpawnSubagentResult>

SpawnSubagentParams 的核心字段:

typescript 复制代码
type SpawnSubagentParams = {
  task: string;              // 子任务描述(注入为子 Agent 的第一条消息)
  label?: string;            // 可读标签(用于状态显示)
  agentId?: string;          // 指定使用哪个 agent 配置
  model?: string;            // 子 Agent 的模型(可以与父 Agent 不同)
  thinking?: string;         // 子 Agent 的思考等级
  runTimeoutSeconds?: number; // 超时控制
  thread?: boolean;          // 是否绑定到聊天线程(结果直接发到聊天)
  mode?: "run" | "session";  // 一次性 vs 持久会话
  cleanup?: "delete" | "keep"; // 完成后是否清理会话
};

子 Agent 的生命周期

scss 复制代码
父 Agent 调用 sessions.spawn
  ↓
spawnSubagentDirect()
  → 创建新 SessionKey(格式:agentId:session-xxxxxxxx)
  → 在 AGENT_LANE_SUBAGENT 通道里排队
  → 注册到 SubagentRegistry(追踪运行状态)
  ↓
子 Agent 在独立 Lane 里执行(与父 Agent 并行)
  ↓
完成后:subagent-announce 把结果发回给父 Agent
  → 作为父 Agent 会话里的一条用户消息
  → 父 Agent 继续处理

"不要轮询"注意事项 :子 Agent 完成后会主动宣告(announce)结果,而不是等父 Agent 来查询。父 Agent 在派生子任务后应该继续其他工作,而不是在循环里等待子任务完成。子任务的结果会自动以用户消息的形式重新注入父 Agent 的上下文。

arduino 复制代码
// 子 Agent 结果宣告文案(src/agents/subagent-spawn.ts)
export const SUBAGENT_SPAWN_ACCEPTED_NOTE =
  "auto-announces on completion, do not poll/sleep. The response will be sent back as an user message.";

深度限制

为了防止无限递归(子 Agent 再派生子 Agent 再派生......),系统维护一个深度计数器subagent-depth.ts),并且有默认上限:

typescript 复制代码
// src/config/agent-limits.ts
export const DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH = 3;

超过这个深度的派生请求会被拒绝,返回 "forbidden" 状态。

子 Agent 绑定线程

thread: true 参数让子 Agent 的结果直接投递到当前聊天线程(而不是等父 Agent 转发):

arduino 复制代码
父 Agent(在 Telegram 对话里)
  → 派生子 Agent,thread: true
  → 子 Agent 完成后,结果直接发送到 Telegram 对话
  → 用户在 Telegram 里直接看到子任务的输出

这个功能依赖 subagent_spawning 生命周期钩子------只有实现了该钩子的通道插件(能绑定线程的聊天平台)才能使用。


小结:三个"边界突破"机制

机制 突破的边界 核心数据流
Node Host 执行边界------让 AI 触达远程机器 Gateway → WebSocket → Node → spawn → 结果返回
Canvas + A2UI UI 边界------让 AI 的输出变成交互式 UI AI 写文件 → chokidar → WebSocket → WebView 重载;用户点击 → native bridge → node.event → Agent
子 Agent 并发边界------让 AI 并行分解任务 父 Agent spawn → 子 Agent 独立执行 → 完成后 announce 回注入父会话

三者共同扩展了"个人 AI 助手"的能力边界:不只是对话,而是可编程的执行引擎 + 交互式 UI 宿主 + 多 Agent 协作系统

下一篇也是系列的最后一篇,我们将进入 OpenClaw 的安全模型与沙盒------系统性梳理 Gateway 认证、工具策略、沙盒隔离、API Key 保护、以及整个系统的信任边界设计。

相关推荐
刀法如飞3 小时前
AI提示词框架深度对比分析
人工智能·ai编程
IT_陈寒4 小时前
Python开发者必知的5大性能陷阱:90%的人都踩过的坑!
前端·人工智能·后端
1G5 小时前
openclaw控制浏览器/自动化的playwright MCP + Mcporter方案实现
人工智能
踩着两条虫5 小时前
VTJ.PRO 双向代码转换原理揭秘
前端·vue.js·人工智能
扉川川5 小时前
OpenClaw 架构解析:一个生产级 AI Agent 是如何设计的
前端·人工智能
星浩AI5 小时前
让模型自己写 Skills——从素材到自动生成工作流
人工智能·后端·agent
IvorySQL9 小时前
PostgreSQL 技术日报 (3月7日)|生态更新与内核性能讨论
数据库·postgresql·开源
千寻girling10 小时前
Python 是用来做 AI 人工智能 的 , 不适合开发 Web 网站 | 《Web框架》
人工智能·后端·算法
AI攻城狮10 小时前
OpenClaw 里 TAVILY_API_KEY 明明写在 ~/.bashrc,为什么还是失效?一次完整排查与修复
人工智能·云原生·aigc