Fork me on GitHub

分类 open claw 下的文章

让小龙虾自己写手册:Skill Workshop

上一篇我们围绕自定义 skill 做了三件事:从 ClawHub 上搜索并安装别人的 skill、从零开始自己手写一份 skill、再把它发布回 ClawHub 让别人也能安装。三件事做下来你应该感觉到了,手写 skill 终究是个体力活,触发短语得自己琢磨、正文得自己组织、哪些步骤值得沉淀也得自己判断。前面提过的 skill-creator 能稍微帮你减负,但本质还是一份「按步骤填空」的写作指导,触发权和判断权都在你手里。

OpenClaw 有个实验性的内置插件叫 Skill Workshop,方向反过来:每轮成功的会话结束之后,它会自动扫一遍历史消息,把里面值得沉淀的可复用流程提议成一份 workspace skill,如果安全扫描通过就能直接写盘,让小龙虾把这一轮里学到的流程顺手写进下一轮自己能用的手册

开启与配置

这是个实验性插件,默认关闭,需要你在配置文件里显式打开才会加载。打开 OpenClaw 的配置文件,在 plugins.entries 下加上这么一段:

{
  plugins: {
    entries: {
      "skill-workshop": {
        "enabled": true,
        "config": {
          "autoCapture": true,
          "approvalPolicy": "pending",
          "reviewMode": "hybrid"
        }
      }
    }
  }
}

改完保存后,再运行 openclaw gateway restart 重启网关让它生效。

这几个配置参数逐个看一下:

  • autoCapture: true —— 让它在每轮成功对话结束后,自动扫一遍历史消息找可沉淀的东西;关掉的话它就不主动扫了,只有你开口让小龙虾「把这个存成 skill」时才会存。
  • approvalPolicy: "pending" —— 它扫到觉得该存的东西时,不会直接写成文件,而是先攒成一条「提议」排进待办队列,等你审批通过后才真正落到磁盘上。建议先使用该配置,等你确认它提议得靠谱了,再换成 auto 让它跳过审批、自己直接写。
  • reviewMode: "hybrid" —— 用哪种方式去发现可沉淀的内容,一共四挡:off 完全不自动找,heuristic 只靠关键词扫描,llm 让模型回看一遍整段对话,hybrid 则是前两者一起上。新手用 hybrid 就行,几挡的差别后面讲。

因为还在实验阶段,它的内部行为各版本之间可能会变,所以第一次用务必先 pending 模式,亲眼看几轮它都想存些什么,别一上来就让它自动写文件。

这几个参数取不同的值,能搭出好几种不同的组合,对应不同的使用场景:

// 保守:只接受模型显式调工具,关闭一切自动捕获
{ autoCapture: false, approvalPolicy: "pending", reviewMode: "off" }

// 评审优先(推荐):自动捕获,但都先排队等审批
{ autoCapture: true,  approvalPolicy: "pending", reviewMode: "hybrid" }

// 受信自动化:本地工作区里安全提案自动写盘
{ autoCapture: true,  approvalPolicy: "auto",    reviewMode: "hybrid" }

// 省钱:不跑回看模型,只认显式纠正短语
{ autoCapture: true,  approvalPolicy: "pending", reviewMode: "heuristic" }

除了上面这四个,还有几个调阈值、限大小的配置项,第一次上手用不到,附在这里备查:

默认范围作用
reviewInterval151..200累计多少轮成功对话后跑一次回看模型
reviewMinToolCalls81..500累计多少次工具调用后跑回看模型
reviewTimeoutMs450005000..180000回看模型单次运行的超时
maxPending501..200每工作区最多保留多少提案
maxSkillBytes400001024..200000生成的 skill / 支持文件单文件大小上限

实战 Skill Workshop

插件启用之后,这一节我们手动走一遍完整流程,亲眼看它怎么把一句话变成一份 skill。整个过程就三件事:在对话里说一句带「纠正口吻」的话、看看提案有没有进队列、再把它 apply 落地。

说一句带「纠正口吻」的话

第一步,造一段能被插件捕到的对话。插件会盯着你消息里有没有 next time / always / from now on 这类纠正口吻(完整短语列表后面会讲),命中了就尝试把它沉淀成 skill。注意这些短语得出现在用户消息里,小龙虾回复里出现不算。比如:

next time we work with animated GIFs, always verify the URL resolves
to image/gif before committing the file.

小龙虾会正常回复:

next-time.png

但是在回复之后自动触发 skill workshop 插件,对当前会话进行沉淀。

查看提案队列

第二步,看队列里进东西没有。在同一会话或新会话里跟小龙虾说一句:

> 帮我看下 skill workshop 现在有几个 pending 提案

小龙虾会调 skill_workshop 工具,参数是 { "action": "list_pending" },把待审队列列出来,大致这样:

skill-workshop-pending.png

如果查看 Control UI 中的对话详情,可以看到工具调用结果如下:

[
  {
    "id": "1548054d-4aa2-493d-a1f5-96409a4e56be",
    "createdAt": 1780959647860,
    "updatedAt": 1780959647860,
    "workspaceDir": "~/.openclaw/workspace",
    "agentId": "main",
    "sessionId": "161be786-5ac8-439c-ab61-245fa0bdbb56",
    "skillName": "animated-gif-workflow",
    "title": "Animated GIF Workflow",
    "reason": "User correction for animated GIF requests",
    "source": "agent_end",
    "status": "pending",
    "change": {
      "kind": "create",
      "description": "Reusable workflow notes for animated GIF requests.",
      "body": "# Animated GIF Workflow\n\n## Workflow\n\n- next time we work with animated GIFs, always verify the URL resolves to image/gif before committing the file.\n- Verify the result before final reply.\n- Record durable pitfalls as short bullets; avoid copying transcript noise."
    },
    "scanFindings": []
  }
]

这条记录里有几个字段值得注意。

先看 status:它是 pending,说明此刻还没创建任何 skill 文件,这只是一条排在待审队列里的提议,真正写盘要等下一步 apply

再看 skillName:插件从你那句话里识别出 animated GIFs,给这条提议起了个现成的名字 animated-gif-workflow。OpenClaw 内置了一张话题映射表,把几类常见话题各对到一个固定的 skill 名:

用户话里出现落到的默认 skill 名
animated / gifanimated-gif-workflow
screenshot / screen capture / asset / imageoptimscreenshot-asset-workflow
qa / scenario / test planqa-scenario-workflow
pr / pull request / githubgithub-pr-workflow
其它都进兜底名称learned-workflows

你这句话里有 animated GIFs,命中第一行,自然就归到 animated-gif-workflow;要是一句话谁都不沾,就统一落到 learned-workflows 这个兜底名称。注意这只是这份 skill 将来 apply 后会用的目录名,现在还只是提议里的一个建议值。

另外还有个 source 字段,表示这个提案是从哪冒出来的,它的取值有:tool(你或模型显式调工具生成)、agent_end(自动扫到纠正口吻)、reviewer(回看模型产出),正好对应插件捕获提案的三条路径,后面「三条捕获路径」一节会拆开讲。

应用 skill 提案

第三步,把它应用:

apply.png

OpenClaw 会调用下面的工具:

// skill_workshop({ "action": "apply", "id": "1548..." })

应用成功后,自动生成 <workspace>/skills/animated-gif-workflow/SKILL.md 文件。关键的一点是:所有写入都会立刻刷新内存里的 skills 快照,新 skill 不用 /new 也不用重启网关就能在当前会话被看到。

如果觉得这个提案不够好,就拒绝:

// skill_workshop({ "action": "reject", "id": "1548..." })

被拒的提案状态变成 rejected,留在状态文件里供审计。

工具用法速查

跑通之后你会发现,整个生命周期其实都是围绕 skill_workshop 这个工具撑起来的。它的 action 一共八个:

