docs: 守护/调度进程分离实现计划
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
# 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** |
|
||||
Reference in New Issue
Block a user