修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离

- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条)
- queue 加 conversationId 字段,按会话精准 drain
- regenerate/editMessage 只清本会话排队消息
- newConversation 保留旧会话排队消息
- AiError 只清出错会话的队列项
This commit is contained in:
lxy
2026-07-20 00:19:50 +08:00
parent 42efb31bbf
commit e9e3578d26
59 changed files with 2875 additions and 1330 deletions
+49 -12
View File
@@ -30,6 +30,7 @@ use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use anyhow::Result;
use tokio::sync::{Mutex, RwLock};
@@ -117,6 +118,12 @@ pub struct AppState {
/// 标 allow 保留作批3 接入(零调用方≠垃圾,预留保留)。
#[allow(dead_code)]
pub ai_event_bus: crate::commands::ai::event_bus::EventBus,
/// B-Phase3: ConvState 无锁并发存储(单源收敛)。
///
/// ConvState 从 session.per_conv[conv_id] 提到独立 DashMap,
/// guard/reset/ai_is_generating 全部零锁操作。PerConvState.conv_state 字段已删。
/// 治卡死连环(AiCompleted 延迟 / 工具后中断 / 第二条进队列同源根因)。
pub conv_states: Arc<crate::commands::ai::agentic::conv_state::ConvStateStore>,
// ── 知识库 ──
/// 知识库 Repo
pub knowledge: KnowledgeRepo,
@@ -185,6 +192,39 @@ fn workspace_root_path() -> PathBuf {
.unwrap_or_else(|| PathBuf::from("."))
}
/// async IPC 路径的 canonicalize 防卡包装(spawn_blocking + 2s timeout)。
///
/// `std::fs::canonicalize` 在网络挂载盘/坏符号链接/盘符掉线等场景可能长时间阻塞;
/// 在 async IPC handler 中直接同步调用会卡 tokio runtime 工作线程。本函数把同步
/// canonicalize 移入 spawn_blocking 隔离 + 2s 总超时,超时返原路径兜底(降级匹配,
/// 不阻断合法访问 — 与现有 canonicalize 失败回退词法路径的语义一致)。
///
/// - 成功 → canonicalize 后真实路径(经 strip_verbatim 去 Windows verbatim 前缀)
/// - 失败/超时 → 原字面量路径 trim 末尾分隔符(与 reload_allowed_dirs 既有兜底语义对齐)
async fn canonicalize_with_timeout(p: PathBuf) -> PathBuf {
// 提前留一份原始字面量用于兜底/日志(spawn_blocking 闭包 move 后 p 不再可借用)。
let raw_display = p.to_string_lossy().trim_end_matches(['/', '\\']).to_string();
match tokio::time::timeout(
Duration::from_secs(2),
tokio::task::spawn_blocking(move || std::fs::canonicalize(&p).ok()),
)
.await
{
Ok(JoinResult::Ok(Some(real))) => strip_verbatim(real),
// 超时 / canonicalize 失败 / JoinError:降级回原词法路径
_ => {
tracing::warn!(
"[canonicalize] 超时或失败,按字面量保存: {}",
raw_display
);
PathBuf::from(raw_display)
}
}
}
/// tokio::task::JoinResult 别名(简化 match 类型签名,避免长泛型)。
type JoinResult<T> = Result<T, tokio::task::JoinError>;
impl AppState {
/// 初始化应用状态:打开(或创建)数据库并执行迁移,构建各 Repo 与节点注册表
pub async fn init(db_path: &Path, data_dir: PathBuf) -> Result<Self> {
@@ -223,6 +263,8 @@ impl AppState {
ai_session: Arc::new(Mutex::new(AiSession::new())),
// L3 批2b:AI 事件总线入 AppState(独立于工作流 event_bus)。emit 双写留批3。
ai_event_bus: crate::commands::ai::event_bus::EventBus::new(),
// B-Phase1: ConvStateStore 无锁存储(空构造;双轨期无写点,B-Phase2 写侧切入)。
conv_states: Arc::new(crate::commands::ai::agentic::conv_state::ConvStateStore::new()),
// Phase3 Layer1:tunnel 客户端未连接实例,lib.rs setup 内 connect
tunnel: std::sync::Arc::new(df_tunnel::WsTunnelClient::new()),
knowledge: KnowledgeRepo::new(&db),
@@ -402,12 +444,9 @@ impl AppState {
continue;
}
let p = PathBuf::from(d);
// canonicalize 成功用真实路径(去 symlink/大小写归一);失败回退原字面量 trim
// (兼容"先授权目录,目录暂不存在"用例)。失败打 warn 便于排查静默授权错路径
let normalized = strip_verbatim(std::fs::canonicalize(&p).unwrap_or_else(|_| {
tracing::warn!("[allowed_dirs] canonicalize 失败(目录可能不存在),按字面量保存: {}", d);
PathBuf::from(d.trim_end_matches(['/', '\\']))
}));
// canonicalize 成功用真实路径(去 symlink/大小写归一);失败/超时回退原字面量 trim
// (兼容"先授权目录,目录暂不存在"用例)。spawn_blocking+timeout(2s) 防 fs 卡死拖垮 IPC
let normalized = canonicalize_with_timeout(p).await;
set.insert(normalized);
}
// F-260619-03 Phase B: reload 时保留当前会话临时授权(session 不落库,仅内存),
@@ -481,9 +520,8 @@ impl AppState {
return;
}
let p = PathBuf::from(d);
let normalized = strip_verbatim(std::fs::canonicalize(&p).unwrap_or_else(|_| {
PathBuf::from(d.trim_end_matches(['/', '\\']))
}));
// spawn_blocking+timeout(2s) 防 fs 卡死拖垮 IPC;失败/超时降级词法路径(与既有兜底语义一致)。
let normalized = canonicalize_with_timeout(p).await;
self.allowed_dirs.write().await.session.insert(normalized);
}
@@ -506,9 +544,8 @@ impl AppState {
return;
}
let p = PathBuf::from(d);
let normalized = strip_verbatim(std::fs::canonicalize(&p).unwrap_or_else(|_| {
PathBuf::from(d.trim_end_matches(['/', '\\']))
}));
// spawn_blocking+timeout(2s) 防 fs 卡死拖垮 IPC;失败/超时降级词法路径(与既有兜底语义一致)。
let normalized = canonicalize_with_timeout(p).await;
self.allowed_dirs.write().await.once.insert(normalized);
}