OpenClaw 技能开发完全指南:从入门到精通,打造你的专属 AI 工具箱
【AI前线导读】 想让你的 OpenClaw 更强大?本文手把手教你开发自定义技能,从零开始构建专属 AI 工具。涵盖技能架构设计、开发实战、调试技巧、发布流程,附完整代码示例和最佳实践。5000字深度长文,助你成为 OpenClaw 技能开发专家。
引言:为什么你需要自定义技能?
OpenClaw 作为新一代 AI Agent 框架,内置了丰富的工具能力。但真正的威力在于可扩展性——通过自定义技能,你可以:
• 连接私有系统 - 对接企业内部 API、数据库、消息队列• 封装专业工具 - 将领域知识固化成可复用的技能模块• 提升工作效率 - 自动化重复任务,一键完成复杂操作• 打造个人品牌 - 发布到 ClawHub,与社区共享成果
本文将带你完整走一遍技能开发全流程,从概念到落地,从代码到发布。
第一章:OpenClaw 技能架构解析
1.1 什么是技能(Skill)?
在 OpenClaw 中,技能是可插拔的功能模块,遵循统一的接口规范。一个技能通常包含:
my-skill/ ├── SKILL.md # 技能说明书(必需) ├── package.json # Node.js 项目配置 ├── src/ │ ├── index.ts # 主入口文件 │ └── types.ts # 类型定义 ├── dist/ # 编译输出 └── README.md # 详细文档1.2 技能的生命周期
发现 Discovery → 加载 Load → 初始化 Initialize → 执行 Execute → 卸载 Unload关键阶段说明:
1.3 技能与工具的关系
技能(Skill) 是容器,工具(Tool) 是具体功能。一个技能可以暴露多个工具:
// 技能暴露的工具示例 export const tools = { // 工具1:查询天气 'weather.get': async (params) => { /* ... */ }, // 工具2:获取预报 'weather.forecast': async (params) => { /* ... */ }, // 工具3:设置提醒 'weather.alert': async (params) => { /* ... */ } };第二章:开发环境搭建
2.1 前置要求
# 检查 OpenClaw 版本 openclaw --version # 要求 >= 2026.3.0 # 检查 Node.js node --version # 要求 >= 18.0.0 # 检查 TypeScript(推荐) npm install -g typescript2.2 创建技能目录
# 进入 OpenClaw 技能目录 cd ~/.openclaw/skills # 创建新技能 mkdir my-first-skill cd my-first-skill # 初始化项目 npm init -y2.3 安装依赖
# 核心依赖 npm install uuid # 开发依赖 npm install --save-dev typescript @types/node @types/uuid2.4 配置 TypeScript
创建 tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }第三章:Hello World 技能实战
3.1 创建 SKILL.md
这是技能的"身份证",必须放在根目录:
--- name: hello-world slug: hello-world version: 1.0.0 description: 我的第一个 OpenClaw 技能 - 向世界问好 author: Your Name license: MIT --- # Hello World Skill 一个简单的示例技能,演示 OpenClaw 技能开发基础。 ## 功能 - 👋 打招呼 - 向指定对象问好 - ⏰ 获取时间 - 返回当前时间 - 📊 统计 - 记录调用次数 ## 使用方法 ```javascript // 打招呼 await tools['hello-world.greet']({ name: 'OpenClaw' }); // 获取时间 await tools['hello-world.time'](); // 查看统计 await tools['hello-world.stats']();License
MIT
### 3.2 编写核心代码 创建 `src/index.ts`: ```typescript /** * Hello World 技能 - OpenClaw 开发入门示例 */ import { v4 as uuidv4 } from 'uuid'; // 技能状态(内存中) interface SkillState { callCount: number; startTime: number; greetings: string[]; } // 初始化状态 const state: SkillState = { callCount: 0, startTime: Date.now(), greetings: [] }; // 工具1:打招呼 export async function greet(params: { name: string; language?: string }): Promise { state.callCount++; const { name, language = 'zh' } = params; const greetings: Record = { zh: `你好,${name}!欢迎使用 OpenClaw 👋`, en: `Hello, ${name}! Welcome to OpenClaw 👋`, jp: `こんにちは、${name}さん!OpenClaw へようこそ 👋`, fr: `Bonjour, ${name}! Bienvenue sur OpenClaw 👋` }; const message = greetings[language] || greetings.zh; state.greetings.push({ name, language, time: new Date().toISOString() }); return message; } // 工具2:获取当前时间 export async function getTime(params?: { timezone?: string }): Promise<{ timestamp: number; iso: string; formatted: string; timezone: string; }> { state.callCount++; const now = new Date(); const timezone = params?.timezone || 'Asia/Shanghai'; return { timestamp: now.getTime(), iso: now.toISOString(), formatted: now.toLocaleString('zh-CN', { timeZone: timezone }), timezone }; } // 工具3:获取统计信息 export async function getStats(): Promise<{ callCount: number; uptime: number; uptimeFormatted: string; greetings: string[]; }> { state.callCount++; const uptime = Date.now() - state.startTime; const hours = Math.floor(uptime / 3600000); const minutes = Math.floor((uptime % 3600000) / 60000); const seconds = Math.floor((uptime % 60000) / 1000); return { callCount: state.callCount, uptime, uptimeFormatted: `${hours}h ${minutes}m ${seconds}s`, greetings: state.greetings.slice(-10) // 最近10条 }; } // 导出工具映射(OpenClaw 会读取这个) export const tools = { 'hello-world.greet': greet, 'hello-world.time': getTime, 'hello-world.stats': getStats }; // 默认导出 export default { tools };3.3 编译代码
# 编译 TypeScript npm run build # 或手动编译 npx tsc3.4 测试技能
# 进入 OpenClaw 工作目录 cd ~/.openclaw/workspace # 创建测试脚本 cat > test-skill.js << 'EOF' const skill = require('../skills/my-first-skill/dist/index.js'); async function test() { console.log('=== 测试 Hello World 技能 ===\n'); // 测试打招呼 console.log('1. 打招呼(中文):'); const greeting = await skill.tools['hello-world.greet']({ name: 'OpenClaw' }); console.log(greeting); console.log('\n2. 打招呼(英文):'); const greetingEn = await skill.tools['hello-world.greet']({ name: 'World', language: 'en' }); console.log(greetingEn); // 测试获取时间 console.log('\n3. 获取时间:'); const time = await skill.tools['hello-world.time'](); console.log(time); // 测试统计 console.log('\n4. 查看统计:'); const stats = await skill.tools['hello-world.stats'](); console.log(stats); } test().catch(console.error); EOF # 运行测试 node test-skill.js第四章:进阶技能开发 - 实战案例
4.1 案例:Web 搜索技能
实现一个聚合多个搜索引擎的技能:
/** * Web Search 技能 - 多引擎搜索聚合 */ import * as https from 'https'; interface SearchResult { title: string; url: string; snippet: string; source: string; } // Bing 搜索 async function searchBing(query: string, count: number = 5): Promise { const apiKey = process.env.BING_API_KEY; if (!apiKey) { throw new Error('BING_API_KEY not set'); } const url = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(query)}&count=${count}`; return new Promise((resolve, reject) => { const req = https.get(url, { headers: { 'Ocp-Apim-Subscription-Key': apiKey } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const result = JSON.parse(data); const items = result.webPages?.value || []; resolve(items.map((item: any) => ({ title: item.name, url: item.url, snippet: item.snippet, source: 'Bing' }))); } catch (e) { reject(e); } }); }); req.on('error', reject); }); } // Tavily 搜索(AI 搜索) async function searchTavily(query: string, count: number = 5): Promise { const apiKey = process.env.TAVILY_API_KEY; if (!apiKey) { throw new Error('TAVILY_API_KEY not set'); } // 实现略... return []; } // 聚合搜索工具 export async function search(params: { query: string; engine?: 'bing' | 'tavily' | 'all'; count?: number; }): Promise<{ query: string; results results: SearchResult[]; total: number; sources: string[]; }> { const { query, engine = 'bing', count = 5 } = params; let results: SearchResult[] = []; const sources: string[] = []; if (engine === 'bing' || engine === 'all') { const bingResults = await searchBing(query, count); results = results.concat(bingResults); sources.push('Bing'); } if (engine === 'tavily' || engine === 'all') { const tavilyResults = await searchTavily(query, count); results = results.concat(tavilyResults); sources.push('Tavily'); } // 去重并排序 const seen = new Set (); results = results.filter(r => { if (seen.has(r.url)) return false; seen.add(r.url); return true; }); return { query, results: results.slice(0, count), total: results.length, sources }; } export const tools = { 'web-search.search': search };4.2 案例:文件处理技能
实现文件读写、格式转换等工具:
/** * File Utils 技能 - 文件处理工具集 */ import * as fs from 'fs/promises'; import * as path from 'path'; // 读取文件 export async function readFile(params: { path: string; encoding?: 'utf8' | 'base64'; }): Promise<{ content: string; size: number; encoding: string; }> { const { path: filePath, encoding = 'utf8' } = params; // 安全检查:限制在工作目录 const resolvedPath = path.resolve(filePath); const workspaceRoot = process.env.OPENCLAW_WORKSPACE || process.cwd(); if (!resolvedPath.startsWith(workspaceRoot)) { throw new Error('Access denied: file outside workspace'); } const content = await fs.readFile(resolvedPath, encoding); const stats = await fs.stat(resolvedPath); return { content, size: stats.size, encoding }; } // 写入文件 export async function writeFile(params: { path: string; content: string; encoding?: 'utf8' | 'base64'; }): Promise<{ path: string; size: number; success: boolean; }> { const { path: filePath, content, encoding = 'utf8' } = params; const resolvedPath = path.resolve(filePath); const workspaceRoot = process.env.OPENCLAW_WORKSPACE || process.cwd(); if (!resolvedPath.startsWith(workspaceRoot)) { throw new Error('Access denied: file outside workspace'); } await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); await fs.writeFile(resolvedPath, content, encoding); const stats = await fs.stat(resolvedPath); return { path: resolvedPath, size: stats.size, success: true }; } // 列出目录 export async function listDir(params: { path: string; recursive?: boolean; }): Promise<{ path: string; items: Array<{ name: string; type: 'file' | 'directory'; size: number; modified: string; }>; }> { const { path: dirPath, recursive = false } = params; const entries = await fs.readdir(dirPath, { withFileTypes: true, recursive }); const items = await Promise.all( entries.map(async (entry) => { const fullPath = path.join(dirPath, entry.name); const stats = await fs.stat(fullPath); return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', size: stats.size, modified: stats.mtime.toISOString() }; }) ); return { path: dirPath, items }; } export const tools = { 'file.read': readFile, 'file.write': writeFile, 'file.list': listDir };第五章:调试与测试技巧
5.1 本地调试
# 1. 使用 tsx 直接运行(无需编译) npx tsx src/index.ts # 2. 启用详细日志 DEBUG=openclaw:* npx tsx src/index.ts # 3. 使用 Node.js 调试器 node --inspect-brk dist/index.js5.2 单元测试
创建 test/skill.test.ts:
import { describe, it, expect } from 'vitest'; import { greet, getTime, getStats } from '../src/index'; describe('Hello World Skill', () => { it('should greet in Chinese by default', async () => { const result = await greet({ name: 'Test' }); expect(result).toContain('你好'); expect(result).toContain('Test'); }); it('should greet in English', async () => { const result = await greet({ name: 'Test', language: 'en' }); expect(result).toContain('Hello'); }); it('should return valid time', async () => { const result = await getTime(); expect(result.timestamp).toBeGreaterThan(0); expect(result.iso).toMatch(/^\d{4}-\d{2}-\d{2}/); }); });5.3 集成测试
# 在 OpenClaw 环境中测试 openclaw tool call hello-world.greet '{"name": "Integration Test"}'第六章:发布与分享
6.1 准备发布
# 1. 确保代码已编译 npm run build # 2. 更新版本号 npm version patch # 或 minor/major # 3. 更新 CHANGELOG.md # 4. 提交到 Git git add . git commit -m "Release v1.0.0" git tag v1.0.0 git push origin main --tags6.2 发布到 ClawHub
# 使用 ClawHub CLI(如果已安装) clawhub publish # 或手动打包 tar -czvf my-skill-v1.0.0.tar.gz \ SKILL.md package.json README.md \ dist/ src/6.3 文档规范
好的技能文档应包含:
第七章:最佳实践
7.1 代码规范
// ✅ 好的做法:清晰的参数命名 export async function sendEmail(params: { to: string; subject: string; body: string; attachments?: string[]; }) { } // ❌ 避免:模糊的参数名 export async function sendEmail(p: any) { } // ✅ 好的做法:详细的错误信息 throw new Error('Email sending failed: SMTP server unreachable'); // ❌ 避免:模糊的错误 throw new Error('Error');7.2 性能优化
// 使用缓存 const cache = new Map(); export async function fetchWithCache(params: { url: string }) { if (cache.has(params.url)) { return cache.get(params.url); } const result = await fetch(params.url); cache.set(params.url, result); // 限制缓存大小 if (cache.size > 100) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } return result; } // 使用连接池 import { Pool } from 'pg'; const pool = new Pool({ /* config */ });7.3 安全注意事项
// ✅ 验证输入 function sanitizePath(inputPath: string): string { const resolved = path.resolve(inputPath); const allowedRoot = process.env.OPENCLAW_WORKSPACE || '/workspace'; if (!resolved.startsWith(allowedRoot)) { throw new Error('Path traversal detected'); } return resolved; } // ✅ 敏感信息使用环境变量 const apiKey = process.env.API_KEY; if (!apiKey) { throw new Error('API_KEY not configured'); } // ❌ 避免硬编码密钥 const apiKey = 'sk-1234567890abcdef'; // 危险!第八章:常见问题 FAQ
Q1:技能加载失败怎么办?
检查清单:
SKILL.md 是否在根目录? package.json 是否存在? 代码是否已编译(dist/ 目录是否存在)? 依赖是否已安装?
Q2:如何调试技能?
# 启用 OpenClaw 调试日志 openclaw logs --follow --level debug # 在技能代码中添加日志 console.log('[MySkill]', 'Debug info:', data);Q3:技能之间可以互相调用吗?
// 可以!通过 OpenClaw 运行时 export async function myTool(params: any) { // 调用其他技能的工具 const result = await runtime.tools['other-skill.tool'](params); return result; }Q4:如何处理异步任务?
// 使用 Promise export async function longRunningTask(params: any) { return new Promise((resolve, reject) => { // 异步操作 setTimeout(() => { resolve({ success: true }); }, 5000); }); }结语:开启你的技能开发之旅
OpenClaw 技能开发并不复杂,关键在于:理解架构 → 动手实践 → 持续迭代。
本文从基础概念到实战案例,从调试技巧到发布流程,为你提供了完整的技能开发指南。现在,是时候动手创建你的第一个技能了!
下一步行动
- 从 Hello World 开始
- 按照第三章完成你的第一个技能 - 参考现有技能
- 学习 ~/.openclaw/skills/ 目录下的开源技能 - 加入社区
- 在 Discord/GitHub 与其他开发者交流 - 分享成果
- 将你的技能发布到 ClawHub,帮助更多人
资源链接
官方文档: https://docs.openclaw.ai
技能仓库: https://clawhub.com
GitHub: https://github.com/openclaw/openclaw
Discord: https:// results: SearchResult[];
total: number;sources: string[];
}> { const { query, engine = 'bing', count = 5 } = params;
let results: SearchResult[] = []; const sources: string[] = [];
if (engine === 'bing' || engine === 'all') { const bingResults = await searchBing(query, count); results = results.concat(bingResults); sources.push('Bing'); }
if (engine === 'tavily' || engine === 'all') { const tavilyResults = await searchTavily(query, count); results = results.concat(tavilyResults); sources.push('Tavily'); }
// 去重并排序 const seen = new Set
return { query, results: results.slice(0, count), total: results.length, sources };}
export const tools = { 'web-search.search': search};
### 4.2 案例:文件处理技能 实现文件读写、格式转换等工具: ```typescript /** * File Utils 技能 - 文件处理工具集 */ import * as fs from 'fs/promises'; import * as path from 'path'; // 读取文件 export async function readFile(params: { path: string; encoding?: 'utf8' | 'base64'; }): Promise<{ content: string; size: number; encoding: string; }> { const { path: filePath, encoding = 'utf8' } = params; // 安全检查:限制在工作目录 const resolvedPath = path.resolve(filePath); const workspaceRoot = process.env.OPENCLAW_WORKSPACE || process.cwd(); if (!resolvedPath.startsWith(workspaceRoot)) { throw new Error('Access denied: file outside workspace'); } const content = await fs.readFile(resolvedPath, encoding); const stats = await fs.stat(resolvedPath); return { content, size: stats.size, encoding }; } // 写入文件 export async function writeFile(params: { path: string; content: string; encoding?: 'utf8' | 'base64'; }): Promise<{ path: string; size: number; success: boolean; }> { const { path: filePath, content, encoding = 'utf8' } = params; const resolvedPath = path.resolve(filePath); const workspaceRoot = process.env.OPENCLAW_WORKSPACE || process.cwd(); if (!resolvedPath.startsWith(workspaceRoot)) { throw new Error('Access denied: file outside workspace'); } await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); await fs.writeFile(resolvedPath, content, encoding); const stats = await fs.stat(resolvedPath); return { path: resolvedPath, size: stats.size, success: true }; } // 列出目录 export async function listDir(params: { path: string; recursive?: boolean; }): Promise<{ path: string; items: Array<{ name: string; type: 'file' | 'directory'; size: number; modified: string; }>; }> { const { path: dirPath, recursive = false } = params; const entries = await fs.readdir(dirPath, { withFileTypes: true, recursive }); const items = await Promise.all( entries.map(async (entry) => { const fullPath = path.join(dirPath, entry.name); const stats = await fs.stat(fullPath); return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', size: stats.size, modified: stats.mtime.toISOString() }; }) ); return { path: dirPath, items }; } export const tools = { 'file.read': readFile, 'file.write': writeFile, 'file.list': listDir };第五章:调试与测试技巧
5.1 本地调试
# 1. 使用 tsx 直接运行(无需编译) npx tsx src/index.ts # 2. 启用详细日志 DEBUG=openclaw:* npx tsx src/index.ts # 3. 使用 Node.js 调试器 node --inspect-brk dist/index.js5.2 单元测试
创建 test/skill.test.ts:
import { describe, it, expect } from 'vitest'; import { greet, getTime, getStats } from '../src/index'; describe('Hello World Skill', () => { it('should greet in Chinese by default', async () => { const result = await greet({ name: 'Test' }); expect(result).toContain('你好'); expect(result).toContain('Test'); }); it('should greet in English', async () => { const result = await greet({ name: 'Test', language: 'en' }); expect(result).toContain('Hello'); }); it('should return valid time', async () => { const result = await getTime(); expect(result.timestamp).toBeGreaterThan(0); expect(result.iso).toMatch(/^\d{4}-\d{2}-\d{2}/); }); });5.3 集成测试
# 在 OpenClaw 环境中测试 openclaw tool call hello-world.greet '{"name": "Integration Test"}'第六章:发布与分享
6.1 准备发布
# 1. 确保代码已编译 npm run build # 2. 更新版本号 npm version patch # 或 minor/major # 3. 更新 CHANGELOG.md # 4. 提交到 Git git add . git commit -m "Release v1.0.0" git tag v1.0.0 git push origin main --tags6.2 发布到 ClawHub
# 使用 ClawHub CLI(如果已安装) clawhub publish # 或手动打包 tar -czvf my-skill-v1.0.0.tar.gz \ SKILL.md package.json README.md \ dist/ src/6.3 文档规范
好的技能文档应包含:
第七章:最佳实践
7.1 代码规范
// ✅ 好的做法:清晰的参数命名 export async function sendEmail(params: { to: string; subject: string; body: string; attachments?: string[]; }) { } // ❌ 避免:模糊的参数名 export async function sendEmail(p: any) { } // ✅ 好的做法:详细的错误信息 throw new Error('Email sending failed: SMTP server unreachable'); // ❌ 避免:模糊的错误 throw new Error('Error');7.2 性能优化
// 使用缓存 const cache = new Map(); export async function fetchWithCache(params: { url: string }) { if (cache.has(params.url)) { return cache.get(params.url); } const result = await fetch(params.url); cache.set(params.url, result); // 限制缓存大小 if (cache.size > 100) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } return result; } // 使用连接池 import { Pool } from 'pg'; const pool = new Pool({ /* config */ });7.3 安全注意事项
// ✅ 验证输入 function sanitizePath(inputPath: string): string { const resolved = path.resolve(inputPath); const allowedRoot = process.env.OPENCLAW_WORKSPACE || '/workspace'; if (!resolved.startsWith(allowedRoot)) { throw new Error('Path traversal detected'); } return resolved; } // ✅ 敏感信息使用环境变量 const apiKey = process.env.API_KEY; if (!apiKey) { throw new Error('API_KEY not configured'); } // ❌ 避免硬编码密钥 const apiKey = 'sk-1234567890abcdef'; // 危险!第八章:常见问题 FAQ
Q1:技能加载失败怎么办?
检查清单:
SKILL.md 是否在根目录? package.json 是否存在? 代码是否已编译(dist/ 目录是否存在)? 依赖是否已安装?
Q2:如何调试技能?
# 启用 OpenClaw 调试日志 openclaw logs --follow --level debug # 在技能代码中添加日志 console.log('[MySkill]', 'Debug info:', data);Q3:技能之间可以互相调用吗?
// 可以!通过 OpenClaw 运行时 export async function myTool(params: any) { // 调用其他技能的工具 const result = await runtime.tools['other-skill.tool'](params); return result; }Q4:如何处理异步任务?
// 使用 Promise export async function longRunningTask(params: any) { return new Promise((resolve, reject) => { // 异步操作 setTimeout(() => { resolve({ success: true }); }, 5000); }); }结语:开启你的技能开发之旅
OpenClaw 技能开发并不复杂,关键在于:理解架构 → 动手实践 → 持续迭代。
本文从基础概念到实战案例,从调试技巧到发布流程,为你提供了完整的技能开发指南。现在,是时候动手创建你的第一个技能了!
下一步行动
- 从 Hello World 开始
- 按照第三章完成你的第一个技能 - 参考现有技能
- 学习 ~/.openclaw/skills/ 目录下的开源技能 - 加入社区
- 在 Discord/GitHub 与其他开发者交流 - 分享成果
- 将你的技能发布到 ClawHub,帮助更多人
资源链接
- 官方文档
: https://docs.openclaw.ai - 技能仓库
: https://clawhub.com - GitHub
: https://github.com/openclaw/openclaw - Discord
: https://discord.com/invite/clawd
本文首发于微信公众号「Alman」,转载请注明出处。
作者:Alman | 编辑:AI前线 | 发布时间:2025年3月
夜雨聆风