Files
DevFlow/src-tauri/src/commands/ai/agentic/guard.rs
绝尘 612de4cd7c 新增: Phase3阶段2 emit双写全接入(D2全19变体)
chat.rs 19 + audit/mod.rs 12 + guard.rs 3 + stream_recv.rs 2 + agentic/mod.rs 7(emit-only 补全,原11已落地)。每处 app.emit 前端 + publish_event tunnel 双路独立(emit 不删)。D2 全 19 变体透传 EventBus 供 tunnel subscriber 透传 miniapp。cargo EXIT=0。
2026-06-22 03:31:58 +08:00

224 lines
12 KiB
Rust

//! 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<ConvState>` 视图层冗余字段已删除。
///
/// 背景:批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<Mutex<AiSession>>,
/// 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<Mutex<AiSession>>, 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::<crate::state::AppState>().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::<crate::state::AppState>().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::<crate::state::AppState>().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));
}
}