refactor: skill 脚本输出改为结构化 JSON

- query_weather: {location, now: {temp,text}, forecast: [{date,day,night,tempMin,tempMax}]}
- search_web: {query, answer, results: [{title,url,content}]}
- get_current_datetime: {datetime}
- list_scheduled_tasks: {tasks: [{name,description,enabled,command,...}]}
- manage_scheduled_task: {ok, action, name} / {error}

LLM 直接看到结构化数据,不再依赖脚本拼接的中文自然语言
This commit is contained in:
2026-06-02 10:32:27 +08:00
parent fac2016b7e
commit 81c736f873
5 changed files with 111 additions and 253 deletions
+40 -147
View File
@@ -1,160 +1,53 @@
#!/bin/bash
# 和风天气查询脚本
# 读取 stdin JSON: {"location":"北京","days":3}
# 和风天气查询 → 输出 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)
LOCATION=$(echo "$input" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('location', ''))
" 2>/dev/null)
[ -z "$LOCATION" ] && echo '{"error":"请提供 location 参数"}' && exit 0
DAYS=$(echo "$input" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('days', 3))
" 2>/dev/null)
if [ -z "$LOCATION" ]; then
echo "请提供 location 参数"
exit 1
fi
# 从环境变量读取配置
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}"
KEY_FILE="${QWEATHER_JWT_PRIVATE_KEY_FILE:-qweather/ed25519-private.pem}"
TTL="${QWEATHER_JWT_TTL_SECONDS:-3600}"
if [ -z "$API_HOST" ]; then
echo "请设置 QWEATHER_API_HOST 环境变量"
exit 1
fi
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
# 生成 JWT(使用 python3
JWT=$(python3 -c "
import json, time, base64, struct, sys
from pathlib import Path
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
key_id = '$KEY_ID'
project_id = '$PROJECT_ID'
key_file = '$KEY_FILE'
ttl = int('$TTL')
if not key_id or not project_id:
sys.exit(0)
# Read Ed25519 private key
key_path = Path(key_file)
if not key_path.exists():
sys.exit(0)
key_data = key_path.read_text()
# Simple base64url
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
# Extract raw key bytes from PEM
import re
pem_match = re.search(r'-----BEGIN PRIVATE KEY-----\\s*(.*?)\\s*-----END PRIVATE KEY-----', key_data, re.DOTALL)
if not pem_match:
sys.exit(0)
der = base64.b64decode(pem_match.group(1))
# PKCS8 Ed25519 private key: last 32 bytes are the seed
seed = der[-32:]
from hashlib import sha512
h = sha512(seed).digest()
a = bytearray(h[:32])
a[0] &= 248
a[31] &= 127
a[31] |= 64
# Sign using RFC 8032
def ed25519_sign(message, seed_bytes):
h = sha512(seed_bytes).digest()
a = bytearray(h[:32])
a[0] &= 248
a[31] &= 127
a[31] |= 64
r = sha512(bytes(a[32:]) + message).digest()
R = _sc_reduce(r)
R_bytes = _sc_mul_base(R)
S = _sc_add(R, _sc_mul(_sc_reduce(sha512(bytes(R_bytes) + bytes(a) + message).digest()), _sc_reduce(bytes(a))))
return bytes(R_bytes) + _sc_encode(S)
# Skipping full impl - use openssl instead
print('')
" 2>/dev/null)
# If python3 JWT failed, try openssl
if [ -z "$JWT" ]; then
NOW=$(date +%s)
IAT=$((NOW - 30))
EXP=$((NOW + TTL))
HEADER=$(echo -n '{"alg":"EdDSA","kid":"'"$KEY_ID"'"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
PAYLOAD=$(echo -n '{"sub":"'"$PROJECT_ID"'","iat":'"$IAT"',"exp":'"$EXP"'}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
SIGNING_INPUT="${HEADER}.${PAYLOAD}"
# Try openssl pkeyutl (OpenSSL >= 3.0, needs temp file for -rawin)
TMPFILE=$(mktemp)
echo -n "$SIGNING_INPUT" > "$TMPFILE"
SIGNATURE=$(openssl pkeyutl -sign -inkey "$KEY_FILE" -rawin -in "$TMPFILE" 2>/dev/null | base64 -w0 | tr '+/' '-_' | tr -d '=') || true
rm -f "$TMPFILE"
if [ -z "$SIGNATURE" ]; then
# Fallback: try older openssl
SIGNATURE=$(echo -n "$SIGNING_INPUT" | openssl dgst -sign "$KEY_FILE" -keyform PEM -sha512 2>/dev/null | base64 -w0 | tr '+/' '-_' | tr -d '=') || true
fi
if [ -n "$SIGNATURE" ]; then
JWT="${SIGNING_INPUT}.${SIGNATURE}"
fi
fi
# Try env var JWT as fallback
if [ -z "$JWT" ]; then
JWT="${QWEATHER_JWT}"
fi
if [ -z "$JWT" ]; then
echo "无法生成 QWeather JWT,请检查 QWEATHER_JWT_KEY_ID/QWEATHER_JWT_PROJECT_ID/QWEATHER_JWT_PRIVATE_KEY_FILE 配置"
exit 1
fi
# 查询城市 ID
CITY_RESP=$(curl -s --compressed "https://geoapi.qweather.com/v2/city/lookup?location=$(echo -n "$LOCATION" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")" -H "Authorization: Bearer $JWT")
CITY_ID=$(echo "$CITY_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['location'][0]['id'] if d.get('location') else '')" 2>/dev/null)
if [ -z "$CITY_ID" ]; then
echo "未找到城市: $LOCATION"
exit 1
fi
# 查询实况天气
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")
NOW_TEMP=$(echo "$NOW_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"{d['now']['temp']}°C, {d['now']['text']}\")" 2>/dev/null)
DAILY_RESP=$(curl -s --compressed "https://${API_HOST}/v7/weather/${EP_DAYS}d?location=${CITY_ID}&lang=zh&unit=m" -H "Authorization: Bearer $JWT")
# 查询预报(最多7天)
ENDPOINT_DAYS=$DAYS
[ "$DAYS" -gt 3 ] && ENDPOINT_DAYS=7
[ "$DAYS" -le 3 ] && ENDPOINT_DAYS=3
DAILY_RESP=$(curl -s --compressed "https://${API_HOST}/v7/weather/${ENDPOINT_DAYS}d?location=${CITY_ID}&lang=zh&unit=m" -H "Authorization: Bearer $JWT")
FORECAST=$(echo "$DAILY_RESP" | python3 -c "
import sys, json
d = json.load(sys.stdin)
days = d.get('daily', [])[:int('$DAYS')]
lines = ['未来天气:']
for day in days:
lines.append(f\" {day['fxDate']}: {day['textDay']} {day['tempMin']}~{day['tempMax']}°C\")
print('\\n'.join(lines))
" 2>/dev/null)
echo "${LOCATION}天气: 当前 ${NOW_TEMP}"
echo ""
echo "$FORECAST"
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
]
}
print(json.dumps(result, ensure_ascii=False))
" 2>/dev/null || echo '{"error":"解析天气数据失败"}'