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:
@@ -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 + DB,spawn 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** |
|
|
||||||
@@ -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 <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` | 发送输入状态 |
|
|
||||||
@@ -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<user_id, Vec<mem>>)+ 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: 工具简化为「查询 + 调用」二元接口
|
|
||||||
```
|
|
||||||
@@ -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 ChatSession(checkpoint/messages/summaries)
|
||||||
|
│ ├── builder.rs Token budget 估算 + 上下文构建 + 摘要触发
|
||||||
|
│ └── tools.rs MemoryStore(HashMap<user_id, Vec<mem>>)
|
||||||
|
├── 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 | 审批确认码加密 |
|
||||||
@@ -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
@@ -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
@@ -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};
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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 天 |
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user