乐于分享
好东西不私藏

OpenClaw 深度解析(二):技能开发与工具集成

OpenClaw 深度解析(二):技能开发与工具集成

本文适合:想扩展 OpenClaw 能力的开发者前置知识:Python 基础、OpenClaw 架构基础


目录

  1. 1. Skill 系统概述
  2. 2. 开发第一个 Skill
  3. 3. Skill 规范详解
  4. 4. 工具函数开发实战
  5. 5. 集成外部 API
  6. 6. 调试与测试
  7. 7. 发布与分享

1. Skill 系统概述

1.1 什么是 Skill?

Skill 是 OpenClaw 的能力扩展包,让 Agent 学会新技能。

类比

  • • Chrome 扩展 → 扩展浏览器功能
  • • VSCode 插件 → 扩展编辑器功能
  • • OpenClaw Skill → 扩展 Agent 能力

1.2 Skill 能做什么?

类别
示例
文件处理
PDF 解析、Excel 处理、图片压缩
网络服务
发送邮件、推送通知、API 调用
数据分析
股票分析、天气查询、新闻聚合
自动化
定时任务、文件同步、数据备份
通讯集成
飞书机器人、微信自动回复、邮件收发

1.3 Skill 目录结构

my-skill/├── SKILL.md              # 必需:技能说明文档├── tools/                # 工具函数目录│   ├── __init__.py│   ├── query.py         # 查询功能│   └── update.py        # 更新功能├── scripts/             # 辅助脚本│   └── setup.py├── references/          # 参考资料│   └── api_doc.md└── tests/               # 测试用例    └── test_query.py

2. 开发第一个 Skill

2.1 案例:天气查询 Skill

目标:让 Agent 能查询任意城市天气

2.2 创建目录

cd ~/.openclaw/workspace/skillsmkdir weather-skillcd weather-skillmkdir tools scripts references tests

2.3 编写 SKILL.md

# Weather Skill - 天气查询## 功能描述查询全球任意城市的实时天气和天气预报## 触发条件用户提到以下关键词时自动触发:- "天气"- "气温"- "下雨"- "预报"## 可用工具### weather_query(city, date="today")查询指定城市天气**参数**- `city` (str): 城市名,如"北京"、"New York"- `date` (str): 日期,"today" 或 "YYYY-MM-DD"**返回**```json{  "city": "北京",  "date": "2026-03-16",  "condition": "晴",  "temperature": {"min": 5, "max": 18},  "humidity": 45,  "wind": {"direction": "西北", "speed": 12}}

示例

weather_query("北京")weather_query("上海""2026-03-17")

数据源

  • • 国内城市:中国天气网
  • • 国际城市:Open-Meteo API

配置项

