From f953127613285c13b9873b2460020867d25c41d4 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Wed, 22 Jul 2026 22:20:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E4=BF=AE=E5=A4=8D=E5=BC=95?= =?UTF-8?q?=E5=AF=BC/=E5=8E=8B=E7=BC=A9=E7=AD=89=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=9C=9F=E6=B3=A8=E5=85=A5=E6=B6=88=E6=81=AF=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E4=B8=8B=E5=88=B7=E6=96=B0=E5=AF=BC=E8=87=B4=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E9=87=8D=E5=A4=8D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 刷新恢复运行中任务时,重建判据只看最后一条消息:当运行区间中夹着 运行期注入的 user 消息(运行中引导/通知、多智能体内联消息、压缩续接 引导语)时,引导前的静态 assistant 段未被清空,而事件重放从 0 重建 整个任务内容,导致同一段运行记录显示两遍。 修复:重建前先界定本次运行区间(越过 assistant 与运行期注入 user 消 息,直到任务发起消息),复用区间内第一条 assistant 作为重放容器并 移除区间其余部分(注入消息与后续分段由事件流按原序重建),同时绑定 currentMessageIndex 使容器复用真正生效。 --- .../app/methods/taskPolling/compression.ts | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/static/src/app/methods/taskPolling/compression.ts b/static/src/app/methods/taskPolling/compression.ts index 84a3c69c..f4fddef8 100644 --- a/static/src/app/methods/taskPolling/compression.ts +++ b/static/src/app/methods/taskPolling/compression.ts @@ -401,28 +401,87 @@ export const compressionMethods = { if (needsRebuild) { debugLog('[TaskPolling] 需要从头重建 assistant 响应'); + // 定位「本次运行区间」:从消息末尾向前,越过 assistant 消息与运行期注入的 + // user 消息(运行中引导/通知、多智能体内联消息、压缩续接引导语),直到发起 + // 本次任务的 user 消息(或更早的历史消息)为止。 + // + // 背景:事件重放会从 0 重建本次运行的全部 assistant 内容。若运行区间中夹着 + // 静态的运行期 user 消息(典型:运行中发送了引导对话、运行中触发对话压缩), + // 只清空末条 assistant 会让引导/压缩消息之前的静态分段残留,重放又把全部 + // 内容重建到新 assistant 中,导致同一段运行记录显示两遍。 + // + // 处理:复用运行区间内【第一条】assistant 作为重放容器(清空 actions 并绑定 + // currentMessageIndex),并移除区间内其后的所有消息——运行期注入的 user + // 消息在任务事件流中都有对应 user_message 事件,会与后续 assistant 段一起 + // 由事件重放按原顺序重建,位置与运行时一致。 + const isMidRunRuntimeUserMessage = (msg: any): boolean => { + if (!msg || msg.role !== 'user') { + return false; + } + const meta = msg.metadata || {}; + if (meta.runtime_injected === true) { + return true; + } + // 压缩续接引导语走任务递归入口持久化,没有 runtime_injected 标记, + // 但它同样是运行中插入、且事件流中有对应 user_message 事件。 + const src = String(meta.message_source || '').trim().toLowerCase(); + return src === 'compression' || src === 'compression_handoff'; + }; + let runBoundaryIdx = this.messages.length - 1; + while (runBoundaryIdx >= 0) { + const msg = this.messages[runBoundaryIdx]; + if (msg && msg.role === 'assistant') { + runBoundaryIdx -= 1; + continue; + } + if (isMidRunRuntimeUserMessage(msg)) { + runBoundaryIdx -= 1; + continue; + } + break; + } + let rebuildContainer: any = null; + let rebuildContainerIdx = -1; + for (let j = Math.max(runBoundaryIdx + 1, 0); j < this.messages.length; j += 1) { + if (this.messages[j] && this.messages[j].role === 'assistant') { + rebuildContainer = this.messages[j]; + rebuildContainerIdx = j; + break; + } + } + // 关键修复:不要 pop assistant 消息,而是清空其 actions 并设置标记, // 让 handleAiMessageStart 检测到 isRefreshRestore 并复用现有消息容器。 // 这样避免视觉闪回(消息不会突然消失)+ 新事件通过轮询增量追加,不会一次性同步处理几百个事件导致卡死。 - if (isAssistantMessage) { + if (rebuildContainer) { debugLog('[TaskPolling] 复用现有 assistant 消息容器,清空 actions 等待事件重建', { - hasAssistantContentEvent + hasAssistantContentEvent, + rebuildContainerIdx, + removedAfterContainer: this.messages.length - 1 - rebuildContainerIdx }); - lastMessage.actions = []; + // 移除重建容器之后的运行区间消息(运行期注入的 user 消息 / 后续 assistant + // 段),它们会随事件重放按原位置重建;不移除会导致同一段运行记录显示两遍。 + if (rebuildContainerIdx < this.messages.length - 1) { + this.messages.splice(rebuildContainerIdx + 1); + } + rebuildContainer.actions = []; // 如果事件流中已经有内容事件(tool/thinking/text),说明回复已经开始过, // 重放事件会恢复内容,不显示等待提示。 // 如果还没有内容事件,说明回复还没开始,应该显示等待提示。 - lastMessage.awaitingFirstContent = !hasAssistantContentEvent; - lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...'; + rebuildContainer.awaitingFirstContent = !hasAssistantContentEvent; + rebuildContainer.generatingLabel = rebuildContainer.generatingLabel || '思考中...'; // 清理旧的流式标记,确保新事件能正确设置 - if (typeof lastMessage.streaming === 'boolean') { - lastMessage.streaming = true; + if (typeof rebuildContainer.streaming === 'boolean') { + rebuildContainer.streaming = true; } + // 绑定流式写入索引,确保重放事件追加进该容器(否则 ensureAssistantMessage + // 会因 currentMessageIndex=-1 新建 assistant,留下空壳并错位)。 + this.currentMessageIndex = rebuildContainerIdx; } this.streamingMessage = true; this.taskInProgress = true; - if (!hasAssistantContentEvent && !isAssistantMessage) { + if (!hasAssistantContentEvent && !rebuildContainer) { this.ensureRunningAssistantPlaceholder?.(runningTask, 'restore:no-visible-content-yet'); } this.$forceUpdate();