修复: 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
+71 -37
View File
@@ -6,6 +6,20 @@ mod state;
use tauri::{Emitter, Listener, Manager};
use state::AppState;
/// 用户本地时区日志时间戳格式化器。
/// tracing_subscriber 默认输出 UTC,用户在中国 UTC+8,日志与本地时间差 8 小时不便调试。
/// 使用 chrono::Local 输出含时区偏移的本地时间(如 "2026-07-17T15:30:00.123456+08:00")。
///
/// 注意:FormatTime::format_time 参数类型为 tracing_subscriber::fmt::format::Writer,
/// 非 std::fmt::Write。Writer 实现了 fmt::Write,可直接 write!。
struct LocalTimer;
impl tracing_subscriber::fmt::time::FormatTime for LocalTimer {
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
write!(w, "{}", chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.6f%:z"))
}
}
// Phase3 跨端隧道:TunnelClient trait(connect/send_raw_event/disconnect 方法)需在作用域内
use df_tunnel::TunnelClient;
@@ -20,6 +34,7 @@ pub fn run() {
.expect("创建日志文件失败");
let (non_blocking, _guard) = tracing_appender::non_blocking(log_file);
tracing_subscriber::fmt()
.with_timer(LocalTimer)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
@@ -88,27 +103,36 @@ pub fn run() {
// F-260616-09 B 批8(设计 §3 batch8 + §5.2):遍历 per_conv(HashMap)清多 conv
// 残留 generating(HMR/dev 热载场景多 conv 并发跑 loop 致多 conv 卡 generating)。
// 批4:per_conv 唯一真相源,删顶层 session.generating 双写复位(顶层字段已退役)。
// 读点(双轨收口批2):改读 conv_state.is_active()(Generating+Compressed),比原
// generating 更全面——HMR 热载下 Compressed 残留态也一并清理(避免压缩中 conv 卡死)。
let dirty_convs: Vec<String> = session
.per_conv
.iter()
.filter(|(_, c)| c.conv_state.is_active())
.map(|(id, _)| id.clone())
.collect();
// 读点(B-Phase2):活跃 conv 列表改读无锁 conv_states(零锁竞争);不再 iter per_conv
// 判 conv_state.is_active()。
// BUG-2026-07-08: L0 握手清理 dirty conv 前,必须先 stop 旧 loop。
// 原逻辑直接把 ConvState 改 Idle,但后台 run_agentic_loop 仍在跑(等 LLM 响应)。
// 改 Idle 后用户重发 → can_accept_request 放行 → 新旧两个 loop 同操作一个 conv →
// 消息覆盖/guard 冲突/generating 紊乱。
// 修复:先设 stop_flag + notify_one()(旧 loop 的 stream_llm select! 即时唤醒)
// + 再改 ConvState。notify_one 让阻塞在 stream.next() 的 loop 即时检查 stop_flag 退出。
let app_state_ref = app_h.state::<AppState>();
let dirty_convs: Vec<String> = app_state_ref.conv_states.active_convs();
let was_generating = !dirty_convs.is_empty();
// 复位每个残留生成态的 conv(批8 多 conv 全覆盖)。
// 批3 双轨收口:generating bool 已退役,复位改 ConvState 迁移(Generating/Compressed→Idle)。
// 先对每个 dirty conv 设 stop_flag + notify 让旧 loop 退出(防双 loop 并发)。
for cid in &dirty_convs {
if let Some(conv) = session.per_conv.get_mut(cid) {
match conv.conv_state.transition_to(crate::commands::ai::agentic::conv_state::ConvState::Idle) {
Ok(ns) => conv.conv_state = ns,
Err(e) => tracing::warn!(
conv_id = %cid,
error = %e,
"[ai] HMR 热载 ConvState→Idle 非法(不阻断热载)"
),
}
if let Some(c) = session.per_conv.get_mut(cid) {
c.stop_flag.store(true, std::sync::atomic::Ordering::SeqCst);
c.notify.notify_one();
}
}
// 复位每个残留生成态的 conv(批8 多 conv 全覆盖)。
// B-Phase3:conv_state 写切 ConvStateStore 单源。
for cid in &dirty_convs {
if let Err(e) = app_state_ref.conv_states.transition(
cid,
crate::commands::ai::agentic::conv_state::ConvState::Idle,
) {
tracing::warn!(
conv_id = %cid,
error = %e,
"[ai] HMR 热载 conv_states→Idle 非法(不阻断热载)"
);
}
}
// 对话透明化 L1:收集每个 dirty conv 的 pinned_goals 快照(供 emit AiCompleted 携带)
@@ -123,23 +147,35 @@ pub fn run() {
// 保留 restore 重建(recovered=true,audit.rs:331),对齐 switchConversation retain 保护意图。
// 阶段3a 单真相源合并:单表按 !recovered retain(kind 不区分,path 审批恢复恒无)。
session.pending_approvals.retain(|_, a| !a.recovered);
// 在 drop(session) 前快照 active_conversation_id,供下方 idle emit 用。
let active_conv = session.active_conversation_id.clone();
drop(session);
if was_generating {
// 补偿事件:每个残留 conv 各发一个 AiCompleted(按 conversation_id 路由),
// 前端 useAiEvents.ts:133-140 按 conv_id 各归各复位 streaming/generatingConvId
for cid in &dirty_convs {
let _ = app_h.emit(
"ai-chat-event",
commands::ai::AiChatEvent::AiCompleted {
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
incomplete: None,
conversation_id: Some(cid.clone()),
pinned_goals: pinned_goals_map.get(cid).cloned().unwrap_or_default(),
},
);
}
// 根治:不论是否有 dirty conv,HMR 重连后始终向前端推 idle 事件,
// 防前端残留 streaming=true 导致发送按钮变停止、消息排队不发。
// dirty conv 走 AiCompleted 收尾路由(idle conv 无历史需收尾无需气泡)
for cid in &dirty_convs {
let _ = app_h.emit(
"ai-chat-event",
commands::ai::AiChatEvent::AiCompleted {
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
incomplete: None,
conversation_id: Some(cid.clone()),
pinned_goals: pinned_goals_map.get(cid).cloned().unwrap_or_default(),
},
);
}
// 始终推 AiConvStateChanged{idle} 通知前端同步(即使后端已是 idle)。
// 前端 handleConvStateEvent → setConvState(convId, 'idle') → convStates 删项。
if let Some(ref cid) = active_conv {
let _ = app_h.emit(
"ai-chat-event",
commands::ai::AiChatEvent::AiConvStateChanged {
conv_state: crate::commands::ai::agentic::conv_state::ConvState::Idle,
conversation_id: Some(cid.clone()),
},
);
}
tracing::info!(
"[L0-handshake] 前端重连握手完成, was_generating={}, dirty_convs={:?}",
@@ -460,8 +496,6 @@ pub fn run() {
commands::settings::delete_template,
// CI 状态
commands::ci_status::get_commit_status,
// CI 检查状态(Gitea commit statuses,失败返回空列表不阻断工作流)
commands::ci_status::get_commit_status,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");