老周的工具笔记

老周的工具笔记

已发布 24 篇文章

`{baseDir}` 和 `scripts/` 目录:给技能配上真正能跑的脚本

`{baseDir}` 和 `scripts/` 目录:给技能配上真正能跑的脚本
`{baseDir}` 和 `scripts/` 目录:给技能配上真正能跑的脚本

一、先说个场景

你写了一个 Skill,想让 agent 帮你把 Markdown 转成 HTML。于是你在 SKILL.md 正文里详细写了每一段 sed/awk 命令。结果发现:换一台电脑用户名不一样,路径全错;Windows 上没有 sed;代码块太长,agent 看着看着就丢了步骤。

正确的做法是把复杂逻辑放进 scripts/ 目录的脚本文件里,SKILL.md 只负责告诉 agent “什么时候调用哪个脚本、传什么参数”。这样既安全又便于维护,也更符合 OpenClaw 官方的 best practices:SKILL.md 写”做什么”,脚本写”怎么做”。

二、为什么是这样

OpenClaw 的 agent 读的是 Markdown 指令。如果指令里塞满 shell 命令,会出现几个问题:

  • 可移植性差:写死 /home/alice/... 在别人机器上直接失效。
  • 上下文浪费:长命令序列挤占模型上下文,容易出错。
  • 安全风险:把参数直接拼进 shell 命令,容易被不可信输入注入。
  • 难测试:在 Markdown 里调命令,不如单独跑脚本来得直接。

把脚本放到 scripts/ 目录,SKILL.md 用 {baseDir} 引用,就能解决前两个问题:路径随技能目录自适应,正文保持简洁。把参数校验、错误处理放进脚本,还能降低注入风险。

模型并不擅长”精确复现一长串命令”。它读 Markdown 时容易把多行命令里的某一行漏掉,或者在引号、转义上出错。但你让模型”去跑 {baseDir}/scripts/convert.sh 并传这两个参数”,它只需要做一件简单且明确的事,成功率陡增。scripts/ 是把”模糊的自然语言指令”翻译成”确定的程序执行”的桥梁——模型负责理解意图,脚本负责保证结果,各司其职,比让模型亲自拼命令稳得多。

三、跟着做:分步操作

1. 目录结构

~/.openclaw/workspace/skills/md-to-html/
├── SKILL.md
└── scripts/
    └── convert.sh

这个布局里,SKILL.md 和 scripts/ 平级,是最简单也最推荐的形式。技能目录下还可以有 assets/、examples/、references/、templates/ 等规范目录(后面误区里会提到),但 scripts/ 是你真正放可执行逻辑的地方。只要 SKILL.md 在技能根目录、scripts/ 也在根目录,相对关系就永远成立,不会因为技能被装到不同机器而改变。

2. 写脚本

scripts/convert.sh:

#!/bin/bash
set -euo pipefail
INPUT="$1"
OUTPUT="$2"

if [[ ! -f "$INPUT" ]]; then
  echo "Error: input file not found: $INPUT" >&2
  exit 1
fi

pandoc "$INPUT" -o "$OUTPUT"
echo "Converted: $OUTPUT"

注意 set -euo pipefail 这三件套:它让脚本在出错、遇到未定义变量、管道失败时立刻停下,而不是带着错误状态继续往下跑。对 agent 调用的脚本尤其重要——你宁可它干脆报错退出,也不要它” silently 跑出错误结果”。if [[ ! -f ... ]] 那段则是参数校验的范本:先确认输入文件存在,再动手,避免把空参数传进 pandoc 造成诡异失败。

3. 写 SKILL.md

---
name: md-to-html
description: Convert a Markdown file to HTML using pandoc.
metadata: { "openclaw": { "requires": { "bins": ["pandoc"] } } }
---
# Markdown to HTML

When the user wants to convert a Markdown file to HTML, use the `exec` tool to run `{baseDir}/scripts/convert.sh <input.md> <output.html>`. Confirm the input path with the user before running.

注意 {baseDir} 会被 OpenClaw 解析成该技能目录的绝对路径。无论 OpenClaw 装在哪台机器、哪个用户下,脚本路径都是对的。

这里有个细节值得强调:requires: { bins: ["pandoc"] } 和 {baseDir} 是搭档关系。gating 保证”机器上有 pandoc 才加载这个技能”,{baseDir} 保证”加载后脚本路径一定找得到”。两者一起,才让这个技能真正做到即装即用、换机不慌。另外正文里那句”Confirm the input path with the user before running”也很关键:让 agent 在动真格之前先跟用户确认路径,能避免误改、误删文件,是给脚本类技能加的一道人工保险。

4. 验证

openclaw skills list
openclaw agent --message "把 notes/draft.md 转成 HTML"

验证分两步:先 openclaw skills list 确认技能确实被加载(gating 没把它挡掉),再用 openclaw agent --message 发一条真实指令,看 agent 是否正确地调用了脚本、产出预期文件。如果 list 里没有它,回头查 requires.bins 和 pandoc 是否真的在 PATH;如果 list 有但执行失败,多半是脚本权限或参数问题。两层验证分开做,定位问题会快很多。

四、可以复制的指令

# 创建技能目录
mkdir -p ~/.openclaw/workspace/skills/md-to-html/scripts

# 查看 {baseDir} 会被解析到哪
# 在 SKILL.md 里写:Run `{baseDir}/scripts/convert.sh ...`
---
name: pdf-to-text
description: Extract text from a PDF using pdftotext.
metadata: { "openclaw": { "requires": { "bins": ["pdftotext"] } } }
---

# PDF to Text