action用途
status统计当前工作区里各个状态(pending / applied / rejected / quarantined)的提案各有多少条
list_pending列出待审队列里的提案,默认只看 pending,也可以传 status 换成查看其它状态
list_quarantine列出被安全扫描拦下、隔离起来的提案
inspectid 查看某一条提案的完整详情
suggest由模型主动提交一条新提案,pending 策略下默认入队等审批
apply把某一条 pending 提案应用掉,真正把 skill 文件写到磁盘上
reject拒掉某一条提案,状态改为 rejected 但保留在记录里
write_support_file往 skill 目录下的支持目录写一份支持文件(如 references/scripts/

其中大部分都比较简单,只有 suggestwrite_support_file 值得单独讲下。

suggest 是模型主动建议的入口,比自动捕获更精确。一个完整的 suggest 长这样:

{
  "action": "suggest",
  "skillName": "animated-gif-workflow",
  "title": "Animated GIF Workflow",
  "reason": "User established reusable GIF validation rules.",
  "description": "Validate animated GIF assets before using them.",
  "body": "## Workflow\n\n- Verify the URL resolves to image/gif.\n- Confirm it has multiple frames.\n- Record attribution and license.\n- Avoid hotlinking when a local asset is needed."
}

上面例子用的是基础参数,默认走 create 模式新建一份 skill 文件。除此之外 suggest 还有几个额外参数,用来切换写入方式或绕开审批策略:

  • apply —— 强制指定写不写盘,绕开当前的 approvalPolicyapply: true 在 pending 策略下也强行立即写(仍然要过安全扫描);反过来 apply: false 在 auto 策略下也强行只入队
  • section —— 传了它就切到 append 模式,把 body 追加到已有 skill 的指定 section 下,而不是新建。
  • oldText + newText —— 这俩一起传就切到 replace 模式,把 skill 正文里的 oldText 精确替换成 newText(要求 oldText 在文件里唯一存在,否则会拒绝)。

write_support_file 用于往 skill 目录下的支持目录写一份支持文件,要知道,skill 不止是 SKILL.md 一份文件,它可以带 references/templates/scripts/assets/ 这四种支持目录,支持目录里的内容不会自动进 prompt,只在 SKILL.md 正文显式引用时被小龙虾 Read 进来。这条命令就是让 Workshop 把 skill 写到一个比 SKILL.md 更深的位置:

{
  "action": "write_support_file",
  "skillName": "release-workflow",
  "relativePath": "references/checklist.md",
  "body": "# Release Checklist\n\n- Run release docs.\n- Verify changelog.\n"
}

和写 SKILL.md 一样,这条命令也守着同一套安全约束:写入位置被严格限定在 workspace 目录内、不许越界,文件大小受 maxSkillBytes 限制,内容同样要过一遍安全扫描,最后再原子落盘,不会写到一半留下半截文件。

三条捕获路径

前面讲 source 字段时提到,一条提案可能从三条不同的路径冒出来(tool / agent_end / reviewer)。这一节就把这三条路径拆开:

skill-workshop-seq.png

1. 工具显式建议:模型看到一段可复用流程、或用户明说「把这个存成 skill」时,直接调 skill_workshop 工具。这是最显式的一条,autoCapture: false 也能用,是关掉自动捕获时唯一的入口。

2. 启发式捕获autoCapture 开 + reviewModeheuristic 时,插件会扫成功那一轮对话里用户消息中的明确纠正短语(也就是上面实战里说的那句话能被捕到的原因)。这套短语列表是写死在 extensions/skill-workshop/src/signals.ts 里的:

const CORRECTION_PATTERNS = [
  /\bnext time\b/i,
  /\bfrom now on\b/i,
  /\bremember to\b/i,
  /\bmake sure to\b/i,
  /\balways\b.{0,80}\b(use|check|verify|record|save|prefer)\b/i,
  /\bprefer\b.{0,120}\b(when|for|instead|use)\b/i,
  /\bwhen asked\b/i,
];

命中之后,会根据内置的那张话题映射表给提案起名,就是前面实战里见过的(animated/gifanimated-gif-workflow,其它一律进 learned-workflows 兜底),这里不再重复。

3. 回看模型(LLM reviewer)reviewModellm 时,攒够阈值(默认 15 轮成功对话或 8 次工具调用)后,起一次紧凑的内嵌回看。这里的「回看模型」其实就是当前对话用的那个模型,起一次独立子调用,重新审一遍对话。这次调用有几个限制:

  • 输入只喂最近 12,000 字对话记录、最多 12 个已有 skill(每个截到 2,000 字);
  • 不给它任何工具;
  • 只允许输出 JSON 格式;

模型返回结果只能是 {"action":"none"} 或一个提案对象:

{
  "action": "create",
  "skillName": "media-asset-qa",
  "title": "Media Asset QA",
  "reason": "Reusable animated media acceptance workflow",
  "description": "Validate externally sourced animated media before product use.",
  "body": "## Workflow\n\n- Verify true animation.\n- Record attribution.\n- Store a local approved copy.\n- Verify in product UI before final reply."
}

其中 action 可以是:create 建新 skill、append 往现有 skill 加 section、replace 替换段落里的精确字符串。

安全扫描兜底

在实战里,我们已经见过提案的三种状态:pending(在队列里排队待批)、applied(应用后已写入磁盘)、rejected(被你手动拒掉)。其实还有第四种 quarantined(被安全扫描拦下隔离),这一节我们就来看下它。

生成 SKILL.md 和支持文件落盘前会先经过安全扫描,这个扫描器不是模型,而是一组写死的正则规则。规则分两档:命中 critical 的提案直接隔离;命中 warn 的只记录,不拦截。

五条 critical 规则(命中即隔离):

  • prompt-injection-ignore-instructions —— 匹配「ignore all/previous instructions」这类经典的越权开场白。skill 正文里如果出现「忽略上面/之前的所有指令」,摆明了是想覆盖更高优先级的系统指令。
  • prompt-injection-system —— 匹配「system prompt」「developer message」「hidden instructions」这些字眼。正经的流程 skill 没理由去提隐藏 prompt 指令,如果提了八成是想撬动或套出上层指令。
  • prompt-injection-tool —— 匹配「调用某工具……无需……许可/审批」这种句式,也就是怂恿小龙虾绕过工具审批关。
  • shell-pipe-to-shell —— 匹配 curl https://... | sh 这种命令,把远程脚本直接输入 shell 运行,等于让小龙虾执行陌生代码。
  • secret-exfiltration —— 匹配同一句里既出现 env / process.env、又出现网络动作(fetch / curl / http 等)的情况,大概率是想把环境变量里的密钥往外发。

两条 warn 规则(只记录、不拦截):

  • destructive-delete —— 匹配 rm -rf /rm -rf ~ 这种冲着根目录 / 家目录 / 当前目录去的大范围删除。不直接拦,是因为正常的清理脚本也可能这么写,留给人工判断。
  • unsafe-permissions —— 匹配 chmod 777 / chmod -R 777 这种把权限全部放开的危险操作,同样只记录、不拦截。

这里 OpenClaw 为什么用死正则、不用模型呢?我想可能有三方面原因:一是确定性,同样的内容每次扫结果一样,使用模型的话不够稳定;二是零成本零延迟,每次写盘都过一遍也不占 token、速度也很快;三是不会被反将一军,它本来就是防 prompt injection 自我投毒的最后一道闸,要是它自己也是个模型,反倒有可能被同一波注入策反。

注意:这里的三条 prompt-injection 规则全是英文关键词(ignore ... instructions / system prompt / ... tool ... without approval),用中文写的注入话术,比如「忽略以上所有指令」「调用工具无需审批」,它就失效了。

小结

今天我们学习了 skill 系列的第三篇,通过 Skill Workshop 让小龙虾在每轮对话后,把可复用流程沉淀成 workspace skill。这也是整个系列的最后一篇,回头看这一路,我们其实是按一个由浅入深的顺序,把一只小龙虾从「只会说话」一点点养成了「能干活、可扩展」的个人 AI 助手。

最开始是接入,我们把它装起来,接上 Telegram 和飞书,让它能在你天天用的聊天软件里收发消息;接着是自动化,靠 cron、heartbeat、webhook、standing orders 这些机制,让它不用你每次戳一下才动,而是能按时间按事件自己运行起来。再往后是协作与隔离,我们给它配齐了后台任务、多 agent 与子 agent、沙箱、远程网关,还有 macOS 和手机上的 Node,让它既能分身同时干好几件事,又能被安排到该跑的机器上、关进该关的沙箱里。

接着,我们学习它的手脚,从内置工具体系,到浏览器 browser 工具,再到能把活转派给外部编码 agent 的 ACP,学习了它能调动的各种能力。最后这几篇则落在手册上,先学习了 skill 的基本原理,然后是通过 ClawHub 安装别人的 skill 以及怎么把自己的 skill 发布出去,再到今天这篇 Skill Workshop,教它怎么在干活的过程中给自己写手册。

走到这里,小龙虾已经不是一个只会回消息的 bot,而是一个有手有脚、有手册、还能在干活中自我升级的 agent。

这个系列就到这里。感谢一路读到最后,现在,去给你自己的小龙虾配齐工具箱吧。

参考


带小龙虾逛 ClawHub:自定义 Skill 实战

上一篇我们把 Skills 系统的基本盘讲完了:一份 SKILL.md 就是一份操作手册,仓库自带 53 个 skill,加上内置插件捎来的 14 个,开箱就有 67 个能用;加载时按优先级合并、按声明的依赖条件筛选,再把每个 skill 的「名字 + 描述 + 路径」拼成一段索引塞进系统提示,正文则由模型在命中后自己用 Read 工具按需加载。

但这 67 个终究是官方给的,对每个人的工作流来说都谈不上贴身。真正让 OpenClaw 生态贴近场景的,是自定义 skill,既包括别人写好放到第三方市场 ClawHub 上的,也包括你自己写出来的。今天我们就先带小龙虾去 ClawHub 上逛逛,挑一个 skill 装到本地试试;然后自己动手写一个最小可用的 sysinfo skill;最后把它发布回 ClawHub,让别人也能装来用。

ClawHub 是什么

ClawHub 是 OpenClaw 官方的 skill 和 plugin 注册中心,站点是 clawhub.ai

clawhub.png

它在 OpenClaw 生态里的位置,类似 npm 之于 Node、PyPI 之于 Python:一个能公开浏览、版本化、可搜索的技能仓库。官方文档把 ClawHub 提供的能力归成下面这张特性表:

特性说明
公开浏览skill 目录和 SKILL.md 全文都可以匿名公开查看
语义搜索走 embedding 向量匹配,而不是只对关键词
版本管理用语义化版本号管理,每次发布生成一个新版本,配带变更说明和标签
下载分发每个版本一份 zip 包
社区反馈支持收藏(star)和评论
安全扫描详情页展示 SkillSpector 和 VirusTotal 安全扫描结果,安装前一眼可见
作者修复面板被扫描扣留的版本,作者能在 /dashboard 看到并申诉复扫
复扫请求误判时作者可以申请有限次数的复扫
人工审核申请审核和审计流程
CLI 友好的 API适合自动化和脚本调用

装一个 skill 试试

ClawHub 上的 skill 怎么装到本地呢?官方提供了两套 CLI 命令行工具,我们可以使用 CLI 手动搜索和安装,也可以直接和小龙虾对话,让它给你安装。

两个 CLI 工具

ClawHub 有两套官方 CLI 入口:

  • openclaw skills:OpenClaw 自家命令族里的一个子命令,装 OpenClaw 时一并就有。负责搜索、安装、更新 ClawHub 上的 skill,外加查看本地 skill 状态(list / info / check)。
  • clawhub:独立的 CLI 工具,靠 npm i -g clawhub 单独安装,覆盖 ClawHub 全部功能,除了上面那些不用登录就能做的搜索、安装类操作,还包括登录、发布、删除、复扫、同步等需要鉴权的写操作。

两者常用命令对照如下:

操作openclaw skillsclawhub备注
搜索openclaw skills search "<query>"clawhub search "<query>"走向量语义搜索,不只匹配关键词
安装openclaw skills install <slug>clawhub install <slug>默认装 latest 标签的版本,拉到 workspace 的 skills/
安装指定版本openclaw skills install <slug> --version <ver>clawhub install <slug> --version <ver>想锁定某个旧版本时用
更新openclaw skills update <slug>clawhub update <slug>.clawhub/lock.json 里的来源元数据重新拉新版本
全部更新openclaw skills update --allclawhub update --all把 lock.json 里登记过的 ClawHub skill 一起升级
查看列表openclaw skills listclawhub listopenclaw skills list 列本地全部 skill(bundled / managed / workspace);clawhub list 只列从 ClawHub 装下来的
查看详情openclaw skills info <slug>看本地 skill 的来源、frontmatter 元数据、当前是否可用
验证openclaw skills check把本地所有 skill 的依赖条件挨个核对,不可用的列出来排障
登录clawhub login默认走浏览器授权;也可以 --token <token> 直接贴 API token
发布clawhub skill publish <path>把本地 skill 目录推到 registry,生成新 semver 版本
删除clawhub delete <slug> --yes作者下架自己发布的 skill
复扫clawhub skill rescan <slug>安全扫描误判时申请重扫(每个版本有次数限制)
同步clawhub sync扫本地 skills 目录,把改过或新增的批量推上去,按内容 hash 比对,不重发

一般来说,安装别人写的 skill 两种 CLI 工具都可以,但 OpenClaw 自家的 openclaw skills 用起来更顺手一些,因为它还兼顾了 list / info / check 这些看本地状态、排查问题的操作,一个命令全部搞定。要自己往市场上发布 skill,就必须切到 clawhub 了,登录、发布、删除、同步这些写操作只它那边有。

来跑一遍直观感受下,比如我们想要找一个做 PPT 的技能,先搜:

$ openclaw skills search "ppt"

ppt  ppt  将用户讲稿一键生成乔布斯风极简科技感竖屏HTML演示稿...
hnytit-ppt-generator  河南油田工程科技PPT生成器  河南油田工程科技股份有限公司专属PPT制作技能...
...

然后选第一个安装:

$ openclaw skills install ppt

Downloading ppt@1.0.0 from ClawHub…
Installing to ~/.openclaw/workspace/skills/ppt…
Installed ppt@1.0.0 -> ~/.openclaw/workspace/skills/ppt

openclaw skills install 拉的就是 ClawHub 上对应 slug + 版本的 zip 包,解压到当前 workspace 的 skills/ 目录下,并往 .clawhub/lock.json 里写一条来源记录。下次 openclaw skills update --all 时它就知道这份 skill 是从 ClawHub 来的、应该去哪里拉新版本。clawhub install ppt 的效果完全一致,只是走的是独立 CLI 的实现。

让小龙虾帮你装

除了 CLI 手动安装之外,OpenClaw 还提供了一种更顺手的玩法。仓库自带了一份名叫 clawhub 的 skill,作用就是把上面的 CLI 包装成大白话:你用人话告诉 agent「装个能做幻灯片的技能」,它自己去搜索、判断、调命令。skill 本身写得非常薄,frontmatter 里只声明依赖一个二进制:

---
name: clawhub
description: Search, install, update, sync, or publish agent skills with the ClawHub CLI and registry.
metadata:
  {
    "openclaw":
      {
        "requires": { "bins": ["clawhub"] },
        "install":
          [
            { "id": "node", "kind": "node", "package": "clawhub", "bins": ["clawhub"], "label": "Install ClawHub CLI (npm)" }
          ],
      },
  }
---

正文部分就是一张 cheat sheet:search、install、update、list、publish 几个子命令各贴了一段示例。agent 加载这个 skill 后,看到用户说要装个能做幻灯片的技能,就知道该去跑 clawhub install

先确认 CLI 装好了:

$ clawhub --cli-version
0.18.0

$ clawhub whoami
not logged in

匿名状态可以装公开 skill,发布才需要 clawhub login。回到 Telegram 或飞书的会话里,直接用大白话跟小龙虾说:

openclaw-skills-install.png

这背后就是 clawhub skill 驱动的,agent 实际做的事和你自己敲 clawhub search + clawhub install 完全等价。装完之后当前会话就能直接用,让它做一个 PPT 试试:

openclaw-skills-use.png

使用浏览器打开,一个 8 页的关于 OpenClaw 的 PPT 就做好了:

openclaw-ppt.png

关于 skill 安全

通过上面的学习我们知道,从 ClawHub 上装一个 skill 又简单又方便,但方便里隐藏着风险。把别人写的 skill 装到本地,本质上就是在跑陌生人写的代码。2026 年以来,ClawHub 上就出过几起规模化投放恶意 skill 的真实事件,因此 skill 安全不能忽略。

ClawHub 的发布门槛刻意做得很低。按官方文档的说法,任何人都能上传 skill,唯一的限制是发布者的 GitHub 账号要至少注册满一周,这意味着市场里什么都有。OpenClaw 官方对第三方 skill 的态度也很明确:把它当作未经审查的代码看待,启用前一定要先读一遍。

为此,ClawHub 这边主要靠两道防线:自动扫描人工举报。每个版本的详情页都挂着一行 Security audit 状态,点进去就是完整的审计报告,由两家扫描器并行出结果:

  • SkillSpector:NVIDIA 出的安全检查工具,对 prompt 注入、数据外泄、权限提升、供应链风险、过度代理几类规则挨条做检查,命中哪条就在哪条上标红;

skill-spector.png

  • VirusTotal:把 skill 的下载包丢进多家杀软引擎跑一遍,将结果汇总,量化判断有没有被投毒;

virustotal.png

没过扫描的版本会从公开安装面下架,普通用户搜不到也装不了,只有作者自己能在 /dashboard 里看到被扣留的版本。如果作者觉得是误判,可以用前面命令表里的 clawhub skill rescan <slug> 申请重扫,每个版本有限次数,避免反复滥用。

第二道防线是人工举报。任何登录用户都能就某个版本发起举报,超过 3 个独立举报,该版本也会被自动隐藏。

最后,skill 装到本地之后,真正的安全其实还是在用户自己手里,可以对照着 skill 的 metadata.openclaw.requires 检查一遍,里面的 bins / env / config 是这份 skill 的权限申报,扫一眼看看合不合理。一个号称只做幻灯片的 skill,不该去读 ~/.openclaw/openclaw.json,也不该往陌生 webhook 发数据。还拿不准的,丢进前面讲过的 sandbox 里跑一段时间再决定。

从零写一个 skill

装别人写好的 skill 我们已经讲完了,下面进入本篇的第二步,自己动手写一份。我们要写的这个叫 sysinfo,目标很朴素:用户随口问一句「这台机器现在剩余多少空间」、「内核是哪个版本」,agent 自动跑 df / uname 给个答案。

仓库里其实带了一个官方推荐的 skill-creator 技能,专门用来指导 agent 一步步创建新 skill:它会先跟你确认需求和触发场景,再生成目录骨架和 SKILL.md 模板,最后做校验和打包。你完全可以直接在 Telegram 或飞书里跟小龙虾说一句「帮我生成一个查系统信息的 skill」,它会按照 skill-creator 的说明,自动把 sysinfo 这份 skill 写出来。不过为了把 SKILL.md 的字段挨个过一遍,我们这次不靠它,纯手写一份只有一个文件的 skill。

第一步,在 workspace 里建目录:

$ mkdir -p ~/.openclaw/workspace/skills/sysinfo

第二步,写 SKILL.md,内容如下:

---
name: sysinfo
description: "用 df / uname / uptime 报告本机的磁盘占用、内存、运行时间和系统信息。"
user-invocable: true
metadata:
  {
    "openclaw":
      {
        "emoji": "🖥️",
        "os": ["darwin", "linux"],
        "requires": { "bins": ["df", "uname", "uptime"] }
      }
  }
---

# sysinfo

当用户询问本机的磁盘空间、内存、运行时长、内核 / 操作系统版本,或者「这台机器现在状态怎么样」时,
通过 `exec` 工具运行对应命令,并用一两句话汇报结果。

## 命令对照

- 各挂载卷的磁盘占用:  `df -h`
- 内核和系统版本:       `uname -a`
- 运行时长与负载:       `uptime`

## 规则

- 总是先把原始命令打出来,再附一句简短总结,不要花哨格式化;
- 不要编造数字——命令失败就老实说;
- 拒绝任何需要 sudo 或会写入文件系统的请求。

frontmatter 字段详解

上一篇介绍 summarize 的 SKILL.md 时已经讲过 name / description / emoji / requires.bins / install 几个字段的含义,sysinfo 这份例子里新引入的字段主要是两个:

  1. user-invocable:设为 true 时,表示这个 skill 同时变成一个 slash command,用户可以直接 /sysinfo 内核版本是什么 强制调用,不用等模型自己判断;
  2. metadata.openclaw.os:把 skill 限定在指定平台,["darwin", "linux"] 表示只在 macOS 和 Linux 上加载,跑在 Windows 上的 OpenClaw 就看不到;

除此之外,frontmatter 里还有几个常用字段虽然 sysinfo 没用到,也一并介绍一下:

requires.env 声明必须存在的环境变量,常用在依赖 API key 才能工作的 skill 上:

"requires": { "env": ["OPENAI_API_KEY"] }

意思是环境变量里必须有 OPENAI_API_KEY,否则不启用该 skill。

requires.config 声明依赖的 OpenClaw 配置项必须开启:

"requires": { "config": ["browser.enabled"] }

意思是 openclaw.jsonbrowser.enabled 配置的值必须为真才会加载,常用在「依赖 browser 工具才能跑」「依赖某个插件启用了才能用」这类场景。

requires.anyBins 声明一组二进制里至少要有一个。比如某个跨平台截图 skill:

"requires": { "anyBins": ["screencapture", "gnome-screenshot", "scrot"] }

三者有任意一个就算可用,解决 macOS / Linux 不同发行版工具名不一致的问题。

primaryEnv 声明这份 skill 默认从哪个环境变量取认证,让用户在 openclaw.json 里能用 apiKey 这个简写配 API key。比如仓库自带的 gh-issues 在 SKILL.md 里写了:

"primaryEnv": "GH_TOKEN"

不声明 primaryEnv 时,用户得在 openclaw.json 里写完整的环境变量名:

{
  skills: {
    entries: {
      "gh-issues": {
        env: { GH_TOKEN: "ghp_xxx" },
      },
    },
  },
}

声明了 primaryEnv 之后,可以直接用 apiKey 简写:

{
  skills: {
    entries: {
      "gh-issues": {
        apiKey: "ghp_xxx",
      },
    },
  },
}

OpenClaw 在 skill 运行前会把这个值注入到 GH_TOKEN 环境变量里。此外,apiKey 比直接写 env.<KEY> 还多了一个好处:它能接 SecretRef 对象,从 keychain、1Password 这类密钥存储里取值:

{
  skills: {
    entries: {
      "gh-issues": {
        apiKey: { source: "keychain", provider: "default", id: "gh-token" },
      },
    },
  },
}

env.<KEY> 字段只接受明文。

skill 验证

SKILL.md 写完之后,先让 OpenClaw 把它扫进来。skill 快照是按会话锁定的,所以新加的 skill 要么 /new 起个新会话,要么直接重启网关:

$ openclaw gateway restart

接着用 CLI 检查一遍,确认 skill 被识别到了,并且当前确实 eligible:

$ openclaw skills list --eligible | grep sysinfo

│ ✓ ready  │ 🖥️ sysinfo │ 用 df / uname / uptime │ openclaw-workspace │

$ openclaw skills info sysinfo

🖥️ sysinfo ✓ Ready

用 df / uname / uptime 报告本机的磁盘占用、内存、运行时间和系统信息。

Details:
  Source: openclaw-workspace
  Path: ~/.openclaw/workspace/skills/sysinfo/SKILL.md
  Visible to model: yes
  Available as command: yes

Requirements:
  Binaries: ✓ df, ✓ uname, ✓ uptime
  OS: ✓ darwin, ✓ linux

CLI 验证没问题之后,回到聊天窗口试两种触发方式:

# 模型自动触发
> 帮我看下这台机器现在还剩多少空间

# 用户显式触发
> /sysinfo 内核版本是什么

第一种走 description 匹配,第二种因为 user-invocable: true 直接以 slash command 命中,效果相同:agent 调一次 exec,把 df -h / uname -a 的原始输出附上一两句总结回给你。

把它发布到 ClawHub

sysinfo 已经在本地跑通了,下面把它发布到 ClawHub 上,让别人也能像我们前面装 ppt 那样一行命令装下来。发布这一步走独立的 clawhub 命令,我们先登录:

$ clawhub login

$ clawhub whoami
✔ aneasystone

clawhub login 默认走浏览器授权,也可以 clawhub login --token <token> 直接贴 token。登录后发布单个 skill:

$ clawhub skill publish ~/.openclaw/workspace/skills/sysinfo \
    --slug sysinfo-demo \
    --name "Sysinfo Demo" \
    --version 1.0.0 \
    --changelog "Initial release" \
    --tags latest

这里的 --slug 是市场上的唯一标识,sysinfo 已经存在了,因此我这里改成了 sysinfo-demo,发布成功后可以在 ClawHub 的 dashboard 中查看:

clawhub-dashboard.png

如果本地有一堆 skill 要一起处理,用 clawhub sync --all 扫一遍当前工作目录全量上传,它会按内容 hash 和 registry 比对,只发新增或有改动的。

发布后 ClawHub 会自动跑一遍安全扫描,扫描状态会显示在详情页上。如果误报了,作者可以在 dashboard 里申请有限次数的重新扫描,或者用 clawhub skill rescan <slug> 触发。

小结

回顾今天的内容,我们围绕自定义 skill 学习了如何从 ClawHub 上装别人写好的 skill、如何自己动手手写一份 skill、以及如何将自己写的 skill 发布回 ClawHub 市场。

前面提过的 skill-creator 虽然能让小龙虾按步骤帮你写 skill,但本质只是一份写作指导文档,触发权还在你手里;OpenClaw 还有个叫 Skill Workshop 的内置插件,方向反过来,每轮会话结束自动扫一遍历史消息,把可复用的流程沉淀成 workspace skill,下一篇我们就来看看它。

参考


给小龙虾写本操作手册:Skills 系统

前面几篇我们把 OpenClaw 的工具挨个过了一遍,但工具终究只是零件。想象一下你跟小龙虾说一句「把 openclaw 仓库里带 bug 标签的 issue 都过一遍,能修的就开个 PR 修掉」,它得先用 execgh issue list 拉 issue 列表,按 label 筛出要动的几个,再用 sessions_spawn 分头派子 agent 去改代码,修完再用 gh pr create 开 PR,最后还得盯着 review 评论看要不要再改一轮。这一串环节该按什么顺序走、被 rate-limit 怎么回退、PR 评审打回来怎么继续,光靠模型每次临场发挥并不稳。它需要一份写好的操作手册。这正是 Skill 这套机制要解决的问题。

围绕 Skills 系统我们打算分三篇讲完:今天这一篇先把基本盘讲清楚,什么是 skill、从哪儿加载、如何参与对话;下一篇带大家逛一逛 ClawHub 市场,再动手写一个自己的 skill 并发布上去;第三篇看一个实验性玩法 Skill Workshop,让 agent 把自己干活时学到的流程自动写成 workspace skill。今天先从最基础的开始。

什么是 Skill

根据官方文档,OpenClaw 的 skill 用的是 Agent Skills 规范:每个 skill 就是磁盘上的一个目录,目录里至少有一份 SKILL.md,文件头是一段 YAML frontmatter,正文是 Markdown 写的操作说明。说白了,Skill 就是给 agent 备的一份操作手册:什么时候该触发、要调哪些底层工具、按什么步骤走、出错了怎么办。它和直接把这些步骤塞进系统提示词最大的区别是按需加载。OpenClaw 只在 agent 真有可能用到时才把它的存在告诉模型,平时不占用上下文;模型觉得用户问题和哪个 skill 相关了,再去把手册正文读出来。

agent-skills.png

最小可用的 SKILL.md 长这样:

---
name: hello-world
description: A simple skill that says hello.
---

# Hello World Skill

When the user asks for a greeting, use the `echo` tool to say
"Hello from your custom skill!".

frontmatter 里 namedescription 是必填的,如果缺一个,这份 skill 直接被丢掉。其中 description 是给模型看的一句话说明,模型靠它判断当前对话要不要使用这个 skill;正文则是给 agent 看的执行手册。

除了 name 和 description 这两个必填字段,OpenClaw 还在 frontmatter 里加了不少自家扩展:声明依赖二进制 / 环境变量 / 配置项的 requires.bins / env / config、控制触发方式的 user-invocable / disable-model-invocation / command-dispatch、限定平台的 os、给桌面端 UI 用的 install 安装提示,以及配合密钥注入的 primaryEnv 等等。具体含义留到后面再细看。

三类来源

OpenClaw 把 skill 按来源分了好几类,信任级别和生命周期各不相同。优先级从高到低依次是:

优先级来源路径
1Workspace skills<workspace>/skills
2Project agent skills<workspace>/.agents/skills
3Personal agent skills~/.agents/skills
4Managed / 本地 skills~/.openclaw/skills
5Bundled skills随安装包发出
6额外目录skills.load.extraDirs(配置里指定)

如果几个来源里有同名 skill,高优先级覆盖低优先级。这套规则保证了用户工作区里自己定制的版本永远盖得住官方版本,方便做覆写和打补丁。中间那两层 .agents/skills 是给项目级或个人级的 agent profile 用的,平时不太会动,一般我们只关心三类:

  • bundled:仓库自带,随安装包发出
  • managed~/.openclaw/skills,从 ClawHub 装下来的或本地打的补丁
  • workspace<workspace>/skills,用户自己写的定制版本

除此之外,插件也可以自带 skill:在 openclaw.plugin.json 里声明一个 skills 目录,插件启用时这些 skill 就会按和 extraDirs 同等的最低优先级合并进来。前面 browser 那篇提过的 browser-automation 手册,就是浏览器插件顺带捎进来的 skill。

多 agent 模式下每个 agent 有自己的工作区,所以 workspace 类的 skill 是 per-agent 的,互相看不到。这一点在企业部署里很关键:同一台机器上同时跑客服 agent 和开发 agent,让他们看不到对方的私有 skill 就能做到权限隔离。另外要提醒一句,第三方 skill 等同于未经审查的代码,启用前一定先读一遍,不放心就丢进沙箱里跑。

仓库自带 skill 一览

先看 bundled 这一类,也就是仓库自带的 skills/ 目录,它会直接被打包进发行版,截至本系列动笔时一共 53 个,覆盖面相当广。下面按主题分组挨个过一遍:

笔记、任务与项目管理

skill用途
apple-notes在 macOS 上通过 memo CLI 创建、查看、编辑、搜索、移动、导出 Apple 笔记
apple-reminders通过 remindctl 增删改查 Apple 提醒事项和列表
bear-notes通过 grizzly CLI 创建、搜索、管理 Bear 笔记
obsidian通过 obsidian-cli 操作 Obsidian vault(纯 Markdown 笔记)
notion用 Notion API 创建、管理 page、database、block
things-mac在 macOS 的 Things 3 里增删改查 todo、inbox、today、project、area、tag
trello通过 Trello REST API 管理 board、list、card
taskflow把多步骤分离任务编排成一份持久的 TaskFlow 作业,带 owner 上下文、状态、等待和子任务
taskflow-inbox-triageTaskFlow 的示例:邮件收件箱分流、意图路由、等回复、回头总结

IM 与社交

skill用途
imsg通过 Messages.app 读写 iMessage / SMS:列会话、看历史、发消息
bluebubbles通过 BlueBubbles 收发 iMessage,支持附件、tapback、编辑、回复、群聊
slack通过 Slack 工具发消息、贴 reaction、pin/unpin、改/删消息、查成员
discord通过 message 工具(channel=discord)做 Discord 日常操作
wacli通过 wacli 给第三方 WhatsApp 发消息或同步/搜历史(不接管你的活跃聊天)
xurl通过 xurl 认证后做 X 发帖、回复、搜索、DM、上传媒体、查粉丝等 v2 API 调用
voice-call通过 OpenClaw 的 voice-call 插件发起语音通话

邮件与办公

skill用途
himalaya用 himalaya 收发、搜、组织 IMAP / SMTP 邮件
gogGoogle Workspace 全家桶 CLI:Gmail、Calendar、Drive、Contacts、Sheets、Docs

编码、GitHub 与 OpenClaw 生态

skill用途
github用 gh CLI 处理 GitHub issue、PR 状态、CI/日志、评论、review、release 和 API 查询
gh-issues自动从 GitHub 拉 issue,派给子 agent 去修,开 PR,跟踪 review
coding-agent把编码任务派给 Codex、Claude Code、OpenCode、Pi,跑在后台进程里
skill-creator创建、编辑、改进、整理、审核、重构 AgentSkills 和 SKILL.md
clawhub和 ClawHub 注册中心交互,搜索、安装、更新、同步、发布 skill
mcporter通过 mcporter 列出、配置、认证、调用、查看 MCP server / 工具(HTTP 或 stdio)
oracle用 oracle CLI 打包 prompt 和文件喂给第二个模型做 debug、refactor、设计或审查
model-usage汇总 CodexBar 的本地成本日志,按模型拆 Codex 或 Claude 的当前/全部花销
session-logs用 jq 搜索分析自己的会话日志(更早或父级会话)

音频、视频与媒体

skill用途
spotify-player在终端里走 spogo(首选)或 spotify_player 控制 Spotify 播放和搜索
sonoscli控制 Sonos 音箱:发现 / 状态 / 播放 / 音量 / 分组
blucliBluOS CLI(blu),发现、播放、分组、调音量
songsee用 songsee CLI 给音频生成频谱图和特征面板
sherpa-onnx-ttssherpa-onnx 本地 TTS(离线、不走云)
sag用 ElevenLabs TTS,仿 macOS say 的 UX
openai-whisper本地 Whisper CLI 做语音转文字(无 API key)
openai-whisper-apiOpenAI Whisper API 转写音频
summarize用 summarize.sh 总结或转写 URL、YouTube 视频、播客、文章、转录稿、PDF 和本地文件
video-frames用 ffmpeg 从视频里抽帧或剪短片段
gifgrep在 GIF 提供方搜索(CLI/TUI),下载并提取静帧 / 拼图
camsnap从 RTSP / ONVIF 摄像头抓帧或录短片
nano-pdf用 nano-pdf CLI 通过自然语言指令编辑 PDF
blogwatcher用 blogwatcher CLI 监控博客和 RSS/Atom feed 的更新

搜索、信息与查询

skill用途
weather查天气、降水、温度、未来几天预报(出行用)
goplaces通过 goplaces 查 Google Places:文本搜索、地点详情、评论、脚本化 JSON
gemini用 Gemini CLI 做 one-shot 问答、总结、生成

系统、家居与杂项

skill用途
1password配置 1Password CLI:登录、桌面端集成、读取/注入 secret
healthcheck审计加固跑 OpenClaw 的主机:SSH、防火墙、更新、暴露面、cron、风险姿态
node-connect排查 Android / iOS / macOS Node 的配对、二维码、路由、认证、连接故障
tmux远程控制 tmux 会话,靠发按键和抓 pane 输出来驱动交互式 CLI
peekaboo用 Peekaboo CLI 抓屏和自动化 macOS UI
canvas在已连接的 OpenClaw Node(Mac/iOS/Android)上展示 HTML 内容,用来跑游戏、可视化、仪表盘
openhue通过 OpenHue CLI 控制 Philips Hue 灯和场景
eightctl控制 Eight Sleep 床垫:状态、温度、闹钟、日程
ordercliFoodora 订单 CLI:查历史订单、看活跃订单状态(Deliveroo 在开发中)

插件自带 skill 一览

除了 skills/ 目录下的内置技能,extensions/ 目录下的内置插件也会顺带捎上自己的 skill,截至本系列动笔时,共有 8 个内置插件贡献了 14 个 skill:

插件skill用途
acpxacp-router把用户的自然语言请求路由到合适的 ACP harness(Claude Code、Codex、Cursor、Gemini CLI、Kimi、Qwen 等),相当于 ACP 的路由器
browserbrowser-automationbrowser 工具控制网页时的操作手册:多步流程、登录检查、tab 管理、stale ref 恢复
diffsdiffsdiffs 工具生成真正可分享的 diff(viewer URL 或文件 artifact),而不是让 agent 手写「我改了哪些行」的总结
feishufeishu-doc飞书云文档读写、docx 表格创建
feishufeishu-drive飞书云空间的文件夹和文件管理
feishufeishu-perm飞书文档和文件的权限、协作者管理
feishufeishu-wiki飞书知识库导航
memory-wikiobsidian-vault-maintainer把记忆 wiki 维护成 Obsidian 友好格式:wikilink、frontmatter、obsidian-cli 配合
memory-wikiwiki-maintainer维护 OpenClaw memory wiki:确定性页面、托管块、源回溯更新
open-proseproseOpenProse VM 的 skill pack,处理 prose 命令和 .prose 文件,编排多 agent 工作流
qqbotqqbot-channelQQ 频道管理:列频道、子频道、成员、发帖、公告、日程
qqbotqqbot-mediaQQ 富媒体收发:图片、语音、视频、文件,靠扩展名自动识别
qqbotqqbot-remindQQ 定时提醒:一次性 + 周期性的创建、查询、取消
tavilytavilyTavily 网页搜索、内容抽取和研究类工具的入口

这里有两点值得留意。一是每个插件支持挂多个 skill,feishuqqbotmemory-wiki 都拆成了好几份,触发能更精准,用户问飞书权限只会激活 feishu-perm,不会把发文档的步骤一起喂进来,代价是 skill 列表涨得快,得靠 description 写得准来避免互相误触。二是这些 skill 都跟着所属插件走,不需要单独配置,如果插件没有启用,对应的 skill 也就不会启用。

Skill 实战

这一节我们以 summarize 技能为例,实战一下 skill 的用法。先看下它的 SKILL.md 头部声明了哪些东西:

---
name: summarize
description: Summarize or transcribe URLs, YouTube/videos, podcasts, articles, transcripts, PDFs, and local files.
homepage: https://summarize.sh
metadata:
  {
    "openclaw":
      {
        "emoji": "🧾",
        "requires": { "bins": ["summarize"] },
        "install":
          [
            {
              "id": "brew",
              "kind": "brew",
              "formula": "steipete/tap/summarize",
              "bins": ["summarize"],
              "label": "Install summarize (brew)",
            },
          ],
      },
  }
---

这块信息量浓缩了几条:

  • description:列了它能处理的输入类型(URL、YouTube、播客、文章、转录稿、PDF、本地文件),模型靠这一行决定是否进入;
  • homepage:给 UI 用,显示成「Website」链接;
  • emoji: 🧾:纯粹是给 Skills 列表前面挂个图标,不影响功能;
  • requires.bins: ["summarize"]:这份 skill 的启用条件只有一条,PATH 上必须有 summarize 这个二进制,否则 OpenClaw 加载时就把它过滤掉;
  • install:装这条二进制的官方建议,当你使用 openclaw configure 走 onboarding 流程时就会读这一段自动安装;

bundled skill 默认是启用的,只要上面的依赖满足了,该 skill 就能用。我们可以运行 openclaw configure 命令,自动安装依赖:

configure-skills.png

或者在 Control UI 的 Skills 列表中找到该 skill,点击安装:

summarize.png

也可以照着 install 那段,手动安装依赖:

$ brew install steipete/tap/summarize

安装完成后,检查 summarize 是否可用:

$ summarize --version
0.16.3

虽然该 skill 的 requires 里没讲,但实际上该 CLI 工具还需要配置一个模型的 API key,比如:

$ openclaw config set skills.entries.summarize.env.GEMINI_API_KEY "your-key-here"

改完配置要让 OpenClaw 起一个新会话才生效,skill 快照是按会话锁定的,老会话不会自动刷新。在聊天窗口里输 /new 起个新会话,或者直接重启网关:

$ openclaw gateway restart

接着到 Telegram 或飞书里跟小龙虾说一句:

帮我总结下这个视频:https://youtu.be/dQw4w9WgXcQ

agent 看到 URL 加上一个总结意图,于是命中 summarize 的触发条件,它会先使用 read 阅读该技能的 SKILL.md 文件,然后按照说明运行 summarize "<url>" --youtube auto 命令,再把结果整理一下回给我们。

我们可以在 Control UI 中看到执行流程:

summarize-telegram-toolcall.png

聊天界面大致是这样:

summarize-telegram.png

Skill 加载原理

实战完了之后,我们回过头看看 Skills 的工作原理。从 Gateway 启动到 skill 出现在对话里,整条链路画成图大概是这样:

skill-seq.png

整条链路其实就两件事:先把 skill 集齐(扫描 → 去重 → 过滤 → 注入),再把它装进对话(拼 XML → 进 system prompt → 按需 Read 正文)。我们一步步看:

  • 扫描:从前面讲过的 6 个来源各扫一遍,每个目录读 SKILL.md、解析 frontmatter,缺 namedescription 直接丢弃;
  • 合并去重:同名 skill 按优先级表保留高优先级的那一份;
  • gating 过滤:对每份 skill 核对 metadata.openclaw.requires.bins / env / config / os 等条件,不满足就当做不可用,直接踢出列表,这一步保证模型只看到当前真正跑的 skill,不会去尝试一个根本调不起来的命令;
  • per-agent allowlist 过滤:再按 agents.list[*].skillsagents.defaults.skills 把当前 agent 不让看的那些过滤掉;
  • env / apiKey 注入:对剩下的 skill,把 skills.entries.<name>.envapiKey 临时塞进 process.env,每轮 agent 会话结束再还原。

到这里,当前会话能用的 skill 集合就锁定了。接下来是它怎么进对话,OpenClaw 并不会把每份 SKILL.md 的正文都拼进 system prompt,而是拼成一段 索引型 XML 格式:

<available_skills>
  <skill>
    <name>summarize</name>
    <description>Summarize or transcribe URLs, YouTube/videos, ...</description>
    <location>/Users/.../skills/summarize/SKILL.md</location>
  </skill>
  ...
</available_skills>

每条只有三个字段:name 是身份description 是触发说明location 是 SKILL.md 的绝对路径。XML 前面还附了一段说明告诉模型:

When the task matches a skill's description, **use the read tool to load** the skill's file.

翻译过来就是:若某个 skill 的描述贴合当前任务,便循着它的 location,用 Read 工具取来正文细读。这本质上是和 Anthropic 那套渐进式披露同源的设计:只把摘要放进上下文,正文按需加载。

为了防止 description 太长把这层索引撑爆,OpenClaw 还留了个 compact 降级:如果整段 XML 超过 maxSkillsPromptChars 上限,就自动把 description 去掉、只留 name 和 location,模型仍然知道有这么一份 skill 存在,只是判断要不要打开时少了点上下文。降级时 prompt 顶端会附一行 ⚠️ Skills catalog using compact format,方便排查。

和 Anthropic 官方 Agent Skills 的区别

OpenClaw 的 skill 格式不是自己另发明的。SKILL.md 加 YAML frontmatter 这套,源头是 Anthropic 在 2025 年 10 月推出的 Agent Skills 规范,现在已经发展成一个被 Claude Code、Codex、Cursor 等一批工具共同采用的开放标准,OpenClaw 也完全兼容 Agent Skills 格式:一个目录、一份 SKILL.md、frontmatter 里必填 namedescription、正文是给模型看的操作说明,加载策略也都是渐进式披露,先只让模型看到 name + description,匹配上了再读完整正文。

但是两者在运行时机制上还是有些差别的:

  • Anthropic 官方走的是软门控:能不能用某个 skill 几乎完全看 description 写得准不准、模型自己怎么判断。它默认假设 agent 自带文件系统和代码执行环境,能自己决定什么时候去读更多、什么时候跑脚本,所以官方仓库里的 skill 常常做成「一份 SKILL.md + 一整套配套脚本和模板」的形态(references/scripts/ 这些子目录),运行时让模型把脚本通过 code execution 去执行。
  • OpenClaw 在它前面再叠了一层硬门控metadata.openclaw.requires.bins / env / config / os 在加载期就把跑不了的 skill 直接筛出去,根本不进 system prompt 给模型添乱;再叠一层 per-agent allowlist,让同一个网关上多个 agent 看到的 skill 列表完全不同;再叠一层中心化的密钥注入,把 skills.entries.<name>.apiKey 在一轮会话开始时塞进环境变量、结束再还原。

将两者的差异对比总结成表格如下:

维度Anthropic 官方 Agent SkillsOpenClaw
进上下文方式渐进式披露:description → 正文 → 附件渐进式披露之外多一层 compact 降级,预算紧时去掉 description 只留 name + location
触发门控主要靠 description 让模型判断description 之外还有 requires.bins/env/config、os 硬门控
来源与优先级personal / project / plugin 几个位置6 级 precedence 加 per-agent allowlist
密钥注入靠环境或容器自带中心化 skills.entries.env/apiKey,按 run 注入再还原
默认重心偏 bundled 脚本,使用 code execution 跑脚本偏纯 SKILL.md,指挥 agent 调 OpenClaw 自己的工具
额外入口以模型调用为主可同时是 slash command,还能 bypass 模型直派工具

这里也能看出 OpenClaw 的定位,它面向的不是「一个独立的 IDE 用户」,而是「一个常驻、多账户、多 agent 的网关」。

小结

回顾今天的学习内容,Skills 系统其实就一件事:给 agent 备一份操作手册,一个目录,一份 SKILL.md,文件头声明元数据,正文写操作步骤。

从加载到出现在对话里,一共六步:扫描所有来源目录、按优先级合并去重、按声明的依赖条件过滤掉跑不了的、注入配置里写好的环境变量、把每个 skill 的「名字 + 描述 + 路径」拼成一段索引塞进系统提示,最后由模型按需把对应的 SKILL.md 正文读进来。

这套设计和 Anthropic 官方的 Agent Skills 同源,核心都是按需加载,差别在于 OpenClaw 在模型自行判断之前,先按二进制、环境变量、配置项、平台是否满足做了一道硬筛,把跑不了的 skill 提前挡掉;又把所有密钥注入收到一处统一管理,既兼容开放标准,又留住了网关侧的治理。

到这里,我们对 OpenClaw 的 Skills 系统也学习的差不多了。不过内置的 53 个 skill 和插件捎进来的 14 个 skill 终究是官方给的,真正让 OpenClaw 生态长起来的,是一个公开的第三方 skill 注册中心,那就是 ClawHub。我们下一篇继续。

参考


让小龙虾给 Claude Code 派活:学习 OpenClaw 的 ACP 工具

用了好几篇把 OpenClaw 的内置工具箱挨个过完,又花了两篇专门讲浏览器,小龙虾「自己能干哪些活」到这里基本就讲齐了。今天换个方向,看它的另一项能力:收到任务后,它不亲自处理,而是把整个任务交给一个真正的外部编码 agent 去完成。

这事其实和之前「让小龙虾分身」那篇里讲的 sub-agents 有点像。当时我们看过,主 agent 跑到一半可以用 sessions_spawn 把子任务派给后台的子 agent。但那些子 agent 都是 OpenClaw 自己的 agent,同一套运行时、同一批工具、同一份 system prompt,本质上还是小龙虾在跟自己的分身协作。

ACP 要解决的是另一种诉求:派出去的不是 OpenClaw 子 agent,而是 Claude Code、Gemini CLI、Cursor、Codex 这些外部编码 harness。你在飞书上给小龙虾发一句「把这个仓库里的调试日志清理一下」,它接到之后并不自己写代码,而是在网关那台机器上拉起一个真正的 Claude Code 进程,让 Claude Code 去改文件、跑命令,干完再把结果播报回飞书。

OpenClaw 的文档和命令里把这些外部 agent 统称为 harness,本文后面也沿用这个叫法。

标准的 ACP 协议

动手之前,先花点篇幅认识一下 ACP 这套协议本身,它并不是 OpenClaw 自创的东西。

ACP 是 Agent Client Protocol(Agent 客户端协议) 的缩写,由开发 Zed 编辑器的团队提出并开源(Apache 许可),项目主页在 agentclientprotocol.com。它要解决的是一个典型的 N×M 问题:一边是越来越多的编辑器(Zed、JetBrains 系、各种 CLI),另一边是越来越多的 AI 编码 agent(Claude Code、Gemini CLI、Codex 等)。如果每个编辑器都要为每个 agent 单独写一套对接,每个 agent 又得反过来适配每个编辑器的私有接口,组合数量很快就会失控,用户也被绑死在某个特定的「编辑器 + agent」组合上。

熟悉 LSP(Language Server Protocol,语言服务器协议) 的同学对这个套路应该不陌生。当年 LSP 用一套标准协议把「编辑器」和「语言能力」解耦,任何编辑器配任何语言服务器都能用;ACP 想做的是同一件事,只是解耦的两端换成了「编辑器」和「编码 agent」。

协议本身定义了两个角色:

  • Client:通常是编辑器,或者任何想接入 agent 的宿主程序,它掌握着工作区、权限和界面。
  • Agent:真正干编码活的 AI 工具,比如 Claude Code。

技术实现上,ACP 走的是 JSON-RPC 2.0 over stdio:Client 把 Agent 作为一个子进程拉起来,双方通过标准输入输出收发 JSON-RPC 消息。一次典型的交互大致是:Client 先 initialize 握手,再用 session/new 开一个会话,然后把用户的指令通过 session/prompt 发过去;Agent 干活的过程中,靠 JSON-RPC 的通知把进展实时流式推回 Client,需要读写文件或执行命令时,则用反向请求回头向 Client 申请权限。整个过程中,掌权的始终是 Client:权限给不给、界面怎么渲染、能碰工作区里的哪些东西,都由 Client 说了算,Agent 只能请求、不能擅自越界。

把上面这条交互链画成时序图,大致是这样:

acp-seq.png

如果你了解 MCP(Model Context Protocol,模型上下文协议),会觉得上面这套流程很眼熟:同样是 JSON-RPC 2.0、同样把对方作为子进程通过 stdio 拉起来、同样有 initialize 握手和流式通知。这并不是巧合,ACP 在设计上就刻意向 MCP 看齐、复用它的传输模型。两者的区别在于解决的问题不同:MCP 标准化的是「agent 如何连上工具和数据源」,这时 agent 是 client,MCP server 提供工具;ACP 标准化的是「编辑器或宿主如何连上一个 agent」,这时编辑器是 client,agent 负责编码。它们还能叠在一起用:使用 session/new 开新会话时,可以顺带把要连的 MCP server 一起声明掉,让被拉起来的 ACP Agent 自己再去当一回 MCP Client,连上它需要的工具。

acp.png

讲清楚标准协议,再看 OpenClaw 的位置就顺理成章了:在 ACP 这套协议里,OpenClaw 扮演的是 Client 角色。平时 Zed、JetBrains 是用 ACP 在编辑器里接入 Claude Code,OpenClaw 则是在一个聊天网关里做同样的事。它通过官方的 @openclaw/acpx 后端插件,把 Claude Code、Codex、Cursor、Copilot、Droid、Gemini CLI、OpenCode、Qwen 等一长串 agent 当成子进程拉起来,让你可以通过小龙虾去跟它们对话。

值得注意的是,OpenClaw 反过来也能充当 server:openclaw mcp serve 把它暴露成 MCP server、openclaw acp 把它暴露成 ACP server 供外部客户端或 IDE 连进来,方向和本文讲的正好相反,感兴趣的同学可以尝试一下。

安装 acpx 插件

ACP 的后端是个独立插件,先把它装上并启用:

$ openclaw plugins install @openclaw/acpx
$ openclaw config set plugins.entries.acpx.enabled true

如果你是从源码 checkout 跑的 OpenClaw,仓库里的 extensions/acpx 就是 acpx 的工作区版本,pnpm install 装完依赖后这个插件直接可用,前面那条 openclaw plugins install 去 npm 拉发布版的步骤就不用跑了。

还有一步建议顺手做掉:ACP 默认拿 codex 当就绪探针,而本篇以 Claude Code 为主,所以把探针 agent 换成 claude

$ openclaw config set plugins.entries.acpx.config.probeAgent claude

配好之后,跑一下就绪检查:

$ /acp doctor

如果一切顺利,会显示 healthy: yes

acp-doctor.png

开始派活之前,还有一点要注意,各个编码 harness 得在宿主机上提前登录好。OpenClaw 只负责把 harness 进程拉起来,碰不到 harness 进程内部,所以这一步必须自己完成:要派 claude,宿主机上得先有 Claude Code 的登录态;要派 gemini,得配好 Gemini CLI 的认证;其它 harness 同理。

第一次派活

环境就绪后,我们走一遍最典型的流程:在飞书或 Telegram 这种真实 IM 频道里拉起一个 Claude Code,让它去改一个真实的仓库。

第一步,在飞书或 Telegram 里找一个 OpenClaw 已经接入的会话,发 spawn 命令:

$ /acp spawn claude --bind here --cwd /Users/zhangchangzhi/Codes/demo/sudoku

--bind here 是这条命令的关键,它把当前这个对话直接绑到新起的 ACP 会话上;--cwd 指定 Claude Code 干活的目录,要写完整的绝对路径。绑定一旦建立,这个会话里之后发的每一句话,都会被直接路由给 Claude Code,它的输出也回到同一个会话里。绑定之后,这个对话就成了你和那台机器上 Claude Code 的一条直连通道。

acp-spawn.png

--bind here 要求所在频道支持「当前对话绑定」能力。飞书、Telegram、Discord、Slack 这类 IM 频道都支持;本地的 webchat / TUI 没有这个抽象,跑这条命令会直接报 Conversation bindings are unavailable for webchat。这种情况要么改到 IM 频道里演示,要么去掉 --bind here、让 agent 自己用 sessions_spawn 把活派到后台跑。

接着就可以在这个对话里直接给它派活,跟平时用 Claude Code 没两样:

介绍下这个项目

acp-spawn-claude.png

可以看到,Claude Code 真的在那台机器上跑起来了:它读取文件,总结代码,过程和你在终端里亲自敲 claude 一模一样,只不过这一切是被小龙虾代理着,发生在聊天频道里。中途想看看它现在是个什么状态,随时一条 /acp status

$ /acp status

status 会把这个会话的后端、绑定的 harness、当前模式(persistent 还是 oneshot)、运行状态、各项运行时选项和能力都列出来;要是上一轮出过错,lastError 里也会留着。

任务做完后,收尾有两条命令:

$ /acp cancel   # 只中止当前这一轮,会话还留着,能接着发指令
$ /acp close    # 从 OpenClaw 视角结束会话并解绑当前对话

cancel 只在 harness 支持取消时中止当前会话轮次,它并不会删除绑定和会话元数据,停下来之后你还能继续给它发新指令。close 才是真正的结束,它从 OpenClaw 这边结束会话,解除当前对话的绑定。

非交互权限

在真正使用过程中,你会很快撞上 ACP 实战里最常见的一个坑。

接着上面那个例子,假设你在飞书里给绑定好的 Claude Code 派一句「帮我加一个计时器功能」,期待它直接动手改文件。结果会发现飞书这一头迟迟没有动静,过了一会儿就超时报错了。回到网关那台机器的终端一看,原来是 Claude Code 想写文件时弹了一条权限请求,一直在那等你按 y/n 确认:

[permission] Allow Edit src/i18n/locales/zh.ts [edit]?  (y/N)

Claude Code 要写文件的时候,按 ACP 协议会向 OpenClaw 这边的 Client 反向发出权限请求;OpenClaw 默认配置下需要询问用户,也不知道是不是 OpenClaw 的 bug,ACP 反向权限请求目前不会被转发到飞书 / Telegram 这些聊天频道,只会将弹窗渲染到了网关进程的本地 TTY 上,因此你在飞书里不会收到任何通知。也就是说,只要批准这一动作的人不在网关那台机器的 TTY 前,权限请求就没人能批。如果网关是以 daemon / systemd 这种无 TTY 方式启动,甚至会直接以 AcpRuntimeError: Permission prompt unavailable in non-interactive mode 报错中止。

下面是我以 TTY 方式启动网关后弹出的权限请求:

acp-edit-permission-tty.png

你需要一个个的按 y 确认,可以看到权限弹框和日志混在一起,既不直观,也不方便。为此,OpenClaw 提供了两个配置项管这件事:

  • permissionMode:粗粒度总开关,三种取值。approve-all 全部放行;approve-reads默认)只放行读,写操作和命令执行仍走审批;deny-all 把所有请求全部拒掉,实际上等于把 Claude Code 锁死,一般是用不到的。
  • nonInteractivePermissions:该开关只在 permissionMode=approve-reads 当前没有 TTY 时才生效。fail默认)直接以 AcpRuntimeError 中止会话;deny 把这次操作静默拒掉,会话仍然继续跑。

