手把手教你开发OpenClaw插件:从零到发布

手把手教你开发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天气查询插件

![npm version](https://www.npmjs.com/package/openclaw-plugin-weather) ![License: MIT](https://opensource.org/licenses/MIT) 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 文件了解详情。

支持

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服务(文心一言、通义千问)
  • 工作流插件:创建复杂的自动化流程
  • 数据分析插件:处理和分析数据

相关资源:

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容

七天热门