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,31 @@
|
||||
# email
|
||||
|
||||
收发邮件(IMAP)。
|
||||
|
||||
## Risk Level
|
||||
High
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "操作类型",
|
||||
"enum": ["list_unread", "read", "mark_read"]
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "列出最近几封邮件,默认5",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/email.sh
|
||||
```
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
// 邮件工具(IMAP 收发)
|
||||
// 通过 stdin 接收 JSON 参数
|
||||
let input = '';
|
||||
process.stdin.on('data', (chunk) => input += chunk);
|
||||
process.stdin.on('end', async () => {
|
||||
try {
|
||||
const params = JSON.parse(input.trim() || '{}');
|
||||
const action = params.action || 'list_unread';
|
||||
const limit = params.limit || 5;
|
||||
|
||||
const host = process.env.EMAIL_IMAP_HOST;
|
||||
const port = parseInt(process.env.EMAIL_IMAP_PORT || '993');
|
||||
const user = process.env.EMAIL_IMAP_USER;
|
||||
const password = process.env.EMAIL_IMAP_PASSWORD;
|
||||
|
||||
if (!host || !user || !password) {
|
||||
console.log('请设置 EMAIL_IMAP_HOST, EMAIL_IMAP_USER, EMAIL_IMAP_PASSWORD');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 使用 openssl s_client 连接 IMAP over SSL
|
||||
// 简单实现:列出收件箱最近 N 封邮件摘要
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
|
||||
const socket = tls.connect(port, host, { rejectUnauthorized: false }, () => {
|
||||
let buf = '';
|
||||
let step = 0;
|
||||
let tag = 0;
|
||||
|
||||
function cmd(line) {
|
||||
tag++;
|
||||
socket.write(`A${tag} ${line}\r\n`);
|
||||
return `A${tag}`;
|
||||
}
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buf += data.toString('utf-8');
|
||||
// 简化处理:等服务器就绪后依次发命令
|
||||
if (step === 0 && buf.includes('OK')) {
|
||||
step = 1;
|
||||
cmd(`LOGIN "${user}" "${password}"`);
|
||||
} else if (step === 1 && buf.includes(`A1 OK`)) {
|
||||
step = 2;
|
||||
cmd('SELECT INBOX');
|
||||
} else if (step === 2 && buf.includes(`A2 OK`)) {
|
||||
step = 3;
|
||||
cmd(`FETCH 1:${limit} (FLAGS INTERNALDATE BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)])`);
|
||||
} else if (step === 3 && buf.includes(`A3 OK`)) {
|
||||
step = 4;
|
||||
// 解析结果
|
||||
const lines = buf.split('\n').filter(l => l.trim());
|
||||
const results = [];
|
||||
let current = {};
|
||||
for (const line of lines) {
|
||||
if (line.match(/^\* \d+ FETCH/)) {
|
||||
if (current.subject) results.push(current);
|
||||
current = {};
|
||||
}
|
||||
const subjMatch = line.match(/^SUBJECT: (.+)$/i);
|
||||
const fromMatch = line.match(/^FROM: (.+)$/i);
|
||||
const dateMatch = line.match(/^DATE: (.+)$/i);
|
||||
if (subjMatch) current.subject = subjMatch[1].trim();
|
||||
if (fromMatch) current.from = fromMatch[1].trim();
|
||||
if (dateMatch) current.date = dateMatch[1].trim();
|
||||
}
|
||||
if (current.subject) results.push(current);
|
||||
|
||||
console.log(`最近 ${results.length} 封邮件:`);
|
||||
results.forEach((r, i) => {
|
||||
console.log(`\n${i+1}. 主题: ${r.subject || '(无主题)'}`);
|
||||
console.log(` 发件人: ${r.from || '未知'}`);
|
||||
console.log(` 时间: ${r.date || '未知'}`);
|
||||
});
|
||||
cmd('LOGOUT');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.setTimeout(15000, () => {
|
||||
console.log('连接 IMAP 超时');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log(`邮件查询失败: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
# execute_shell
|
||||
|
||||
在服务器上执行 shell 命令。
|
||||
|
||||
## Risk Level
|
||||
High
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "要执行的 shell 命令"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "超时秒数,默认30",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/shell.sh
|
||||
```
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# 执行 shell 命令
|
||||
# 高风险:需要确认码审批
|
||||
set -e
|
||||
|
||||
read -r input
|
||||
CMD=$(echo "$input" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('command', ''))
|
||||
" 2>/dev/null)
|
||||
|
||||
TIMEOUT=$(echo "$input" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('timeout', 30))
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -z "$CMD" ]; then
|
||||
echo "请提供 command 参数"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安全限制:禁止危险命令
|
||||
LOWER_CMD=$(echo "$CMD" | tr '[:upper:]' '[:lower:]')
|
||||
for DANGEROUS in "rm -rf /" "mkfs" "dd if=" ":(){ :|:& };:" "> /dev/" "wget http" "curl http"; do
|
||||
if echo "$LOWER_CMD" | grep -q "$DANGEROUS"; then
|
||||
echo "禁止执行危险命令: $DANGEROUS"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 输出前 N 行防止刷屏
|
||||
OUTPUT=$(timeout "$TIMEOUT" bash -c "$CMD" 2>&1 || echo "命令执行失败(退出码: $?)")
|
||||
echo "$OUTPUT" | head -50
|
||||
[ "$(echo "$OUTPUT" | wc -l)" -gt 50 ] && echo "...(输出已截断,共 $(echo "$OUTPUT" | wc -l) 行)"
|
||||
@@ -0,0 +1,26 @@
|
||||
# get_current_datetime
|
||||
|
||||
获取当前日期时间(北京时间 UTC+8)。
|
||||
|
||||
## Risk Level
|
||||
Low
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "时间格式: full | date | time",
|
||||
"enum": ["full", "date", "time"],
|
||||
"default": "full"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/datetime.sh
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# 读取 stdin JSON 参数
|
||||
read -r input
|
||||
format=$(echo "$input" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('format', 'full'))
|
||||
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' ;;
|
||||
esac
|
||||
@@ -0,0 +1,19 @@
|
||||
# list_scheduled_tasks
|
||||
|
||||
列出所有已配置的定时任务。
|
||||
|
||||
## Risk Level
|
||||
Low
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/list.sh
|
||||
```
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# 列出定时任务(从 PostgreSQL)
|
||||
set -e
|
||||
|
||||
DB_URL="${DATABASE_URL}"
|
||||
if [ -z "$DB_URL" ]; then
|
||||
echo "数据库未配置(DATABASE_URL)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 使用 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
|
||||
@@ -0,0 +1,36 @@
|
||||
# manage_memos
|
||||
|
||||
管理备忘录:添加、列出、删除。
|
||||
|
||||
备忘录存储在 `.data/memos.json` 文件中。
|
||||
|
||||
## Risk Level
|
||||
Low
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "操作类型",
|
||||
"enum": ["add", "list", "delete"]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "备忘内容(add 时需要)"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "备忘编号(delete 时需要)"
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/memo.sh
|
||||
```
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# 备忘录管理 — 存储 .data/memos.json
|
||||
MEMO_FILE=".data/memos.json"
|
||||
mkdir -p "$(dirname "$MEMO_FILE")"
|
||||
[ ! -f "$MEMO_FILE" ] && echo '[]' > "$MEMO_FILE"
|
||||
|
||||
read -r INPUT
|
||||
export MEMO_INPUT="$INPUT"
|
||||
|
||||
python3 - "$MEMO_FILE" << 'PYEOF'
|
||||
import sys, json, os, datetime
|
||||
|
||||
path = sys.argv[1]
|
||||
input_raw = os.environ.get('MEMO_INPUT', '{}')
|
||||
|
||||
try: req = json.loads(input_raw)
|
||||
except:
|
||||
print('无效的 JSON 输入')
|
||||
sys.exit(1)
|
||||
|
||||
action = req.get('action', 'list')
|
||||
content = req.get('content', '')
|
||||
memo_id = req.get('id', '')
|
||||
|
||||
data = json.load(open(path)) if os.path.exists(path) and os.path.getsize(path) > 0 else []
|
||||
|
||||
if action == 'add':
|
||||
if not content:
|
||||
print('请提供 content 参数')
|
||||
sys.exit(1)
|
||||
next_id = max([m.get('id', 0) for m in data], default=0) + 1
|
||||
data.append({'id': next_id, 'content': content, 'created_at': datetime.datetime.now().isoformat()})
|
||||
json.dump(data, open(path, 'w'), ensure_ascii=False, indent=2)
|
||||
print(f'已添加备忘录 #{next_id}')
|
||||
|
||||
elif action == 'list':
|
||||
if not data:
|
||||
print('暂无备忘录')
|
||||
else:
|
||||
for m in data:
|
||||
ts = m.get('created_at', '?')[:16]
|
||||
print(f"#{m['id']} [{ts}] {m['content']}")
|
||||
|
||||
elif action == 'delete':
|
||||
if not memo_id:
|
||||
print('请提供 id 参数')
|
||||
sys.exit(1)
|
||||
new = [m for m in data if str(m.get('id', '')) != str(memo_id)]
|
||||
if len(new) == len(data):
|
||||
print(f'未找到备忘 #{memo_id}')
|
||||
else:
|
||||
json.dump(new, open(path, 'w'), ensure_ascii=False, indent=2)
|
||||
print(f'已删除备忘 #{memo_id}')
|
||||
|
||||
else:
|
||||
print(f'未知操作: {action}(支持: add, list, delete)')
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
@@ -0,0 +1,42 @@
|
||||
# manage_scheduled_task
|
||||
|
||||
创建、修改、删除或启用/禁用定时任务。
|
||||
|
||||
## Risk Level
|
||||
High
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "操作: add | delete | enable | disable",
|
||||
"enum": ["add", "delete", "enable", "disable"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "任务名称"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "要执行的命令(add 时需要)"
|
||||
},
|
||||
"interval_seconds": {
|
||||
"type": "integer",
|
||||
"description": "执行间隔秒数(add 时需要)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "任务描述"
|
||||
}
|
||||
},
|
||||
"required": ["action", "name"]
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/manage.sh
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# 管理定时任务
|
||||
set -e
|
||||
|
||||
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)
|
||||
CMD=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))" 2>/dev/null)
|
||||
INTERVAL=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('interval_seconds',3600))" 2>/dev/null)
|
||||
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
|
||||
|
||||
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
|
||||
;;
|
||||
esac
|
||||
@@ -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"
|
||||
@@ -0,0 +1,30 @@
|
||||
# search_web
|
||||
|
||||
通过网络搜索获取最新信息(Tavily Search API)。
|
||||
|
||||
## Risk Level
|
||||
Low
|
||||
|
||||
## Parameters
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "搜索关键词"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "返回结果数量,默认5",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
```
|
||||
|
||||
## Execute
|
||||
```bash
|
||||
scripts/search.sh
|
||||
```
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Tavily 搜索
|
||||
|
||||
read -r input
|
||||
QUERY=$(echo "$input" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('query', ''))
|
||||
" 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
|
||||
|
||||
API_KEY="${TAVILY_API_KEY}"
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo "请设置 TAVILY_API_KEY 环境变量"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user