OpenClaw Skill 编写规范 与示例
完整的 Skill 开发指南,从基础结构到高级实践
📁 一、目录结构
标准结构
~/.openclaw/workspace/skills/<skill-name>/
├── SKILL.md # 必需:技能定义文件
├── scripts/ # 可选:执行脚本
│ ├── main.py
│ └── helper.sh
├── references/ # 可选:参考文档
│ └── api_examples.md
├── assets/ # 可选:静态资源
│ └── config_template.json
└── README.md # 可选:使用说明
最小结构
~/.openclaw/workspace/skills/<skill-name>/
└── SKILL.md # 唯一必需文件
📝 二、SKILL.md 标准格式
完整模板
markdown
---
name: <skill-name>
description: <一句话描述,说明触发场景和功能>
homepage: <https://...> # 可选:项目主页
metadata:
{
"openclaw": {
"emoji": "<emoji>",
"os": ["darwin", "linux"], # 可选:支持的系统
"requires": {
"bins": ["curl", "python3"], # 必需的二进制
"anyBins": ["node", "bun"], # 任一存在即可
"env": ["API_KEY"], # 必需的环境变量
"config": ["feature.enabled"] # 必需的配置项
},
"primaryEnv": "API_KEY", # 主环境变量(用于 apiKey 配置)
"skillKey": "<custom-key>", # 可选:自定义配置键
"install": [ # 可选:安装说明
{
"id": "brew",
"kind": "brew",
"formula": "package-name",
"bins": ["binary-name"],
"label": "Install Package (brew)"
},
{
"id": "node",
"kind": "node",
"package": "@scope/package",
"bins": ["binary-name"],
"label": "Install Package (npm)"
}
]
}
}
---
# <Skill Name> - 技能标题
简短介绍(1-2 句话)。
## 使用场景
✅ **USE when:**
- 用户询问/请求 X
- 需要执行 Y 操作
- 场景 Z
❌ **DON'T use when:**
- 情况 A(用其他技能)
- 情况 B(超出范围)
## 快速开始
```bash
# 基本用法
<command> <args>
# 进阶用法
<command> <args> --flag
详细用法
功能 1
说明 + 示例命令。
功能 2
说明 + 示例命令。
配置选项
json
{
"option1": "value",
"option2": 123
}
| 参数 | 说明 | 默认值 |
|---|---|---|
option1 |
说明 | default |
错误处理
| 错误 | 处理方式 |
|---|---|
| API 失败 | 重试 3 次,使用缓存 |
| 认证失败 | 提示用户配置 API Key |
安全注意
⚠️ 风险提示、限制条件、合规说明等。
相关文件
scripts/xxx.py- 说明references/yyy.md- 说明
依赖安装
bash
# 安装依赖
pip install ...
# 或
npm install ...
---
## 🏷️ 三、Frontmatter 字段详解
### 必需字段
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `name` | string | 技能唯一标识(小写,下划线分隔) | `stock_analyzer` |
| `description` | string | 触发场景 + 功能描述 | "股票分析。Use when user asks for stock..." |
### metadata.openclaw 字段
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `emoji` | string | 推荐 | Skills UI 显示的 emoji |
| `os` | string[] | 可选 | 支持的系统:`darwin`/`linux`/`win32` |
| `requires.bins` | string[] | 可选 | 所有必需的二进制文件 |
| `requires.anyBins` | string[] | 可选 | 任一存在即可的二进制 |
| `requires.env` | string[] | 可选 | 必需的环境变量 |
| `requires.config` | string[] | 可选 | 必需的 openclaw.json 配置路径 |
| `primaryEnv` | string | 可选 | 主环境变量(用于 apiKey 配置) |
| `skillKey` | string | 可选 | 自定义配置键(默认用 name) |
| `install` | object[] | 可选 | 安装程序定义 |
### 安装器格式
```json
{
"id": "brew", // 安装器 ID
"kind": "brew", // brew | node | go | uv | download
"formula": "package-name", // kind=brew 时必需
"package": "@scope/pkg", // kind=node 时必需
"bins": ["binary-name"], // 安装后提供的二进制
"label": "Install (brew)", // UI 显示文本
"os": ["darwin"] // 可选:仅限某些系统
}
🎯 四、技能类型判断
判断流程图
读 SKILL.md
↓
有 metadata.requires 吗?
├─ 有 → 需要外部工具
│ ├─ bins: ["curl"] → 系统工具技能
│ ├─ bins: ["oracle"] → 外部 CLI 技能
│ └─ 调用 mcporter → MCP 工具技能
│
└─ 没有 → 纯文本技能
└─ 用 exec/web_fetch 等基础工具实现
类型对比表
| 类型 | 特征 | 例子 | 调用方式 |
|---|---|---|---|
| 系统工具 | requires.bins: ["curl"] |
weather | exec 运行系统命令 |
| 外部 CLI | requires.bins: ["oracle"] + install |
oracle, himalaya | exec 运行 CLI |
| MCP 工具 | 调用 mcporter call |
mcporter | MCP 协议调用 |
| 纯文本 | 无 metadata.requires | 自定义业务逻辑 | exec/web_fetch 组合 |
📦 五、混合技能编写
场景:静态文本 + HTTP + MCP
markdown
---
name: hybrid_skill
description: 混合数据源技能
metadata:
{
"openclaw": {
"requires": {
"bins": ["curl", "python3", "mcporter"],
"env": ["API_KEY"],
"config": ["mcp.enabled"]
}
}
}
---
# Hybrid Skill
## 数据源总览
| 类型 | 来源 | 调用方式 |
|------|------|---------|
| 静态文本 | 本技能文档 | 直接阅读 |
| HTTP API | 公开 API | `curl` / `python` |
| MCP 服务 | MCP 服务器 | `mcporter call` |
## 调用流程
1. **静态文本**:读取配置规则
2. **HTTP 请求**:抓取数据
```bash
curl "https://api.example.com/data"
-
MCP 服务 :获取实时数据
bashmcporter call server.tool arg:value
降级方案
| 错误 | 降级方式 |
|---|---|
| MCP 不可用 | 降级到 HTTP API |
| HTTP 失败 | 使用缓存数据 |
---
## 🔐 六、安全规范
### 环境变量
```json
// ~/.openclaw/openclaw.json
{
"skills": {
"entries": {
"my-skill": {
"enabled": true,
"apiKey": {
"source": "env",
"provider": "default",
"id": "MY_API_KEY"
},
"env": {
"MY_API_KEY": "sk-xxx" // 或用 SecretRef
}
}
}
}
}
安全清单
- ✅ 不在 SKILL.md 中硬编码密钥
- ✅ 敏感操作前要求用户确认
- ✅ 外部输入需要验证/转义
- ✅ 使用
trash而非rm(可恢复) - ✅ 沙箱环境运行不受信代码
🧪 七、测试流程
1. 创建技能
bash
mkdir -p ~/.openclaw/workspace/skills/my-skill
# 创建 SKILL.md
2. 刷新技能
bash
# 方式 1:重启 Gateway
openclaw gateway restart
# 方式 2:等待自动刷新(watcher 默认启用)
3. 测试技能
bash
# 测试触发
openclaw agent --message "使用 my-skill 做 XXX"
# 或直接在聊天中测试
4. 调试
bash
# 查看技能加载状态
openclaw skills list
# 查看技能详情
openclaw skills show my-skill
📋 八、最佳实践
命名规范
- ✅
snake_case:stock_analyzer - ✅ 小写字母 + 下划线
- ❌ 不要大写字母
- ❌ 不要用空格或特殊字符
描述写法
markdown
# ✅ 好:包含触发场景
description: "股票分析。Use when user asks for stock recommendations, financial news..."
# ❌ 差:太模糊
description: "分析股票"
指令清晰
markdown
# ✅ 好:具体明确
"使用 curl 调用 wttr.in API,格式:curl 'wttr.in/{city}?format=3'"
# ❌ 差:模糊
"获取天气数据"
错误处理
markdown
## 错误处理
| 错误 | 处理方式 |
|------|---------|
| HTTP 429 | 等待 60 秒后重试 |
| HTTP 401 | 提示用户检查 API Key |
| 超时 | 使用缓存数据(如果有) |
文档结构
- 使用场景(USE/DON'T USE)
- 快速开始(最常用命令)
- 详细用法(分功能)
- 配置选项
- 错误处理
- 安全注意
- 相关文件
🔧 九、配置参考
openclaw.json 完整示例
json5
{
"skills": {
// 仅允许列表中的 bundled skills
"allowBundled": ["gemini", "peekaboo"],
// 加载配置
"load": {
"extraDirs": ["~/Projects/my-skills"], // 额外技能目录
"watch": true, // 自动刷新
"watchDebounceMs": 250
},
// 安装配置
"install": {
"preferBrew": true, // 优先 brew
"nodeManager": "npm" // npm | pnpm | yarn | bun
},
// 单个技能配置
"entries": {
"my-skill": {
"enabled": true,
"apiKey": "sk-xxx", // 或 SecretRef 对象
"env": {
"MY_API_KEY": "xxx"
},
"config": {
"custom_option": "value"
}
},
"another-skill": {
"enabled": false // 禁用
}
}
}
}
📚 十、参考资源
官方文档
- Creating Skills
- Skills Config
- ClawHub - 技能市场
示例技能
bash
# 系统自带技能
ls /opt/homebrew/lib/node_modules/openclaw/skills/
# 用户技能
ls ~/.openclaw/skills/
# 工作区技能
ls ~/.openclaw/workspace/skills/
优先级
工作区技能 (最高) → ~/.openclaw/skills → bundled skills (最低)
🚀 十一、快速模板
最简技能
markdown
---
name: hello
description: 打招呼技能。Use when user says hello or asks for a greeting.
---
# Hello Skill
当用户打招呼时,回复友好的问候语。
```bash
echo "Hello! How can I help you today?"
### 完整技能
复制上面的 [完整模板](#完整模板) 开始编写。
---
---
## 📘 十二、完整案例:天气活动规划师
这是一个**综合型技能**,根据天气情况推荐去哪里玩、适合做什么活动。
### 技能特点
- ✅ 使用 HTTP API 获取天气数据
- ✅ 静态文本定义活动规则(什么天气适合什么活动)
- ✅ 决策逻辑(天气 → 活动推荐)
- ✅ 多城市支持
- ✅ 降级方案(API 失败时的处理)
---
### 目录结构
~/.openclaw/workspace/skills/weather-activity-planner/
├── SKILL.md
├── scripts/
│ └── fetch_weather.py
├── references/
│ └── activity_rules.md
└── assets/
└── cities.json
---
### SKILL.md 完整示例
```markdown
---
name: weather_activity_planner
description: 根据天气推荐活动。Use when user asks where to go or what to do based on weather. 输入城市,输出活动建议。
homepage: https://wttr.in/:help
metadata:
{
"openclaw": {
"emoji": "🎯",
"requires": { "bins": ["curl", "python3"] },
"install": [
{ "id": "pip", "kind": "pip", "package": "requests", "label": "Install Python deps" }
]
}
}
---
# Weather Activity Planner - 天气活动规划师
根据实时天气情况,推荐适合的活动和去处。
## 使用场景
✅ **USE when:**
- "今天天气适合干什么?"
- "推荐个地方去玩"
- "周末去哪里玩比较好"
- "这个天气适合户外活动吗"
❌ **DON'T use when:**
- 需要精确到小时的预报(用专业天气 App)
- 极端天气警报(查看官方预警)
## 数据源
| 类型 | 来源 | 调用方式 |
|------|------|---------|
| **天气数据** | wttr.in API | `curl wttr.in/{city}?format=j1` |
| **活动规则** | 本技能文档(静态文本) | 直接阅读 |
| **城市列表** | `assets/cities.json` | 读取文件 |
## 天气 → 活动映射规则
### 🌞 晴天 (Sunny/Clear)
| 温度范围 | 推荐活动 | 推荐地点 |
|----------|---------|---------|
| >25°C | 游泳、海滩、水上乐园 | 海边、水上公园 |
| 15-25°C | 徒步、骑行、野餐、露营 | 公园、山区、郊外 |
| 5-15°C | 城市漫步、摄影、放风筝 | 市中心、广场、公园 |
| <5°C | 晒太阳、温泉 | 温泉度假村、露天咖啡厅 |
### ☁️ 多云 (Cloudy)
| 温度范围 | 推荐活动 | 推荐地点 |
|----------|---------|---------|
| >20°C | 徒步、骑行、观光 | 山区、景点 |
| 10-20°C | 城市探索、骑行 | 市区、古镇 |
| <10°C | 室内景点、咖啡厅 | 博物馆、书店 |
### 🌧️ 雨天 (Rainy)
| 雨量 | 推荐活动 | 推荐地点 |
|------|---------|---------|
| 小雨 | 室内活动、雨中漫步(带伞) | 商场、室内景点 |
| 中雨 | 纯室内活动 | 博物馆、电影院、室内游乐场 |
| 大雨 | 宅家、线上活动 | 家、酒店 |
### ❄️ 雪天 (Snowy)
| 雪量 | 推荐活动 | 推荐地点 |
|------|---------|---------|
| 小雪 | 赏雪、拍照、堆雪人 | 公园、山区 |
| 中雪 | 滑雪、温泉 | 滑雪场、温泉度假村 |
| 大雪 | 避免外出 | 室内 |
### 🌡️ 极端温度
| 情况 | 建议 |
|------|------|
| >35°C | 避免户外活动,选择室内空调场所 |
| <0°C | 保暖为主,选择室内或温泉 |
## 快速开始
### 基本用法
```bash
# 获取天气(JSON 格式)
curl -s "wttr.in/Beijing?format=j1"
# 使用 Python 脚本(推荐,自动解析)
python {baseDir}/scripts/fetch_weather.py --city Beijing
完整流程
用户问:"北京今天适合干什么?"
bash
# 1. 获取天气数据
curl -s "wttr.in/Beijing?format=j1" > /tmp/weather.json
# 2. 解析天气(使用 Python 脚本)
python {baseDir}/scripts/fetch_weather.py --city Beijing --output /tmp/result.json
# 3. 根据天气匹配活动规则(参考本文档的映射表)
# 输出推荐结果
脚本用法
fetch_weather.py
bash
# 基本用法
python {baseDir}/scripts/fetch_weather.py --city Beijing
# 指定日期(0=今天,1=明天,2=后天)
python {baseDir}/scripts/fetch_weather.py --city Beijing --day 1
# 输出详细结果
python {baseDir}/scripts/fetch_weather.py --city Beijing --verbose
# 保存结果
python {baseDir}/scripts/fetch_weather.py --city Beijing --output result.json
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
--city |
城市名(英文或拼音) | 必需 |
--day |
日期(0-2) | 0(今天) |
--output |
输出文件 | stdout |
--verbose |
详细输出 | false |
输出示例
输入
用户:上海今天适合干什么?
输出
📍 上海天气概览
━━━━━━━━━━━━━━━━━━━━
🌤️ 天气:多云
🌡️ 温度:18°C (体感 17°C)
💨 风力:3 级 东北风
💧 湿度:65%
🌧️ 降水概率:10%
━━━━━━━━━━━━━━━━━━━━
🎯 推荐活动
━━━━━━━━━━━━━━━━━━━━
✅ 最佳推荐:城市探索、骑行
📍 推荐地点:
• 外滩(城市漫步)
• 法租界(骑行)
• 田子坊(拍照)
📋 活动建议:
• 温度适宜,适合户外活动
• 多云天气,拍照光线柔和
• 建议带薄外套,早晚较凉
⚠️ 注意事项:
• 降水概率低,无需带伞
• 风力适中,不影响活动
配置选项
在 assets/cities.json 中配置热门城市:
json
{
"cities": [
{ "name": "北京", "en": "Beijing", "region": "华北" },
{ "name": "上海", "en": "Shanghai", "region": "华东" },
{ "name": "广州", "en": "Guangzhou", "region": "华南" }
]
}
错误处理
| 错误 | 处理方式 |
|---|---|
| API 返回失败 | 重试 3 次,提示用户稍后再试 |
| 城市名无效 | 提示用户检查城市名,建议用拼音 |
| 无天气数据 | 使用备用 API 或提示无法获取 |
降级方案
wttr.in API 失败
↓
尝试备用 API (Open-Meteo)
↓
提示用户查看当地天气预报
安全注意
⚠️ 仅供参考 - 实际出行前请查看最新天气预报
⚠️ 极端天气 - 暴雨、台风等请遵循官方指引
⚠️ 个人差异 - 老人、儿童、孕妇需额外注意
相关文件
scripts/fetch_weather.py- 天气获取脚本references/activity_rules.md- 详细活动规则assets/cities.json- 城市列表
依赖安装
bash
# Python 依赖
pip install requests
# 验证安装
python -c "import requests; print('OK')"
扩展建议
添加 MCP 服务(可选)
如果有 MCP 本地生活服务,可以整合:
bash
# 调用 MCP 获取附近景点
mcporter call local.get_attractions city:"Beijing" type:"outdoor"
# 调用 MCP 预订门票
mcporter call local.book_ticket attraction:"Great Wall" date:"2026-03-15"
添加用户偏好
在配置中记录用户偏好:
json
{
"user_preferences": {
"prefer_outdoor": true,
"max_walking_distance": "5km",
"budget": "medium"
}
}
---
### scripts/fetch_weather.py 示例
```python
#!/usr/bin/env python3
"""
Weather Activity Planner - 天气获取脚本
根据天气数据推荐适合的活动
"""
import requests
import json
import argparse
from datetime import datetime
# 天气 → 活动映射
ACTIVITY_MAP = {
"sunny": {
"hot": {"temp_min": 25, "activities": ["游泳", "海滩", "水上乐园"], "places": ["海边", "水上公园"]},
"warm": {"temp_min": 15, "activities": ["徒步", "骑行", "野餐", "露营"], "places": ["公园", "山区", "郊外"]},
"cool": {"temp_min": 5, "activities": ["城市漫步", "摄影", "放风筝"], "places": ["市中心", "广场", "公园"]},
"cold": {"temp_min": -999, "activities": ["晒太阳", "温泉"], "places": ["温泉度假村", "露天咖啡厅"]},
},
"cloudy": {
"warm": {"temp_min": 20, "activities": ["徒步", "骑行", "观光"], "places": ["山区", "景点"]},
"cool": {"temp_min": 10, "activities": ["城市探索", "骑行"], "places": ["市区", "古镇"]},
"cold": {"temp_min": -999, "activities": ["室内景点", "咖啡厅"], "places": ["博物馆", "书店"]},
},
"rainy": {
"light": {"activities": ["室内活动", "雨中漫步"], "places": ["商场", "室内景点"]},
"medium": {"activities": ["纯室内活动"], "places": ["博物馆", "电影院", "室内游乐场"]},
"heavy": {"activities": ["宅家", "线上活动"], "places": ["家", "酒店"]},
},
}
def get_weather(city, day=0):
"""获取天气数据"""
url = f"https://wttr.in/{city}?format=j1"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"❌ 获取天气失败:{e}")
return None
def parse_weather(data, day=0):
"""解析天气数据"""
if not data or "current_condition" not in data:
return None
current = data["current_condition"][0]
forecast = data["weather"][day] if "weather" in data else None
return {
"city": data.get("nearest_area", [{}])[0].get("areaName", [{}])[0].get("value", "Unknown"),
"condition": current.get("weatherDesc", [{}])[0].get("value", "Unknown"),
"condition_code": current.get("weatherCode", "0"),
"temp_c": int(current.get("temp_C", 0)),
"feels_like": int(current.get("FeelsLikeC", 0)),
"humidity": current.get("humidity", 0),
"wind_speed": current.get("windspeedKmph", 0),
"wind_dir": current.get("winddir16Point", ""),
"precip_prob": forecast.get("avgdailyChanceOfRain", 0) if forecast else 0,
}
def recommend_activity(weather):
"""根据天气推荐活动"""
condition = weather["condition"].lower()
temp = weather["temp_c"]
precip = weather["precip_prob"]
# 判断天气类型
if "rain" in condition or precip > 50:
weather_type = "rainy"
if precip > 70:
rain_level = "heavy"
elif precip > 30:
rain_level = "medium"
else:
rain_level = "light"
activities = ACTIVITY_MAP.get("rainy", {}).get(rain_level, {}).get("activities", ["室内活动"])
places = ACTIVITY_MAP.get("rainy", {}).get(rain_level, {}).get("places", ["室内场所"])
elif "cloud" in condition or "overcast" in condition:
weather_type = "cloudy"
if temp >= 20:
level = "warm"
elif temp >= 10:
level = "cool"
else:
level = "cold"
activities = ACTIVITY_MAP.get("cloudy", {}).get(level, {}).get("activities", ["室内活动"])
places = ACTIVITY_MAP.get("cloudy", {}).get(level, {}).get("places", ["室内场所"])
elif "sun" in condition or "clear" in condition:
weather_type = "sunny"
if temp >= 25:
level = "hot"
elif temp >= 15:
level = "warm"
elif temp >= 5:
level = "cool"
else:
level = "cold"
activities = ACTIVITY_MAP.get("sunny", {}).get(level, {}).get("activities", ["室内活动"])
places = ACTIVITY_MAP.get("sunny", {}).get(level, {}).get("places", ["室内场所"])
else:
activities = ["室内活动"]
places = ["室内场所"]
return {
"activities": activities,
"places": places,
"weather_type": weather_type,
}
def format_output(weather, recommendation):
"""格式化输出"""
output = []
output.append(f"📍 {weather['city']}天气概览")
output.append("━" * 40)
output.append(f"🌤️ 天气:{weather['condition']}")
output.append(f"🌡️ 温度:{weather['temp_c']}°C (体感 {weather['feels_like']}°C)")
output.append(f"💨 风力:{weather['wind_speed']}km/h {weather['wind_dir']}")
output.append(f"💧 湿度:{weather['humidity']}%")
output.append(f"🌧️ 降水概率:{weather['precip_prob']}%")
output.append("")
output.append("━" * 40)
output.append("🎯 推荐活动")
output.append("━" * 40)
output.append("")
output.append(f"✅ 最佳推荐:{'、'.join(recommendation['activities'])}")
output.append("")
output.append("📍 推荐地点:")
for place in recommendation['places']:
output.append(f" • {place}")
output.append("")
output.append("📋 活动建议:")
if weather['temp_c'] >= 25:
output.append(" • 温度较高,注意防晒补水")
elif weather['temp_c'] <= 10:
output.append(" • 温度较低,注意保暖")
else:
output.append(" • 温度适宜,适合户外活动")
if weather['precip_prob'] > 50:
output.append(" • 降水概率高,建议带伞")
else:
output.append(" • 降水概率低,无需带伞")
output.append("")
output.append("⚠️ 注意事项:")
output.append(" • 仅供参考,出行前请查看最新预报")
output.append(" • 极端天气请遵循官方指引")
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(description="Weather Activity Planner")
parser.add_argument("--city", required=True, help="城市名(英文或拼音)")
parser.add_argument("--day", type=int, default=0, help="日期(0=今天,1=明天,2=后天)")
parser.add_argument("--output", help="输出文件路径")
parser.add_argument("--verbose", action="store_true", help="详细输出")
args = parser.parse_args()
# 获取天气
if args.verbose:
print(f"🔄 正在获取 {args.city} 的天气数据...")
weather_data = get_weather(args.city, args.day)
if not weather_data:
print("❌ 无法获取天气数据,请稍后再试")
return
# 解析天气
weather = parse_weather(weather_data, args.day)
if not weather:
print("❌ 无法解析天气数据")
return
if args.verbose:
print(f"✅ 天气数据获取成功")
# 推荐活动
recommendation = recommend_activity(weather)
# 格式化输出
output = format_output(weather, recommendation)
# 输出结果
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(output)
print(f"✅ 结果已保存到 {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
使用示例
bash
# 基本用法
python {baseDir}/scripts/fetch_weather.py --city Beijing
# 明天天气
python {baseDir}/scripts/fetch_weather.py --city Shanghai --day 1
# 保存结果
python {baseDir}/scripts/fetch_weather.py --city Guangzhou --output result.json
# 详细模式
python {baseDir}/scripts/fetch_weather.py --city Shenzhen --verbose
这个案例展示了
| 技能要素 | 实现方式 |
|---|---|
| 静态文本 | 天气→活动映射规则表 |
| HTTP 请求 | curl wttr.in/{city}?format=j1 |
| 本地脚本 | fetch_weather.py 处理逻辑 |
| 决策逻辑 | 根据天气条件匹配活动 |
| 降级方案 | API 失败→备用 API→提示用户 |
| 配置选项 | cities.json 城市列表 |
| 错误处理 | 超时、无效城市、无数据 |