forked from Selig/openclaw-skill
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.
483 lines
13 KiB
Markdown
483 lines
13 KiB
Markdown
# OpenClaw Skill 開發指南
|
||
|
||
> 從零開始建立一個 OpenClaw workspace skill 的完整步驟與慣例。
|
||
> 基於 x550v 主機上的實作經驗整理(2026-03-02)。
|
||
|
||
---
|
||
|
||
## 目錄
|
||
|
||
1. [檔案結構](#1-檔案結構)
|
||
2. [SKILL.md 格式](#2-skillmd-格式)
|
||
3. [handler.ts 格式](#3-handlerts-格式)
|
||
4. [觸發機制(Triggers)](#4-觸發機制triggers)
|
||
5. [可用工具(Tools)](#5-可用工具tools)
|
||
6. [Context 物件](#6-context-物件)
|
||
7. [回傳格式](#7-回傳格式)
|
||
8. [Skill 間呼叫(callSkill)](#8-skill-間呼叫callskill)
|
||
9. [Internal Skill(僅供內部呼叫)](#9-internal-skill僅供內部呼叫)
|
||
10. [安裝與驗證](#10-安裝與驗證)
|
||
11. [實戰範例](#11-實戰範例)
|
||
12. [常見踩坑](#12-常見踩坑)
|
||
|
||
---
|
||
|
||
## 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 格式
|
||
|
||
```yaml
|
||
---
|
||
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 格式
|
||
|
||
```typescript
|
||
/**
|
||
* 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` 結構:
|
||
|
||
```typescript
|
||
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 提供的介面(若有)
|
||
}
|
||
```
|
||
|
||
### 常用存取模式
|
||
|
||
```typescript
|
||
// 取得使用者訊息
|
||
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. 回傳格式
|
||
|
||
```typescript
|
||
return {
|
||
reply: string; // 必填:回覆使用者的文字(支援 Markdown)
|
||
metadata?: { // 選填:結構化資料(記錄 / 追蹤用,不顯示給使用者)
|
||
[key: string]: any;
|
||
};
|
||
files?: string[]; // 選填:附件檔案路徑陣列(如產出的音訊、圖片)
|
||
};
|
||
```
|
||
|
||
### 範例
|
||
|
||
```typescript
|
||
// 簡單文字回覆
|
||
return { reply: '✅ 完成!' };
|
||
|
||
// 附帶 metadata
|
||
return {
|
||
reply: '🧠 搜尋完成',
|
||
metadata: { query: 'nginx', results: 5 },
|
||
};
|
||
|
||
// 附帶檔案
|
||
return {
|
||
reply: '🔊 語音合成完成',
|
||
files: ['/tmp/output.wav'],
|
||
metadata: { text: '你好' },
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Skill 間呼叫(callSkill)
|
||
|
||
一個 skill 可以呼叫另一個 skill:
|
||
|
||
```typescript
|
||
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 對使用者不可見:
|
||
|
||
```yaml
|
||
---
|
||
name: dispatch-webhook
|
||
description: 發送 Webhook 到 VPS
|
||
triggers: []
|
||
tools:
|
||
- web_fetch
|
||
- exec
|
||
internal: true
|
||
---
|
||
```
|
||
|
||
用途:底層工具 skill,由其他 skill 透過 `callSkill` 呼叫。
|
||
|
||
---
|
||
|
||
## 10. 安裝與驗證
|
||
|
||
### 從原始碼安裝
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 修改原始碼後,重新複製 + 重啟
|
||
cp -r /home/selig/openclaw-skill/skills/my-skill \
|
||
~/.openclaw/workspace/skills/
|
||
systemctl --user restart openclaw-gateway
|
||
```
|
||
|
||
### 除錯
|
||
|
||
```bash
|
||
# 查看 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 時,複製此模板:
|
||
|
||
```bash
|
||
SKILL_NAME="my-new-skill"
|
||
mkdir -p ~/openclaw-skill/skills/$SKILL_NAME
|
||
```
|
||
|
||
**SKILL.md**:
|
||
```yaml
|
||
---
|
||
name: my-new-skill
|
||
description: 一句話描述功能
|
||
triggers:
|
||
- "觸發詞1"
|
||
- "trigger2"
|
||
tools:
|
||
- exec
|
||
---
|
||
|
||
# My New Skill
|
||
|
||
## 功能說明
|
||
做什麼、怎麼觸發。
|
||
|
||
## 觸發範例
|
||
使用者:「觸發詞1 某些參數」→ skill 做某事 → 回覆結果
|
||
```
|
||
|
||
**handler.ts**:
|
||
```typescript
|
||
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 },
|
||
};
|
||
}
|
||
```
|
||
|
||
**安裝**:
|
||
```bash
|
||
cp -r ~/openclaw-skill/skills/$SKILL_NAME ~/.openclaw/workspace/skills/
|
||
systemctl --user restart openclaw-gateway
|
||
openclaw skills list # 確認 ✓ ready
|
||
```
|