优化: L2统一状态机完整(ConvState持久化+事件暴露+前端status联合)

1a PerConvState.conv_state持久化(入口/reset/drop写收敛,CONV_STATE_ENABLED门控)
1b AiChatEvent加AiConvStateChanged变体,guard持app_handle在迁移点emit
1c 前端convStates真相源+停止按钮三态(status union)+MaxRoundsCard读conv_state
双轨过渡:enum视图与generating bool并行,off降级可回退
This commit is contained in:
2026-06-22 00:34:43 +08:00
parent d2cada97cd
commit cdb227f999
9 changed files with 264 additions and 13 deletions

View File

@@ -5,9 +5,10 @@
use std::sync::Arc;
use tauri::{AppHandle, Emitter};
use tokio::sync::Mutex;
use crate::commands::ai::AiSession;
use crate::commands::ai::{AiChatEvent, AiSession};
use super::conv_state::{self, ConvState};
@@ -44,10 +45,13 @@ pub(super) struct GeneratingGuard {
/// L2 状态机视图(CONV_STATE_ENABLED 门控)。None = 开关 off 降级,guard 不持视图。
/// 仅 guard 生命周期内的本地视图,不持久化(批2+ 提升至 PerConvState 字段)。
state: Option<ConvState>,
/// 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) -> Self {
pub(super) fn new(session: Arc<Mutex<AiSession>>, conv_id: String, app_handle: AppHandle) -> Self {
// CONV_STATE_ENABLED 门控:off → state=None,guard 不持/不迁 enum(纯旧 bool 行为)。
// on → state 从 Idle 起,入口即经守卫迁移到 Generating(写收敛:状态机入口校验)。
let mut state = if conv_state::CONV_STATE_ENABLED {
@@ -59,7 +63,16 @@ impl GeneratingGuard {
// Idle → Generating 守卫迁移。正常路径(self-loop 幂等 / 首次进入)合法;理论非法不发生
// (新 guard 必 Idle),失败记 warn 不阻断(对齐失败兜底原则)。
match s.transition_to(ConvState::Generating) {
Ok(new_state) => *s = new_state,
Ok(new_state) => {
*s = new_state;
// L2 批2 1b:本地视图迁移成功后 emit ConvState=Generating 通知前端(同步,失败忽略)。
// 入口的持久化迁移(PerConvState.conv_state)由 run_agentic_loop 独立写,
// 此处仅反映 guard 视图层状态(双轨过渡,enum 视图与 bool 并行)。
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiConvStateChanged {
conv_state: new_state,
conversation_id: Some(conv_id.clone()),
});
}
Err(e) => {
tracing::warn!(
conv_id = %conv_id,
@@ -67,11 +80,11 @@ impl GeneratingGuard {
"[ai] guard.new ConvState Idle→Generating 非法(状态机视图层,不阻断核心生成)"
);
// 状态机层失败:enum 保留 Idle(不写非法态),核心 generating 仍由调用方 set true。
// 即 enum 视图与 bool 暂时不同步,但 enum 仅视图不影响核心复位语义。
// 即 enum 视图与 bool 暂时不同步,但 enum 仅视图不影响核心复位语义。不 emit(避免误报非生成态)。
}
}
}
Self { session, conv_id, done: false, state }
Self { session, conv_id, done: false, state, app_handle }
}
/// 显式复位 generating=false。emit 前调用保证顺序。幂等。
@@ -81,7 +94,27 @@ impl GeneratingGuard {
pub(super) async fn reset(&mut self) {
if !self.done {
let mut session = self.session.lock().await;
session.conv(&self.conv_id).generating = false;
let conv = session.conv(&self.conv_id);
conv.generating = false;
// L2 批2:ConvState 持久化(Generating→Idle 写收敛,CONV_STATE_ENABLED 门控)。
// 与本地 state 视图双写(本地保留批1,持久化为读侧/前端;1b 清理本地)。
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 前端(同步,失败忽略)。
let _ = self.app_handle.emit("ai-chat-event", AiChatEvent::AiConvStateChanged {
conv_state: ns,
conversation_id: Some(self.conv_id.clone()),
});
}
Err(e) => tracing::warn!(
conv_id = %self.conv_id,
error = %e,
"[ai] guard.reset ConvState→Idle 非法(持久化层,不阻断核心复位)"
),
}
}
self.done = true;
}
// 状态机视图同步:Generating → Idle(正常收敛退出)。仅记日志验证迁移合法(批2+ 接入
@@ -127,9 +160,24 @@ impl Drop for GeneratingGuard {
}
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;
s.conv(&conv_id).generating = false;
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 前端(异常退出兜底也需通知前端退出生成态)。
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiConvStateChanged {
conv_state: ns,
conversation_id: Some(conv_id.clone()),
});
}
}
});
}
}

