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