1. OpenClaw 自定义 Skills 开发实战指南
OpenClaw 最吸引人的地方在于它的可扩展性。作为一个长期使用 OpenClaw 的开发者,我发现官方预置的基础能力虽然实用,但真正让这个工具发挥价值的,是能够根据个人需求定制专属技能。本文将分享我从零开始开发 OpenClaw 自定义 Skills 的完整经验,包含两个典型实战案例和大量避坑技巧。
1.1 为什么需要自定义 Skills?
在日常工作中,我们经常会遇到一些重复性的任务:
- 每周五下午需要整理项目文档并生成周报
- 需要从多个数据源提取信息制作汇总报表
- 要定期检查服务器日志中的异常情况
这些任务如果手动完成,不仅耗时而且容易出错。虽然 OpenClaw 提供了许多内置功能,但面对这些个性化需求时,自定义 Skills 就成为了最佳解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境准备
2.1 基础工具安装
在开始开发前,需要确保系统环境配置正确。以下是详细的安装步骤:
bash复制# 安装 Node.js(推荐使用 nvm 管理版本)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
nvm install 18
nvm use 18
# 验证安装
node -v
npm -v
注意:Node.js 版本必须 ≥18,因为 OpenClaw 使用了部分较新的 JavaScript 特性。
2.2 OpenClaw 本地部署
如果还没有运行 OpenClaw,可以按照以下步骤快速搭建本地环境:
bash复制# 克隆 OpenClaw 仓库
git clone https://github.com/openclaw/core.git
cd core
# 安装依赖
npm install
# 启动服务
npm start
启动后,可以通过 http://localhost:3000 访问 OpenClaw 的 Web 界面。
3. 第一个 Skill:文件统计报表生成器
3.1 项目初始化
创建一个新的 Skill 项目:
bash复制mkdir file-report-skill
cd file-report-skill
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
3.2 核心代码实现
plugin.json 是 Skill 的配置文件,定义了 Skill 的基本信息和权限:
json复制{
"name": "file-report-skill",
"version": "1.0.0",
"description": "统计指定目录的文件类型和数量",
"skills": [
{
"action": "generate-file-report",
"parameters": [
{
"name": "dirPath",
"type": "string",
"required": true
}
],
"permissions": ["file.read", "file.write"]
}
]
}
index.ts 包含核心业务逻辑:
typescript复制import fs from 'fs';
import path from 'path';
export default async function run(action: string, params: any) {
if (action !== 'generate-file-report') {
return { success: false, message: '不支持的 action' };
}
const { dirPath } = params;
try {
const stats = await getFileStats(dirPath);
const report = generateReport(stats, dirPath);
return {
success: true,
message: '报表生成成功',
data: { report }
};
} catch (error) {
return {
success: false,
message: `错误: ${error.message}`,
data: null
};
}
}
3.3 部署与测试
将开发好的 Skill 部署到 OpenClaw:
bash复制# 编译 TypeScript
npx tsc
# 复制到 OpenClaw 的 skills 目录
cp -r ./dist /path/to/openclaw/skills/file-report-skill
重启 OpenClaw 后,就可以通过指令调用这个 Skill 了。
4. 进阶 Skill:快递查询接口
4.1 第三方 API 集成
这个 Skill 需要连接快递100的API,首先安装必要的依赖:
bash复制npm install axios
npm install @types/axios --save-dev
4.2 实现 API 调用
核心的快递查询功能实现:
typescript复制import axios from 'axios';
async function queryExpress(expressNo: string, apiKey: string) {
const response = await axios.get('https://www.kuaidi100.com/api', {
params: {
type: 'auto',
postid: expressNo,
key: apiKey
}
});
if (response.data.status !== '200') {
throw new Error(response.data.message);
}
return response.data;
}
4.3 错误处理与重试机制
为了提高稳定性,我们增加了错误处理和自动重试:
typescript复制async function queryWithRetry(expressNo: string, apiKey: string, retries = 3) {
let lastError;
for (let i = 0; i < retries; i++) {
try {
return await queryExpress(expressNo, apiKey);
} catch (error) {
lastError = error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
throw lastError;
}
5. 调试与优化技巧
5.1 本地测试方法
在部署前,可以编写测试脚本验证 Skill 功能:
typescript复制// test.ts
import run from './dist/index.js';
async function test() {
const result = await run('generate-file-report', {
dirPath: '/path/to/test'
});
console.log(result);
}
test();
5.2 性能优化建议
对于频繁调用的 Skill,可以考虑以下优化措施:
- 缓存机制:对不常变的数据添加缓存
- 批量处理:支持一次处理多个请求
- 懒加载:延迟初始化耗资源的模块
typescript复制// 示例:简单的内存缓存
const cache = new Map();
async function getWithCache(key: string, fn: () => Promise<any>) {
if (cache.has(key)) {
return cache.get(key);
}
const result = await fn();
cache.set(key, result);
return result;
}
6. 安全最佳实践
6.1 输入验证
所有外部输入都必须经过严格验证:
typescript复制function validateDirPath(path: string) {
if (!path || typeof path !== 'string') {
throw new Error('路径不能为空');
}
if (path.includes('../') || path.startsWith('/etc')) {
throw new Error('非法路径');
}
}
6.2 权限控制
遵循最小权限原则,只申请必要的权限:
json复制{
"permissions": [
"file.read" // 只读权限
]
}
7. 项目结构优化
7.1 模块化组织
随着 Skill 复杂度增加,建议采用模块化结构:
code复制my-skill/
├── src/
│ ├── core/ # 核心逻辑
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ └── index.ts # 入口文件
├── tests/ # 测试代码
├── plugin.json # Skill 配置
└── package.json # 项目配置
7.2 配置管理
将配置信息从代码中分离:
typescript复制// config.ts
interface Config {
apiEndpoint: string;
timeout: number;
}
export const config: Config = {
apiEndpoint: process.env.API_ENDPOINT || 'https://api.example.com',
timeout: parseInt(process.env.TIMEOUT || '5000')
};
8. 持续集成与部署
8.1 自动化测试
在 CI 流程中添加自动化测试:
yaml复制# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm test
8.2 自动部署
使用脚本自动部署到 OpenClaw:
bash复制#!/bin/bash
# 构建
npm run build
# 同步到 OpenClaw 目录
rsync -avz ./dist/ user@server:/opt/openclaw/skills/my-skill/
# 重启 OpenClaw
ssh user@server "systemctl restart openclaw"
9. 监控与日志
9.1 添加详细日志
typescript复制import { createLogger, transports, format } from 'winston';
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new transports.File({ filename: 'skill.log' })
]
});
export default logger;
9.2 性能监控
记录关键操作的执行时间:
typescript复制async function trackPerformance<T>(name: string, fn: () => Promise<T>) {
const start = Date.now();
try {
const result = await fn();
const duration = Date.now() - start;
logger.info(`[Perf] ${name} completed in ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - start;
logger.error(`[Perf] ${name} failed after ${duration}ms`, { error });
throw error;
}
}
10. 扩展与进阶
10.1 多语言支持
为 Skill 添加国际化支持:
typescript复制// i18n.ts
interface Translations {
[key: string]: {
en: string;
zh: string;
};
}
const translations: Translations = {
welcome: {
en: "Welcome",
zh: "欢迎"
}
};
export function t(key: string, lang = 'en') {
return translations[key]?.[lang] || key;
}
10.2 插件系统
设计可扩展的插件架构:
typescript复制interface Plugin {
name: string;
init: (context: PluginContext) => void;
}
class PluginManager {
private plugins: Plugin[] = [];
register(plugin: Plugin) {
this.plugins.push(plugin);
}
initialize(context: PluginContext) {
this.plugins.forEach(plugin => plugin.init(context));
}
}
11. 实战经验分享
在开发自定义 Skills 的过程中,我积累了一些宝贵经验:
- 保持单一职责:每个 Skill 应该只做一件事,并把它做好
- 完善的错误处理:考虑所有可能的失败情况
- 详细的文档:为每个 Skill 编写清晰的文档
- 版本控制:使用语义化版本控制 Skill 的变更
markdown复制# File Report Skill 文档
## 功能
统计指定目录下的文件类型分布
## 使用方法
执行 generate-file-report,参数:
code复制
## 参数
- dirPath: 要统计的目录路径(必须存在)
12. 常见问题解决
12.1 Skill 加载失败
可能原因及解决方案:
- plugin.json 格式错误:使用 JSON 验证工具检查
- 权限不足:检查文件权限和 OpenClaw 配置
- 依赖缺失:确保所有依赖已正确安装
12.2 性能问题
优化建议:
- 使用流式处理大文件
- 避免同步 IO 操作
- 对重复计算添加缓存
typescript复制// 使用流读取大文件
function countLines(filePath: string) {
return new Promise((resolve, reject) => {
let count = 0;
fs.createReadStream(filePath)
.on('data', chunk => {
for (const char of chunk.toString()) {
if (char === '\n') count++;
}
})
.on('end', () => resolve(count))
.on('error', reject);
});
}
13. 测试策略
13.1 单元测试
使用 Jest 编写单元测试:
typescript复制// countFiles.test.ts
import { countFilesByType } from './countFiles';
describe('countFilesByType', () => {
it('应该正确统计文件类型', async () => {
const stats = await countFilesByType('./test-fixtures');
expect(stats['.txt']).toBe(3);
});
});
13.2 集成测试
验证整个 Skill 的工作流程:
typescript复制// integration.test.ts
import run from '../dist/index';
describe('File Report Skill', () => {
it('应该成功生成报表', async () => {
const result = await run('generate-file-report', {
dirPath: './test-fixtures'
});
expect(result.success).toBe(true);
expect(result.data.report).toContain('文件统计报表');
});
});
14. 发布与分享
14.1 打包发布
创建可发布的 Skill 包:
bash复制# 打包所有必要文件
tar -czvf file-report-skill-1.0.0.tar.gz \
dist/ \
plugin.json \
package.json \
README.md
14.2 版本更新
遵循语义化版本控制:
- MAJOR:不兼容的 API 修改
- MINOR:向后兼容的功能新增
- PATCH:向后兼容的问题修正
json复制{
"name": "file-report-skill",
"version": "1.1.0", // 新增功能
"dependencies": {
"lodash": "^4.17.21" // 使用兼容版本范围
}
}
15. 性能监控与优化
15.1 关键指标监控
监控 Skill 的关键性能指标:
typescript复制interface Metrics {
invocationCount: number;
successCount: number;
errorCount: number;
avgDuration: number;
}
const metrics: Record<string, Metrics> = {};
function trackInvocation(action: string, duration: number, success: boolean) {
if (!metrics[action]) {
metrics[action] = {
invocationCount: 0,
successCount: 0,
errorCount: 0,
avgDuration: 0
};
}
const m = metrics[action];
m.invocationCount++;
if (success) m.successCount++; else m.errorCount++;
m.avgDuration = (m.avgDuration * (m.invocationCount - 1) + duration) / m.invocationCount;
}
15.2 内存管理
避免内存泄漏的实践:
typescript复制// 使用 WeakMap 存储临时数据
const tempData = new WeakMap<object, any>();
function processData(data: any) {
const context = {};
tempData.set(context, heavyProcessing(data));
// 当 context 不再被引用时,相关数据会自动被 GC 回收
return doWork(context);
}
16. 安全加固
16.1 输入消毒
对所有输入数据进行消毒处理:
typescript复制function sanitizeInput(input: string) {
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
16.2 安全审计
使用工具进行安全扫描:
bash复制# 使用 npm audit 检查依赖漏洞
npm audit
# 使用 snyk 进行深度扫描
npx snyk test
17. 用户体验优化
17.1 进度反馈
对于耗时操作,提供进度反馈:
typescript复制async function longRunningTask(onProgress: (percent: number) => void) {
const total = 100;
for (let i = 0; i <= total; i++) {
await doChunkOfWork();
onProgress((i / total) * 100);
}
}
17.2 结果格式化
将原始数据转换为易读格式:
typescript复制function formatFileSize(bytes: number) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
18. 调试技巧
18.1 远程调试
配置远程调试支持:
json复制{
"scripts": {
"debug": "node --inspect=9229 dist/index.js"
}
}
18.2 日志分级
实现分级的日志系统:
typescript复制enum LogLevel {
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4
}
class Logger {
constructor(public level: LogLevel) {}
log(level: LogLevel, message: string) {
if (level <= this.level) {
console.log(`[${LogLevel[level]}] ${message}`);
}
}
}
19. 性能基准测试
19.1 基准测试工具
使用 benchmark.js 进行性能测试:
typescript复制import Benchmark from 'benchmark';
const suite = new Benchmark.Suite;
suite
.add('RegExp#test', () => /o/.test('Hello World!'))
.add('String#indexOf', () => 'Hello World!'.indexOf('o') > -1)
.on('cycle', (event: any) => {
console.log(String(event.target));
})
.run();
19.2 内存分析
使用内存分析工具检测泄漏:
bash复制node --inspect --expose-gc ./your-script.js
然后在 Chrome DevTools 中检查内存使用情况。
20. 持续学习与改进
开发高质量的 OpenClaw Skills 是一个持续改进的过程。建议:
- 定期回顾代码,寻找优化机会
- 关注 OpenClaw 社区的优秀案例
- 学习新的 TypeScript/JavaScript 特性
- 收集用户反馈,持续迭代改进
typescript复制// 示例:使用新特性优化代码
function newFeaturesDemo() {
// 使用可选链
const name = user?.profile?.name ?? 'Anonymous';
// 使用空值合并
const pageSize = input.pageSize ?? 10;
// 使用 Promise.allSettled
const results = await Promise.allSettled(promises);
}
