docs: 统一 README,删除冗余文档;refactor: 消除全部 15 个 warning

- 新建 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 配置
This commit is contained in:
2026-06-03 17:14:01 +08:00
parent b2fe054eaf
commit 69ba8887ff
13 changed files with 267 additions and 1301 deletions
-440
View File
@@ -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<serde_json::Value>
```
**测试**
```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 + DBspawn worker。
**新文件**`src/daemon.rs`
```rust
pub async fn run(db: Arc<Database>, 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<UserId>,
// 等待队列
pending: HashMap<UserId, VecDeque<IncomingMessage>>,
// 准备就绪待处理的用户
waiting: VecDeque<UserId>,
}
```
**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** |
-151
View File
@@ -1,151 +0,0 @@
# iPet → Rust (iAs) 迁移计划
## 项目定位
将 iPetBun/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 流式 APISSE + 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分钟过期)|
**系统 Skills7个):**
| 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 <token>`
- `AuthorizationType: ilink_bot_token`
- `Content-Type: application/json`
- `iLink-App-Id: <app_id>`
- `iLink-App-ClientVersion: <encoded_uint32>`
- `X-WECHAT-UIN: <random_uint32_b64>`
### 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/ilink/bot/get_bot_qrcode?bot_type=3` | 获取登录二维码 |
| GET | `/ilink/bot/get_qrcode_status?qrcode=<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` | 发送输入状态 |
-171
View File
@@ -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/runtimePostgreSQL 不可用时的降级方案)
├── scheduler.rs 定时任务调度器(轮询 PostgreSQL + 事务保护)
├── skills_importer.rs Skills 导入扫描器
├── context/ 上下文管理
│ ├── types.rs ChatSessioncheckpoint/messages/summaries/user_id
│ ├── builder.rs token budget 构建 + 摘要触发
│ ├── tools.rs MemoryStoreHashMap<user_id, Vec<mem>>+ read_summaries
│ └── DESIGN.md 上下文管理设计(未完全实现)
├── db/ 数据库
│ ├── mod.rs Database 连接池 + 迁移
│ └── models.rs CRUDauth/chat/usage/summary
├── llm/ 对话系统
│ ├── types.rs Message / Role / StreamChunk / Usage
│ ├── provider.rs create_provider / parse_chat_chunkSSE 解析)
│ ├── deepseek.rs DeepSeek 流式客户端(thinking + tool_calls
│ └── conversation.rs Conversationchat + chat_with_tools 工具循环)
├── tools/ 工具系统
│ ├── skill.rs SkillSpec / RiskLevel / SkillResult
│ ├── registry.rs SkillRegistry(加载/执行/审批流程)
│ ├── builtin.rs BuiltinRegistrydatetime + 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/OHigh 先审批)
└── SkillRegistry(外部 Skills:
├── query_weather → bash + openssl JWT
├── search_web → python Tavily
├── email → node.js IMAP
├── execute_shell → bashHigh
├── list_scheduled_tasks → psql
├── manage_scheduled_task → psqlHigh
└── manage_skill → bashHigh
```
---
## 审批流程
```
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 | ⚠️ 需运行时 |
| 核心工具未全用 Rustweather/search/email | ⚠️ 仍为 bash |
---
## 最近重要 commit
```
6708fe3 fix: 3 项问题 (审批DB精确匹配 + delete id提取 + 清理变量)
065229b fix: 优先审批 + 首用户隔离 + 审批DB闭环 + memo错误
079150b refactor: 删除 config.json + 内置工具抽离 + 统一接口
b710518 refactor: 工具简化为「查询 + 调用」二元接口
```
+240
View File
@@ -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 <id> --text <msg>` | 手动发送消息 |
| `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 <URL>` | 提取网页正文 |
| `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 ChatSessioncheckpoint/messages/summaries
│ ├── builder.rs Token budget 估算 + 上下文构建 + 摘要触发
│ └── tools.rs MemoryStoreHashMap<user_id, Vec<mem>>
├── db/ 数据库
│ ├── mod.rs Database 连接池 + 迁移
│ └── models.rs CRUDauth/chat/usage/summary
├── llm/ 对话系统
│ ├── types.rs Message / Role / ToolCall / StreamChunk / Usage
│ ├── provider.rs LlmProvider trait + SSE 解析
│ ├── deepseek.rs DeepSeek 流式客户端
│ └── conversation.rs Conversationchat + 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 + DBspawn 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 | 审批确认码加密 |
-193
View File
@@ -1,193 +0,0 @@
# iAs 上下文管理系统设计 v2
## 核心理念
1. **长期记忆不固定注入** — 通过工具让 LLM 按需读写
2. **摘要不入上下文** — 除非因上下文空间不足被迫压缩
3. **摘要总结点** — 记录压缩位置,之后的消息始终完整保留
4. **12 小时空闲触发** — 超过 12h 自动压缩,但不插入会话
5. **Token Budget 驱动** — 达到上限才压缩,保持消息完整度优先
---
## 状态模型
```
ChatSession
├── checkpoint: usize = 0 ← 摘要总结点(消息列表中的位置)
├── messages: Vec<Message> ← 全部消息(checkpoint 之前的是历史)
├── summaries: Vec<SummaryEntry> ← 所有摘要
│ ├── pos: usize ← 对应消息列表中的位置
│ ├── text: String ← 摘要内容
│ └── reason: "overflow"|"timeout" ← 生成原因
└── last_user_at: Option<DateTime> ← 上一条用户消息时间
```
### 消息列表视图(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<Message>,
summaries: Vec<SummaryEntry>,
last_user_at: Option<DateTime<Utc>>,
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 |
+9 -16
View File
@@ -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) { pub async fn save_summary_to_db(&self, text: &str, reason: &str, message_count: i32) {
if let Some(pool) = &self.db_pool { let Some(pool) = &self.db_pool else { return };
let uid = if self.current_user_id.is_empty() { let uid = if self.current_user_id.is_empty() {
if self.user_id.is_empty() { "default" } else { &self.user_id } if self.user_id.is_empty() { "default" } else { &self.user_id }
} else { } else {
&self.current_user_id &self.current_user_id
}; };
let _ = sqlx::query( if let Err(e) = crate::db::models::save_summary(pool, uid, text, reason, message_count).await {
"INSERT INTO session_summaries (user_id, summary_text, reason, message_count) VALUES ($1, $2, $3, $4)", tracing::warn!("保存摘要到数据库失败: {}", e);
)
.bind(uid)
.bind(text)
.bind(reason)
.bind(message_count)
.execute(pool.as_ref())
.await;
} }
} }
} }
+2 -2
View File
@@ -3,5 +3,5 @@ pub mod deepseek;
pub mod provider; pub mod provider;
pub mod types; pub mod types;
pub use conversation::{Conversation, Summarizer, ToolExecutor, DEFAULT_SYSTEM_PROMPT}; pub use conversation::{Conversation, ToolExecutor, DEFAULT_SYSTEM_PROMPT};
pub use types::{ConversationConfig, Message, Role, StreamChunk, Usage}; pub use types::{ConversationConfig, Usage};
+2
View File
@@ -34,6 +34,7 @@ pub fn create_provider(_config: &ConversationConfig) -> Result<BoxedProvider, St
// ─── 内部:SSE 解析工具 ─── // ─── 内部:SSE 解析工具 ───
/// 流式块解析结果 /// 流式块解析结果
#[allow(dead_code)]
pub(crate) enum ParsedChunk { pub(crate) enum ParsedChunk {
Text(String), Text(String),
Reasoning(String), Reasoning(String),
@@ -65,6 +66,7 @@ pub(crate) fn parse_chat_chunk(line: &str) -> Option<ParsedChunk> {
#[serde(default)] #[serde(default)]
id: Option<String>, id: Option<String>,
#[serde(rename = "type", default)] #[serde(rename = "type", default)]
#[allow(dead_code)]
call_type: Option<String>, call_type: Option<String>,
#[serde(default)] #[serde(default)]
function: Option<ToolFunctionDelta>, function: Option<ToolFunctionDelta>,
+2
View File
@@ -67,6 +67,7 @@ pub struct ToolFunctionCall {
// ─── 流式响应块 ─── // ─── 流式响应块 ───
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum StreamChunk { pub enum StreamChunk {
/// 文本片段 delta /// 文本片段 delta
Text(String), Text(String),
@@ -75,6 +76,7 @@ pub enum StreamChunk {
/// 完成(携带完整文本,以及可选的工具调用) /// 完成(携带完整文本,以及可选的工具调用)
Done { Done {
text: String, text: String,
#[allow(dead_code)]
reasoning: Option<String>, reasoning: Option<String>,
tool_calls: Option<Vec<ToolCall>>, tool_calls: Option<Vec<ToolCall>>,
usage: Option<Usage>, usage: Option<Usage>,
-328
View File
@@ -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<String, Arc<Skill>>,
}
impl SkillRegistry {
pub async fn load(system_dir: &str, user_dir: &str) -> Result<Self, LoadError>;
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<SkillResult, SkillError>;
}
pub struct ExecutionContext {
pub user_id: String,
pub db: Option<Arc<Database>>,
/// 发送微信消息的回调
pub send_wechat: Option<Box<dyn Fn(&str, &str) -> Result<(), String> + Send + Sync>>,
/// 等待用户回复的回调(返回用户消息文本)
pub wait_for_reply: Option<Box<dyn Fn(&str, Duration) -> Result<Option<String>, 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 天 |
+2
View File
@@ -76,6 +76,7 @@ struct SearchRequest {
// ─── 响应 ─── // ─── 响应 ───
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SearchResponse { struct SearchResponse {
query: String, query: String,
@@ -102,6 +103,7 @@ struct SearchResponse {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SearchResult { struct SearchResult {
title: String, title: String,
url: String, url: String,
+2
View File
@@ -25,6 +25,7 @@ pub struct SkillSpec {
#[serde(default)] #[serde(default)]
pub parameters: serde_json::Value, pub parameters: serde_json::Value,
#[serde(default = "default_timeout")] #[serde(default = "default_timeout")]
#[allow(dead_code)]
pub timeout_secs: u64, pub timeout_secs: u64,
} }
@@ -70,6 +71,7 @@ impl SkillResult {
} }
} }
#[allow(dead_code)]
pub fn expired(skill_name: &str) -> Self { pub fn expired(skill_name: &str) -> Self {
Self { Self {
success: false, success: false,
+8
View File
@@ -145,9 +145,13 @@ pub struct MessageItem {
impl MessageItem { impl MessageItem {
pub const TYPE_TEXT: i32 = 1; pub const TYPE_TEXT: i32 = 1;
#[allow(dead_code)]
pub const TYPE_IMAGE: i32 = 2; pub const TYPE_IMAGE: i32 = 2;
#[allow(dead_code)]
pub const TYPE_VOICE: i32 = 3; pub const TYPE_VOICE: i32 = 3;
#[allow(dead_code)]
pub const TYPE_FILE: i32 = 4; pub const TYPE_FILE: i32 = 4;
#[allow(dead_code)]
pub const TYPE_VIDEO: i32 = 5; pub const TYPE_VIDEO: i32 = 5;
pub fn text(text: &str) -> Self { pub fn text(text: &str) -> Self {
@@ -235,6 +239,7 @@ impl Default for WeixinMessage {
} }
impl WeixinMessage { impl WeixinMessage {
#[allow(dead_code)]
pub const TYPE_NONE: i32 = 0; pub const TYPE_NONE: i32 = 0;
pub const TYPE_USER: i32 = 1; pub const TYPE_USER: i32 = 1;
pub const TYPE_BOT: i32 = 2; pub const TYPE_BOT: i32 = 2;
@@ -296,6 +301,7 @@ pub struct GetUpdatesResp {
#[serde(default)] #[serde(default)]
pub get_updates_buf: String, pub get_updates_buf: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[allow(dead_code)]
pub longpolling_timeout_ms: Option<i64>, pub longpolling_timeout_ms: Option<i64>,
} }
@@ -320,11 +326,13 @@ pub struct LoginResult {
pub token: String, pub token: String,
pub account_id: String, pub account_id: String,
pub base_url: String, pub base_url: String,
#[allow(dead_code)]
pub user_id: String, pub user_id: String,
} }
/// 收到的消息事件 /// 收到的消息事件
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct MessageEvent { pub struct MessageEvent {
pub from_user_id: String, pub from_user_id: String,
pub text: String, pub text: String,