手把手教你开发OpenClaw插件:从零到发布
为什么要开发OpenClaw插件?
OpenClaw本身已经很强大了,但每个团队、每个用户都有独特的需求。通过插件,你可以:
1. 扩展功能:添加OpenClaw原生不支持的能力
2. 集成外部系统:连接企业内部工具(CRM、ERP、OA等)
3. 自动化工作流:创建定制化的自动化流程
4. 数据转换:将数据转换为特定格式
5. 分享给社区:贡献你的插件,帮助其他用户
本文将带你从零开始,开发一个完整的OpenClaw插件,并发布到官方插件市场。
OpenClaw插件架构解析
插件系统架构
┌─────────────────────────────────────────┐
│ OpenClaw核心 │
├─────────────────────────────────────────┤
│ 插件管理器 (Plugin Manager) │
├─────────────────────────────────────────┤
│ 插件A │ 插件B │ 插件C │ 插件D │
└─────────────────────────────────────────┘
插件类型
1. 工具插件:添加新的工具能力(如天气查询、股票分析)
2. 集成插件:连接外部API或服务
3. 处理器插件:处理特定类型的数据
4. UI插件:添加新的用户界面组件
5. 工作流插件:定义复杂的自动化流程
第一步:开发环境搭建
1.1 安装Node.js和npm
检查Node.js版本
node --version # 需要 >= 18.0.0
npm --version # 需要 >= 9.0.0
如果未安装,使用nvm安装
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18
1.2 创建插件开发目录
创建插件项目
mkdir openclaw-plugin-weather
cd openclaw-plugin-weather
初始化npm项目
npm init -y
安装OpenClaw插件开发工具包
npm install @openclaw/plugin-sdk typescript @types/node --save-dev
1.3 配置TypeScript
创建 `tsconfig.json`:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/*/"],
"exclude": ["node_modules", "dist"]
}
1.4 配置package.json
更新 `package.json`:
{
"name": "openclaw-plugin-weather",
"version": "1.0.0",
"description": "OpenClaw天气查询插件",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "jest",
"lint": "eslint src/*/.ts"
},
"keywords": ["openclaw", "plugin", "weather"],
"author": "Your Name",
"license": "MIT",
"openclaw": {
"plugin": {
"name": "weather",
"version": "1.0.0",
"description": "查询全球城市天气信息",
"author": "Your Name",
"category": "tools"
}
},
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "^1.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"ts-jest": "^29.0.0",
"eslint": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0"
}
}
第二步:创建第一个插件 – 天气查询
2.1 创建插件目录结构
src/
├── index.ts # 插件入口文件
├── weather/
│ ├── index.ts # 天气模块主文件
│ ├── types.ts # 类型定义
│ ├── api.ts # API调用
│ └── utils.ts # 工具函数
└── tests/
└── weather.test.ts
2.2 定义插件类型
创建 `src/weather/types.ts`:
export interface WeatherData {
location: string;
temperature: number;
feelsLike: number;
humidity: number;
windSpeed: number;
windDirection: string;
condition: string;
icon: string;
sunrise: string;
sunset: string;
lastUpdated: string;
}
export interface WeatherRequest {
city: string;
country?: string;
units?: 'metric' | 'imperial';
lang?: string;
}
export interface WeatherResponse {
success: boolean;
data?: WeatherData;
error?: string;
}
export interface WeatherConfig {
apiKey: string;
baseUrl: string;
cacheDuration: number; // 缓存时间(分钟)
}
2.3 创建API客户端
创建 `src/weather/api.ts`:
import axios, { AxiosInstance } from 'axios';
import { WeatherData, WeatherRequest, WeatherResponse, WeatherConfig } from './types';
export class WeatherAPI {
private client: AxiosInstance;
private config: WeatherConfig;
private cache: Map = new Map();
constructor(config: WeatherConfig) {
this.config = config;
this.client = axios.create({
baseURL: config.baseUrl,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
}
async getWeather(request: WeatherRequest): Promise {
try {
// 检查缓存
const cacheKey = this.getCacheKey(request);
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.config.cacheDuration 60 1000) {
return {
success: true,
data: cached.data,
};
}
// 调用API
const response = await this.client.get('/weather', {
params: {
q: `${request.city},${request.country || ''}`,
units: request.units || 'metric',
lang: request.lang || 'zh',
appid: this.config.apiKey,
},
});
const weatherData: WeatherData = this.transformResponse(response.data);
// 更新缓存
this.cache.set(cacheKey, {
data: weatherData,
timestamp: Date.now(),
});
return {
success: true,
data: weatherData,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.message || error.message || '获取天气失败',
};
}
}
private getCacheKey(request: WeatherRequest): string {
return `${request.city}-${request.country || ''}-${request.units || 'metric'}`;
}
private transformResponse(data: any): WeatherData {
return {
location: `${data.name}, ${data.sys.country}`,
temperature: Math.round(data.main.temp),
feelsLike: Math.round(data.main.feels_like),
humidity: data.main.humidity,
windSpeed: data.wind.speed,
windDirection: this.getWindDirection(data.wind.deg),
condition: data.weather[0].description,
icon: `https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`,
sunrise: new Date(data.sys.sunrise * 1000).toLocaleTimeString('zh-CN'),
sunset: new Date(data.sys.sunset * 1000).toLocaleTimeString('zh-CN'),
lastUpdated: new Date().toLocaleString('zh-CN'),
};
}
private getWindDirection(degrees: number): string {
const directions = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
const index = Math.round(degrees / 45) % 8;
return directions[index];
}
}
2.4 创建工具函数
创建 `src/weather/utils.ts`:
import { WeatherData } from './types';
export function formatWeatherMessage(data: WeatherData): string {
return `
🌤️ ${data.location} 天气报告
🌡️ 温度:${data.temperature}°C(体感 ${data.feelsLike}°C)
💧 湿度:${data.humidity}%
💨 风速:${data.windSpeed} m/s,风向 ${data.windDirection}
☁️ 天气:${data.condition}
🌅 日出:${data.sunrise}
🌇 日落:${data.sunset}
⏰ 更新时间:${data.lastUpdated}
`.trim();
}
export function validateCityName(city: string): boolean {
// 简单的城市名验证
const regex = /^[a-zA-Z\u4e00-\u9fa5\s\-']+$/;
return regex.test(city) && city.length >= 2 && city.length <= 50;
}
export function getWeatherEmoji(condition: string): string {
const emojiMap: Record = {
'晴': '☀️',
'多云': '⛅',
'阴': '☁️',
'雨': '🌧️',
'雷阵雨': '⛈️',
'雪': '❄️',
'雾': '🌫️',
'大风': '💨',
};
for (const [key, emoji] of Object.entries(emojiMap)) {
if (condition.includes(key)) {
return emoji;
}
}
return '🌤️';
}
2.5 创建天气模块主文件
创建 `src/weather/index.ts`:
import { WeatherAPI } from './api';
import { formatWeatherMessage, validateCityName, getWeatherEmoji } from './utils';
import { WeatherRequest, WeatherResponse, WeatherConfig } from './types';
export class WeatherPlugin {
private api: WeatherAPI;
private config: WeatherConfig;
constructor(config: WeatherConfig) {
this.config = config;
this.api = new WeatherAPI(config);
}
async queryWeather(request: WeatherRequest): Promise {
// 验证输入
if (!validateCityName(request.city)) {
return {
success: false,
error: '城市名称无效,请使用中文或英文城市名',
};
}
// 查询天气
const result = await this.api.getWeather(request);
if (result.success && result.data) {
// 添加表情符号
result.data.condition = `${getWeatherEmoji(result.data.condition)} ${result.data.condition}`;
}
return result;
}
async getWeatherReport(request: WeatherRequest): Promise {
const result = await this.queryWeather(request);
if (result.success && result.data) {
return formatWeatherMessage(result.data);
} else {
return `❌ 获取天气信息失败:${result.error}`;
}
}
getConfig(): WeatherConfig {
return { ...this.config };
}
updateConfig(newConfig: Partial): void {
this.config = { ...this.config, ...newConfig };
}
}
第三步:创建插件入口文件
创建 `src/index.ts`:
import { Plugin, Tool, PluginContext } from '@openclaw/plugin-sdk';
import { WeatherPlugin } from './weather';
import { WeatherRequest } from './weather/types';
// 插件配置接口
interface WeatherPluginConfig {
apiKey: string;
baseUrl?: string;
cacheDuration?: number;
}
// 工具定义
const weatherTool: Tool = {
name: 'get_weather',
description: '查询指定城市的天气信息',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称(中文或英文)',
},
country: {
type: 'string',
description: '国家代码(可选,如CN、US)',
optional: true,
},
units: {
type: 'string',
enum: ['metric', 'imperial'],
description: '温度单位:metric=摄氏度,imperial=华氏度',
optional: true,
default: 'metric',
},
lang: {
type: 'string',
description: '语言代码(如zh、en)',
optional: true,
default: 'zh',
},
},
required: ['city'],
},
execute: async (params: any, context: PluginContext) => {
const config = context.config as WeatherPluginConfig;
const weatherPlugin = new WeatherPlugin({
apiKey: config.apiKey,
baseUrl: config.baseUrl || 'https://api.openweathermap.org/data/2.5',
cacheDuration: config.cacheDuration || 10,
});
const request: WeatherRequest = {
city: params.city,
country: params.country,
units: params.units,
lang: params.lang,
};
const result = await weatherPlugin.getWeatherReport(request);
return result;
},
};
// 插件定义
const weatherPlugin: Plugin = {
name: 'weather',
version: '1.0.0',
description: 'OpenClaw天气查询插件',
author: 'Your Name',
category: 'tools',
// 插件配置
configSchema: {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'OpenWeatherMap API密钥',
secret: true,
},
baseUrl: {
type: 'string',
description: 'API基础URL',
default: 'https://api.openweathermap.org/data/2.5',
},
cacheDuration: {
type: 'number',
description: '缓存时间(分钟)',
default: 10,
minimum: 1,
maximum: 60,
},
},
required: ['apiKey'],
},
// 提供的工具
tools: [weatherTool],
// 初始化函数
initialize: async (config: any, context: PluginContext) => {
console.log('天气插件初始化完成');
return {
status: 'ready',
message: '天气插件已就绪',
};
},
// 清理函数
cleanup: async () => {
console.log('天气插件清理完成');
},
};
export default weatherPlugin;
第四步:测试插件
4.1 创建测试文件
创建 `src/tests/weather.test.ts`:
import { WeatherPlugin } from '../weather';
import { WeatherAPI } from '../weather/api';
// Mock axios
jest.mock('axios');
const axios = require('axios');
describe('WeatherPlugin', () => {
let plugin: WeatherPlugin;
beforeEach(() => {
plugin = new WeatherPlugin({
apiKey: 'test-api-key',
baseUrl: 'https://api.test.com',
cacheDuration: 10,
});
});
test('应该正确初始化', () => {
expect(plugin).toBeInstanceOf(WeatherPlugin);
});
test('应该验证城市名称', async () => {
// 测试有效城市名
const validRequest = { city: '北京' };
const invalidRequest = { city: '123' };
// 这里需要模拟API调用
axios.get.mockResolvedValueOnce({
data: {
name: '北京',
sys: { country: 'CN', sunrise: 1678246800, sunset: 1678289400 },
main: { temp: 20, feels_like: 19, humidity: 60 },
wind: { speed: 3, deg: 90 },
weather: [{ description: '晴', icon: '01d' }],
},
});
const validResult = await plugin.queryWeather(validRequest);
expect(validResult.success).toBe(true);
const invalidResult = await plugin.queryWeather(invalidRequest);
expect(invalidResult.success).toBe(false);
expect(invalidResult.error).toContain('城市名称无效');
});
test('应该格式化天气消息', async () => {
// 模拟API响应
axios.get.mockResolvedValueOnce({
data: {
name: '上海',
sys: { country: 'CN', sunrise: 1678246800, sunset: 1678289400 },
main: { temp: 22, feels_like: 21, humidity: 65 },
wind: { speed: 4, deg: 180 },
weather: [{ description: '多云', icon: '02d' }],
},
});
const result = await plugin.getWeatherReport({ city: '上海' });
expect(result).toContain('上海');
expect(result).toContain('22°C');
expect(result).toContain('65%');
});
});
describe('WeatherAPI', () => {
let api: WeatherAPI;
beforeEach(() => {
api = new WeatherAPI({
apiKey: 'test-key',
baseUrl: 'https://api.test.com',
cacheDuration: 10,
});
});
test('应该缓存响应', async () => {
const mockResponse = {
data: {
name: '北京',
sys: { country: 'CN', sunrise: 1678246800, sunset: 1678289400 },
main: { temp: 20, feels_like: 19, humidity: 60 },
wind: { speed: 3, deg: 90 },
weather: [{ description: '晴', icon: '01d' }],
},
};
axios.get.mockResolvedValue(mockResponse);
// 第一次调用
const result1 = await api.getWeather({ city: '北京' });
expect(axios.get).toHaveBeenCalledTimes(1);
expect(result1.success).toBe(true);
// 第二次调用(应该使用缓存)
const result2 = await api.getWeather({ city: '北京' });
expect(axios.get).toHaveBeenCalledTimes(1); // 没有新的API调用
expect(result2.success).toBe(true);
});
});
4.2 配置Jest测试
创建 `jest.config.js`:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['/src'],
testMatch: ['/__tests__//.ts', '/?(.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/*/.ts',
'!src/*/.d.ts',
'!src/tests/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
};
4.3 运行测试
安装测试依赖
npm install --save-dev jest @types/jest ts-jest
运行测试
npm test
运行测试并生成覆盖率报告
npm test -- --coverage
第五步:构建和打包插件
5.1 构建脚本
更新 `package.json` 的scripts部分:
{
"scripts": {
"build": "tsc",
"build:prod": "tsc --project tsconfig.prod.json",
"dev": "tsc --watch",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src/*/.ts",
"lint:fix": "eslint src/*/.ts --fix",
"format": "prettier --write \"src/*/.ts\"",
"prepack": "npm run build:prod",
"pack": "npm pack",
"publish:local": "npm run build:prod && npm pack"
}
}
5.2 生产构建配置
创建 `tsconfig.prod.json`:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declarationMap": false,
"incremental": false
},
"exclude": ["src/tests/", "/.test.ts", "/.spec.ts"]
}
5.3 创建构建脚本
创建 `scripts/build.js`:
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
console.log('🚀 开始构建OpenClaw插件...');
// 清理dist目录
const distDir = path.join(__dirname, '..', 'dist');
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true });
}
fs.mkdirSync(distDir, { recursive: true });
// 编译TypeScript
console.log('📦 编译TypeScript...');
execSync('npx tsc --project tsconfig.prod.json', { stdio: 'inherit' });
// 复制必要的文件
console.log('📋 复制配置文件...');
const filesToCopy = [
'package.json',
'README.md',
'LICENSE',
'plugin.config.json',
];
filesToCopy.forEach(file => {
const source = path.join(__dirname, '..', file);
const target = path.join(distDir, file);
if (fs.existsSync(source)) {
fs.copyFileSync(source, target);
console.log(` ✓ ${file}`);
}
});
// 生成版本文件
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const versionInfo = {
name: packageJson.name,
version: packageJson.version,
buildTime: new Date().toISOString(),
nodeVersion: process.version,
};
fs.writeFileSync(
path.join(distDir, 'version.json'),
JSON.stringify(versionInfo, null, 2)
);
console.log('✅ 构建完成!');
console.log(`📁 输出目录: ${distDir}`);
console.log(`🏷️ 版本: ${packageJson.version}`);
第六步:本地测试插件
6.1 创建测试环境
创建 `test-environment/` 目录:
mkdir -p test-environment
cd test-environment
初始化测试项目
npm init -y
npm install openclaw
创建测试配置
mkdir config
6.2 创建测试配置文件
创建 `test-environment/config/plugins.json`:
{
"plugins": {
"weather": {
"enabled": true,
"config": {
"apiKey": "your-openweathermap-api-key",
"baseUrl": "https://api.openweathermap.org/data/2.5",
"cacheDuration": 10
}
}
}
}
6.3 创建测试脚本
创建 `test-environment/test-plugin.js`:
const { OpenClaw } = require('openclaw');
const path = require('path');
async function testWeatherPlugin() {
console.log('🧪 测试天气插件...');
// 创建OpenClaw实例
const openclaw = new OpenClaw({
plugins: {
weather: {
enabled: true,
config: {
apiKey: process.env.OPENWEATHER_API_KEY || 'test-key',
cacheDuration: 1, // 1分钟缓存,便于测试
},
},
},
});
// 初始化
await openclaw.init();
console.log('✅ OpenClaw初始化完成');
// 测试天气查询
try {
const result = await openclaw.executeTool('get_weather', {
city: '北京',
lang: 'zh',
});
console.log('🌤️ 天气查询结果:');
console.log(result);
// 测试缓存
console.log('\n🔄 测试缓存(第二次查询应该更快)...');
const startTime = Date.now();
const cachedResult = await openclaw.executeTool('get_weather', {
city: '北京',
lang: 'zh',
});
const endTime = Date.now();
console.log(`⏱️ 缓存查询耗时: ${endTime - startTime}ms`);
} catch (error) {
console.error('❌ 测试失败:', error.message);
}
// 清理
await openclaw.cleanup();
console.log('🧹 清理完成');
}
// 运行测试
if (require.main === module) {
testWeatherPlugin().catch(console.error);
}
module.exports = { testWeatherPlugin };
6.4 运行本地测试
设置环境变量
export OPENWEATHER_API_KEY=your_actual_api_key
构建插件
cd openclaw-plugin-weather
npm run build:prod
打包插件
npm pack
在测试环境中安装插件
cd ../test-environment
npm install ../openclaw-plugin-weather/openclaw-plugin-weather-1.0.0.tgz
运行测试
node test-plugin.js
第七步:插件配置管理
7.1 创建配置界面
创建 `src/config-ui.tsx`(React组件):
import React, { useState } from 'react';
import { PluginConfigUI } from '@openclaw/plugin-sdk';
interface WeatherConfigUIProps {
config: any;
onConfigChange: (config: any) => void;
}
export const WeatherConfigUI: React.FC = ({
config,
onConfigChange,
}) => {
const [apiKey, setApiKey] = useState(config?.apiKey || '');
const [cacheDuration, setCacheDuration] = useState(config?.cacheDuration || 10);
const [baseUrl, setBaseUrl] = useState(config?.baseUrl || 'https://api.openweathermap.org/data/2.5');
const handleSave = () => {
onConfigChange({
apiKey,
cacheDuration: parseInt(cacheDuration.toString(), 10),
baseUrl,
});
};
return (
天气插件配置
setApiKey(e.target.value)}
placeholder="输入你的API密钥"
/>
获取免费API密钥
setBaseUrl(e.target.value)}
placeholder="https://api.openweathermap.org/data/2.5"
/>
setCacheDuration(parseInt(e.target.value, 10))}
/>
减少API调用频率,提高响应速度
);
};
// 导出配置UI组件
export const configUI: PluginConfigUI = {
component: WeatherConfigUI,
defaultConfig: {
apiKey: '',
baseUrl: 'https://api.openweathermap.org/data/2.5',
cacheDuration: 10,
},
};
7.2 集成配置UI
更新 `src/index.ts`:
import { configUI } from './config-ui';
// 在插件定义中添加configUI
const weatherPlugin: Plugin = {
name: 'weather',
version: '1.0.0',
description: 'OpenClaw天气查询插件',
author: 'Your Name',
category: 'tools',
configSchema: {
// ... 原有的配置schema
},
// 添加配置UI
configUI,
tools: [weatherTool],
initialize: async (config: any, context: PluginContext) => {
// ... 原有的初始化逻辑
},
cleanup: async () => {
// ... 原有的清理逻辑
},
};
第八步:错误处理和日志
8.1 增强错误处理
创建 `src/error-handler.ts`:
import { PluginContext } from '@openclaw/plugin-sdk';
export class PluginError extends Error {
constructor(
message: string,
public code: string,
public details?: any
) {
super(message);
this.name = 'PluginError';
}
}
export class WeatherError extends PluginError {
constructor(message: string, code: string, details?: any) {
super(message, code, details);
this.name = 'WeatherError';
}
}
export function handlePluginError(error: any, context: PluginContext): string {
// 记录错误日志
context.logger.error('插件错误', {
error: error.message,
stack: error.stack,
code: error.code,
details: error.details,
});
// 用户友好的错误消息
if (error instanceof WeatherError) {
switch (error.code) {
case 'INVALID_CITY':
return '❌ 城市名称无效,请检查输入';
case 'API_ERROR':
return '❌ 天气API服务暂时不可用,请稍后重试';
case 'NETWORK_ERROR':
return '❌ 网络连接失败,请检查网络设置';
case 'RATE_LIMIT':
return '❌ API调用频率超限,请稍后重试';
default:
return `❌ 天气查询失败:${error.message}`;
}
}
// 未知错误
return '❌ 发生未知错误,请检查插件配置或联系管理员';
}
export function validateApiKey(apiKey: string): boolean {
if (!apiKey || apiKey.trim().length === 0) {
throw new WeatherError('API密钥未配置', 'CONFIG_ERROR');
}
if (apiKey.length < 32) {
throw new WeatherError('API密钥格式无效', 'CONFIG_ERROR');
}
return true;
}
8.2 更新天气模块使用错误处理
更新 `src/weather/index.ts`:
import { handlePluginError, validateApiKey, WeatherError } from '../error-handler';
import { PluginContext } from '@openclaw/plugin-sdk';
export class WeatherPlugin {
private api: WeatherAPI;
private config: WeatherConfig;
private context?: PluginContext;
constructor(config: WeatherConfig, context?: PluginContext) {
this.config = config;
this.context = context;
// 验证配置
try {
validateApiKey(config.apiKey);
} catch (error) {
if (context) {
context.logger.error('插件配置验证失败', { error });
}
throw error;
}
this.api = new WeatherAPI(config);
}
async queryWeather(request: WeatherRequest): Promise {
try {
// 验证输入
if (!validateCityName(request.city)) {
throw new WeatherError('城市名称无效', 'INVALID_CITY', { city: request.city });
}
// 查询天气
const result = await this.api.getWeather(request);
if (!result.success) {
throw new WeatherError(result.error || '获取天气失败', 'API_ERROR');
}
if (result.data) {
// 添加表情符号
result.data.condition = `${getWeatherEmoji(result.data.condition)} ${result.data.condition}`;
}
return result;
} catch (error) {
// 记录错误
if (this.context) {
this.context.logger.error('天气查询失败', {
request,
error: error.message,
});
}
if (error instanceof WeatherError) {
return {
success: false,
error: error.message,
};
}
return {
success: false,
error: '天气查询过程中发生未知错误',
};
}
}
async getWeatherReport(request: WeatherRequest): Promise {
try {
const result = await this.queryWeather(request);
if (result.success && result.data) {
return formatWeatherMessage(result.data);
} else {
if (this.context) {
return handlePluginError(
new WeatherError(result.error || '未知错误', 'UNKNOWN_ERROR'),
this.context
);
}
return `❌ 获取天气信息失败:${result.error}`;
}
} catch (error) {
if (this.context) {
return handlePluginError(error, this.context);
}
return `❌ 发生错误:${error.message}`;
}
}
// ... 其他方法
}
第九步:国际化支持
9.1 创建语言文件
创建 `src/locales/zh-CN.json`:
{
"plugin": {
"name": "天气插件",
"description": "查询全球城市天气信息"
},
"errors": {
"invalid_city": "城市名称无效,请使用中文或英文城市名",
"api_error": "天气API服务暂时不可用,请稍后重试",
"network_error": "网络连接失败,请检查网络设置",
"config_error": "插件配置错误,请检查API密钥",
"rate_limit": "API调用频率超限,请稍后重试"
},
"ui": {
"config": {
"title": "天气插件配置",
"apiKey": "OpenWeatherMap API密钥",
"apiKeyHelp": "获取免费API密钥",
"baseUrl": "API基础URL",
"cacheDuration": "缓存时间(分钟)",
"cacheDurationHelp": "减少API调用频率,提高响应速度",
"save": "保存配置",
"test": "测试连接"
},
"weather": {
"location": "位置",
"temperature": "温度",
"feelsLike": "体感温度",
"humidity": "湿度",
"windSpeed": "风速",
"windDirection": "风向",
"condition": "天气状况",
"sunrise": "日出",
"sunset": "日落",
"lastUpdated": "更新时间"
}
}
}
创建 `src/locales/en-US.json`:
{
"plugin": {
"name": "Weather Plugin",
"description": "Query weather information for cities worldwide"
},
"errors": {
"invalid_city": "Invalid city name, please use Chinese or English city names",
"api_error": "Weather API service temporarily unavailable, please try again later",
"network_error": "Network connection failed, please check your network settings",
"config_error": "Plugin configuration error, please check API key",
"rate_limit": "API rate limit exceeded, please try again later"
},
"ui": {
"config": {
"title": "Weather Plugin Configuration",
"apiKey": "OpenWeatherMap API Key",
"apiKeyHelp": "Get free API key",
"baseUrl": "API Base URL",
"cacheDuration": "Cache Duration (minutes)",
"cacheDurationHelp": "Reduce API call frequency, improve response speed",
"save": "Save Configuration",
"test": "Test Connection"
},
"weather": {
"location": "Location",
"temperature": "Temperature",
"feelsLike": "Feels Like",
"humidity": "Humidity",
"windSpeed": "Wind Speed",
"windDirection": "Wind Direction",
"condition": "Condition",
"sunrise": "Sunrise",
"sunset": "Sunset",
"lastUpdated": "Last Updated"
}
}
}
9.2 创建国际化工具
创建 `src/i18n.ts`:
import zhCN from './locales/zh-CN.json';
import enUS from './locales/en-US.json';
type Locale = 'zh-CN' | 'en-US';
type TranslationKey = string;
const translations: Record = {
'zh-CN': zhCN,
'en-US': enUS,
};
export class I18n {
private locale: Locale = 'zh-CN';
constructor(locale?: Locale) {
if (locale && translations[locale]) {
this.locale = locale;
}
}
setLocale(locale: Locale): void {
if (translations[locale]) {
this.locale = locale;
}
}
getLocale(): Locale {
return this.locale;
}
t(key: TranslationKey, params?: Record): string {
const keys = key.split('.');
let value: any = translations[this.locale];
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return key; // 返回键名作为后备
}
}
if (typeof value === 'string' && params) {
return this.replaceParams(value, params);
}
return value || key;
}
private replaceParams(text: string, params: Record): string {
return text.replace(/\{(\w+)\}/g, (match, key) => {
return params[key] !== undefined ? params[key] : match;
});
}
}
// 默认实例
export const i18n = new I18n();
// 工具函数
export function t(key: TranslationKey, params?: Record): string {
return i18n.t(key, params);
}
9.3 更新天气模块使用国际化
更新 `src/weather/index.ts`:
import { i18n, t } from '../i18n';
import { PluginContext } from '@openclaw/plugin-sdk';
export class WeatherPlugin {
// ... 现有代码
constructor(config: WeatherConfig, context?: PluginContext) {
this.config = config;
this.context = context;
// 设置语言
if (context?.locale) {
i18n.setLocale(context.locale as any);
}
// ... 其他初始化
}
async getWeatherReport(request: WeatherRequest): Promise {
try {
const result = await this.queryWeather(request);
if (result.success && result.data) {
return this.formatLocalizedWeatherMessage(result.data);
} else {
return t('errors.api_error');
}
} catch (error) {
return t('errors.network_error');
}
}
private formatLocalizedWeatherMessage(data: WeatherData): string {
return `
${getWeatherEmoji(data.condition)} ${t('ui.weather.location')}: ${data.location}
🌡️ ${t('ui.weather.temperature')}: ${data.temperature}°C (${t('ui.weather.feelsLike')} ${data.feelsLike}°C)
💧 ${t('ui.weather.humidity')}: ${data.humidity}%
💨 ${t('ui.weather.windSpeed')}: ${data.windSpeed} m/s, ${t('ui.weather.windDirection')} ${data.windDirection}
☁️ ${t('ui.weather.condition')}: ${data.condition}
🌅 ${t('ui.weather.sunrise')}: ${data.sunrise}
🌇 ${t('ui.weather.sunset')}: ${data.sunset}
⏰ ${t('ui.weather.lastUpdated')}: ${data.lastUpdated}
`.trim();
}
}
第十步:发布到OpenClaw插件市场
10.1 准备发布文件
创建 `plugin.config.json`:
{
"name": "openclaw-plugin-weather",
"displayName": "天气查询插件",
"version": "1.0.0",
"description": "查询全球城市天气信息的OpenClaw插件",
"author": "Your Name",
"license": "MIT",
"homepage": "https://github.com/yourusername/openclaw-plugin-weather",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/openclaw-plugin-weather.git"
},
"bugs": {
"url": "https://github.com/yourusername/openclaw-plugin-weather/issues"
},
"keywords": [
"openclaw",
"plugin",
"weather",
"api"
],
"categories": ["tools"],
"engines": {
"openclaw": ">=1.0.0",
"node": ">=18.0.0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"prepublishOnly": "npm run build:prod"
}
}
10.2 创建README.md
创建 `README.md`:
OpenClaw天气查询插件


