add weather

This commit is contained in:
朱潮 2026-04-04 21:06:56 +08:00
parent 2cc8b893f7
commit 2774069f8e
8 changed files with 882 additions and 99 deletions

View File

@ -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 对中国地区数据最为精准
- 有频率限制,请避免短时间内频繁请求

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn72jm08z8zg612dswg2c11sbs80w1w8",
"slug": "caiyun-weather",
"version": "1.1.0",
"publishedAt": 1772199654265
}

View File

@ -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 <name> 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/
PM10: {r['air_quality']['pm10']} μg/
O3: {r['air_quality']['o3']} μg/
SO2: {r['air_quality']['so2']} μg/
NO2: {r['air_quality']['no2']} μg/
CO: {r['air_quality']['co']} mg/
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()

View File

@ -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. **网络依赖**: 需要能访问 <www.weather.com.cn>
4. **无需 API Key**: 直接解析天气网页面数据

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn73tn1djj79kf3zsca8gm8tw182bhke",
"slug": "weather-china",
"version": "1.0.2",
"publishedAt": 1772678191581
}

View File

@ -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"</{tag}>")
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天天气预报数据。
<div id="7d"> 区域提取每天的天气信息
实际HTML结构 (per <li>):
<h1>日期</h1>
<p title="天气" class="wea">天气</p>
<p class="tem"><span>高温</span>/<i>低温</i></p>
<p class="win">
<em><span title="风向"></span>...</em>
<i>风力等级</i>
</p>
"""
block = self._parser.extract(html, "7d")
if not block:
return []
forecasts = []
# 按 <li> 拆分,逐个解析
li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', block, re.DOTALL)
for li in li_blocks:
# 跳过没有 <h1> 的空 li 元素
m_date = re.search(r'<h1>([^<]+)</h1>', 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'<p\s+title="([^"]*)"[^>]*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()
# 温度 - <p class="tem"><span>高温℃</span>/<i>低温℃</i></p>
# 注意:晚间"今天"可能无<span>高温,仅有<i>低温℃</i>
m = re.search(r'<span>(\d+)℃?</span>', li)
if m:
day["temp_high"] = m.group(1) + ""
m = re.search(r'<i>(\d+)℃</i>', li)
if m:
day["temp_low"] = m.group(1) + ""
# 风向 - <em> 内第一个 <span title="风向">
m = re.search(r'<span\s+title="([^"]*)"[^>]*class="[A-Z]', li)
if m:
day["wind"] = m.group(1).strip()
# 风力等级 - <p class="win"> 内紧跟 </em> 后的 <i>等级</i>
m = re.search(r'</em>\s*<i>([^<]+)</i>', li)
if m:
day["wind_level"] = m.group(1).strip()
forecasts.append(day)
return forecasts
def parse_life_indices(self, html):
"""解析生活指数数据。
<div id="livezs"> 区域提取生活指数
实际HTML结构 (per <li>):
<li>
<i class="gmi"></i>
<span>级别</span>
<em>类型指数</em>
<p>描述</p>
</li>
"""
block = self._parser.extract(html, "livezs")
if not block:
return []
indices = []
# 按 <li> 拆分解析
li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', block, re.DOTALL)
for li in li_blocks:
# 级别 - <span>易发</span>
m_level = re.search(r'<span>([^<]+)</span>', li)
# 指数名 - <em>感冒指数</em>
m_name = re.search(r'<em>([^<]+)</em>', li)
# 描述 - <p>描述内容</p>
m_desc = re.search(r'<p>([^<]+)</p>', 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 <command> <city>")
print("Commands:")
print(" query <city> - 查询天气(格式化文本输出)")
print(" json <city> - 查询天气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()

View File

@ -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&current_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

View File

@ -1,6 +0,0 @@
{
"ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26",
"slug": "weather",
"version": "1.0.0",
"publishedAt": 1767545394459
}