Skill: tjefferson/caiyun-weather (version 1.2.0)

--- name: caiyun-weather description: "天气查询 / Weather query. 实时查询天气、气温、空气质量（AQI/PM2.5）、逐小时预报、一周天气预报、历史天气和天气预警，支持中文城市名（北京/上海/深圳等）直接查询。Real-time weather, temperature, AQI, hourly/weekly forecast, historical weather and alerts via Caiyun Weather API. Supports Chinese city names directly. Requires CAIYUN_WEATHER_API_TOKEN." category: Data & APIs 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 对中国地区数据最为精准 - 有频率限制，请避免短时间内频繁请求

path: scripts/caiyun_weather.py
content: #!/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():
    [REDACTED_SECRET]"CAIYUN_WEATHER_API_TOKEN")
    if not [REDACTED_SECRET]"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):
    [REDACTED_SECRET])
    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):
    [REDACTED_SECRET])
    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):
    [REDACTED_SECRET])
    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):
    [REDACTED_SECRET])
    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):
    [REDACTED_SECRET])
    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()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)