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.
This commit is contained in:
2026-03-13 10:58:30 +08:00
commit 4c966a3ad2
884 changed files with 140761 additions and 0 deletions

482
create-skill.md Normal file
View File

@@ -0,0 +1,482 @@
# 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
```