{"default_units":"metric",// metric/imperial"language":"zh-CN"}
### 2.4 实现工具函数**tools/query.py**:```python"""天气查询工具函数"""import requestsfrom typing import Dict, Optionalclass WeatherAPI:    """天气 API 封装"""    def __init__(self):        self.base_url = "https://api.open-meteo.com/v1/forecast"    def get_coordinates(self, city: str) -> tuple:        """        获取城市经纬度        实际项目中可调用地理编码 API        这里用简化版本        """        # 简化示例,实际应调用地理编码服务        coordinates = {            "北京": (39.9042, 116.4074),            "上海": (31.2304, 121.4737),            "广州": (23.1291, 113.2644),            "深圳": (22.5431, 114.0579),        }        return coordinates.get(city, (39.9042, 116.4074))  # 默认北京    def query(self, city: str, date: str = "today") -> Dict:        """        查询天气        Args:            city: 城市名            date: 日期(today 或具体日期)        Returns:            天气数据字典        """        lat, lon = self.get_coordinates(city)        params = {            "latitude": lat,            "longitude": lon,            "current_weather": True,            "daily": ["temperature_2m_max", "temperature_2m_min", "precipitation_sum"],            "timezone": "Asia/Shanghai"        }        try:            response = requests.get(self.base_url, params=params, timeout=10)            response.raise_for_status()            data = response.json()            # 解析数据            current = data.get("current_weather", {})            daily = data.get("daily", {})            result = {                "city": city,                "date": date,                "condition": self._parse_weather_code(current.get("weathercode", 0)),                "temperature": {                    "current": current.get("temperature", 0),                    "max": daily.get("temperature_2m_max", [0])[0],                    "min": daily.get("temperature_2m_min", [0])[0]                },                "wind": {                    "direction": current.get("winddirection", 0),                    "speed": current.get("windspeed", 0)                }            }            return result        except Exception as e:            return {                "error": str(e),                "city": city            }    def _parse_weather_code(self, code: int) -> str:        """解析天气代码"""        weather_map = {            0: "晴",            1: "多云",            2: "阴",            3: "雾",            45: "雾",            48: "雾凇",            51: "毛毛雨",            53: "小雨",            55: "大雨",            61: "小雨",            63: "中雨",            65: "大雨",            71: "小雪",            73: "中雪",            75: "大雪",            95: "雷阵雨"        }        return weather_map.get(code, "未知")# 导出函数供 Agent 调用def weather_query(city: str, date: str = "today") -> Dict:    """    天气查询函数(Agent 调用入口)    Args:        city: 城市名        date: 日期    Returns:        天气数据    """    api = WeatherAPI()    return api.query(city, date)

2.5 注册 Skill

在 openclaw.json 中添加:

{"skills":{"enabled":["weather-skill"],"paths":{"weather-skill":"~/.openclaw/workspace/skills/weather-skill"}}}

2.6 测试 Skill

# 重启 Gatewayopenclaw gateway restart# 测试对话openclaw chat "北京今天天气怎么样?"

预期输出

北京今天天气:晴,气温 5-18°C,西北风 12km/h。


3. Skill 规范详解

3.1 SKILL.md 必选字段

# [Skill 名称]## 功能描述[清晰描述 Skill 的功能和用途]## 触发条件[列出触发关键词或场景]## 可用工具[列出所有可被 Agent 调用的函数]### [函数名](参数列表)[函数说明]**参数**-`param1` (类型): 说明**返回**[返回数据结构]**示例**```python[调用示例代码]

配置项

[可选的配置参数]

依赖

[外部依赖,如 API Key、库等]

注意事项

[使用限制、警告等]

### 3.2 工具函数命名规范**推荐格式**:`[领域]_[功能]_[动作]`| 领域 | 功能 | 动作 | 示例 ||------|------|------|------|| `weather` | `query` | - | `weather_query` || `stock` | `price` | `get` | `stock_price_get` || `file` | `pdf` | `parse` | `file_pdf_parse` || `email` | `send` | - | `email_send` |### 3.3 错误处理规范```pythondef safe_function(param: str) -> Dict:    """    安全的函数实现    Returns:        成功:{"success": True, "data": ...}        失败:{"success": False, "error": "错误信息"}    """    try:        # 业务逻辑        result = do_something(param)        return {"success": True, "data": result}    except Exception as e:        return {"success": False, "error": str(e)}

4. 工具函数开发实战

4.1 案例:股票数据查询

需求:查询 A 股股票实时行情

tools/stock_query.py

"""股票数据查询工具"""import requestsfrom typing importDictListfrom datetime import datetimeclassStockAPI:"""股票 API 封装"""def__init__(self):# 使用新浪财经 API(免费)self.base_url = "http://hq.sinajs.cn/list="defget_stock_info(self, symbol: str) -> Dict:"""        获取股票实时行情        Args:            symbol: 股票代码,如"600519"        Returns:            股票数据        """# A 股代码前缀if symbol.startswith("6"):            code = f"sh{symbol}"else:            code = f"sz{symbol}"try:            response = requests.get(self.base_url + code,                timeout=5,                headers={"User-Agent""Mozilla/5.0"}            )            response.encoding = "gbk"# 新浪返回 GBK 编码# 解析返回数据# 格式:var hq_str_sh600519="贵州茅台,1234.56,..."            content = response.text.strip()if"="in content:                data_str = content.split("=")[1].strip('"')                fields = data_str.split(",")iflen(fields) >= 32:return {"symbol": symbol,"name": fields[0],"current"float(fields[3]),"open"float(fields[1]),"high"float(fields[4]),"low"float[5],"volume"float(fields[8]),"amount"float(fields[9]),"time"f"{fields[30]}{fields[31]}","change_percent"round(                            (float(fields[3]) - float(fields[2])) / float(fields[2]) * 1002                        )                    }return {"error""数据解析失败"}except Exception as e:return {"error"str(e)}defget_stock_history(        self,         symbol: str        start_date: str        end_date: str,        period: str = "daily") -> List[Dict]:"""        获取历史行情        Args:            symbol: 股票代码            start_date: 开始日期 YYYY-MM-DD            end_date: 结束日期 YYYY-MM-DD            period: 周期 (daily/weekly/monthly)        Returns:            历史数据列表        """# 实现略(可调用聚宽、Tushare 等 API)pass# Agent 调用入口defstock_query(symbol: str) -> Dict:"""查询股票实时行情"""    api = StockAPI()return api.get_stock_info(symbol)defstock_history(    symbol: str,    start_date: str,    end_date: str) -> List[Dict]:"""查询股票历史行情"""    api = StockAPI()return api.get_stock_history(symbol, start_date, end_date)

4.2 案例:文件批量处理

tools/file_batch.py

"""文件批量处理工具"""import osimport shutilfrom pathlib import Pathfrom typing importListDictfrom datetime import datetimeclassFileBatchProcessor:"""文件批量处理器"""def__init__(self, workspace: str):self.workspace = Path(workspace)deffind_by_extension(self, ext: str) -> List[Path]:"""        按扩展名查找文件        Args:            ext: 扩展名(不含点),如"pdf"        Returns:            文件路径列表        """        files = []for path inself.workspace.rglob(f"*.{ext}"):if path.is_file():                files.append(path)return filesdefmove_to_folder(        self,         files: List[Path],         target_folder: str) -> Dict:"""        批量移动文件到文件夹        Args:            files: 文件列表            target_folder: 目标文件夹名        Returns:            操作结果        """        target = self.workspace / target_folder        target.mkdir(exist_ok=True)        moved = []        errors = []for file in files:try:                shutil.move(str(file), str(target / file.name))                moved.append(file.name)except Exception as e:                errors.append({"file": file.name, "error"str(e)})return {"success"len(errors) == 0,"moved": moved,"errors": errors,"target"str(target)        }defbackup_files(        self,         pattern: str,        backup_suffix: str = ".bak") -> Dict:"""        批量备份文件        Args:            pattern: 文件匹配模式,如"*.py"            backup_suffix: 备份后缀        Returns:            备份结果        """        files = list(self.workspace.glob(pattern))        backed_up = []for file in files:            backup_path = file.with_suffix(file.suffix + backup_suffix)            shutil.copy2(file, backup_path)            backed_up.append(file.name)return {"success"True,"backed_up": backed_up,"count"len(backed_up)        }# Agent 调用入口deffile_find(ext: str) -> List[str]:"""查找指定扩展名的文件"""    processor = FileBatchProcessor(os.environ.get("WORKSPACE""."))    files = processor.find_by_extension(ext)return [str(f) for f in files]deffile_backup(pattern: str) -> Dict:"""批量备份文件"""    processor = FileBatchProcessor(os.environ.get("WORKSPACE""."))return processor.backup_files(pattern)

5. 集成外部 API

5.1 API Key 管理

安全做法

import osfrom pathlib import Pathdefget_api_key(service: str) -> str:"""    安全获取 API Key    优先级:    1. 环境变量    2. 配置文件    3. 抛出异常    """# 方法 1:环境变量    key = os.environ.get(f"{service.upper()}_API_KEY")if key:return key# 方法 2:配置文件    config_path = Path.home() / ".openclaw" / "api_keys.json"if config_path.exists():import jsonwithopen(config_path) as f:            config = json.load(f)return config.get(service)raise ValueError(f"{service} API Key not found")

5.2 调用 REST API

import requestsfrom typing importDictOptionalclassAPIClient:"""通用 API 客户端"""def__init__(self, base_url: str, api_key: str):self.base_url = base_urlself.session = requests.Session()self.session.headers.update({"Authorization"f"Bearer {api_key}","Content-Type""application/json"        })defget(self, endpoint: str, params: Optional[Dict] = None) -> Dict:"""GET 请求"""        url = f"{self.base_url}/{endpoint}"        response = self.session.get(url, params=params, timeout=30)        response.raise_for_status()return response.json()defpost(self, endpoint: str, data: Dict) -> Dict:"""POST 请求"""        url = f"{self.base_url}/{endpoint}"        response = self.session.post(url, json=data, timeout=30)        response.raise_for_status()return response.json()defupload_file(self, endpoint: str, file_path: str) -> Dict:"""文件上传"""        url = f"{self.base_url}/{endpoint}"withopen(file_path, "rb"as f:            files = {"file": f}            response = self.session.post(url, files=files, timeout=60)        response.raise_for_status()return response.json()

5.3 处理 API 限流

import timefrom functools import wrapsdefrate_limit(calls: int, period: int):"""    API 限流装饰器    Args:        calls: 允许调用次数        period: 时间窗口(秒)    """    timestamps = []defdecorator(func):        @wraps(func)defwrapper(*args, **kwargs):nonlocal timestamps            now = time.time()# 移除过期时间戳            timestamps = [t for t in timestamps if now - t < period]iflen(timestamps) >= calls:                wait_time = period - (now - timestamps[0])                time.sleep(wait_time)            timestamps.append(time.time())return func(*args, **kwargs)return wrapperreturn decorator# 使用示例@rate_limit(calls=10, period=60)  # 每分钟最多 10 次defapi_call():pass

6. 调试与测试

6.1 本地测试工具函数

tests/test_weather.py

"""天气工具测试"""import unittestfrom tools.query import weather_queryclassTestWeatherQuery(unittest.TestCase):deftest_beijing(self):"""测试北京天气查询"""        result = weather_query("北京")self.assertIn("city", result)self.assertEqual(result["city"], "北京")self.assertIn("temperature", result)deftest_invalid_city(self):"""测试无效城市"""        result = weather_query("InvalidCity123")# 应该返回默认数据或错误self.assertIsInstance(result, dict)deftest_response_structure(self):"""测试返回数据结构"""        result = weather_query("上海")        required_keys = ["city""date""condition""temperature"]for key in required_keys:self.assertIn(key, result)if __name__ == "__main__":    unittest.main()

6.2 运行测试

# 运行单个测试文件python tests/test_weather.py# 运行所有测试python -m pytest tests/# 查看覆盖率python -m pytest tests/ --cov=tools

6.3 调试技巧

添加日志

import logginglogging.basicConfig(level=logging.DEBUG)logger = logging.getLogger(__name__)defweather_query(city: str):    logger.debug(f"查询天气:{city}")# ...    logger.info(f"查询成功:{result}")

使用断点

defdebug_function():import pdb; pdb.set_trace()  # 断点# ... 代码

7. 发布与分享

7.1 打包 Skill

# 创建压缩包cd ~/.openclaw/workspace/skills/weather-skilltar -czf weather-skill.tar.gz \    SKILL.md \    tools/ \    scripts/# 或创建 git 仓库git initgit add .git commit -m "Initial commit"git remote add origin https://github.com/yourname/weather-skill.gitgit push -u origin main

7.2 发布到 ClawHub

ClawHub 是 OpenClaw 的官方技能市场。

步骤

  1. 1. 在 clawhub.com 注册账号
  2. 2. 创建新 Skill 页面
  3. 3. 上传 SKILL.md 和源代码
  4. 4. 填写描述、截图、使用文档
  5. 5. 提交审核

7.3 分享文档

README.md 模板

# Weather Skill## 快速开始```bash# 安装git clone https://github.com/yourname/weather-skill.gitcp -r weather-skill ~/.openclaw/workspace/skills/# 配置# 在 openclaw.json 中添加技能路径# 使用openclaw chat "北京天气怎么样?"

功能

  • • 实时天气查询
  • • 3 天天气预报
  • • 空气质量指数

配置

{"api_key":"your-key-here","units":"metric"}

总结

开发 Skill 的核心步骤:

  1. 1. 编写 SKILL.md 文档
  2. 2. 实现工具函数
  3. 3. 本地测试
  4. 4. 注册到 OpenClaw
  5. 5. 发布分享

作者:阿财 | 更新时间:2026-03-16