所以实际可用的组合其实就两种:

  • permissionMode=approve-all:让 harness 全自动改文件或跑命令,这也是最常见的设置;
  • permissionMode=approve-reads + nonInteractivePermissions=deny:不放权写操作但会话会正常进行;

对应的两条命令如下:

# 选项一:让 harness 全自动放行读写和 shell(破窗开关,请确认 cwd 和权限范围可控)
$ openclaw config set plugins.entries.acpx.config.permissionMode approve-all

# 选项二:不放权,但让它静默拒绝后继续,而不是让整个会话中止
$ openclaw config set plugins.entries.acpx.config.nonInteractivePermissions deny

选项一是把 permissionMode 调成 approve-all,让 harness 写文件、跑命令都不再过问。它是 ACP 会话的破窗(break-glass)开关,效果最直接,但也意味着这个会话能在 cwd 范围内不受限制地操作,开之前务必确认目录和权限范围是你能接受的。选项二保守得多,权限照旧不放,只是把 nonInteractivePermissionsfail 改成 deny,让那些过不了审批的操作被静默拒掉、会话继续往下走,至少不会因为一次写文件就让整个会话中止。

我这里选择选项一,就可以在聊天窗口里愉快的写代码了:

acp-edit-ok.png

要注意的是,关于 acpx 这套 harness 权限,和我们在工具篇讲过的 tools.exec.security 审批配置,是两套完全独立的东西。前者管 ACP harness 在它自己进程里的行为,后者管 OpenClaw 内置 exec 工具的审批,互不影响。另一个差异是:tools.exec.security 已经支持把审批弹窗推到飞书 / Telegram 让你点卡片,ACP 目前还没看到这样的实现。

命令速查

前面用到的 spawnstatuscancelclose 只是 /acp 的一部分。这里将全部子命令列出来供参考:

acp-commands.png

这里有几点值得展开说说。

其一,几乎每条命令后面都能跟一个目标参数,可以是会话 key、会话 id 或会话 label 三种写法。不带目标时,作用在当前绑定的会话上;想操作另一个会话,先 /acp sessions 把它的 key 或 label 列出来,再贴到命令后面。

其二,/acp spawn 有几个常用的参数:

  • --mode persistent|oneshot 决定会话生命周期
  • --bind here|off 决定要不要绑当前对话
  • --thread auto|here|off 决定要不要绑到「线程」
  • --cwd <path> 指工作目录
  • --label <name> 给会话起个好记的名字

会话生命周期有两种:persistent 开的是持久会话,spawn 完之后 ACP 会话一直留着,绑定的对话里之后发的消息都接着走它,直到你显式 /acp closeoneshot 开的是一次性会话,跑完当前轮次会话就自动收尾、元数据清掉,再发消息得重新 spawn;手动在对话里敲 /acp spawn 默认走 persistent,因为你这时通常想持续跟 harness 对话。

另外上面的「线程」指的是聊天频道对话内部的子会话面,每个频道叫法不同:Discord、Slack 里都叫 Thread(针对一条父消息开出的回复线索),Telegram 群里叫 Topic(开了 Topic 模式的群里那一栏栏话题),飞书群里也叫话题。三个取值的含义是:--thread off 不绑任何线程;--thread here 要求你当前就在某个线程里敲这条命令,会把那个线程绑到新 ACP 会话上,否则报错;--thread auto 自动判断,在线程里就绑当前线程,不在线程里则在频道支持的前提下新开一个子线程来承载这个 ACP 会话,原对话不受打扰。

要注意 --bind here--thread ... 不能各自独立生效:前者把整条对话(DM/群/频道本体)原地钉住、不另开任何线程,所有消息都直接走 ACP;后者只钉住一个子线程,原对话里其它消息照常进 agent。两者同时传时 --bind here 优先级更高,--thread 会被忽略。

其三,调参那几条命令背后其实是写运行时的配置项:比如 /acp modelmodel/acp permissionsapproval_policy/acp timeouttimeout,剩下叫不上名的就用通用的 /acp set <key> <value> 兜底,想一键还原就 /acp reset-options

两种派活方式

前面 /acp spawn 那个例子是人在对话里手动绑定,属于 ACP 的第一种派活方式:交互式绑定会话,它将当前对话钉到一个 ACP 会话上,之后这个对话里的消息直接转给 harness,输出回到同一通道。其实还有第二种派活方式,直接用自然语言的方式,让 agent 用 sessions_spawn 工具把活派给 Claude Code 去跑:

sessions-spawn-acp.png

我们可以打开 OpenClaw 的 Control UI 页面,找到这次对话:

webchat-tool-use.png

这里可以看到 agent 调用了两次工具:

第一次是 Read,读的是 ~/.openclaw/plugin-skills/acp-router/SKILL.md,这是 OpenClaw 内置的「ACP 派活路由」skill 文件,agent 在动手 spawn 之前先翻了下手册看该怎么派(skill 体系下一篇会专门讲)。

第二次才是真正干活的 sessions_spawn,传入的参数如下:

{
  "mode": "run",
  "agentId": "claude",
  "runtime": "acp",
  "task": "..."
}

这本质上是个后台子任务,每次运行会生成一条 background task 记录,和前面讲的子 agent 用的是同一个工具、同一套机制,只是将参数 runtime 换成了 acp(默认值是 subagent)。和子 agent 一样,后端的 harness 运行时不会卡住主会话,运行结束后会自动通知到当前会话。可以将它和子 agent 放一起对照一下:

acp-vs-subagents.png

参数 agentId 用于指定派给哪个 harness,省略时会用配置里的 acp.defaultAgent(如果设了的话)。常用的几个如下:

harness-id.png

除这些之外,清单里还有 iflowkilocodekiropi,以及一个比较特别的 openclaw,它走的是 openclaw acp 桥接,让一个支持 ACP 的 harness 反过来连回 OpenClaw 的会话。它们都可以作为 /acp spawn <id>sessions_spawn({ runtime: "acp", agentId: "<id>" }) 的目标。

参数 mode 和前面 /acp spawn--mode 是同一件事换了个名字,run 对应 oneshot、session 对应 persistent,后者还得配合 thread: true 才能真的留住绑定。

参数 task 值得注意,我们原话只是「总结一下这个项目」这种口语化指令,agent 派给子任务时却自己把它扩成了一段结构化的英文 prompt:

Summarize this project at /Users/zhangchangzhi/Codes/demo/sudoku. Focus on:
1. What the project does
2. Project structure and key files
3. Main technologies used
4. How to run it

Return a clear, concise summary.

这正是 sessions_spawn 设计上鼓励的做法:父 agent 拿到模糊指令后自己先把它翻译成清晰、可执行的任务描述,再用 task 字段独立派出去,子会话拿到的是一份完整自洽的任务说明,不需要把整段聊天上下文一起 fork 过去。

进阶用法

通过上面的学习,我们已经将 OpenClaw 的 ACP 工具基本流程跑通了,除了上面介绍的内容,ACP 还有一些进阶玩法,感兴趣的同学可以尝试一下。

MCP 桥接

默认情况下,OpenClaw 的内置工具和插件工具并不会暴露给 ACP harness。Claude Code 在 ACP 会话里用的还是它自己那套原生工具,碰不到小龙虾的 cronmessage 这些。如果你确实想把 OpenClaw 的某些能力透出去给 harness 用,acpx 提供了两个默认关闭的 MCP 桥接开关:

# 把已启用的「插件工具」透给 harness
$ openclaw config set plugins.entries.acpx.config.pluginToolsMcpBridge true
# 把选定的「内置核心工具」(目前是 cron)透给 harness
$ openclaw config set plugins.entries.acpx.config.openClawToolsMcpBridge true

打开后,acpx 会在 ACP 会话启动时分别注入一个名为 openclaw-plugin-toolsopenclaw-tools 的内置 MCP server,harness 就能通过 MCP 调到这些工具了。要注意,这等于扩大了外部 harness 的能力面,开之前最好先盘一遍当前装了哪些插件。把插件工具透给 harness,相当于让 harness 拥有了和这些插件在 OpenClaw 内部执行同等的信任级别。

常驻绑定

前面 /acp spawn --bind here 是临时绑定,对话一关、会话一 close 就没了。如果你想要的是长期效果,比如 Discord 上那个 #codex 频道,以后所有消息都直接进一个常驻的 Codex ACP 会话,可以直接在配置里声明一条 type: "acp" 的绑定:

{
  agents: {
    list: [
      {
        id: "codex",
        runtime: {
          type: "acp",
          acp: { agent: "codex", backend: "acpx", mode: "persistent", cwd: "/workspace/repo" },
        },
      },
    ],
  },
  bindings: [
    {
      type: "acp",
      agentId: "codex",
      match: { channel: "discord", accountId: "default", peer: { kind: "channel", id: "222222222222222222" } },
      acp: { label: "codex-main" },
    },
  ],
}

这样配好之后,那个频道就成了一个常驻的 Codex 工作台,不用每次手动 spawn。它和我们在「让小龙虾分身」那篇讲的普通 type: "route" 路由规则差不多,只是把目标换成了一个 ACP 运行时。

小结

