Files
iAs/DAEMON_WORKER_PLAN.md
T

441 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** |