refactor: query_weather bash→Rust builtin, 删除旧 skill

QWeather 天气查询从外部 bash 脚本重构为 Rust 原生 builtin 工具:
- ed25519-dalek 替代 openssl JWT 签名
- reqwest + tokio::join! 并发查询替代串行 curl
- serde_json 直接解析替代 python3 子进程
- city_id 内存缓存 (HashMap, 1h TTL)
- 支持 3/7/10/15/30d 逐日 + 24/72/168h 逐时预报
- 删除 resources/skills/query_weather/ (builtin 优先级更高)
This commit is contained in:
2026-06-02 22:42:37 +08:00
parent 031f160393
commit afc43ce692
8 changed files with 659 additions and 88 deletions
-30
View File
@@ -1,30 +0,0 @@
# query_weather
查询天气信息(和风天气 API)。
## Risk Level
Low
## Parameters
```json
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名,如 北京、上海、合肥"
},
"days": {
"type": "integer",
"description": "预报天数,默认3",
"default": 3
}
},
"required": ["location"]
}
```
## Execute
```bash
scripts/weather.sh
```
@@ -1,56 +0,0 @@
#!/bin/bash
# 和风天气查询 → 输出 JSON
read -r input
LOCATION=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('location',''))" 2>/dev/null)
DAYS=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('days',3))" 2>/dev/null)
[ -z "$LOCATION" ] && echo '{"error":"请提供 location 参数"}' && exit 0
API_HOST="${QWEATHER_API_HOST}"
KEY_FILE="${QWEATHER_JWT_PRIVATE_KEY_FILE:-qweather/ed25519-private.pem}"
KEY_ID="${QWEATHER_JWT_KEY_ID}"
PROJECT_ID="${QWEATHER_JWT_PROJECT_ID}"
TTL="${QWEATHER_JWT_TTL_SECONDS:-3600}"
NOW=$(date +%s)
HEADER=$(echo -n '{"alg":"EdDSA","kid":"'"$KEY_ID"'"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
PAYLOAD=$(echo -n '{"sub":"'"$PROJECT_ID"'","iat":'$((NOW-30))',"exp":'$((NOW+TTL))'}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
INPUT="${HEADER}.${PAYLOAD}"
TMP=$(mktemp) && echo -n "$INPUT" > "$TMP"
SIG=$(openssl pkeyutl -sign -inkey "$KEY_FILE" -rawin -in "$TMP" 2>/dev/null | base64 -w0 | tr '+/' '-_' | tr -d '=')
rm -f "$TMP"
JWT="${INPUT}.${SIG}"
[ -z "$JWT" ] && JWT="${QWEATHER_JWT}"
[ -z "$JWT" ] && echo '{"error":"JWT 生成失败"}' && exit 0
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$LOCATION'))")
CITY=$(curl -s --compressed "https://geoapi.qweather.com/v2/city/lookup?location=${ENCODED}" -H "Authorization: Bearer $JWT")
CITY_ID=$(echo "$CITY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['location'][0]['id'] if d.get('location') else '')" 2>/dev/null)
[ -z "$CITY_ID" ] && echo '{"error":"未找到城市: '"$LOCATION"'"}' && exit 0
EP_DAYS=$([ "$DAYS" -le 3 ] && echo 3 || echo 7)
NOW_RESP=$(curl -s --compressed "https://${API_HOST}/v7/weather/now?location=${CITY_ID}&lang=zh&unit=m" -H "Authorization: Bearer $JWT")
DAILY_RESP=$(curl -s --compressed "https://${API_HOST}/v7/weather/${EP_DAYS}d?location=${CITY_ID}&lang=zh&unit=m" -H "Authorization: Bearer $JWT")
export NOW_RESP DAILY_RESP LOCATION DAYS
python3 -c "
import os, json
now = json.loads(os.environ['NOW_RESP'])
daily = json.loads(os.environ['DAILY_RESP'])
days = daily.get('daily', [])[:int(os.environ['DAYS'])]
result = {
'location': os.environ['LOCATION'],
'now': {
'temp': now['now']['temp'],
'text': now['now']['text'],
},
'forecast': [
{'date': d['fxDate'], 'day': d['textDay'], 'night': d.get('textNight',''), 'tempMin': d['tempMin'], 'tempMax': d['tempMax']}
for d in days
]
}
# 过滤空字段
result['now'] = {k: v for k, v in result['now'].items() if v}
result['forecast'] = [{k: v for k, v in f.items() if v} for f in result['forecast']]
print(json.dumps(result, ensure_ascii=False))
" 2>/dev/null || echo '{"error":"解析天气数据失败"}'