最后,我们来总结下今天学习的内容:

  1. ACP 是把活派给外部编码 agent 的标准协议。它由 Zed 团队提出并开源,用来解决编辑器和编码 agent 之间 N×M 对接的麻烦。OpenClaw 在这套协议里扮演 Client 的角色,通过 acpx 后端插件把 Claude Code、Gemini CLI、Codex 这些 harness 当成子进程拉起来,让你在飞书 / Telegram 这种聊天频道里就能调度它们。
  2. 环境准备并不复杂。把 acpx 装上、启用,把就绪探针指到你常用的 harness,跑一次 /acp doctor 看是否正常即可。harness 自己的厂商登录得在网关那台机器上提前准备好,OpenClaw 只管把进程拉起来,碰不到它们内部。
  3. 派活有两种姿势。你本人想在某个聊天频道里持续盯着 harness 干活,就 /acp spawn --bind here 把整个对话接到它上面;让另一个 agent 在自己轮次里甩一个后台任务出去,则用 sessions_spawn 把 runtime 换成 acp,按后台 task 跑完播报回来。
  4. 非交互权限是头号坑。ACP 的反向权限弹窗目前不会被转发到聊天频道,所以远程操作时几乎都得把 acpx 的 permissionMode 调成 approve-all,让 harness 在你给的工作目录里自由读写。
  5. 两个进阶用法:MCP 桥接能把 OpenClaw 的工具透给 harness 调用;常驻绑定能把一个聊天频道钉到某个 ACP 会话上当工作台用。

到这儿,小龙虾的工具体系算是彻底讲完了:内置工具让它能直接动手处理各种操作,浏览器是其中能力最强的一种,ACP 则是把整个任务交给外部 agent 来完成的方式。不过,工具和外部 agent 终究只是一个个单独的能力,agent 接到一个稍复杂的任务,该按什么顺序调用、中间出错怎么回退、什么场景触发哪套流程,光靠模型临场发挥并不稳定,它还需要一份事先写好的操作手册,这就是 OpenClaw 的 Skills 系统。我们下一篇继续~

参考


给小龙虾配个浏览器:学习 browser 工具(二)

上一篇我们把 browser 工具的运行环境从头捋了一遍:首先学习了它的参数定义,然后使用 host / sandbox / node 确定浏览器运行在哪里,以及使用 profile 确定运行哪个浏览器。环境备齐,这一篇书接上文,来看 agent 怎么驱动浏览器在页面上点点点,以及这套动作背后的原理。

标签页管理

browser 一次只在一个标签页上干活,所以为了保证后续的动作没问题,我们首先要认准操作哪个标签页。我们可以使用 openclaw browser <动作> 命令行来管理它,开新标签页用 open,网址作参数,再用 --label 顺手贴个标签:

$ openclaw browser open https://www.aneasystone.com --label blog

opened: https://www.aneasystone.com/
tab: t1
label: blog
id: 7E73979D64CBF48058818D78D50FB94B

该命令返回了几个参数:tab 是形如 t1 的稳定 tabIdlabel 是我们自己起的 blog 标签,id 则是 CDP 协议的原始 targetId。这几个参数都可以作为后续操作这个标签页时的标识。比如之后想再操作它,用 focus 把它切到前台就行,tabIdlabeltargetId 这三种都认,下面三条指向的都是同一个页面:

$ openclaw browser focus t1          # 用 tabId
$ openclaw browser focus blog        # 用 label
$ openclaw browser focus 7E73979D    # 用 targetId(前缀也认)

顺带分清一个常被搞混的点:open 每次都新开一个标签页,而 navigate 是让当前标签页原地跳到另一个网址,所以常见用法是先 focus 选中、再 navigate 到某个网址;也可以加上可选的 --target-id 直接跳转:

$ openclaw browser navigate https://www.aneasystone.com/about-me.html --target-id blog

再开一个不带 label 的,然后用 tabs 列出所有的标签页:

$ openclaw browser open https://www.baidu.com

opened: https://www.baidu.com/
tab: t2
id: 9B0D7E3F1A2C4856E7F0A1B2C3D4E5F6

$ openclaw browser tabs

1. aneasystone's blog [t1 label:blog]
   https://www.aneasystone.com/
   id: 7E73979D64CBF48058818D78D50FB94B
2. 百度一下,你就知道 [t2]
   https://www.baidu.com/
   id: 9B0D7E3F1A2C4856E7F0A1B2C3D4E5F6

我们还可以加一个 --json 参数,看到完整的数据结构:

$ openclaw browser --json tabs

{
  "tabs": [
    {
      "targetId": "7E73979D64CBF48058818D78D50FB94B",
      "title": "aneasystone's blog",
      "url": "https://www.aneasystone.com/",
      "wsUrl": "ws://127.0.0.1:18800/devtools/page/7E73979D64CBF48058818D78D50FB94B",
      "type": "page",
      "suggestedTargetId": "blog",
      "tabId": "t1",
      "label": "blog"
    }
  ]
}

收尾要关掉标签页就用 close 参数:

$ openclaw browser close blog

closed tab

如果忘记关也没有关系,OpenClaw 给主 agent 的浏览器会话配了一套自动清理机制:空闲超过一定时间(默认 120 分钟)的标签页会被回收,每个会话还有个标签页数量上限(默认 8 个),后台每隔几分钟扫一遍;子 agent、cron、ACP 这类任务跑完时,也会顺手把自己开的标签页关掉,不至于在后台留一堆的孤儿窗口。

SSRF 防护

刚才用 opennavigate 打开网址,看着什么链接都能打开,其实不然。之前学习工具箱时就提到过,web_fetch 自带一套 SSRF 防护:凡是能由外部输入决定去访问哪个地址的工具,都得防着被人诱导去访问内网。浏览器更是如此,它能被导航到任意 URL,风险敞口只大不小,所以 OpenClaw 也给它单配了一套独立的、默认 fail-closed 的 SSRF 防护,下面就稍微展开看看。

OpenClaw 会在导航和新开标签页之前先过一道 SSRF 检查,等浏览器真正解析出最终那个 http(s) 地址之后,再复查一遍。这一前一后是有讲究的,专门防 30x 重定向绕过:链接乍看是个干净的公网地址,轻松过了第一关,跳转之后才露出内网地址,正好被第二道复查逮住。默认拦在门外的,包括私网地址、本机环回、link-local,以及云厂商的元数据地址。

那真要放行某个内网地址呢?最稳妥的做法是只给信得过的那几个域名开放,把它们加进 browser.ssrfPolicy 的白名单。比如想让 agent 访问内网的 grafana.corp.example,配置写在 ~/.openclaw/openclaw.json 里:

{
  browser: {
    ssrfPolicy: {
      // 精确匹配:列出的域名原样放行
      allowedHostnames: ["grafana.corp.example"],
      // 通配匹配:* 能匹配子域,一条顶一片
      hostnameAllowlist: ["*.corp.example"],
    },
  },
}

这两个参数的差别只在匹配方式:allowedHostnames 按完整域名精确比对,hostnameAllowlist 支持 * 通配符。命中白名单的域名,即便最终解析到私网地址,也照样放行。

要是内网地址比较多、一个个域名往白名单里加嫌麻烦,也可以用一个开关把私网整个放开:

{
  browser: {
    ssrfPolicy: {
      dangerouslyAllowPrivateNetwork: true,
    },
  },
}

不过光看名字里那个 dangerously 就该警觉:它会让浏览器对所有私网地址都不再拦截,所以默认是关着的,只有在你确实信任、也评审过的私网环境里才建议打开。

页面操作

标签页开好了,接下来就是在这个页面上干活。browser 在页面这一层的动作不少,但总体来说无非是看、做、核对三件事:先用 snapshot 看清页面上有什么,再用 act 系列动手操作,需要时用 screenshot 截图核对;除此之外还有几个偏辅助的动作。下面一类一类来看。

看清页面:snapshot

OpenClaw 在看清页面这块做得比较巧妙,它不会把页面的原始 HTML/DOM 一股脑塞给模型。原因很简单,网页源码又长又乱,满屏 <div><div>、一堆样式类名,模型读起来既理不清结构,又白白烧掉一大把 token。

那它给模型看什么呢?是浏览器的无障碍树(accessibility tree)。这名字听着陌生,其实天天都有人在用:盲人靠屏幕阅读器读网页,背后就是这棵树。它不管页面长什么样,只按语义把内容归成按钮、输入框、链接、标题这些角色,每个角色再配一个看得懂的名字(被称为 accessible name,一般就是按钮上的文字、输入框旁边的 label 等),父子层级用缩进表示。说白了,它把一个给人看的花哨页面,压缩成了一份给机器读的、干净的结构清单。

OpenClaw 又在这棵树上加了道工序:给每个能点、能填的节点编一个稳定的编号,也就是 ref。这么一来,模型根本不用管某个元素长什么样、藏在第几层,认准要操作哪个 ref 就行。快照的结果大致长这样:

- button "Save" [ref=e1]
- link "Docs" [ref=e2]:
  - /url: https://docs.openclaw.ai/
- generic "Clickable Card" [ref=e3] [cursor=pointer]
- textbox "Email" [ref=e4]

每一行就是「角色 + 名称 + [ref=eN]」:链接底下会挂一个 /url: 标出它的目标地址,可点击的非标准元素则注明 [cursor=pointer](表示它虽然不是标准的按钮或链接,但点得动)。碰到 iframe,则嵌套着往里展开:

- Iframe "Child" [ref=e1]
  - button "Inside" [ref=e2]

模型拿到这棵树,想点「Save」,只回一句 act 带上 ref=e1 就行,完全不用关心它在 DOM 里到底是第几个 <button>、外面套了几层 div。这背后是 OpenClaw 的一个刻意设计:点击、输入这类操作只认 ref,不收 CSS selector。原因是 selector 出了名的脆,页面结构稍微一改,像 .btn-primary > span:nth-child(2) 这种立马失效;换成语义稳定的 ref,多步操作才不容易因为页面的一点微调就整条链路崩掉。

你可能注意到 browser 工具里有个 selector 参数,但它管的不是「点哪个元素」,而是另外几件事:给快照划定范围(snapshot 时只取某个子树或某个 iframe)、截取单个元素(screenshot --element)、或者等某个元素出现(wait)。

知道快照长什么样,命令行里 snapshot 一下就能看到真东西。还是上一节那个博客标签页,输出就是上面那种带 ref 的缩进树:

snapshot-ai.png

上面这份输出走的是 snapshot 默认的 ai 格式。其实 snapshot 支持两种格式,用 format 参数切换:

  • ai(默认):给模型优化过的形态,就是上面那种缩进表示层级、每个可交互节点都带 ref 的文本树。简洁好读,模型能直接挑出 ref 来操作,日常驱动页面用它就够了。
  • aria:用 --format aria 切过去,吐出的是更原始的无障碍树节点,结构上更贴近浏览器内部,主要拿来排查页面结构,refs 不一定能直接拿去点。

snapshot-aria.png

说到底,aria 就是那棵原始的无障碍树,ai 则是 OpenClaw 在它基础上加工出来的好读版本(编上 ref、补上链接和可点击提示)。

默认情况下 ai 快照不会主动精简,复杂页面拉出来的树有时相当庞大,所以 snapshot 还留了几个参数专门给它瘦身。最基础的是 maxChars(默认 4 万)这个字符上限兜底,超出就自动截断;如果还是嫌它啰嗦的话,可以加 compact 拉一份精简版,把没名字的纯容器节点、以及底下不含任何可操作元素的空枝条都剪掉,只留有内容的骨架,或者用 interactive=true 只留按钮、链接、输入框这类能交互的节点,把纯展示用的文字一股脑滤掉;页面层级套得太深时,还能用 depth 限制往下钻几层。

ref 又是怎么映射回真实元素的呢?这取决于 refs 参数选的风格。OpenClaw 支持两种 ref 风格:默认的 role 风格里,e1e2 这些编号背后其实存着「角色 + 名称 + 第几个」这组信息,OpenClaw 收到 ref=e1,就把它翻译成 Playwright 的 getByRole("button", { name: "Save" }) 去重新定位;另一种 aria 风格走的是 Playwright 原生的 aria-ref,编号形如 ax1,OpenClaw 会通过 CDP 给对应的 DOM 节点打上一个 data-openclaw-browser-ref 标记属性,靠这个属性精确命中。

不管哪种风格,都要注意的是,ref 只对最近一次快照有效。每取一次新快照,旧的那张 ref 表就被整张覆盖。所以页面一旦变了,如果没有重新 snapshot 就拿着老 ref 去操作,多半会遇到 Unknown ref "e1". Run a new snapshot and use a ref from that snapshot 这样的报错,这时就得老老实实重拍一张了。

操作页面:act

看清了页面结构,接下来就能用 act 动手操作页面了。它本身是个分派器,要执行哪种动作由 kind 参数决定;命令行里把这些 kind 拆成了一个个独立的子命令,对应关系大致如下:

kind.png

我们挑几个重点的看下。

click

最常见的就是拿 ref 点一下,比如点击上面快照里那个「归档」链接:

$ openclaw browser click e13

clicked ref e13

这个命令默认是左键单击,除此之外,还可以实现 --double 双击、--button right 唤出右键菜单、--modifiers Meta(或 Control)按住某个键再点击,能在新标签页打开链接。另外,如果某个要点击的元素快照不出来或点不到(比如被遮挡、藏在 shadow DOM 里、和其他元素的 z-index 冲突),我们还可以使用 clickCoords 这个参数,它不按 ref 来点击,而是直接按视口坐标 click-coords 120 340 点击。

这里还有一个挺关键的细节:click 点击后不会立刻返回,而是检查一下页面有没有因为这次点击发生跳转,一旦跳了,就把新地址带回结果里,并对这个新地址补一道 SSRF 复查。

type / fill

type 往输入框里打字,最常见就是 type <ref> "文本"。它还有两个开关:--slowly 让它放慢、一个字一个字地敲,装得像真人在打字,可以绕过那些根据输入速度判人机的站点;--submit 则在打完之后顺手回车,省得再单独按一下 Enter:

$ openclaw browser type e4 "openclaw" --submit --slowly

有时候要一次填一整张表单,一个个 type 就太慢了,可以用 fill 把多个字段打包进一次调用:

$ openclaw browser fill --fields '[{"ref":"e4","value":"Ada"},{"ref":"e5","value":"ada@example.com"},{"ref":"e6","type":"checkbox","value":true}]'

fill 一次能填一整批字段,每个字段可以带个 type 说明控件类型:默认 text 直接填值,标成 checkboxradio 就改成勾选(值传 true 勾上、false 取消)。

顺带说说 typefill 底下的实现,它们其实默认调的都是 Playwright 的 fill() 方法,把值一次性整体填到输入框里;而 type --slowly 使用的是 Playwright 真正逐字符的 type() 方法(每个字停 75ms);fill 碰到 checkbox、radio 则走的是 setChecked 方法。

select

这个命令用于选择下拉框里的选项,一次选一个或多个都行:

$ openclaw browser select e7 cn

selected cn

它底层走 Playwright 的 selectOption 方法,多选下拉一次能选好几项(select e7 a b c 就是把 a、b、c 一起选上)。传进去的字符串会同时匹配 <option>value 和显示文字,填哪个都行。

那怎么知道一个下拉里有哪些选项可选?snapshot 里下拉通常会把 option 一项项列出来,名字就是它的显示文字,照着填即可:

$ openclaw browser snapshot

- search [ref=e45]:
  - textbox "请输入关键字" [ref=e46]
  - combobox [ref=e47]:
    - option "所有" [selected]
    - option "中国"
    - option "美国"
    - option "日本"
  - button "筛选" [ref=e48] [cursor=pointer]

要是想拿到确切的 value 值(比如显示的文字有重复时),可以用 evaluate 针对 DOM 执行一段 JS 即可:

$ openclaw browser evaluate --ref e47 --fn '(el) => [...el.options].map(o => ({ value: o.value, label: o.text }))'

[
  {
    "value":"cn",
    "label":"中国"
  },
  {
    "value":"us",
    "label":"美国"
  }
]

press

直接敲键盘,比如回车、Tab、方向键、Esc 这些都行,常用来在没有明确按钮时触发提交或翻页:

$ openclaw browser press Enter

pressed Enter

它直接调 Playwright 的 keyboard.press 方法,除了单个键,也能按组合键,比如 press Control+A 全选、press Shift+Tab 反着跳焦点。另外 pressclick 一样会做导航检测:按 Enter 很可能提交表单、触发跳转,OpenClaw 会盯着这次跳转,照样走一遍前面那套 SSRF 复查。

hover

把鼠标悬到某个元素上,常用来触发那种鼠标悬停才显示的下拉菜单、提示气泡:

$ openclaw browser hover e5

hovered ref e5

底层是 Playwright 的 locator.hover,只改 UI 状态、不触发导航。它最常见的搭配是先 hoversnapshot:很多菜单是悬停才渲染出来的,先让它冒出来、再拍一张快照,才能拿到菜单项的 ref 去点。

drag

从一个 ref 拖到另一个 ref,做拖拽排序、移动滑块这类操作:

$ openclaw browser drag e3 e8

dragged e3 → e8

它接收起止两个 ref,首先自动算好坐标,底层调 Playwright 的 dragTo 方法,然后模拟按下、移动、松开这一整套动作。拖拽排序、移动滑块、把文件拖到上传区这类交互都可以靠它来实现。

resize

改视口尺寸,用于测试页面在不同屏幕宽度下的响应式表现:

$ openclaw browser resize 1280 720

resized to 1280x720

底层是 Playwright 的 setViewportSize 方法,改完视口浏览器会重新布局,并触发 CSS 媒体查询,所以拿它配合多次 snapshot / screenshot,就能看页面在手机、平板、桌面不同宽度下分别长什么样。

wait

多步操作里最容易翻车的,就是上一步点完、页面还没加载好,就急着点下一步,结果扑空。browserwait 命令就是用来避免这种情况的,它能等到某个具体状态再往下走:等某段文字出现(或用 --text-gone 等它消失)、等 URL 匹配某个 glob、等加载状态(load / domcontentloaded / networkidle)、等某个 CSS 元素可见、甚至等一段 JS 谓词为真。每个条件对应一个参数,可以单用,也能叠着用:

$ openclaw browser wait --text "上传完成"              # 等某段文字出现
$ openclaw browser wait --text-gone "上传中"           # 等某段文字消失
$ openclaw browser wait --url "**/dashboard"           # 等 URL 匹配某个 glob
$ openclaw browser wait --load networkidle             # 等加载状态(load/domcontentloaded/networkidle)
$ openclaw browser wait "#main"                         # 等某个 CSS 元素可见
$ openclaw browser wait --fn "window.ready === true"   # 等一段 JS 谓词为真

# 叠着用:下面几个条件全部满足才往下走
$ openclaw browser wait "#main" --url "**/dashboard" --load networkidle

比起等一个固定的时长,这样等到确切状态再继续,要可靠得多。

注意这里的 "#main" 是个 CSS selector,不是 ref。前面说过点击、输入这些操作只认 ref,但是 wait 则是少数几个例外之一。道理也好理解:你要等的元素往往还没出现在页面上,自然没进快照,也就没有 ref 可用,只能拿 selector 去等。

evaluate

它能往页面里注入一段 JS 脚本并运行,再把结果回传给你。它有两种用法:不带 ref 时,函数跑在整个页面的上下文里;带上 ref 时,OpenClaw 会把那个元素当参数喂进你的函数。

比如不带 ref,取一下当前页面的标题:

$ openclaw browser evaluate --fn '() => document.title'

aneasystone's blog

带上 ref,就能针对某个元素读数据,比如把一个链接喂进去,读出它的真实地址(前面 select 那节列出下拉有哪些选项,用的也是这招):

$ openclaw browser evaluate --ref e2 --fn '(el) => el.href'

https://www.aneasystone.com/archives.html

这等于在页面里开了个口子,能跑任意 JS、做的事几乎没有限制,威力大、风险也大。所以 OpenClaw 允许把它一键关停,在 ~/.openclaw/openclaw.json 里把 evaluateEnabled 设成 false

{
  browser: {
    evaluateEnabled: false,
  },
}

关掉之后再调 evaluate,会返回一个 ACT_EVALUATE_DISABLED 错误。要留意的是,这开关一关,连 wait 里那条 JS 谓词(--fn)也一并禁了,因为它俩底层都是在页面里执行你给的代码。

截图核对:screenshot

想把当前页面存成一张图片,或者让人眼或视觉模型核对一下前面那些操作的结果,就用 screenshot 命令。它截的是真实像素,返回截图文件的本地路径。最常用的是 --full-page,把整页连同滚动区域一起截下来:

$ openclaw browser screenshot --full-page

MEDIA:~/.openclaw/media/browser/6a8827a2-e75e-4c9a-b3de-3949d4b12683.jpg

除了整页,也能只截某一个元素,有两种指定方式:--ref 指定快照里的 ref--element 指定 CSS selector:

$ openclaw browser screenshot --ref e13          # 截快照里的某个 ref
$ openclaw browser screenshot --element "#main"  # 截某个 CSS 元素

另外,截图存成什么文件格式,可以用 --type 指定,支持 png(默认)和 jpeg 两种。

不过你可能注意到了,上面截全屏时我们并没有指定 --type,按说该存成 png 的,但是最终生成的却是 .jpg 文件。这是因为 OpenClaw 存图前会先做一道归一化:截图最长边超过 2000 像素、或者体积超过 5MB 时,就自动缩放并转成 JPEG,按先压边长、再降画质的顺序依次尝试,直到压到 5MB 以内。整页截一张长页面很容易超,于是被转成了 JPEG 格式。这道工序是为了不让一张超大图占掉模型一大截上下文,顺带也减轻传输和存储的负担。

还有个给视觉模型量身定制的参数 --labels,它会在截图上把每个 ref 的位置用橙色方框圈出来、标上编号,这样视觉模型不光看得到画面,还能照着编号说出要操作哪个 ref

screenshot-labels.jpg

它只标当前视口里看得见的元素,滚动条以外的会跳过,而且最多标 150 个,免得整张图被标签填满。

辅助动作

除了上面这些主力动作,browser 还有几个偏辅助的 action,平时用得不算频繁,但偶尔可以用来救急:

  • console 把页面控制台的日志读出来,配上 level 还能按级别(errorwarning 等)过滤,调试页面脚本时有用;
  • pdf 把当前页导出成 PDF,适合存档或者把长报告整页留底,注意这是托管 profile 才有的能力,也依赖 Playwright;
  • upload 处理文件上传,要传的本地文件写进 paths,再用 inputRef 指准页面上那个文件选择框;
  • dialog 应付 alert / confirm / prompt 这类原生弹窗,accept 决定点「确定」还是「取消」,prompt 弹窗要填的内容则写在 promptText 里。

小结

讲了这么多,browser 其实还有不少细节值得你接着挖。比如它是个能伪装的环境:地理位置、时区、语言、设备类型都能改,还能切到离线模式、直接读写 cookie 和 localStorage,拿来测网站在不同环境下的表现正合适;比如它带熔断,某个 profile 的 Chromium 反复起不来时,OpenClaw 会按 profile 暂停一阵子启动尝试,免得一个配置损坏的浏览器把网关反复拖垮;又比如浏览器插件除了 browser 工具,还顺手捎了一份 browser-automation 技能,把前面那套先看、再动、变了重看的循环写成给 agent 看的操作手册,而且按需加载、平时不占 system prompt 的篇幅。这些就留给你自己去探索了。

最后,我们再来总结下今天的学习内容:

  1. 认准标签页:browser 一次只盯着一个标签页干活,所以动手之前得先认准目标。open 开新页、focus 切到前台、navigate 原地跳转、tabs 列清单、close 收尾,这几条命令都认 tabId、label、targetId 三种标识;就算忘了关,后台也有一套自动清理在兜底。
  2. SSRF 防护:不是什么 URL 都能打开。browser 单配了一套默认 fail-closed 的 SSRF 防护,导航前后各查一遍以防重定向绕过,私网、本机环回、云厂商元数据这些地址一律拦在门外;真要放行内网,优先用 allowedHostnames、hostnameAllowlist 开个窄口子,而不是把整片私网敞开。
  3. 看清页面(snapshot):它不把原始 DOM 丢给模型,而是走无障碍树,把页面压成一棵带 ref 的语义树。默认的 ai 格式好读、能直接挑出 ref 来操作,aria 格式更原始、适合排查结构;遇上复杂页面,还能用 compact、interactive、depth 几个参数瘦身省 token。
  4. 操作页面(act):动手的活儿都收在一个 act 里,靠 kind 分派成 click 点击、type / fill 输入填表、select 选择、wait 等待、evaluate 跑 JS 等一整套动作,底层大多落到 Playwright 的对应方法上。
  5. 核对与救急:screenshot 截的是真实像素,存图前会自动归一化,把尺寸和体积压进 2000 像素、5MB 以内;另外还有 console、pdf、upload、dialog 几个辅助动作,平时用得不多,调试、存档、传文件、应付弹窗时拿来救急。

至此,OpenClaw 工具箱里最复杂的 browser 就算彻底过完了。下一篇我们换个方向,看另一类「派活儿」的本事:通过 ACP,把整个任务直接甩给 Claude Code、Gemini CLI、Codex 这些外部编码 agent 去跑,敬请期待~

