修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
+49
-12
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user