重构:架构重组 + 6轮缺陷修复

Phase 1: 工具模块统一
- 新建 tools/executor.rs — 统一工具执行器,消除 main/worker/daemon 三处重复
- tools/builtin.rs → tools/registry.rs
- tools/mod.rs 新增 build_tools_list() / build_env_map(),daemon/main 共用

Phase 2: 主模块重组 context/ → core/
- core/pipeline.rs — 消息处理流水线 (从 main.rs 抽取 listen_loop)
- core/session.rs — ChatSession 会话管理
- core/context.rs — 上下文构建 + token 估算
- core/summary.rs — 摘要管理 (溢出/空闲触发)
- core/memory.rs — MemoryStore 长期记忆
- main.rs 精简至 ~200 行

Phase 3: listen 模式全局会话锁 + Pipeline 收尾
- PipelineContext 持有全局 conversation_lock,所有消息串行处理防串话

Phase 4: wechat/ → channel/ 重命名

缺陷修复:
- daemon send_frame 失败后消息重回 waiting 队列
- daemon 审批通过后按 code_hash 精确执行,不重跑 LLM
- listen 模式执行器从 session.current_user_id 动态获取用户
- listen 模式 LLM 用量统计传入实际 user_id
- send_text 响应校验改用 serde_json::Value 宽松解析
- pi_subagent 工具 (JSON 事件流模式, RiskLevel::High, timeout clamp 1-300)
- listen 审批超时 300s→60s,cancel_by_code 精确保护
- approval.rs 新增 cancel_by_code 方法同步 DB
This commit is contained in:
2026-06-04 16:18:06 +08:00
parent 8dc232ebab
commit af0620aa2d
25 changed files with 1405 additions and 890 deletions
+192
View File
@@ -0,0 +1,192 @@
use crate::llm::types::Message;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// 摘要原因
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SummaryReason {
/// 上下文 token 超 budget 触发
Overflow,
/// 距上条用户消息超过 12 小时触发
Timeout,
}
/// 一条摘要记录
#[derive(Debug, Clone)]
pub struct SummaryEntry {
/// 摘要对应的消息位置(checkpoint)
pub checkpoint: usize,
/// 摘要文本
pub text: String,
/// 生成原因
pub reason: SummaryReason,
/// 生成时间
pub created_at: DateTime<Utc>,
}
/// 聊天会话状态
#[derive(Debug, Clone)]
pub struct ChatSession {
pub user_id: String,
pub db_pool: Option<Arc<sqlx::PgPool>>,
/// 当前正在处理消息的用户 IDspawn 前设置,executor 中读取)
pub current_user_id: String,
/// 摘要总结点——此位置之前的消息已被压缩
pub checkpoint: usize,
/// 全部消息(checkpoint 之后的保持完整)
pub messages: Vec<Message>,
/// 所有摘要(keyed by checkpoint
pub summaries: Vec<SummaryEntry>,
/// 上一条用户消息的时间
pub last_user_at: Option<DateTime<Utc>>,
/// Token 预算(默认 28000,留 4000 给回复)
pub token_budget: u32,
/// 12 小时空闲阈值(秒)
pub idle_timeout_secs: i64,
}
impl Default for ChatSession {
fn default() -> Self {
Self {
user_id: "default".to_string(),
db_pool: None,
current_user_id: String::new(),
checkpoint: 0,
messages: Vec::new(),
summaries: Vec::new(),
last_user_at: None,
token_budget: 28000,
idle_timeout_secs: 12 * 3600,
}
}
}
impl ChatSession {
pub fn new() -> Self {
Self::default()
}
/// 记录一条用户消息
pub fn add_user(&mut self, content: String) {
self.messages.push(Message::user(content));
self.last_user_at = Some(Utc::now());
}
/// 记录一条助手消息
pub fn add_assistant(&mut self, content: String) {
self.messages.push(Message::assistant(content));
}
/// 记录一条带 tool_calls 的助手消息
pub fn add_assistant_tool_calls(&mut self, tool_calls: Vec<crate::llm::types::ToolCall>) {
self.messages
.push(Message::assistant_with_tool_calls(tool_calls));
}
/// 记录一条工具结果
pub fn add_tool_result(&mut self, tool_call_id: &str, name: &str, result: &str) {
self.messages
.push(Message::tool_result(tool_call_id, name, result));
}
/// 是否因空闲超过阈值需要摘要
pub fn is_idle_timeout(&self) -> bool {
if let Some(last) = self.last_user_at {
let elapsed = Utc::now().signed_duration_since(last).num_seconds();
elapsed > self.idle_timeout_secs
} else {
false
}
}
/// 获取检查点之后的消息
pub fn recent_messages(&self) -> &[Message] {
if self.checkpoint < self.messages.len() {
&self.messages[self.checkpoint..]
} else {
&[]
}
}
/// 获取最新的 overflow 摘要
pub fn latest_overflow_summary(&self) -> Option<&SummaryEntry> {
self.summaries
.iter()
.rev()
.find(|s| s.reason == SummaryReason::Overflow)
}
/// 从预载的历史记录初始化会话(Worker 模式)
pub fn load_from_history(&mut self, history: &[crate::ipc::HistoryEntry]) {
for entry in history {
match entry.role.as_str() {
"user" => self.add_user(entry.content.clone()),
"assistant" => self.add_assistant(entry.content.clone()),
_ => {}
}
}
if !history.is_empty() {
self.last_user_at = Some(chrono::Utc::now());
}
}
/// 从数据库加载最近的聊天记录到会话中
pub async fn load_recent_messages(&mut self, limit: i64) {
let pool = match &self.db_pool {
Some(p) => p.clone(),
None => return,
};
let uid = self.current_user_id.clone();
let rows = if uid.is_empty() {
sqlx::query_as::<_, (String, String, String, String)>(
"SELECT direction, user_id, text, message_id FROM chat_records ORDER BY created_at DESC LIMIT $1",
)
.bind(limit)
.fetch_all(pool.as_ref())
.await
} else {
sqlx::query_as::<_, (String, String, String, String)>(
"SELECT direction, user_id, text, message_id FROM chat_records WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2",
)
.bind(&uid)
.bind(limit)
.fetch_all(pool.as_ref())
.await
};
if let Ok(rows) = rows {
let msgs: Vec<Message> = rows
.into_iter()
.rev() // 恢复时间顺序
.map(|(dir, _uid, text, _mid)| {
if dir == "inbound" {
Message::user(text)
} else {
Message::assistant(text)
}
})
.collect();
if !msgs.is_empty() {
tracing::info!("从数据库恢复 {} 条历史消息", msgs.len());
self.messages = msgs;
self.checkpoint = 0;
self.last_user_at = Some(Utc::now());
}
}
}
/// 保存摘要到数据库(委托给 db::models::save_summary
pub async fn save_summary_to_db(&self, text: &str, reason: &str, message_count: i32) {
let Some(pool) = &self.db_pool else { return };
let uid = if self.current_user_id.is_empty() {
if self.user_id.is_empty() { "default" } else { &self.user_id }
} else {
&self.current_user_id
};
if let Err(e) = crate::db::models::save_summary(pool, uid, text, reason, message_count).await {
tracing::warn!("保存摘要到数据库失败: {}", e);
}
}
}