参考


给小龙虾配齐工具箱:OpenClaw 的工具体系(二)

上一篇我们把 OpenClaw 的内置工具分成了八大类,并且把前四类一一看了一遍,这一篇书接上文,继续后面四类的学习。

设备控制

这一类管的是小龙虾对配对进来的设备 / node 的操控,两个工具配合着用:nodes 负责发现、配对、指定要遥控哪个 node,以及对它干什么(拍照、录屏、取定位、推送通知等),canvas 负责在某个 node 的屏幕上画东西(显示页面、跑 JS 脚本、截屏画面)。

我们先来看 nodes 的参数,它和昨天学习的 message 类似,也是个按 action 分派的工具:

nodes.png

完整的 action 一共 19 个,每个 action 还各带一批专属的细分参数(定位精度、通知优先级、录屏的 screen 选择等等),一张表列不下,用到时再查文档即可。这 19 个动作按功能分组大致如下:

nodes-actions_01.jpg

图里的分组命名,正好牵出我们之前曾学习过的一组概念:设备(Device)节点(Node)。一台 Mac、一部手机这样的物理设备,在 OpenClaw 里其实分两层看:Device 是它的「身份证」,管它是谁、准不准进来;Node 是它以 role: node 连进来之后对外贴出的「能力清单」,管它能被调用哪些功能。

nodes 这个工具名字虽叫 nodes,实际上横跨了这两层:图里的「配对审批」和「设备信息」管的是 Device(这台设备是谁、批不批),「摄像头 / 屏幕 / 定位 / 通知 / 自定义命令」几组才是真正在调 Node 的能力。说白了它就是一台「设备遥控器」,node 参数选遥控哪一台,action 选按哪个键。

接下来,我们再来看下 canvas 工具的核心参数:

canvas.png

这个工具其实我们之前在 macOS app 篇里已经见过了,那块吸在菜单栏边、给小龙虾当可视化工作区的面板,就是它的画布;它和 nodes 一样,每个 action 底层都对应一条 canvas.* 节点命令,发给目标 node 上的画布去执行。

运行时管理

这一类是小龙虾用来管理自身运行时的两个工具:cron 管定时任务(比如每天早上八点提醒我打卡),对应前面 cron / heartbeat 那篇;而 gateway 则能查看和修改自己的运行时配置,甚至重启和自我升级。

我们先看 cron,它的参数也是按 action 分派,外加一个嵌套的 job 对象,核心部分如下:

cron.png

job 对象里最关键的是 schedule(什么时候跑)和 payload(跑的时候干什么):

job.png

光看参数还是有点抽象,举个具体例子:开头说的「每天早上八点提醒打卡」,用 add 建出来就是这么一个 job:

{
  action: "add",
  job: {
    name: "打卡提醒",
    schedule: { kind: "cron", expr: "0 8 * * *", tz: "Asia/Shanghai" }, // 何时跑:每天上海时间 8:00
    payload: { kind: "systemEvent", text: "提醒主人:该打卡了" },          // 跑时干啥:推一条提醒
    delivery: { mode: "announce" },                                      // 结果去哪:播报到当前通道
  },
}

这个 job 拆开看其实就三块:

先是 schedule,它指的是任务什么时候跑,有三种写法:at 是「某个时间点只跑一次」,给一个具体时刻就行(比如下周一上午十点),适合一次性的提醒;every 是「每隔多久跑一次」,给一个毫秒间隔(比如每两小时一次);cron 最灵活,用一条 cron 表达式配上时区来描述周期,上面例子里的 0 8 * * *Asia/Shanghai,意思就是「每天上海时间早上八点」。

再是 payload,它说的是到点之后具体干什么,分两种。一种是 systemEvent,只往通道里推一条提醒文本,内容写在 text 字段里,上面打卡那个例子就是它,跑起来无非是按时吼你一嗓子。另一种是 agentTurn,到点会触发一整轮完整的 agent 任务,要它干的事写在 message 字段里。比如把上面的 payload 换成:

payload: { kind: "agentTurn", message: "拉一下我今天的日程,挑出最重要的三件发我" }

到了八点,小龙虾就不只是提醒你,而是真的去翻日程、筛选、再把结果发过来。注意这两种类型的字段不一样:systemEventtextagentTurnmessage

最后是 delivery,它管干完之后结果往哪儿送,有三档:none 是不往外送,任务在后台默默跑完就算(常配那些不需要回话的任务);announce 是把结果播报到当前通道,你在 Telegram、飞书里就能直接收到,也是最常用的一档;webhook 则是把结果 POST 到你指定的一个回调地址,方便接进自己的系统或别的自动化流程里。

对照着这个例子回头看,就能明白前面 cron / heartbeat 篇里那些定时任务是怎么配出来的了。

第二个工具是 gateway,下面是它的参数定义:

gateway.png

这个工具比较特殊,它是 仅限 owner(也就是你本人) 才能调用的,OpenClaw 根据这轮消息是谁发来的判定:本机 CLI、菜单栏 app、Control UI 这些本地操作天然算 owner;远程 IM 通道则需要把自己的账号加到 commands.ownerAllowFrom 配置里:

{
  commands: {
    ownerAllowFrom: [
      "telegram:123456789",  // 通道名 + 该通道里的用户 id,只在这个通道生效
      "feishu:ou_xxxxxxxx",  // 别的通道照此再写一条
      "+8613800138000",      // 不带通道前缀:按账号本身匹配,不限通道(手机号会规范成 E.164)
      "*",                   // 所有人都是 owner,强烈不建议使用,等于把门彻底敞开
    ],
  },
}

它的六个 action 按用途可分为三组:

先是「看」的两个。config.schema.lookup 用来查某一块配置长什么样、有哪些字段、各是什么类型,相当于动手之前先翻一遍说明书;config.get 则是把当前生效的配置整个拉出来看一眼,并附带一个 hash,这个 hash 是这份配置算出来的指纹,记住它,等下改的时候用得上。

再是「改」的两个。config.patch 只动其中一小块,日常最常用,注意它并不靠某个 path 参数来定位,而是把要改的内容写成一个 只包含目标项的嵌套对象 塞进 raw 里,OpenClaw 再把它 深合并 进当前配置:你写到的字段被改掉,没提到的一律原样保留,嵌套的层级本身就等于一条路径(这个看下面的例子就明白了)。config.apply 则相反,raw 给的是 一整份 新配置、直接把旧的全量覆盖,你漏写哪项哪项就没了,动静大、容易误伤,非必要不用。两者都可以带上刚才那个指纹(参数叫 baseHash),OpenClaw 会拿它跟当前配置比一比,要是中途配置被别处改过、对不上号,就拒绝这次改动,免得两边并发把对方的修改覆盖掉。

最后是「重启 / 升级」的两个。restart 直接重启 Gateway;update.run 触发一次自我升级再重启。这俩重启完,如果还有没回完的话,可以靠 note(重启后由系统替它转达的一句完成提示)和 continuationMessage(提醒它重启后该回头去核实、汇报什么)把上下文接回来。

对于一次局部配置的修改,官方推荐的做法是,先用 config.schema.lookup 看清楚配置的格式,然后再用 config.patch 小改,别动不动就整份 config.apply。比如想把子 agent 的默认思考强度从 medium 提到 high,一次 patch 就够了:

{
  action: "config.patch",
  baseHash: "<config.get 拿到的那个指纹>",
  raw: `{ "agents": { "defaults": { "subagents": { "thinking": "high" } } } }`,
}

这里还藏着一道挺关键的安全设计:config.patch / config.apply 并不是想改什么就能改什么,它们背后有一道安全守卫,专门拦那些会削弱小龙虾自身安全的改动。比如关掉沙箱、放开插件加载、改写 Gateway 的鉴权 token、去掉 web_fetch 的 SSRF 防护,当然也包括前面 exec 那节讲的 tools.exec.asktools.exec.security 那两个开关。换句话说,小龙虾可以借 gateway 给自己做点微创手术,但没办法通过改配置悄悄突破自己的安全防线,这道兜底写死在代码里,谁也绕不过去。

媒体生成

这一类让小龙虾造出图片、视频、音乐和语音,一共五个工具:image 是看图(分析理解一张已有的图),image_generate 生成或修改图,video_generate 生成视频,music_generate 生成音乐,tts 把文字读成语音。

这一组工具参数都不少,但骨架高度一致,我们不妨对照着看。先看「看图」和「文字转语音」这两个最简单的:

image_tts.png

image 是这五个工具里唯一读而不写的,其余四个都在产出新东西,它却反过来,输入一张已有的图,让小龙虾看懂里面有什么。参数 prompt 表示你想知道什么(比如:这张图里有几个人,把发票上的金额读出来),参数 imageimages 表示图的本地路径或 URL(一次最多塞 20 张,方便让它对比着看几张),参数 model 只在你想指定某个视觉模型时才填,不填就用自动探测到的那个。

tts(text to speech,文字转语音)则是把一段文字读成语音。必填的只有 text,也就是要念的那段话;另一个 channel 表示发到哪个通道,OpenClaw 根据这个自动匹配输出格式,必要时还用 ffmpeg 转码一道,因为各家 IM 认的格式不一样,大致分三档:支持语音条的通道(Telegram、飞书、WhatsApp、Matrix 等)优先用 Opus(48kHz,裹在 Ogg/WebM 里,听感好、体积小),普通通道发 MP3(约 44.1kHz / 128kbps),Talk、电话这类实时语音通道则要原始 PCM(或 Gradium 那种 ulaw 8kHz),此外本地 CLI、Gemini、xAI 等还能产出 WAV,这些你基本不用操心,填对 channel 它自己会挑。最后还有一点值得留意:tts 是明示意图才触发的,平时聊天默认还是回文字,只有你明确说「读出来」、或者用了 /tts 斜杠命令、或者开了 Auto-TTS 模式,它才会输出语音,不会动不动就给你来段朗读。

再看三个「生成」类工具,它们骨架相通:都用 action 切换模式,prompt 给提示词,model 指定模型,timeoutMs 设置超时。但生成的东西不一样,各自的专属参数也不一样。

先是 image_generate(生成或编辑图片),关键在尺寸、张数和图片专属的质量、格式、背景等参数:

image_generate.png

video_generate(生成视频)的重头是参考素材和视频特有的时长、宽高比等参数,它支持文生视频、图生视频、视频生视频三种玩法:

video_generate.png

music_generate(生成音乐)则换成歌词、纯伴奏这类音乐专属的参数:

music_generate.png

这几个工具在使用之前,都得先配好至少一个对应的 provider 才行。四个生成型工具的 provider 可选如下:

  • image_generate:OpenAI(gpt-image 系列)、Google(Gemini 图像)、xAI(Grok Imagine)、fal、DeepInfra(FLUX)、MiniMax、OpenRouter、ComfyUI、Vydra 等。
  • video_generate:OpenAI(Sora)、Google(Veo)、Runway、MiniMax(Hailuo)、阿里(Wan)、字节 BytePlus(Seedance)、Qwen、fal、xAI、Vydra 等十几家。
  • music_generate:就 Google(Lyria)、MiniMax、ComfyUI 三家。
  • tts:OpenAI、ElevenLabs、Google、Azure Speech、MiniMax、火山引擎、Inworld、小米 MiMo,外加免 key 的微软 Edge 神经语音和本地 CLI 等。

前三个工具的配置相似,都在 agents.defaults.<X>GenerationModel 下写 primary(首选)和 fallbacks(依次兜底),取值用 provider/model 的形式:

{
  agents: {
    defaults: {
      imageGenerationModel: {
        primary: "openai/gpt-image-2",
        fallbacks: ["google/gemini-3.1-flash-image-preview", "fal/fal-ai/flux/dev"],
      },
      // videoGenerationModel、musicGenerationModel 同理
    },
  },
}

tts 略有不同,配在 messages.tts 下,可以挂好几个 provider,再用 provider 指定默认那家:

{
  messages: {
    tts: {
      provider: "openai", // 默认用哪家;没配则按已配好的挨个兜底
      providers: {
        openai: { apiKey: "${OPENAI_API_KEY}", model: "gpt-4o-mini-tts", voice: "alloy" },
        elevenlabs: { apiKey: "${ELEVENLABS_API_KEY}", voiceId: "EXAVITQu4vr4xnSDxMaL" },
      },
    },
  },
}

到底用哪家 provider,OpenClaw 有一套自己的优先级规则:调用时 model 参数写死的最优先,且不再往下兜底;其次是配置里的 primaryfallbacks;如果两样都没指定,还会走一遍 自动探测,把配了 API key 的 provider 按固定顺序挨个试,第一个能用的就用它(这个自动兜底可以用 agents.defaults.mediaGenerationAutoProviderFallback: false 关掉)。想知道当前手头到底有哪些可用,可以通过 action: "list" 列出 provider 清单。

最后说回第五个 image(看图)工具。前面说过它不算生成而是理解,配置也和上面四个不一样:不在 agents.defaults 下,而是在 tools.media 里按能力(image / audio / video)各列一组 models,每个条目指明用哪个 provider/模型来「读」媒体,列多条就是按序兜底(一条失败、或媒体太大超限,就换下一条):

{
  tools: {
    media: {
      image: {
        models: [
          {
            type: "provider",     // 也可以是 "cli",跑一条本地命令来看图
            provider: "openai",
            model: "gpt-5.5",
            prompt: "用不超过 500 字描述这张图",
            maxChars: 500,        // 摘要最多多少字
            timeoutSeconds: 60,
          },
          // 再列一条,就是下一顺位的兜底
        ],
      },
    },
  },
}

要是你压根没配 tools.media 参数,OpenClaw 也有一个自动探测机制,如果当前回复用的模型本身带视觉能力,就直接拿它来看图。

这里你可能会犯嘀咕:tools.media 支持配置 image / audio / video 三种能力,为什么内置工具里却只有 image 一个,没有 audiovideo 呢?这是因为 tools.media 配的其实是「媒体理解管线」,而不是工具。用户发进来的附件,会在 agent 开始干活之前先被它自动消化一道:图片转成描述、音频转写成文字、视频生成摘要,三种能力都覆盖,模型直接读到消化好的文字即可,不用主动调工具。而模型能主动调的「读媒体」工具,目前只给了 image 一个,我猜是因为主动看图这种需求最独立、最常见(比如分析一个刚抓到的图片 URL、或带个具体问题把某张图重看一遍);音频、视频的理解基本都发生在「用户发来语音或视频」这条入站通道上,直接处理即可,也就不必再单列成可调用的工具了,当然也不排除后面 OpenClaw 将这些能力也变成内置工具。

provider 和配置说完了,最后再补两条容易被忽略的注意事项:

  1. 这些工具的调用会自动区分同步还是异步,这关系到结果什么时候回来。同步的(图片生成、TTS)当场就出,跟着这一轮回复一起给你;异步的(视频和音乐)因为生成慢,会先告诉你「在做了」,等后台跑完再把成品单独推回来;
  2. 这些工具只有在你至少配了一个对应的 provider 时,才会出现在小龙虾能看到的工具清单里。如果没配图片 provider,模型压根看不到 image_generate 这个工具的存在,自然也就不会、也没法去调它。所以如果你发现「让它画图它说不会」,那么多半是 provider 没配。

子 agent 编排

这一类就是小龙虾把任务拆分后交给「分身」并行干,我们在前面「多 agent 与子 agent」那篇已经学习过。它包含一组 sessions_* 开头的工具(派生、列会话、读历史、发消息、收结果)、管理已派出去分身的 subagents、以及列出可用 agent 的 agents_list。其中派活儿的主力是 sessions_spawn,先看它的核心参数:

sessions_spawn.png

配套的 subagents 工具用来管理已派出去的子 agent:

subagents.png

剩下几个都比较简单:

  • sessions_list:列出自己派生的那些子会话,默认谁派生的归谁看,也可按 agentId、标签(label)、关键词(search)筛选,还能用 limitactiveMinutes 限定条数和最近多久活跃过。
  • sessions_history:读某个会话的历史。sessionKey 指定读哪个会话,limit 控制取最近几条,includeTools 决定要不要把工具调用消息也带上。
  • sessions_send:往某个会话发条消息。用 sessionKey / label / agentId 任选一种方式指定目标,message 是要发的内容,timeoutSeconds 设定回复的超时时间。
  • sessions_yield:主动结束当前这一轮、把发言权让出去,自己先歇着,等某个子 agent 的结果作为下一条消息推回来时再被唤醒接着处理。
  • agents_list:列出有哪些 agent 可用。
  • session_status:轻量回读一个会话的状态,相当于 /status 命令,看它当前用的什么模型、用量、时间、可用时还有花费,以及挂在它名下的后台任务。两个参数都可选:sessionKey 指定看哪个会话(传 current 表示当前),model 还能临时修改这个会话的模型(传 default 恢复默认)。

我们挑最核心的 sessions_spawn 工具,详细看看它的几个设计点,这些我们在之前的文章中也学习过,这里复习一下。

最要紧的是隔离。每个子 agent 默认都在一个全新的会话里起步,有自己独立的上下文,有自己的记忆和对话历史,它不知道你和主 agent 之前聊过什么。可以将 context 设置成 fork,这样 OpenClaw 才会把当前这条对话分叉,复制一份给它当起点,不过要注意这样 token 消耗会明显增大,只有那种必须依赖当前对话才说得清的任务才建议开这个参数。

然后任务派出去之后,结果是推送式送回来的,不会阻塞主会话。调一次 sessions_spawn 会立刻返回一个 run id(这次任务的编号),子 agent 随后在后台慢慢干,干完会主动把结果推送回发起方所在的通道。所以派完任务就可以去忙别的,不用反复查 sessions_list 轮询结果。

还有一点,默认情况下,子 agent 不能再派生自己的子 agent,防止无限繁殖。maxSpawnDepth 默认是 1,只允许「主 → 子」这一层。把它调到 2,才解锁所谓的编排者(orchestrator)模式:主 agent 先派一个编排者子 agent,编排者再把活儿拆给一批 worker 并行干,最后结果逐层往上汇总(worker 报给编排者、编排者汇总后报给主 agent,每一层只看得见自己直接下级)。再往上调到 3、4、5 会逐层加深嵌套(上限是 5),只是层数越深越烧 token、链路越难排查,实际很少用得着。另外,OpenClaw 会按照深度分配工具权限,只有编排者那层才拿得到 sessions_spawn 这类调度工具,最底层的 worker 一律不给,从机制上就堵死了它继续往下套娃的可能。配额上也有几道兜底:单个 agent 同时最多挂 5 个活跃子 agent(maxChildrenPerAgent)、全局并发上限 8 条(maxConcurrent)、每个 spawn 默认 900 秒超时(runTimeoutSeconds),这些都集中在配置文件的 agents.defaults.subagents 这块下面:

{
  agents: {
    defaults: {
      subagents: {
        maxSpawnDepth: 2,        // 允许的嵌套层数,默认 1(不嵌套),上限 5
        maxChildrenPerAgent: 5,  // 单个 agent 同时最多挂几个活跃子 agent,默认 5
        maxConcurrent: 8,        // 全局并发上限,默认 8
        runTimeoutSeconds: 900,  // 每个 spawn 的默认超时(秒),0 表示不超时
        model: "openai/gpt-4.1-mini", // 子 agent 默认用的模型,不写就继承主 agent
        thinking: "low",         // 子 agent 默认思考强度,同样不写就继承
      },
    },
  },
}

这些是默认值,写在 agents.defaults 下对所有 agent 生效;想给某个 agent 单独设,挪到 agents.list[].subagents 下即可。

最后留意一下 cleanup 的语义,它表示子 agent 运行结束后这个会话留不留:默认 keep 留着,delete 则是在推送完结果后清理,这个清理其实是归档而非物理删除,会话记录会被改名留存,事后照样能翻出来看。另外,子 agent 干活期间开的浏览器标签和进程,在它跑完时也会被尽力关掉,不至于留一堆孤儿进程在后台。

所谓改名留存,就是给会话记录那个 .jsonl 文件名末尾加一个「原因 + 时间戳」后缀。归档原因一共三种:.deleted. 是被主动清理掉的(对应这里的 cleanup: "delete")、.reset. 是会话被重置(对应用户手动执行 /clear 命令)、.bak. 是备份。

最后的最后,我们再看看 runtime 参数,小龙虾在派任务时不只能派给自己的子 agent,通过 runtime: "acp" 模式,小龙虾还能将任务丢给 外部的编码工具,比如 Claude Code、Gemini CLI、Codex、OpenCode 这些。不过严格说这已经不算内置工具了(得额外装官方插件 @openclaw/acpx),细节也自成体系,后面有机会再单开一篇吧。

工具治理

讲了这么多工具,接下来看看怎么管它们。道理很简单:不是每个 agent 都需要全量工具,一个只在群里闲聊的客服 agent,没必要能 exec 跑命令,更没必要能 gateway 改配置。OpenClaw 为此提供了几层控制,我们从最熟悉的说起。

最直接的两个配置是 tools.allowtools.deny,前面几篇配 agent 时已经反复用过:allow 是白名单、deny 是黑名单,黑名单永远压过白名单,同一个工具两边都写了,最终结果就是禁用:

{
  tools: {
    allow: ["group:fs", "browser", "web_search"],
    deny: ["exec"],
  },
}

正如之前学过的,allow / deny 里不必一个个工具地列,可以用 group:* 简写批量指定。常用的组有:

包含的工具
group:runtimeexec, process, code_execution
group:fsread, write, edit, apply_patch
group:webweb_search, x_search, web_fetch
group:uibrowser, canvas
group:sessionssessions_list, sessions_history, sessions_send, sessions_spawn, sessions_yield, subagents, session_status
group:memorymemory_search, memory_get
group:automationcron, gateway
group:messagingmessage
group:nodesnodes
group:agentsagents_list
group:mediaimage, image_generate, music_generate, video_generate, tts
group:openclaw所有内置工具(不含插件工具)

另外,光用 allow 从零开始一个个往上加,配起来还是有点累。所以 OpenClaw 还有一个 tools.profile 参数:它先按用途给你预设一套工具集合当起点,加工具用 tools.alsoAllow,砍工具用 tools.deny。内置的 profile 有四档:

  • full:所有核心和插件工具,最宽的基线。
  • coding:文件、运行时、web、会话、记忆等组,外加 cronimage/image_generate/music_generate/video_generate(注意:不含 tts,也不含 browser)。
  • messaging:消息组加几个会话工具,通道 agent 专用。
  • minimal:只有 session_status

这四档是写死的,profile 是个固定枚举,暂时没法自己新增一个 profile,也改不了某档里到底含哪些工具。想要一套专属工具集就挑个最接近的 profile 当基线,再用 alsoAllow / deny 做加减。

小结

上一篇我们学习了前四类工具,命令执行、网络访问、文件读写、消息收发。这一篇接着把剩下的四类工具和工具治理讲完了,简单回顾一下:

  1. 设备控制nodes 是台「设备遥控器」,一个工具横跨 Device(身份、准入)和 Node(能力清单)两层;canvas 配合它在某台设备的屏幕上「画东西」,两者底层都转成 camera.snaplocation.getcanvas.snapshot 这类节点命令派给目标 node 执行。
  2. 运行时管理cronschedule(何时跑)+ payload(干什么)+ delivery(结果送哪)三块拼出定时任务;gateway 仅 owner 可用,能查看 / 修改 / 重启 / 自升级自己的运行时配置。
  3. 媒体生成image 看图、image_generate 生成或修改图、video_generate 生成视频、music_generate 生成音乐、tts 把文字读成语音;用之前记得先配 provider,没配 provider 工具压根不出现,慢活儿(视频、音乐)还会丢后台异步跑。
  4. 子 agent 编排sessions_spawn 把任务拆给隔离的分身并行干,完成后推送式把结果送回。
  5. 工具治理:用 tools.allow / tools.deny 圈定某个 agent 能用哪些工具,工具名还能用 group:* 简写批量指定;嫌从零配麻烦,就用 tools.profile 选一套预设基线,再拿 alsoAllow 往上加、deny 往下减。

现在小龙虾手里有哪些工具,哪个工具能用,哪个工具不能用,我们都清楚了。其中有两个工具细节多,坑也深,因此后面准备花两篇分别学习下:下一篇专讲 browser 浏览器工具,再下一篇讲 ACP 工具。敬请期待~

参考


给小龙虾配齐工具箱:OpenClaw 的工具体系

在上一篇的最后,我们举了一个场景,对着手机喊一句让小龙虾查附近的咖啡馆,它先调 location.get 拿坐标、再用 web 搜索查咖啡馆、最后把结果合成语音念回来。这一连串动作里,location.get、web 搜索、语音合成,其实都是 OpenClaw 里的工具。除此之外,小龙虾的工具箱里还有很多内置工具,我们今天就来系统的学习一遍。

