diff --git a/src-tauri/src/commands/ai/agentic.rs b/src-tauri/src/commands/ai/agentic.rs index 7e566cd..eda8dc0 100644 --- a/src-tauri/src/commands/ai/agentic.rs +++ b/src-tauri/src/commands/ai/agentic.rs @@ -140,7 +140,11 @@ pub(crate) async fn run_agentic_loop( ); let tool_defs = tools_arc.tool_definitions(); // 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁 - let stop_flag = session_arc.lock().await.stop_flag.clone(); + // notify 同取一份 Arc 引用(B-260615-14):stream_llm select! 监听 notified() 即时唤醒 + let (stop_flag, notify) = { + let session = session_arc.lock().await; + (session.stop_flag.clone(), session.notify.clone()) + }; // token 累加器:loop 生命周期内各轮叠加,退出时传 save_conversation(累加模式落库) let mut tokens = TokenAccumulator::default(); @@ -221,7 +225,7 @@ pub(crate) async fn run_agentic_loop( let _global_permit = llm_concurrency.acquire_global().await; let _per_conv_permit = llm_concurrency.acquire_per_conv().await; // 流式接收(内部处理 idle timeout / 断连检测 / 停止信号) - let (full_text, tool_calls_acc, round_usage) = match stream_llm(&*provider, request, &app_handle, &stop_flag, &conv_id).await { + let (full_text, tool_calls_acc, round_usage) = match stream_llm(&*provider, request, &app_handle, &stop_flag, ¬ify, &conv_id).await { Some(result) => result, None => { // 错误已在 stream_llm 中 emit,直接结束 diff --git a/src-tauri/src/commands/ai/commands.rs b/src-tauri/src/commands/ai/commands.rs index a2890ae..3c82c56 100644 --- a/src-tauri/src/commands/ai/commands.rs +++ b/src-tauri/src/commands/ai/commands.rs @@ -259,6 +259,10 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result< } // 流式生成中:置位让 loop 自行收尾 session.stop_flag.store(true, Ordering::SeqCst); + // B-260615-14:置位后立即 notify_one 唤醒阻塞在 stream.next() 的 select!, + // 不再等 30s 心跳 tick 或 120s idle timeout 才轮到 stop_flag 检查。 + // Notify 仅承载「即时唤醒」,停止真值仍由 stop_flag 决定(stream_llm 唤醒后再判 flag)。 + session.stop_notify().notify_one(); let conv_id = session.active_conversation_id.clone(); drop(session); diff --git a/src-tauri/src/commands/ai/mod.rs b/src-tauri/src/commands/ai/mod.rs index 2d6502f..fd91b73 100644 --- a/src-tauri/src/commands/ai/mod.rs +++ b/src-tauri/src/commands/ai/mod.rs @@ -139,6 +139,17 @@ pub struct AiSession { pub agent_language: Option, /// 停止信号:ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出 pub stop_flag: Arc, + /// 即时停止唤醒(B-260615-14):stream_llm 阻塞在 `stream.next()` 等 chunk 时, + /// `notify_one()` 立即唤醒跳出 select!,不再等 30s 心跳 tick 轮询。 + /// + /// 语义边界:Notify 仅承载「即时唤醒」这一职责——停止的真值仍由 `stop_flag` + /// (AtomicBool)决定。`ai_chat_stop` 置 `stop_flag=true` 后调 `notify_one()`; + /// 被唤醒的 select! 分支再判 `stop_flag` 决定退出或继续,避免 Notify 的通知 + /// 与「真的该停」耦合(防误唤醒继续跑)。 + /// + /// `Arc` 可 Clone(Notify 内部用 Atomic 计数,Clone 等价于 Arc 引用 +1), + /// 不影响 AiSession 其他字段语义。 + pub notify: Arc, } impl AiSession { @@ -152,9 +163,19 @@ impl AiSession { generating: false, agent_language: None, stop_flag: Arc::new(AtomicBool::new(false)), + notify: Arc::new(tokio::sync::Notify::new()), } } + /// 取即时停止唤醒器引用(B-260615-14)。 + /// + /// 供 `stream_llm`(接 `&Notify` 进 select! 监听 `notified()`)与 `ai_chat_stop` + /// (置 `stop_flag` 后调 `notify_one()`)等外部持有方获取同一 Notify 实例。 + /// 返回 `&Notify` 而非 clone,避免不必要的 Arc 引用计数调整。 + pub fn stop_notify(&self) -> &tokio::sync::Notify { + &self.notify + } + /// 推导会话读侧状态视图(只读,不写任何字段)。 /// /// 优先级:`pending_approvals` 非空 → [`SessionState::AwaitingApproval`]; diff --git a/src-tauri/src/commands/ai/stream_recv.rs b/src-tauri/src/commands/ai/stream_recv.rs index 2d874c5..0548c9a 100644 --- a/src-tauri/src/commands/ai/stream_recv.rs +++ b/src-tauri/src/commands/ai/stream_recv.rs @@ -109,6 +109,7 @@ pub(crate) async fn stream_llm( request: CompletionRequest, app_handle: &AppHandle, stop_flag: &AtomicBool, + notify: &tokio::sync::Notify, conv_id: &str, ) -> Option<(String, HashMap, df_ai::provider::TokenUsage)> { /// 流式读取空闲超时:超过此时长无任何 chunk 即判定连接已断 @@ -239,6 +240,16 @@ pub(crate) async fn stream_llm( conversation_id: Some(conv_id.to_string()), }); } + // 3) 即时停止唤醒(B-260615-14):用户点 stop → ai_chat_stop 置 stop_flag 后 notify_one()。 + // stream.next() 正阻塞等 chunk 时立即被唤醒,不再等 30s 心跳 tick 或 120s idle timeout。 + // 唤醒后判 stop_flag:真值仍由 AtomicBool 决定,Notify 仅承载「即时唤醒」职责 + // (防误唤醒继续跑——若非 stop 触发的 notify,flag 仍 false 则照常进下一轮循环)。 + _ = notify.notified() => { + if stop_flag.load(std::sync::atomic::Ordering::SeqCst) { + stopped = true; + break; + } + } } }