From 46521124deec8447a330d82b5fa727c2a5728969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Thu, 8 Oct 2026 00:32:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E8=BF=9F=E5=88=B0=E6=94=B6=E5=B0=BE=E4=BB=B2=E8=A3=81=E8=AF=AF?= =?UTF-8?q?=E6=9D=80=E6=96=B0=E4=BB=BB=E5=8A=A1=E6=B5=81=E5=BC=8F=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 症状: 上条处理中→发新消息入队→点「立即发送」→新气泡丢失+红按钮状态丢失 - 根因: force_send 打断旧任务后旧任务 AiCompleted 迟到到达, cleanup 无差别 setStreaming(false) 误杀新任务 streaming - 仲裁: 四终态收尾前 IPC 查后端真实 generating(aiApi.isGenerating), 仍在生成=迟到伪影跳过全部下游收敛; 查态失败按正常收尾兜底 - 附: 901 看板验收附言补录(任务书⑤闭环) --- docs/todo.md | 2 +- src/composables/ai/useAiLifecycleEvents.ts | 53 ++++++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 444c691..d3135a2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -957,7 +957,7 @@ graph TD 900. [x](自动续作) 🔴 测试条目 -901. [x](自动续作) 🔴 提交工作区改动(转交实战,2026-10-07):将当前工作区改动按主题分组提交并推送 origin main。(21:4x AI-Ops 重置回队:前两轮引擎会话被 20:38-21:02 u-switch 故障窗口「网络中断不完整」腰斩,重入预算耗尽停 [→],网络已恢复)要求:①提交前 G4 前置走查(风格/明显缺陷/调试残留/注释规范,发现问题先处理再提交) ②分组建议:挂机模式 P1 总装一笔(UnattendedPanel/Settings/TopBar/TaskBoundaryCard/i18n/unattended 两 IPC/lib.rs/feishu_notify)+看板销账一笔(todo.md) ③swap-install.ps1 若有改动先审 diff 再定去留;删除 scripts/check_plaintext_key.py(一次性诊断脚本已完成使命) ④提交信息中文类型开头逐件列全,commit-msg hook 会拦截违规 ⑤完成后本条改 [x] 并附 git log --oneline -3 输出。 +901. [x](自动续作) 🔴 提交工作区改动(转交实战,2026-10-07):将当前工作区改动按主题分组提交并推送 origin main。(21:4x AI-Ops 重置回队:前两轮引擎会话被 20:38-21:02 u-switch 故障窗口「网络中断不完整」腰斩,重入预算耗尽停 [→],网络已恢复)要求:①提交前 G4 前置走查(风格/明显缺陷/调试残留/注释规范,发现问题先处理再提交) ②分组建议:挂机模式 P1 总装一笔(UnattendedPanel/Settings/TopBar/TaskBoundaryCard/i18n/unattended 两 IPC/lib.rs/feishu_notify)+看板销账一笔(todo.md) ③swap-install.ps1 若有改动先审 diff 再定去留;删除 scripts/check_plaintext_key.py(一次性诊断脚本已完成使命) ④提交信息中文类型开头逐件列全,commit-msg hook 会拦截违规 ⑤完成后本条改 [x] 并附 git log --oneline -3 输出。验收(10-08 AI-Ops 补录,引擎 22:06 完成):git log --oneline -3 → 0aac14e 文档: 看板销账: 挂机模式 P1 总装与换装校验修复入库,901 销账 / 6bfcf01 修复: 换装校验同版本重装误判不通过 / 2441101 新增: 挂机模式 P1 总装: 设置面板+顶栏徽章+边界卡精简+监控告警推送;main=origin/main 已推,工作区清零。 902. [ ] 超时失败后的一键继续入口: 流式首字节超时(120s,服务收了不响应,不可自动重试)报「响应超时,请重试」后流程中断——轮次重入预算耗尽(3/3)即彻底停,用户只见裸文案。修:超时失败落停靠语义(复用 emit_task_parked),停靠卡带「继续」(ai_continue_loop parked 分流已修好)——错误后一键续跑而非中断。体验缺口实证 2026-10-07 晚 u-switch 故障窗口。 903. [ ] u-switch 20:38-21:02 故障窗口根因深查: 11 连败+120s 首字节超时风暴(多会话多 provider 同时),21:0x 自愈。查 moyu-ops 机 u-switch 日志定因(上游抖动/服务重启/连接池耗尽),根治或加监控告警。**【21:5x AI-Ops 定因已实锤,journalctl 实测】上游连环降级雪崩非服务故障:api.deepseek.com 402 余额不足(冷却10m)+enterprise.moyu.info 401 认证失效(冷却10m)+sensenova kimi-k3 429/SSE空响应体(30s冷却循环)+bigmodel glm 429/SSE截断缺 message_stop——多通道同时打光致无首字节,冷却走完自愈。剩余工作=根治项:①switch-gw 的 deepseek 系 4 通道确认摘除(用户指认已摘但配置仍在役)②429 风暴时客户端退避协同(30s 冷却对重试风暴无效)③故障窗口告警。** diff --git a/src/composables/ai/useAiLifecycleEvents.ts b/src/composables/ai/useAiLifecycleEvents.ts index 5e7366f..3d1f8db 100644 --- a/src/composables/ai/useAiLifecycleEvents.ts +++ b/src/composables/ai/useAiLifecycleEvents.ts @@ -9,6 +9,7 @@ import { emit } from '@tauri-apps/api/event' import { useAppSettingsStore } from '@/stores/appSettings' import { state } from '@/stores/ai' import { t } from '@/i18n/i18n-helpers' +import { aiApi } from '@/api/ai' import { nextMsgId, convStates, clearApprovalTimer, flushCurrentText, clearAllToolSlowTimers, friendlyError, notifyConversationChanged, clearLastDelta, discardConvPendingApprovals, notifyQueueCleared, clearConvRound } from './aiShared' import { clearTextIdleTimer, pendingMaxRounds, pendingHelp, pendingDirAuths, pendingTaskParked, pendingTaskProgress } from './useAiPendingState' import { clearStreamWatchdog, clearResumeAttempts } from './useAiStream' @@ -45,6 +46,9 @@ function isShowTokenUsage(): boolean { /** * 公共会话终止收尾(AiCompleted/AiError/AiHelpRequired/AiTaskParked 共用):清看门狗/计时器/流式态/挂起。 * + * 返回:boolean=true 正常收尾(调用方继续执行下游逻辑);false=迟到收尾被仲裁拦截 + * (调用方必须跳过下游:drain 队列/错误气泡/停靠卡等都是旧任务事件伪影,不应执行)。 + * * 语义差异(调用方自行处理): * - AiCompleted: 调用后追加 incomplete 气泡 / token 用量 / 队列 drain * - AiError: 调用后追加错误气泡 + 清 queue @@ -52,7 +56,33 @@ function isShowTokenUsage(): boolean { * - AiTaskParked: 调用后登记 pendingTaskParked 驱动停靠卡(摘旧卡;不清 pendingHelp, * 熔断并存场景见下方 pendingHelp 分支注释) */ -function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | 'AiError' | 'AiHelpRequired' | 'AiTaskParked') { +async function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | 'AiError' | 'AiHelpRequired' | 'AiTaskParked'): Promise { + // ── 迟到收尾仲裁(BUG-2026-10-07 根修)── + // + // 症状:上条处理中→发新消息入队→点「立即发送」→新气泡丢失+红按钮状态丢失。 + // + // 链路:sendQueuedNow 先 stopChat(streaming=false)→sendMessage(force) push 新气泡+ + // setStreaming(true)→force_send 原子打断旧任务→旧任务的 AiCompleted 迟到到达→ + // 本函数无差别 setStreaming(false) 误杀新任务的 streaming→红按钮消失+新 delta 无 + // 流式渲染载体(气泡空壳)。 + // + // 仲裁:收尾前 IPC 查后端该会话真实 generating。仍在生成=这是旧任务迟到收尾(新任务 + // 已在跑),跳过全部运行态收敛与下游事件伪影;后端确实已终止=正常收尾,原语义全量执行。 + // 查态失败按正常收尾兜底(不因仲裁 IPC 挂而卡终态)。 + // + // 为什么不用前端 epoch:迟到事件本身不携带归属标记,前端纪元在「新任务收尾 vs 旧任务 + // 迟到收尾」两种场景下数值无法区分;后端 generating 是唯一可区分两场景的真值源。 + let backendGenerating = false + try { + backendGenerating = await aiApi.isGenerating(convId || null) + } catch { + backendGenerating = false // 查态失败:按正常收尾兜底,防终态被仲裁 IPC 永久挂起 + } + if (backendGenerating && state.streaming) { + console.warn(`[AI-Lifecycle] 迟到收尾仲裁命中(conv=${convId.slice(0, 8)}, reason=${reason}):后端仍在生成,跳过收敛与下游伪影`) + return false + } + // ── 以下为正常收尾路径(后端已终止或查态失败兜底)── clearStreamWatchdog(convId || undefined) // 正常收尾路径清该会话的 delta 去重记录:flushCurrentText 只清活跃会话的,这里按 convId // 显式清一遍兜住 convId 与 active 不一致或事件缺 convId 的时序,防残留记录误丢下轮同内容 delta @@ -114,6 +144,7 @@ function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | ' } localStorage.removeItem('df-ai-gen') localStorage.removeItem('df-ai-text') + return true // 正常收尾完成,调用方继续执行下游逻辑 } /** @@ -131,13 +162,16 @@ export function recordTaskParked(convId: string | null | undefined, reason: stri }) } -/** lifecycle 域:整轮收尾(AiCompleted)与异常中断(AiError)。返回 true=已处理。 */ -export function handleLifecycleEvent(event: AiChatEvent): boolean { +/** lifecycle 域:整轮收尾(AiCompleted)与异常中断(AiError)。返回 true=已处理。 + * 异步说明:cleanup 内含仲裁 IPC(ai_is_generating),故本函数 async;handleEvent 以 void 调用, + * 每个终态 case 必须先 await cleanup 拿到仲裁结果再决定下游执行。 */ +export async function handleLifecycleEvent(event: AiChatEvent): Promise { switch (event.type) { case 'AiCompleted': { console.warn('[FE-AICOMPLETED] 收到 conv=', event.conversation_id, 'streaming=', state.streaming) - cleanupTerminatedConversation(event.conversation_id || '', 'AiCompleted') - console.warn('[FE-AICOMPLETED] cleanup 后 streaming=', state.streaming) + const completed = await cleanupTerminatedConversation(event.conversation_id || '', 'AiCompleted') + console.warn('[FE-AICOMPLETED] cleanup 后 streaming=', state.streaming, '仲裁通过=', completed) + if (!completed) return true // 迟到收尾:事件伪影(incomplete/token/drain)全跳过,新任务的终态再统一收 // 流中途失败保文:后端 emit AiCompleted(incomplete=true),前端追加系统提示气泡。 // incomplete_reason 细分文案:"max_rounds"=达上限自动收尾(非网络中断,语义不同), // 其他/缺省=网络中断等异常走原提示。 @@ -202,7 +236,8 @@ export function handleLifecycleEvent(event: AiChatEvent): boolean { } case 'AiError': { - cleanupTerminatedConversation(event.conversation_id || '', 'AiError') + const errored = await cleanupTerminatedConversation(event.conversation_id || '', 'AiError') + if (!errored) return true // 迟到收尾:旧任务的错误事件伪影,跳过清队/错误气泡/回调 // 只清除出错会话的队列项,不误伤其他会话的排队消息; // UX-P2-A2:清队从静默改 toast 提示条数(用户排队的消息无声消失原无任何反馈) if (event.conversation_id) { @@ -230,7 +265,8 @@ export function handleLifecycleEvent(event: AiChatEvent): boolean { case 'AiHelpRequired': { // 求助协议:后端断路器熔断已 guard.reset(loop 终止)。按终止态收尾但不创建错误气泡, // 改为翻 pendingHelp 驱动求助卡(reason + options 按钮供用户选)。 - cleanupTerminatedConversation(event.conversation_id || '', 'AiHelpRequired') + const helped = await cleanupTerminatedConversation(event.conversation_id || '', 'AiHelpRequired') + if (!helped) return true // 迟到收尾:旧任务的求助卡伪影,跳过(新任务无求助语境) // 用 convId 兜底(后端必带,无时默认当前活跃会话,优于丢卡片) pendingHelp.value = { reason: event.reason, @@ -248,7 +284,8 @@ export function handleLifecycleEvent(event: AiChatEvent): boolean { // generating(guard 复位非死等)。对齐 AiHelpRequired 家族:终止态收尾不建错误气泡, // 改登记 pendingTaskParked 驱动停靠卡(reason + stop_kind 标签 + 继续/停止归档)。 // 队列不动:停靠等用户拍板,排队消息不自动续发不静默清(继续/停止由用户决定)。 - cleanupTerminatedConversation(event.conversation_id || '', 'AiTaskParked') + const parked = await cleanupTerminatedConversation(event.conversation_id || '', 'AiTaskParked') + if (!parked) return true // 迟到收尾:旧任务的停靠卡伪影,跳过登记 recordTaskParked(event.conversation_id, event.reason, event.stop_kind ?? undefined) // FE1:停靠刷新侧栏(更新活跃时间排序),由 listener 防抖调度统一收口。 notifyConversationChanged()