diff --git a/src-tauri/src/commands/ai/agentic.rs b/src-tauri/src/commands/ai/agentic.rs index b205b92..7e566cd 100644 --- a/src-tauri/src/commands/ai/agentic.rs +++ b/src-tauri/src/commands/ai/agentic.rs @@ -372,10 +372,23 @@ pub(crate) async fn run_agentic_loop( /// (前端审批态 watchdog 已 clear,不卡) /// 2) get_active_provider Err → 无可用 provider(配置丢失/全删),无法续生成,emit AiError /// (语义:配置错误,用户需设 provider;非 generating 复位可恢复) +/// +/// R-PD-6: conv_id 来源从全局 active_conversation_id 解耦到审批所属会话。 +/// 触发本函数的 ai_approve 已 remove 触发审批,但 pending_approvals 内剩余审批(若 has_pending) +/// 仍各自携带 conversation_id(审批产生时由 process_tool_calls 写入,业务真相源)。 +/// 故 has_pending=true 分支(审批等待态)直接取剩余审批的 conversation_id 做 conv_id, +/// 不读 active_conversation_id 全局单例——该字段在审批等待态(非 generating-only 期)可被 +/// ai_chat_stop/clear/switch 并发改写,属竞态耦合。has_pending=false(全部审批已处理,续生成) +/// 分支:审批已被 remove,改为以剩余 pending_approvals 任一 conversation_id 做一致性校验 +/// (此处空,校验通过即沿用全局值,该期 generating=true 且 switch 为 readonly 不并发)。 pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) { - let (is_generating, has_pending) = { + let (is_generating, has_pending, pending_conv_id) = { let session = state.ai_session.lock().await; - (session.generating, !session.pending_approvals.is_empty()) + // pending_approvals 中任一审批的 conversation_id:审批等待态(has_pending)下作为 conv_id 来源, + // 取第一个非空值(同一对话的审批 conversation_id 一致,见 process_tool_calls 写入路径)。 + let pending_conv_id = session.pending_approvals.values() + .find_map(|a| a.conversation_id.clone()); + (session.generating, !session.pending_approvals.is_empty(), pending_conv_id) }; let should_continue = is_generating && !has_pending; @@ -389,9 +402,14 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) { } else { // generating 已复位(用户 stop 或前序循环已 emit Completed):补发 AiCompleted 防前端卡住 tracing::info!("[ai] try_continue 跳过:generating 已复位(被 stop/已结束),补发 AiCompleted 清前端 streaming"); - let conv_id = { - let session = state.ai_session.lock().await; - session.active_conversation_id.clone().unwrap_or_default() + // R-PD-6: 优先用审批所属 conversation_id(审批等待态被 stop 触发,审批仍在 pending_approvals), + // 仅当无任何审批(has_pending=false 且 generating=false)时回退 active_conversation_id。 + let conv_id = match pending_conv_id { + Some(cid) => cid, + None => { + let session = state.ai_session.lock().await; + session.active_conversation_id.clone().unwrap_or_default() + } }; let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: 0, @@ -413,7 +431,11 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) { // 全函数 guard 会误复位。此点单点 provider-Err 复位,语义独立。 let mut session = state.ai_session.lock().await; session.generating = false; - let conv_id = session.active_conversation_id.clone().unwrap_or_default(); + // R-PD-6: 续生成被拒(provider 缺失)回退 conv_id 优先审批所属;无审批再读全局。 + let conv_id = session.pending_approvals.values() + .find_map(|a| a.conversation_id.clone()) + .or_else(|| session.active_conversation_id.clone()) + .unwrap_or_default(); drop(session); tracing::warn!(error = %e, "[ai] try_continue 失败:无可用 provider"); let _ = app.emit("ai-chat-event", AiChatEvent::AiError { @@ -423,6 +445,9 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) { return; } }; + // R-PD-6: 续生成路径 conv_id 解耦——has_pending=false 时审批已 remove,无审批 conversation_id 可取; + // 此期 generating=true 且 switchConversation 为 readonly 不并发改 active_conversation_id, + // 故读全局值安全(非竞态期);若 has_pending=true 已在上面 return,不会到此。 let (lang, conv_id) = { let session = state.ai_session.lock().await; let lang = session.agent_language.clone().unwrap_or_else(|| "zh-CN".to_string());