fix(frontend): 修复引导/压缩等运行期注入消息场景下刷新导致运行记录重复显示

刷新恢复运行中任务时,重建判据只看最后一条消息:当运行区间中夹着
运行期注入的 user 消息(运行中引导/通知、多智能体内联消息、压缩续接
引导语)时,引导前的静态 assistant 段未被清空,而事件重放从 0 重建
整个任务内容,导致同一段运行记录显示两遍。

修复:重建前先界定本次运行区间(越过 assistant 与运行期注入 user 消
息,直到任务发起消息),复用区间内第一条 assistant 作为重放容器并
移除区间其余部分(注入消息与后续分段由事件流按原序重建),同时绑定
currentMessageIndex 使容器复用真正生效。
This commit is contained in:
JOJO 2026-07-22 22:20:25 +08:00
parent eb037ccb7c
commit f953127613

View File

@ -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();