OpenClaw天气查询插件,支持查询全球城市天气信息。
功能特性
- 🌤️ 查询全球城市天气
- ⚡ 响应缓存,减少API调用
- 🌍 多语言支持(中文、英文)
- 🔧 可配置API密钥和缓存时间
- 🧪 完整的单元测试
- 📱 响应式配置界面
安装
bash
通过npm安装
npm install openclaw-plugin-weather
或通过OpenClaw插件市场安装
openclaw plugin install weather
配置
1. 获取OpenWeatherMap API密钥
1. 访问 OpenWeatherMap
2. 注册账号并获取免费API密钥
3. 免费套餐支持每分钟60次调用
2. 配置插件
在OpenClaw配置文件中添加:
json
{
"plugins": {
"weather": {
"enabled": true,
"config": {
"apiKey": "your_openweathermap_api_key",
"cacheDuration": 10
}
}
}
}
使用方法
通过工具调用
javascript
// 查询北京天气
const result = await openclaw.executeTool('get_weather', {
city: '北京',
lang: 'zh'
});
console.log(result);
通过聊天界面
用户:今天北京天气怎么样?
OpenClaw:🌤️ 北京, CN 天气报告...
API参考
工具:get_weather
查询指定城市的天气信息。
参数:
- `city` (string, required): 城市名称(中文或英文)
- `country` (string, optional): 国家代码(如CN、US)
- `units` (string, optional): 温度单位,`metric`(摄氏度) 或 `imperial`(华氏度),默认`metric`
- `lang` (string, optional): 语言代码,如`zh`、`en`,默认`zh`
示例:
json
{
"city": "上海",
"country": "CN",
"units": "metric",
"lang": "zh"
}
开发
环境设置
bash
克隆仓库
git clone https://github.com/yourusername/openclaw-plugin-weather.git
cd openclaw-plugin-weather
安装依赖
npm install
开发模式(监听文件变化)
npm run dev
运行测试
npm test
构建生产版本
npm run build:prod
项目结构
src/
├── index.ts # 插件入口
├── weather/ # 天气模块
│ ├── index.ts # 主逻辑
│ ├── api.ts # API客户端
│ ├── types.ts # 类型定义
│ └── utils.ts # 工具函数
├── i18n.ts # 国际化
├── error-handler.ts # 错误处理
├── config-ui.tsx # 配置界面
└── tests/ # 测试文件
贡献
欢迎提交Issue和Pull Request!
1. Fork本仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 打开Pull Request
许可证
本项目基于MIT许可证 - 查看 LICENSE 文件了解详情。
支持
- 问题反馈:GitHub Issues
- 文档:OpenClaw插件开发指南
- 社区:OpenClaw Discord
10.3 发布到npm
1. 登录npm(如果没有账号,先注册)
npm login
2. 更新版本号
npm version patch # 或 minor, major
3. 构建生产版本
npm run build:prod
4. 发布到npm
npm publish --access public
5. 验证发布
npm view openclaw-plugin-weather
10.4 提交到OpenClaw插件市场
创建 `marketplace-submission.json`:
{
"name": "weather",
"displayName": "天气查询插件",
"description": "查询全球城市天气信息的OpenClaw插件",
"version": "1.0.0",
"author": "Your Name",
"license": "MIT",
"homepage": "https://github.com/yourusername/openclaw-plugin-weather",
"repository": "https://github.com/yourusername/openclaw-plugin-weather",
"keywords": ["weather", "api", "tools"],
"categories": ["tools"],
"compatibility": {
"openclaw": ">=1.0.0"
},
"screenshots": [
"https://raw.githubusercontent.com/yourusername/openclaw-plugin-weather/main/screenshots/weather-query.png",
"https://raw.githubusercontent.com/yourusername/openclaw-plugin-weather/main/screenshots/config-ui.png"
],
"dependencies": {
"axios": "^1.6.0"
}
}
提交到OpenClaw插件市场:
通过OpenClaw CLI提交
openclaw plugin submit marketplace-submission.json
或通过GitHub提交PR到官方插件仓库
第十一步:维护和更新
11.1 版本管理策略
使用语义化版本控制:
- 主版本号(1.x.x):不兼容的API更改
- 次版本号(x.1.x):向后兼容的功能添加
- 修订号(x.x.1):向后兼容的问题修复
11.2 更新日志
创建 `CHANGELOG.md`:
更新日志
[1.0.0] - 2024-02-26
新增
- 初始版本发布
- 支持全球城市天气查询
- 多语言支持(中文、英文)
- 响应缓存机制
- 完整的单元测试
- 配置界面
修复
- 无
变更
- 无
11.3 自动化发布流程
创建 `.github/workflows/release.yml`:
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build:prod
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
files: |
dist/*/
README.md
CHANGELOG.md
相关文章
OpenClaw企业级部署
OpenClaw配置大全
OpenClaw实战案例
总结
通过以上11个步骤,你已经完成了一个完整的OpenClaw插件开发流程:
1. 环境搭建:配置开发环境
2. 插件开发:实现天气查询功能
3. 测试验证:编写单元测试
4. 构建打包:创建生产版本
5. 本地测试:在真实环境中测试
6. 配置管理:添加配置界面
7. 错误处理:增强健壮性
8. 国际化:支持多语言
9. 文档编写:创建完整文档
10. 发布部署:发布到npm和市场
11. 维护更新:建立维护流程
这个天气插件只是一个起点,你可以基于这个模板开发更复杂的插件,如:
- 数据库插件:连接MySQL、PostgreSQL、MongoDB
- 消息插件:集成微信、钉钉、飞书
- AI插件:连接其他AI服务(文心一言、通义千问)
- 工作流插件:创建复杂的自动化流程
- 数据分析插件:处理和分析数据
相关资源:
- OpenClaw插件开发文档
- OpenWeatherMap API文档
- TypeScript官方文档
- [npm发布指南](https://docs.npm





暂无评论内容