From cdb227f99903f97ebf785bd0996e4e37f48d2766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 22 Jun 2026 00:34:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20L2=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E5=AE=8C=E6=95=B4(ConvState?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96+=E4=BA=8B=E4=BB=B6=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2+=E5=89=8D=E7=AB=AFstatus=E8=81=94=E5=90=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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降级可回退 --- src-tauri/src/commands/ai/agentic/guard.rs | 62 +++++++++++++++-- src-tauri/src/commands/ai/agentic/mod.rs | 15 ++++- src-tauri/src/commands/ai/mod.rs | 21 ++++++ src/api/types.ts | 22 ++++++ src/components/ai/ChatInput.vue | 55 ++++++++++++++- src/components/ai/MaxRoundsCard.vue | 18 ++++- src/composables/ai/useAiEvents.ts | 78 +++++++++++++++++++++- src/i18n/en/aiChat.ts | 3 + src/i18n/zh-CN/aiChat.ts | 3 + 9 files changed, 264 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands/ai/agentic/guard.rs b/src-tauri/src/commands/ai/agentic/guard.rs index 69e1263..d28f3c2 100644 --- a/src-tauri/src/commands/ai/agentic/guard.rs +++ b/src-tauri/src/commands/ai/agentic/guard.rs @@ -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, + /// 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) -> Self { + pub(super) fn new(session: Arc>, 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()), + }); + } + } }); } } diff --git a/src-tauri/src/commands/ai/agentic/mod.rs b/src-tauri/src/commands/ai/agentic/mod.rs index c469eb9..ed7d1b4 100644 --- a/src-tauri/src/commands/ai/agentic/mod.rs +++ b/src-tauri/src/commands/ai/agentic/mod.rs @@ -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 候选列表。 diff --git a/src-tauri/src/commands/ai/mod.rs b/src-tauri/src/commands/ai/mod.rs index aed84c7..4580a77 100644 --- a/src-tauri/src/commands/ai/mod.rs +++ b/src-tauri/src/commands/ai/mod.rs @@ -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, conversation_id: Option, }, + /// 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, + }, } // ============================================================ @@ -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, /// 即时停止唤醒(会话级):阻塞在 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, diff --git a/src/api/types.ts b/src/api/types.ts index 0edaa4c..af93ff5 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -287,6 +287,21 @@ export interface ModelConfig { */ export type AiErrorType = 'auth' | 'network' | 'timeout' | 'provider_config' | 'unknown' +/** + * L2 统一状态机 — 对话生命周期状态字面量(对齐后端 ConvState serde rename_all=snake_case, + * crates/df-ai/.../agentic/conv_state.rs)。5 态: + * - idle:空闲,可接新请求 + * - generating:生成中(含审批挂起期,审批是 loop 内暂停点非独立态) + * - stopping:停中(用户已点停止,loop 收尾中,瞬态) + * - error:出错(用户可重试回 generating) + * - compressed:压缩中(Generating/Idle 期间瞬态派生,压缩完成回原态) + * + * 后端开关 CONV_STATE_ENABLED 门控:on 时 GeneratingGuard set/reset 同步迁移并发 + * AiConvStateChanged 事件;off 时纯旧 generating bool 行为(可回退)。前端 conv_state 为 + * 新增派生源,旧 streaming/generating bool 双轨过渡保留,不强制全替(防回归)。 + */ +export type ConvState = 'idle' | 'generating' | 'stopping' | 'error' | 'compressed' + /** AI 聊天事件(后端 emit 的结构,conversation_id 用于多对话流式路由) */ export type AiChatEvent = ({ type: 'AiTextDelta'; delta: string @@ -333,6 +348,13 @@ export type AiChatEvent = ({ // 前端渲染求助卡显 reason + options 按钮供用户选,与 AiApprovalRequired(工具执行前审批)/ // AiMaxRoundsReached(达 max 被动截断)语义区分——求助=AI 主动「我搞不定」。 type: 'AiHelpRequired'; reason: string; context: string; options: string[] +} | { + // L2 统一状态机(批2 1b/1c):ConvState 经 AiChatEvent 推前端,供 status union 消费。 + // conv_state 值为 idle|generating|stopping|error|compressed(snake_case,对齐后端 ConvState serde)。 + // 后端在 conv_state 变化时(入口→generating / reset·drop→idle)emit;conversation_id 标识 + // 哪个会话的状态变了(F-09 多会话并发下前端按 convId 路由)。CONV_STATE_ENABLED 门控:off 时 + // 后端不 emit,前端 conv_state 收不到 → 消费方须回退旧 streaming/generating bool 判断(双轨过渡)。 + type: 'AiConvStateChanged'; conv_state: ConvState } | { // F-260616-07: 流式调用自动重试提示(前端可在错误气泡内显示「重试 n/m」) type: 'AiStreamRetry'; attempt: number; max_attempts: number diff --git a/src/components/ai/ChatInput.vue b/src/components/ai/ChatInput.vue index 39f1c96..d496b53 100644 --- a/src/components/ai/ChatInput.vue +++ b/src/components/ai/ChatInput.vue @@ -84,8 +84,26 @@ + + +