View File

@@ -396,7 +396,8 @@ pub(crate) async fn run_agentic_loop(
) {
// B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底)
// F-260616-09 B 批2:guard 持 conv_id,复位改 per-conv.generating(设计 §4.3)。
let mut guard = GeneratingGuard::new(session_arc.clone(), conv_id.clone());
// L2 批2 1b:guard 持 app_handle,ConvState 迁移后 emit AiConvStateChanged 推前端。
let mut guard = GeneratingGuard::new(session_arc.clone(), conv_id.clone(), app_handle.clone());
// F-260616-09 B 批2 入口桥接:loop 启动前确保 per_conv 存在(已存在则保留累积,不存在则建)。
//
@@ -423,6 +424,18 @@ pub(crate) async fn run_agentic_loop(
let mut session = session_arc.lock().await;
let conv = session.conv(&conv_id);
conv.generating = true;
// L2 批2:ConvState 持久化(Idle→Generating 写收敛,CONV_STATE_ENABLED 门控)。
// enum 与 generating bool 双轨(bool 核心复位,enum 供读侧/前端);off 降级跳过。
if conv_state::CONV_STATE_ENABLED {
match conv.conv_state.transition_to(conv_state::ConvState::Generating) {
Ok(ns) => conv.conv_state = ns,
Err(e) => tracing::warn!(
conv_id = %conv_id,
error = %e,
"[ai] 入口 ConvState→Generating 非法(状态机持久化层,不阻断核心生成)"
),
}
}
}
// F-260614-04 / F-260614-04b: 多 Provider 负载均衡池 — 选主 + fallback 候选列表。

View File

@@ -43,6 +43,8 @@ pub mod stream_recv;
pub mod title;
pub mod tool_registry;
use agentic::conv_state::ConvState;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -197,6 +199,20 @@ pub enum AiChatEvent {
options: Vec<String>,
conversation_id: Option<String>,
},
/// L2 统一状态机(批2 1b,2026-06-22):对话生命周期状态变更通知。
///
/// 后端 `ConvState`(Idle/Generating/Stopping/Error/Compressed)经写收敛
/// (入口/guard reset/drop 迁移)后 emit 此事件,前端 status union 据此更新展示态
/// (替代散布的 generating bool 派生判断,收敛状态机读侧到事件流)。
///
/// 渐进/兜底:`CONV_STATE_ENABLED` 开关门控 —— off 时不 emit(前端按旧 bool 行为派生,
/// 向后兼容);on 时此事件与既有 generating 行为双轨,前端可按需切换消费源。
/// `conv_state` 序列化为 snake_case(idle/generating/stopping/error/compressed),
/// 供前端 union discriminator 直接使用。
AiConvStateChanged {
conv_state: ConvState,
conversation_id: Option<String>,
},
}
// ============================================================
@@ -626,6 +642,10 @@ pub struct PerConvState {
pub messages: ContextManager,
/// 是否正在生成
pub generating: bool,
/// L2 统一状态机:对话生命周期状态(单一真相源,批2 持久化供读侧/前端消费)。
/// 写收敛:经 GeneratingGuard/入口 transition_to 守卫迁移,非直接赋值。
/// 与 generating bool 双轨过渡(bool 核心复位,enum 视图),批2+ 读侧迁移后 enum 主。
pub conv_state: ConvState,
/// 停止信号(会话级):ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出
pub stop_flag: Arc<AtomicBool>,
/// 即时停止唤醒(会话级):阻塞在 stream.next() 时 notify_one() 立即唤醒跳出 select!
@@ -676,6 +696,7 @@ impl PerConvState {
Self {
messages: ContextManager::new(ContextConfig::default()),
generating: false,
conv_state: ConvState::Idle,
stop_flag: Arc::new(AtomicBool::new(false)),
notify: Arc::new(tokio::sync::Notify::new()),
iteration_used: 0,