diff --git a/resources/skills/get_current_datetime/scripts/datetime.sh b/resources/skills/get_current_datetime/scripts/datetime.sh index c9b5747..39fc3d2 100755 --- a/resources/skills/get_current_datetime/scripts/datetime.sh +++ b/resources/skills/get_current_datetime/scripts/datetime.sh @@ -10,7 +10,7 @@ except: print('full') " 2>/dev/null || echo "full") case "$format" in - date) TZ='Asia/Shanghai' date '+%Y-%m-%d' ;; - time) TZ='Asia/Shanghai' date '+%H:%M:%S' ;; - *) TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M:%S' ;; + date) TZ='Asia/Shanghai' date '+{"datetime": "%Y-%m-%d"}' ;; + time) TZ='Asia/Shanghai' date '+{"datetime": "%H:%M:%S"}' ;; + *) TZ='Asia/Shanghai' date '+{"datetime": "%Y-%m-%d %H:%M:%S"}' ;; esac diff --git a/resources/skills/list_scheduled_tasks/scripts/list.sh b/resources/skills/list_scheduled_tasks/scripts/list.sh index 149cfa5..6075f42 100755 --- a/resources/skills/list_scheduled_tasks/scripts/list.sh +++ b/resources/skills/list_scheduled_tasks/scripts/list.sh @@ -1,27 +1,26 @@ #!/bin/bash -# 列出定时任务(从 PostgreSQL) -set -e - +# 列出定时任务 → JSON 输出 DB_URL="${DATABASE_URL}" -if [ -z "$DB_URL" ]; then - echo "数据库未配置(DATABASE_URL)" - exit 0 -fi +[ -z "$DB_URL" ] && echo '{"tasks":[],"error":"数据库未配置"}' && exit 0 -# 使用 psql 查询 psql "$DB_URL" -t -A -F '|' -c " - SELECT name, description, enabled, next_run_at, interval_seconds - FROM scheduled_tasks - ORDER BY name -" 2>/dev/null | while IFS='|' read -r name desc enabled next_run interval; do - if [ -n "$name" ]; then - echo "- $name${desc:+ ($desc)}" - echo " 启用: $enabled | 间隔: ${interval}s | 下次: ${next_run:-无}" - fi -done - -# 检查是否有任何任务 -COUNT=$(psql "$DB_URL" -t -A -c "SELECT count(*) FROM scheduled_tasks" 2>/dev/null || echo "0") -if [ "$COUNT" = "0" ] || [ -z "$COUNT" ]; then - echo "暂无定时任务" -fi + SELECT jsonb_build_object( + 'name', name, + 'description', description, + 'enabled', enabled, + 'command', command, + 'interval_seconds', interval_seconds, + 'next_run_at', next_run_at + ) + FROM scheduled_tasks ORDER BY name +" 2>/dev/null | python3 -c " +import sys, json +tasks = [] +for line in sys.stdin: + line = line.strip() + if line: + try: tasks.append(json.loads(line)) + except: pass +print(json.dumps({'tasks': tasks}, ensure_ascii=False)) +" +[ -z "$(psql "$DB_URL" -t -A -c "SELECT count(*) FROM scheduled_tasks" 2>/dev/null)" ] && echo '{"tasks":[]}' diff --git a/resources/skills/manage_scheduled_task/scripts/manage.sh b/resources/skills/manage_scheduled_task/scripts/manage.sh index 81c7d54..337c07b 100755 --- a/resources/skills/manage_scheduled_task/scripts/manage.sh +++ b/resources/skills/manage_scheduled_task/scripts/manage.sh @@ -1,7 +1,5 @@ #!/bin/bash -# 管理定时任务 -set -e - +# 管理定时任务 → JSON 输出 read -r input ACTION=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('action',''))" 2>/dev/null) NAME=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null) @@ -10,39 +8,32 @@ INTERVAL=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdi DESC=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('description',''))" 2>/dev/null) DB_URL="${DATABASE_URL}" - -if [ -z "$DB_URL" ]; then - echo "数据库未配置(DATABASE_URL)" - exit 1 -fi +[ -z "$DB_URL" ] && echo '{"error":"数据库未配置"}' && exit 0 +[ -z "$NAME" ] && echo '{"error":"请提供 name 参数"}' && exit 0 case "$ACTION" in - add) - if [ -z "$CMD" ]; then - echo "add 操作需要 command 参数" - exit 1 - fi - psql "$DB_URL" -c " - INSERT INTO scheduled_tasks (id, name, description, command, interval_seconds, enabled, next_run_at) - VALUES (gen_random_uuid(), '$NAME', '$DESC', '$CMD', $INTERVAL, true, NOW() + interval '$INTERVAL seconds') - ON CONFLICT (name) DO UPDATE - SET command = EXCLUDED.command, interval_seconds = EXCLUDED.interval_seconds, enabled = true, description = EXCLUDED.description - " 2>/dev/null && echo "✅ 已添加/更新定时任务: $NAME" || echo "❌ 添加失败(表 scheduled_tasks 可能未创建)" - ;; - delete) - psql "$DB_URL" -c "DELETE FROM scheduled_tasks WHERE name = '$NAME'" 2>/dev/null - echo "✅ 已删除定时任务: $NAME" - ;; - enable) - psql "$DB_URL" -c "UPDATE scheduled_tasks SET enabled = true WHERE name = '$NAME'" 2>/dev/null - echo "✅ 已启用定时任务: $NAME" - ;; - disable) - psql "$DB_URL" -c "UPDATE scheduled_tasks SET enabled = false WHERE name = '$NAME'" 2>/dev/null - echo "✅ 已禁用定时任务: $NAME" - ;; - *) - echo "未知操作: $ACTION(支持: add, delete, enable, disable)" - exit 1 - ;; +add) + [ -z "$CMD" ] && echo '{"error":"add 需要 command 参数"}' && exit 0 + psql "$DB_URL" -c " + INSERT INTO scheduled_tasks (id, name, description, command, interval_seconds, enabled, next_run_at) + VALUES (gen_random_uuid(), '$NAME', '$DESC', '$CMD', $INTERVAL, true, NOW() + interval '$INTERVAL seconds') + ON CONFLICT (name) DO UPDATE SET command=EXCLUDED.command, interval_seconds=EXCLUDED.interval_seconds, enabled=true, description=EXCLUDED.description + " 2>/dev/null && echo '{"ok":true,"action":"add","name":"'"$NAME"'"}' || echo '{"error":"添加失败"}' + ;; +delete) + psql "$DB_URL" -c "DELETE FROM scheduled_tasks WHERE name = '$NAME'" 2>/dev/null + echo '{"ok":true,"action":"delete","name":"'"$NAME"'"}' + ;; +enable) + psql "$DB_URL" -c "UPDATE scheduled_tasks SET enabled = true WHERE name = '$NAME'" 2>/dev/null + echo '{"ok":true,"action":"enable","name":"'"$NAME"'"}' + ;; +disable) + psql "$DB_URL" -c "UPDATE scheduled_tasks SET enabled = false WHERE name = '$NAME'" 2>/dev/null + echo '{"ok":true,"action":"disable","name":"'"$NAME"'"}' + ;; +*) + echo '{"error":"未知操作: '"$ACTION"'(支持: add, delete, enable, disable)"}' + exit 1 + ;; esac diff --git a/resources/skills/query_weather/scripts/weather.sh b/resources/skills/query_weather/scripts/weather.sh index b950463..0a586c7 100755 --- a/resources/skills/query_weather/scripts/weather.sh +++ b/resources/skills/query_weather/scripts/weather.sh @@ -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":"解析天气数据失败"}' diff --git a/resources/skills/search_web/scripts/search.sh b/resources/skills/search_web/scripts/search.sh index 95a0fc0..e1dea13 100755 --- a/resources/skills/search_web/scripts/search.sh +++ b/resources/skills/search_web/scripts/search.sh @@ -1,54 +1,29 @@ #!/bin/bash -# Tavily 搜索 - +# Tavily 搜索 → JSON 输出 read -r input -QUERY=$(echo "$input" | python3 -c " -import sys, json -d = json.load(sys.stdin) -print(d.get('query', '')) -" 2>/dev/null) +QUERY=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('query',''))" 2>/dev/null) +COUNT=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('count',5))" 2>/dev/null) -COUNT=$(echo "$input" | python3 -c " -import sys, json -d = json.load(sys.stdin) -print(d.get('count', 5)) -" 2>/dev/null) - -if [ -z "$QUERY" ]; then - echo "请提供 query 参数" - exit 1 -fi +[ -z "$QUERY" ] && echo '{"error":"请提供 query 参数"}' && exit 0 API_KEY="${TAVILY_API_KEY}" -if [ -z "$API_KEY" ]; then - echo "请设置 TAVILY_API_KEY 环境变量" - exit 1 -fi +[ -z "$API_KEY" ] && echo '{"error":"请设置 TAVILY_API_KEY"}' && exit 0 RESP=$(curl -s -X POST "https://api.tavily.com/search" \ -H "Content-Type: application/json" \ -d "{\"api_key\":\"$API_KEY\",\"query\":\"$QUERY\",\"max_results\":$COUNT,\"include_answer\":true}") -# 解析并输出 -ANSWER=$(echo "$RESP" | python3 -c " -import sys, json -d = json.load(sys.stdin) -answer = d.get('answer', '') -results = d.get('results', []) -lines = [] -if answer: - lines.append(f'总结: {answer}') - lines.append('') -for i, r in enumerate(results[:$COUNT], 1): - title = r.get('title', '无标题') - url = r.get('url', '') - content = r.get('content', '')[:200] - lines.append(f'{i}. {title}') - lines.append(f' {url}') - if content: - lines.append(f' {content}') - lines.append('') -print('\\n'.join(lines)) -" 2>/dev/null) - -echo "$ANSWER" +export RESP QUERY COUNT +python3 -c " +import os, json +d = json.loads(os.environ['RESP']) +result = { + 'query': os.environ['QUERY'], + 'answer': d.get('answer', ''), + 'results': [ + {'title': r.get('title', ''), 'url': r.get('url', ''), 'content': r.get('content', '')[:300]} + for r in d.get('results', [])[:int(os.environ['COUNT'])] + ] +} +print(json.dumps(result, ensure_ascii=False)) +" 2>/dev/null || echo '{"error":"搜索请求失败"}'