工具 vs. 技能 vs. 插件

在正式学习之前,我们先看下 OpenClaw 里三个容易混淆的概念:

  • 工具(Tool)是 agent 实际调用的东西。一个工具就是一个带类型签名的函数,比如 execbrowserweb_searchmessage。OpenClaw 自带一批内置工具,还能通过插件注册新工具。对模型来说,工具是以结构化函数定义的形式发给模型 API 的。
  • 技能(Skill)教 agent 什么时候、怎么用工具。它是一份注入到 system prompt 的 SKILL.md,给 agent 提供上下文、约束和分步指引。
  • 插件(Plugin)把这些打包到一起。一个插件可以同时注册通道、模型 provider、工具、技能等任意组合。

三者的关系大致是这样:

tools-vs-skills.png

简单说,工具是手脚,技能是手册,插件是打包。今天这篇只讲最底下那层手脚,手册和打包留到后面再讲。

内置工具清单

按官方文档,下面这个表格列出了 OpenClaw 所有的内置工具:

工具干什么
exec跑 shell 命令
process管后台进程
code_execution跑沙箱化的远程 Python 分析
browser控制 Chromium 浏览器(导航、点击、截图)
web_search搜网页
x_search搜 X 帖子
web_fetch抓页面内容
read读工作区内的文件
write写工作区内的文件
edit编辑工作区内的文件
apply_patch多块(multi-hunk)文件补丁
message把消息发到各个通道
canvas驱动 node 上的 Canvas(演示、求值、截屏)
nodes发现并指定已配对的设备
cron管定时任务
gateway查看、改配置、重启或自更新网关
image分析图片
image_generate生成或编辑图片
music_generate生成音乐
video_generate生成视频
tts一次性文本转语音
memory_search按语义/关键词检索记忆笔记
memory_get读取指定记忆文件或行区间
sessions_*列出会话、读会话历史、给别的会话发消息、派生子会话
subagents子 agent 编排
agents_list列出可用 agent
session_status轻量的 /status 式回读,可临时切换会话模型

这张表看着很多,其实可以分为八类:命令执行、网络访问、文件读写、消息收发、设备控制、运行时管理、媒体生成、子 agent 编排。下面我们一类一类来看,重点挑前面几篇文章里反复出现过的工具展开讲。每个工具我都先附一张参数表,直接从 OpenClaw 源码里的 schema 定义整理出来的,列清楚它收哪些参数、每个参数什么意思、哪些必填。看懂一个工具的参数表,基本就摸清了它能干什么、边界在哪。你会发现,决定一个工具好不好用、安不安全的,往往不是它能干什么,而是那些藏在参数里的细节。第一次接触的话,不用每个参数都背下来,先扫一眼有个印象,建立「哪类活找哪个工具」的直觉就够了。

命令执行

这一类管的是「让小龙虾运行命令或代码」,一共三个工具:exec 在你的机器上跑 shell 命令,几乎是整个工具箱里用得最多的一个:装依赖、跑测试、提交代码,几乎都靠它;processexec 配对,专管那些转入后台还没退出的命令;code_execution 则只需你说清「要算什么」,由 xAI 的 Grok 自己写 Python 并在远程沙箱里跑,全程不碰你的机器。下面一个个看它们的参数。

先看 exec 的参数定义:

exec.png

一眼看过去,必填的只有 command 一个,丢一条命令进去就能跑。不过剩下那一长串可选参数,才是这个工具真正的门道所在:

  • 跑在前台还是后台? 默认前台跑,命令执行完结果就回来了。但有些命令(比如启动一个开发服务器)会一直挂着不退出,总不能让小龙虾干等着。所以 OpenClaw 设了个超时:默认跑超过 10 秒(参数 yieldMs)就自动转后台,先把控制权交还给对话,命令继续在后台跑。你也可以一上来就 background: true 直接丢后台。命令转后台之后,再想看它输出了什么、给它敲个回车或粘点东西进去,就得换用配套的 process 工具,它的 actionpoll(看最新输出)、send-keys(发按键)、submit(回车)、paste(粘贴)四种。可以把这一对工具想象成:exec 开一个会话,process 是事后回头去操作这个还开着的会话。这些后台会话还按 agent 各管各的,A 这个 agent 看不见 B 的后台进程,互不打扰。
  • 要不要一个伪终端? 有些命令行程序很挑剔,只肯在真正的终端里跑,检测到自己不是终端就罢工或者输出乱掉(典型的是带交互界面的 TUI 工具或者编码类 agent)。这时给 exec 加上 pty: true,OpenClaw 会造一个 伪终端(pty,pseudo-terminal),让它以为自己跑在真终端里。一般命令是用不到的。
  • 命令在哪台机器上跑? 这是 host 参数,它决定了命令的执行环境,共四个取值:sandbox(隔离沙箱)、gateway(OpenClaw 主程序所在的机器)、node(某台配对的设备,比如你的手机或另一台 Mac)、auto(有沙箱就进沙箱,没有就落到网关)。
  • 要不要先问过你? 这由 securityask 两个参数控制。security 管「允许跑哪些命令」:deny(基本不让跑)、allowlist(只放行白名单里的)、full(不限制)。ask 管「跑之前要不要确认」:off(不确认)、on-miss(不在白名单里才确认)、always(每次都确认)。沙箱里跑的代码本就被当作不可信,所以在隔离之外再叠一层最严的 deny;而网关和 node 是机主自己的机器,OpenClaw 默认假设你想在自己机器上丝滑运行,于是给了 full + ask=off,也就是所谓的 YOLO 模式,它是明知有风险仍为了顺手而放开,所以放到真实机器上跑时尤其要留神这组配置。

配套的 process 工具参数定义如下:

process.png

