上一篇我们围绕自定义 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.小龙虾会正常回复：但是在回复之后自动触发 skill workshop 插件，对当前会话进行沉淀。查看提案队列第二步，看队列里进东西没有。在同一会话或新会话里跟小龙虾说一句：> 帮我看下 skill workshop 现在有几个 pending 提案小龙虾会调 skill_workshop 工具，参数是 { "action": "list_pending" }，把待审队列列出来，大致这样：如果查看 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-workflowscreenshot / screen capture / asset / imageoptimscreenshot-asset-workflowqa / scenario / test planqa-scenario-workflowpr / pull request / githubgithub-pr-workflow其它都进兜底名称learned-workflows你这句话里有 animated GIFs，命中第一行，自然就归到 animated-gif-workflow；要是一句话谁都不沾，就统一落到 learned-workflows 这个兜底名称。注意这只是这份 skill 将来 apply 后会用的目录名，现在还只是提议里的一个建议值。另外还有个 source 字段，表示这个提案是从哪冒出来的，它的取值有：tool（你或模型显式调工具生成）、agent_end（自动扫到纠正口吻）、reviewer（回看模型产出），正好对应插件捕获提案的三条路径，后面「三条捕获路径」一节会拆开讲。应用 skill 提案第三步，把它应用：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列出被安全扫描拦下、隔离起来的提案inspect按 id 查看某一条提案的完整详情suggest由模型主动提交一条新提案，pending 策略下默认入队等审批apply把某一条 pending 提案应用掉，真正把 skill 文件写到磁盘上reject拒掉某一条提案，状态改为 rejected 但保留在记录里write_support_file往 skill 目录下的支持目录写一份支持文件（如 references/、scripts/）其中大部分都比较简单，只有 suggest 和 write_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 —— 强制指定写不写盘，绕开当前的 approvalPolicy。apply: 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）。这一节就把这三条路径拆开：1. 工具显式建议：模型看到一段可复用流程、或用户明说「把这个存成 skill」时，直接调 skill_workshop 工具。这是最显式的一条，autoCapture: false 也能用，是关掉自动捕获时唯一的入口。2. 启发式捕获：autoCapture 开 + reviewMode 含 heuristic 时，插件会扫成功那一轮对话里用户消息中的明确纠正短语（也就是上面实战里说的那句话能被捕到的原因）。这套短语列表是写死在 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/gif → animated-gif-workflow，其它一律进 learned-workflows 兜底），这里不再重复。3. 回看模型（LLM reviewer）：reviewMode 含 llm 时，攒够阈值（默认 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。这个系列就到这里。感谢一路读到最后，现在，去给你自己的小龙虾配齐工具箱吧。参考OpenClaw 官方文档OpenClaw GitHub 仓库Skill Workshop 插件文档Skills 系统文档Creating skills 文档Plugins 总览Skill Workshop 插件源码
上一篇我们把 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。它在 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/pptopenclaw 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 或飞书的会话里，直接用大白话跟小龙虾说：这背后就是 clawhub skill 驱动的，agent 实际做的事和你自己敲 clawhub search + clawhub install 完全等价。装完之后当前会话就能直接用，让它做一个 PPT 试试：使用浏览器打开，一个 8 页的关于 OpenClaw 的 PPT 就做好了：关于 skill 安全通过上面的学习我们知道，从 ClawHub 上装一个 skill 又简单又方便，但方便里隐藏着风险。把别人写的 skill 装到本地，本质上就是在跑陌生人写的代码。2026 年以来，ClawHub 上就出过几起规模化投放恶意 skill 的真实事件，因此 skill 安全不能忽略。ClawHub 的发布门槛刻意做得很低。按官方文档的说法，任何人都能上传 skill，唯一的限制是发布者的 GitHub 账号要至少注册满一周，这意味着市场里什么都有。OpenClaw 官方对第三方 skill 的态度也很明确：把它当作未经审查的代码看待，启用前一定要先读一遍。为此，ClawHub 这边主要靠两道防线：自动扫描和人工举报。每个版本的详情页都挂着一行 Security audit 状态，点进去就是完整的审计报告，由两家扫描器并行出结果：SkillSpector：NVIDIA 出的安全检查工具，对 prompt 注入、数据外泄、权限提升、供应链风险、过度代理几类规则挨条做检查，命中哪条就在哪条上标红；VirusTotal：把 skill 的下载包丢进多家杀软引擎跑一遍，将结果汇总，量化判断有没有被投毒；没过扫描的版本会从公开安装面下架，普通用户搜不到也装不了，只有作者自己能在 /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 这份例子里新引入的字段主要是两个：user-invocable：设为 true 时，表示这个 skill 同时变成一个 slash command，用户可以直接 /sysinfo 内核版本是什么 强制调用，不用等模型自己判断；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.json 里 browser.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, ✓ linuxCLI 验证没问题之后，回到聊天窗口试两种触发方式：# 模型自动触发 > 帮我看下这台机器现在还剩多少空间 # 用户显式触发 > /sysinfo 内核版本是什么第一种走 description 匹配，第二种因为 user-invocable: true 直接以 slash command 命中，效果相同：agent 调一次 exec，把 df -h / uname -a 的原始输出附上一两句总结回给你。把它发布到 ClawHubsysinfo 已经在本地跑通了，下面把它发布到 ClawHub 上，让别人也能像我们前面装 ppt 那样一行命令装下来。发布这一步走独立的 clawhub 命令，我们先登录：$ clawhub login $ clawhub whoami ✔ aneasystoneclawhub 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 中查看：如果本地有一堆 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，下一篇我们就来看看它。参考OpenClaw 官方文档OpenClaw GitHub 仓库ClawHub 文档Skills 系统文档Creating skills 文档openclaw skills CLI 文档ClawHub 公共注册中心站点AgentSkills 规范anthropics/skills 官方 skill 仓库
前面几篇我们把 OpenClaw 的工具挨个过了一遍，但工具终究只是零件。想象一下你跟小龙虾说一句「把 openclaw 仓库里带 bug 标签的 issue 都过一遍，能修的就开个 PR 修掉」，它得先用 exec 跑 gh 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 相关了，再去把手册正文读出来。最小可用的 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 里 name 和 description 是必填的，如果缺一个，这份 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>/skills2Project agent skills<workspace>/.agents/skills3Personal agent skills~/.agents/skills4Managed / 本地 skills~/.openclaw/skills5Bundled 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、blockthings-mac在 macOS 的 Things 3 里增删改查 todo、inbox、today、project、area、tagtrello通过 Trello REST API 管理 board、list、cardtaskflow把多步骤分离任务编排成一份持久的 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，跟踪 reviewcoding-agent把编码任务派给 Codex、Claude Code、OpenCode、Pi，跑在后台进程里skill-creator创建、编辑、改进、整理、审核、重构 AgentSkills 和 SKILL.mdclawhub和 ClawHub 注册中心交互，搜索、安装、更新、同步、发布 skillmcporter通过 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 的 UXopenai-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 通过自然语言指令编辑 PDFblogwatcher用 blogwatcher CLI 监控博客和 RSS/Atom feed 的更新搜索、信息与查询：skill用途weather查天气、降水、温度、未来几天预报（出行用）goplaces通过 goplaces 查 Google Places：文本搜索、地点详情、评论、脚本化 JSONgemini用 Gemini CLI 做 one-shot 问答、总结、生成系统、家居与杂项：skill用途1password配置 1Password CLI：登录、桌面端集成、读取/注入 secrethealthcheck审计加固跑 OpenClaw 的主机：SSH、防火墙、更新、暴露面、cron、风险姿态node-connect排查 Android / iOS / macOS Node 的配对、二维码、路由、认证、连接故障tmux远程控制 tmux 会话，靠发按键和抓 pane 输出来驱动交互式 CLIpeekaboo用 Peekaboo CLI 抓屏和自动化 macOS UIcanvas在已连接的 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-automation用 browser 工具控制网页时的操作手册：多步流程、登录检查、tab 管理、stale ref 恢复diffsdiffs用 diffs 工具生成真正可分享的 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，feishu、qqbot、memory-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 命令，自动安装依赖：或者在 Control UI 的 Skills 列表中找到该 skill，点击安装：也可以照着 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/dQw4w9WgXcQagent 看到 URL 加上一个总结意图，于是命中 summarize 的触发条件，它会先使用 read 阅读该技能的 SKILL.md 文件，然后按照说明运行 summarize "<url>" --youtube auto 命令，再把结果整理一下回给我们。我们可以在 Control UI 中看到执行流程：聊天界面大致是这样：Skill 加载原理实战完了之后，我们回过头看看 Skills 的工作原理。从 Gateway 启动到 skill 出现在对话里，整条链路画成图大概是这样：整条链路其实就两件事：先把 skill 集齐（扫描 → 去重 → 过滤 → 注入），再把它装进对话（拼 XML → 进 system prompt → 按需 Read 正文）。我们一步步看：扫描：从前面讲过的 6 个来源各扫一遍，每个目录读 SKILL.md、解析 frontmatter，缺 name 或 description 直接丢弃；合并去重：同名 skill 按优先级表保留高优先级的那一份；gating 过滤：对每份 skill 核对 metadata.openclaw.requires.bins / env / config / os 等条件，不满足就当做不可用，直接踢出列表，这一步保证模型只看到当前真正跑的 skill，不会去尝试一个根本调不起来的命令；per-agent allowlist 过滤：再按 agents.list[*].skills 或 agents.defaults.skills 把当前 agent 不让看的那些过滤掉；env / apiKey 注入：对剩下的 skill，把 skills.entries.<name>.env 和 apiKey 临时塞进 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 里必填 name 和 description、正文是给模型看的操作说明，加载策略也都是渐进式披露，先只让模型看到 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。我们下一篇继续。参考OpenClaw 官方文档OpenClaw GitHub 仓库Skills 系统文档Skills config 配置文档Creating skills 文档openclaw skills CLI 文档ClawHub 公共 skill 注册中心AgentSkills 规范Anthropic：Equipping agents for the real world with Agent SkillsClaude 官方文档：Agent Skills 总览anthropics/skills 官方 skill 仓库summarize.sh CLIobsidian-clichokidar 文件监听库
让小龙虾给 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 只能请求、不能擅自越界。把上面这条交互链画成时序图，大致是这样：如果你了解 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，连上它需要的工具。讲清楚标准协议，再看 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：开始派活之前，还有一点要注意，各个编码 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 的一条直连通道。--bind here 要求所在频道支持「当前对话绑定」能力。飞书、Telegram、Discord、Slack 这类 IM 频道都支持；本地的 webchat / TUI 没有这个抽象，跑这条命令会直接报 Conversation bindings are unavailable for webchat。这种情况要么改到 IM 频道里演示，要么去掉 --bind here、让 agent 自己用 sessions_spawn 把活派到后台跑。接着就可以在这个对话里直接给它派活，跟平时用 Claude Code 没两样：介绍下这个项目可以看到，Claude Code 真的在那台机器上跑起来了：它读取文件，总结代码，过程和你在终端里亲自敲 claude 一模一样，只不过这一切是被小龙虾代理着，发生在聊天频道里。中途想看看它现在是个什么状态，随时一条 /acp status：$ /acp statusstatus 会把这个会话的后端、绑定的 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 方式启动网关后弹出的权限请求：你需要一个个的按 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 范围内不受限制地操作，开之前务必确认目录和权限范围是你能接受的。选项二保守得多，权限照旧不放，只是把 nonInteractivePermissions 从 fail 改成 deny，让那些过不了审批的操作被静默拒掉、会话继续往下走，至少不会因为一次写文件就让整个会话中止。我这里选择选项一，就可以在聊天窗口里愉快的写代码了：要注意的是，关于 acpx 这套 harness 权限，和我们在工具篇讲过的 tools.exec.security 审批配置，是两套完全独立的东西。前者管 ACP harness 在它自己进程里的行为，后者管 OpenClaw 内置 exec 工具的审批，互不影响。另一个差异是：tools.exec.security 已经支持把审批弹窗推到飞书 / Telegram 让你点卡片，ACP 目前还没看到这样的实现。命令速查前面用到的 spawn、status、cancel、close 只是 /acp 的一部分。这里将全部子命令列出来供参考：这里有几点值得展开说说。其一，几乎每条命令后面都能跟一个目标参数，可以是会话 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 close；oneshot 开的是一次性会话，跑完当前轮次会话就自动收尾、元数据清掉，再发消息得重新 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 model 写 model、/acp permissions 写 approval_policy、/acp timeout 写 timeout，剩下叫不上名的就用通用的 /acp set <key> <value> 兜底，想一键还原就 /acp reset-options。两种派活方式前面 /acp spawn 那个例子是人在对话里手动绑定，属于 ACP 的第一种派活方式：交互式绑定会话，它将当前对话钉到一个 ACP 会话上，之后这个对话里的消息直接转给 harness，输出回到同一通道。其实还有第二种派活方式，直接用自然语言的方式，让 agent 用 sessions_spawn 工具把活派给 Claude Code 去跑：我们可以打开 OpenClaw 的 Control UI 页面，找到这次对话：这里可以看到 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 放一起对照一下：参数 agentId 用于指定派给哪个 harness，省略时会用配置里的 acp.defaultAgent（如果设了的话）。常用的几个如下：除这些之外，清单里还有 iflow、kilocode、kiro、pi，以及一个比较特别的 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 会话里用的还是它自己那套原生工具，碰不到小龙虾的 cron、message 这些。如果你确实想把 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-tools 和 openclaw-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 运行时。小结最后，我们来总结下今天学习的内容：ACP 是把活派给外部编码 agent 的标准协议。它由 Zed 团队提出并开源，用来解决编辑器和编码 agent 之间 N×M 对接的麻烦。OpenClaw 在这套协议里扮演 Client 的角色，通过 acpx 后端插件把 Claude Code、Gemini CLI、Codex 这些 harness 当成子进程拉起来，让你在飞书 / Telegram 这种聊天频道里就能调度它们。环境准备并不复杂。把 acpx 装上、启用，把就绪探针指到你常用的 harness，跑一次 /acp doctor 看是否正常即可。harness 自己的厂商登录得在网关那台机器上提前准备好，OpenClaw 只管把进程拉起来，碰不到它们内部。派活有两种姿势。你本人想在某个聊天频道里持续盯着 harness 干活，就 /acp spawn --bind here 把整个对话接到它上面；让另一个 agent 在自己轮次里甩一个后台任务出去，则用 sessions_spawn 把 runtime 换成 acp，按后台 task 跑完播报回来。非交互权限是头号坑。ACP 的反向权限弹窗目前不会被转发到聊天频道，所以远程操作时几乎都得把 acpx 的 permissionMode 调成 approve-all，让 harness 在你给的工作目录里自由读写。两个进阶用法：MCP 桥接能把 OpenClaw 的工具透给 harness 调用；常驻绑定能把一个聊天频道钉到某个 ACP 会话上当工作台用。到这儿，小龙虾的工具体系算是彻底讲完了：内置工具让它能直接动手处理各种操作，浏览器是其中能力最强的一种，ACP 则是把整个任务交给外部 agent 来完成的方式。不过，工具和外部 agent 终究只是一个个单独的能力，agent 接到一个稍复杂的任务，该按什么顺序调用、中间出错怎么回退、什么场景触发哪套流程，光靠模型临场发挥并不稳定，它还需要一份事先写好的操作手册，这就是 OpenClaw 的 Skills 系统。我们下一篇继续~参考OpenClaw 官方文档OpenClaw GitHub 仓库ACP agents 文档ACP agents 配置文档Sub-agents 文档Background Tasks 文档Agent Client Protocol 规范Agent Client Protocol 架构文档Zed 编辑器Language Server Protocol（LSP）Model Context Protocol（MCP）openclaw acp CLI 文档openclaw mcp serve 文档
上一篇我们把 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 的稳定 tabId，label 是我们自己起的 blog 标签，id 则是 CDP 协议的原始 targetId。这几个参数都可以作为后续操作这个标签页时的标识。比如之后想再操作它，用 focus 把它切到前台就行，tabId、label、targetId 这三种都认，下面三条指向的都是同一个页面：$ 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 防护刚才用 open 和 navigate 打开网址，看着什么链接都能打开，其实不然。之前学习工具箱时就提到过，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 截图核对；除此之外还有几个偏辅助的动作。下面一类一类来看。看清页面：snapshotOpenClaw 在看清页面这块做得比较巧妙，它不会把页面的原始 HTML/DOM 一股脑塞给模型。原因很简单，网页源码又长又乱，满屏 套 、一堆样式类名，模型读起来既理不清结构，又白白烧掉一大把 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 里到底是第几个 、外面套了几层 div。这背后是 OpenClaw 的一个刻意设计：点击、输入这类操作只认 ref，不收 CSS selector。原因是 selector 出了名的脆，页面结构稍微一改，像 .btn-primary > span:nth-child(2) 这种立马失效；换成语义稳定的 ref，多步操作才不容易因为页面的一点微调就整条链路崩掉。你可能注意到 browser 工具里有个 selector 参数，但它管的不是「点哪个元素」，而是另外几件事：给快照划定范围（snapshot 时只取某个子树或某个 iframe）、截取单个元素（screenshot --element）、或者等某个元素出现（wait）。知道快照长什么样，命令行里 snapshot 一下就能看到真东西。还是上一节那个博客标签页，输出就是上面那种带 ref 的缩进树：上面这份输出走的是 snapshot 默认的 ai 格式。其实 snapshot 支持两种格式，用 format 参数切换：ai（默认）：给模型优化过的形态，就是上面那种缩进表示层级、每个可交互节点都带 ref 的文本树。简洁好读，模型能直接挑出 ref 来操作，日常驱动页面用它就够了。aria：用 --format aria 切过去，吐出的是更原始的无障碍树节点，结构上更贴近浏览器内部，主要拿来排查页面结构，refs 不一定能直接拿去点。说到底，aria 就是那棵原始的无障碍树，ai 则是 OpenClaw 在它基础上加工出来的好读版本（编上 ref、补上链接和可点击提示）。默认情况下 ai 快照不会主动精简，复杂页面拉出来的树有时相当庞大，所以 snapshot 还留了几个参数专门给它瘦身。最基础的是 maxChars（默认 4 万）这个字符上限兜底，超出就自动截断；如果还是嫌它啰嗦的话，可以加 compact 拉一份精简版，把没名字的纯容器节点、以及底下不含任何可操作元素的空枝条都剪掉，只留有内容的骨架，或者用 interactive=true 只留按钮、链接、输入框这类能交互的节点，把纯展示用的文字一股脑滤掉；页面层级套得太深时，还能用 depth 限制往下钻几层。那 ref 又是怎么映射回真实元素的呢？这取决于 refs 参数选的风格。OpenClaw 支持两种 ref 风格：默认的 role 风格里，e1、e2 这些编号背后其实存着「角色 + 名称 + 第几个」这组信息，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 拆成了一个个独立的子命令，对应关系大致如下：我们挑几个重点的看下。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 / filltype 往输入框里打字，最常见就是 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 直接填值，标成 checkbox 或 radio 就改成勾选（值传 true 勾上、false 取消）。顺带说说 type 和 fill 底下的实现，它们其实默认调的都是 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 一起选上）。传进去的字符串会同时匹配 的 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 反着跳焦点。另外 press 和 click 一样会做导航检测：按 Enter 很可能提交表单、触发跳转，OpenClaw 会盯着这次跳转，照样走一遍前面那套 SSRF 复查。hover把鼠标悬到某个元素上，常用来触发那种鼠标悬停才显示的下拉菜单、提示气泡：$ openclaw browser hover e5 hovered ref e5底层是 Playwright 的 locator.hover，只改 UI 状态、不触发导航。它最常见的搭配是先 hover 再 snapshot：很多菜单是悬停才渲染出来的，先让它冒出来、再拍一张快照，才能拿到菜单项的 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多步操作里最容易翻车的，就是上一步点完、页面还没加载好，就急着点下一步，结果扑空。browser 的 wait 命令就是用来避免这种情况的，它能等到某个具体状态再往下走：等某段文字出现（或用 --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：它只标当前视口里看得见的元素，滚动条以外的会跳过，而且最多标 150 个，免得整张图被标签填满。辅助动作除了上面这些主力动作，browser 还有几个偏辅助的 action，平时用得不算频繁，但偶尔可以用来救急：console 把页面控制台的日志读出来，配上 level 还能按级别（error、warning 等）过滤，调试页面脚本时有用；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 的篇幅。这些就留给你自己去探索了。最后，我们再来总结下今天的学习内容：认准标签页：browser 一次只盯着一个标签页干活，所以动手之前得先认准目标。open 开新页、focus 切到前台、navigate 原地跳转、tabs 列清单、close 收尾，这几条命令都认 tabId、label、targetId 三种标识；就算忘了关，后台也有一套自动清理在兜底。SSRF 防护：不是什么 URL 都能打开。browser 单配了一套默认 fail-closed 的 SSRF 防护，导航前后各查一遍以防重定向绕过，私网、本机环回、云厂商元数据这些地址一律拦在门外；真要放行内网，优先用 allowedHostnames、hostnameAllowlist 开个窄口子，而不是把整片私网敞开。看清页面（snapshot）：它不把原始 DOM 丢给模型，而是走无障碍树，把页面压成一棵带 ref 的语义树。默认的 ai 格式好读、能直接挑出 ref 来操作，aria 格式更原始、适合排查结构；遇上复杂页面，还能用 compact、interactive、depth 几个参数瘦身省 token。操作页面（act）：动手的活儿都收在一个 act 里，靠 kind 分派成 click 点击、type / fill 输入填表、select 选择、wait 等待、evaluate 跑 JS 等一整套动作，底层大多落到 Playwright 的对应方法上。核对与救急：screenshot 截的是真实像素，存图前会自动归一化，把尺寸和体积压进 2000 像素、5MB 以内；另外还有 console、pdf、upload、dialog 几个辅助动作，平时用得不多，调试、存档、传文件、应付弹窗时拿来救急。至此，OpenClaw 工具箱里最复杂的 browser 就算彻底过完了。下一篇我们换个方向，看另一类「派活儿」的本事：通过 ACP，把整个任务直接甩给 Claude Code、Gemini CLI、Codex 这些外部编码 agent 去跑，敬请期待~参考OpenClaw 官方文档OpenClaw GitHub 仓库Browser 工具文档Browser 控制 API 文档Browser Linux 排障Browser WSL2 + Windows 远程 CDP 排障Tools and plugins 总览Chrome DevTools MCPPlaywright
给小龙虾配齐工具箱：OpenClaw 的工具体系（二）
上一篇我们把 OpenClaw 的内置工具分成了八大类，并且把前四类一一看了一遍，这一篇书接上文，继续后面四类的学习。设备控制这一类管的是小龙虾对配对进来的设备 / node 的操控，两个工具配合着用：nodes 负责发现、配对、指定要遥控哪个 node，以及对它干什么（拍照、录屏、取定位、推送通知等），canvas 负责在某个 node 的屏幕上画东西（显示页面、跑 JS 脚本、截屏画面）。我们先来看 nodes 的参数，它和昨天学习的 message 类似，也是个按 action 分派的工具：完整的 action 一共 19 个，每个 action 还各带一批专属的细分参数（定位精度、通知优先级、录屏的 screen 选择等等），一张表列不下，用到时再查文档即可。这 19 个动作按功能分组大致如下：图里的分组命名，正好牵出我们之前曾学习过的一组概念：设备（Device） 和 节点（Node）。一台 Mac、一部手机这样的物理设备，在 OpenClaw 里其实分两层看：Device 是它的「身份证」，管它是谁、准不准进来；Node 是它以 role: node 连进来之后对外贴出的「能力清单」，管它能被调用哪些功能。nodes 这个工具名字虽叫 nodes，实际上横跨了这两层：图里的「配对审批」和「设备信息」管的是 Device（这台设备是谁、批不批），「摄像头 / 屏幕 / 定位 / 通知 / 自定义命令」几组才是真正在调 Node 的能力。说白了它就是一台「设备遥控器」，node 参数选遥控哪一台，action 选按哪个键。接下来，我们再来看下 canvas 工具的核心参数：这个工具其实我们之前在 macOS app 篇里已经见过了，那块吸在菜单栏边、给小龙虾当可视化工作区的面板，就是它的画布；它和 nodes 一样，每个 action 底层都对应一条 canvas.* 节点命令，发给目标 node 上的画布去执行。运行时管理这一类是小龙虾用来管理自身运行时的两个工具：cron 管定时任务（比如每天早上八点提醒我打卡），对应前面 cron / heartbeat 那篇；而 gateway 则能查看和修改自己的运行时配置，甚至重启和自我升级。我们先看 cron，它的参数也是按 action 分派，外加一个嵌套的 job 对象，核心部分如下：job 对象里最关键的是 schedule（什么时候跑）和 payload（跑的时候干什么）：光看参数还是有点抽象，举个具体例子：开头说的「每天早上八点提醒打卡」，用 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: "拉一下我今天的日程，挑出最重要的三件发我" }到了八点，小龙虾就不只是提醒你，而是真的去翻日程、筛选、再把结果发过来。注意这两种类型的字段不一样：systemEvent 用 text，agentTurn 用 message。最后是 delivery，它管干完之后结果往哪儿送，有三档：none 是不往外送，任务在后台默默跑完就算（常配那些不需要回话的任务）；announce 是把结果播报到当前通道，你在 Telegram、飞书里就能直接收到，也是最常用的一档；webhook 则是把结果 POST 到你指定的一个回调地址，方便接进自己的系统或别的自动化流程里。对照着这个例子回头看，就能明白前面 cron / heartbeat 篇里那些定时任务是怎么配出来的了。第二个工具是 gateway，下面是它的参数定义：这个工具比较特殊，它是 仅限 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.ask 和 tools.exec.security 那两个开关。换句话说，小龙虾可以借 gateway 给自己做点微创手术，但没办法通过改配置悄悄突破自己的安全防线，这道兜底写死在代码里，谁也绕不过去。媒体生成这一类让小龙虾造出图片、视频、音乐和语音，一共五个工具：image 是看图（分析理解一张已有的图），image_generate 生成或修改图，video_generate 生成视频，music_generate 生成音乐，tts 把文字读成语音。这一组工具参数都不少，但骨架高度一致，我们不妨对照着看。先看「看图」和「文字转语音」这两个最简单的：image 是这五个工具里唯一读而不写的，其余四个都在产出新东西，它却反过来，输入一张已有的图，让小龙虾看懂里面有什么。参数 prompt 表示你想知道什么（比如：这张图里有几个人，把发票上的金额读出来），参数 image 或 images 表示图的本地路径或 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（生成或编辑图片），关键在尺寸、张数和图片专属的质量、格式、背景等参数：video_generate（生成视频）的重头是参考素材和视频特有的时长、宽高比等参数，它支持文生视频、图生视频、视频生视频三种玩法：music_generate（生成音乐）则换成歌词、纯伴奏这类音乐专属的参数：这几个工具在使用之前，都得先配好至少一个对应的 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 参数写死的最优先，且不再往下兜底；其次是配置里的 primary → fallbacks；如果两样都没指定，还会走一遍 自动探测，把配了 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 一个，没有 audio、video 呢？这是因为 tools.media 配的其实是「媒体理解管线」，而不是工具。用户发进来的附件，会在 agent 开始干活之前先被它自动消化一道：图片转成描述、音频转写成文字、视频生成摘要，三种能力都覆盖，模型直接读到消化好的文字即可，不用主动调工具。而模型能主动调的「读媒体」工具，目前只给了 image 一个，我猜是因为主动看图这种需求最独立、最常见（比如分析一个刚抓到的图片 URL、或带个具体问题把某张图重看一遍）；音频、视频的理解基本都发生在「用户发来语音或视频」这条入站通道上，直接处理即可，也就不必再单列成可调用的工具了，当然也不排除后面 OpenClaw 将这些能力也变成内置工具。provider 和配置说完了，最后再补两条容易被忽略的注意事项：这些工具的调用会自动区分同步还是异步，这关系到结果什么时候回来。同步的（图片生成、TTS）当场就出，跟着这一轮回复一起给你；异步的（视频和音乐）因为生成慢，会先告诉你「在做了」，等后台跑完再把成品单独推回来；这些工具只有在你至少配了一个对应的 provider 时，才会出现在小龙虾能看到的工具清单里。如果没配图片 provider，模型压根看不到 image_generate 这个工具的存在，自然也就不会、也没法去调它。所以如果你发现「让它画图它说不会」，那么多半是 provider 没配。子 agent 编排这一类就是小龙虾把任务拆分后交给「分身」并行干，我们在前面「多 agent 与子 agent」那篇已经学习过。它包含一组 sessions_* 开头的工具（派生、列会话、读历史、发消息、收结果）、管理已派出去分身的 subagents、以及列出可用 agent 的 agents_list。其中派活儿的主力是 sessions_spawn，先看它的核心参数：配套的 subagents 工具用来管理已派出去的子 agent：剩下几个都比较简单：sessions_list：列出自己派生的那些子会话，默认谁派生的归谁看，也可按 agentId、标签（label）、关键词（search）筛选，还能用 limit、activeMinutes 限定条数和最近多久活跃过。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.allow 和 tools.deny，前面几篇配 agent 时已经反复用过：allow 是白名单、deny 是黑名单，黑名单永远压过白名单，同一个工具两边都写了，最终结果就是禁用：{ tools: { allow: ["group:fs", "browser", "web_search"], deny: ["exec"], }, }正如之前学过的，allow / deny 里不必一个个工具地列，可以用 group:* 简写批量指定。常用的组有：组包含的工具group:runtimeexec, process, code_executiongroup:fsread, write, edit, apply_patchgroup:webweb_search, x_search, web_fetchgroup:uibrowser, canvasgroup:sessionssessions_list, sessions_history, sessions_send, sessions_spawn, sessions_yield, subagents, session_statusgroup:memorymemory_search, memory_getgroup:automationcron, gatewaygroup:messagingmessagegroup:nodesnodesgroup:agentsagents_listgroup:mediaimage, image_generate, music_generate, video_generate, ttsgroup:openclaw所有内置工具（不含插件工具）另外，光用 allow 从零开始一个个往上加，配起来还是有点累。所以 OpenClaw 还有一个 tools.profile 参数：它先按用途给你预设一套工具集合当起点，加工具用 tools.alsoAllow，砍工具用 tools.deny。内置的 profile 有四档：full：所有核心和插件工具，最宽的基线。coding：文件、运行时、web、会话、记忆等组，外加 cron 和 image/image_generate/music_generate/video_generate（注意：不含 tts，也不含 browser）。messaging：消息组加几个会话工具，通道 agent 专用。minimal：只有 session_status。这四档是写死的，profile 是个固定枚举，暂时没法自己新增一个 profile，也改不了某档里到底含哪些工具。想要一套专属工具集就挑个最接近的 profile 当基线，再用 alsoAllow / deny 做加减。小结上一篇我们学习了前四类工具，命令执行、网络访问、文件读写、消息收发。这一篇接着把剩下的四类工具和工具治理讲完了，简单回顾一下：设备控制：nodes 是台「设备遥控器」，一个工具横跨 Device（身份、准入）和 Node（能力清单）两层；canvas 配合它在某台设备的屏幕上「画东西」，两者底层都转成 camera.snap、location.get、canvas.snapshot 这类节点命令派给目标 node 执行。运行时管理：cron 用 schedule（何时跑）+ payload（干什么）+ delivery（结果送哪）三块拼出定时任务；gateway 仅 owner 可用，能查看 / 修改 / 重启 / 自升级自己的运行时配置。媒体生成：image 看图、image_generate 生成或修改图、video_generate 生成视频、music_generate 生成音乐、tts 把文字读成语音；用之前记得先配 provider，没配 provider 工具压根不出现，慢活儿（视频、音乐）还会丢后台异步跑。子 agent 编排：sessions_spawn 把任务拆给隔离的分身并行干，完成后推送式把结果送回。工具治理：用 tools.allow / tools.deny 圈定某个 agent 能用哪些工具，工具名还能用 group:* 简写批量指定；嫌从零配麻烦，就用 tools.profile 选一套预设基线，再拿 alsoAllow 往上加、deny 往下减。现在小龙虾手里有哪些工具，哪个工具能用，哪个工具不能用，我们都清楚了。其中有两个工具细节多，坑也深，因此后面准备花两篇分别学习下：下一篇专讲 browser 浏览器工具，再下一篇讲 ACP 工具。敬请期待~参考OpenClaw 官方文档OpenClaw GitHub 仓库Tools and plugins 总览Exec 工具文档Web search 文档Web Fetch 文档Browser 工具文档Code Execution 文档Apply Patch 文档Exec Approvals 文档Agent Send 文档Media 总览Sub-agents 文档Skills 系统文档
在上一篇的最后，我们举了一个场景，对着手机喊一句让小龙虾查附近的咖啡馆，它先调 location.get 拿坐标、再用 web 搜索查咖啡馆、最后把结果合成语音念回来。这一连串动作里，location.get、web 搜索、语音合成，其实都是 OpenClaw 里的工具。除此之外，小龙虾的工具箱里还有很多内置工具，我们今天就来系统的学习一遍。工具 vs. 技能 vs. 插件在正式学习之前，我们先看下 OpenClaw 里三个容易混淆的概念：工具（Tool）是 agent 实际调用的东西。一个工具就是一个带类型签名的函数，比如 exec、browser、web_search、message。OpenClaw 自带一批内置工具，还能通过插件注册新工具。对模型来说，工具是以结构化函数定义的形式发给模型 API 的。技能（Skill）教 agent 什么时候、怎么用工具。它是一份注入到 system prompt 的 SKILL.md，给 agent 提供上下文、约束和分步指引。插件（Plugin）把这些打包到一起。一个插件可以同时注册通道、模型 provider、工具、技能等任意组合。三者的关系大致是这样：简单说，工具是手脚，技能是手册，插件是打包。今天这篇只讲最底下那层手脚，手册和打包留到后面再讲。内置工具清单按官方文档，下面这个表格列出了 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列出可用 agentsession_status轻量的 /status 式回读，可临时切换会话模型这张表看着很多，其实可以分为八类：命令执行、网络访问、文件读写、消息收发、设备控制、运行时管理、媒体生成、子 agent 编排。下面我们一类一类来看，重点挑前面几篇文章里反复出现过的工具展开讲。每个工具我都先附一张参数表，直接从 OpenClaw 源码里的 schema 定义整理出来的，列清楚它收哪些参数、每个参数什么意思、哪些必填。看懂一个工具的参数表，基本就摸清了它能干什么、边界在哪。你会发现，决定一个工具好不好用、安不安全的，往往不是它能干什么，而是那些藏在参数里的细节。第一次接触的话，不用每个参数都背下来，先扫一眼有个印象，建立「哪类活找哪个工具」的直觉就够了。命令执行这一类管的是「让小龙虾运行命令或代码」，一共三个工具：exec 在你的机器上跑 shell 命令，几乎是整个工具箱里用得最多的一个：装依赖、跑测试、提交代码，几乎都靠它；process 和 exec 配对，专管那些转入后台还没退出的命令；code_execution 则只需你说清「要算什么」，由 xAI 的 Grok 自己写 Python 并在远程沙箱里跑，全程不碰你的机器。下面一个个看它们的参数。先看 exec 的参数定义：一眼看过去，必填的只有 command 一个，丢一条命令进去就能跑。不过剩下那一长串可选参数，才是这个工具真正的门道所在：跑在前台还是后台？ 默认前台跑，命令执行完结果就回来了。但有些命令（比如启动一个开发服务器）会一直挂着不退出，总不能让小龙虾干等着。所以 OpenClaw 设了个超时：默认跑超过 10 秒（参数 yieldMs）就自动转后台，先把控制权交还给对话，命令继续在后台跑。你也可以一上来就 background: true 直接丢后台。命令转后台之后，再想看它输出了什么、给它敲个回车或粘点东西进去，就得换用配套的 process 工具，它的 action 有 poll（看最新输出）、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（有沙箱就进沙箱，没有就落到网关）。要不要先问过你？ 这由 security 和 ask 两个参数控制。security 管「允许跑哪些命令」：deny（基本不让跑）、allowlist（只放行白名单里的）、full（不限制）。ask 管「跑之前要不要确认」：off（不确认）、on-miss（不在白名单里才确认）、always（每次都确认）。沙箱里跑的代码本就被当作不可信，所以在隔离之外再叠一层最严的 deny；而网关和 node 是机主自己的机器，OpenClaw 默认假设你想在自己机器上丝滑运行，于是给了 full + ask=off，也就是所谓的 YOLO 模式，它是明知有风险仍为了顺手而放开，所以放到真实机器上跑时尤其要留神这组配置。配套的 process 工具参数定义如下：这里比较有意思的是，send-keys 动作有 keys / literal / hex 三个参数，OpenClaw 将「往终端里送一段输入」抽象为三个层次，由高到低分别是：keys（按键 token）：用一个符号名字代表一次按键，而不是字面文本。像「回车」「Ctrl-C」「方向键上」这些按键根本不是可打印字符，回车是字节 \r、Ctrl-C 是字节 0x03、方向上键是转义序列 ESC [ A，没法直接当文本传给终端，所以 keys 接受 token，由 OpenClaw 翻译成终端真正认的字节。支持的写法大致三类：具名键（enter、tab、escape、up、backspace、f1 等）、Ctrl 组合（^c 即 Ctrl-C）、修饰键前缀（c- Ctrl / m- Alt / s- Shift，如 c-c、m-x、s-tab）。literal（原样字符串）：直接发、不翻译，你写什么就送什么字符。适合往程序里「打字」，比如填一句 hello world、回答一个文件名。注意它不会自动帮你按回车，要回车得再配 keys: ["enter"]。hex（十六进制字节）：最底层，自己一个字节一个字节地拼，比如 ["1b","5b","41"] 就是 ESC [ A（方向上键）。token 表达不了的冷门控制序列才用得到，平时基本碰不上。三者可以在同一次 send-keys 调用里一起传，OpenClaw 按 literal → hex → keys 的固定顺序拼成最终要写入的字节流。常见组合就是「先 literal 打一段文本，再 keys: ["enter"] 敲回车」。这个工具和 exec 是天生一对：exec 起一个会话，process 用 sessionId 认准那个还开着的后台会话，再用 action 去操作它。第三个工具是 code_execution，它的名字和 exec 长得很像，但是它干的却是完全不同的活。它的参数定义简单到只有一个：exec 跑的是你机器上的 shell，要你自己给出确切命令；code_execution 则不用你写代码，你只给一句话描述「要算什么」，由 xAI 的 Grok 自己写出 Python 并在它的远程沙箱里跑，再把结果回给你，全程不碰你的电脑、文件和代码仓库。使用这个工具的典型场景包括，精确数值计算、复杂数据分析、金融建模、科学计算、生成并实测一段代码等。值得注意的是，这里的沙箱并不是 OpenClaw 自己的沙箱，而是直接套用了 xAI 官方的 Code Execution 工具：按官方的说法，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 能传的参数也比想象中多，比如 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_KEY（sk-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。三种配置方式：命令行向导（最省事）：跑 openclaw configure --section web，它会引导你选 provider 并存好凭证。环境变量：直接设上面列的那个变量（如 export BRAVE_API_KEY=...），API 类 provider 就能用了，连配置文件都不用动。配置文件（最可控）：在 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 专门搜 X（推特）上的帖子，它和前面的 code_execution 类似：同样由 xai 插件注册，同样通过 xAI 的 Responses API（https://api.x.ai/v1/responses）调用，由 Grok 在 xAI 那边执行。区别只在带的服务端工具不同，code_execution 带的是 code_interpreter，x_search 带的是 xAI 官方的 X Search 工具，默认模型 grok-4-1-fast-non-reasoning。x_search 返回的是一段带引用出处的合成答案（不是一堆原始链接）。如果你想要某条帖子精确的转发数、点赞数这类细粒度数据，最好分两步走，先用关键词搜，搜到那条帖子，拿到它的链接，再用这条确切的帖子 URL 跑第二次 x_search。因为一次宽泛的关键词搜索通常能帮你「找到」帖子，却给不全单条帖子的精确数据。接下来是 web_fetch 的参数定义：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 多一个 content，是「整篇覆盖」；edit 的 edits 是一组 {oldText → newText}，并要求 oldText 在文件里唯一，并且各处替换互不重叠，所以它比 write 安全，因此日常编辑文件或修改代码，几乎清一色用 edit，write 一般只在新建文件时才上场。另外 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 机器人能干的事都归它：这里只挑最常用的几个参数看下：还剩下几十个参数都是某个具体 action 才用得到的（比如建话题、改群名、踢人），运行时还会按当前通道的能力过滤。我们暂时只要知道 message 是个按 action 分派的大工具就够了，具体动作用到时再查。未完待续这一篇我们先把 OpenClaw 工具体系的地基和前半批工具过了一遍：三层关系：工具是 agent 实际调用的带类型函数，技能是教它何时怎么用工具的手册，插件是把这些打包到一起的箱子内置工具分八类：命令执行、网络访问、文件读写、消息收发、设备控制、运行时管理、媒体生成、子 agent 编排，这一篇讲完了前四类命令执行：exec 靠 host（在哪跑）+ security（让不让跑）+ ask（要不要问你）三个旋钮划定边界，配套的 process 用来操作转后台的会话，code_execution 则是套了 xAI 官方的 Code Execution 工具、让 Grok 远程编写并运行 Python 脚本网络访问：轻量三件套 web_search（认 provider，要先配）、x_search（走 xAI）、web_fetch（自带 SSRF 防护），重型的 browser 留到后面单独讲文件读写：read / write / edit / apply_patch 能力递进，日常改代码最常用 edit，apply_patch 用的是 OpenAI 那套补丁格式、默认只对 OpenAI / Codex 模型开启消息收发：message 一个工具按 action 分派，五十多个动作几乎涵盖一个 IM 机器人能干的所有事剩下的设备控制（nodes / canvas）、运行时管理（cron / gateway）、媒体生成和子 agent 编排这四类工具，内容还不少，我们下一篇继续学习。
在上一篇里，我们装好并体验了 macOS app，它横跨 operator 和 node 两类身份，既能当控制台，又能把这台 Mac 的屏幕、摄像头、命令行暴露给 agent。今天我们把目光转到手机上：iOS Node 和 Android Node，两个纯 node 客户端。它们身上没有 operator 那半边，唯一的任务就是当小龙虾的远程感官，把手机的麦克风、摄像头、GPS、屏幕暴露给家里的 agent 调用。配合上一篇讲的 Gateway 远程访问，出门在外也能随时唤起家里的小龙虾。配对 iOS Node在 Gateway 协议里 iOS Node 的 role 是 node，走的是和 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.snap、canvas.snapshot、location.get）。Gateway 把它记进一张运行时的 node 名册，别的客户端就能用 node.invoke 把命令派给它执行。拿到 app值得注意的是，iOS app 暂时还没上架 App Store，官方给它贴的标签是「极早期 alpha」，因此把它当成尝鲜，别当成生产工具。能拿到 app 的途径只有两个：TestFlight 内测：如果你在内测名单里自己编译：根据官方文档，从源码用 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 里打开后，设置如下：Scheme 选 OpenClawDestination 选你那台用数据线连上来的真机。文档明确建议用真机而不是模拟器，因为摄像头、定位、麦克风这些 node 能力在模拟器上要么没有要么是假的，跑真机才看得到真实行为Build Configuration 用 Debug最后点击顶部菜单 Product → Run，编译并运行，标题栏显示 "Build Succeeded" 说明编译通过：在编译的过程中遇到了几个问题，将报错信息丢给小龙虾，很快自己修复了。主要是 SettingsTab.swift 的 body 视图嵌套过深导致 Swift 编译器类型检查超时，把它拆成多个子视图后才编译通过，另外还补了 GatewayOnboardingView.swift 缺失的 import OpenClawKit、给 DeepLinks.swift 静态方法调用加上 self. 前缀。Run 成功后 Xcode 会把 app 直接装到那台连着的手机上，几秒之后手机桌面上就出现了那只红色的小龙虾：另外个人开发者签出来的 app 证书有效期一般只有 7 天，过期了 app 会打不开，重新 Run 一次刷新即可。配对流程第一次点开这个图标，会先跳一个 "Welcome to OpenClaw" 引导页：页面把流程拆成了三步：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 配对页：页面给了两个入口：Scan QR Code（扫二维码）和 Set Up Manually（手动）。两条路最终都是把 Gateway 的 WebSocket 地址加一段一次性的 bootstrap token 喂给 app，区别只在于这段东西是用相机拍进来还是手动粘进来。方式一：setup code（手动配）在 WebChat 里给 agent 发一条 /pair，Gateway 会生成一个 setup code 并给出操作步骤：回复里最关键的是 Setup code（eyJ1cm...），它是一段 base64 编码的 JSON，形如 {"url": "wss://<gateway-host>", "bootstrapToken": "<single-use-token>"}。它不是设备私钥，而是一张一次性入场券，app 拿它换一个正式的 device token，正式 token 才会落进 iOS Keychain。Security 和 Expires 两行合起来看，这张券只能用一次、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：将 setup code 粘贴到输入框中，如果 Tailscale 网络正常的话，几秒之后这一页就会变成这样：可以看到状态显示已连接。方式二：扫二维码这种方式的原理和第一种几乎一样，但要更简单。在 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 能力的总开关都在这里：Voice Wake：唤醒词模式，持续监听麦克风，当听到唤醒词后开始录音，并将识别到的文本作为一条普通消息发给 agentTalk 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..."：下面的 Live agents 列表（main / personal / work），是我们之前创建的多个智能体，点击任意一个将它设置为这台手机当前的 Active agent，下次连同一个 Gateway 时还能记得用的是哪个 agent。iOS app 上也内置了一个轻量 Chat 入口，发条消息测试一下：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.latest、contacts.*、calendar.*、callLog.search、sms.*、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.json 的 talk 段，最省事的是先用 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 里确认有你想喊的词（建议别用 hey、ok 这种太常见的，误触发率高），iOS 端再把本地的 Voice Wake 开关打开。第五步：出门，让手机走 Tailscale 连回家里的 Gateway。 上一篇配的 Tailscale Serve 这时候就派上用场了，家里的 Gateway 通过 https://<magicdns> / wss://<magicdns> 对 tailnet 内可见，手机装了 Tailscale 客户端、登录同一个账号之后，无论走 4G 还是外面的 WiFi，都能反向连回家。这时候对手机喊 "老钳，看看我现在在哪，帮我查附近最近的咖啡馆"：iOS Node 的 VoiceWakeManager 在本地命中唤醒词，开始录音检测到一段静默后截断，把转写文本通过 WebSocket 推给 GatewayGateway 把它送进 agent 一轮 turnagent 判断需要位置，调 location.get 拿到经纬度agent 用 web 搜索工具查这个坐标附近的咖啡馆agent 把结果整理成一句话，通过 talk.speak 经配置的 Talk provider 合成语音音频流回 iOS Node 播放，你在耳机里听到答案整条链路的时序大致是这样：小结这一篇我们把 OpenClaw 在手机上的两个客户端（iOS 和 Android）走了一遍，最后来总结一下：Device 和 Node 的区别： Device 是设备的身份证，管的是这台设备是谁、准不准进来；Node 是它进来之后贴出的一张能力清单，管的是它能被调用哪些功能。iOS Node 的配对和使用： 目前还很早起，没上架 App Store，得走 TestFlight 内测或自己用 Xcode 编译。一共三种配对方式，配上之后，它能为家里的 agent 提供这些远程感官：拍照、录视频、截屏、获取 GPS 位置、连续语音对话、唤醒词唤起等。Android Node 和 iOS 大体一致： 但是它默认不开唤醒词，主要靠手动的录音/对话模式；为了保持连接，它会在状态栏常驻一条通知。作为补偿，它比 iOS 多出一批读手机数据的能力（通知、通讯录、日历、短信、通话记录、运动步数等）。它同样没有公开发布，需要自己编译。目前为止，我们忙活的全是怎么给小龙虾接上手脚、眼睛、耳朵和 GPS，却从没系统看过它接到任务之后到底能调哪些工具，这些工具又是怎么串起来的。回头看看这两篇的实战例子，上一篇让小龙虾帮忙排查屏幕上的报错，这一篇让它查找附近的咖啡馆，我们会发现这两个任务其实都不简单。就拿查咖啡馆来说，小龙虾先调 location.get 拿坐标、再用 web 搜索查、最后把结果合成语音念出来。这一连串的动作其实涉及两个问题：OpenClaw 有哪些工具可用，OpenClaw 拿到任务后使用什么工具，按什么顺序来调用工具。下一篇我们就先把 OpenClaw 的工具箱挨个翻一遍，之后再讲怎么给它配操作手册。参考OpenClaw 官方文档OpenClaw GitHub 仓库iOS app 文档Android app 文档Nodes 索引Devices CLI 文档Location command 文档Camera capture 文档Voice wake 文档Talk mode 文档Pairing 文档Bonjour / Discovery 文档Remote access 文档
上一篇我们把 OpenClaw 的 macOS app 装上了，弄清了它「菜单栏伴侣」的定位，看了它作为 operator 那半边的功能，结尾停在了它的另一重身份上：它启动后还会以 role: node 连进同一个 Gateway，把自己的一组 caps 和 commands 报上去、登记进 node 注册表，别的客户端就能用 node.invoke 把命令转过来。这一篇就接着上一篇的结尾，详细学习下 macOS app 作为 node 的几个能力 —— Canvas 画板、摄像头与录屏、system.run 和 Exec approvals、Voice Wake 与 Push-to-talk，最后再把它们串起来，跑一个实战的例子。CanvasCanvas 是 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.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.* 命令和本地脚手架页照常用）。这里顺便介绍下 A2UI。A2UI（Agent-to-UI） 是一套声明式的 UI 协议，agent 不用手写 HTML，而是发一串 JSONL 消息来描述「界面长什么样」：用 Column、Text 这类组件搭出一棵界面树，配上 usageHint（标题 / 正文之类）这种语义提示，由客户端的 A2UI 渲染器统一把它画成真正的控件；用户在界面上的点击、输入再作为事件反向回传给 agent，是双向的。OpenClaw 把这个渲染器（A2UI host）做成了 Canvas 面板里的内嵌页，上面的 canvas.present 默认跳过去的就是它。目前 Canvas 只认 A2UI v0.8 的 server→client 消息（beginRendering / surfaceUpdate / dataModelUpdate / deleteSurface），v0.9 的 createSurface 还不支持。除了 canvas.present 命令，也可以用 canvas.navigate 打开一个本地 canvas 路径，或者 http(s) 或 file:// URL，或者直接传 "/" 返回本地脚手架页：$ openclaw nodes canvas navigate "/" --node <node-id>显示如下：如果想看到自定义内容，可以用 canvas.a2ui push 推一段 A2UI 协议：$ openclaw nodes canvas a2ui push --text "hi from canvas" --node <node-id>面板中就会显示出这段文本：还可以用 canvas.eval 直接在 WebView 里跑 JS：$ openclaw nodes canvas eval --node <node-id> --js "document.body.innerHTML=' hi '"然后用 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 往面板里塞渲染好的 HTMLagent 渲染完想确认效果，会自己 snapshot 把面板截成图喂回自己，再决定要不要改调哪个 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.snap、camera.clip、screen.record 这几个命令的隐私分量很重，需要在 gateway.nodes.allowCommands 里显式开启。system.run 与 Exec approvalssystem.run 是 macOS app 最有分量的一项 node 能力，它可以让 agent 在你这台 Mac 上运行命令。它跟 agent 自带的 exec 工具是两个很容易混淆的概念，其实，它们两不在一个层面上：exec 是模型在对话里直接调的工具，跟 read_file 平级，它内部有三种后端 —— sandbox（丢进沙箱隔离环境跑）、gateway（在 Gateway 进程里直接 spawn）、node（转交给一台远端 node）；system.run 则是 macOS app 作为 node 暴露出来的一项能力，跟 screen.snapshot、canvas.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）：这些配置全部存储在这台 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，那条命令就被加进 allowlistallowlist 是按 agent 分的：多个 agent 的话，在 app 里切换你正在编辑哪个 agent，一个 agent 的批准不会泄漏到别的 agent如果 macOS app 没在跑（比如你只起了 CLI 和 Gateway），任何本来该弹窗的请求会走 ask fallback，默认就是 denyVoice Wake 与 Push-to-talkmacOS app 还能监听全局麦克风，把语音变成 agent 的输入。不过有一个前提条件，该功能依赖 macOS 26 才引入的新接口，macOS 26 以下整块 Voice Wake 配置都是灰色的，用不了：满足版本之后，设置页里有三个触发模式和两个 Talk Mode 行为开关：先说三个触发模式：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 / 会话日志里看到。设置页这几个开关下面还有一组语音相关的细项可以调：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.run 跑 npm test → app 弹 exec approval → 你点 Always Allow → agent 把分析回到菜单栏面板的 WebChat 里。这条链路把 Voice Wake、screen 截图、system.run、Exec approvals 全串起来了，分六步。第一步：开 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.run 跑 npm 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 的几样能力：Canvas：内嵌的 WKWebView 面板，给 agent 当轻量可视化工作区；摄像头与录屏：使用 camera.snap / camera.clip 调用你的摄像头拍照或录视频，使用 screen.snapshot / screen.record 对你的电脑屏幕进行截屏或录屏；system.run 与 Exec approvals：让 agent 直接在你的 Mac 上执行命令，最后通过 Exec approvals 经过人工审批；Voice Wake 与 Push-to-talk：让 app 监听全局麦克风，把语音变成 agent 的输入；最后通过一个实战的例子，把上面这些串起来。至此 macOS app 这只横跨 operator 和 node 的小龙虾就搞定了。下一篇我们接着这条线往下走，把视线挪到揣在兜里的那台手机，学习 iOS / Android Node 配对，把手机上的 OpenClaw Node 和家里的 Gateway 配上对，让它当远程麦克风、摄像头和位置传感器，出门在外也能随时唤起 agent。参考OpenClaw 官方文档OpenClaw GitHub 仓库OpenClaw GitHub Release 页面macOS app 文档macOS dev setupmacOS 权限文档macOS Canvas 文档Voice wake（macOS）文档Voice wake（全局唤醒词）文档Exec approvals 文档Camera capture 文档Nodes 索引Remote access 文档Sparkle 自动更新框架
上一篇我们把 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 appmacOS app 的官方说法是 menu-bar companion（菜单栏伴侣）。它跟前面一直在用的 openclaw CLI 是配套关系，不是替代关系：CLI 负责跑 Gateway 和敲命令，app 负责那些 CLI 干不了的事，弹 macOS 系统权限弹窗、在菜单栏挂个图标、把这台 Mac 当成一个 node 暴露摄像头和录屏。第一步：装上 app去 GitHub Release 页面 找最新版本，macOS 这块一次会放三个文件：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 上跑命令、读写文件、截屏，能干多少取决于你给它多少权限。仅当你了解这些风险再使用。然后是 选择 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 是一样的，这里不重复。接着 开启 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 的位置交给 agentTCC 全称 Transparency, Consent and Control（透明、知情同意与控制），是 macOS 的隐私授权机制：摄像头、麦克风、屏幕录制、辅助功能、输入监控、定位、通讯录这些敏感资源都被它挡在一道系统弹窗后面，app 第一次访问时会弹窗问你「允许吗」，确认后的结果记在 TCC.db 文件中，之后能在「系统设置 → 隐私与安全」里逐项开关。这里有个关键点：这些权限是 macOS app 申请的，不是 Gateway 申请的。Gateway 是个无 UI 的后台进程，申请不了 macOS 的 TCC 弹窗流程，所以但凡涉及麦克风、录屏、辅助功能这些，都必须由有 GUI 的 app 来代申请。你不想用的能力这页可以先空着，后面要用了再回这页补。最后向导走完，一切准备就绪：此时菜单栏右上角会出现一只小龙虾图标，本地模式的 Gateway 也起来了，点一下图标就能开始跟 agent 聊天：第三步：装 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“ 进入设置页：这里的功能很多之前都学习过，和 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 作为 nodemacOS 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 安装体验了一把，了解了：它是「菜单栏伴侣」：跟 openclaw CLI 是配套不是替代 —— CLI 跑 Gateway、敲命令，app 干 CLI 干不了的事，比如弹 TCC 弹窗、菜单栏挂图标、把这台 Mac 当 node 等安装 macOS app：GitHub Release 里的三个文件，分别是给人用的 .dmg、给 Sparkle 自动更新用的 .zip、给开发者排崩溃的 .dSYM.zip首次启动向导：欢迎页 → 选 Gateway → 开 TCC 权限 → 完成配置Local / Remote 两种模式：Local 托管本地 Gateway（自动装 launchd 服务 ai.openclaw.gateway），Remote 连远程 Gateway、不在本机起 Gateway 但起一个 headless 的 node host 服务，让远程 Gateway 反过来也能调用这台 Mac作为 operator：菜单栏图标右键 → Settings，渠道 / 配置 / 实例 / 会话 / 定时任务 / SKILLS 这些跟 CLI、Control UI 等价，外加 macOS 特有的 Permissions（八大 TCC 权限 + Exec 审核策略）和 Voice Wake作为 node：以 role: node 也连进同一个 Gateway，握手时报上自己的 caps 和 commands，被登记进 node 注册表，别的客户端就能用 node.invoke 把命令转过来其中，作为 node 刚刚开了个头，Canvas 画板、摄像头、录屏、system.run 和 Exec approvals、Voice Wake 与 push-to-talk，这些内容值得一样样掰开了看，我们下一篇再见。