Files
openclaw-skill/create-skill.md
Selig 4c966a3ad2 Initial commit: OpenClaw Skill Collection
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.
2026-03-13 10:58:30 +08:00

483 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 訊息過長
---
### 範例 DInternal 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
```