这里比较有意思的是,send-keys 动作有 keys / literal / hex 三个参数,OpenClaw 将「往终端里送一段输入」抽象为三个层次,由高到低分别是:

  • keys(按键 token):用一个符号名字代表一次按键,而不是字面文本。像「回车」「Ctrl-C」「方向键上」这些按键根本不是可打印字符,回车是字节 \r、Ctrl-C 是字节 0x03、方向上键是转义序列 ESC [ A,没法直接当文本传给终端,所以 keys 接受 token,由 OpenClaw 翻译成终端真正认的字节。支持的写法大致三类:具名键(entertabescapeupbackspacef1 等)、Ctrl 组合(^c 即 Ctrl-C)、修饰键前缀(c- Ctrl / m- Alt / s- Shift,如 c-cm-xs-tab)。

    • literal(原样字符串):直接发、不翻译,你写什么就送什么字符。适合往程序里「打字」,比如填一句 hello world、回答一个文件名。注意它不会自动帮你按回车,要回车得再配 keys: ["enter"]
    • hex(十六进制字节):最底层,自己一个字节一个字节地拼,比如 ["1b","5b","41"] 就是 ESC [ A(方向上键)。token 表达不了的冷门控制序列才用得到,平时基本碰不上。

三者可以在同一次 send-keys 调用里一起传,OpenClaw 按 literalhexkeys 的固定顺序拼成最终要写入的字节流。常见组合就是「先 literal 打一段文本,再 keys: ["enter"] 敲回车」。

这个工具和 exec 是天生一对:exec 起一个会话,processsessionId 认准那个还开着的后台会话,再用 action 去操作它。

第三个工具是 code_execution,它的名字和 exec 长得很像,但是它干的却是完全不同的活。它的参数定义简单到只有一个:

code_execution.png

exec 跑的是你机器上的 shell,要你自己给出确切命令;code_execution 则不用你写代码,你只给一句话描述「要算什么」,由 xAI 的 Grok 自己写出 Python 并在它的远程沙箱里跑,再把结果回给你,全程不碰你的电脑、文件和代码仓库。使用这个工具的典型场景包括,精确数值计算、复杂数据分析、金融建模、科学计算、生成并实测一段代码等。

值得注意的是,这里的沙箱并不是 OpenClaw 自己的沙箱,而是直接套用了 xAI 官方的 Code Execution 工具

xai-code-execution-tool.png

按官方的说法,Code Execution 工具让 Grok 能实时编写并执行 Python 代码,代码跑在一个沙箱化的 Python 环境里,预装了 NumPy、Pandas、Matplotlib、SciPy 这些常用库。code_execution 工具本质上就是对这个接口的封装,它注册在 xai 插件里,默认用 grok-4-1-fast 模型。

网络访问

这一类是小龙虾「上网查东西」的本事,一共四个工具:web_search 搜网页、x_search 搜 X 帖子、web_fetch 抓某个具体网页、browser 真的开一个浏览器像人一样操作。前三个是轻量级工具,本质是「发送 HTTP 请求获取结果」,速度快、省资源、不调用浏览器,绝大多数查资料的活,使用这三件套就够了;只有遇到要登录、要点按钮、内容靠 JS 渲染的网站,才需要动用 browser 工具。

我们先看 web_search 的参数定义:

web_search.png

web_search 能传的参数也比想象中多,比如 freshness(只要最近一天/周/月/年的结果)、country/language(限定国家语言)、date_after/before(限定日期范围),还有 Perplexity 专属的 domain_filter(只在指定网站里搜)等等。

另外,使用 web_search 必须提前配置好至少一个搜索引擎的 provider,支持的 provider 如下:

  • Brave —— 结构化结果带摘要,支持国家/语言/时间过滤,有免费额度;环境变量 BRAVE_API_KEY
  • Perplexity —— 有两条路:直连 PERPLEXITY_API_KEY 走原生 Search API,返回结构化结果、支持域名/时间/语言等过滤;或用 OPENROUTER_API_KEYsk-or- 开头)经 OpenRouter 调 Perplexity 的联网模型 Sonar,拿到带引用的合成答案,但那些结构化过滤用不了。
  • Exa —— 关键词+语义 搜索,带内容抽取(highlights 相关摘录 / text 正文全文 / summary AI 摘要);环境变量 EXA_API_KEY
  • Tavily —— 结构化结果,带搜索深度、主题过滤,还配套一个 tavily_extract(相当于 Tavily 版的批量抓正文工具,一次能抽 1~20 个 URL,比 web_fetch 一次一个更省事,常用来把搜到的一批链接一口气捞回正文);环境变量 TAVILY_API_KEY
  • Firecrawl —— 结构化结果,擅长配合 firecrawl_search / firecrawl_scrape 做深度抽取;环境变量 FIRECRAWL_API_KEY
  • Gemini —— 对接 Google 搜索接口,返回带引用的 AI 合成答案;环境变量 GEMINI_API_KEY
  • Grok —— 对接 xAI 的 web 搜索接口,返回带引用的 AI 合成答案;环境变量 XAI_API_KEY
  • Kimi —— 走 Moonshot 网络搜索,返回带引用的合成答案(注意:若退化成无搜索的纯聊天会显式报错);环境变量 KIMI_API_KEY / MOONSHOT_API_KEY
  • MiniMax —— 走 MiniMax Token Plan 搜索 API,结构化结果;环境变量 MINIMAX_CODE_PLAN_KEY / MINIMAX_CODING_API_KEY / MINIMAX_OAUTH_TOKEN
  • DuckDuckGo —— 无需账号和 key,非官方 HTML 集成(没有官方 API,靠抓取并解析 DuckDuckGo 网页拿结果,脆且易被限流,仅作保底),最常用的兜底。
  • Ollama Web Search —— 走你本地登录的 Ollama 主机,或配 OLLAMA_API_KEY 调托管的 ollama.com
  • SearXNG —— 自建的元搜索,聚合 Google/Bing/DuckDuckGo 等,需配 SEARXNG_BASE_URL

三种配置方式:

  1. 命令行向导(最省事):跑 openclaw configure --section web,它会引导你选 provider 并存好凭证。
  2. 环境变量:直接设上面列的那个变量(如 export BRAVE_API_KEY=...),API 类 provider 就能用了,连配置文件都不用动。
  3. 配置文件(最可控):在 tools.web.search 里指定用哪家,key 放在对应插件的 plugins.entries.<plugin>.config.webSearch.apiKey 下:
{
  "tools": {
    "web": {
      "search": {
        "enabled": true,        // 默认就是 true
        "provider": "brave",    // 钉死用哪家;省略这行就走自动探测
        "cacheTtlMinutes": 15,  // 同一搜索词的缓存时长(分钟)
      },
    },
  },
  "plugins": {
    "entries": {
      "brave": { "config": { "webSearch": { "apiKey": "YOUR_KEY" } } },
    },
  },
}

如果你不写 provider(即自动探测),OpenClaw 会按一个固定优先级自己挨个试:先试配了 key 的,顺序是 Brave → MiniMax → Gemini → Grok → Kimi → Perplexity → Firecrawl → Exa → Tavily;一个能用的都没有,才退回到免 key 的 DuckDuckGo → Ollama → SearXNG。

同样的搜索词 15 分钟内再搜会直接给缓存结果(这个时长就是上面的 cacheTtlMinutes),省得重复花钱。

再来看下 x_search 的参数定义:

x_search.png

x_search 专门搜 X(推特)上的帖子,它和前面的 code_execution 类似:同样由 xai 插件注册,同样通过 xAI 的 Responses API(https://api.x.ai/v1/responses)调用,由 Grok 在 xAI 那边执行。区别只在带的服务端工具不同,code_execution 带的是 code_interpreterx_search 带的是 xAI 官方的 X Search 工具,默认模型 grok-4-1-fast-non-reasoning

x_search 返回的是一段带引用出处的合成答案(不是一堆原始链接)。如果你想要某条帖子精确的转发数、点赞数这类细粒度数据,最好分两步走,先用关键词搜,搜到那条帖子,拿到它的链接,再用这条确切的帖子 URL 跑第二次 x_search。因为一次宽泛的关键词搜索通常能帮你「找到」帖子,却给不全单条帖子的精确数据。

接下来是 web_fetch 的参数定义:

web_fetch.png

web_fetch 是给定一个具体网址,然后把那一页的内容抓回来。它默认不依赖任何 provider,开箱即用,自己发一个普通 HTTP GET,再用 Readability(网页主内容抽取)把正文扒出来,转成 markdown 或纯文本。

Readability 是 Mozilla 开源的一个网页正文提取库(Firefox「阅读模式」用的就是它):丢一个 HTML 页面进去,它会按一套启发式规则把导航栏、广告、侧边栏、页脚这些噪音剥掉,只留下文章主体(标题 + 正文)。web_fetch 用它把抓回来的原始 HTML 清洗成干净的 markdown/纯文本,省得把整页杂七杂八的标签都塞给模型。

当 Readability 抽取失败了(页面被反爬、结构太怪),你也可以配置 provider 再抓一次,当前内置的 provider 只有 Firecrawl 一个(可通过插件扩展)。它的配置如下:

{
  "tools": {
    "web": {
      "fetch": {
        "provider": "firecrawl",  // 可省略,省略则按凭证自动探测
      },
    },
  },
  "plugins": {
    "entries": {
      "firecrawl": {
        "enabled": true,
        "config": { "webFetch": { "apiKey": "fc-..." } },  // 或设 FIRECRAWL_API_KEY
      },
    },
  },
}

关于 web_fetch,它还有一道独立的安全防护,叫 SSRF 防护。SSRF 全称服务端请求伪造,它的问题根源在于 web_fetch 要抓的那个 URL 是不可信的外部输入,它可能来自搜索结果、模型读到的某段网页内容、用户的一句话,甚至是别人通过提示注入诱导模型去填的 URL。攻击者只要能影响这个 URL,就能让小龙虾以服务器自己的身份去访问本不该碰的地址,比如公司内网的机器、本机回环地址(127.0.0.1)、或者云服务器上那个一访问就能读到密钥的「元数据接口」(169.254.169.254)。更阴的一招是用一个看着人畜无害的公网链接,靠 30x 跳转把请求重定向到内网地址。web_fetch 默认就把这些私网、环回、元数据地址全挡掉,并且对重定向也逐跳重新校验,除非你明确放行。

169.254.169.254 是一个特殊的保留 IPv4 链路本地地址(Link-local address),几乎所有的云计算平台(如 AWS EC2、Azure、阿里云等)都将其作为 实例元数据服务(Instance Metadata Service, IMDS)。它允许虚拟机从其主机获取配置信息、安全凭证等,且该地址仅在局域网内有效,不可路由到外网。正因为它的特殊性,所以它往往是 SSRF 攻击的头号目标,攻击者只要能让服务器替自己请求这个地址,往往就能直接拿到云凭证、进而接管你的云上资源。

除了这三个轻量级工具,我们还可以使用 browser 进行网络访问,它会真的开一个 Chromium 浏览器,像人一样去操作网页。这个工具能力最强,风险也最高,比较复杂,我们放到下一篇专门学习。

文件读写

这一类管的是小龙虾在工作区里读写文件,一共四个工具:read 读文件、write 整篇覆盖写、edit 按原文精确替换地改、apply_patch 把多文件多处改动打成一个补丁一次性提交。

我们先来看下这四个工具的定义:

read_write_edit_apply_patch.png

前三个比较好理解,它们能力递进,参数从少到多:read 只要个路径;write 多一个 content,是「整篇覆盖」;editedits 是一组 {oldText → newText},并要求 oldText 在文件里唯一,并且各处替换互不重叠,所以它比 write 安全,因此日常编辑文件或修改代码,几乎清一色用 editwrite 一般只在新建文件时才上场。

另外 read 还有一个细节,它默认最多读 2000 行或 50KB,先到为准,如果拿来读长日志可能会被截断,这时候得搭配 offset / limit 分段读。

最后的 apply_patch 参数也很简单,只有一个 input,但它是这一组里功能最强、也最复杂的一个,门道全在这段字符串的格式里。先解释一个词:hunk(块)。我们改一个文件,往往不是只动一处,而是东一段西一段,好几处不相邻的改动。程序员之间习惯用一种叫 diff 的文本格式来记录这个文件改了什么;这份文本拿去给工具自动应用,就叫 补丁(patch)。在这种格式里,每一段连续的改动就被叫作一个 hunk。edit 一次只能改一处,要改五处就得调五次,文件一大很容易对不上行,改错地方。apply_patch 的思路是:把「这个文件第几段删什么加什么、那个文件又改哪里、再删掉某个文件」全写进一段补丁文本里,一次性提交。它接受的 input 参数长这样:

*** Begin Patch
*** Add File: 新文件路径         # 新建文件
*** Update File: 已有文件路径    # 改已有文件,下面可跟多个 @@ 块
*** Move to: 新文件路径          # 可选:顺带给这个文件改名
@@
-旧的一行
+新的一行
*** Delete File: 要删的文件      # 删文件
*** End Patch

可以看到,一个补丁里能同时 Add / Update / Delete 多个文件,每个文件里还能有多个 hunk,Update 时跟一行 Move to: 还能顺带对文件进行改名。

这套格式初看和 git diff 很像,但要注意的是,它并不是标准的 git diff 格式,它是 OpenAI 自己定的一套语法,外面套着 *** Begin Patch*** End Patch@@ 后面也不带行号(标准 diff 是 @@ -10,7 +10,7 @@,模型容易数错),改用上下文行来定位。也正因为它是 OpenAI 阵营的格式,所以只有 OpenAI 家的模型被专门训练成会输出这种补丁,所以 apply_patch 在 OpenClaw 里默认只对 OpenAI / OpenAI Codex 模型开启,换成其他模型效果可能会不太理想。

消息收发

这一类只有 message 一个工具,它是小龙虾跟各个通道(Telegram、飞书等各种 IM)打交道的总接口,也是整个工具箱里参数最多的一个。它的 schema 是按「动作」动态拼出来的,光 action 一个枚举就有五十多个值:send(发消息)、reply(回复)、react(贴表情)、poll(发起投票)、pin(置顶消息)、thread-create(建话题)、kick(踢人)、ban(封禁)……几乎一个 IM 机器人能干的事都归它:

message-actions_01.jpg

这里只挑最常用的几个参数看下:

message.png

还剩下几十个参数都是某个具体 action 才用得到的(比如建话题、改群名、踢人),运行时还会按当前通道的能力过滤。我们暂时只要知道 message 是个按 action 分派的大工具就够了,具体动作用到时再查。

未完待续

这一篇我们先把 OpenClaw 工具体系的地基和前半批工具过了一遍:

  1. 三层关系:工具是 agent 实际调用的带类型函数,技能是教它何时怎么用工具的手册,插件是把这些打包到一起的箱子
  2. 内置工具分八类:命令执行、网络访问、文件读写、消息收发、设备控制、运行时管理、媒体生成、子 agent 编排,这一篇讲完了前四类
  3. 命令执行exechost(在哪跑)+ security(让不让跑)+ ask(要不要问你)三个旋钮划定边界,配套的 process 用来操作转后台的会话,code_execution 则是套了 xAI 官方的 Code Execution 工具、让 Grok 远程编写并运行 Python 脚本
  4. 网络访问:轻量三件套 web_search(认 provider,要先配)、x_search(走 xAI)、web_fetch(自带 SSRF 防护),重型的 browser 留到后面单独讲
  5. 文件读写read / write / edit / apply_patch 能力递进,日常改代码最常用 editapply_patch 用的是 OpenAI 那套补丁格式、默认只对 OpenAI / Codex 模型开启
  6. 消息收发message 一个工具按 action 分派,五十多个动作几乎涵盖一个 IM 机器人能干的所有事

剩下的设备控制(nodes / canvas)、运行时管理(cron / gateway)、媒体生成和子 agent 编排这四类工具,内容还不少,我们下一篇继续学习。


把小龙虾装进口袋:iOS / Android Node 配对

在上一篇里,我们装好并体验了 macOS app,它横跨 operator 和 node 两类身份,既能当控制台,又能把这台 Mac 的屏幕、摄像头、命令行暴露给 agent。今天我们把目光转到手机上:iOS Node 和 Android Node,两个纯 node 客户端。它们身上没有 operator 那半边,唯一的任务就是当小龙虾的远程感官,把手机的麦克风、摄像头、GPS、屏幕暴露给家里的 agent 调用。配合上一篇讲的 Gateway 远程访问,出门在外也能随时唤起家里的小龙虾。

配对 iOS Node

在 Gateway 协议里 iOS Node 的 rolenode,走的是和 Telegram bot 那种 channel 完全不同的握手路径。channel 那套叫 DM 配对(DM pairing),陌生人第一次给 bot 发消息时,bot 回一段 8 位的一次性 pairing code,你在 CLI 里 openclaw pairing approve 批准这个码,它管的是谁能跟 bot 说话。iOS Node 这套叫 设备配对(device pairing),手机带着自己的 ED25519 设备身份连上 Gateway 的 WebSocket,Gateway 挂起一条 pending 请求(用 requestId 标识),你在 CLI 里 openclaw devices approve 批准这台设备,Gateway 随即签发一个会轮换的 token,它管的是哪台设备能进网络。这套握手流程里还有另一组容易混的词,Node 和 Device,我们先了解下它们的区别。

Node vs. Device

明明连进来的是一台 Node(iOS 这台手机),批准用的却是 openclaw devices approve,查状态又换回 openclaw nodes status。它俩在 OpenClaw 里是上下两层:

  • Device(设备身份) 管这是谁。可以把它理解成这台设备的身份证。设备自己生成一对 ED25519 密钥,由公钥算出一个 deviceId 当唯一编号,再配一份授权记录,记下它被谁批准过、能用哪些角色。Gateway 靠它做身份识别和准入:没见过的 deviceId 必须经操作员批准,批准后按角色发 token(这个 token 可以随时换发新的、把旧的作废,也可以直接吊销,但不管怎么换都不能超出配对时批准的 role / scope 范围)。一个 Device 还能同时持有多个角色的 token,比如上一篇那台 Mac,既是 operator 又是 node
  • Node(能力主机) 管它能干啥。可以把它理解成这台设备对外贴出的一张能力清单。设备以 role: node 连进 Gateway 时,会报上自己的 caps(有哪些硬件能力,比如摄像头、屏幕、定位、麦克风)和 commands(允许别人调用的命令白名单,像 camera.snapcanvas.snapshotlocation.get)。Gateway 把它记进一张运行时的 node 名册,别的客户端就能用 node.invoke 把命令派给它执行。

拿到 app

值得注意的是,iOS app 暂时还没上架 App Store,官方给它贴的标签是「极早期 alpha」,因此把它当成尝鲜,别当成生产工具。能拿到 app 的途径只有两个:

  1. TestFlight 内测:如果你在内测名单里
  2. 自己编译:根据官方文档,从源码用 Xcode 编译,我走的就是这条开发路径

首先确认几个前置工具装齐了:

  • Xcode 16+:低于这个版本工程文件可能打不开
  • Node 的 pnpm:OpenClaw 整个仓库的包管理器,iOS 这边的脚本也靠它
  • xcodegen:这个工程不把 .xcodeproj 提交进仓库,而是用一份 YAML 描述、靠 xcodegen 现场生成,所以这步不能省(brew install xcodegen 装一下)
  • Xcode 里配好 Apple 开发者签名:哪怕只是个人免费 Apple ID 也行,但得先在 Xcode 的 Settings → Accounts 里登进去

一切就绪之后,在仓库根目录依次运行如下命令:

$ pnpm install                        # 装 monorepo 依赖
$ ./scripts/ios-configure-signing.sh  # 给你这台机器生成一套唯一的本地 bundle ID
$ cd apps/ios
$ xcodegen generate                   # 用 YAML 现场生成 OpenClaw.xcodeproj
$ open OpenClaw.xcodeproj              # 用 Xcode 打开

这几步等价于一条 pnpm ios:open 快捷命令,效果一样。

工程在 Xcode 里打开后,设置如下:

  • SchemeOpenClaw
  • Destination 选你那台用数据线连上来的真机。文档明确建议用真机而不是模拟器,因为摄像头、定位、麦克风这些 node 能力在模拟器上要么没有要么是假的,跑真机才看得到真实行为
  • Build ConfigurationDebug

最后点击顶部菜单 Product → Run,编译并运行,标题栏显示 "Build Succeeded" 说明编译通过:

xcode-build.png

在编译的过程中遇到了几个问题,将报错信息丢给小龙虾,很快自己修复了。主要是 SettingsTab.swiftbody 视图嵌套过深导致 Swift 编译器类型检查超时,把它拆成多个子视图后才编译通过,另外还补了 GatewayOnboardingView.swift 缺失的 import OpenClawKit、给 DeepLinks.swift 静态方法调用加上 self. 前缀。

Run 成功后 Xcode 会把 app 直接装到那台连着的手机上,几秒之后手机桌面上就出现了那只红色的小龙虾:

openclaw-icon.png

另外个人开发者签出来的 app 证书有效期一般只有 7 天,过期了 app 会打不开,重新 Run 一次刷新即可。

配对流程

第一次点开这个图标,会先跳一个 "Welcome to OpenClaw" 引导页:

welcoma-to-openclaw.png

页面把流程拆成了三步:Connect to your gateway → Choose device permissions → Use OpenClaw from your phone,下面还有一条橙色的 Security notice,提醒连进来的 agent 可以使用你勾上的设备能力(摄像头、麦克风、照片、联系人、日历、位置等),只在你信任这个 gateway 和 agent 的时候继续。这条提醒值得认真对待。iOS Node 的所有能力命令都跑在这台手机本地,读的也是手机上的真实硬件,谁连进来就能把这台手机当远程感官。所以 Gateway 是否暴露给互联网、Gateway 后面挂的 agent 是不是你能管的,直接决定了风险有多大。

点 Continue 进入 Connect Gateway 配对页:

connect-gateway.png

页面给了两个入口:Scan QR Code(扫二维码)和 Set Up Manually(手动)。两条路最终都是把 Gateway 的 WebSocket 地址加一段一次性的 bootstrap token 喂给 app,区别只在于这段东西是用相机拍进来还是手动粘进来。

方式一:setup code(手动配)

在 WebChat 里给 agent 发一条 /pair,Gateway 会生成一个 setup code 并给出操作步骤:

pair-code.png

回复里最关键的是 Setup codeeyJ1cm...),它是一段 base64 编码的 JSON,形如 {"url": "wss://<gateway-host>", "bootstrapToken": "<single-use-token>"}。它不是设备私钥,而是一张一次性入场券,app 拿它换一个正式的 device token,正式 token 才会落进 iOS Keychain。SecurityExpires 两行合起来看,这张券只能用一次、10 分钟内有效,过期或用过就作废,得重发一条 /pair 再生成一次。最后 Gateway 那行 wss://macbook-air.tail7d2cad.ts.net,就是上一篇配 Tailscale Serve 时拿到的 magicDNS 名加 WSS,远程访问已经就绪时 setup code 里塞的就是它。

/pair 这条 slash 命令本质上是 WebChat 里的一个 operator 操作,等价于在 Gateway 主机上预签一份单次 bootstrap 凭证。它和 openclaw devices approve <requestId> 方向正好相反:devices approve 是设备先连过来挂起一个 requestId 等操作员批准,setup code 是操作员先签好凭证、设备再扫码进门。

把整段 setup code 复制下来,回到手机,进 OpenClaw 的 Settings → Gateway:

gateway-setting.png

将 setup code 粘贴到输入框中,如果 Tailscale 网络正常的话,几秒之后这一页就会变成这样:

gateway-setting-done.png

可以看到状态显示已连接。

方式二:扫二维码

这种方式的原理和第一种几乎一样,但要更简单。

在 WebChat 里发 /pair qr,agent 会把同样的 setup code 渲染成一张二维码。回到 app 的 Connect Gateway 页(或 Settings → Gateway 里的 Scan QR Code 按钮),把相机对准二维码,扫到之后 app 自动解析出里面的 {url, bootstrapToken} 并完成连接。

方式三:Bonjour + 手动批准(同子网时的另一条路)

前面两种方式对应的就是配对页上那两个入口,共同点是都得先去 Gateway 那头要一段 setup code。不过如果你的手机和 Gateway 本来就在同一个局域网里,还有第三条路,连 setup code 都省了,靠 Bonjour 让 app 自己把 Gateway 找出来。

先解释下 Bonjour 是什么。它是苹果的零配置局域网服务发现协议(苹果对 mDNS / DNS-SD 这套标准的实现),作用是让同一个局域网里的设备不用手填 IP 和端口就能互相广播、发现服务。你平时见到的 AirDrop、AirPlay、打印机自动出现在列表里、局域网里能直接访问 xxx.local 主机名,背后靠的都是它。

回到这里,Gateway 启动后会用 Bonjour 在局域网里广播一个 _openclaw-gw._tcp 服务(TXT 记录里带着 host、端口、TLS 指纹);iOS app 用 Network.framework 的 NWBrowser 浏览这个服务类型,前面那张截图里的 Settings 里那个 "Discovery: Searching..." 就是它在扫。扫到了 Gateway 就自动出现在列表里,点一下就能连,不用 setup code、也不用手填地址。选好之后 app 会发起配对请求,这时回到 Gateway 主机上批准它:

$ openclaw devices list
$ openclaw devices approve <requestId>

这种方式最大的局限是,只在同子网有效,跨网段(比如走 Tailscale 远程)mDNS 广播过不去就发现不到,那种情况只能回到方式一或方式二。

iOS Node 的能力详情

配对之后,打开 Settings 配置页,再展开 Device → Features,所有 node 能力的总开关都在这里:

device-features.png

  • Voice Wake:唤醒词模式,持续监听麦克风,当听到唤醒词后开始录音,并将识别到的文本作为一条普通消息发给 agent
  • Talk Mode:连续对话模式,唤醒词命中后不再是录音 + 转写 + 发文字,而是直接把 app 切到对话模式,这是一个 STT → LLM → TTS 的连续语音对话循环;iOS 的静默判定窗口默认 900 毫秒,且默认开 interrupt-on-speech,你在它说话时插话会自动打断播放;
  • Speech Language:转写和合成的语言,我这里选的是 中文(中国),会传给 STT/TTS 服务
  • Background Listening:允许 Voice Wake / Talk Mode 在 app 不在前台时继续监听麦克风。iOS 这块管得很严,开了也只是尽量维持、不保证一直管用,app 不在前台、或者锁屏几分钟之后,系统会切断麦克风,喊唤醒词没反应是正常的
  • Wake Words:触发词列表,自动从网关同步,可以看到就是我们上一篇在 macOS app 里的配置的
  • Allow Camera:摄像头能力总闸,和 macOS app 不一样的是,iOS 这边默认是开的,另外 iOS 有后台限制,canvas.* / camera.* / screen.* 这几类命令要求 app 在前台,切到后台调用一律返回 NODE_BACKGROUND_UNAVAILABLE,这是 iOS 系统层面的限制,OpenClaw 绕不过去
  • Location Access:三段选择器,Off 完全不开,While Using 只在 OpenClaw 在前台时可用,Always 允许后台持续读位置
  • Prevent Sleep:防止屏幕自动锁定。出门跑 voice + location 场景时开着它能减少 app 被系统挂起的概率
  • Advanced:网络、日志、Talk provider 的细参数

这一页本质上就是 iOS Node 的能力声明,和 agent 对话时,它可以通过 node.invoke 来调你手机的这些能力,你也可以通过 CLI 命令手动触发来验证。这些在上一篇已经介绍过,此处不再赘述。

配上之后的首页

把 Settings 关掉回到主视图,能看到 app 已经标记 "CONNECTED TO MACBOOK-AIR.TAIL7D2CAD.TS...":

app-home.png

下面的 Live agents 列表(main / personal / work),是我们之前创建的多个智能体,点击任意一个将它设置为这台手机当前的 Active agent,下次连同一个 Gateway 时还能记得用的是哪个 agent。

iOS app 上也内置了一个轻量 Chat 入口,发条消息测试一下:

main-chat.png

Android Node 简介

Android 的玩法和 iOS 大体一致,下面挑几处不一样的地方说一说。

  • 安装包一样得自己编译。 和 iOS 一样,应用商店里没有,得自己用 Android SDK 加 Java 17 手工打包。
  • 它会一直挂一条通知,这是正常的。 Android 不允许应用长期在后台偷偷跑,想保持连接就得开一个前台服务,代价是状态栏会常驻一条通知。这是系统强制要求,不是 bug,别去关它。进入 Talk Mode 时,它会临时给自己加上麦克风权限,对话结束再收回去。
  • 配对方式和 iOS 一样。 在 Connect 标签页里,同样是 Setup Code(输入配对码)或 Manual(手填地址)两条路,连上后照例回到 Gateway 主机上跑 openclaw devices approve 批准这台设备。
  • 远程连接必须走加密通道。 只要不是在同一个局域网里(比如通过上一篇讲的 Tailscale 远程访问),就必须用加密的 wss:// 地址,明文的 ws:// 在首次远程配对时会被拒。只有当手机和 Gateway 确实在同一个内网时(比如局域网 IP、.local 主机名、本机 localhost、模拟器的桥接地址 10.0.2.2 这些情况)才允许用明文 ws://
  • Android 没有唤醒词模式。 取而代之的是 Voice 标签页里两个手动模式(同时只能开一个):Mic 是手动单次录音,说一段、停顿一下就发一轮,切出应用就停;Talk 是连续语音对话,开了之后一直聊,直到你手动关掉或者连接断开。之所以砍掉唤醒词,是因为 Android 对后台录音管得比 iOS 还死,常驻监听很容易被系统直接掐掉。
  • 多了一批读取手机个人数据的能力。 具体能用哪些,取决于设备型号和你给了哪些权限,大致覆盖这几类:读通知和点通知按钮、取最近的照片、查/加联系人、查/加日历日程、翻通话记录、查/发短信、读运动步数,外加查设备本身的状态、信息、权限和电量。对应的命令名分别是 notifications.*photos.latestcontacts.*calendar.*callLog.searchsms.*motion.*device.*,需要时再去文档查具体参数即可。
  • 通知转发可以按需收窄。 默认会把手机通知都转给 agent,嫌吵的话可以调:用 allowPackages / denyPackages 指定只转(或不转)哪些应用的通知,用 quietHours 设一个免打扰时段,用 rateLimit 限制每个应用每分钟最多转几条。这套要 Android 的通知使用权限,应用首次引导时会主动来要。
  • 还能让系统语音助手当入口。 你可以直接对 Google Assistant 说 "Hey Google, ask OpenClaw…",这句话会被当成一条普通聊天消息送进 OpenClaw。这靠的是应用清单里声明的一段元数据,Gateway 那边不用做任何配置。

我手边没有 Android 设备,就不实际操作了,感兴趣的朋友可以参考文档自己体验试试。

实战 iOS app

这一节我同样让 cc 给我设计了一个场景:出差路上,对手机说 "老钳,看看我现在在哪,帮我查附近最近的咖啡馆",手机录到语音 → 推给家里的 agent → agent 调 location.get 拿 GPS → 查询位置和附近的咖啡馆 → 通过 talk.speak 把答案语音播回手机。这条链路把 node、location、语音、远程访问全串起来了,分五步。

第一步:先把 iOS Node 跟 Gateway 配上对。 在 WebChat 里给 agent 发一条 /pair 拿到 setup code,整段复制下来,回到手机进 OpenClaw 的 Settings → Gateway 粘贴进去,状态显示已连接即可。

第二步:在 iOS app 里打开 Location 权限。 Settings → Location,选 While Using。在 Gateway 主机上验证一下能不能拿到坐标:

$ openclaw nodes location get --node "iOS Node"

正常会返回 { "lat": 31.23, "lon": 121.47, "accuracyMeters": 12.5, "isPrecise": true, ... } 这样的 payload。如果报 LOCATION_DISABLED 是 app 里的选择器没开,LOCATION_PERMISSION_REQUIRED 是系统权限没给,LOCATION_BACKGROUND_UNAVAILABLE 是 app 被切到后台了,测的时候保持 app 在前台。

第三步:配好 Talk provider,让 agent 能把答案念回来。 编辑 ~/.openclaw/openclaw.jsontalk 段,最省事的是先用 macOS 自带的系统 TTS 兜底(不要 API key),跑通了再换 ElevenLabs 这种更自然的:

{
  "talk": {
    "provider": "system",
    "providers": {
      "system": {},
      "elevenlabs": {
        "voiceId": "elevenlabs_voice_id",
        "modelId": "eleven_v3",
        "apiKey": "elevenlabs_api_key"
      }
    }
  }
}

Talk 的几个 provider 是按配置顺序回退的:ElevenLabs 不通就走 mlx(macOS 本地推理),再不通就走 system(macOS 自带的 say)。个人用 ElevenLabs 配额挺紧(免费档每月就 1 万字符),先用 system 把链路跑通是更稳的选择。

第四步:检查唤醒词。 唤醒词是一份由 Gateway 统一管的全局列表,存在 ~/.openclaw/settings/voicewake.json 里:

{
  "triggers": [
    "老钳",
    "老钱",
    "老秦"
  ],
  "updatedAtMs":  1778973181234
}

任何客户端 UI 改了它都会广播给所有 node 同步。在 iOS app 的 Settings → Wake Words 里确认有你想喊的词(建议别用 heyok 这种太常见的,误触发率高),iOS 端再把本地的 Voice Wake 开关打开。

第五步:出门,让手机走 Tailscale 连回家里的 Gateway。 上一篇配的 Tailscale Serve 这时候就派上用场了,家里的 Gateway 通过 https://<magicdns> / wss://<magicdns> 对 tailnet 内可见,手机装了 Tailscale 客户端、登录同一个账号之后,无论走 4G 还是外面的 WiFi,都能反向连回家。这时候对手机喊 "老钳,看看我现在在哪,帮我查附近最近的咖啡馆":

  1. iOS Node 的 VoiceWakeManager 在本地命中唤醒词,开始录音
  2. 检测到一段静默后截断,把转写文本通过 WebSocket 推给 Gateway
  3. Gateway 把它送进 agent 一轮 turn
  4. agent 判断需要位置,调 location.get 拿到经纬度
  5. agent 用 web 搜索工具查这个坐标附近的咖啡馆
  6. agent 把结果整理成一句话,通过 talk.speak 经配置的 Talk provider 合成语音
  7. 音频流回 iOS Node 播放,你在耳机里听到答案

整条链路的时序大致是这样:

sequence.png

小结

这一篇我们把 OpenClaw 在手机上的两个客户端(iOS 和 Android)走了一遍,最后来总结一下:

  1. Device 和 Node 的区别: Device 是设备的身份证,管的是这台设备是谁、准不准进来;Node 是它进来之后贴出的一张能力清单,管的是它能被调用哪些功能。
  2. iOS Node 的配对和使用: 目前还很早起,没上架 App Store,得走 TestFlight 内测或自己用 Xcode 编译。一共三种配对方式,配上之后,它能为家里的 agent 提供这些远程感官:拍照、录视频、截屏、获取 GPS 位置、连续语音对话、唤醒词唤起等。
  3. Android Node 和 iOS 大体一致: 但是它默认不开唤醒词,主要靠手动的录音/对话模式;为了保持连接,它会在状态栏常驻一条通知。作为补偿,它比 iOS 多出一批读手机数据的能力(通知、通讯录、日历、短信、通话记录、运动步数等)。它同样没有公开发布,需要自己编译。

目前为止,我们忙活的全是怎么给小龙虾接上手脚、眼睛、耳朵和 GPS,却从没系统看过它接到任务之后到底能调哪些工具,这些工具又是怎么串起来的。回头看看这两篇的实战例子,上一篇让小龙虾帮忙排查屏幕上的报错,这一篇让它查找附近的咖啡馆,我们会发现这两个任务其实都不简单。就拿查咖啡馆来说,小龙虾先调 location.get 拿坐标、再用 web 搜索查、最后把结果合成语音念出来。这一连串的动作其实涉及两个问题:OpenClaw 有哪些工具可用,OpenClaw 拿到任务后使用什么工具,按什么顺序来调用工具。

下一篇我们就先把 OpenClaw 的工具箱挨个翻一遍,之后再讲怎么给它配操作手册。

参考


把小龙虾钉在菜单栏:OpenClaw 的 macOS app(二)

上一篇我们把 OpenClaw 的 macOS app 装上了,弄清了它「菜单栏伴侣」的定位,看了它作为 operator 那半边的功能,结尾停在了它的另一重身份上:它启动后还会以 role: node 连进同一个 Gateway,把自己的一组 capscommands 报上去、登记进 node 注册表,别的客户端就能用 node.invoke 把命令转过来。

这一篇就接着上一篇的结尾,详细学习下 macOS app 作为 node 的几个能力 —— Canvas 画板、摄像头与录屏、system.run 和 Exec approvals、Voice Wake 与 Push-to-talk,最后再把它们串起来,跑一个实战的例子。

Canvas

Canvas 是 macOS app 内嵌的一块 WKWebView 面板,专门给 agent 当轻量可视化工作区用,渲染 HTML/CSS/JS、A2UI、小的交互 UI。它的状态存在 ~/Library/Application Support/OpenClaw/canvas/<session>/ 下,面板通过一个自定义 URL scheme openclaw-canvas://<session>/<path> 来读这些文件(这样不用起 loopback server,也天生挡掉了目录穿越)。如果某个 session 根目录下没有 index.html,app 会显示一个内置的脚手架页。

面板本身是无边框、可缩放的,吸在菜单栏附近(或鼠标位置),按 session 记住大小和位置,本地 canvas 文件一改它自动重载,同一时刻只显示一个 Canvas 面板。可以在 Settings 配置页面的 Allow Canvas 选项来整个关掉,关掉之后所有 canvas 命令返回 CANVAS_DISABLED

平时是 agent 在对话里通过 Gateway WebSocket 驱动 Canvas,但 CLI 也能手动操作来验证。最直接的就是让它弹出来:

$ openclaw nodes canvas present --node <node-id>

跑完之后,菜单栏附近会弹出一块画板,但里面几乎是空白的:

canvas-a2ui.png

这个空白页是正常现象,canvas.present 只管「把面板显示出来」,不管往里塞内容。macOS 第一次打开 Canvas 会跳到 Gateway 托管的 A2UI host 页,这个页面本身就是个空壳:它只是 A2UI 的渲染运行时,在 agent 真正推内容之前页面上当然什么都没有。

注意:画板里有时会显示一行 Unauthorized 错误。这不是 macOS 的 TCC 权限问题,而是 WebView 去拉 A2UI host 页时 token 失效了(token 有 10 分钟 TTL,Gateway 重启过、连接断过、或长时间没动过画板都会让它过期)。可以从菜单栏退出 app 再重开,让它重新握手拿到新鲜的 token 就好了;要么在 Gateway 配置里把 canvasHost.enabled 设成 false 彻底关掉这个托管页(直接的 canvas.* 命令和本地脚手架页照常用)。

这里顺便介绍下 A2UIA2UI(Agent-to-UI) 是一套声明式的 UI 协议,agent 不用手写 HTML,而是发一串 JSONL 消息来描述「界面长什么样」:用 ColumnText 这类组件搭出一棵界面树,配上 usageHint(标题 / 正文之类)这种语义提示,由客户端的 A2UI 渲染器统一把它画成真正的控件;用户在界面上的点击、输入再作为事件反向回传给 agent,是双向的。OpenClaw 把这个渲染器(A2UI host)做成了 Canvas 面板里的内嵌页,上面的 canvas.present 默认跳过去的就是它。目前 Canvas 只认 A2UI v0.8 的 server→client 消息(beginRendering / surfaceUpdate / dataModelUpdate / deleteSurface),v0.9 的 createSurface 还不支持。

a2ui.png

除了 canvas.present 命令,也可以用 canvas.navigate 打开一个本地 canvas 路径,或者 http(s)file:// URL,或者直接传 "/" 返回本地脚手架页:

$ openclaw nodes canvas navigate "/" --node <node-id>

显示如下:

canvas-home.png

如果想看到自定义内容,可以用 canvas.a2ui push 推一段 A2UI 协议:

$ openclaw nodes canvas a2ui push --text "hi from canvas" --node <node-id>

面板中就会显示出这段文本:

canvas-hi.png

还可以用 canvas.eval 直接在 WebView 里跑 JS:

$ openclaw nodes canvas eval --node <node-id> --js "document.body.innerHTML='<h1>hi</h1>'"

然后用 canvas.snapshot 把当前画面截成图:

$ openclaw nodes canvas snapshot --node <node-id>

在 agent 对话里怎么用

上面这一串 openclaw nodes canvas ... 是给你手动验证用的,平时你不会这么敲。Gateway 给 agent 装了一个 canvas 工具,动作就是 present / hide / navigate / eval / snapshot / a2ui_push / a2ui_reset 这几个,跟上面的命令一一对应。你在对话里用自然语言提需求,agent 自己决定调它,往菜单栏这块面板里渲染东西。比如:

  • 「把这周的 commit 按作者画个柱状图给我看」→ agent 写段 HTML/JS、navigate 到一个本地 canvas 文件,或直接 a2ui_push 推一段 A2UI
  • 「做个表单让我填 name / 环境 / 分支三个参数」→ agent 推一段带输入框和按钮的 A2UI,你在面板上填完点提交,A2UI 把事件回传给 agent,它接着往下走 —— 这就是前面说的「双向」
  • 「这份 JSON 太长,渲染得好看点」→ agent eval 往面板里塞渲染好的 HTML
  • agent 渲染完想确认效果,会自己 snapshot 把面板截成图喂回自己,再决定要不要改

canvas-form.png

调哪个 node 一般不用你指定,这个工具默认优先挑本机的 macOS node,没有再挑第一个连着的、有 canvas 能力的 node。所以只要 macOS app 开着、Allow Canvas 没关,跟 agent 说「画个 xxx」它就会渲染到这儿。另外,如果你用的是 Control UI / WebChat 这种带聊天气泡的客户端,agent 还会把 HTML 写成 Gateway 托管的 canvas 文档,在回复里用 [embed ...] 标签直接嵌进聊天气泡。

摄像头与录屏

摄像头这边,macOS app 暴露 camera.snap(拍一张 jpg 照片)和 camera.clip(录一段 mp4 视频,可带音频,时长上限 60 秒)。和 iOS / Android 不一样的是,macOS 上摄像头默认是关的(Settings → General → Allow Camera)。打开之后 camera.snap 在拍之前会等一个 delayMs(默认 2000 毫秒)让曝光稳定,照片会被重新压缩保证 base64 在 5 MB 以下。

可以通过下面两个 CLI 命令进行测试:

$ openclaw nodes camera snap --node <node-id>
$ openclaw nodes camera clip --node <node-id> --duration 10s --no-audio

录屏走 screen.snapshot(截当前屏)和 screen.record(录一段 mp4 视频,同样 60 秒上限),需要 macOS 的 Screen Recording TCC 权限。这也是为什么这类操作必须由 app 来做,无头进程拿不到这个权限。

测试命令如下:

$ openclaw nodes screen record --node <node-id> --duration 10s --fps 15

值得注意的是,openclaw nodes screen 下面没有 snapshot 子命令,可以使用 openclaw nodes invoke 来调用:

$ openclaw nodes invoke \
  --node <node-id> \
  --command screen.snapshot \
  --params '{"screenIndex":0}'

这里还有一个 Gateway 的配置需要注意:由于 camera.snapcamera.clipscreen.record 这几个命令的隐私分量很重,需要在 gateway.nodes.allowCommands 里显式开启。

system.run 与 Exec approvals

system.run 是 macOS app 最有分量的一项 node 能力,它可以让 agent 在你这台 Mac 上运行命令。它跟 agent 自带的 exec 工具是两个很容易混淆的概念,其实,它们两不在一个层面上:exec 是模型在对话里直接调的工具,跟 read_file 平级,它内部有三种后端 —— sandbox(丢进沙箱隔离环境跑)、gateway(在 Gateway 进程里直接 spawn)、node(转交给一台远端 node);system.run 则是 macOS app 作为 node 暴露出来的一项能力,跟 screen.snapshotcanvas.present 平级。当 agent 的 exec 选了 host=node 这个后端时,请求会被序列化成 node.invoke system.run 走 Gateway 转过来,也就是说,system.run 可以理解成 exec(host=node) 在你这台 Mac 上的远端实现。

分为两种情况:

  • Local 模式:Gateway 把 system.run 请求直接交给 macOS app,app 在自己的 UI / TCC context 里执行
  • Remote 模式:远程 Gateway 先把请求发给本机那个 headless node host 服务(它以 node 身份连进远程 Gateway),node host 再通过一个本地 Unix socket(带 token + HMAC + TTL)把请求转给 macOS app 去跑,弹窗和输出都留在 app 里

不论哪种情况,最后都会被 macOS app 里的 Exec approvals 拦下(Settings → Permissions → Exec approvals):

settings-permissions.png

这些配置全部存储在这台 Mac 本地的 ~/.openclaw/exec-approvals.json 文件里,默认大致长这样:

{
  "version": 1,
  "defaults": {
    "security": "deny",
    "ask": "on-miss"
  },
  "agents": {
    "main": {
      "security": "allowlist",
      "ask": "on-miss",
      "allowlist": [{ "pattern": "/opt/homebrew/bin/rg" }]
    }
  }
}

需要注意的几点:

  • security 三选一:deny(全挡,默认)、allowlist(只放行白名单里的)、full(全放,等于 YOLO 模式)
  • ask 三选一:off(不弹)、on-miss(只在不命中白名单时弹)、always(每条都弹)
  • allowlist 里要么写解析后的二进制路径 glob(/opt/homebrew/bin/rg~/.local/bin/*),要么写裸命令名(rg,只匹配走 PATH 调起来的,不匹配 ./rg/tmp/rg
  • 命令文本里只要带 shell 控制或展开语法(&&||;| ` $<>()),一律算 allowlist miss,得单独批,或者把那个 shell 二进制本身 allowlist 掉
  • 弹窗里点 Always Allow,那条命令就被加进 allowlist
  • allowlist 是按 agent 分的:多个 agent 的话,在 app 里切换你正在编辑哪个 agent,一个 agent 的批准不会泄漏到别的 agent
  • 如果 macOS app 没在跑(比如你只起了 CLI 和 Gateway),任何本来该弹窗的请求会走 ask fallback,默认就是 deny

Voice Wake 与 Push-to-talk

macOS app 还能监听全局麦克风,把语音变成 agent 的输入。不过有一个前提条件,该功能依赖 macOS 26 才引入的新接口,macOS 26 以下整块 Voice Wake 配置都是灰色的,用不了:

voice-wake.png

满足版本之后,设置页里有三个触发模式和两个 Talk Mode 行为开关:

voice-wake-available.png

先说三个触发模式:

  • Voice Wake(唤醒词 → 文字,默认):一直挂着 macOS 的 Speech recognizer(即系统自带的 SFSpeechRecognizer,跟 Siri / 听写共用一套引擎,支持完全离线),听到唤醒词才开始录音,识别到的文本作为一条普通消息发给 agent。唤醒词是一份由 Gateway 统一管的全局列表(没有按 node 定制的唤醒词),存在 Gateway 主机的 ~/.openclaw/settings/voicewake.json 里,默认是 ["openclaw", "claude", "computer"],任何客户端 UI 改了它都会广播给所有 node 同步。
  • Talk Mode(唤醒词 → Talk Mode):勾上这个选项后,唤醒词命中后不再是录音 + 转写 + 发文字,而是直接把 app 切到 Talk Mode,这是一个 STT → LLM → TTS 的连续语音对话循环,屏幕上一直挂着一个小面板,在「聆听(listening)→ 思考(thinking)→ 回复(speaking)」三个阶段之间循环切换,各自播一段对应的动画,回复也是直接念出来而不是只写进 WebChat。
  • Push-to-Talk(按住右 Option):按住右 Option 键立刻开始录音,松开就停,不用喊唤醒词,松开后会留一小段延迟让你改文字再发。Push-to-talk 一启动会暂停 Voice Wake 的麦克风,松开后自动恢复;如果 Talk Mode 正开着,Push-to-talk 会被自动让位,等 Talk Mode 关掉再回来。

再两个是 Talk Mode 用的辅助开关,仅在用 Talk Mode 时才有意义:

  • 播放阶段切换音效(Play phase-transition sounds):Talk Mode 在 listening / thinking / speaking 之间切换时放一小段系统提示音,方便你在不看屏幕时也能听出现在 agent 处在哪个阶段。
  • 打断 agent 说话(Press Right Option to stop speech):agent 正在说话时,轻点一下右 Option 键就能打断 TTS,立刻回到 listening 状态。

使用 Voice Wake 模式时,唤醒词命中后会等一个约 0.55 秒的停顿(确认这是唤醒词,不是某个长词的一部分),然后在屏幕右上角飘出一个浮在所有窗口最上层的小面板,把你正在说的话实时转成文字显示在里面。

说话有内容时静默窗口是 2 秒,只听到唤醒词没听到后续是 5 秒;最外面还有个 120 秒的硬上限兜底,万一前面的静默检测失灵(比如背景一直有人声、或者识别卡住没吐结果),到点也会强制截断,不让它无限录下去。识别到的文本会被转发给当前 agent,回复送到上次用的那个主 channel(WhatsApp / Telegram / Discord / WebChat);如果投递失败,错误记下来,这一轮运行仍然能在 WebChat / 会话日志里看到。

设置页这几个开关下面还有一组语音相关的细项可以调:

voice-wake-settings.png

  • Recognition language(识别语言):识别用的语言,我这里选的是 “中文”,这样它就能听懂我说的话了。可以点 "Add additional language" 加几个备选,按列表顺序往下试。
  • Microphone(麦克风):选哪个麦克风去监听,默认是 System default,app 会记着你选的那个设备的唯一编号,万一拔了,会贴个 "Disconnected" 的小提示,临时回退到系统默认,等它接回来会自动切回去。
  • Live level(实时电平):实时电平条加 dB 数值,告诉你 app 现在到底听到了多少声音,选完麦克风后看一眼这个,就能立刻知道选对了没有。
  • Test Voice Wake(测试语音唤醒):本地测一下唤醒词识别能不能通,点 Start 后说一个唤醒词,如果能检测到,会显示 Detected。
  • Sounds(提示音):唤醒词命中(Trigger sound)和文本发出去(Send sound)两个时刻分别可以挂一段提示音,默认都是 macOS 的 "Glass" 系统音。
  • Trigger words(唤醒词列表):前面提到的那份 Gateway 全局共享的列表,可以在这里增删,改完会通过 voicewake.set 广播给其他客户端 / node。截图里我自己加了 "老钳"、"老钱" 两个中文唤醒词(有时候识别不准,多加几个),配合上面把识别语言切到 "中文",就能用中文喊它了。唤醒词越短越容易识别出来,但太短也更容易在日常聊天里被误触发,要权衡下。

实战 macOS app

把 macOS app 的几样能力串起来,我让 cc 帮我设计了一个场景:你正在 VS Code 里调一个跑不起来的脚本,懒得复制粘贴报错,直接对着电脑喊一句 "老钳,看看我屏幕上这个报错是怎么回事,运行 npm test 帮我排查下",菜单栏的小龙虾接收消息 → 调 screen.snapshot 拿屏幕截图 → 多模态识别 → 调 system.runnpm test → app 弹 exec approval → 你点 Always Allow → agent 把分析回到菜单栏面板的 WebChat 里。这条链路把 Voice Wake、screen 截图、system.run、Exec approvals 全串起来了,分六步。

sequence.png

第一步:开 Voice Wake,把麦克风 / 语音识别 / 录屏三个权限给上。 Voice Wake 开关在 Settings → Voice Wake,第一次开它会要 Microphone 和 Speech Recognition;screen.snapshot 需要 Screen Recording。到「系统设置 → 隐私与安全」确认这三项都给了 OpenClaw。然后在 Voice Wake 设置页配上你自己的唤醒词。

第二步:给 Exec approvals 加 allowlist。 agent 待会儿要跑 npm test,但 system.run 默认是 deny。到 Settings → Exec approvals,给 main agent 把 npm(或解析后的 /opt/homebrew/bin/npm)加进 allowlist,也可以等弹窗时点 Always Allow,效果一样,都会写进 ~/.openclaw/exec-approvals.json

{
  "version": 1,
  "defaults": { "security": "deny", "ask": "on-miss" },
  "agents": {
    "main": {
      "security": "allowlist",
      "ask": "on-miss",
      "allowlist": [{ "pattern": "/opt/homebrew/bin/npm" }]
    }
  }
}

第三步:喊唤醒词。 对着 Mac 说 "老钳,看下我屏幕上这个报错"。由于 VoiceWakeRuntime 一直挂着,听到 老钳 之后等那 0.55 秒停顿确认,然后开始录音,菜单栏弹出带转写文本的浮框;说完停 2 秒它自动截断,把转写文本发送给 agent。

第四步:agent 看屏幕。 agent 判断要看屏幕,调 screen.snapshot。Local 模式下这一步直接由 macOS app 执行(它有 Screen Recording 权限);Remote 模式下远程 Gateway 先发给本机的 node host 服务,node host 再通过本地 Unix socket 把请求转给 macOS app 去截,截完图回传给 agent。

第五步:agent 跑命令。 agent 看完图,觉得还得看测试结果,调 system.runnpm test。如果第二步没提前 allowlist,macOS app 这时弹出 exec approval 弹窗,列出要跑的命令、cwd、哪个 agent 发起的。你点 Always Allow,命令在 app 的 UI context 里跑(不是在 Gateway 进程里),stdout / stderr / 退出码回传,同时 allowlist 里多了一条 npm

第六步:看结果。 agent 把屏幕报错加测试输出综合分析,回一段话到 main session。点菜单栏的小龙虾,控制面板的 WebChat 里就能看到这段回复,跟你在键盘上敲进去的消息一样。agent 干活的那几秒,菜单栏图标会从小龙虾换成忙碌态的小图示。

这个场景整条链路比较长,中间可能会遇到各种问题,我也没有完全跑通,有兴趣的朋友可以试试看。

小结

这一篇我们深入学习了 macOS app 作为 node 的几样能力:

  1. Canvas:内嵌的 WKWebView 面板,给 agent 当轻量可视化工作区;
  2. 摄像头与录屏:使用 camera.snap / camera.clip 调用你的摄像头拍照或录视频,使用 screen.snapshot / screen.record 对你的电脑屏幕进行截屏或录屏;
  3. system.run 与 Exec approvals:让 agent 直接在你的 Mac 上执行命令,最后通过 Exec approvals 经过人工审批;
  4. Voice Wake 与 Push-to-talk:让 app 监听全局麦克风,把语音变成 agent 的输入;

最后通过一个实战的例子,把上面这些串起来。至此 macOS app 这只横跨 operator 和 node 的小龙虾就搞定了。下一篇我们接着这条线往下走,把视线挪到揣在兜里的那台手机,学习 iOS / Android Node 配对,把手机上的 OpenClaw Node 和家里的 Gateway 配上对,让它当远程麦克风、摄像头和位置传感器,出门在外也能随时唤起 agent。

参考


把小龙虾钉在菜单栏:OpenClaw 的 macOS app

上一篇我们把 Gateway 从 127.0.0.1 搬了出来,让外网的客户端也能访问。当时我们把连进 Gateway 的客户端分成了三类:

  • channel:Telegram、飞书、Slack、WhatsApp 这类聊天工具,你通过它们发文字消息让 agent 干活
  • operator:CLI、macOS app、浏览器 Control UI,你直接坐在控制台跟 agent 对话、改配置、看健康状态
  • node:iOS / Android Node、macOS 节点模式,把摄像头、麦克风、GPS、屏幕、Live Canvas 暴露给 agent 调用

在之前的系列文章里,channel(Telegram、飞书)和 operator(Control UI、openclaw CLI)这两类客户端我们都用得不少,唯独 node 还没怎么介绍过。今天就把这块补上。说到 operator,还有一个客户端前面只是顺嘴提过、一直没真正装来用过 —— macOS 菜单栏 app。它的身份比较特殊:既是 operator,又是 node,刚好横跨两类。所以这一篇先把 macOS app 装来用一遍,体验下它的各项能力;下一篇再讲纯 node 的两个移动端 iOS / Android Node。它们解决的是 channel 解决不了的另一件事:channel 让小龙虾出现在你聊天的地方,原生客户端让小龙虾调用你设备上的能力。

三个原生客户端一览

动手之前先看下这三个客户端的现状:

  • macOS app(macOS 15+,operator + node):GitHub Release 有打包好的 .dmg(外加给 Sparkle 自动更新用的 .zip),也可从源码编译。能力上是菜单栏常驻、拥有所有 TCC 权限、托管本地 Gateway,再加上 Canvas / 摄像头 / 录屏 / system.run / Voice Wake / push-to-talk 等功能
  • iOS Node(iOS 18+,纯 node):还处在内部预览阶段,没上架 App Store,只能走 TestFlight 内测或自己用 Xcode 编译。能提供 Canvas、摄像头、屏幕快照、Location、Talk Mode、Voice Wake 等能力
  • Android Node(Android 12+,纯 node):未公开发布,得用 Android SDK + Java 17 自己编 APK。除了 Canvas、摄像头、Talk Mode,还多一批通知转发、通讯录 / 日历 / 短信 / 通话记录 / 运动传感器命令

可以看到现状是:只有 macOS app 有现成的二进制可下,iOS 还在很早期的 alpha 阶段(仓库里 apps/ios/README.md 第一行就写了 internal-use only),Android 也是只给了源码。所以在 App Store / Google Play 暂时还搜不到,得靠我们自己动手。这一篇专门讲 macOS app,iOS / Android 留到下一篇。

安装 macOS app

macOS app 的官方说法是 menu-bar companion(菜单栏伴侣)。它跟前面一直在用的 openclaw CLI 是配套关系,不是替代关系:CLI 负责跑 Gateway 和敲命令,app 负责那些 CLI 干不了的事,弹 macOS 系统权限弹窗、在菜单栏挂个图标、把这台 Mac 当成一个 node 暴露摄像头和录屏。

第一步:装上 app

GitHub Release 页面 找最新版本,macOS 这块一次会放三个文件:

openclaw-assets.png

  • OpenClaw-<版本>.dmg —— 给人用的标准安装包,一般用户直接下载这个。双击挂载,把 OpenClaw.app 拖进 /Applications,弹出磁盘,启动。dmg 是签名 + 公证过的,Gatekeeper 不会拦。
  • OpenClaw-<版本>.zip —— 同一个 OpenClaw.app,只是换成 zip 打包。它真正的用途是提供给 app 自带的 Sparkle 自动更新:装完之后,以后的小版本 app 会自己提示升级,后台拉的就是这个 zip,不用你每次手动下。当然你想跳过 dmg、直接解压这个 zip 拖进 /Applications 也行,结果一样。
  • OpenClaw-<版本>.dSYM.zip —— 调试符号(debug symbols),给崩溃日志做符号化(symbolication)用的,普通用户用不到,下载列表里直接略过。

注一:Sparkle 是 macOS 上用得最广的开源应用自动更新框架,专门解决一个问题:不走 Mac App Store 分发的 app,怎么让用户自动收到新版本。Transmission、IINA、Bartender 这些老牌 Mac app 用的都是它,算是这个领域 20 多年的事实标准。

注二:为什么同一个版本要发 .dmg.zip 两个包呢?因为 .dmg 带 Finder 拖拽界面,对第一次装的人友好;而 Sparkle 使用 zip/tar.bz2 这种能直接解压替换的格式,.dmg 它反而不好处理,所以自动升级走的是 .zip。至于为什么 OpenClaw 不干脆上 Mac App Store 省掉这一步呢?因为它要弹一堆 TCC 权限、要跑本地 launchd 服务、要 system.run 在你机器上运行命令,过不了 App Store 的沙箱审核,只能自分发;自分发就得自己解决更新,所以 Sparkle 就成了最优选择。

第二步:首次启动向导

第一次启动 app,会弹出一个分页的引导向导。

首先是 欢迎页。 一句话介绍 OpenClaw,下面跟着一个橙色的安全提示,大意是:连进来的 AI agent(比如 Claude)能在你这台 Mac 上跑命令、读写文件、截屏,能干多少取决于你给它多少权限。仅当你了解这些风险再使用。

welcome.png

然后是 选择 Gateway。 让你挑这台 Mac 上的 OpenClaw 连哪个 Gateway:

  • This Mac(在本机起 Gateway,会自动装那个 launchd 服务)
  • Nearby Gateways(发现附近的 Gateway,同局域网或 tailnet 里 Bonjour 扫到的)
  • Configure later(先不起 Gateway)

点 Advanced 还能填远程 Gateway 的 SSH 目标或直连的 wss:// 地址。这一页其实就是 Local 和 Remote 之分。选了 This Mac 之后,本地模式还会接着跑一遍 Gateway 端的配置向导(配模型 API key 那些),跟前面几篇讲过的 openclaw onboard 是一样的,这里不重复。

gateway.png

接着 开启 TCC 权限。 这一页才是重头戏,我们根据需要逐项申请 macOS 的系统权限。大致分为下面这八类,每一类对应 app 的一组能力:

  • 通知(Notifications):菜单栏 app 弹原生系统通知(system.notify
  • 辅助功能(Accessibility):UI 自动化、监听全局快捷键(push-to-talk 的右 Option 快捷键就靠它)
  • 屏幕录制(Screen Recording)screen.snapshot 截屏、screen.record 录屏
  • 摄像头(Camera)camera.snap 拍照、camera.clip 录短视频(注意 macOS 上摄像头默认是关的,见后文)
  • 麦克风(Microphone):Voice Wake / Talk Mode / push-to-talk 录音,以及 camera.clip 带音频
  • 语音识别(Speech Recognition):把麦克风进来的语音在本地转成文字(Voice Wake、Talk)
  • 自动化(Automation):通过 AppleScript 控制别的 app(比如 PeekabooBridge 那套 UI 自动化)
  • 地理位置(Location)location.get 把这台 Mac 的位置交给 agent

TCC 全称 Transparency, Consent and Control(透明、知情同意与控制),是 macOS 的隐私授权机制:摄像头、麦克风、屏幕录制、辅助功能、输入监控、定位、通讯录这些敏感资源都被它挡在一道系统弹窗后面,app 第一次访问时会弹窗问你「允许吗」,确认后的结果记在 TCC.db 文件中,之后能在「系统设置 → 隐私与安全」里逐项开关。

permissions.png

这里有个关键点:这些权限是 macOS app 申请的,不是 Gateway 申请的。Gateway 是个无 UI 的后台进程,申请不了 macOS 的 TCC 弹窗流程,所以但凡涉及麦克风、录屏、辅助功能这些,都必须由有 GUI 的 app 来代申请。你不想用的能力这页可以先空着,后面要用了再回这页补。

最后向导走完,一切准备就绪:

allset.png

此时菜单栏右上角会出现一只小龙虾图标,本地模式的 Gateway 也起来了,点一下图标就能开始跟 agent 聊天:

first-chat.png

第三步:装 CLI(如果还没装)

如果你是从前面几篇一路跟下来的,openclaw CLI 早就装好了,这步可以跳过。如果是直接从 app 入坑的,打开 app → General 设置页 → 点 Install CLI,它会用 npm / pnpm / bun 帮你装一个全局的 openclaw 命令。app 内部的一些后台操作(管理定时任务之类)依赖这个 CLI。

第四步:选 Local 还是 Remote 模式

app 有两种工作模式,对应上一篇讲的两类部署:

  • Local(默认):Gateway 就在这台 Mac 上。app 会自动 attach 到正在跑的本地 Gateway;如果还没起,它会通过 openclaw gateway install 装一个 launchd 用户级服务(label 是 ai.openclaw.gateway,用 --profile 时是 ai.openclaw.<profile>)让它常驻。注意 app 自己不会把 Gateway 当子进程拉起来,它只负责装好 launchd 服务、然后 attach 上去。
  • Remote:Gateway 在别的机器上(比如家里那台 Mac mini)。app 通过 SSH 或 Tailscale 连过去,不会在本机起 Gateway 进程,但会在本机起一个 headless 的 node host 服务(也是 launchd),这样远程的 Gateway 反过来也能调用这台 Mac 的能力。Remote 模式下 app 的 Gateway 发现机制现在优先用 Tailscale 的 MagicDNS 名而不是裸 tailnet IP,所以 tailnet IP 变了也能自愈。

出差带的笔记本就选 Remote,家里那台主力机就选 Local。Remote 模式的 SSH / Tailscale 怎么配,上一篇「Gateway 远程访问」已经讲过,这里也就不再重复了。

macOS app 作为 operator

正如上文所述,macOS app 不仅可以跟 agent 对话,还可以作为 operator 来使用。我们在菜单栏图标上右键,点击 ”Settings“ 进入设置页:

general.png

这里的功能很多之前都学习过,和 CLI 和 Control UI 是等价的,比如渠道管理、配置管理、实例管理、会话管理、定时任务、SKILLS 技能;也能看最近的会话和 token 用量、Gateway 健康状态、已配对的 node 设备等。其中 Voice Wake 和 Permissions 是 macOS app 特有的。Permissions 就是我们上面讲的八大 TCC 权限,以及 Exec 审核策略配置,Voice Wake 我们后面再讲。

这里还有一个小彩蛋:app 注册了 openclaw:// URL scheme。运行 open 'openclaw://agent?message=Hello' 就能从命令行 / 脚本 / 快捷指令里直接调一次 agent 对话。不带 key 参数时 app 会弹确认框防误触,而且会无视 deliver / to / channel 这几个参数;带上配置好的 key 可以静默执行,适合做个人自动化。

macOS app 作为 node

macOS app 启动后会以 role: node 的身份也连进同一个 Gateway,握手时声明自己的 caps(canvas、camera、screen、system ...)和 commands(命令白名单),Gateway 把它登记进运行时的 node 注册表,别的客户端(CLI、Control UI、agent 本身)就能用 node.invoke 把命令转给它跑。常见命令是这几组:

组别命令干什么
Canvascanvas.present / canvas.navigate / canvas.eval / canvas.snapshot / canvas.a2ui.*agent 驱动的可视化画板
Cameracamera.snap / camera.clip拍照、录短视频
Screenscreen.snapshot / screen.record截屏、录屏
Systemsystem.run / system.notify在这台 Mac 上跑命令、发系统通知

未完待续

这一篇我们把 OpenClaw 的 macOS app 安装体验了一把,了解了:

  1. 它是「菜单栏伴侣」:跟 openclaw CLI 是配套不是替代 —— CLI 跑 Gateway、敲命令,app 干 CLI 干不了的事,比如弹 TCC 弹窗、菜单栏挂图标、把这台 Mac 当 node 等
  2. 安装 macOS app:GitHub Release 里的三个文件,分别是给人用的 .dmg、给 Sparkle 自动更新用的 .zip、给开发者排崩溃的 .dSYM.zip
  3. 首次启动向导:欢迎页 → 选 Gateway → 开 TCC 权限 → 完成配置
  4. Local / Remote 两种模式:Local 托管本地 Gateway(自动装 launchd 服务 ai.openclaw.gateway),Remote 连远程 Gateway、不在本机起 Gateway 但起一个 headless 的 node host 服务,让远程 Gateway 反过来也能调用这台 Mac
  5. 作为 operator:菜单栏图标右键 → Settings,渠道 / 配置 / 实例 / 会话 / 定时任务 / SKILLS 这些跟 CLI、Control UI 等价,外加 macOS 特有的 Permissions(八大 TCC 权限 + Exec 审核策略)和 Voice Wake
  6. 作为 node:以 role: node 也连进同一个 Gateway,握手时报上自己的 capscommands,被登记进 node 注册表,别的客户端就能用 node.invoke 把命令转过来

其中,作为 node 刚刚开了个头,Canvas 画板、摄像头、录屏、system.run 和 Exec approvals、Voice Wake 与 push-to-talk,这些内容值得一样样掰开了看,我们下一篇再见。