//! 审批超时取消 //! //! 当用户长时间未处理待审批时,自动将其转为拒绝状态,防止会话永久卡死。 //! //! 触发点:[`cleanup_expired_approvals`] 由 [`try_continue_agent_loop`] 入口调用, //! 覆盖所有审批恢复路径(ai_approve / ai_authorize_dir / ai_continue_loop 等)。 //! //! 超时时长来自 AppState.approval_timeout_minutes(Settings KV 持久化,默认 15 分钟), //! 0 表示禁用超时。 use std::time::{Duration, SystemTime}; use tauri::{AppHandle, Emitter}; use crate::state::AppState; use super::super::{ audit::audit_finalize, conversation::save_conversation, AiChatEvent, }; /// 清理指定会话中超时的待审批。 /// /// 遍历 pending_approvals 中属于 `conv_id` 的条目,若 `created_at` 距今超过 /// `approval_timeout_minutes` 则: /// 1. 从 pending_approvals 移除 /// 2. 把占位 tool_result 内容替换为"审批超时已自动取消" /// 3. emit AiApprovalResult(rejected=超时) /// 4. audit_finalize 写入审计表(status=rejected) /// /// 设 0 时整体跳过(禁用超时)。无 pending 或均未超时则空操作。 /// 失败(DB 错误等)不抛错,仅日志告警,避免影响主流程。 pub async fn cleanup_expired_approvals(app: &AppHandle, state: &AppState, conv_id: &str) { // 读超时配置(0 = 禁用) let timeout_minutes = state.approval_timeout_minutes.load(std::sync::atomic::Ordering::SeqCst); if timeout_minutes == 0 { return; } let timeout_dur = Duration::from_secs(timeout_minutes * 60); let now = SystemTime::now(); // 单次锁取出所有超时的审批(锁内不做 await,避免锁跨 await) let expired: Vec = { let session = state.ai_session.lock().await; session .pending_approvals .iter() .filter(|(_, a)| { // 只处理本会话的审批(按 conversation_id 过滤) a.conversation_id.as_deref() == Some(conv_id) && a.created_at .map(|t| now.duration_since(t).map(|d| d >= timeout_dur).unwrap_or(false)) .unwrap_or(false) // created_at=None(异常)不超时 }) .map(|(_, a)| a.clone()) .collect() }; if expired.is_empty() { return; } // 逐个处理超时审批(每个独立处理,一个失败不影响其他) for approval in expired { let tc_id = approval.tool_call_id.clone(); tracing::info!( conv_id = %conv_id, tool_call_id = %tc_id, tool_name = %approval.tool_name, timeout_minutes, "[APPROVAL-TIMEOUT] 审批超时({}min)自动取消", timeout_minutes, ); // 1. 从内存 pending_approvals 移除 { let mut session = state.ai_session.lock().await; session.pending_approvals.remove(&tc_id); // 2. 替换占位 tool_result 为超时取消提示(让 LLM 看到回执不再盲猜) session .conv(conv_id) .messages .replace_tool_result_content(&tc_id, &format!("审批超时({}分钟)已自动取消,请改换其他方案", timeout_minutes)); } // 3. emit AiApprovalResult(让前端清除 pending UI) let ev = AiChatEvent::AiApprovalResult { id: tc_id.clone(), approved: false, conversation_id: Some(conv_id.to_string()), }; let _ = app.emit("ai-chat-event", ev.clone()); let _ = state.ai_event_bus.publish_event(ev); // 4. audit_finalize 记入审计表 audit_finalize( state, &tc_id, "rejected", Some(format!("审批超时({}分钟)自动取消", timeout_minutes)), ) .await; } // 落库会话状态变更(占位消息替换后需持久化,防重启后丢失) save_conversation(&state.ai_session, &state.db, conv_id, None, None, true).await; }