diff --git a/src/composables/ai/useAiEvents.ts b/src/composables/ai/useAiEvents.ts index cd32cd9..91d298c 100644 --- a/src/composables/ai/useAiEvents.ts +++ b/src/composables/ai/useAiEvents.ts @@ -218,6 +218,7 @@ export function handleEvent(event: AiChatEvent) { state.streaming = false state.generatingConvId = null state.currentText = '' + state.queue = [] // B-32:错误收尾清队列,防生成中入队的消息被静默丢失(drainQueue 仅 AiCompleted 触发) localStorage.removeItem('df-ai-gen') localStorage.removeItem('df-ai-text') state.messages.push({ diff --git a/src/composables/ai/useAiSend.ts b/src/composables/ai/useAiSend.ts index 691d9f3..6d3507f 100644 --- a/src/composables/ai/useAiSend.ts +++ b/src/composables/ai/useAiSend.ts @@ -95,6 +95,7 @@ async function approveToolCall(toolCallId: string, approved: boolean) { } catch (e) { // IPC 未送达:用户已点过按钮,清看门狗(避免 130s 后误触发假错误)再回滚到失败态 clearStreamWatchdog() + state.queue = [] // B-32:IPC 失败即对话结束,清队列防生成中入队消息静默丢失 // 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed,不进此分支 // 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败 console.error('[AI] 审批操作未送达后端:', e) @@ -122,6 +123,7 @@ function clearQueue() { async function stopChat() { state.streaming = false // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死) clearStreamWatchdog() // 清残留看门狗 + state.queue = [] // B-32:用户主动停止,清队列防生成中入队的消息被静默丢弃 await aiApi.stopChat() } diff --git a/src/composables/ai/useAiStream.ts b/src/composables/ai/useAiStream.ts index 0af4916..0b302d5 100644 --- a/src/composables/ai/useAiStream.ts +++ b/src/composables/ai/useAiStream.ts @@ -25,20 +25,28 @@ export function onStreamTimeout() { state.streaming = false state.generatingConvId = null state.currentText = '' + state.queue = [] // B-32:超时收尾同步清队列,防生成中入队的消息静默丢失 clearStreamWatchdog() - // 文案区分:若已有 completed 工具调用,说明工具执行完后续回复被中断(常见为审批后 - // 续生成卡住),引导用户继续;否则按纯流式中断处理。 - // 数据源:state.messages[].toolCalls[].status,反向扫尾部提前退出(O(1) 均,同 findToolCall); - // 不复用 useAiEvents.findToolCall:本模块不依赖 useAiEvents(已下沉 nextMsgId 到 aiShared 破环), - // 且此处需遍历查找任一 completed 卡片而非按 id 定位,语义不同 + // 一次性遍历同时做两件事(单次 O(n),不再分两遍扫): + // 1) 回滚 running 态工具调用 → rejected:approveToolCall 乐观置 running,后端 hang 时 + // 看门狗触发本回调,若不回滚则卡片永久骨架屏(ToolCard:24 渲染 running 无审批按钮,无重审入口)。 + // B-260615-07 残留。 + // 2) 探测任一 completed 卡片以区分错误文案(工具已执行完后续回复中断 vs 纯流式中断)。 + // 反向扫尾部提前退出(同 findToolCall 策略);不复用 useAiEvents.findToolCall:本模块 + // 不依赖 useAiEvents(已下沉 nextMsgId 到 aiShared 破环),且此处需全量回滚 running 而非按 id 定位。 let hasCompletedTool = false for (let i = state.messages.length - 1; i >= 0; i--) { const toolCalls = state.messages[i].toolCalls if (!toolCalls) continue for (let j = 0; j < toolCalls.length; j++) { - if (toolCalls[j].status === 'completed') { hasCompletedTool = true; break } + const tc = toolCalls[j] + if (tc.status === 'running') { + // B-33:running 态无后续事件回流,置 rejected 释放骨架屏,恢复重审入口 + tc.status = 'rejected' + } else if (tc.status === 'completed') { + hasCompletedTool = true + } } - if (hasCompletedTool) break } const content = hasCompletedTool ? '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。'