feat: iPet → iAs 完整迁移,Rust 版微信 AI 助手
Phase 1 ✅ CLI 框架 + 配置系统 - clap 子命令: login / listen / send / whoami / usage - config.json + env var 替换 - tracing 日志系统 - state 持久化(auth/runtime 文件存 + PostgreSQL) Phase 2 ✅ 微信通道 - wechat::client — 完整 iLink Bot HTTP API 实现 - 扫码登录(终端二维码 + 轮询状态) - 长轮询 getupdates / 消息收发 / 监听注册 Phase 3 ✅ AI 对话(纯文本 + function calling) - LlmProvider trait: DeepSeek + LM Studio 实现 - SSE 流式解析(text / reasoning / tool_calls delta / usage) - Conversation: 消息历史 + chat / chat_with_tools 工具循环 Phase 4 ✅ PostgreSQL 集成 - app_state(认证 KV 存储) - chat_records(消息收发记录) - llm_usage(Token 用量统计缓存命中率) - user_memories(长期记忆持久化) - pending_approvals(审批确认码) - scheduled_tasks(定时任务表) Phase 5 ✅ 一切皆 Skill(工具系统) - SkillRegistry: 系统 + 用户 skills 双目录合并 - SKILL.md 解析器 + 子进程执行器(stdin JSON → stdout) - 9 个系统 Skills: datetime / weather / search / email / shell / schedule / memos / read_memories / read_summaries - ApprovalManager: High 风险技能 → 确认码审批(透明模式) - High 风险技能:确认码审批(透明模式) Phase 6 ✅ 定时任务调度器 上下文管理 - ChatSession: checkpoint + token budget (28K) + summaries - Token 估算器(中英文自适应) - 12h 空闲 → trigger_idle_summary(不入会话) - Budget 溢出 → trigger_overflow_summary(入会话 + drain 旧消息) - Summarizer: LLM 生成自然语言摘要(fallback 简单截断) - 长期记忆 / 摘要 通过 read_memories / read_summaries 工具按需读取 工具调用日志 + Token 统计 - INFO: 工具名 + 参数 + 结果摘要 - DEBUG: 子进程 exit/stdout/stderr - ias usage --since --until --model 查看用量和缓存命中率
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
```
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
# 和风天气查询脚本
|
||||
# 读取 stdin JSON: {"location":"北京","days":3}
|
||||
|
||||
read -r input
|
||||
|
||||
LOCATION=$(echo "$input" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('location', ''))
|
||||
" 2>/dev/null)
|
||||
|
||||
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_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
|
||||
|
||||
# 生成 JWT(使用 python3)
|
||||
JWT=$(python3 -c "
|
||||
import json, time, base64, struct, sys
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
# 查询实况天气
|
||||
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)
|
||||
|
||||
# 查询预报(最多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"
|
||||
Reference in New Issue
Block a user