From 2774069f8efea9021ea96609e08d64d6baaad6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Sat, 4 Apr 2026 21:06:56 +0800 Subject: [PATCH] add weather --- skills/caiyun-weather/SKILL.md | 102 +++++ skills/caiyun-weather/_meta.json | 6 + .../caiyun-weather/scripts/caiyun_weather.py | 270 +++++++++++ skills/weather-china/SKILL.md | 73 +++ skills/weather-china/_meta.json | 6 + skills/weather-china/lib/weather_cn.py | 425 ++++++++++++++++++ skills/weather-forecasts/SKILL.md | 93 ---- skills/weather-forecasts/_meta.json | 6 - 8 files changed, 882 insertions(+), 99 deletions(-) create mode 100644 skills/caiyun-weather/SKILL.md create mode 100644 skills/caiyun-weather/_meta.json create mode 100644 skills/caiyun-weather/scripts/caiyun_weather.py create mode 100644 skills/weather-china/SKILL.md create mode 100644 skills/weather-china/_meta.json create mode 100644 skills/weather-china/lib/weather_cn.py delete mode 100644 skills/weather-forecasts/SKILL.md delete mode 100644 skills/weather-forecasts/_meta.json diff --git a/skills/caiyun-weather/SKILL.md b/skills/caiyun-weather/SKILL.md new file mode 100644 index 0000000..13fad5c --- /dev/null +++ b/skills/caiyun-weather/SKILL.md @@ -0,0 +1,102 @@ +--- +name: caiyun-weather +description: "通过彩云天气 API 查询天气数据 — 实时天气、逐小时/一周预报、历史天气和天气预警。当用户询问任何城市的天气、温度、空气质量、天气预报、降雨概率、历史天气或天气预警时使用此技能。需要设置 CAIYUN_WEATHER_API_TOKEN 环境变量。Use when: user asks about current weather, temperature, air quality, forecast, rain, historical weather, or alerts for any city." +metadata: + { + "openclaw": + { + "requires": + { + "bins": ["python3"], + "env": ["CAIYUN_WEATHER_API_TOKEN"], + }, + "primaryEnv": "CAIYUN_WEATHER_API_TOKEN", + }, + } +--- + +# 彩云天气 (Caiyun Weather) + +通过彩云天气 API 查询天气数据。支持直接使用城市名称(中文或英文),无需提供经纬度。 + +## 前置条件 + +使用前需设置环境变量: + +```bash +export CAIYUN_WEATHER_API_TOKEN="你的API密钥" +``` + +免费申请 API 密钥:https://docs.caiyunapp.com/weather-api/ + +## 何时使用 + +✅ **使用此技能:** +- "北京现在天气怎么样?" +- "上海明天会下雨吗?" +- "深圳未来一周天气预报" +- "广州空气质量如何?" +- "杭州过去24小时的天气" +- "成都有没有天气预警?" +- "What's the weather in Beijing?" +- 用户询问任何城市的天气、温度、空气质量、预报或预警 + +❌ **不要使用此技能:** +- 气候趋势分析或长期历史数据 +- 航空/航海专业气象(METAR、TAF) +- 用户未配置彩云天气 API Token + +## 命令 + +使用 `--city` 加城市名称(中文或英文)。如需精确定位,可使用 `--lng` 和 `--lat`。 + +### 实时天气 + +```bash +python3 "{{skill_path}}/scripts/caiyun_weather.py" realtime --city "北京" +``` + +### 逐小时预报(72小时) + +```bash +python3 "{{skill_path}}/scripts/caiyun_weather.py" hourly --city "上海" +``` + +### 一周预报(7天) + +```bash +python3 "{{skill_path}}/scripts/caiyun_weather.py" weekly --city "深圳" +``` + +### 历史天气(过去24小时) + +```bash +python3 "{{skill_path}}/scripts/caiyun_weather.py" history --city "杭州" +``` + +### 天气预警 + +```bash +python3 "{{skill_path}}/scripts/caiyun_weather.py" alerts --city "成都" +``` + +### 使用坐标(可选) + +对于无法通过城市名识别的地点: + +```bash +python3 "{{skill_path}}/scripts/caiyun_weather.py" realtime --lng 116.4074 --lat 39.9042 +``` + +## 内置城市(即时查询) + +北京、上海、广州、深圳、杭州、成都、武汉、南京、重庆、西安、天津、苏州、郑州、长沙、青岛、大连、厦门、昆明、贵阳、哈尔滨、沈阳、长春、福州、合肥、济南、南昌、石家庄、太原、呼和浩特、南宁、海口、三亚、拉萨、乌鲁木齐、兰州、西宁、银川、香港、澳门、台北、珠海、东莞、佛山、无锡、宁波、温州 + +英文名和其他全球城市通过在线地理编码自动解析。 + +## 说明 + +- 脚本仅使用 Python 标准库,无需 pip 安装 +- 内置城市即时解析,其他城市通过 OpenStreetMap 地理编码 +- API 对中国地区数据最为精准 +- 有频率限制,请避免短时间内频繁请求 diff --git a/skills/caiyun-weather/_meta.json b/skills/caiyun-weather/_meta.json new file mode 100644 index 0000000..4be8f2c --- /dev/null +++ b/skills/caiyun-weather/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn72jm08z8zg612dswg2c11sbs80w1w8", + "slug": "caiyun-weather", + "version": "1.1.0", + "publishedAt": 1772199654265 +} \ No newline at end of file diff --git a/skills/caiyun-weather/scripts/caiyun_weather.py b/skills/caiyun-weather/scripts/caiyun_weather.py new file mode 100644 index 0000000..4f46be2 --- /dev/null +++ b/skills/caiyun-weather/scripts/caiyun_weather.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Caiyun Weather API client for OpenClaw skill.""" + +import argparse +import json +import os +import sys +import urllib.request +import urllib.parse +from datetime import datetime, timedelta + + +API_BASE = "https://api.caiyunapp.com/v2.6" + +# Built-in city coordinates (lng, lat) +CITY_COORDS = { + "北京": (116.4074, 39.9042), "beijing": (116.4074, 39.9042), + "上海": (121.4737, 31.2304), "shanghai": (121.4737, 31.2304), + "广州": (113.2644, 23.1291), "guangzhou": (113.2644, 23.1291), + "深圳": (114.0579, 22.5431), "shenzhen": (114.0579, 22.5431), + "杭州": (120.1551, 30.2741), "hangzhou": (120.1551, 30.2741), + "成都": (104.0665, 30.5728), "chengdu": (104.0665, 30.5728), + "武汉": (114.3054, 30.5931), "wuhan": (114.3054, 30.5931), + "南京": (118.7969, 32.0603), "nanjing": (118.7969, 32.0603), + "重庆": (106.5516, 29.5630), "chongqing": (106.5516, 29.5630), + "西安": (108.9402, 34.2615), "xian": (108.9402, 34.2615), + "天津": (117.1901, 39.1256), "tianjin": (117.1901, 39.1256), + "苏州": (120.5853, 31.2989), "suzhou": (120.5853, 31.2989), + "郑州": (113.6254, 34.7466), "zhengzhou": (113.6254, 34.7466), + "长沙": (112.9388, 28.2282), "changsha": (112.9388, 28.2282), + "青岛": (120.3826, 36.0671), "qingdao": (120.3826, 36.0671), + "大连": (121.6147, 38.9140), "dalian": (121.6147, 38.9140), + "厦门": (118.0894, 24.4798), "xiamen": (118.0894, 24.4798), + "昆明": (102.8329, 25.0389), "kunming": (102.8329, 25.0389), + "贵阳": (106.6302, 26.6477), "guiyang": (106.6302, 26.6477), + "哈尔滨": (126.5350, 45.8038), "haerbin": (126.5350, 45.8038), + "沈阳": (123.4315, 41.8057), "shenyang": (123.4315, 41.8057), + "长春": (125.3235, 43.8171), "changchun": (125.3235, 43.8171), + "福州": (119.2965, 26.0745), "fuzhou": (119.2965, 26.0745), + "合肥": (117.2272, 31.8206), "hefei": (117.2272, 31.8206), + "济南": (117.0009, 36.6758), "jinan": (117.0009, 36.6758), + "南昌": (115.8581, 28.6820), "nanchang": (115.8581, 28.6820), + "石家庄": (114.5149, 38.0428), "shijiazhuang": (114.5149, 38.0428), + "太原": (112.5489, 37.8706), "taiyuan": (112.5489, 37.8706), + "呼和浩特": (111.7490, 40.8424), "huhehaote": (111.7490, 40.8424), + "南宁": (108.3200, 22.8240), "nanning": (108.3200, 22.8240), + "海口": (110.3494, 20.0174), "haikou": (110.3494, 20.0174), + "三亚": (109.5120, 18.2528), "sanya": (109.5120, 18.2528), + "拉萨": (91.1322, 29.6600), "lasa": (91.1322, 29.6600), + "乌鲁木齐": (87.6168, 43.8256), "wulumuqi": (87.6168, 43.8256), + "兰州": (103.8343, 36.0611), "lanzhou": (103.8343, 36.0611), + "西宁": (101.7782, 36.6171), "xining": (101.7782, 36.6171), + "银川": (106.2782, 38.4664), "yinchuan": (106.2782, 38.4664), + "香港": (114.1694, 22.3193), "hongkong": (114.1694, 22.3193), + "澳门": (113.5439, 22.1987), "macau": (113.5439, 22.1987), + "台北": (121.5654, 25.0330), "taipei": (121.5654, 25.0330), + "珠海": (113.5767, 22.2710), "zhuhai": (113.5767, 22.2710), + "东莞": (113.7518, 23.0208), "dongguan": (113.7518, 23.0208), + "佛山": (113.1214, 23.0218), "foshan": (113.1214, 23.0218), + "无锡": (120.3119, 31.4912), "wuxi": (120.3119, 31.4912), + "宁波": (121.5440, 29.8683), "ningbo": (121.5440, 29.8683), + "温州": (120.6994, 28.0015), "wenzhou": (120.6994, 28.0015), +} + + +def get_token(): + token = os.environ.get("CAIYUN_WEATHER_API_TOKEN") + if not token: + print("Error: CAIYUN_WEATHER_API_TOKEN environment variable is not set.", file=sys.stderr) + print("Apply for a free API key at: https://docs.caiyunapp.com/weather-api/", file=sys.stderr) + sys.exit(1) + return token + + +def geocode_city(city_name): + """Look up city coordinates using Nominatim (OpenStreetMap) as fallback.""" + encoded = urllib.parse.quote(city_name) + url = f"https://nominatim.openstreetmap.org/search?q={encoded}&format=json&limit=1" + req = urllib.request.Request(url) + req.add_header("User-Agent", "caiyun-weather-skill/1.0") + try: + with urllib.request.urlopen(req, timeout=10) as resp: + results = json.loads(resp.read().decode("utf-8")) + if results: + lat = float(results[0]["lat"]) + lng = float(results[0]["lon"]) + name = results[0].get("display_name", city_name).split(",")[0] + return lng, lat, name + except Exception: + pass + return None, None, None + + +def resolve_location(args): + """Resolve lng/lat from --city or --lng/--lat arguments.""" + if args.city: + city_key = args.city.strip().lower() + if city_key in CITY_COORDS: + lng, lat = CITY_COORDS[city_key] + return lng, lat + # Also try the original case + if args.city.strip() in CITY_COORDS: + lng, lat = CITY_COORDS[args.city.strip()] + return lng, lat + # Fallback: geocode via Nominatim + lng, lat, name = geocode_city(args.city) + if lng is not None: + return lng, lat + print(f"Error: Cannot find coordinates for city '{args.city}'.", file=sys.stderr) + print("Please use --lng and --lat to specify coordinates manually.", file=sys.stderr) + sys.exit(1) + elif args.lng is not None and args.lat is not None: + return args.lng, args.lat + else: + print("Error: Please specify --city or both --lng and --lat.", file=sys.stderr) + sys.exit(1) + + +def make_request(url, params=None): + if params: + query = "&".join(f"{k}={v}" for k, v in params.items()) + url = f"{url}?{query}" + req = urllib.request.Request(url) + req.add_header("User-Agent", "caiyun-weather-skill/1.0") + try: + with urllib.request.urlopen(req, timeout=15) as resp: + return json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"Connection Error: {e.reason}", file=sys.stderr) + sys.exit(1) + + +def format_percent(value): + return f"{value * 100:.0f}%" + + +def cmd_realtime(args): + token = get_token() + lng, lat = resolve_location(args) + url = f"{API_BASE}/{token}/{lng},{lat}/realtime" + data = make_request(url, {"lang": "en_US", "unit": "metric:v2"}) + r = data["result"]["realtime"] + print(f"""=== Realtime Weather === +Temperature: {r['temperature']}°C +Humidity: {format_percent(r['humidity'])} +Wind: {r['wind']['speed']} km/h, Direction {r['wind']['direction']}° +Precipitation: {r['precipitation']['local']['intensity']} mm/hr +Air Quality: + PM2.5: {r['air_quality']['pm25']} μg/m³ + PM10: {r['air_quality']['pm10']} μg/m³ + O3: {r['air_quality']['o3']} μg/m³ + SO2: {r['air_quality']['so2']} μg/m³ + NO2: {r['air_quality']['no2']} μg/m³ + CO: {r['air_quality']['co']} mg/m³ + AQI (China): {r['air_quality']['aqi']['chn']} + AQI (USA): {r['air_quality']['aqi']['usa']} +Life Index: + UV: {r['life_index']['ultraviolet']['desc']} + Comfort: {r['life_index']['comfort']['desc']}""") + + +def cmd_hourly(args): + token = get_token() + lng, lat = resolve_location(args) + url = f"{API_BASE}/{token}/{lng},{lat}/hourly" + data = make_request(url, {"hourlysteps": "72", "lang": "en_US", "unit": "metric:v2"}) + hourly = data["result"]["hourly"] + print("=== 72-Hour Forecast ===") + for i in range(len(hourly["temperature"])): + t = hourly["temperature"][i] + sky = hourly["skycon"][i]["value"] + rain = hourly["precipitation"][i]["probability"] + wind = hourly["wind"][i] + print(f""" +Time: {t['datetime']} +Temperature: {t['value']}°C +Weather: {sky} +Rain Probability: {rain}% +Wind: {wind['speed']} km/h, {wind['direction']}° +------------------------""") + + +def cmd_weekly(args): + token = get_token() + lng, lat = resolve_location(args) + url = f"{API_BASE}/{token}/{lng},{lat}/daily" + data = make_request(url, {"dailysteps": "7", "lang": "en_US", "unit": "metric:v2"}) + daily = data["result"]["daily"] + print("=== 7-Day Forecast ===") + for i in range(len(daily["temperature"])): + temp = daily["temperature"][i] + date = temp["date"].split("T")[0] + sky = daily["skycon"][i]["value"] + rain = daily["precipitation"][i]["probability"] + print(f""" +Date: {date} +Temperature: {temp['min']}°C ~ {temp['max']}°C +Weather: {sky} +Rain Probability: {rain}% +------------------------""") + + +def cmd_history(args): + token = get_token() + lng, lat = resolve_location(args) + timestamp = int((datetime.now() - timedelta(hours=24)).timestamp()) + url = f"{API_BASE}/{token}/{lng},{lat}/hourly" + data = make_request(url, { + "hourlysteps": "24", + "begin": str(timestamp), + "lang": "en_US", + "unit": "metric:v2", + }) + hourly = data["result"]["hourly"] + print("=== Past 24-Hour Weather ===") + for i in range(len(hourly["temperature"])): + t = hourly["temperature"][i] + sky = hourly["skycon"][i]["value"] + print(f""" +Time: {t['datetime']} +Temperature: {t['value']}°C +Weather: {sky} +------------------------""") + + +def cmd_alerts(args): + token = get_token() + lng, lat = resolve_location(args) + url = f"{API_BASE}/{token}/{lng},{lat}/weather" + data = make_request(url, {"alert": "true", "lang": "en_US", "unit": "metric:v2"}) + alerts = data["result"].get("alert", {}).get("content", []) + if not alerts: + print("No active weather alerts.") + return + print("=== Weather Alerts ===") + for alert in alerts: + print(f""" +Title: {alert.get('title', 'N/A')} +Code: {alert.get('code', 'N/A')} +Status: {alert.get('status', 'N/A')} +Description: {alert.get('description', 'N/A')} +------------------------""") + + +def main(): + parser = argparse.ArgumentParser(description="Caiyun Weather API client") + subparsers = parser.add_subparsers(dest="command", required=True) + + for name, func in [ + ("realtime", cmd_realtime), + ("hourly", cmd_hourly), + ("weekly", cmd_weekly), + ("history", cmd_history), + ("alerts", cmd_alerts), + ]: + sub = subparsers.add_parser(name) + sub.add_argument("--city", type=str, help="City name (Chinese or English, e.g. 北京 or beijing)") + sub.add_argument("--lng", type=float, help="Longitude (use with --lat)") + sub.add_argument("--lat", type=float, help="Latitude (use with --lng)") + sub.set_defaults(func=func) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/skills/weather-china/SKILL.md b/skills/weather-china/SKILL.md new file mode 100644 index 0000000..ea58ac8 --- /dev/null +++ b/skills/weather-china/SKILL.md @@ -0,0 +1,73 @@ +--- +name: weather-china +description: 中国天气预报查询 - 基于中国天气网(weather.com.cn)获取7天天气预报和生活指数数据。纯 Python 实现,无需 API Key。 +version: 1.0.2 +tags: [weather, china, forecast, chinese, weather-cn, life-index, 7day-forecast] +metadata: {"openclaw":{"emoji":"🌤️","requires":{"bins":["python3"]}}} +allowed-tools: [exec] +--- + +# 中国天气预报查询 (China Weather) + +基于 [中国天气网](https://www.weather.com.cn) 获取 7 天天气预报和生活指数数据。纯 Python 实现,无需 API Key。 + +## Quick Usage + +```bash +# 查询天气(格式化文本输出) +python3 skills/weather-china/lib/weather_cn.py query 南京 +python3 skills/weather-china/lib/weather_cn.py query 北京 +python3 skills/weather-china/lib/weather_cn.py query 成都 + +# JSON 输出(结构化数据) +python3 skills/weather-china/lib/weather_cn.py json 上海 +``` + +## Example Output + +```text +城市: 南京 (代码: 101190101) +数据来源: weather.com.cn +查询时间: 2026-03-04 16:31:55 + +[4日(今天)] 阴转多云, 10℃/5℃, 东风 4-5级转3-4级 + 感冒指数: 易发 - 风较大,易发生感冒,注意防护。 + 运动指数: 较适宜 - 风力较强且气温较低,请进行室内运动。 + 过敏指数: 较易发 - 外出需远离过敏源,适当采取防护措施。 + 穿衣指数: 冷 - 建议着棉衣加羊毛衫等冬季服装。 + 洗车指数: 较不宜 - 风力较大,洗车后会蒙上灰尘。 + 紫外线指数: 最弱 - 辐射弱,涂擦SPF8-12防晒护肤品。 + +[5日(明天)] 阴转多云, 11℃/4℃, 北风 3-4级 + 感冒指数: 少发 - 无明显降温,感冒机率较低。 + ... +``` + +## Supported Cities + +支持查询中国天气网覆盖的所有城市和地区。输入城市名称即可自动搜索匹配,无需手动配置。 + +常见城市(如北京、上海、广州、深圳、成都、杭州、南京等 60+ 城市)已内置代码,查询更快。其他城市会通过搜索接口自动查找城市代码。 + +## Data Available + +- **7天预报**: 日期、天气状况、高/低温度、风向风力 +- **生活指数**: 感冒、运动、过敏、穿衣、洗车、紫外线等 + +## Use Cases + +当用户询问以下问题时使用本 skill: + +- "今天天气怎么样" +- "明天会下雨吗" +- "[城市名]天气预报" +- "南京这周天气如何" +- "出门需要带伞吗" +- "穿什么衣服合适" + +## Notes + +1. **数据来源**: 中国天气网,数据可能略有延迟 +2. **城市名称**: 使用标准城市名,如"成都"、"南京" +3. **网络依赖**: 需要能访问 +4. **无需 API Key**: 直接解析天气网页面数据 diff --git a/skills/weather-china/_meta.json b/skills/weather-china/_meta.json new file mode 100644 index 0000000..49ea7d4 --- /dev/null +++ b/skills/weather-china/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn73tn1djj79kf3zsca8gm8tw182bhke", + "slug": "weather-china", + "version": "1.0.2", + "publishedAt": 1772678191581 +} \ No newline at end of file diff --git a/skills/weather-china/lib/weather_cn.py b/skills/weather-china/lib/weather_cn.py new file mode 100644 index 0000000..3f41cb1 --- /dev/null +++ b/skills/weather-china/lib/weather_cn.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +""" +中国天气网天气预报查询工具 (weather.com.cn) +查询7天天气预报和生活指数数据,输出结构化数据供AI模型使用。 +""" +import re +import sys +import json +import urllib.parse +import urllib.request +from html.parser import HTMLParser +from datetime import datetime + + +class WeatherHTMLParser(HTMLParser): + """解析天气页面HTML,提取7天预报和生活指数数据。""" + + def __init__(self): + super().__init__() + self._capture = False + self._target_id = None + self._depth = 0 + self._data_parts = [] + self._result = "" + + def extract(self, html, target_id): + """提取指定id的div内容。""" + self._capture = False + self._target_id = target_id + self._depth = 0 + self._data_parts = [] + self._result = "" + self.feed(html) + return self._result + + def handle_starttag(self, tag, attrs): + attrs_dict = dict(attrs) + if tag == "div" and attrs_dict.get("id") == self._target_id: + self._capture = True + self._depth = 1 + return + if self._capture and tag == "div": + self._depth += 1 + if self._capture: + attr_str = " ".join(f'{k}="{v}"' for k, v in attrs) + self._data_parts.append(f"<{tag} {attr_str}>" if attr_str else f"<{tag}>") + + def handle_endtag(self, tag): + if self._capture: + self._data_parts.append(f"") + if tag == "div": + self._depth -= 1 + if self._depth <= 0: + self._capture = False + self._result = "".join(self._data_parts) + + def handle_data(self, data): + if self._capture: + self._data_parts.append(data) + + +class ChinaWeather: + """中国天气网天气查询。""" + + # 城市搜索API + SEARCH_URL = "https://toy1.weather.com.cn/search?cityname={query}&callback=success_jsonpCallback&_={ts}" + + # 天气预报页面 + WEATHER_URL = "https://www.weather.com.cn/weather/{code}.shtml" + + # 请求超时(秒) + REQUEST_TIMEOUT = 15 + + # 请求头 + REQUEST_HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + "Accept": "*/*", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Referer": "https://www.weather.com.cn/", + } + + # 预置常用城市代码 + PRESET_CITIES = { + "北京": "101010100", "上海": "101020100", "广州": "101280101", + "深圳": "101280601", "成都": "101270101", "杭州": "101210101", + "南京": "101190101", "武汉": "101200101", "西安": "101110101", + "重庆": "101040100", "天津": "101030100", "沈阳": "101070101", + "哈尔滨": "101050101", "长春": "101060101", "济南": "101120101", + "青岛": "101120201", "郑州": "101180101", "长沙": "101250101", + "南昌": "101240101", "福州": "101230101", "厦门": "101230201", + "南宁": "101300101", "海口": "101310101", "三亚": "101310201", + "贵阳": "101260101", "昆明": "101290101", "兰州": "101160101", + "银川": "101170101", "西宁": "101150101", "拉萨": "101140101", + "乌鲁木齐": "101130101", "石家庄": "101090101", "太原": "101100101", + "呼和浩特": "101080101", "大连": "101070201", "苏州": "101190401", + "无锡": "101190201", "宁波": "101210401", "温州": "101210701", + "绍兴": "101210601", "金华": "101210901", "台州": "101211101", + "嘉兴": "101210301", "湖州": "101210201", "衢州": "101211001", + "丽水": "101211201", "舟山": "101211301", "东莞": "101281901", + "佛山": "101281701", "珠海": "101280701", "中山": "101281801", + "惠州": "101280301", "江门": "101280603", "肇庆": "101280901", + "湛江": "101281001", "茂名": "101281101", "汕头": "101280401", + "合肥": "101220101", "常州": "101191101", "徐州": "101190801", + "烟台": "101120501", "潍坊": "101120601", "临沂": "101120901", + "洛阳": "101180901", "襄阳": "101200201", "宜昌": "101200901", + "芜湖": "101220301", "泉州": "101230501", "漳州": "101230601", + "桂林": "101300501", "柳州": "101300301", "遵义": "101260201", + "大理": "101290601", "丽江": "101291401", "西双版纳": "101291601", + "延安": "101110600", "宝鸡": "101110901", "咸阳": "101110200", + "香港": "101320101", "澳门": "101330101", + } + + # 生活指数名称映射 + LIFE_INDEX_NAMES = { + "感冒": "cold", + "运动": "exercise", + "过敏": "allergy", + "穿衣": "clothing", + "洗车": "carwash", + "紫外线": "uv", + "钓鱼": "fishing", + "旅游": "travel", + "晾晒": "drying", + "交通": "traffic", + "防晒": "sunscreen", + } + + def __init__(self): + self._parser = WeatherHTMLParser() + + def _http_get(self, url, encoding="utf-8"): + """发送HTTP GET请求。""" + req = urllib.request.Request(url, headers=self.REQUEST_HEADERS) + try: + with urllib.request.urlopen(req, timeout=self.REQUEST_TIMEOUT) as resp: + return resp.read().decode(encoding, errors="replace") + except Exception as e: + raise ConnectionError(f"HTTP请求失败: {url} - {e}") + + def search_city(self, city_name): + """通过搜索API查询城市代码。 + + Returns: + tuple: (城市代码, 城市显示名) 或 None + """ + encoded = urllib.parse.quote(city_name) + ts = int(datetime.now().timestamp() * 1000) + url = self.SEARCH_URL.format(query=encoded, ts=ts) + + try: + text = self._http_get(url) + except ConnectionError: + return None + + # 解析JSONP: success_jsonpCallback([...]) + match = re.search(r"success_jsonpCallback\((\[.*\])\)", text, re.DOTALL) + if not match: + return None + + try: + results = json.loads(match.group(1)) + except json.JSONDecodeError: + return None + + if not results: + return None + + # 取第一个结果,解析格式: "代码~省份~城市名~英文名~..." + ref = results[0].get("ref", "") + parts = ref.split("~") + if len(parts) >= 3: + code = parts[0] + display_name = parts[2] + # 确保是有效的城市代码(以101开头的9位数字) + if re.match(r"^101\d{6}$", code): + return (code, display_name) + + return None + + def get_city_code(self, city_name): + """获取城市代码,优先使用预置数据。 + + Returns: + tuple: (城市代码, 城市显示名) + """ + city_name = city_name.strip() + + # 优先查预置城市 + if city_name in self.PRESET_CITIES: + return (self.PRESET_CITIES[city_name], city_name) + + # 实时搜索 + result = self.search_city(city_name) + if result: + return result + + raise ValueError(f"未找到城市: {city_name}") + + def fetch_weather_html(self, city_code): + """获取天气预报页面HTML。""" + url = self.WEATHER_URL.format(code=city_code) + return self._http_get(url) + + def parse_7day_forecast(self, html): + """解析7天天气预报数据。 + + 从
区域提取每天的天气信息。 + 实际HTML结构 (per
  • ): +

    日期

    +

    天气

    +

    高温/低温℃

    +

    + ... + 风力等级 +

    + """ + block = self._parser.extract(html, "7d") + if not block: + return [] + + forecasts = [] + + # 按
  • 拆分,逐个解析 + li_blocks = re.findall(r']*>(.*?)
  • ', block, re.DOTALL) + + for li in li_blocks: + # 跳过没有

    的空 li 元素 + m_date = re.search(r'

    ([^<]+)

    ', li) + if not m_date: + continue + + day = { + "date": m_date.group(1).strip(), + "weather": "", + "temp_high": "", + "temp_low": "", + "wind": "", + "wind_level": "", + } + + # 天气状况 - 优先取title属性 + m = re.search(r']*class="wea"', li) + if m: + day["weather"] = m.group(1).strip() + else: + m = re.search(r'class="wea"[^>]*>([^<]+)<', li) + if m: + day["weather"] = m.group(1).strip() + + # 温度 -

    高温℃/低温℃

    + # 注意:晚间"今天"可能无高温,仅有低温℃ + m = re.search(r'(\d+)℃?', li) + if m: + day["temp_high"] = m.group(1) + "℃" + m = re.search(r'(\d+)℃', li) + if m: + day["temp_low"] = m.group(1) + "℃" + + # 风向 - 内第一个 + m = re.search(r']*class="[A-Z]', li) + if m: + day["wind"] = m.group(1).strip() + + # 风力等级 -

    内紧跟 后的 等级 + m = re.search(r'\s*([^<]+)', li) + if m: + day["wind_level"] = m.group(1).strip() + + forecasts.append(day) + + return forecasts + + def parse_life_indices(self, html): + """解析生活指数数据。 + + 从

    区域提取生活指数。 + 实际HTML结构 (per
  • ): +
  • + + 级别 + 类型指数 +

    描述

    +
  • + """ + block = self._parser.extract(html, "livezs") + if not block: + return [] + + indices = [] + + # 按
  • 拆分解析 + li_blocks = re.findall(r']*>(.*?)
  • ', block, re.DOTALL) + + for li in li_blocks: + # 级别 - 易发 + m_level = re.search(r'([^<]+)', li) + # 指数名 - 感冒指数 + m_name = re.search(r'([^<]+)', li) + # 描述 -

    描述内容

    + m_desc = re.search(r'

    ([^<]+)

    ', li) + + if not (m_level and m_name): + continue + + level = m_level.group(1).strip() + name = m_name.group(1).strip() + desc = m_desc.group(1).strip() if m_desc else "" + + # 提取指数类型关键词(去掉"指数"后缀) + index_type = name.replace("指数", "").strip() + key = self.LIFE_INDEX_NAMES.get(index_type, index_type) + + indices.append({ + "name": name, + "key": key, + "level": level, + "description": desc, + }) + + return indices + + def query(self, city_name): + """查询指定城市的天气预报。 + + Args: + city_name: 城市名称,如"南京"、"北京" + + Returns: + dict: 包含城市信息、7天预报(含每日生活指数)的结构化数据 + """ + code, display_name = self.get_city_code(city_name) + html = self.fetch_weather_html(code) + + forecast = self.parse_7day_forecast(html) + life_indices = self.parse_life_indices(html) + + # 将生活指数按天分组后合并到对应天的forecast中 + # livezs中的指数是连续排列的,每天的指数类型数量相同 + if forecast and life_indices: + num_days = len(forecast) + indices_per_day = len(life_indices) // num_days if num_days > 0 else 0 + if indices_per_day > 0: + for i, day in enumerate(forecast): + start = i * indices_per_day + end = start + indices_per_day + day["life_indices"] = life_indices[start:end] + else: + # 无法均分时,全部放到第一天 + forecast[0]["life_indices"] = life_indices + + return { + "city": display_name, + "city_code": code, + "query_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "source": "weather.com.cn", + "forecast": forecast, + } + + def format_output(self, data): + """格式化天气数据为文本输出(供AI模型读取)。""" + if "error" in data: + return f"错误: {data['error']}" + + lines = [ + f"城市: {data['city']} (代码: {data['city_code']})", + f"数据来源: {data['source']}", + f"查询时间: {data['query_time']}", + ] + + for day in data.get("forecast", []): + lines.append("") + temp = f"{day['temp_high']}/{day['temp_low']}" if day.get("temp_high") else day.get("temp_low", "") + wind_info = f"{day['wind']} {day['wind_level']}" if day.get("wind") else "" + lines.append(f"[{day['date']}] {day['weather']}, {temp}, {wind_info}".rstrip(", ")) + + # 每天的生活指数 + for idx in day.get("life_indices", []): + lines.append(f" {idx['name']}: {idx['level']} - {idx['description']}") + + return "\n".join(lines) + + +def main(): + if len(sys.argv) < 2: + print("Usage: weather_cn.py ") + print("Commands:") + print(" query - 查询天气(格式化文本输出)") + print(" json - 查询天气(JSON输出)") + print() + print("Examples:") + print(" weather_cn.py query 南京") + print(" weather_cn.py json 北京") + sys.exit(1) + + weather = ChinaWeather() + command = sys.argv[1].lower() + city = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else "" + + if not city: + print("错误: 请指定城市名称", file=sys.stderr) + sys.exit(1) + + try: + if command == "query": + data = weather.query(city) + print(weather.format_output(data)) + elif command == "json": + data = weather.query(city) + print(json.dumps(data, ensure_ascii=False, indent=2)) + else: + print(f"未知命令: {command}", file=sys.stderr) + sys.exit(1) + except ValueError as e: + print(f"错误: {e}", file=sys.stderr) + sys.exit(1) + except ConnectionError as e: + print(f"网络错误: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"未知错误: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/weather-forecasts/SKILL.md b/skills/weather-forecasts/SKILL.md deleted file mode 100644 index eca7dd6..0000000 --- a/skills/weather-forecasts/SKILL.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: weather-forecasts -description: Get current weather and forecasts (no API key required). -homepage: https://wttr.in/:help -metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}} ---- - -# Weather - -Two free services, no API keys needed. - -## wttr.in (primary) - -Quick one-liner: -```bash -curl -s "wttr.in/London?format=3" -# Output: London: ⛅️ +8°C -``` - -Compact format: -```bash -curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w" -# Output: London: ⛅️ +8°C 71% ↙5km/h -``` - -Full forecast (3 days): -```bash -curl -s "wttr.in/London?T" -``` - -### Querying by time range - -Current weather only: -```bash -curl -s "wttr.in/London?0&format=3" -``` - -Today's forecast: -```bash -curl -s "wttr.in/London?1&T" -``` - -Tomorrow's forecast: -```bash -curl -s "wttr.in/London?2&T" -# Shows today + tomorrow; extract the second day -``` - -Day after tomorrow: -```bash -curl -s "wttr.in/London?T" -# Default output shows 3 days (today, tomorrow, day after tomorrow) -``` - -Next 7 days (weekly forecast) — use Open-Meteo: -```bash -curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto&forecast_days=7" -``` - -### Format codes - -`%c` condition · `%t` temp · `%h` humidity · `%w` wind · `%l` location · `%m` moon - -### Tips - -- URL-encode spaces: `wttr.in/New+York` -- Airport codes: `wttr.in/JFK` -- Units: `?m` (metric) `?u` (USCS) -- Language: `?lang=ja` (Japanese), `?lang=zh` (Chinese), etc. -- PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png` - -## Open-Meteo (fallback / extended forecasts, JSON) - -Free, no key, good for programmatic use and longer-range forecasts. - -Current weather: -```bash -curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true" -``` - -7-day daily forecast (min/max temp, weather code): -```bash -curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto&forecast_days=7" -``` - -Hourly forecast for the next 2 days: -```bash -curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&hourly=temperature_2m,weathercode,precipitation&timezone=auto&forecast_days=2" -``` - -Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode. - -Docs: https://open-meteo.com/en/docs diff --git a/skills/weather-forecasts/_meta.json b/skills/weather-forecasts/_meta.json deleted file mode 100644 index 4556002..0000000 --- a/skills/weather-forecasts/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26", - "slug": "weather", - "version": "1.0.0", - "publishedAt": 1767545394459 -} \ No newline at end of file