//! B-260615-09: generating 状态 RAII guard —— 从 agentic/mod.rs 抽离(重构第一批,纯结构搬迁)。 //! //! 行为零变更:仅文件位置移动,逻辑/字段/语义完全保留。 //! 调用方(agentic/mod.rs run_agentic_loop)经 `use super::guard::GeneratingGuard;` 复用。 use std::sync::Arc; use tauri::{AppHandle, Emitter, Manager}; use tokio::sync::Mutex; use crate::commands::ai::{AiChatEvent, AiSession}; use super::conv_state::{self, ConvState}; /// generating 复位 RAII guard,取代散布的手动 `session.generating = false`。 /// /// 两路复位: /// - 正常路径:exit 点显式 `reset().await` 即时复位(emit 前调,保证"复位→emit"顺序, /// 前端收事件时后端已可接下一条)。 /// - 异常路径(panic/未走正常 return):Drop 兜底 spawn 复位,防 generating 永真卡死前端。 /// /// 注:try_continue_agent_loop 不用 guard——其 should_continue=false 路径需保持 /// generating=true(审批等待态),全函数 guard 会误复位;该函数单点 provider-Err 复位保持手动。 /// /// F-260616-09 B 批4:guard 持 `conv_id`,复位改写 `session.conv(&conv_id).generating = false` /// (per-conv 唯一真相源)。顶层 `session.generating` 字段已在批4 删除,reset/Drop 仅写 per_conv; /// IPC(ai_is_generating/ai_chat_send 等)亦改读 per_conv,无需双写桥接。 /// /// L2 统一状态机收尾(2026-06-22):本地 `state: Option` 视图层冗余字段已删除。 /// /// 背景:批1 曾在 guard 内嵌 `state` 作「写收敛视图层」(双轨过渡),批2 已把 `ConvState` 落到 /// `PerConvState.conv_state` 字段持久化(读侧真相源)。批2 同时在 `reset`/`drop` 内读 /// `conv.conv_state` 持久化字段做迁移 + emit AiConvStateChanged——故本地 `state` 成纯冗余 /// (双源,且本地 state 在 Drop 无 await 上下文无法持久化,语义本就不完整)。 /// /// 收尾删除后: /// - `new`:读 `conv.conv_state` 持久化字段,若 Idle→Generating 迁移成功则 emit 通知前端 /// (入口 `run_agentic_loop` 已独立写持久化迁移但**不 emit**,故此处补 emit 防前端漏收生成态)。 /// - `reset`/`drop`:迁移 + emit 均读 `conv.conv_state` 持久化字段(批2 已落地,本批仅删冗余本地块)。 /// - `disarm`:不动 ConvState(审批等待是 Generating 内的暂停点)。 /// /// 开关 `CONV_STATE_ENABLED`(默认 on)门控:off 时跳过 enum 迁移与 emit,纯旧 bool 行为(可回退)。 pub(super) struct GeneratingGuard { session: Arc>, /// guard 所属会话(loop 启动时快照的 conv_id,来自 run_agentic_loop 入参)。 conv_id: String, done: bool, /// L2 批2 1b:emit AiConvStateChanged 用的 AppHandle(Clone 廉价,run_agentic_loop 入参传入)。 /// CONV_STATE_ENABLED 门控:off 时仍持(无副作用),但 new/reset/drop 跳过 emit。 app_handle: AppHandle, } impl GeneratingGuard { pub(super) fn new(session: Arc>, conv_id: String, app_handle: AppHandle) -> Self { // L2 收尾:入口 `run_agentic_loop` 已独立完成 Idle→Generating 的持久化迁移(写 conv.conv_state), // 但**不 emit** AiConvStateChanged。此处补 emit 通知前端进入生成态——读 conv.conv_state // 持久化字段(真相源)做迁移校验,迁移成功则 emit,非法记 warn 不阻断。 // // 注:new 是同步函数(无 await),无法持 session 锁读 conv.conv_state;此处经 // tauri::async_runtime::spawn 异步补 emit(非阻塞 guard 创建,入口持久化已发生故 emit 有据)。 // spawn 失败(运行时不可用,理论场景)仅漏一次 emit,前端 watchdog 兜底(对齐失败不阻断核心生成)。 if conv_state::CONV_STATE_ENABLED { let session = session.clone(); let app_handle = app_handle.clone(); let cid = conv_id.clone(); tauri::async_runtime::spawn(async move { let new_state = { let mut session = session.lock().await; let conv = session.conv(&cid); match conv.conv_state.transition_to(ConvState::Generating) { Ok(ns) => { conv.conv_state = ns; ns } Err(e) => { tracing::warn!( conv_id = %cid, error = %e, "[ai] guard.new ConvState→Generating 非法(持久化层,不阻断核心生成)" ); return; } } }; // 持久化层迁移成功后 emit 前端(同步,失败忽略)。锁已随作用域 drop,可安全 emit。 // L3 emit 双写:提变量避免构造两次,publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。 let ev = AiChatEvent::AiConvStateChanged { conv_state: new_state, conversation_id: Some(cid), }; let _ = app_handle.emit("ai-chat-event", ev.clone()); let _ = app_handle.state::().ai_event_bus.publish_event(ev); }); } Self { session, conv_id, done: false, app_handle } } /// 显式复位 generating=false。emit 前调用保证顺序。幂等。 /// /// 仅写 per_conv.conv_id.generating(唯一真相源)。 /// L2:同步迁移 `conv.conv_state → Idle`(持久化层,CONV_STATE_ENABLED 门控)。 pub(super) async fn reset(&mut self) { if !self.done { let mut session = self.session.lock().await; let conv = session.conv(&self.conv_id); conv.generating = false; // L2 批2:ConvState 持久化(Generating→Idle 写收敛,CONV_STATE_ENABLED 门控)。 if conv_state::CONV_STATE_ENABLED { match conv.conv_state.transition_to(ConvState::Idle) { Ok(ns) => { conv.conv_state = ns; // L2 批2 1b:持久化层 Idle 收敛成功后 emit 前端(同步,失败忽略)。 // L3 emit 双写:提变量避免构造两次,publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。 let ev = AiChatEvent::AiConvStateChanged { conv_state: ns, conversation_id: Some(self.conv_id.clone()), }; let _ = self.app_handle.emit("ai-chat-event", ev.clone()); let _ = self.app_handle.state::().ai_event_bus.publish_event(ev); } Err(e) => tracing::warn!( conv_id = %self.conv_id, error = %e, "[ai] guard.reset ConvState→Idle 非法(持久化层,不阻断核心复位)" ), } } self.done = true; } } /// 解除 Drop 兜底复位但不复位 generating。审批等待 return 路径调用: /// 保持 generating=true 留 try_continue 续生成,同时 Drop 因 done=true 跳过复位 spawn。 /// (B-260615-26: 修复审批执行后对话不续生成回归) /// /// L2:disarm 不迁移 ConvState(审批等待是 Generating 内的暂停点,ConvState 仍 Generating, /// 与「generating 保持 true」语义一致)。续跑时新 guard.new 重新迁移(Generating 自环幂等)。 pub(super) fn disarm(&mut self) { self.done = true; } } impl Drop for GeneratingGuard { fn drop(&mut self) { // 未 done 即异常退出(panic/未走正常 return):核心 generating 经 spawn 复位, // ConvState 持久化迁移 + emit 也在同一 spawn 内异步完成(无 await 上下文,对齐核心复位路径)。 // 开关 off / done=true(正常 reset/disarm 已走) → 跳过。 if !self.done { let session = self.session.clone(); let conv_id = self.conv_id.clone(); // L2 批2 1b:Drop 无 await 上下文,持久化迁移 + emit 在 spawn 内异步完成(对齐核心复位 spawn 路径)。 // app_handle Clone 廉价,随 session/conv_id 一起 move 进 async 块。 let app_handle = self.app_handle.clone(); tauri::async_runtime::spawn(async move { let mut s = session.lock().await; let conv = s.conv(&conv_id); conv.generating = false; // L2 批2:ConvState 持久化(异常退出兜底→Idle,CONV_STATE_ENABLED 门控)。 if conv_state::CONV_STATE_ENABLED { if let Ok(ns) = conv.conv_state.transition_to(ConvState::Idle) { conv.conv_state = ns; // 1b:持久化 Idle 收敛后 emit 前端(异常退出兜底也需通知前端退出生成态)。 // L3 emit 双写:提变量避免构造两次,publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。 let ev = AiChatEvent::AiConvStateChanged { conv_state: ns, conversation_id: Some(conv_id.clone()), }; let _ = app_handle.emit("ai-chat-event", ev.clone()); let _ = app_handle.state::().ai_event_bus.publish_event(ev); } } }); } } } // ============================================================ // 单元测试(guard ConvState 迁移语义,纯逻辑路径验证) // ============================================================ #[cfg(test)] mod tests { use super::*; /// guard.new 路径:持久化层 Idle→Generating 迁移(读 conv.conv_state)。 /// /// 注:不构造真实 AiSession(lock/Mutex 建会话状态成本高),仅校验迁移语义—— // guard 内部 transition_to 是纯函数,直接断言逻辑路径。 #[test] fn test_guard_new_migrates_to_generating() { // CONV_STATE_ENABLED on → Idle → Generating(读侧持久化字段迁移路径)。 assert!(conv_state::CONV_STATE_ENABLED, "开关默认 on,测试假设"); let mut s = ConvState::Idle; s = s.transition_to(ConvState::Generating).unwrap(); assert_eq!(s, ConvState::Generating); } /// reset 迁移 Generating→Idle(正常收敛),验证迁移合法。 #[test] fn test_guard_reset_migrates_to_idle() { let mut s = ConvState::Generating; s = s.transition_to(ConvState::Idle).unwrap(); assert_eq!(s, ConvState::Idle); } /// 异常 Drop 路径同样 Generating→Idle(兜底),验证迁移合法。 #[test] fn test_guard_drop_path_migrates_to_idle() { // guard.drop 异常路径(!done)迁移 Generating→Idle(读持久化字段)。 let mut s = ConvState::Generating; s = s.transition_to(ConvState::Idle).unwrap(); assert_eq!(s, ConvState::Idle); } /// disarm 不迁移状态(审批等待保持 Generating)。 /// 验证:disarm 后 enum 仍 Generating(下次 new 时 Generating 自环幂等通过)。 #[test] fn test_guard_disarm_keeps_generating_then_self_loop_on_new() { let s = ConvState::Generating; // disarm 不动 enum;续跑新 guard.new 时 Generating→Generating 自环通过。 assert_eq!(s.transition_to(ConvState::Generating), Ok(ConvState::Generating)); } }