6 custom skills (assign-task, dispatch-webhook, daily-briefing, task-capture, qmd-brain, tts-voice) with technical documentation. Compatible with Claude Code, OpenClaw, Codex CLI, and OpenCode.
13 KiB
OpenClaw Skill 開發指南
從零開始建立一個 OpenClaw workspace skill 的完整步驟與慣例。 基於 x550v 主機上的實作經驗整理(2026-03-02)。
目錄
- 檔案結構
- SKILL.md 格式
- handler.ts 格式
- 觸發機制(Triggers)
- 可用工具(Tools)
- Context 物件
- 回傳格式
- Skill 間呼叫(callSkill)
- Internal Skill(僅供內部呼叫)
- 安裝與驗證
- 實戰範例
- 常見踩坑
1. 檔案結構
每個 skill 是一個資料夾,包含兩個檔案:
skill-name/
├── SKILL.md # 元資料(frontmatter)+ Markdown 說明
└── handler.ts # TypeScript 實作
| 路徑 | 用途 |
|---|---|
/home/selig/openclaw-skill/skills/{name}/ |
原始碼(版本控管) |
~/.openclaw/workspace/skills/{name}/ |
安裝位置(Gateway 讀取) |
資料夾名稱必須與 SKILL.md frontmatter 的
name欄位一致。
2. SKILL.md 格式
---
name: my-skill # 必填,和資料夾同名,kebab-case
description: 一句話說明功能 # 必填,顯示在 skills list
triggers: # 必填,陣列;空陣列 = 使用者不可觸發
- "關鍵字1"
- "keyword2"
tools: # 必填,宣告此 skill 可使用的工具
- exec
- web_fetch
internal: false # 選填,true = 隱藏(不顯示、不可由使用者觸發)
---
# My Skill 標題
## 功能說明
詳細描述 skill 做什麼、怎麼做。
## 觸發範例
使用者怎麼說會觸發這個 skill、agent 會怎麼回應。
## 設定(如果需要)
需要哪些環境變數、外部服務、CLI 工具。
Frontmatter 欄位一覽
| 欄位 | 必填 | 型別 | 說明 |
|---|---|---|---|
name |
是 | string | Skill 識別名,kebab-case,等同資料夾名 |
description |
是 | string | 一行描述,顯示在 openclaw skills list |
triggers |
是 | string[] | 觸發關鍵字陣列,空陣列 [] 表示僅供內部呼叫 |
tools |
是 | string[] | 可用工具宣告(exec, web_fetch, web_search, memory) |
internal |
否 | boolean | 預設 false;true 時對使用者隱藏 |
3. handler.ts 格式
/**
* my-skill handler
* 功能簡述
*/
import { execSync } from 'child_process';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
export async function handler(ctx: any) {
const message = ctx.message?.text || ctx.message?.content || '';
if (!message.trim()) {
return { reply: '請提供輸入。' };
}
// ... 業務邏輯 ...
return {
reply: '回覆文字(支援 Markdown)',
metadata: { key: 'value' },
};
}
重點
- 必須
export async function handler(ctx: any) - 函式名必須是
handler,必須 async,必須 export - 可使用 Node.js 內建模組(
fs,path,child_process,util等) - 不能
import第三方 npm 套件(skill 環境沒有 node_modules) - 外部依賴用
execSync/exec呼叫 CLI 或 curl
4. 觸發機制(Triggers)
OpenClaw 收到使用者訊息時,對 triggers 做 case-insensitive substring match:
使用者訊息:「幫我搜尋 nginx 設定」
^^^^
triggers: ["搜尋", "查找", "recall"]
↑ 命中 → 啟動 qmd-brain skill
設計觸發詞的原則
| 原則 | 說明 |
|---|---|
| 中英文兼備 | 使用者可能用中文或英文,如 ["tts", "文字轉語音"] |
| 避免太短 | 一兩個字容易誤觸發(如「找」可能在任何對話出現) |
| 避免太泛 | 「幫我」會攔截大量無關訊息 |
| 口語化 | 使用者自然語言,如「唸出來」、「記一下」 |
| 順序無關 | 只要訊息中包含任一觸發詞即命中 |
多 skill 觸發衝突
若多個 skill 的 trigger 同時命中,agent 會根據上下文選擇最適合的 skill。盡量讓 trigger 夠獨特以避免衝突。
5. 可用工具(Tools)
在 SKILL.md 的 tools 陣列中宣告,OpenClaw 會依此限制 skill 的能力。
| 工具 | 用途 | handler 中的使用方式 |
|---|---|---|
exec |
執行 shell 指令 | execSync() / exec() from child_process |
web_fetch |
HTTP 請求 | fetch(url) 或 execSync('curl ...') |
web_search |
搜尋引擎 | 由 agent 呼叫,非 handler 直接使用 |
memory |
對話記憶 | ctx.memory.*(需 memory plugin) |
實務上,大部分 skill 只需要
exec。需要打 API 或抓網頁時加web_fetch。
6. Context 物件
handler 收到的 ctx 結構:
interface SkillContext {
message: {
text: string; // 使用者原始訊息
content: string; // 同 text(備用欄位)
};
env: {
OPENCLAW_WORKSPACE: string; // workspace 路徑(預設 ~/.openclaw/workspace)
HOME: string; // 使用者家目錄
[key: string]: string; // 其他環境變數
};
agent: {
id: string; // 當前 agent 名稱(如 "main", "kaiwu")
};
callSkill: (name: string, params: any) => Promise<any>; // 呼叫其他 skill
memory?: any; // memory plugin 提供的介面(若有)
}
常用存取模式
// 取得使用者訊息
const message = ctx.message?.text || ctx.message?.content || '';
// 取得 workspace 路徑
const workspace = ctx.env?.OPENCLAW_WORKSPACE || process.env.HOME + '/.openclaw/workspace';
// 讀寫 workspace 檔案
const filePath = join(workspace, 'TODO.md');
if (existsSync(filePath)) {
const content = readFileSync(filePath, 'utf-8');
}
writeFileSync(filePath, newContent, 'utf-8');
7. 回傳格式
return {
reply: string; // 必填:回覆使用者的文字(支援 Markdown)
metadata?: { // 選填:結構化資料(記錄 / 追蹤用,不顯示給使用者)
[key: string]: any;
};
files?: string[]; // 選填:附件檔案路徑陣列(如產出的音訊、圖片)
};
範例
// 簡單文字回覆
return { reply: '✅ 完成!' };
// 附帶 metadata
return {
reply: '🧠 搜尋完成',
metadata: { query: 'nginx', results: 5 },
};
// 附帶檔案
return {
reply: '🔊 語音合成完成',
files: ['/tmp/output.wav'],
metadata: { text: '你好' },
};
8. Skill 間呼叫(callSkill)
一個 skill 可以呼叫另一個 skill:
export async function handler(ctx: any) {
// 呼叫 dispatch-webhook skill
const result = await ctx.callSkill('dispatch-webhook', {
target: 'vps-a',
payload: { task: '部署新版本' },
webhookUrl: 'https://vps-a.example.com/webhook',
});
return { reply: `派發結果:${result.reply}` };
}
被呼叫的 skill 會收到 ctx.message 為傳入的參數物件。
9. Internal Skill(僅供內部呼叫)
設定 internal: true + 空 triggers,使 skill 對使用者不可見:
---
name: dispatch-webhook
description: 發送 Webhook 到 VPS
triggers: []
tools:
- web_fetch
- exec
internal: true
---
用途:底層工具 skill,由其他 skill 透過 callSkill 呼叫。
10. 安裝與驗證
從原始碼安裝
# 1. 複製到 workspace
cp -r /home/selig/openclaw-skill/skills/my-skill \
~/.openclaw/workspace/skills/
# 2. 確認檔案結構
ls ~/.openclaw/workspace/skills/my-skill/
# 應該看到:SKILL.md handler.ts
# 3. 重啟 Gateway(載入新 skill)
systemctl --user restart openclaw-gateway
# 4. 確認載入成功
openclaw skills list
# 應該看到 my-skill 狀態為 ✓ ready
更新已安裝的 skill
# 修改原始碼後,重新複製 + 重啟
cp -r /home/selig/openclaw-skill/skills/my-skill \
~/.openclaw/workspace/skills/
systemctl --user restart openclaw-gateway
除錯
# 查看 Gateway 日誌(skill 載入錯誤會在這裡)
journalctl --user -u openclaw-gateway -f
# 常見問題
# - SKILL.md frontmatter 格式錯誤(YAML 語法)
# - handler.ts 語法錯誤(TypeScript 編譯失敗)
# - name 和資料夾名不一致
# - tools 未宣告就使用 → 權限被擋
11. 實戰範例
範例 A:最簡 Skill(純文字處理)
task-capture:使用者說「記一下…」→ 解析優先級/截止日/標籤 → 寫入 TODO.md
skills/task-capture/
├── SKILL.md triggers: ["記住", "記一下", "待辦", "todo", ...]
└── handler.ts 讀寫 workspace/TODO.md
重點技巧:
cleanTaskText()移除觸發詞,提取純任務文字detectPriority()/detectDueDate()/detectTags()自動分類- 直接用
readFileSync/writeFileSync操作 workspace 檔案
範例 B:呼叫外部 API 的 Skill
tts-voice:使用者說「tts 你好」→ 呼叫本機 LuxTTS API → 回傳音訊檔
skills/tts-voice/
├── SKILL.md triggers: ["tts", "文字轉語音", "語音合成", ...]
└── handler.ts curl → localhost:7860 API
重點技巧:
- 用
execSync('curl ...')呼叫 HTTP API(無法直接import axios) - 認證:先取 cookie 再帶 cookie 呼叫 API
- 帳密:
readFileSync('.env')+ regex 解析 - 長時間操作(合成 ~20 秒):設定
timeout: 120000 - 回傳
files: ['/tmp/output.wav']讓 agent 附送檔案
範例 C:外部 CLI 工具 + 多重搜尋引擎
qmd-brain:使用者說「搜尋 nginx」→ 並行 BM25 + pgvector → 整合結果
skills/qmd-brain/
├── SKILL.md triggers: ["搜尋", "查找", "recall", "知識庫", ...]
└── handler.ts execAsync → qmd CLI + embed_to_pg.py
重點技巧:
promisify(exec)做非同步 shell 呼叫Promise.all([qmdSearch(), pgSearch()])並行多搜尋detectIntent()判斷使用者意圖(搜尋 / 更新索引 / 統計)extractQuery()移除觸發詞,提取搜尋關鍵字- 結果截斷防止 Telegram 訊息過長
範例 D:Internal Skill(底層工具)
dispatch-webhook:由 assign-task 呼叫,發送 webhook + 重試
skills/dispatch-webhook/
├── SKILL.md triggers: [], internal: true
└── handler.ts fetch → VPS webhook endpoint
重點技巧:
triggers: []+internal: true→ 使用者看不到- 由其他 skill 用
ctx.callSkill('dispatch-webhook', payload)呼叫 - 實作重試邏輯、錯誤碼分類、超時處理
12. 常見踩坑
| 問題 | 原因 | 解決 |
|---|---|---|
openclaw skills list 看不到新 skill |
未重啟 Gateway | systemctl --user restart openclaw-gateway |
handler.ts 中 import axios 失敗 |
skill 環境無 node_modules | 改用 execSync('curl ...') 或內建 fetch |
| SKILL.md parse 失敗 | frontmatter YAML 語法錯(如缺引號、縮排錯) | 用 YAML lint 檢查,字串值加引號 |
| skill name 和資料夾名不一致 | Gateway 比對 name ↔ 資料夾名 | 確保兩者完全相同(kebab-case) |
sudo openclaw skills list 看不到 workspace skills |
sudo 以 root 跑,PATH 不同 | 改用 openclaw skills list(不加 sudo) |
| trigger 誤觸發無關對話 | 觸發詞太短/太泛 | 用更具體的詞,避免單字觸發 |
ctx.env.OPENCLAW_WORKSPACE undefined |
舊版或特殊環境 | fallback:process.env.HOME + '/.openclaw/workspace' |
| 呼叫 nvm 安裝的 CLI 找不到 | Gateway PATH 不含 nvm 路徑 | symlink 到 ~/.local/bin/ |
| handler 回傳後 Telegram 沒顯示 | reply 欄位為空字串 |
確保 reply 有內容 |
.env 讀不到(Permission denied) |
檔案權限 600 但 Gateway 是 selig user | selig user service 可以讀自己的 600 檔案,正常不會有問題;確認檔案 owner |
快速模板
建立新 skill 時,複製此模板:
SKILL_NAME="my-new-skill"
mkdir -p ~/openclaw-skill/skills/$SKILL_NAME
SKILL.md:
---
name: my-new-skill
description: 一句話描述功能
triggers:
- "觸發詞1"
- "trigger2"
tools:
- exec
---
# My New Skill
## 功能說明
做什麼、怎麼觸發。
## 觸發範例
使用者:「觸發詞1 某些參數」→ skill 做某事 → 回覆結果
handler.ts:
import { execSync } from 'child_process';
const TRIGGER_WORDS = ['觸發詞1', 'trigger2'];
function cleanInput(message: string): string {
let cleaned = message;
for (const t of TRIGGER_WORDS) {
cleaned = cleaned.replace(new RegExp(t, 'gi'), '');
}
return cleaned.replace(/^[\s::,,]+/, '').trim();
}
export async function handler(ctx: any) {
const message = ctx.message?.text || ctx.message?.content || '';
const input = cleanInput(message);
if (!input) {
return { reply: '請提供輸入內容。' };
}
// TODO: 業務邏輯
return {
reply: `✅ 完成:${input}`,
metadata: { input },
};
}
安裝:
cp -r ~/openclaw-skill/skills/$SKILL_NAME ~/.openclaw/workspace/skills/
systemctl --user restart openclaw-gateway
openclaw skills list # 確認 ✓ ready