重构: F-09 batch8 启动恢复多conv restore + L0清残留(纯后端,F-09后端完整)

- audit.rs restore_pending_approvals: 多conv分配(按conversation_id分组惰性建/复用PerConvState,无主None不建R-9) + 2单测(分配不变量/全None无per_conv)
- lib.rs L0握手: 遍历per_conv清多conv残留generating(dirty_convs+逐个复位+各发AiCompleted补偿) + active兜底
与双写兼容(顶层双写复位),不改IPC签名/前端/顶层字段
主代兜底: cargo check --workspace 0 + test 98 + grep restore多conv/L0遍历印证
F-09后端完整: batch1 PerConvState + batch2 per-conv迁移 + batch5 限流 + batch8 恢复; batch4(启用·前后端)待用户
This commit is contained in:
2026-06-19 02:26:24 +08:00
parent 5c15b72d3d
commit 6ad4ec2848
2 changed files with 178 additions and 30 deletions

View File

@@ -40,37 +40,56 @@ pub fn run() {
let app_h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let mut session = session_arc.lock().await;
// F-260616-09 B 批2:读 active conv 的 per_conv.generating + 双写复位。
// conv_id 来源:active_conversation_id(HMR 重连场景的当前展示 conv)。
let conv_id = session.active_conversation_id.clone();
let active_for_check = conv_id.clone().unwrap_or_default();
let was_generating = if active_for_check.is_empty() {
session.generating
} else {
session.conv_read(&active_for_check).map(|c| c.generating).unwrap_or(session.generating)
};
if was_generating {
if !active_for_check.is_empty() {
session.conv(&active_for_check).generating = false;
// F-260616-09 B 批8(设计 §3 batch8 + §5.2):遍历 per_conv(HashMap)清多 conv
// 残留 generating(HMR/dev 热载场景多 conv 并发跑 loop 致多 conv 卡 generating)。
// 批2 前:仅读 active per_conv.generating 单值;批8 改遍历全部 per_conv 各归各复位。
// 顶层 session.generating(批2 双写桥接)同步复位(批4 IPC 迁移后顶层移除)。
let mut dirty_convs: Vec<String> = session
.per_conv
.iter()
.filter(|(_, c)| c.generating)
.map(|(id, _)| id.clone())
.collect();
let was_generating = !dirty_convs.is_empty() || session.generating;
// 复位每个残留 generating 的 conv(批8 多 conv 全覆盖)。
for cid in &dirty_convs {
if let Some(conv) = session.per_conv.get_mut(cid) {
conv.generating = false;
}
session.generating = false;
session.pending_approvals.clear();
drop(session);
// 补偿事件:通知前端收尾(复位 streaming / generatingConvId
let _ = app_h.emit(
"ai-chat-event",
commands::ai::AiChatEvent::AiCompleted {
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
incomplete: None,
conversation_id: conv_id,
},
);
} else {
session.pending_approvals.clear();
}
tracing::info!("[L0-handshake] 前端重连握手完成, was_generating={}", was_generating);
// 顶层双写复位(批2 桥接:IPC ai_is_generating 等仍读顶层,须同步)。
session.generating = false;
session.pending_approvals.clear();
// active_conversation_id 兜底入 dirty_convs:批2 双写期顶层 generating=true 但
// per_conv 为空(理论上不会发生,防御性保证 active conv 至少收一个 AiCompleted
// 复位前端 streaming/generatingConvId)。
if let Some(active) = session.active_conversation_id.clone() {
if !active.is_empty() && !dirty_convs.contains(&active) {
dirty_convs.push(active);
}
}
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()),
},
);
}
}
tracing::info!(
"[L0-handshake] 前端重连握手完成, was_generating={}, dirty_convs={:?}",
was_generating,
dirty_convs
);
});
});