# 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; // 呼叫其他 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 ```