From d809cf4b187d354c5ff082d18631c9a6a3028113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 15 Jun 2026 05:48:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20B-22=20=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5(ai=5Fis=5Fgenerat?= =?UTF-8?q?ing=20IPC+=E5=8F=91=E9=80=81=E5=89=8D=E6=9F=A5=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E7=9C=9F=E5=80=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commands.rs 加 ai_is_generating IPC(读 session.generating)+lib.rs 注册;useAiSend sendMessage 预检先 invoke('ai_is_generating')查后端真值:后端 generating=true 时入队(撞 QUEUE_LIMIT 抛错)+本地 streaming=false 则复位 streaming=true 让用户感知,IPC 失败退化为 false 不阻断。消除前端 streaming 与后端 generating 不同步致预检放行撞拦截。批6 w8qbkxxvz,cargo+vue-tsc 0 --- src-tauri/src/commands/ai/commands.rs | 12 ++++++++++++ src-tauri/src/lib.rs | 1 + src/composables/ai/useAiSend.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src-tauri/src/commands/ai/commands.rs b/src-tauri/src/commands/ai/commands.rs index dffe744..0c84954 100644 --- a/src-tauri/src/commands/ai/commands.rs +++ b/src-tauri/src/commands/ai/commands.rs @@ -25,6 +25,18 @@ use super::AiChatEvent; // 发送 / 审批 / 控制 // ============================================================ +/// 查询后端真实 generating 状态(B-260615-22:方案 A 发送前 IPC 查后端真值) +/// +/// 前端 `state.streaming` 与后端 `AiSession.generating` 各自维护: +/// 后端 loop 异常退出 / AiError 已复位 generating=false 时,前端若仅查本地 streaming +/// 可能仍为 true(或反之),预检放行后撞后端 `ai_chat_send` 的 generating 拦截。 +/// 发送前调此命令取后端真值,据之与本地 streaming 对齐,消除状态不同步。 +#[tauri::command] +pub async fn ai_is_generating(state: State<'_, AppState>) -> Result { + let session = state.ai_session.lock().await; + Ok(session.generating) +} + /// 发送消息并获取流式 AI 响应 /// /// 非阻塞:立即返回 "ok",通过 ai-chat-event 事件流式推送 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index df27fab..2b39404 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -73,6 +73,7 @@ pub fn run() { commands::ai::ai_approve, commands::ai::ai_pending_tool_calls, commands::ai::ai_chat_clear, + commands::ai::ai_is_generating, commands::ai::ai_list_providers, commands::ai::ai_save_provider, commands::ai::ai_set_provider, diff --git a/src/composables/ai/useAiSend.ts b/src/composables/ai/useAiSend.ts index 55a6b7e..ae1e248 100644 --- a/src/composables/ai/useAiSend.ts +++ b/src/composables/ai/useAiSend.ts @@ -9,6 +9,7 @@ //! - sendMessage 调 useAiStream.resetStreamWatchdog //! - drainQueue 在 sendMessage 完成后由 handleEvent(AiCompleted) 调用 — 故 drainQueue 必须为模块级 export +import { invoke } from '@tauri-apps/api/core' import { aiApi } from '../../api' import { useAppSettingsStore } from '../../stores/appSettings' import { state } from '../../stores/ai' @@ -32,6 +33,32 @@ export function drainQueue() { async function sendMessage(text: string, skill?: string) { if (!text.trim()) return + // B-260615-22(方案 A):发送前 IPC 查后端真实 generating。 + // 前端 state.streaming 与后端 AiSession.generating 各自维护:后端 loop 异常退出/ + // AiError 已复位 generating=false 时,本地 streaming 可能仍为 false(状态不同步), + // 仅查本地预检会放行,随后撞 ai_chat_send:45 的 generating 拦截。此处对齐后端真值。 + let backendGenerating = false + try { + backendGenerating = await invoke('ai_is_generating') + } catch { + // IPC 失败不阻断发送:退化到仅靠本地 streaming 预检(原行为),让 ai_chat_send 兜底。 + backendGenerating = false + } + if (backendGenerating) { + if (state.queue.length >= QUEUE_LIMIT) { + throw new Error(`待发送队列已满(最多 ${QUEUE_LIMIT} 条)`) + } + // 后端真在生成但本地 streaming 已复位(false):状态不同步——对齐本地 streaming=true + // 让用户感知(禁用输入框等),并入队等待 AiCompleted 续发;不入队则消息静默丢失。 + if (!state.streaming) { + state.streaming = true + } + state.queue.push({ text: text.trim(), skill: skill || undefined }) + return + } + + // 本地 streaming=true 但后端 generating=false(看门狗/AiError 已复位本地,或反向不同步): + // 后端可接收,本地直接进入发送(无需复位 streaming)。 // 生成中:进入待发送队列,当前对话完成后(AiCompleted)由 drainQueue 自动续发 if (state.streaming) { if (state.queue.length >= QUEUE_LIMIT) {