When the user asks to extract text from a PDF, run `{baseDir}/scripts/extract.sh <pdf-path>` and return the first 2000 characters.
#!/bin/bash
# scripts/extract.sh
set -euo pipefail
PDF="$1"
pdftotext "$PDF" - | head -c 2000

这一段给了第二个现成样板:pdf-to-text。它和 md-to-html 是同一套路——gating 检查 pdftotext 在不在,脚本用 head -c 2000 控制输出长度,避免把整本 PDF 灌进对话上下文。{baseDir} 再次出现,证明它是脚本类技能的统一寻址方式。你照这两个例子,几乎可以把任何”读入一个文件、产出一些内容”的需求,都套进”SKILL.md 写意图 + scripts 写实现”的模板里。

五、实战案例

你有一个 daily-report skill,需要:

  1. 读取 ~/notes/ 下当天修改的文件。
  2. 调用 Python 脚本做摘要。
  3. 输出成三段式日报。

目录结构:

~/.openclaw/workspace/skills/daily-report/
├── SKILL.md
├── scripts/
│   ├── collect.sh
│   └── summarize.py
└── templates/
    └── report.md.tpl

SKILL.md:

---
name: daily-report
description: Generate a daily standup report from today's notes.
metadata: { "openclaw": { "requires": { "bins": ["python3"] } } }
---

# Daily Report

When the user says "写日报" or "daily report", run these steps:

1. Use `exec` to run `{baseDir}/scripts/collect.sh` to get today's modified files.
2. Pass the result to `{baseDir}/scripts/summarize.py`.
3. Render the output using `{baseDir}/templates/report.md.tpl`.
4. Save the final report to `~/daily-reports/<today>.md`.

scripts/collect.sh:

#!/bin/bash
find "$HOME/notes" -type f -mtime -1 -print

scripts/summarize.py:

#!/usr/bin/env python3
import sys, re
files = sys.stdin.read().strip().splitlines()
for f in files:
    with open(f) as fh:
        text = fh.read()
    print(f"## {f}\n{text[:300]}\n")

这样,SKILL.md 保持简短,真正的文件遍历、读取、格式化都交给脚本。你单独调试脚本也很方便,不用每次让 agent 跑一遍。

这个案例的目录结构里还有一个值得注意的点:templates/ 单独放模板,而不是把模板内容硬塞进 SKILL.md。模板是”数据”不是”逻辑”,和脚本分开才清晰,将来改排版只动 report.md.tpl 一个文件。这也呼应了误区 5 提到的 Skill Workshop 目录规范——把不同类型的文件归到各自该在的目录,你的技能会随着功能变多而仍然井井有条,而不是所有东西挤在一个根目录里。

六、常见误区

  • 误区 1:把脚本内容写进 SKILL.md 正文。正文应该”告诉模型做什么”,复杂脚本放到 scripts/。
  • 误区 2:在脚本里写死用户名路径。用 $HOME、{baseDir} 等变量,保证可移植。
  • 误区 3:不校验脚本参数。脚本应校验参数存在、路径合法,避免不可信输入导致注入。
  • 误区 4:脚本没有可执行权限。某些场景下 agent 通过 exec 调用脚本时,需要脚本有 chmod +x。
  • 误区 5:把模板、示例、参考资料乱放。OpenClaw 的 Skill Workshop 提案目录规范是 assets/、examples/、references/、scripts/、templates/,早养成习惯。

补充误区 3 的实际危害:当 agent 把用户提供的文件名直接拼进脚本参数,而你没有校验,恶意或手滑的输入(比如带空格、分号、反引号的路径)就可能变成额外命令被执行——这就是经典的命令注入。在脚本开头用 set -u 配合”参数个数检查 + 文件存在检查”,是成本极低却很有效的防御。至于误区 4,很多新手写完脚本忘了 chmod +x,结果 agent 一调用就报”permission denied”,排查半天才发现是权限问题,所以建脚本后顺手加执行权限应成为肌肉记忆。

七、进阶玩法

按平台分发不同脚本:

scripts/
├── run.sh
├── run.ps1
└── run.py

SKILL.md 正文里让 agent 根据平台选择脚本:

On macOS or Linux, run `{baseDir}/scripts/run.sh`. On Windows, run `{baseDir}/scripts/run.ps1`.

把脚本作为配置项:

{
  skills: {
    entries: {
      "md-to-html": {
        config: { converter: "pandoc" },
      },
    },
  },
}

脚本里读取环境变量或 skills.entries.<name>.config 注入的配置,实现同一技能在不同环境下用不同后端。

按平台分发这个玩法,正好呼应了误区 2 说的”Windows 上没有 sed”——与其写一个跨平台兼容性糟糕的 shell 脚本,不如直接为不同系统准备 run.sh / run.ps1,让 SKILL.md 根据平台挑一个。而 config 注入则更灵活:比如上面的 converter: "pandoc",将来你想换成 wkhtmltopdf,只改 openclaw.json 里的 config,脚本读这个值决定调用谁,SKILL.md 和脚本本身都不用动。这种”行为由配置驱动”的思路,让同一个技能能平滑适配不同团队、不同机器的差异,是技能走向”可复用组件”的关键一步。

八、小结

{baseDir} 和 scripts/ 是把 Skill 从”演示玩具”变成”实用工具”的关键。SKILL.md 负责”何时调用”,脚本负责”如何执行”。路径用 {baseDir} 相对引用,脚本用 scripts/ 目录组织,配合 gating 确保依赖存在,技能就会变得稳定、可移植、易维护。下次写 Skill 时,只要逻辑超过三行命令,就值得拆成脚本。