从零开发一个 OpenClaw Skill

Skill 是 OpenClaw 的能力扩展单元。通过安装 Skill,你的 Agent 可以获得新的工具和能力 —— 比如查天气、控制智能家居、调用特定 API。本文以一个「天气查询 Skill」为例,手把手教你从零开发到发布。

Skill 概念

Skill 本质上是一个包含配置文件和可选脚本的目录。安装到 Agent 工作空间后,Agent 就能理解并使用该 Skill 提供的工具。

Skill 的组成:

  • SKILL.md —— Skill 的核心定义文件,包含描述和使用说明
  • metadata.openclaw —— 元数据配置(依赖、安装脚本等)
  • 可选的脚本、配置文件、模板等

Skill 目录结构

一个标准的 Skill 目录结构如下:

weather-skill/
├── SKILL.md              # Skill 定义(必须)
├── metadata.openclaw     # 元数据配置(必须)
├── scripts/
│   ├── get_weather.py    # 业务逻辑脚本
│   └── requirements.txt  # Python 依赖
├── templates/
│   └── weather_report.md # 输出模板
└── README.md             # 说明文档(可选)

SKILL.md —— Skill 定义文件

SKILL.md 是 Skill 最核心的文件。它使用 frontmatter + Markdown 格式,既定义元信息,又包含 Agent 使用说明。

Frontmatter 格式

yaml

---
name: weather
displayName: 天气查询
version: 1.0.0
description: 查询全球城市天气预报,支持当前天气和未来 7 天预报
author: your-name
license: MIT
tags:
  - weather
  - utility
  - api
requires:
  env:
    - OPENWEATHER_API_KEY    # 需要的环境变量
  tools:
    - exec                    # 需要的 OpenClaw 工具
  commands:
    - python3                 # 需要的系统命令
---

Frontmatter 字段说明

字段必填说明
nameSkill 唯一标识,小写字母和连字符
displayName展示名称
version语义化版本号
description简短描述
author作者名
tags标签,用于搜索和分类
requires.env需要的环境变量
requires.tools需要的 OpenClaw 工具权限
requires.commands需要的系统命令

Markdown 内容

Frontmatter 之后的 Markdown 内容是给 Agent 看的使用说明。Agent 会阅读这些内容来理解如何使用该 Skill:

markdown

# 天气查询 Skill

## 功能
- 查询全球任意城市的当前天气
- 获取未来 7 天天气预报
- 支持中英文城市名

## 使用方式

### 查询当前天气
运行脚本获取天气数据:

```bash
python3 ~/.openclaw/skills/weather/scripts/get_weather.py --city "上海" --type current
```

### 查询天气预报

```bash
python3 ~/.openclaw/skills/weather/scripts/get_weather.py --city "Beijing" --type forecast --days 7
```

## 输出格式
脚本输出 JSON 格式的天气数据,请解析后以友好的格式呈现给用户。

## 注意事项
- 需要设置 OPENWEATHER_API_KEY 环境变量
- 城市名支持中文,但建议使用英文以提高准确度
- API 免费套餐每分钟限制 60 次调用

metadata.openclaw 配置

metadata.openclaw 是 YAML 格式的元数据文件,定义安装和运行时行为:

yaml

# metadata.openclaw

# Skill 标识
name: weather
version: 1.0.0

# 运行环境
primaryEnv: python    # 主要编程语言/环境

# 安装步骤
install:
  - command: pip3 install -r scripts/requirements.txt
    cwd: "${SKILL_DIR}"

# 依赖声明
requires:
  env:
    - name: OPENWEATHER_API_KEY
      description: "OpenWeatherMap API 密钥"
      required: true
  system:
    - python3
    - pip3
  tools:
    - exec

# 健康检查(安装后验证)
healthCheck:
  command: python3 scripts/get_weather.py --check
  cwd: "${SKILL_DIR}"

字段详解

字段说明
primaryEnv主要运行环境:pythonnodeshellrust
install安装时执行的命令列表
install[].command要执行的命令
install[].cwd工作目录,${SKILL_DIR} 指向 Skill 安装目录
requires.env需要的环境变量及说明
requires.system需要的系统级命令
requires.tools需要的 OpenClaw 工具权限
healthCheck安装后的健康检查命令

完整示例:天气查询 Skill

1. 创建项目结构

bash

mkdir -p weather-skill/scripts
cd weather-skill

2. 编写天气查询脚本

python

#!/usr/bin/env python3
"""
weather-skill/scripts/get_weather.py
天气查询脚本 - 使用 OpenWeatherMap API
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.parse

API_KEY = os.environ.get("OPENWEATHER_API_KEY")
BASE_URL = "https://api.openweathermap.org/data/2.5"


def get_current_weather(city: str) -> dict:
    """获取当前天气"""
    params = urllib.parse.urlencode({
        "q": city,
        "appid": API_KEY,
        "units": "metric",
        "lang": "zh_cn"
    })
    url = f"{BASE_URL}/weather?{params}"
    with urllib.request.urlopen(url) as response:
        data = json.loads(response.read())
    return {
        "city": data["name"],
        "country": data["sys"]["country"],
        "temperature": data["main"]["temp"],
        "feels_like": data["main"]["feels_like"],
        "humidity": data["main"]["humidity"],
        "description": data["weather"][0]["description"],
        "wind_speed": data["wind"]["speed"],
        "icon": data["weather"][0]["icon"]
    }


def get_forecast(city: str, days: int = 7) -> dict:
    """获取天气预报"""
    params = urllib.parse.urlencode({
        "q": city,
        "appid": API_KEY,
        "units": "metric",
        "lang": "zh_cn",
        "cnt": min(days * 8, 40)  # API 每3小时一个数据点
    })
    url = f"{BASE_URL}/forecast?{params}"
    with urllib.request.urlopen(url) as response:
        data = json.loads(response.read())
    forecasts = []
    seen_dates = set()
    for item in data["list"]:
        date = item["dt_txt"].split(" ")[0]
        if date not in seen_dates:
            seen_dates.add(date)
            forecasts.append({
                "date": date,
                "temp_max": item["main"]["temp_max"],
                "temp_min": item["main"]["temp_min"],
                "description": item["weather"][0]["description"],
                "humidity": item["main"]["humidity"]
            })
    return {
        "city": data["city"]["name"],
        "country": data["city"]["country"],
        "forecasts": forecasts[:days]
    }


def health_check() -> bool:
    """检查 API Key 是否有效"""
    if not API_KEY:
        print("ERROR: OPENWEATHER_API_KEY not set", file=sys.stderr)
        return False
    try:
        get_current_weather("London")
        print("OK: API Key is valid")
        return True
    except Exception as e:
        print(f"ERROR: {e}", file=sys.stderr)
        return False


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="天气查询")
    parser.add_argument("--city", help="城市名")
    parser.add_argument("--type", choices=["current", "forecast"], default="current")
    parser.add_argument("--days", type=int, default=7)
    parser.add_argument("--check", action="store_true", help="健康检查")
    args = parser.parse_args()

    if args.check:
        sys.exit(0 if health_check() else 1)

    if not args.city:
        print("ERROR: --city is required", file=sys.stderr)
        sys.exit(1)

    if args.type == "current":
        result = get_current_weather(args.city)
    else:
        result = get_forecast(args.city, args.days)

    print(json.dumps(result, ensure_ascii=False, indent=2))