摘要
OpenClaw 的技能(Skills)系统是其生态的核心组成部分,允许开发者扩展 AI 助手的能力边界。本文将详细介绍 Skills 的架构设计、开发流程、测试方法和发布规范。通过完整的实战案例,帮助开发者掌握从零开始创建高质量 Skill 的全过程,并了解如何将其发布到 ClawHub 技能市场供全球用户使用。
关键词:OpenClaw、Skills 开发、插件系统、ClawHub、AI 扩展
目录
- 一、引言
- [二、Skills 系统架构](#二 skills 系统架构)
- [三、Skill 类型与选择](#三 skill 类型与选择)
- 四、开发环境搭建
- [五、SKILL.md 规范详解](#五 skillmd 规范详解)
- [六、第一个 Skill 开发](#六第一个 skill 开发)
- [七、高级 Skill 开发技巧](#七高级 skill 开发技巧)
- 八、测试与调试
- [九、发布到 ClawHub](#九发布到 clawhub)
- [十、Skill 运营与推广](#十 skill 运营与推广)
- 总结
- 参考资料
一、引言
1.1 为什么需要 Skills?
虽然 OpenClaw 内置了丰富的工具,但面对无限的应用场景,我们需要:
- 领域专业化:医疗、法律、金融等垂直领域需要专门知识
- 服务集成:对接第三方 API(GitHub、Notion、Slack 等)
- 工作流自动化:封装复杂的业务流程
- 社区共建:开发者贡献自己的独特能力
Skills 系统正是为此而生,它让每个开发者都能:
1.2 Skills 生态系统
#mermaid-svg-Sr9AmWUjwN28G5hZ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sr9AmWUjwN28G5hZ .error-icon{fill:#552222;}#mermaid-svg-Sr9AmWUjwN28G5hZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sr9AmWUjwN28G5hZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .marker.cross{stroke:#333333;}#mermaid-svg-Sr9AmWUjwN28G5hZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sr9AmWUjwN28G5hZ p{margin:0;}#mermaid-svg-Sr9AmWUjwN28G5hZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .cluster-label text{fill:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .cluster-label span{color:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .cluster-label span p{background-color:transparent;}#mermaid-svg-Sr9AmWUjwN28G5hZ .label text,#mermaid-svg-Sr9AmWUjwN28G5hZ span{fill:#333;color:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .node rect,#mermaid-svg-Sr9AmWUjwN28G5hZ .node circle,#mermaid-svg-Sr9AmWUjwN28G5hZ .node ellipse,#mermaid-svg-Sr9AmWUjwN28G5hZ .node polygon,#mermaid-svg-Sr9AmWUjwN28G5hZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .rough-node .label text,#mermaid-svg-Sr9AmWUjwN28G5hZ .node .label text,#mermaid-svg-Sr9AmWUjwN28G5hZ .image-shape .label,#mermaid-svg-Sr9AmWUjwN28G5hZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sr9AmWUjwN28G5hZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .rough-node .label,#mermaid-svg-Sr9AmWUjwN28G5hZ .node .label,#mermaid-svg-Sr9AmWUjwN28G5hZ .image-shape .label,#mermaid-svg-Sr9AmWUjwN28G5hZ .icon-shape .label{text-align:center;}#mermaid-svg-Sr9AmWUjwN28G5hZ .node.clickable{cursor:pointer;}#mermaid-svg-Sr9AmWUjwN28G5hZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .arrowheadPath{fill:#333333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sr9AmWUjwN28G5hZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sr9AmWUjwN28G5hZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sr9AmWUjwN28G5hZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sr9AmWUjwN28G5hZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .cluster text{fill:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ .cluster span{color:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sr9AmWUjwN28G5hZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sr9AmWUjwN28G5hZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sr9AmWUjwN28G5hZ .icon-shape,#mermaid-svg-Sr9AmWUjwN28G5hZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sr9AmWUjwN28G5hZ .icon-shape p,#mermaid-svg-Sr9AmWUjwN28G5hZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sr9AmWUjwN28G5hZ .icon-shape rect,#mermaid-svg-Sr9AmWUjwN28G5hZ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sr9AmWUjwN28G5hZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sr9AmWUjwN28G5hZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sr9AmWUjwN28G5hZ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
技能类型
技能消费者
技能平台
技能提供者
官方团队
认证开发者
社区贡献者
ClawHub 市场
评论系统
评分系统
个人用户
企业用户
教育机构
Bundled Skills
Managed Skills
Workspace Skills
1.3 本文你将学到
- ✅ Skills 的三种类型及其适用场景
- ✅ 完整的开发环境和工具链配置
- ✅ SKILL.md 文档规范和最佳实践
- ✅ 从零开发一个实用的 Skill
- ✅ 测试、打包、发布的完整流程
- ✅ Skill 运营和推广策略
二、Skills 系统架构
2.1 Skill 目录结构
一个标准的 Skill 包含以下文件:
my-skill/
├── SKILL.md # 技能描述文档(必需)
├── icon.png # 技能图标(推荐)
├── screenshot.png # 截图(推荐)
├── manifest.json # 清单文件(可选)
├── src/ # 源代码目录
│ ├── __init__.py
│ ├── main.py # 主入口
│ └── utils.py # 工具函数
├── tests/ # 测试代码
│ ├── test_main.py
│ └── fixtures/ # 测试数据
├── requirements.txt # Python 依赖
├── package.json # Node.js 依赖(如果有)
└── README.md # 详细文档
2.2 Skill 生命周期
#mermaid-svg-i5ifgv4IdXJBzgIB{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-i5ifgv4IdXJBzgIB .error-icon{fill:#552222;}#mermaid-svg-i5ifgv4IdXJBzgIB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-i5ifgv4IdXJBzgIB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-i5ifgv4IdXJBzgIB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-i5ifgv4IdXJBzgIB .marker.cross{stroke:#333333;}#mermaid-svg-i5ifgv4IdXJBzgIB svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-i5ifgv4IdXJBzgIB p{margin:0;}#mermaid-svg-i5ifgv4IdXJBzgIB .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB .cluster-label text{fill:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB .cluster-label span{color:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB .cluster-label span p{background-color:transparent;}#mermaid-svg-i5ifgv4IdXJBzgIB .label text,#mermaid-svg-i5ifgv4IdXJBzgIB span{fill:#333;color:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB .node rect,#mermaid-svg-i5ifgv4IdXJBzgIB .node circle,#mermaid-svg-i5ifgv4IdXJBzgIB .node ellipse,#mermaid-svg-i5ifgv4IdXJBzgIB .node polygon,#mermaid-svg-i5ifgv4IdXJBzgIB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-i5ifgv4IdXJBzgIB .rough-node .label text,#mermaid-svg-i5ifgv4IdXJBzgIB .node .label text,#mermaid-svg-i5ifgv4IdXJBzgIB .image-shape .label,#mermaid-svg-i5ifgv4IdXJBzgIB .icon-shape .label{text-anchor:middle;}#mermaid-svg-i5ifgv4IdXJBzgIB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-i5ifgv4IdXJBzgIB .rough-node .label,#mermaid-svg-i5ifgv4IdXJBzgIB .node .label,#mermaid-svg-i5ifgv4IdXJBzgIB .image-shape .label,#mermaid-svg-i5ifgv4IdXJBzgIB .icon-shape .label{text-align:center;}#mermaid-svg-i5ifgv4IdXJBzgIB .node.clickable{cursor:pointer;}#mermaid-svg-i5ifgv4IdXJBzgIB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-i5ifgv4IdXJBzgIB .arrowheadPath{fill:#333333;}#mermaid-svg-i5ifgv4IdXJBzgIB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-i5ifgv4IdXJBzgIB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-i5ifgv4IdXJBzgIB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i5ifgv4IdXJBzgIB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-i5ifgv4IdXJBzgIB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i5ifgv4IdXJBzgIB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-i5ifgv4IdXJBzgIB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-i5ifgv4IdXJBzgIB .cluster text{fill:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB .cluster span{color:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-i5ifgv4IdXJBzgIB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-i5ifgv4IdXJBzgIB rect.text{fill:none;stroke-width:0;}#mermaid-svg-i5ifgv4IdXJBzgIB .icon-shape,#mermaid-svg-i5ifgv4IdXJBzgIB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i5ifgv4IdXJBzgIB .icon-shape p,#mermaid-svg-i5ifgv4IdXJBzgIB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-i5ifgv4IdXJBzgIB .icon-shape rect,#mermaid-svg-i5ifgv4IdXJBzgIB .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i5ifgv4IdXJBzgIB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-i5ifgv4IdXJBzgIB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-i5ifgv4IdXJBzgIB :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
官方
个人
创建 Skill
本地开发
测试验证
打包构建
发布到哪里?
ClawHub 审核
工作区使用
审核通过
上架销售/免费
自己使用
用户安装
手动更新
自动更新
2.3 Skill 运行时
# Python 示例:Skill 运行时容器
import importlib
import json
from pathlib import Path
from typing import Any, Dict, Optional
class SkillRuntime:
def __init__(self, skill_path: str):
self.skill_path = Path(skill_path)
self.manifest = None
self.instance = None
self.metadata = {}
def load(self):
"""加载 Skill"""
print(f"正在加载 Skill: {self.skill_path}")
# 1. 加载 SKILL.md
skill_md_path = self.skill_path / "SKILL.md"
if not skill_md_path.exists():
raise FileNotFoundError("缺少 SKILL.md 文件")
with open(skill_md_path, 'r', encoding='utf-8') as f:
skill_md_content = f.read()
# 2. 解析元数据(从 SKILL.md 提取)
self.metadata = self._parse_skill_md(skill_md_content)
# 3. 加载 manifest.json(如果存在)
manifest_path = self.skill_path / "manifest.json"
if manifest_path.exists():
with open(manifest_path, 'r', encoding='utf-8') as f:
self.manifest = json.load(f)
# 4. 导入 Python 模块
src_path = self.skill_path / "src"
if src_path.exists():
self._import_python_module(src_path)
print(f"Skill 加载完成:{self.metadata.get('name', 'Unknown')}")
return self
def _parse_skill_md(self, content: str) –> Dict:
"""解析 SKILL.md 提取元数据"""
metadata = {}
# 提取标题
import re
title_match = re.search(r'^#\\s+(.+)$', content, re.MULTILINE)
if title_match:
metadata['name'] = title_match.group(1).strip()
# 提取描述
desc_match = re.search(r'##\\s+Description\\s*\\n(.*?)(?=##|\\Z)', content, re.DOTALL)
if desc_match:
metadata['description'] = desc_match.group(1).strip()
# 提取版本
version_match = re.search(r'\\*\\*Version\\*\\*:\\s*(\\d+\\.\\d+\\.\\d+)', content)
if version_match:
metadata['version'] = version_match.group(1)
# 提取作者
author_match = re.search(r'\\*\\*Author\\*\\*:\\s*(.+)', content)
if author_match:
metadata['author'] = author_match.group(1).strip()
return metadata
def _import_python_module(self, src_path: Path):
"""导入 Python 模块"""
import sys
# 添加 src 到路径
sys.path.insert(0, str(src_path))
try:
# 导入主模块
main_module = importlib.import_module('main')
# 查找主类
if hasattr(main_module, 'SkillHandler'):
self.instance = main_module.SkillHandler()
print("Python SkillHandler 已实例化")
else:
print("警告:未找到 SkillHandler 类")
finally:
sys.path.remove(str(src_path))
def execute(self, action: str, **kwargs) –> Any:
"""执行 Skill 动作"""
if not self.instance:
raise RuntimeError("Skill 未初始化")
if hasattr(self.instance, action):
method = getattr(self.instance, action)
return method(**kwargs)
else:
raise AttributeError(f"未知动作:{action}")
# 使用示例
runtime = SkillRuntime("./my-skill")
runtime.load()
# 执行技能功能
result = runtime.execute("process_data", input="test")
print(f"执行结果:{result}")
三、Skill 类型与选择
3.1 Bundled Skills(官方捆绑包)
特点:
- ✅ OpenClaw 官方开发和维护
- ✅ 预装在系统中
- ✅ 最高质量和安全性保证
- ✅ 随系统更新自动升级
适用场景:
- 核心功能(GitHub、邮件处理、文件管理等)
- 通用需求(天气查询、新闻推送、日历管理等)
- 官方合作服务集成
示例:
bundled-skills/
├── github-integration/
├── email-handler/
├── file-manager/
└── calendar-sync/
3.2 Managed Skills(社区管理包)
特点:
- ✅ 社区开发者贡献
- ✅ 通过 ClawHub 分发
- ✅ 需要审核但有质量差异
- ✅ 支持付费和免费模式
适用场景:
- 垂直领域应用(法律咨询、医疗建议)
- 特定服务集成(Notion、Airtable)
- 行业专用工具(电商分析、SEO 优化)
提交流程:
#mermaid-svg-CzNrg2hr8sK5kib7{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CzNrg2hr8sK5kib7 .error-icon{fill:#552222;}#mermaid-svg-CzNrg2hr8sK5kib7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CzNrg2hr8sK5kib7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CzNrg2hr8sK5kib7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CzNrg2hr8sK5kib7 .marker.cross{stroke:#333333;}#mermaid-svg-CzNrg2hr8sK5kib7 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CzNrg2hr8sK5kib7 p{margin:0;}#mermaid-svg-CzNrg2hr8sK5kib7 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 .cluster-label text{fill:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 .cluster-label span{color:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 .cluster-label span p{background-color:transparent;}#mermaid-svg-CzNrg2hr8sK5kib7 .label text,#mermaid-svg-CzNrg2hr8sK5kib7 span{fill:#333;color:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 .node rect,#mermaid-svg-CzNrg2hr8sK5kib7 .node circle,#mermaid-svg-CzNrg2hr8sK5kib7 .node ellipse,#mermaid-svg-CzNrg2hr8sK5kib7 .node polygon,#mermaid-svg-CzNrg2hr8sK5kib7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CzNrg2hr8sK5kib7 .rough-node .label text,#mermaid-svg-CzNrg2hr8sK5kib7 .node .label text,#mermaid-svg-CzNrg2hr8sK5kib7 .image-shape .label,#mermaid-svg-CzNrg2hr8sK5kib7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CzNrg2hr8sK5kib7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CzNrg2hr8sK5kib7 .rough-node .label,#mermaid-svg-CzNrg2hr8sK5kib7 .node .label,#mermaid-svg-CzNrg2hr8sK5kib7 .image-shape .label,#mermaid-svg-CzNrg2hr8sK5kib7 .icon-shape .label{text-align:center;}#mermaid-svg-CzNrg2hr8sK5kib7 .node.clickable{cursor:pointer;}#mermaid-svg-CzNrg2hr8sK5kib7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CzNrg2hr8sK5kib7 .arrowheadPath{fill:#333333;}#mermaid-svg-CzNrg2hr8sK5kib7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CzNrg2hr8sK5kib7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CzNrg2hr8sK5kib7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CzNrg2hr8sK5kib7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CzNrg2hr8sK5kib7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CzNrg2hr8sK5kib7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CzNrg2hr8sK5kib7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CzNrg2hr8sK5kib7 .cluster text{fill:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 .cluster span{color:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CzNrg2hr8sK5kib7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CzNrg2hr8sK5kib7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CzNrg2hr8sK5kib7 .icon-shape,#mermaid-svg-CzNrg2hr8sK5kib7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CzNrg2hr8sK5kib7 .icon-shape p,#mermaid-svg-CzNrg2hr8sK5kib7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CzNrg2hr8sK5kib7 .icon-shape rect,#mermaid-svg-CzNrg2hr8sK5kib7 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CzNrg2hr8sK5kib7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CzNrg2hr8sK5kib7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CzNrg2hr8sK5kib7 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
失败
通过
风险
安全
问题
通过
开发者提交
格式检查
返回修改
安全扫描
人工复审
功能测试
上架 ClawHub
用户可见
3.3 Workspace Skills(工作区自定义包)
特点:
- ✅ 用户自行开发或下载
- ✅ 仅在当前工作区可用
- ✅ 无需审核,灵活定制
- ✅ 适合个人和企业内部使用
适用场景:
- 企业内部系统集成
- 个人工作流自动化
- 实验性功能测试
- 敏感数据处理
创建步骤:
# 1. 创建工作区技能目录
mkdir -p ~/.openclaw/workspace/skills/my-custom-skill
# 2. 创建基本文件
cd ~/.openclaw/workspace/skills/my-custom-skill
touch SKILL.md
touch src/main.py
touch requirements.txt
# 3. 编辑 SKILL.md
cat > SKILL.md << 'EOF'
# My Custom Skill
## Description
个人定制的內部工具
## Author
Your Name
## Version
1.0.0
EOF
# 4. 开发实现代码
# 在 src/main.py 中编写逻辑
3.4 类型选择指南
| 开发难度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 审核要求 | N/A | 严格 | 无 |
| 分发范围 | 全局 | 全平台 | 本地 |
| 更新方式 | 自动 | 自动/手动 | 手动 |
| 商业化 | ❌ | ✅ | ❌ |
| 适合阶段 | 官方核心功能 | 成熟产品 | 原型/内部 |
四、开发环境搭建
4.1 基础工具安装
# 1. 安装 Python 3.9+
python –version # 应该 >= 3.9
# 2. 安装 pnpm(Node.js 包管理器)
curl -fsSL https://get.pnpm.io/install.sh | sh –
pnpm –version
# 3. 安装 Git
git –version
# 4. 安装 OpenClaw CLI
npm install -g openclaw@latest
openclaw –version
4.2 创建开发工作区
# 1. 创建工作区目录
mkdir -p ~/openclaw-dev/skills
cd ~/openclaw-dev/skills
# 2. 克隆官方模板仓库
git clone https://github.com/openclaw/skill-template.git my-first-skill
cd my-first-skill
# 3. 安装依赖
pip install -r requirements.txt
# 4. 初始化 Git
git init
git add .
git commit -m "Initial commit"
4.3 开发工具推荐
VS Code 扩展
// .vscode/extensions.json
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"esbenp.prettier-vscode",
"redhat.vscode-yaml",
"docsify.docsify",
"shd101wyy.markdown-preview-enhanced"
]
}
项目配置文件
// .vscode/settings.json
{
"python.defaultInterpreterPath": "${workspaceFolder}/venv/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.provider": "black",
"editor.formatOnSave": true,
"editor.tabSize": 2,
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
".pytest_cache": true
}
}
4.4 虚拟环境管理
# 创建虚拟环境
cd ~/openclaw-dev/skills/my-first-skill
python -m venv venv
# 激活虚拟环境
# macOS/Linux
source venv/bin/activate
# Windows
venv\\Scripts\\activate
# 安装开发依赖
pip install -r requirements-dev.txt
# 验证安装
pip list
五、SKILL.md 规范详解
5.1 完整模板
# Skill 名称
## Description
这里是技能的详细描述,应该包括:
– 这个技能的主要功能
– 解决了什么问题
– 目标用户群体
– 使用场景说明
**字数建议**:100-300 字,简洁明了
## Features
– ✨ 特性 1:简要描述
– 🚀 特性 2:简要描述
– 🔒 特性 3:简要描述
– 🎨 特性 4:简要描述
## Requirements
### 系统要求
– Python >= 3.9
– Node.js >= 18 (如果需要)
– 其他依赖…
### API 密钥
某些功能可能需要:
– GitHub Token(访问 GitHub API)
– API Key(第三方服务)
## Installation
### 方法 1:通过 ClawHub
```bash
openclaw skills install skill-name
方法 2:手动安装
git clone https://github.com/your-username/skill-name.git
cd skill-name
pip install -r requirements.txt
Usage
基本用法
/user: 使用 skill-name 处理 XXX
高级功能
/user: skill-name 高级命令 –option value
配置示例
{
"skills": {
"skill-name": {
"apiKey": "your-api-key",
"enabled": true
}
}
}
API Reference
公开方法
method_name(param1, param2)
参数:
- param1 (str): 参数说明
- param2 (int): 参数说明
返回值:
- dict: 返回值说明
示例:
result = handler.method_name("value", 123)
Examples
示例 1:基本使用场景
输入:
请帮我分析这个项目
输出:
已分析完成,发现以下问题…
示例 2:复杂工作流
输入:
同步我的 GitHub issues 到 Notion
输出:
已同步 15 个 issues 到 Notion workspace
Troubleshooting
常见问题
Q: 安装时出现依赖冲突
A: 尝试使用虚拟环境:
python -m venv venv
source venv/bin/activate
pip install –upgrade pip
pip install -r requirements.txt
Q: API 调用失败
A: 检查以下几点:
Development
本地开发
# 克隆仓库
git clone https://github.com/your-username/skill-name.git
# 安装依赖
pip install -r requirements-dev.txt
# 运行测试
pytest tests/
# 运行示例
python examples/basic.py
提交代码
我们欢迎 PR!请遵循以下步骤:
Testing
单元测试
pytest tests/unit -v
集成测试
pytest tests/integration -v
Contributing
欢迎贡献代码、报告问题或提出建议!
License
本项目采用 MIT 许可证
Author
- 姓名: Your Name
- GitHub: @yourusername
- 邮箱: your.email@example.com
Acknowledgments
感谢以下项目和贡献者:
- OpenClaw 核心团队
- 所有贡献者
- …
Changelog
Version 1.0.0 (2024-01-01)
- ✨ 初始版本发布
- 🐛 修复已知问题
- 📝 完善文档
Version 0.1.0 (2023-12-01)
- 🎉 项目启动
### 5.2 元数据提取脚本
```python
# Python 示例:从 SKILL.md 提取元数据
import re
from pathlib import Path
from typing import Dict, Optional
class SkillMetadataExtractor:
def __init__(self, skill_md_path: str):
self.skill_md_path = Path(skill_md_path)
self.content = ""
self.metadata = {}
def load(self):
"""加载 SKILL.md"""
if not self.skill_md_path.exists():
raise FileNotFoundError(f"找不到 SKILL.md: {self.skill_md_path}")
with open(self.skill_md_path, 'r', encoding='utf-8') as f:
self.content = f.read()
return self
def extract_all(self) -> Dict:
"""提取所有元数据"""
self.metadata = {
'name': self._extract_name(),
'description': self._extract_description(),
'version': self._extract_version(),
'author': self._extract_author(),
'features': self._extract_features(),
'requirements': self._extract_requirements(),
'examples': self._extract_examples()
}
return self.metadata
def _extract_name(self) -> str:
"""提取名称"""
match = re.search(r'^#\\s+(.+)$', self.content, re.MULTILINE)
return match.group(1).strip() if match else "Unknown"
def _extract_description(self) -> str:
"""提取描述"""
match = re.search(r'##\\s+Description\\s*\\n(.*?)(?=##|\\Z)', self.content, re.DOTALL)
return match.group(1).strip() if match else ""
def _extract_version(self) -> Optional[str]:
"""提取版本"""
match = re.search(r'\\*\\*Version\\*\\*:\\s*(\\d+\\.\\d+\\.\\d+)', self.content)
return match.group(1) if match else None
def _extract_author(self) -> Optional[str]:
"""提取作者"""
match = re.search(r'\\*\\*Author\\*\\*:\\s*(.+)', self.content)
return match.group(1).strip() if match else None
def _extract_features(self) -> list:
"""提取特性列表"""
features = []
matches = re.findall(r'^[-*]\\s*[✨🚀🔒🎨]\\s*(.+)$', self.content, re.MULTILINE)
for match in matches:
features.append(match.strip())
return features
def _extract_requirements(self) -> Dict:
"""提取要求"""
reqs = {
'python': None,
'nodejs': None,
'api_keys': []
}
python_match = re.search(r'Python\\s*>=?\\s*(\\d+\\.\\d+)', self.content, re.IGNORECASE)
if python_match:
reqs['python'] = python_match.group(1)
node_match = re.search(r'Node\\.?js\\s*>=?\\s*(\\d+)', self.content, re.IGNORECASE)
if node_match:
reqs['nodejs'] = node_match.group(1)
return reqs
def validate(self) -> tuple:
"""验证 SKILL.md"""
errors = []
warnings = []
# 必需字段
if not self.metadata.get('name') or self.metadata['name'] == "Unknown":
errors.append("缺少有效的名称")
if not self.metadata.get('description'):
errors.append("缺少描述")
# 可选字段警告
if not self.metadata.get('version'):
warnings.append("建议添加版本号")
if not self.metadata.get('author'):
warnings.append("建议添加作者信息")
return errors, warnings
# 使用示例
extractor = SkillMetadataExtractor("./my-skill/SKILL.md")
extractor.load()
metadata = extractor.extract_all()
print(f"Skill 名称:{metadata['name']}")
print(f"描述:{metadata['description'][:100]}…")
print(f"版本:{metadata['version']}")
print(f"作者:{metadata['author']}")
print(f"特性:{metadata['features']}")
# 验证
errors, warnings = extractor.validate()
if errors:
print("\\n❌ 错误:")
for error in errors:
print(f" – {error}")
if warnings:
print("\\n⚠️ 警告:")
for warning in warnings:
print(f" – {warning}")
六、第一个 Skill 开发
6.1 项目初始化
# 1. 创建项目结构
mkdir -p github-helper/src/github_helper
cd github-helper
# 2. 创建必要文件
touch SKILL.md
touch src/github_helper/__init__.py
touch src/github_helper/main.py
touch src/github_helper/utils.py
touch requirements.txt
touch README.md
touch .gitignore
# 3. 初始化 Git
git init
6.2 编写 SKILL.md
# GitHub Helper
## Description
GitHub Helper 是一个强大的 GitHub 集成工具,让你可以通过自然语言与 GitHub 交互。
**主要功能**:
– 查询仓库信息和统计数据
– 管理 Issues 和 Pull Requests
– 查看 CI/CD 工作流状态
– 自动生成周报和月报
**适用场景**:
– 开发者日常 GitHub 管理
– 团队项目进度跟踪
– 开源项目维护
## Features
– ✨ 自然语言查询仓库信息
– 🚀 一键生成项目报告
– 🔒 安全的 Token 管理
– 🎨 美观的统计图表
## Requirements
### 系统要求
– Python >= 3.9
– OpenClaw >= 1.0.0
### API 密钥
需要 GitHub Personal Access Token:
1. 访问 https://github.com/settings/tokens
2. 生成新 token(勾选 repo 权限)
3. 在配置中设置
## Installation
```bash
openclaw skills install github-helper
Usage
基本命令
/user: 使用 github-helper 查询 openclaw/openclaw 仓库信息
/user: github-helper 生成我的本周贡献报告
/user: 查看 openclaw/openclaw 的 open issues
配置
在 ~/.openclaw/openclaw.json 中添加:
{
"skills": {
"github-helper": {
"token": "ghp_xxxxxxxxxxxx",
"enabled": true
}
}
}
Author
- Name: Your Name
- GitHub: @yourusername
Version
1.0.0
### 6.3 实现核心功能
```python
# src/github_helper/main.py
"""GitHub Helper Skill 主模块"""
import os
import requests
from typing import Dict, List, Optional
from datetime import datetime, timedelta
from .utils import format_number, parse_repo_url
class SkillHandler:
"""GitHub Helper 处理器"""
def __init__(self, config: Optional[Dict] = None):
"""
初始化处理器
Args:
config: 配置字典,包含 token 等信息
"""
self.config = config or {}
self.token = self.config.get('token') or os.getenv('GITHUB_TOKEN')
if not self.token:
raise ValueError("GitHub Token 未配置")
self.api_base = "https://api.github.com"
self.headers = {
'Authorization': f'token {self.token}',
'Accept': 'application/vnd.github.v3+json'
}
def get_repo_info(self, repo_full_name: str) -> Dict:
"""
获取仓库信息
Args:
repo_full_name: 仓库全名,如 "openclaw/openclaw"
Returns:
仓库信息字典
"""
url = f"{self.api_base}/repos/{repo_full_name}"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
data = response.json()
return {
'name': data['name'],
'full_name': data['full_name'],
'description': data['description'],
'stars': data['stargazers_count'],
'forks': data['forks_count'],
'watchers': data['watchers_count'],
'open_issues': data['open_issues_count'],
'language': data['language'],
'license': data['license']['name'] if data['license'] else None,
'created_at': data['created_at'],
'updated_at': data['updated_at'],
'url': data['html_url']
}
def list_issues(self, repo_full_name: str, state: str = 'open', limit: int = 10) -> List[Dict]:
"""
列出 Issues
Args:
repo_full_name: 仓库全名
state: 状态(open/closed/all)
limit: 数量限制
Returns:
Issue 列表
"""
url = f"{self.api_base}/repos/{repo_full_name}/issues"
params = {
'state': state,
'per_page': min(limit, 100)
}
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
issues = response.json()
return [
{
'number': issue['number'],
'title': issue['title'],
'state': issue['state'],
'user': issue['user']['login'],
'created_at': issue['created_at'],
'url': issue['html_url']
}
for issue in issues[:limit]
]
def get_contributor_stats(self, repo_full_name: str) -> Dict:
"""
获取贡献者统计
Args:
repo_full_name: 仓库全名
Returns:
统计信息字典
"""
# 获取贡献者列表
url = f"{self.api_base}/repos/{repo_full_name}/contributors"
params = {'per_page': 100}
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
contributors = response.json()
total_commits = sum(c['contributions'] for c in contributors)
return {
'total_contributors': len(contributors),
'total_commits': total_commits,
'top_contributors': [
{
'login': c['login'],
'contributions': c['contributions'],
'avatar': c['avatar_url']
}
for c in sorted(contributors, key=lambda x: x['contributions'], reverse=True)[:10]
]
}
def generate_weekly_report(self, repo_full_name: str) -> str:
"""
生成本周报告
Args:
repo_full_name: 仓库全名
Returns:
格式化的报告文本
"""
# 计算本周时间范围
now = datetime.now()
monday = now – timedelta(days=now.weekday())
report_lines = [
f"# 📊 GitHub 周报 – {repo_full_name}",
f"\\n**统计周期**: {monday.strftime('%Y-%m-%d')} 至今\\n",
]
# 获取仓库信息
repo_info = self.get_repo_info(repo_full_name)
report_lines.extend([
f"## 仓库概况",
f"- ⭐ Stars: {format_number(repo_info['stars'])}",
f"- 🍴 Forks: {format_number(repo_info['forks'])}",
f"- 📝 Open Issues: {format_number(repo_info['open_issues'])}",
f"- 💻 语言:{repo_info['language'] or 'N/A'}",
""
])
# 获取贡献者统计
stats = self.get_contributor_stats(repo_full_name)
report_lines.extend([
f"## 贡献者统计",
f"- 👥 总贡献者:{stats['total_contributors']}人",
f"- 🔢 总提交数:{format_number(stats['total_commits'])}",
"",
"### Top 贡献者",
])
for i, contributor in enumerate(stats['top_contributors'][:5], 1):
report_lines.append(
f"{i}. **{contributor['login']}**: {contributor['contributions']} 次提交"
)
# 获取最新 Issues
issues = self.list_issues(repo_full_name, limit=5)
if issues:
report_lines.extend(["", "## 最新 Issues"])
for issue in issues:
report_lines.append(
f"- [{issue['title']}]({issue['url']}) #{issue['number']}"
)
return "\\n".join(report_lines)
def process_command(self, command: str) -> str:
"""
处理用户命令
Args:
command: 用户命令
Returns:
响应文本
"""
command = command.lower().strip()
# 解析命令
if "查询" in command and "仓库" in command:
# 提取仓库名
repo = parse_repo_url(command)
if repo:
info = self.get_repo_info(repo)
return (
f"📦 **{info['full_name']}**\\n\\n"
f"{info['description']}\\n\\n"
f"⭐ {format_number(info['stars'])} | "
f"🍴 {format_number(info['forks'])} | "
f"📝 {format_number(info['open_issues'])}\\n\\n"
f"[查看仓库]({info['url']})"
)
elif "报告" in command or "周报" in command:
repo = parse_repo_url(command)
if repo:
return self.generate_weekly_report(repo)
elif "issue" in command or "问题" in command:
repo = parse_repo_url(command)
if repo:
issues = self.list_issues(repo)
lines = [f"## 最新 Issues in {repo}"]
for issue in issues:
lines.append(f"- #{issue['number']} {issue['title']}")
return "\\n".join(lines)
return "抱歉,我无法理解该命令。请尝试:\\n- 查询 XXX 仓库信息\\n- 生成 XXX 周报\\n- 查看 XXX 的 issues"
# 使用示例
if __name__ == "__main__":
handler = SkillHandler(config={'token': 'ghp_your_token_here'})
# 查询仓库信息
info = handler.get_repo_info("openclaw/openclaw")
print(f"仓库:{info['full_name']}")
print(f"Stars: {info['stars']}")
# 生成周报
report = handler.generate_weekly_report("openclaw/openclaw")
print(f"\\n{report}")
6.4 工具函数
# src/github_helper/utils.py
"""工具函数模块"""
import re
from typing import Optional
def format_number(num: int) –> str:
"""格式化数字(如 1.2k)"""
if num >= 1000000:
return f"{num / 1000000:.1f}M"
elif num >= 1000:
return f"{num / 1000:.1f}k"
else:
return str(num)
def parse_repo_url(text: str) –> Optional[str]:
"""
从文本中提取仓库全名
Args:
text: 输入文本
Returns:
仓库全名,如 "owner/repo"
"""
# 匹配 GitHub URL
url_pattern = r'github\\.com/([^/\\s]+)/([^/\\s]+)'
match = re.search(url_pattern, text)
if match:
return f"{match.group(1)}/{match.group(2)}"
# 匹配 owner/repo 格式
repo_pattern = r'([a-zA-Z0-9_-]+)/([a-zA-Z0-9_.-]+)'
match = re.search(repo_pattern, text)
if match:
return f"{match.group(1)}/{match.group(2)}"
return None
6.5 依赖配置
# requirements.txt
requests>=2.28.0
python-dotenv>=1.0.0
# requirements-dev.txt
-r requirements.txt
pytest>=7.0.0
black>=23.0.0
flake8>=6.0.0
mypy>=1.0.0
七、高级 Skill 开发技巧
7.1 异步处理
# src/async_helper.py
import asyncio
import aiohttp
from typing import List, Dict
class AsyncSkillHandler:
"""异步 Skill 处理器"""
def __init__(self, token: str):
self.token = token
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession(
headers={'Authorization': f'token {self.token}'}
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.session:
await self.session.close()
async def fetch_multiple_repos(self, repo_names: List[str]) –> List[Dict]:
"""
并发获取多个仓库信息
Args:
repo_names: 仓库名称列表
Returns:
仓库信息列表
"""
async def fetch_repo(name: str):
url = f"https://api.github.com/repos/{name}"
async with self.session.get(url) as response:
if response.status == 200:
return await response.json()
return None
tasks = [fetch_repo(name) for name in repo_names]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [r for r in results if isinstance(r, dict)]
async def batch_process(self, items: List[str], batch_size: int = 10):
"""批量处理"""
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
print(f"处理批次 {i // batch_size + 1}")
await self.process_batch(batch)
async def process_batch(self, batch: List[str]):
"""处理单个批次"""
# 实现具体处理逻辑
pass
# 使用示例
async def main():
repos = ["openclaw/openclaw", "microsoft/vscode", "python/cpython"]
async with AsyncSkillHandler(token="ghp_xxx") as handler:
results = await handler.fetch_multiple_repos(repos)
for repo in results:
print(f"{repo['full_name']}: ⭐ {repo['stargazers_count']}")
asyncio.run(main())
7.2 缓存机制
# src/cache_manager.py
import json
import hashlib
from pathlib import Path
from datetime import datetime, timedelta
from typing import Any, Optional
class CacheManager:
"""缓存管理器"""
def __init__(self, cache_dir: str = "~/.openclaw/cache"):
self.cache_dir = Path(cache_dir).expanduser()
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _generate_key(self, *args, **kwargs) –> str:
"""生成缓存键"""
key_str = f"{args}:{kwargs}"
return hashlib.md5(key_str.encode()).hexdigest()
def get(self, key: str, ttl_minutes: int = 30) –> Optional[Any]:
"""
获取缓存
Args:
key: 缓存键
ttl_minutes: 有效期(分钟)
Returns:
缓存的数据,过期或不存在返回 None
"""
cache_file = self.cache_dir / f"{key}.json"
if not cache_file.exists():
return None
try:
with open(cache_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 检查是否过期
cached_at = datetime.fromisoformat(data['_cached_at'])
if datetime.now() – cached_at > timedelta(minutes=ttl_minutes):
return None
return data['value']
except Exception:
return None
def set(self, key: str, value: Any):
"""设置缓存"""
cache_file = self.cache_dir / f"{key}.json"
data = {
'value': value,
'_cached_at': datetime.now().isoformat()
}
with open(cache_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def delete(self, key: str):
"""删除缓存"""
cache_file = self.cache_dir / f"{key}.json"
if cache_file.exists():
cache_file.unlink()
def clear(self):
"""清空缓存"""
for cache_file in self.cache_dir.glob("*.json"):
cache_file.unlink()
# 使用示例
cache = CacheManager()
# 设置缓存
cache.set("user_123", {"name": "Alice", "age": 30})
# 获取缓存
user_data = cache.get("user_123")
print(user_data) # {'name': 'Alice', 'age': 30}
# 带缓存的函数调用示例
def get_expensive_data(param: str):
cache_key = f"data_{param}"
# 先尝试从缓存获取
cached = cache.get(cache_key, ttl_minutes=60)
if cached:
print("缓存命中")
return cached
# 缓存未命中,执行实际计算
print("缓存未命中,计算中…")
result = {"param": param, "timestamp": datetime.now().isoformat()}
# 保存到缓存
cache.set(cache_key, result)
return result
7.3 错误处理与重试
# src/error_handler.py
import time
import random
from functools import wraps
from typing import Callable, Any
class RetryError(Exception):
"""重试失败异常"""
pass
def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
"""
重试装饰器
Args:
max_attempts: 最大重试次数
delay: 初始延迟(秒)
backoff: 延迟倍数
"""
def decorator(func: Callable) –> Callable:
@wraps(func)
def wrapper(*args, **kwargs) –> Any:
current_delay = delay
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt == max_attempts – 1:
# 最后一次尝试失败
break
# 等待后重试(添加随机抖动)
jitter = random.uniform(0.1, 0.3)
wait_time = current_delay + jitter
print(f"尝试 {attempt + 1} 失败:{e}")
print(f"等待 {wait_time:.2f}秒后重试…")
time.sleep(wait_time)
current_delay *= backoff
raise RetryError(f"重试{max_attempts}次后仍然失败") from last_exception
return wrapper
return decorator
# 使用示例
@retry(max_attempts=3, delay=1.0, backoff=2.0)
def unstable_api_call():
"""不稳定的 API 调用"""
import random
if random.random() < 0.7: # 70% 概率失败
raise ConnectionError("网络连接失败")
return "成功!"
try:
result = unstable_api_call()
print(result)
except RetryError as e:
print(f"最终失败:{e}")
八、测试与调试
8.1 单元测试
# tests/test_github_helper.py
import pytest
import responses
from src.github_helper.main import SkillHandler
class TestSkillHandler:
"""SkillHandler 测试类"""
@pytest.fixture
def handler(self):
"""测试夹具"""
return SkillHandler(config={'token': 'test_token'})
@responses.activate
def test_get_repo_info(self, handler):
"""测试获取仓库信息"""
# Mock API 响应
responses.add(
responses.GET,
"https://api.github.com/repos/test/repo",
json={
'name': 'repo',
'full_name': 'test/repo',
'stargazers_count': 100,
'forks_count': 20,
# … 其他字段
},
status=200
)
# 调用方法
info = handler.get_repo_info("test/repo")
# 断言
assert info['name'] == 'repo'
assert info['stars'] == 100
def test_format_number(self):
"""测试数字格式化"""
from src.github_helper.utils import format_number
assert format_number(500) == "500"
assert format_number(1500) == "1.5k"
assert format_number(1500000) == "1.5M"
def test_parse_repo_url(self):
"""测试仓库 URL 解析"""
from src.github_helper.utils import parse_repo_url
# URL 格式
assert parse_repo_url("https://github.com/owner/repo") == "owner/repo"
# 纯文本格式
assert parse_repo_url("查看 owner/repo 仓库") == "owner/repo"
# 无效格式
assert parse_repo_url("无效输入") is None
8.2 集成测试
# tests/test_integration.py
import os
import pytest
from src.github_helper.main import SkillHandler
@pytest.mark.skipif(not os.getenv('GITHUB_TOKEN'), reason="需要 GITHUB_TOKEN")
class TestIntegration:
"""集成测试"""
@pytest.fixture
def real_handler(self):
"""使用真实 Token 的处理器"""
return SkillHandler(config={'token': os.getenv('GITHUB_TOKEN')})
def test_real_repo_info(self, real_handler):
"""测试真实的 API 调用"""
info = real_handler.get_repo_info("openclaw/openclaw")
assert info['name'] == 'openclaw'
assert info['stars'] > 0
assert 'url' in info
def test_real_issues(self, real_handler):
"""测试获取真实 Issues"""
issues = real_handler.list_issues("openclaw/openclaw", limit=5)
assert len(issues) <= 5
for issue in issues:
assert 'number' in issue
assert 'title' in issue
8.3 运行测试
# 运行所有测试
pytest tests/ -v
# 只运行单元测试
pytest tests/unit -v
# 只运行集成测试(需要配置)
pytest tests/integration -v –integration
# 生成覆盖率报告
pytest –cov=src tests/ –cov-report=html
# 查看 HTML 报告
open htmlcov/index.html
九、发布到 ClawHub
9.1 发布前检查清单
## 发布前检查清单
### 文档
– [ ] SKILL.md 完整且准确
– [ ] README.md 详细说明使用方法
– [ ] 包含足够的示例
– [ ] 更新了 Changelog
### 代码
– [ ] 所有测试通过
– [ ] 代码格式化(black, flake8)
– [ ] 类型注解完整(mypy)
– [ ] 无敏感信息(Token、密码等)
### 安全
– [ ] 依赖无已知漏洞
– [ ] 无恶意代码
– [ ] 权限最小化
### 性能
– [ ] 响应时间 < 2 秒
– [ ] 内存使用合理
– [ ] 有适当的缓存机制
9.2 打包 Skill
# 1. 清理不必要的文件
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
find . -type f -name "*.pyo" -delete
rm -rf .pytest_cache .coverage htmlcov/
# 2. 创建发布包
cd ~/openclaw-dev/skills/github-helper
# 3. 验证包结构
tree -L 2
# 预期结构:
# .
# ├── SKILL.md
# ├── README.md
# ├── requirements.txt
# ├── src
# │ └── github_helper
# │ ├── __init__.py
# │ ├── main.py
# │ └── utils.py
# └── tests
# └── test_*.py
9.3 提交到 ClawHub
# 1. 登录 ClawHub
openclaw clawhub login
# 2. 验证 Skill
openclaw skills validate ./github-helper
# 3. 发布
openclaw skills publish ./github-helper \\
–category "Development Tools" \\
–tags "github,automation,productivity" \\
–license "MIT" \\
–price free # 或 –price 9.99
# 4. 查看发布状态
openclaw skills status github-helper
9.4 审核流程
#mermaid-svg-sk8JcjtYf5obE9ne{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sk8JcjtYf5obE9ne .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sk8JcjtYf5obE9ne .error-icon{fill:#552222;}#mermaid-svg-sk8JcjtYf5obE9ne .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sk8JcjtYf5obE9ne .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sk8JcjtYf5obE9ne .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sk8JcjtYf5obE9ne .marker.cross{stroke:#333333;}#mermaid-svg-sk8JcjtYf5obE9ne svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sk8JcjtYf5obE9ne p{margin:0;}#mermaid-svg-sk8JcjtYf5obE9ne .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-sk8JcjtYf5obE9ne .cluster-label text{fill:#333;}#mermaid-svg-sk8JcjtYf5obE9ne .cluster-label span{color:#333;}#mermaid-svg-sk8JcjtYf5obE9ne .cluster-label span p{background-color:transparent;}#mermaid-svg-sk8JcjtYf5obE9ne .label text,#mermaid-svg-sk8JcjtYf5obE9ne span{fill:#333;color:#333;}#mermaid-svg-sk8JcjtYf5obE9ne .node rect,#mermaid-svg-sk8JcjtYf5obE9ne .node circle,#mermaid-svg-sk8JcjtYf5obE9ne .node ellipse,#mermaid-svg-sk8JcjtYf5obE9ne .node polygon,#mermaid-svg-sk8JcjtYf5obE9ne .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sk8JcjtYf5obE9ne .rough-node .label text,#mermaid-svg-sk8JcjtYf5obE9ne .node .label text,#mermaid-svg-sk8JcjtYf5obE9ne .image-shape .label,#mermaid-svg-sk8JcjtYf5obE9ne .icon-shape .label{text-anchor:middle;}#mermaid-svg-sk8JcjtYf5obE9ne .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sk8JcjtYf5obE9ne .rough-node .label,#mermaid-svg-sk8JcjtYf5obE9ne .node .label,#mermaid-svg-sk8JcjtYf5obE9ne .image-shape .label,#mermaid-svg-sk8JcjtYf5obE9ne .icon-shape .label{text-align:center;}#mermaid-svg-sk8JcjtYf5obE9ne .node.clickable{cursor:pointer;}#mermaid-svg-sk8JcjtYf5obE9ne .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sk8JcjtYf5obE9ne .arrowheadPath{fill:#333333;}#mermaid-svg-sk8JcjtYf5obE9ne .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sk8JcjtYf5obE9ne .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sk8JcjtYf5obE9ne .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sk8JcjtYf5obE9ne .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sk8JcjtYf5obE9ne .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sk8JcjtYf5obE9ne .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sk8JcjtYf5obE9ne .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sk8JcjtYf5obE9ne .cluster text{fill:#333;}#mermaid-svg-sk8JcjtYf5obE9ne .cluster span{color:#333;}#mermaid-svg-sk8JcjtYf5obE9ne div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sk8JcjtYf5obE9ne .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sk8JcjtYf5obE9ne rect.text{fill:none;stroke-width:0;}#mermaid-svg-sk8JcjtYf5obE9ne .icon-shape,#mermaid-svg-sk8JcjtYf5obE9ne .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sk8JcjtYf5obE9ne .icon-shape p,#mermaid-svg-sk8JcjtYf5obE9ne .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sk8JcjtYf5obE9ne .icon-shape rect,#mermaid-svg-sk8JcjtYf5obE9ne .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sk8JcjtYf5obE9ne .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sk8JcjtYf5obE9ne .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sk8JcjtYf5obE9ne :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
格式错误
通过
发现风险
安全
发现问题
通过
确认安全
确认风险
提交发布
自动检查
返回修改
安全扫描
人工复审
功能测试
审核通过
上架 ClawHub
用户可见
拒绝发布
十、Skill 运营与推广
10.1 定价策略
| 免费 | $0 | 引流、建立影响力 | 提供基础功能,高级功能收费 |
| 低价 | $1-9 | 个人开发者工具 | 快速获客 |
| 中价 | $10-49 | 专业工具 | 主流选择 |
| 高价 | $50+ | 企业级解决方案 | 提供技术支持 |
10.2 营销渠道
ClawHub 市场优化
- 精准的标签和分类
- 吸引人的图标和截图
- 详细的文档和示例
社交媒体
- Twitter/X 发布更新
- LinkedIn 专业网络
- 微信公众号(中文用户)
技术社区
- GitHub Trending
- Hacker News
- V2EX
- 知乎
内容营销
- 撰写教程博客
- 制作视频教程
- 直播演示
10.3 用户反馈循环
#mermaid-svg-tMS2PnFwKAaJ7Beh{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tMS2PnFwKAaJ7Beh .error-icon{fill:#552222;}#mermaid-svg-tMS2PnFwKAaJ7Beh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tMS2PnFwKAaJ7Beh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .marker.cross{stroke:#333333;}#mermaid-svg-tMS2PnFwKAaJ7Beh svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tMS2PnFwKAaJ7Beh p{margin:0;}#mermaid-svg-tMS2PnFwKAaJ7Beh .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .cluster-label text{fill:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .cluster-label span{color:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .cluster-label span p{background-color:transparent;}#mermaid-svg-tMS2PnFwKAaJ7Beh .label text,#mermaid-svg-tMS2PnFwKAaJ7Beh span{fill:#333;color:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .node rect,#mermaid-svg-tMS2PnFwKAaJ7Beh .node circle,#mermaid-svg-tMS2PnFwKAaJ7Beh .node ellipse,#mermaid-svg-tMS2PnFwKAaJ7Beh .node polygon,#mermaid-svg-tMS2PnFwKAaJ7Beh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .rough-node .label text,#mermaid-svg-tMS2PnFwKAaJ7Beh .node .label text,#mermaid-svg-tMS2PnFwKAaJ7Beh .image-shape .label,#mermaid-svg-tMS2PnFwKAaJ7Beh .icon-shape .label{text-anchor:middle;}#mermaid-svg-tMS2PnFwKAaJ7Beh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .rough-node .label,#mermaid-svg-tMS2PnFwKAaJ7Beh .node .label,#mermaid-svg-tMS2PnFwKAaJ7Beh .image-shape .label,#mermaid-svg-tMS2PnFwKAaJ7Beh .icon-shape .label{text-align:center;}#mermaid-svg-tMS2PnFwKAaJ7Beh .node.clickable{cursor:pointer;}#mermaid-svg-tMS2PnFwKAaJ7Beh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .arrowheadPath{fill:#333333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tMS2PnFwKAaJ7Beh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tMS2PnFwKAaJ7Beh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tMS2PnFwKAaJ7Beh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tMS2PnFwKAaJ7Beh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .cluster text{fill:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh .cluster span{color:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tMS2PnFwKAaJ7Beh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tMS2PnFwKAaJ7Beh rect.text{fill:none;stroke-width:0;}#mermaid-svg-tMS2PnFwKAaJ7Beh .icon-shape,#mermaid-svg-tMS2PnFwKAaJ7Beh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tMS2PnFwKAaJ7Beh .icon-shape p,#mermaid-svg-tMS2PnFwKAaJ7Beh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tMS2PnFwKAaJ7Beh .icon-shape rect,#mermaid-svg-tMS2PnFwKAaJ7Beh .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tMS2PnFwKAaJ7Beh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tMS2PnFwKAaJ7Beh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tMS2PnFwKAaJ7Beh :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
发布 Skill
用户使用
收集反馈
分析问题
迭代改进
发布更新
好评如潮
更多用户
10.4 成功案例
案例 1:GitHub Helper
- 开发者:@devmaster
- 上线时间:2024 年 1 月
- 定价:免费 + 高级版 $9.99/月
- 成果:
- 首月下载量:500+
- 3 个月用户:2000+
- 月收入:$3000+
成功因素:
总结
本文全面介绍了 OpenClaw Skills 开发的完整流程:
核心要点
最佳实践
- ✅ 从简单的 Workspace Skill 开始练习
- ✅ 参考官方模板和优秀案例
- ✅ 编写全面的测试用例
- ✅ 保持文档与代码同步更新
- ✅ 积极响应用户反馈
- ✅ 持续改进和优化
下一步学习
- 研究 ClawHub 上的热门 Skills
- 参与开源项目积累经验
- 学习更多设计模式和架构知识
- 探索商业化可能性


