From 69ba8887ff6a28f7d7f41f5548c832ec253c5f66 Mon Sep 17 00:00:00 2001 From: wunianxiao Date: Wed, 3 Jun 2026 17:14:01 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=BB=9F=E4=B8=80=20README=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=97=E4=BD=99=E6=96=87=E6=A1=A3=EF=BC=9B?= =?UTF-8?q?refactor:=20=E6=B6=88=E9=99=A4=E5=85=A8=E9=83=A8=2015=20?= =?UTF-8?q?=E4=B8=AA=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 README.md,整合项目架构、命令、配置、数据流 - 删除 6 个冗余/过时 md 文档 - save_summary_to_db 委托 db::models::save_summary,避免 SQL 重复 - #[allow(dead_code)] 标注 serde 反序列化字段和预留 API - 移除未使用的 re-export (Summarizer, Message, Role, StreamChunk) - 修复 README 中 manage_memos 风险等级和缺失的 AMAP_KEY 配置 --- DAEMON_WORKER_PLAN.md | 440 ------------------------------- MIGRATION_PLAN.md | 151 ----------- PROJECT_REFERENCE.md | 171 ------------ README.md | 240 +++++++++++++++++ src/context/DESIGN.md | 193 -------------- src/context/types.rs | 25 +- src/llm/mod.rs | 4 +- src/llm/provider.rs | 2 + src/llm/types.rs | 2 + src/tools/DESIGN.md | 328 ----------------------- src/tools/builtins/web_search.rs | 2 + src/tools/types.rs | 2 + src/wechat/types.rs | 8 + 13 files changed, 267 insertions(+), 1301 deletions(-) delete mode 100644 DAEMON_WORKER_PLAN.md delete mode 100644 MIGRATION_PLAN.md delete mode 100644 PROJECT_REFERENCE.md create mode 100644 README.md delete mode 100644 src/context/DESIGN.md delete mode 100644 src/tools/DESIGN.md diff --git a/DAEMON_WORKER_PLAN.md b/DAEMON_WORKER_PLAN.md deleted file mode 100644 index e017d1d..0000000 --- a/DAEMON_WORKER_PLAN.md +++ /dev/null @@ -1,440 +0,0 @@ -# iAs 守护进程 / 调度进程分离 — 实现计划 - -> 生成于 2026-06-02,供新会话参考当前架构状态。 - ---- - -## 目标 - -``` -ias daemon (常驻) ias worker (短命,每条消息 spawn) -┌──────────────────────┐ ┌──────────────────────┐ -│ WeChat 长轮询 │ │ LLM 对话 │ -│ 消息收发 │ Unix │ 工具调用 │ -│ 审批匹配 │◄─Domain──►│ 无 DB 连接 │ -│ DB 持有 / 上下文预载 │ Socket │ 无状态(纯计算) │ -│ 消息队列(按用户串行) │ │ stdin 读取任务/环境 │ -└──────────────────────┘ └──────────────────────┘ -``` - -- **daemon** 稳定、不更新。持有 WeChat 长连接和数据库。 -- **worker** 每次收到消息 spawn,完成后销毁。代码更新 → 编译 → 下一条消息自动用新 worker。 -- **IPC**:Unix Domain Socket,长度前缀帧 + JSON 消息。 - ---- - -## 当前架构(供参考) - -``` -src/ -├── main.rs CLI 入口 + 监听循环 + 工具执行器 -├── cli.rs clap 子命令 (login/listen/send/whoami/usage/service) -├── db/ -│ ├── mod.rs Database 连接池 -│ └── models.rs CRUD (auth/chat/usage/summary/memories/approvals) -├── llm/ -│ ├── types.rs Message / Role / Usage / StreamChunk -│ ├── provider.rs create_provider / parse_chat_chunk -│ ├── deepseek.rs DeepSeek 流式客户端 -│ └── conversation.rs Conversation (chat + chat_with_tools 工具循环) -├── context/ -│ ├── types.rs ChatSession -│ ├── builder.rs token budget 构建 + 摘要触发 -│ └── tools.rs MemoryStore -├── tools/ -│ ├── types.rs RiskLevel / SkillSpec / SkillResult / ExecutionContext -│ ├── builtin.rs BuiltinRegistry (get_current_datetime / manage_memos / query_weather) -│ ├── weather.rs QWeather 客户端 (JWT + reqwest) -│ └── approval.rs ApprovalManager (确认码 + oneshot 通道) -└── wechat/ - ├── types.rs iLink API 协议类型 - └── client.rs HTTP 客户端 (login/poll/send/notify) -``` - ---- - -## 分阶段实现 - -### Phase 1: 协议层 — IPC 帧格式 - -**目标**:实现与业务无关的 `send_frame` / `recv_frame`。 - -**新文件**:`src/ipc.rs` - -```rust -// 长度前缀帧 -// [4 字节 u32 BE: payload_len] -// [N 字节: JSON payload] - -pub async fn send_frame(stream: &mut UnixStream, msg: &serde_json::Value) -> Result<()> -pub async fn recv_frame(stream: &mut UnixStream) -> Result -``` - -**测试**: -```bash -cargo test ipc -``` - ---- - -### Phase 2: Worker — 无状态执行进程 - -**目标**:`ias worker` 子命令,接收 task JSON,执行 LLM,输出结果帧。 - -**新文件**:`src/worker.rs` - -```rust -// ias worker --sock /tmp/ias_daemon.sock -pub async fn run(sock_path: &str) -> Result<()> { - let mut stream = UnixStream::connect(sock_path).await?; - - // 1. 读 task - let task: TaskFrame = recv_frame(&mut stream).await?; - - // 2. 注入环境变量(从 task.env 字段) - for (k, v) in &task.env { std::env::set_var(k, v); } - - // 3. 构建 Conversation(复用现有代码) - let mut conv = Conversation::new(config)?; - conv.session().load_history(&task.history); - conv.session().load_memories(&task.memories); - conv.session().load_summaries(&task.summaries); - - // 4. 注册工具执行器(复用现有代码) - conv.set_tool_executor(build_executor()); - - // 5. LLM 对话 + 工具循环 - let (reply, used_tools, usage) = conv.chat_with_tools(&task.msg.text).await?; - - // 6. 输出结果帧 - send_frame(&mut stream, &OutputFrame::usage(&usage)).await?; - send_frame(&mut stream, &OutputFrame::reply(&reply)).await?; - send_frame(&mut stream, &OutputFrame::bye()).await?; - - Ok(()) -} -``` - -**改动**: -- `cli.rs`:新增 `Worker { sock_path: String }` 子命令 -- `main.rs`:`Commands::Worker { sock_path } => worker::run(&sock_path).await` - -**不依赖 daemon,可独立测试**: -```bash -# 终端 1:模拟 daemon -socat UNIX-LISTEN:/tmp/ias.sock,fork EXEC:'echo done' - -# 终端 2:运行 worker -ias worker --sock /tmp/ias.sock -``` - ---- - -### Phase 3: Daemon — 常驻进程 + 消息队列 - -**目标**:`ias daemon` 子命令,持有 WeChat + DB,spawn worker。 - -**新文件**:`src/daemon.rs` - -```rust -pub async fn run(db: Arc, client: WeChatClient) -> ! { - let sock_path = "/tmp/ias_daemon.sock"; - let _ = std::fs::remove_file(sock_path); - let listener = UnixListener::bind(sock_path)?; - - let queue = MessageQueue::new(); - - loop { - tokio::select! { - // WeChat 消息 - msg = recv_message(&client) => { - if is_approval_reply(&msg) { - handle_approval(&msg).await; - } else { - queue.enqueue(msg); - process_queue(&queue, &db).await; - } - } - // Worker 连接(worker 启动后 connect) - (stream, _) = listener.accept() => { - let user_id = queue.pop_waiting(); - spawn_process(&user_id, stream, &db).await; - } - } - } -} -``` - -**消息队列设计**(`daemon.rs` 内部): -```rust -struct MessageQueue { - // 每个用户最多一个活跃 worker - active: HashSet, - // 等待队列 - pending: HashMap>, - // 准备就绪待处理的用户 - waiting: VecDeque, -} -``` - -**Daemon 处理一个消息的完整流程**: -```rust -async fn spawn_process(user_id, stream, db) { - // 1. 预载上下文(从 DB) - let history = db::models::list_recent_chat_records(&db.pool, &user_id, 20).await?; - let memories = db.memory_store.read_for(&user_id).await; - let summaries = db::models::load_summaries(&db.pool, 5).await?; - - // 2. 构建 env(从当前进程环境变量) - let env = HashMap::from([ - ("DEEPSEEK_API_KEY", std::env::var("DEEPSEEK_API_KEY")?), - ("DEEPSEEK_MODEL", std::env::var("DEEPSEEK_MODEL")?), - ("QWEATHER_API_HOST", std::env::var("QWEATHER_API_HOST")?), - // ... - ]); - - // 3. 发送 task 帧 - send_frame(&mut stream, &TaskFrame { user_id, msg, history, memories, summaries, env }).await?; - - // 4. 逐帧处理 worker 输出 - loop { - match recv_frame(&mut stream).await? { - OutputFrame::Chunk { text } => { - // 暂存,等最终 reply 一起发送 - reply_buffer.push_str(&text); - } - OutputFrame::DbWrite { table, row } => { - execute_db_write(&db, table, row).await; - } - OutputFrame::Reply { text } => { - client.send_text(&user_id, &text, &ctx_token).await?; - // 发送成功后记录聊天 - } - OutputFrame::NeedApproval { tool, code, message } => { - approval_manager.create(&user_id, &tool, &code).await?; - client.send_text(&user_id, &message, None).await?; - // Worker 退出。审批完成后再 spawn 新 worker。 - break; - } - OutputFrame::Bye => break, - } - } -} -``` - -**改动**: -- `cli.rs`:新增 `Daemon` 子命令(替代 `Service`) -- `main.rs`:`Commands::Daemon => daemon::run(db, client).await` - ---- - -### Phase 4: 审批跨进程 - -**目标**:worker 遇 High 风险工具 → 输出 `need_approval` 帧 → daemon 处理 → 重新 spawn worker。 - -**流程**: - -``` -Worker Daemon - │ │ - ├─ LLM 请求 manage_memos │ - ├─ 遇 High 风险 │ - ├─ write(need_approval) ──────→├─ 读 need_approval - ├─ write(bye) ────────────────→├─ 创建审批记录到 DB - │ ├─ 发确认消息到微信 - │ (Worker 退出) │ - │ ├─ 等待用户回复... - │ ├─ 收到确认码 → 匹配 - │ │ - │ (新 Worker spawn) │ - │◄───── write(task + approved)─├─ task 帧中带 approved_tool - ├─ LLM 继续执行 │ - ├─ write(reply) ──────────────→├─ 发最终回复 -``` - -**task 帧扩展**: -```json -{ - "user_id": "...", - "msg": {...}, - "history": [...], - "approved_tool": "manage_memos", // ← 新增 - "env": {...} -} -``` - ---- - -### Phase 5: 流式输出 - -**目标**:LLM 每生成一个 token,通过 UDS 帧实时推送给 daemon,daemon 可选实时推用户。 - -**当前行为**:等全部生成完 → 一次发送。 -**流式行为**:逐 chunk 发送 → daemon 攒到换行或完成 → 发送微信。 - -```json -{"type":"chunk","text":"北"} -{"type":"chunk","text":"京今"} -{"type":"chunk","text":"天晴"} -// 可攒多个 chunk 再发一帧以减少帧开销 -{"type":"chunk","text":"北京今天晴天,25°C"} -{"type":"reply","text":"北京今天晴天,25°C,微风。"} -``` - -微信不原生支持流式推送,所以收益有限。作为最后阶段可选项。 - ---- - -## 消息协议完整定义 - -### TaskFrame (Daemon → Worker) - -```json -{ - "type": "task", - "user_id": "wxid_abc", - "msg": { - "from": "wxid_abc", - "text": "北京天气", - "account_id": "bot_123", - "context_token": "token_xyz" - }, - "history": [ - {"role": "user", "content": "你好"}, - {"role": "assistant", "content": "你好!有什么可以帮你?"} - ], - "memories": ["用户叫张伟", "住在北京"], - "summaries": ["之前讨论过天气偏好..."], - "approved_tool": null, - "env": { - "DEEPSEEK_API_KEY": "sk-xxx", - "DEEPSEEK_MODEL": "deepseek-v4-flash", - "QWEATHER_API_HOST": "ky5ctpp742.re.qweatherapi.com", - "QWEATHER_JWT_KEY_ID": "KF5AJT3JE9", - "QWEATHER_JWT_PROJECT_ID": "4E2DWXQAVM", - "QWEATHER_JWT_PRIVATE_KEY_FILE": "/home/xiao/project/iAs/qweather/ed25519-private.pem" - } -} -``` - -### OutputFrame (Worker → Daemon) - -```json -{"type": "chunk", "text": "..."} - -{"type": "db_write", "table": "chat_records", "row": {...}} -{"type": "db_write", "table": "llm_usage", "row": {...}} - -{"type": "reply", "text": "最终回复文本"} - -{"type": "need_approval", "tool": "manage_memos", "code": "A1B2C3", - "message": "⚠️ 确认码 A1B2C3,回复确认码继续。"} - -{"type": "bye"} -``` - -### DB Write 表定义 - -| table | row 字段 | -|-------|---------| -| `chat_records` | `direction`, `user_id`, `account_id`, `text`, `source`, `context_token`, `message_id` | -| `llm_usage` | `user_id`, `model`, `provider`, `prompt_tokens`, `completion_tokens`, `cache_hit_tokens`, `cache_miss_tokens` | -| `user_memories` | `user_id`, `content` | - ---- - -## 文件清单 - -| 文件 | Phase | 说明 | -|------|-------|------| -| `src/ipc.rs` | 1 | `UnixStream` + 长度前缀帧 + `send_frame`/`recv_frame` | -| `src/worker.rs` | 2 | Worker 主逻辑:读 task → LLM → 写结果帧 | -| `src/daemon.rs` | 3 | Daemon 主逻辑:WeChat 轮询 + 消息队列 + spawn worker | -| `src/cli.rs` | 1-3 | 新增 `Daemon`、`Worker` 子命令,保留 `Login/Send/Usage` | -| `src/main.rs` | 1-3 | 命令路由,移除 `Service`(由 `Daemon` 替代) | -| `Cargo.toml` | 1 | 新增 `tokio/io-util`(如果有) | - -**不改动的文件**: -- `src/llm/` — 全部复用 -- `src/tools/` — 全部复用 -- `src/db/` — 全部复用(仅 daemon 侧调用) -- `src/wechat/` — 全部复用(仅 daemon 侧调用) -- `src/context/` — 需小改(ChatSession 支持从历史加载) - ---- - -## 测试方法 - -```bash -# 1. 启动 daemon -ias daemon --sock /tmp/ias.sock & - -# 2. 手动测试 worker(不通过 daemon) -echo '{"type":"task","user_id":"test","msg":{"text":"北京天气"},...}' \ - | socat - UNIX-CONNECT:/tmp/ias.sock - -# 3. 完整流程(模拟微信消息) -ias send wxid_test "北京天气" -# daemon 收到 → spawn worker → 回复 -``` - ---- - -## 关键实现细节 - -### ChatSession 从预载数据初始化 - -当前 `ChatSession` 在收到第一条消息时通过 `load_recent_messages` 从 DB 加载历史。Worker 模式需要从 task 帧直接从内存加载: - -```rust -// 在 ChatSession 或 WorkerContext 中新增 -pub fn load_from_history(&mut self, history: &[HistoryEntry]) { - for entry in history { - match entry.role { - "user" => self.add_user(&entry.content), - "assistant" => self.add_assistant(&entry.content), - _ => {} - } - } -} -``` - -### Worker 中的工具执行器 - -Worker 中注册的工具执行器**不连接 DB**: - -```rust -// read_memories / write_memory → 返回消息让 daemon 处理 -// 在 worker 上下文中,只返回 db_write 帧 -fn build_worker_executor() -> ToolExecutor { - Arc::new(move |name, args| { - Box::pin(async move { - match name { - "read_memories" => { - // memories 已在 task 帧中预载 → 直接返回 - Ok(format!("长期记忆: {:?}", loaded_memories)) - } - "write_memory" => { - // 返回 db_write 帧,由 daemon 执行 - Ok(json!({"type":"db_write","table":"user_memories","row":{...}}).to_string()) - } - // 其他 builtin 工具直接执行 - _ => builtin::execute(name, args) - } - }) - }) -} -``` - ---- - -## 工作量估计 - -| Phase | 内容 | 估计 | -|-------|------|------| -| 1 | IPC 帧格式 | 30 min | -| 2 | Worker 独立运行 | 1 h | -| 3 | Daemon + 消息队列 | 2 h | -| 4 | 审批跨进程 | 1 h | -| 5 | 流式输出 | 30 min(可选) | -| **合计** | Phase 1-4 | **~5 h** | diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md deleted file mode 100644 index 2598b96..0000000 --- a/MIGRATION_PLAN.md +++ /dev/null @@ -1,151 +0,0 @@ -# iPet → Rust (iAs) 迁移计划 - -## 项目定位 -将 iPet(Bun/JS 微信 AI 聊天机器人)迁移到 Rust 实现的 `iAs` 项目。 - -## 技术栈 - -| 模块 | Rust 方案 | -|------|-----------| -| 运行时 | `tokio`(full) | -| HTTP 客户端 | `reqwest`(json + stream) | -| 序列化 | `serde` / `serde_json` | -| CLI | `clap`(derive) | -| 数据库 | `sqlx`(postgres + uuid + chrono) | -| 邮箱 | `async-imap` + `mailparse` | -| JWT | `jsonwebtoken` | -| 定时任务 | `tokio-cron-scheduler` | -| 流式 | `tokio-stream` + `futures` | -| 加密 | `sha2` + `hex` + `uuid` | - -## 分阶段计划 - -### Phase 1 ✅ CLI 框架 + 配置系统 -- [x] clap 子命令:login, listen, send, whoami -- [x] 配置文件读取(config.json + env var 替换) -- [x] 日志系统(tracing + EnvFilter) -- [x] 状态管理(auth.json / runtime.json 持久化) - -### Phase 2 🔜 微信通道(当前阶段) -- [x] API 类型定义 -- [ ] 扫码登录(get_bot_qrcode + get_qrcode_status) -- [ ] 长轮询收消息(getupdates) -- [ ] 发送消息(sendmessage) -- [ ] 注册/注销监听器(notifystart/notifystop) -- [ ] 身份和 token 持久化 - -### Phase 3 ✅ AI 对话(纯对话,无工具) -- [x] DeepSeek 流式 API(SSE + thinking 模式) -- [x] LM Studio 兼容(OpenAI 接口) -- [x] Provider 抽象(LlmProvider trait) -- [x] 对话管理器(Conversation) - - 消息历史管理 - - 系统提示词 - - 自动修剪历史 - - 流式消费 + 自动记录 -- [x] listen 命令集成 LLM 回复 - -### Phase 4 ✅ PostgreSQL 集成(认证+聊天记录入库) -- [x] sqlx 迁移框架(`migrations/` 目录) -- [x] `app_state` 表:认证信息 Key-Value 存储 -- [x] `chat_records` 表:消息收发记录 -- [x] 数据库连接池管理(`Database` 结构体) -- [x] 文件存储 → 数据库的自动迁移 -- [x] 文件存储降级(无 DB 时保持可用) -- [x] 入库时机: - - login → auth 入库 - - 收到消息 → inbound 记录 - - echo/LLM 回复 → outbound 记录 - - send 命令 → outbound 记录 - -### Phase 5 🎯 一切皆 Skill(工具系统) - -**设计文档:** `src/tools/DESIGN.md` - -**核心思想:** 没有"内置工具"和"外部技能"的区别——所有工具都是 SKILL.md。 -- 系统 skills — `resources/skills/`,随 iAs 分发 -- 用户 skills — `skills/`,用户自行添加 -- 加载时合并,同名用户覆盖系统 - -**安全模型(仅两级):** -| 等级 | 行为 | -|------|------| -| Low | 自动执行 | -| High | 微信确认码审批(6位码,5分钟过期)| - -**系统 Skills(7个):** -| Skill | 风险 | 说明 | -|-------|------|------| -| `get_current_datetime` | Low | 获取时间 | -| `query_weather` | Low | 和风天气 | -| `search_web` | Low | Tavily 搜索 | -| `email` | High | 收发邮件 | -| `execute_shell` | High | 执行 shell | -| `list_scheduled_tasks` | Low | 列定时任务 | -| `manage_scheduled_task` | High | 管理定时任务 | - -**去掉的模块:** -- ~~MCP 桥接~~(由 Skills 替代) -- ~~Shell 能力~~(由 Skills 替代) -- ~~Medium 风险层~~(仅保留 Low/High) - -**实施步骤:** -- [x] 设计文档(DESIGN.md) -- [x] Step 1: SkillSpec/Skill/SkillRegistry 基础设施 -- [x] Step 2: SKILL.md 解析器 + 目录扫描(system + user 合并) -- [x] Step 3: 技能执行器(子进程 + stdin 参数 + 结果解析) -- [x] Step 4: 系统 Skills 脚本实现(7 个) - - get_current_datetime ✅ / query_weather ✅ / search_web ✅ - - email ✅ / execute_shell ✅ / list_scheduled_tasks ✅ / manage_scheduled_task ✅ -- [x] Step 5: 审批系统(pending_approvals + 确认码 + oneshot 通道) - - approval.rs: ApprovalManager(内存 + 数据库双存储) - - 确认码生成/验证(SHA-256 哈希) - - 取消支持(0 / 取消) - - 3 次重试机制 - - 透明模式(LLM 不感知审批过程,确认码回复被 consume 不传 LLM) -- [x] Step 6: LLM function calling 集成 - - ToolCall 类型 + StreamChunk::ToolCalls - - 流式解析器支持 tool_calls delta - - Conversation::chat_with_tools() 工具循环 - - SkillSpec → LLM tools JSON 转换 - - ToolExecutor 回调桥接 SkillRegistry - - 审批回复拦截(handle_reply 优先于 LLM) - - WeChatClient 可克隆(用于 send_wechat 闭包) - -### Phase 6 定时任务(PostgreSQL 调度) -- [ ] 基于 PostgreSQL 的定时任务调度 - -### Phase 7 上下文管理 + 信任系统 -- [ ] 长短期记忆 -- [ ] 滚动摘要 -- [ ] 用户验证/授权 - -### Phase 8 优化、测试、部署 -- [ ] 错误处理完善 -- [ ] 日志优化 -- [ ] systemd 服务 - -## 微信通道 API 参考 - -Base URL: `https://ilinkai.weixin.qq.com` - -### 认证 -headers: -- `Authorization: Bearer ` -- `AuthorizationType: ilink_bot_token` -- `Content-Type: application/json` -- `iLink-App-Id: ` -- `iLink-App-ClientVersion: ` -- `X-WECHAT-UIN: ` - -### 端点 -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/ilink/bot/get_bot_qrcode?bot_type=3` | 获取登录二维码 | -| GET | `/ilink/bot/get_qrcode_status?qrcode=` | 轮询扫码状态 | -| POST | `/ilink/bot/msg/notifystart` | 注册监听器 | -| POST | `/ilink/bot/msg/notifystop` | 注销监听器 | -| POST | `/ilink/bot/getupdates` | 长轮询消息 | -| POST | `/ilink/bot/sendmessage` | 发送消息 | -| POST | `/ilink/bot/getconfig` | 获取配置 | -| POST | `/ilink/bot/sendtyping` | 发送输入状态 | diff --git a/PROJECT_REFERENCE.md b/PROJECT_REFERENCE.md deleted file mode 100644 index b030d96..0000000 --- a/PROJECT_REFERENCE.md +++ /dev/null @@ -1,171 +0,0 @@ -# iAs 项目参考文档 - -> 自动生成于 2026-06-02,供新会话快速了解当前状态。 -> 4950+ 行 Rust,20 次安全审查迭代后稳定运行。 - ---- - -## 项目定位 - -微信 AI 助手。扫码登录微信 iLink Bot,长轮询接收消息,DeepSeek 回复。 -支持工具调用(天气/搜索/邮件/备忘录/定时任务)、长期记忆、审批确认、Token 统计。 - ---- - -## 运行方式 - -```bash -ias login # 扫码登录 -ias listen --llm # 监听消息 + AI 回复 -ias service # 后台模式(日志写入 ~/.ias/logs/) -ias usage # Token 统计 -ias skills list # 列出可导入 Skill -ias skills import # 交互式导入 Skill 到 ~/.ias/skills/ -ias skills delete # 交互式删除 -``` - ---- - -## 源码结构 - -``` -src/ -├── main.rs (900行) CLI 入口 + 命令分发 + 监听循环 + 工具执行器 -├── cli.rs clap 子命令定义 -├── logger.rs 日志初始化(终端 + 文件滚动) -├── state.rs 文件状态管理(auth/runtime,PostgreSQL 不可用时的降级方案) -├── scheduler.rs 定时任务调度器(轮询 PostgreSQL + 事务保护) -├── skills_importer.rs Skills 导入扫描器 -├── context/ 上下文管理 -│ ├── types.rs ChatSession(checkpoint/messages/summaries/user_id) -│ ├── builder.rs token budget 构建 + 摘要触发 -│ ├── tools.rs MemoryStore(HashMap>)+ read_summaries -│ └── DESIGN.md 上下文管理设计(未完全实现) -├── db/ 数据库 -│ ├── mod.rs Database 连接池 + 迁移 -│ └── models.rs CRUD(auth/chat/usage/summary) -├── llm/ 对话系统 -│ ├── types.rs Message / Role / StreamChunk / Usage -│ ├── provider.rs create_provider / parse_chat_chunk(SSE 解析) -│ ├── deepseek.rs DeepSeek 流式客户端(thinking + tool_calls) -│ └── conversation.rs Conversation(chat + chat_with_tools 工具循环) -├── tools/ 工具系统 -│ ├── skill.rs SkillSpec / RiskLevel / SkillResult -│ ├── registry.rs SkillRegistry(加载/执行/审批流程) -│ ├── builtin.rs BuiltinRegistry(datetime + memos) -│ ├── parser.rs SKILL.md 解析器(支持 YAML frontmatter) -│ ├── approval.rs ApprovalManager(确认码 + oneshot 通道) -│ └── DESIGN.md 工具系统设计文档 -└── wechat/ 微信通道 - ├── types.rs iLink API 协议类型 - └── client.rs HTTP 客户端(login/poll/send/notify) -``` - ---- - -## 配置(仅环境变量) - -| 变量 | 说明 | -|------|------| -| `DEEPSEEK_API_KEY` | DeepSeek API 密钥 | -| `DEEPSEEK_MODEL` | 模型名(默认 deepseek-v4-flash) | -| `DATABASE_URL` | PostgreSQL 连接串(可选,无则用文件存储) | -| `TAVILY_API_KEY` | Tavily 搜索 API | -| `QWEATHER_API_HOST` | 和风天气 API | -| `QWEATHER_JWT_KEY_ID/PROJECT_ID/PRIVATE_KEY_FILE` | 天气 JWT | -| `WEIXIN_LLM_SYSTEM_PROMPT` | 自定义系统提示词 | - -加载顺序:`./.env` → `~/.ias/.env` → 环境变量 - ---- - -## 工具架构 - -### LLM 看到的工具(Tool List) - -``` -query_capabilities → 列出所有可用工具及说明 -call_capability → 按名称调用(name + prompt) -read_memories → 读长期记忆 -write_memory → 写长期记忆 -read_summaries → 读历史摘要 -``` - -### 工具执行路径 - -``` -executor: - ├── 上下文工具: read_memories / write_memory / read_summaries - ├── query_capabilities: 合并内置 + 外部 SkillSpecs - ├── BuiltinRegistry(内置 Rust 工具): - │ ├── get_current_datetime → chrono::Local::now() - │ └── manage_memos → 文件 I/O(High 先审批) - └── SkillRegistry(外部 Skills): - ├── query_weather → bash + openssl JWT - ├── search_web → python Tavily - ├── email → node.js IMAP - ├── execute_shell → bash(High) - ├── list_scheduled_tasks → psql - ├── manage_scheduled_task → psql(High) - └── manage_skill → bash(High) -``` - ---- - -## 审批流程 - -``` -High 风险工具 → builtin_approve() 或 approval_flow() - ├── 生成 6 位确认码 → SHA-256 入库 - ├── 微信发送确认消息 - ├── await oneshot::Receiver(最多 300s) - ├── 用户回复 → handle_reply() → oneshot::Sender - └── 结果返回 LLM(审批过程 LLM 不可见) -``` - ---- - -## 上下文管理(当前实现) - -- **Token Budget**:28K tokens,按中/英文字符估算 -- **检查点**:chatSession.checkpoint,之前的消息已被压缩 -- **摘要触发**:预算溢出或 12h 空闲 -- **用户切换**:清空 session + 加载新用户历史 + 加载记忆 -- **摘要生成**:简单截断占位(未实现 LLM 生成) - ---- - -## 数据库表 - -| 表 | 用途 | -|----|------| -| `app_state` | 认证 KV 存储 | -| `chat_records` | 收发消息记录 | -| `llm_usage` | Token 用量(缓存命中率) | -| `user_memories` | 长期记忆(按 user_id) | -| `pending_approvals` | 审批记录 | -| `session_summaries` | 会话摘要 | -| `scheduled_tasks` | 定时任务 | - ---- - -## 已知限制 - -| 问题 | 状态 | -|------|------| -| 多用户并发(共享 Conversation) | ⚠️ 单用户假设 | -| 上下文摘要未用 LLM 生成 | ⚠️ 占位截断 | -| 子代理模式(call_capability prompt → 参数) | ⚠️ 简单关键词提取 | -| 外部 Skills 依赖 python3/psql/node | ⚠️ 需运行时 | -| 核心工具未全用 Rust(weather/search/email) | ⚠️ 仍为 bash | - ---- - -## 最近重要 commit - -``` -6708fe3 fix: 3 项问题 (审批DB精确匹配 + delete id提取 + 清理变量) -065229b fix: 优先审批 + 首用户隔离 + 审批DB闭环 + memo错误 -079150b refactor: 删除 config.json + 内置工具抽离 + 统一接口 -b710518 refactor: 工具简化为「查询 + 调用」二元接口 -``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad76670 --- /dev/null +++ b/README.md @@ -0,0 +1,240 @@ +# iAs — 微信 AI 智能助手 + +Rust 实现的微信 iLink Bot,支持 DeepSeek AI 自动回复、内置工具调用、审批确认、定时任务和长期记忆。 + +## 快速开始 + +```bash +# 1. 配置环境变量(或创建 .env 文件) +export DEEPSEEK_API_KEY="sk-xxx" +export DATABASE_URL="postgresql://..." # PostgreSQL 可选 + +# 2. 扫码登录微信 +ias login + +# 3. 启动 AI 自动回复 +ias listen --llm + +# 4. 后台运行(配合 systemd) +ias service +``` + +## 命令一览 + +| 命令 | 说明 | +|------|------| +| `ias login` | 扫码登录微信,认证信息存入 PostgreSQL / 本地文件 | +| `ias listen --llm` | 启动长轮询 + AI 自动回复 | +| `ias listen --echo` | 启动长轮询 + 原样回显 | +| `ias listen` | 仅监听,不做回复 | +| `ias send --to --text ` | 手动发送消息 | +| `ias whoami` | 查看登录状态 | +| `ias usage` | Token 用量统计(默认 7 天) | +| `ias service` | 后台服务模式(日志 + systemd) | +| `ias daemon` | 守护进程模式(daemon + worker 分离架构) | +| `ias tool datetime` | 获取当前时间 | +| `ias tool weather <城市>` | 查询天气 | +| `ias tool search <关键词>` | 联网搜索 | +| `ias tool fetch ` | 提取网页正文 | +| `ias tool memos list/add/delete` | 管理备忘录 | +| `ias tool amap poi-search/geocode/route-plan/travel-plan` | 高德地图服务 | + +## 配置(环境变量) + +| 变量 | 必须 | 说明 | +|------|------|------| +| `DEEPSEEK_API_KEY` | ✅ | DeepSeek API 密钥 | +| `DEEPSEEK_MODEL` | — | 模型名,默认 `deepseek-v4-flash` | +| `DATABASE_URL` | — | PostgreSQL 连接串,无则降级文件存储 | +| `WEIXIN_LLM_SYSTEM_PROMPT` | — | 自定义系统提示词 | +| `TAVILY_API_KEY` | — | Tavily 搜索 API Key | +| `QWEATHER_API_HOST` | — | 和风天气 API 地址 | +| `QWEATHER_JWT_KEY_ID` | — | 和风天气 JWT Key ID | +| `QWEATHER_JWT_PROJECT_ID` | — | 和风天气项目 ID | +| `QWEATHER_JWT_PRIVATE_KEY_FILE` | — | 和风天气 Ed25519 私钥路径 | +| `AMAP_WEBSERVICE_KEY` | — | 高德地图 Web 服务 Key(或 `AMAP_KEY`) | + +加载顺序:`./.env` → `~/.ias/.env` → 环境变量 + +## 架构 + +``` +┌─────────────────────────────────────────────────┐ +│ iAs CLI │ +│ main.rs → clap 子命令路由 │ +├─────────────────────────────────────────────────┤ +│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ WeChat │ │ LLM │ │ Tools │ │ +│ │ 长轮询 │ │ DeepSeek │ │ 内置工具 │ │ +│ │ HTTP API │ │ 流式对话 │ │ 审批管理 │ │ +│ └────┬─────┘ └────┬─────┘ └──────┬────────┘ │ +│ │ │ │ │ +│ ┌────┴──────────────┴───────────────┴────────┐ │ +│ │ Context / Memory │ │ +│ │ ChatSession · Token Budget · 摘要压缩 │ │ +│ │ MemoryStore · 长期记忆 │ │ +│ └──────────────────────┬──────────────────────┘ │ +│ │ │ +│ ┌──────────────────────┴──────────────────────┐ │ +│ │ Database (PostgreSQL) │ │ +│ │ auth · chat_records · llm_usage │ │ +│ │ user_memories · pending_approvals │ │ +│ │ session_summaries · scheduled_tasks │ │ +│ └─────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────┤ +│ Daemon / Worker IPC (可选) │ +│ Unix Domain Socket · 帧协议 · 进程隔离 │ +└─────────────────────────────────────────────────┘ +``` + +## 核心数据流 + +``` +微信消息到达 + → 保存 inbound chat_records + → 用户切换:加载历史 + 记忆 + → 构建 LLM 上下文(token budget + 摘要注入) + → Conversation.chat_with_tools() + ├── LLM 流式回复 + ├── 工具调用 → query_capabilities / call_capability + │ ├── 上下文工具:read_memories / write_memory / read_summaries + │ ├── 内置工具:datetime / weather / search / memos / fetch_page / amap + │ └── High 风险工具 → 审批流程 + └── 工具结果注入对话 → LLM 继续 + → 保存 outbound chat_records + LLM usage + → 微信发送回复 +``` + +## 源码结构 + +``` +src/ +├── main.rs CLI 入口 + 命令实现 + 监听循环 + 工具执行器 +├── cli.rs clap 子命令定义(login/listen/send/tool 等) +├── logger.rs 日志初始化(终端 + 文件滚动) +├── state.rs 本地文件状态(auth.json / runtime.json,无DB时降级) +├── scheduler.rs 定时任务调度器 +├── context/ 上下文管理 +│ ├── types.rs ChatSession(checkpoint/messages/summaries) +│ ├── builder.rs Token budget 估算 + 上下文构建 + 摘要触发 +│ └── tools.rs MemoryStore(HashMap>) +├── db/ 数据库 +│ ├── mod.rs Database 连接池 + 迁移 +│ └── models.rs CRUD(auth/chat/usage/summary) +├── llm/ 对话系统 +│ ├── types.rs Message / Role / ToolCall / StreamChunk / Usage +│ ├── provider.rs LlmProvider trait + SSE 解析 +│ ├── deepseek.rs DeepSeek 流式客户端 +│ └── conversation.rs Conversation(chat + chat_with_tools 工具循环) +├── tools/ 工具系统 +│ ├── types.rs RiskLevel / SkillSpec / SkillResult / ExecutionContext +│ ├── builtin.rs BuiltinRegistry 路由 +│ ├── approval.rs ApprovalManager(确认码 + oneshot 通道) +│ └── builtins/ 内置工具实现 +│ ├── datetime.rs 日期时间 +│ ├── weather.rs 和风天气(JWT + reqwest) +│ ├── web_search.rs Tavily 搜索 +│ ├── fetch_page.rs 网页抓取 +│ ├── memos.rs 备忘录 +│ └── amap.rs 高德地图(POI/地理编码/路径/旅游规划) +├── wechat/ 微信通道 +│ ├── types.rs iLink API 协议类型 +│ └── client.rs HTTP 客户端(login/poll/send/notify) +├── daemon.rs 守护进程(持有 WeChat + DB,spawn worker) +├── worker.rs Worker 进程(无状态 LLM 执行) +└── ipc.rs Unix Domain Socket 帧协议 +``` + +## 工具系统 + +### LLM 可见工具 + +| 工具名 | 说明 | +|--------|------| +| `query_capabilities` | 列出所有可用工具及参数说明 | +| `call_capability` | 按名称调用任意工具 | +| `read_memories` | 读取用户长期记忆 | +| `write_memory` | 写入长期记忆 | +| `read_summaries` | 读取历史会话摘要 | + +### 内置工具 + +| 工具 | 风险 | 说明 | +|------|------|------| +| `get_current_datetime` | Low | 获取北京时间 | +| `query_weather` | Low | 和风天气查询 | +| `web_search` | Low | Tavily 联网搜索 | +| `fetch_page` | Low | 网页正文提取 | +| `manage_memos` | Low | 备忘录增删查 | +| `amap_*` | Low | 高德地图系列 | + +### 审批流程 + +High 风险工具需要用户微信确认: + +``` +LLM 调用 High 工具 + → 生成 6 位确认码 → SHA-256 入库 + → 微信发送确认消息(含确认码) + → 等待用户回复(最多 300s,3 次重试) + → 匹配成功 → 执行工具 → 返回结果 + → 匹配失败 / 超时 → 返回拒绝信息 +``` + +LLM 不感知确认码和审批过程,只看到最终的执行结果。 + +## 上下文管理 + +- **Token Budget**:28K tokens,中/英文字符估算 +- **Checkpoint**:摘要压缩点,之前的消息已压缩,之后完整保留 +- **摘要触发**:token 溢出(overflow)或 12h 空闲(timeout) +- **长期记忆**:LLM 按需通过 `read_memories` / `write_memory` 读写 +- **用户隔离**:切换用户时清空 session,按需加载新用户历史 + +## 数据库表 + +| 表 | 用途 | +|----|------| +| `app_state` | 认证信息 KV 存储 | +| `chat_records` | 收发消息记录(message_id 去重) | +| `llm_usage` | Token 用量统计(含缓存命中率) | +| `user_memories` | 长期记忆(按 user_id) | +| `pending_approvals` | 待审批记录(SHA-256 确认码) | +| `session_summaries` | 会话摘要 | +| `scheduled_tasks` | 定时任务 | + +## 部署 + +### systemd 服务 + +```bash +sudo cp systemd/ias.service /etc/systemd/system/ +# 编辑 .env 文件确保 API Key 等环境变量已配置 +sudo systemctl enable --now ias +``` + +### 天气 API 密钥 + +将和风天气 Ed25519 密钥对放入 `qweather/` 目录: + +``` +qweather/ +├── ed25519-private.pem +└── ed25519-public.pem +``` + +## 依赖 + +| 依赖 | 用途 | +|------|------| +| tokio | 异步运行时 | +| clap | CLI 参数解析 | +| reqwest | HTTP 客户端 | +| sqlx | PostgreSQL 驱动 + 迁移 | +| serde / serde_json | 序列化 | +| chrono | 时间处理 | +| tracing | 结构化日志 | +| async-trait | 异步 trait | +| ed25519-dalek | 天气 API JWT 签名 | +| scraper | 网页正文提取 | +| sha2 / hex / rand | 审批确认码加密 | diff --git a/src/context/DESIGN.md b/src/context/DESIGN.md deleted file mode 100644 index 3471c06..0000000 --- a/src/context/DESIGN.md +++ /dev/null @@ -1,193 +0,0 @@ -# iAs 上下文管理系统设计 v2 - -## 核心理念 - -1. **长期记忆不固定注入** — 通过工具让 LLM 按需读写 -2. **摘要不入上下文** — 除非因上下文空间不足被迫压缩 -3. **摘要总结点** — 记录压缩位置,之后的消息始终完整保留 -4. **12 小时空闲触发** — 超过 12h 自动压缩,但不插入会话 -5. **Token Budget 驱动** — 达到上限才压缩,保持消息完整度优先 - ---- - -## 状态模型 - -``` -ChatSession -├── checkpoint: usize = 0 ← 摘要总结点(消息列表中的位置) -├── messages: Vec ← 全部消息(checkpoint 之前的是历史) -├── summaries: Vec ← 所有摘要 -│ ├── pos: usize ← 对应消息列表中的位置 -│ ├── text: String ← 摘要内容 -│ └── reason: "overflow"|"timeout" ← 生成原因 -└── last_user_at: Option ← 上一条用户消息时间 -``` - -### 消息列表视图(LLM 实际看到的内容) - -``` -TOOL: read_memories / write_memory / read_summaries - ↑ LLM 按需调用,不自动注入 - -VIEW: [system_prompt] - [overflow_summary] ← 仅当因 context 满产生时注入 - [messages from checkpoint..] ← 始终完整保留 -``` - ---- - -## 核心类型 - -```rust -pub struct SummaryEntry { - pub checkpoint: usize, // 对应 messages 中的位置 - pub text: String, - pub reason: SummaryReason, -} - -pub enum SummaryReason { - Overflow, // 上下文满了自动压缩 - Timeout, // 超过 12 小时空闲 -} - -pub struct ChatSession { - checkpoint: usize, - messages: Vec, - summaries: Vec, - last_user_at: Option>, - token_budget: u32, // 默认 32000 -} -``` - ---- - -## 核心流程 - -``` -用户发消息 "合肥天气" - │ - ▼ -1. 距离检查 - ├── last_user_at 超过 12h? - │ └── YES → summarize_all(reason=timeout) - │ ├── 生成摘要存入 summaries(不插入会话) - │ └── checkpoint = messages.len() - │ - ▼ -2. 构建 LLM 请求 - ├── system_prompt - ├── 【如果有 overflow_summary 且 checkpoint > 0】 - │ └── { role: "system", content: "历史摘要:..." } - └── messages[checkpoint..] - │ - ▼ -3. Token 估算 - ├── 在 budget 内 → 直接发送 - │ - └── 超出 budget → summarize_all(reason=overflow) - ├── 生成摘要存入 summaries - ├── checkpoint = messages.len() - ├── 摘要作为 system message 注入本次请求 - └── 重新构建请求(现在只有摘要 + 当前消息) - │ - ▼ -4. 发送给 LLM - ├── LLM 回复(可能包含工具调用) - │ ├── read_memories → 返回长期记忆 - │ ├── write_memory → 保存到长期记忆 - │ └── read_summaries → 返回 summaries 列表 - │ - └── LLM 最终回复 → record_turn(user_msg, assistant_msg) - ├── messages.push(user_msg, assistant_msg) - └── last_user_at = now -``` - ---- - -## 摘要触发条件对比 - -| 原因 | 触发条件 | 是否插入会话 | 用途 | -|------|---------|------------|------| -| **Overflow** | token 超 budget | ✅ 自动插入 | 保持上下文不超限 | -| **Timeout** | 距上条消息 >12h | ❌ 不插入(工具可读) | 跨天会话,LLM 自行决定是否查看 | - ---- - -## 长期记忆工具(LLM 按需调用) - -```json -{ - "name": "read_memories", - "description": "读取用户的长期记忆(跨会话持久保存的个人信息)", - "parameters": { "type": "object", "properties": {} } -} - -{ - "name": "write_memory", - "description": "写入一条用户的长期记忆", - "parameters": { - "type": "object", - "properties": { - "content": { "type": "string", "description": "记忆内容" } - }, - "required": ["content"] - } -} - -{ - "name": "read_summaries", - "description": "读取历史会话摘要(包括空闲超时产生的摘要)", - "parameters": { - "type": "object", - "properties": {} - } -} -``` - -这些工具注册为系统 skills(只对 `chat_with_tools` 启用,不暴露给用户)。 - -## 数据库存储 - -```sql --- 长期记忆(跨会话持久) -CREATE TABLE user_memories ( - user_id TEXT PRIMARY KEY, - content JSONB NOT NULL DEFAULT '[]', - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); -``` - -摘要和消息会话不持久化(会话消息保存到 `chat_records` 表,但摘要只存在于运行时的 `ChatSession`)。 - -## 与 iPet 的区别 - -| 特性 | iPet | iAs v2 | -|------|------|--------| -| 长期记忆 | 自动注入每个请求 | 通过工具按需读写 | -| 滚动摘要 | 自动注入每个请求 | 不注入,工具可读 | -| 会话摘要 | 自动注入 | 不注入,overflow 时例外 | -| 摘要触发 | 定期 + 按消息数 | Token budget + 12h 空闲 | -| 历史完整保留 | ❌ 始终压缩 | ✅ checkpoint 后完整保留 | - -## Token 估算 - -```rust -pub fn estimate_tokens(text: &str) -> u32 { - let cjk = text.chars().filter(|c| c >= '\u{4e00}' && c <= '\u{9fff}').count() as u32; - let other = text.chars().count() as u32 - cjk; - cjk * 15 / 10 + other / 4 + 8 // +8 消息 overhead -} -``` - -## 实施步骤 - -| 步骤 | 内容 | -|------|------| -| 1 | ChatSession + SummaryEntry 核心类型 | -| 2 | build_context() — 构建 LLM 消息数组 | -| 3 | record_turn() — 记录对话轮次 | -| 4 | summarize_all() — 摘要生成 + checkpoint 更新 | -| 5 | token_estimate() — Token 估算 | -| 6 | 长期记忆工具(read_memories / write_memory)| -| 7 | 摘要读取工具(read_summaries)| -| 8 | 集成到 Conversation + main.rs | diff --git a/src/context/types.rs b/src/context/types.rs index b05c5c9..af6e630 100644 --- a/src/context/types.rs +++ b/src/context/types.rs @@ -177,23 +177,16 @@ impl ChatSession { } } - /// 保存摘要到数据库 + /// 保存摘要到数据库(委托给 db::models::save_summary) pub async fn save_summary_to_db(&self, text: &str, reason: &str, message_count: i32) { - if let Some(pool) = &self.db_pool { - let uid = if self.current_user_id.is_empty() { - if self.user_id.is_empty() { "default" } else { &self.user_id } - } else { - &self.current_user_id - }; - let _ = sqlx::query( - "INSERT INTO session_summaries (user_id, summary_text, reason, message_count) VALUES ($1, $2, $3, $4)", - ) - .bind(uid) - .bind(text) - .bind(reason) - .bind(message_count) - .execute(pool.as_ref()) - .await; + let Some(pool) = &self.db_pool else { return }; + let uid = if self.current_user_id.is_empty() { + if self.user_id.is_empty() { "default" } else { &self.user_id } + } else { + &self.current_user_id + }; + if let Err(e) = crate::db::models::save_summary(pool, uid, text, reason, message_count).await { + tracing::warn!("保存摘要到数据库失败: {}", e); } } } diff --git a/src/llm/mod.rs b/src/llm/mod.rs index f892753..4ee1972 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -3,5 +3,5 @@ pub mod deepseek; pub mod provider; pub mod types; -pub use conversation::{Conversation, Summarizer, ToolExecutor, DEFAULT_SYSTEM_PROMPT}; -pub use types::{ConversationConfig, Message, Role, StreamChunk, Usage}; +pub use conversation::{Conversation, ToolExecutor, DEFAULT_SYSTEM_PROMPT}; +pub use types::{ConversationConfig, Usage}; diff --git a/src/llm/provider.rs b/src/llm/provider.rs index 5c1cd87..74cecca 100644 --- a/src/llm/provider.rs +++ b/src/llm/provider.rs @@ -34,6 +34,7 @@ pub fn create_provider(_config: &ConversationConfig) -> Result Option { #[serde(default)] id: Option, #[serde(rename = "type", default)] + #[allow(dead_code)] call_type: Option, #[serde(default)] function: Option, diff --git a/src/llm/types.rs b/src/llm/types.rs index 0c473a6..43009fa 100644 --- a/src/llm/types.rs +++ b/src/llm/types.rs @@ -67,6 +67,7 @@ pub struct ToolFunctionCall { // ─── 流式响应块 ─── #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum StreamChunk { /// 文本片段 delta Text(String), @@ -75,6 +76,7 @@ pub enum StreamChunk { /// 完成(携带完整文本,以及可选的工具调用) Done { text: String, + #[allow(dead_code)] reasoning: Option, tool_calls: Option>, usage: Option, diff --git a/src/tools/DESIGN.md b/src/tools/DESIGN.md deleted file mode 100644 index 473b79e..0000000 --- a/src/tools/DESIGN.md +++ /dev/null @@ -1,328 +0,0 @@ -# iAs 工具系统设计 - -## 核心理念 - -**一切皆 Skill。【todo:向内置工具转变】** - -没有"内置工具"和"外部技能"的区别——所有工具都是 SKILL.md,区别仅在来源: -- **系统 skills** — `resources/skills/` 目录,随 iAs 分发,只读 -- **用户 skills** — `skills/` 项目目录,用户自己添加 - -加载时自动合并,同名时用户 skills 覆盖系统 skills。 - ---- - -## 架构 - -``` -┌──────────────────────────────────────────────────────────┐ -│ LLM │ -│ generate_reply() ←→ tool_choice → tool_execute(result) │ -└─────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────┐ -│ SkillRegistry │ -│ │ -│ ┌──────────────────┐ ┌────────────────────────────┐ │ -│ │ 系统 Skills │ │ 用户 Skills │ │ -│ │ resources/skills/ │ │ skills/ │ │ -│ │ │ │ │ │ -│ │ • datetime │ │ • query_qweather/ │ │ -│ │ • weather │ │ • search_tavily/ │ │ -│ │ • search_web │ │ • start_pc/ │ │ -│ │ • email │ │ • ... │ │ -│ │ • shell_exec │ │ │ │ -│ │ • schedule │ │ │ │ -│ └────────┬──────────┘ └────────────┬──────────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ Approval Gate │ │ -│ │ risk == High → 确认码审批 / Low → 自动执行 │ │ -│ │ 审批过程对 LLM 完全透明 │ │ -│ └──────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ -``` - ---- - -## 核心类型 - -```rust -/// 风险等级(仅两级) -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum RiskLevel { - Low, // 只读/安全,自动执行 - High, // 写操作/有风险,需微信确认码审批 -} - -/// 技能来源 -#[derive(Debug, Clone, PartialEq)] -pub enum SkillSource { - System, // resources/skills/ - User, // skills/ -} - -/// 技能元数据(从 SKILL.md 解析) -#[derive(Debug, Clone, Deserialize)] -pub struct SkillSpec { - pub name: String, - pub description: String, - pub risk_level: RiskLevel, - pub parameters: serde_json::Value, // JSON Schema - pub timeout_secs: u64, -} - -/// 技能(加载到内存后) -pub struct Skill { - pub spec: SkillSpec, - pub source: SkillSource, - pub dir: PathBuf, - pub execute_command: String, -} -``` - ---- - -## SKILL.md 格式 - -每个技能是一个目录,包含 `SKILL.md` 文件: - -```markdown -# 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 -#!/usr/bin/env bash -scripts/datetime.sh -\`\`\` -``` - ---- - -## 内置 Skills 一览 - -随 iAs 分发的系统 skills: - -| Skill | 风险 | 说明 | -|-------|------|------| -| `get_current_datetime` | Low | 获取当前时间(UTC+8) | -| `query_weather` | Low | 和风天气查询 | -| `search_web` | Low | Tavily 网络搜索 | -| `email` | High | 收发邮件 | -| `execute_shell` | High | 执行 shell 命令 | -| `list_scheduled_tasks` | Low | 列出定时任务 | -| `manage_scheduled_task` | High | 增删改定时任务 | - ---- - -## SkillRegistry - -```rust -pub struct SkillRegistry { - skills: HashMap>, -} - -impl SkillRegistry { - pub async fn load(system_dir: &str, user_dir: &str) -> Result; - - pub fn list_specs(&self) -> Vec<&SkillSpec>; - pub fn list_specs_by_risk(&self, level: RiskLevel) -> Vec<&SkillSpec>; - pub fn get(&self, name: &str) -> Option<&Skill>; - - /// 执行技能(含审批流程) - pub async fn execute( - &self, - name: &str, - params: serde_json::Value, - ctx: &ExecutionContext, - ) -> Result; -} - -pub struct ExecutionContext { - pub user_id: String, - pub db: Option>, - /// 发送微信消息的回调 - pub send_wechat: Option Result<(), String> + Send + Sync>>, - /// 等待用户回复的回调(返回用户消息文本) - pub wait_for_reply: Option Result, String> + Send + Sync>>, -} -``` - ---- - -## 审批流程(透明模式) - -**核心原则:审批过程对 LLM 完全不可见。** LLM 只看到最终结果。 - -``` -LLM 调用 High 风险技能 - │ - ▼ -SkillRegistry::execute() - │ - ├── 查找 Skill - ├── risk_level == High? - │ - └── YES ──────────────────────────────────────────┐ - │ │ - ▼ │ - ┌──────────────────────────────────────────┐ │ - │ 等待阶段(LLM 不感知) │ │ - │ │ │ - │ ① 生成 6 位确认码(如 482731) │ │ - │ ② SHA-256 哈希存入 pending_approvals │ │ - │ ③ 通过微信发送确认消息给用户: │ │ - │ │ │ - │ ┌─────────────────────────────┐ │ │ - │ │ ⚠️ 操作确认 │ │ │ - │ │ │ │ │ - │ │ 技能:send_email │ │ │ - │ │ 参数:收件人 xxx@xxx.com │ │ │ - │ │ │ │ │ - │ │ 确认码:482731 │ │ │ - │ │ 回复确认码以继续 │ │ │ - │ │ 回复 0 或「取消」以取消 │ │ │ - │ │ (5分钟内有效,最多3次尝试) │ │ │ - │ └─────────────────────────────┘ │ │ - │ │ │ - │ ④ 等待用户回复 │ │ - │ │ │ │ - │ ├── 回复正确确认码 ──┐ │ │ - │ │ │ │ │ - │ │ ▼ │ │ - │ │ ┌──────────────┐ │ │ - │ │ │ ⑤ 执行技能 │ │ │ - │ │ │ ⑥ LLM 看到 │ │ │ - │ │ │ 执行结果 │ │ │ - │ │ └──────────────┘ │ │ - │ │ │ │ - │ ├── 回复错误确认码 │ │ - │ │ → "确认码错误,还剩 N 次" │ │ - │ │ → 重试(最多 3 次) │ │ - │ │ → 3 次都错 │ │ - │ │ → LLM 看到: │ │ - │ │ "用户拒绝了你的调用: │ │ - │ │ send_email" │ │ - │ │ │ │ - │ ├── 回复 0 / 「取消」 │ │ - │ │ → LLM 看到: │ │ - │ │ "用户拒绝了你的调用: │ │ - │ │ send_email" │ │ - │ │ │ │ - │ └── 5 分钟超时 │ │ - │ → LLM 看到: │ │ - │ "用户没有确认操作: │ │ - │ send_email" │ │ - └──────────────────────────────────────────┘ -``` - -### LLM 视角(两种可能) - -``` -✅ 通过: - LLM: 调用 send_email({to: "xxx", subject: "..."}) - 系统: { success: true, message: "邮件已发送" } - -❌ 拒绝: - LLM: 调用 send_email({to: "xxx", subject: "..."}) - 系统: { success: false, error: "用户拒绝了你的调用:send_email" } -``` - -LLM 不知道确认码的存在,不知道审批过程。它只知道自己调用了工具,然后得到了结果或拒绝。 - ---- - -## 数据库表 - -```sql --- 待审批的工具调用 -CREATE TABLE pending_approvals ( - id UUID PRIMARY KEY, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - expires_at TIMESTAMPTZ NOT NULL, -- 5分钟后过期 - user_id TEXT NOT NULL, - skill_name TEXT NOT NULL, - params JSONB NOT NULL, - code_hash TEXT NOT NULL, -- 确认码 SHA-256(不存明文) - attempts_left INTEGER NOT NULL DEFAULT 3, -- 剩余尝试次数 - status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | rejected | expired - result JSONB, - consumed_at TIMESTAMPTZ -); -``` - ---- - -## Skills 目录结构 - -``` -resources/skills/ # 系统内置 Skills(随 iAs 分发) -├── get_current_datetime/ -│ ├── SKILL.md -│ └── scripts/datetime.sh -├── query_weather/ -│ ├── SKILL.md -│ └── scripts/weather.sh -├── search_web/ -│ ├── SKILL.md -│ └── scripts/search.sh -├── email/ -│ ├── SKILL.md -│ └── scripts/email.sh -├── execute_shell/ -│ ├── SKILL.md -│ └── scripts/shell.sh -├── list_scheduled_tasks/ -│ ├── SKILL.md -│ └── scripts/list.sh -└── manage_scheduled_task/ - ├── SKILL.md - └── scripts/manage.sh - -skills/ # 用户 Skills(用户自行添加) -├── query_qweather/ -│ ├── SKILL.md -│ └── scripts/query.sh -├── search_tavily/ -│ ├── SKILL.md -│ └── scripts/search.sh -└── start_pc/ - ├── SKILL.md - └── scripts/wake.sh -``` - ---- - -## 实施步骤 - -| 步骤 | 内容 | 预估 | -|------|------|------| -| Step 1 | SkillSpec/Skill/SkillRegistry 核心类型 + 注册表 | 半天 | -| Step 2 | SKILL.md 解析器 + 目录扫描(system + user 合并) | 半天 | -| Step 3 | 技能执行器:子进程管理 + 参数 JSON 传递 + 结果解析 | 半天 | -| Step 4 | 系统 Skills 脚本实现(7 个 bash 脚本) | 1 天 | -| Step 5 | 审批系统:pending_approvals 表 + 确认码生成/验证 + 微信透明通知 | 1 天 | -| Step 6 | LLM function calling 集成 + 审计日志 | 1 天 | diff --git a/src/tools/builtins/web_search.rs b/src/tools/builtins/web_search.rs index e940696..e56ed70 100644 --- a/src/tools/builtins/web_search.rs +++ b/src/tools/builtins/web_search.rs @@ -76,6 +76,7 @@ struct SearchRequest { // ─── 响应 ─── #[derive(Debug, Deserialize)] +#[allow(dead_code)] struct SearchResponse { query: String, @@ -102,6 +103,7 @@ struct SearchResponse { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] struct SearchResult { title: String, url: String, diff --git a/src/tools/types.rs b/src/tools/types.rs index 954a0d1..c0bb9a8 100644 --- a/src/tools/types.rs +++ b/src/tools/types.rs @@ -25,6 +25,7 @@ pub struct SkillSpec { #[serde(default)] pub parameters: serde_json::Value, #[serde(default = "default_timeout")] + #[allow(dead_code)] pub timeout_secs: u64, } @@ -70,6 +71,7 @@ impl SkillResult { } } + #[allow(dead_code)] pub fn expired(skill_name: &str) -> Self { Self { success: false, diff --git a/src/wechat/types.rs b/src/wechat/types.rs index faf5df7..0583a82 100644 --- a/src/wechat/types.rs +++ b/src/wechat/types.rs @@ -145,9 +145,13 @@ pub struct MessageItem { impl MessageItem { pub const TYPE_TEXT: i32 = 1; + #[allow(dead_code)] pub const TYPE_IMAGE: i32 = 2; + #[allow(dead_code)] pub const TYPE_VOICE: i32 = 3; + #[allow(dead_code)] pub const TYPE_FILE: i32 = 4; + #[allow(dead_code)] pub const TYPE_VIDEO: i32 = 5; pub fn text(text: &str) -> Self { @@ -235,6 +239,7 @@ impl Default for WeixinMessage { } impl WeixinMessage { + #[allow(dead_code)] pub const TYPE_NONE: i32 = 0; pub const TYPE_USER: i32 = 1; pub const TYPE_BOT: i32 = 2; @@ -296,6 +301,7 @@ pub struct GetUpdatesResp { #[serde(default)] pub get_updates_buf: String, #[serde(skip_serializing_if = "Option::is_none")] + #[allow(dead_code)] pub longpolling_timeout_ms: Option, } @@ -320,11 +326,13 @@ pub struct LoginResult { pub token: String, pub account_id: String, pub base_url: String, + #[allow(dead_code)] pub user_id: String, } /// 收到的消息事件 #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct MessageEvent { pub from_user_id: String, pub text: String,