修复: B-32+33 流式收尾清队列+回滚 running toolCall(走查 P0)

- B-32:四路径(onStreamTimeout in useAiStream / AiError case in useAiEvents / stopChat+approveToolCall catch in useAiSend)补 state.queue=[] 清队列——drainQueue 仅 AiCompleted 触发,此前三路径不清致生成中入队消息静默丢失。
- B-33:onStreamTimeout 合并单遍扫描回滚 status===running toolCall→rejected(B-260615-07 残留)——approveToolCall 乐观置 running,后端 hang 时看门狗只复位 streaming 不碰 toolCalls 致卡片永久骨架屏无重审入口,现回滚恢复。
批2 wjg2t6j8d,vue-tsc 0 err
This commit is contained in:
2026-06-15 05:30:06 +08:00
parent 8720b9424d
commit f93b7584ff
3 changed files with 18 additions and 7 deletions

View File

@@ -218,6 +218,7 @@ export function handleEvent(event: AiChatEvent) {
state.streaming = false state.streaming = false
state.generatingConvId = null state.generatingConvId = null
state.currentText = '' state.currentText = ''
state.queue = [] // B-32:错误收尾清队列,防生成中入队的消息被静默丢失(drainQueue 仅 AiCompleted 触发)
localStorage.removeItem('df-ai-gen') localStorage.removeItem('df-ai-gen')
localStorage.removeItem('df-ai-text') localStorage.removeItem('df-ai-text')
state.messages.push({ state.messages.push({

View File

@@ -95,6 +95,7 @@ async function approveToolCall(toolCallId: string, approved: boolean) {
} catch (e) { } catch (e) {
// IPC 未送达:用户已点过按钮,清看门狗(避免 130s 后误触发假错误)再回滚到失败态 // IPC 未送达:用户已点过按钮,清看门狗(避免 130s 后误触发假错误)再回滚到失败态
clearStreamWatchdog() clearStreamWatchdog()
state.queue = [] // B-32:IPC 失败即对话结束,清队列防生成中入队消息静默丢失
// 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed不进此分支 // 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed不进此分支
// 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败 // 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败
console.error('[AI] 审批操作未送达后端:', e) console.error('[AI] 审批操作未送达后端:', e)
@@ -122,6 +123,7 @@ function clearQueue() {
async function stopChat() { async function stopChat() {
state.streaming = false // 本地先复位,不依赖后端 AiCompleted审批态看门狗已 clear竞态丢失则卡死 state.streaming = false // 本地先复位,不依赖后端 AiCompleted审批态看门狗已 clear竞态丢失则卡死
clearStreamWatchdog() // 清残留看门狗 clearStreamWatchdog() // 清残留看门狗
state.queue = [] // B-32:用户主动停止,清队列防生成中入队的消息被静默丢弃
await aiApi.stopChat() await aiApi.stopChat()
} }

View File

@@ -25,20 +25,28 @@ export function onStreamTimeout() {
state.streaming = false state.streaming = false
state.generatingConvId = null state.generatingConvId = null
state.currentText = '' state.currentText = ''
state.queue = [] // B-32:超时收尾同步清队列,防生成中入队的消息静默丢失
clearStreamWatchdog() clearStreamWatchdog()
// 文案区分:若已有 completed 工具调用,说明工具执行完后续回复被中断(常见为审批后 // 一次性遍历同时做两件事(单次 O(n),不再分两遍扫):
// 续生成卡住),引导用户继续;否则按纯流式中断处理。 // 1) 回滚 running 态工具调用 → rejected:approveToolCall 乐观置 running,后端 hang 时
// 数据源:state.messages[].toolCalls[].status,反向扫尾部提前退出(O(1) 均,同 findToolCall); // 看门狗触发本回调,若不回滚则卡片永久骨架屏(ToolCard:24 渲染 running 无审批按钮,无重审入口)。
// 不复用 useAiEvents.findToolCall:本模块不依赖 useAiEvents(已下沉 nextMsgId 到 aiShared 破环), // B-260615-07 残留。
// 且此处需遍历查找任一 completed 卡片而非按 id 定位,语义不同 // 2) 探测任一 completed 卡片以区分错误文案(工具已执行完后续回复中断 vs 纯流式中断)。
// 反向扫尾部提前退出(同 findToolCall 策略);不复用 useAiEvents.findToolCall:本模块
// 不依赖 useAiEvents(已下沉 nextMsgId 到 aiShared 破环),且此处需全量回滚 running 而非按 id 定位。
let hasCompletedTool = false let hasCompletedTool = false
for (let i = state.messages.length - 1; i >= 0; i--) { for (let i = state.messages.length - 1; i >= 0; i--) {
const toolCalls = state.messages[i].toolCalls const toolCalls = state.messages[i].toolCalls
if (!toolCalls) continue if (!toolCalls) continue
for (let j = 0; j < toolCalls.length; j++) { 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 const content = hasCompletedTool
? '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。' ? '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。'