拿到一套OpenClaw环境跑通基础对话之后,大多数人的下一个念头都是同一个:能不能让它干点默认技能列表里没有的事。比如“十五分钟后提醒我去关洗衣机”“明天早上七点播报通勤路况”“家里来客人时自动把客厅灯调成暖光”。这些需求其实都不复杂,但默认技能就是不覆盖。OpenClaw真正值钱的地方就在这儿——它有完整的技能包(Skills)开发机制,你可以像给手机装App一样,给OpenClaw装上自己写的专属能力。这篇文章就专门讲OpenClaw技能包开发,从目录结构、配置文件、输入输出协议讲到完整的实操案例和排坑记录,给想自己动手扩展OpenClaw能力的人一条能直接照着走的路径。
1. 为什么OpenClaw的能力扩展要以“技能包”为单位
1.1 “技能包”到底解决什么问题
先明确一个概念:OpenClaw的技能包,本质上是“一段独立部署的能力单元”。它包含自己的代码、配置、资源文件和权限声明,由OpenClaw主程序统一调度。你不需要修改OpenClaw主程序本身,只要按照约定的格式把技能包放进指定目录(或用命令行安装),主程序就会自动发现它,并在识别到相关指令时把对话分发给它处理。
这种方式和手机App Store的逻辑非常像。主程序只负责最基础的语音识别、意图理解和系统调度,具体业务逻辑全部交由各个技能包自治。好处自然是立竿见影:改技能包不会弄坏核心逻辑;多个技能包可以并行安装、独立升级;第三方开发者写的东西也能被其他用户快速安装使用。
那为什么不是直接在主程序代码里加几个if分支?我刚开始也这么想,直到吃过亏才明白。OpenClaw主程序升级节奏很快,你改动过的代码在每次升级时都会被官方版本覆盖,每一次升级都要重新移植,长期维护成本极高。技能包则完全隔离,升级主程序时技能包原样保留,顶多因为接口变化需要小调整。把扩展能力做成技能包,不是“多此一举”,而是从第一天就该遵循的架构边界。
1.2 什么样的功能适合做成技能包
动手之前先做一道判断题。技能包最适合的,是那些“输入清晰、输出明确、流程固定”的任务。你可以用下面三个标准筛:
- 输入是否可预期:用户指令能不能映射到几个固定的意图和参数槽位。比如“设置一个十分钟后的提醒”,“十分钟”是时间参数,“提醒内容”是文本参数,结构稳定。
- 是否有明确的执行结果:执行完必须能产出一个结果,比如“已创建提醒”或“查询到三条待办事项”,而不是开放式聊天。
- 是否需要访问外部资源:比如设备控制、文件系统、第三方API、定时器,这些场景特别适合技能包隔离权限。
反过来,有些东西不适合做成技能包。比如“长期多轮闲聊”——它的状态管理太复杂,几乎需要一套完整对话引擎;再比如“实时视频流处理”——这种对底层资源占用极高的任务,更适合做成独立服务,让技能包只做转发。技能包的定位是“轻量、快速、单一职责”,不要试图往里塞太重的东西。
1.3 动手开发前要准备哪些东西
理论基础先到这,下面进入实操准备。开发一个OpenClaw技能包,你需要准备四样东西:
- 一台跑通了OpenClaw主环境的机器,虚拟机和实体设备都可以;
- Node.js 18及以上(部分旧版本技能包用Python运行时,后面会提);
- OpenClaw的命令行工具(CLI),用于创建、安装和调试技能包;
- 一个顺手的编辑器。
检查完环境之后,先确认CLI是否可用,终端里执行一下:
openclaw --version openclaw skills --help能看到版本号和帮助信息就行。不同小版本的CLI命令多少有差异,以自己环境里的实际输出为准,别硬背命令。
2. 拆解一份标准技能包:目录、配置与通讯协议
2.1 一眼看懂技能包的目录结构
技能包的所有文件都规范地放在一个独立目录里,标准结构长这样:
household-reminder/ ├── manifest.json # 技能包元数据与权限声明 ├── skill.js # 技能包主逻辑入口 ├── resources/ # 静态资源,比如回复模板、提示音 │ └── replies.json ├── assets/ # 非代码资源,按需使用 └── README.md # 技能包说明文档名义上只有manifest.json和入口文件是必需的,其余目录按需添加。但我的建议是:哪怕只是一个学习Demo,也把README.md写上。技能包一旦多起来,你会发现文档里写清楚意图触发词和参数示例,比你自己翻代码回忆快得多。
入口文件的名字不固定,main入口在manifest.json里声明。只要是CLI支持的运行时,用skill.js、skill.py、skill.ts都可以。
2.2 manifest.json 的每个字段都别乱填
manifest.json是技能包的身份证,决定了主程序如何加载和调度它。以一份家务提醒技能包的配置为例:
{ "name": "household-reminder", "version": "0.1.0", "description": "解析\"N分钟后做某事\"类指令,并生成定时提醒。", "runtime": "node18", "main": "skill.js", "timezone": "Asia/Shanghai", "permissions": ["alarm:set", "timer:create"], "triggers": ["提醒", "待办", "记住"] }逐字段说明一下:
name:技能包唯一标识,只能用小写字母、数字和连字符。我踩过坑,一开始用了中文名,CLI直接报错,连安装都过不去。改回全小写英文,一次通过。version:语义化版本号,主版本.次版本.修订号,比如0.1.0。版本号在发布更新时非常重要。runtime:代码运行的运行时环境。主流的用node18,写Python的可以用python3之类。配错了会直接加载失败。main:入口文件的相对路径,相对于技能包根目录。timezone:时区配置。别小看这个字段,定时任务能不能在正确的时间触发全靠它。我用默认UTC的时候,提醒永远晚八小时,后来显式配了Asia/Shanghai才正常。permissions:权限声明数组。技能包要访问定时器、设备控制、文件系统,都需要在这里声明对应权限。主程序在安装时会校验,没声明就会拒绝执行。triggers:触发词数组。用户说出的指令里包含这些词时,主程序倾向于把对话路由给这个技能包。
2.3 输入输出协议:主程序和技能包之间怎么说话
配置只是静态说明,真正干活的是输入输出协议。OpenClaw技能包每次被调用时,入口文件会收到一个标准载荷(Payload),大致结构如下:
{ "intent": "reminder.create", "text": "15分钟后提醒我关洗衣机", "slots": { "minutes": "15", "thing": "关洗衣机" }, "session": { "userId": "user_abc", "conversationId": "conv_123" } }intent:主程序识别出的意图标识,比如reminder.create表示“创建提醒动作”;text:用户原始输入文本;slots:从原始文本中抽取的参数槽位,这是核心,技能包的所有逻辑都围绕它展开;session:会话上下文信息,包括用户标识和会话标识,用于多轮对话追踪。
技能包的返回值同样有约定。最简单也最规范的结构是:
{ "ok": true, "reply": "记下了,15分钟后提醒你:关洗衣机", "data": { "reminderId": "task_001" } }reply是给用户的回复文本,data携带机器可读的结构化结果,方便其他技能包后续调用,这个在后面的进阶联动里会用到。如果处理失败,返回ok: false并附上错误码,比如:
{ "ok": false, "error": "SLOT_MISSING", "reply": "没听懂要提醒你做什么,再说一次试试?" }这套协议看起来简单,但规范化的价值很大。它保证了主程序不用关心技能包内部实现,统一按约定收发数据即可。你写技能包时,只要守好这层契约,就能稳定挂载到任意版本的OpenClaw上。
3. 实操:5分钟开发一个家务提醒技能包
3.1 用CLI创建技能包工程
准备好之后,开始动手。我用一个“家务提醒”技能包做演示,功能非常简单:解析“15分钟后提醒我关洗衣机”“半小时后收衣服”这类指令,创建定时提醒。
先在终端里创建工程,用CLI提供的骨架命令:
openclaw skills new household-reminder cd household-reminder这条命令会自动生成一个包含manifest.json、skill.js、README.md的最简工程。打开manifest.json,把上一节讲的字段逐个填好,permissions里加上提醒功能需要的权限。
3.2 编写核心逻辑:从文本里提取参数并设置定时任务
核心逻辑写在skill.js里。先做文本解析,再调用定时服务。我的实现如下:
const PARSER_PATTERN = /(\d+)\s*(秒|分钟|小时)后?\s*(.+)/; module.exports = { name: "household-reminder", async handle(payload) { const slots = payload.slots || {}; const text = slots.text || payload.text || ""; const match = text.match(PARSER_PATTERN); if (!match) { return { ok: false, error: "SLOT_MISSING", reply: "没听懂要提醒你做什么,试试说\"15分钟后提醒我关洗衣机\"?" }; } const value = Number(match[1]); const unit = match[2]; const thing = match[3]; const seconds = unit === "小时" ? value * 3600 : unit === "分钟" ? value * 60 : value; const reminderId = await this.createReminder(thing, seconds); return { ok: true, reply: `记下了,${value}${unit}后提醒你:${thing}`, data: { reminderId } }; }, async createReminder(thing, seconds) { // 调用OpenClaw提供的定时服务,返回任务句柄 const handle = await this.context.api.createTimer({ delaySeconds: seconds, message: thing, type: "reminder" }); return handle.id; } };这里有几处要特别说明。首先是入口方法名,技能包实现handle(payload)方法作为统一入口,主程序识别到触发词后就会调用它。方法返回一个Promise,成功后主程序再把你返回的reply播放给用户。
其次,正则表达式是关键。(\d+)提取数字,(秒|分钟|小时)提取时间单位,(.+)捕获具体事项。这套写法的容错能力还行,能覆盖“15分钟后提醒我关洗衣机”“半小时后收衣服”“3秒后测试”这些常见说法。但注意,它不支持“晚上八点提醒”这种绝对时间表达,那是另一个解析维度,需要更复杂的词表和时间解析逻辑,后面进阶部分再谈。
3.3 安装并验证技能包是否生效
代码写完,进入安装验证环节:
openclaw skills install . openclaw skills listinstall .会把当前目录下的技能包装进OpenClaw,list用来确认技能包是否在列表中。然后打开调试终端,模拟用户输入:
openclaw skills test household-reminder --input "15分钟后提醒我关洗衣机"如果一切正常,你能在输出里看到技能包返回的Standard Response,包含回复文本和数据。接着等上15分钟,看提醒是否真的触发。第一次完整验证建议把时间调短一点,比如3秒钟,确认链路通了再改回真实时间。
我在实测中遇到过一个问题:执行install时报“permission denied”。查了半天,是技能包目录的属主不对,用sudo重试或者把目录chown给当前用户就好了。遇到权限报错优先检查目录属主,不要一上来就怀疑代码。
4. 技能包开发最容易踩的坑:排查思路与避坑实录
4.1 加载、触发与槽位:三个高频故障点
技能包开发中,真正让人头秃的问题其实集中在几个固定环节。根据我自己的经历和周围同行的交流,把典型问题整理成了下面的速查表:
| 故障现象 | 最常见原因 | 排查与解决 |
|---|---|---|
| 技能包安装后不在列表里 | manifest.json 格式错误或name含非法字符 | 用openclaw skills validate ./manifest.json做校验 |
| 触发词说了没反应 | 触发词被更高优先级的技能包抢占 | 打开调试日志看意图路由结果,调整触发词优先级 |
| slots一直为空 | 文本没有被正确解析出参数槽位 | 先直接打印原始text,确认正则或实体抽取是否生效 |
| 定时任务不触发 | permissions里缺少alarm:set | 检查权限声明,补上后重新安装 |
| 回复正常但设备没动作 | 权限不足或设备编址错误 | 先操作一个固定设备ID,验证链路通再泛化 |
| 安装后CPU占用异常高 | 代码里有while轮询未释放 | 检查定时器、循环,确保回调结束即释放 |
这六个问题覆盖了绝大多数入门阶段的故障。下面挑三个重点展开。
4.2 技能包加载失败:先校验再改代码
加载失败是出现频率最高的。表现形式是openclaw skills install .执行成功,但list里看不到技能包。绝大多数情况下,问题出在 JSON 配置上。多了一个逗号、字符串没闭合,或者name字段带了中文/空格,都会导致解析失败。
排查顺序很重要。先跑配置校验命令,不要一上来就翻代码:
openclaw skills validate ./manifest.json校验命令会直接告诉你哪一行格式有问题。我遇到过一次校验通过但依然加载失败的情况,最后发现是入口文件路径写错了——manifest.json里main写的"./src/skill.js",但实际文件在根目录。这类路径问题校验命令测不出来的,直接看报错日志最有效。
4.3 触发词不生效:路由优先级比你想的更复杂
触发词写好了,但用户说话就是不被路由到你的技能包。这个问题要记住一个核心概念:OpenClaw的意图路由是有优先级的,同时适配多个技能包时,它只会交给优先级最高的那个处理。
排查方法是用调试模式跑一条输入,观察意图识别日志:
openclaw skills debug household-reminder --input "15分钟后提醒我关洗衣机"日志里能看到命中了哪些技能包、最终选中的是谁。如果发现命中列表里没有你的技能包,大概率是触发词设置问题;如果命中了但没被选中,那就是优先级不够。
我自己的经验是,不要在triggers里写“提醒”这种过于通用的词。它和系统自带技能包的“帮我订个闹钟”冲突很严重。改成“家务提醒”“洗衣机提醒”这类组合短语,冲突概率直线下降。
4.4 时区、超时与资源释放:不显眼但总要还的债
时区问题在前面提过一次,这里再展开。为什么定时任务总是不按预期时间触发?绝大多数是因为manifest.json里没写timezone,或者写了但主程序没识别。时区的作用是让时间类槽位(比如“晚上八点”)能正确换算成绝对时间戳。解决办法就一个:在清单里显式配置你的本地时区。
资源释放是个更隐蔽的坑。技能包可能在执行时开了定时器、建了网络连接,但执行完没清理。这类代码在单次测试时一切正常,一旦高频率调用,内存和句柄占用就涨上去了。OpenClaw的调度机制会强制回收超时任务,但内部创建的timer和socket是自己的责任。我的习惯是:凡是在handle里创建的资源,一律在返回结果前显式释放;如果确实需要任务在返回后继续运行,一定要用框架提供的task机制,不要自己裸开定时器。
5. 从“能用”到“好用”:技能包开发的三个进阶技巧
5.1 匹配置信度阈值:与其答错,不如反问
把技能包装进OpenClaw只是第一步,难的是让它面对模糊指令时不胡说。初始版本的技能包不管三七二十一,只要触发词沾边就执行,结果闹了不少笑话。比如用户说“帮我处理一下洗衣机”,技能包居然开始创建提醒,但用户实际是想让它控制洗衣机执行某个操作。
后来我在技能包里加了置信度判断逻辑。核心思想很简单:模型在解析意图和槽位时,会返回一个置信度分数(0到1)。低于阈值的指令,不硬答,而是反问用户确认:
{ "ok": false, "error": "LOW_CONFIDENCE", "reply": "我理解的是\"15分钟后提醒你关洗衣机\",对吗?" }这样看起来多了一次交互,但用户体验反而更好。宁可多问一句,也不要猜错然后打扰用户。具体阈值取决于你的业务场景,我常用的基准是:槽位全部命中且置信度高于0.6才直接执行;否则走确认流程。这套思路不仅适用于家务提醒技能包,几乎所有涉及执行动作的技能包都该这样做。
5.2 技能包之间的联动:结构化返回是地基
单一技能包能力有限,真正的好玩之处在于多个技能包组合起来。比如“天气查询”技能包查出来明天有雨,“通勤提醒”技能包据此在明早出门前提醒带伞。二者怎么协作?靠的就是技能包返回的data字段。
前面讲过,技能包返回结构里的data是机器可读的结构化数据。只要A技能包返回{ "weather": "rain", "date": "2025-06-10" },B技能包就能在它的执行逻辑里读取这些字段,做下一步判断。OpenClaw的会话上下文会把上一轮技能包的data拼接到下一轮调用的session里,所以技能包A的结果可以作为技能包B的输入。
开发技巧是:不要只看reply好不好看,更要设计好data的schema。字段名、类型、嵌套结构要稳定。我见过有人把data直接塞一个包含换行的字符串,后一个技能包解析的时候直接懵了。结构化数据就要从头保持结构,别偷懒。
5.3 统一回复模板与兜底文案:让每个技能包都像个正规军
技能包多了之后,你会发现每个技能包的回复风格完全不一样。有的说“已设置”,有的说“搞定了”,有的直接甩一堆报错文本。给用户的感觉就是分裂的。OpenClaw官方推荐的做法是使用回复模板,把话术统一放在resources/replies.json里:
{ "reminder.created": "记下了,{value}{unit}后提醒你:{thing}", "reminder.slotMissing": "没听懂要提醒你做什么,试试说\"15分钟后提醒我关洗衣机\"?" }主逻辑里不再直接拼字符串,而是通过模板引擎填充参数。同时一定要写兜底文案,就是拿到无法处理的输入时的最终回复。这个兜底文案有两个要求:一是告诉用户“没听懂”,二是给一个具体的替代说法示例。比如我上面代码写的“试试说‘15分钟后提醒我关洗衣机’”,就是标准的兜底。运行久了你会发现,用户真的会因为这句兜底,把指令改成技能包能解析的格式。
回复模板的另一个好处是方便做多语言。在resources目录下按语言拆开,加载时根据OpenClaw传入的语言参数选择对应模板。这个改造不复杂,但能让技能包直接被不同语言的用户使用,性价比很高。
6. 写在最后的实操体会
技能包开发跑通一遍之后,回头看最明显的感受是:这事的门槛不在代码,而在对协议和边界的理解。把manifest字段写对、把输入输出结构想清楚、把资源和权限管理好,这三点比炫技式的代码技巧重要得多。
再分享一个我自己的小习惯。每次开发技能包,我会先把所有外部依赖全部替换成假数据,比如定时器不真建、API不真调,直接把各种分支输入跑一遍,确认逻辑路径都走通了,再接真实设备和服务。你可能觉得这样多此一举,但实际上一旦开始接设备控制或外部API,限流、超时、鉴权问题混在一起,你就很难判断到底是逻辑错了还是接口错了。先用假数据把逻辑校准,会让整个排错周期缩短一大半。
OpenClaw的技能包机制本身还在快速演进,接口细节会变,但“独立能力单元+标准通讯协议+权限隔离”这套思路是稳定的。把基础打牢,后面不管版本怎么升级,你都能快速跟上。