fix(frontend): 修复引导/压缩等运行期注入消息场景下刷新导致运行记录重复显示
刷新恢复运行中任务时,重建判据只看最后一条消息:当运行区间中夹着 运行期注入的 user 消息(运行中引导/通知、多智能体内联消息、压缩续接 引导语)时,引导前的静态 assistant 段未被清空,而事件重放从 0 重建 整个任务内容,导致同一段运行记录显示两遍。 修复:重建前先界定本次运行区间(越过 assistant 与运行期注入 user 消 息,直到任务发起消息),复用区间内第一条 assistant 作为重放容器并 移除区间其余部分(注入消息与后续分段由事件流按原序重建),同时绑定 currentMessageIndex 使容器复用真正生效。
This commit is contained in:
parent
eb037ccb7c
commit
f953127613
@ -401,28 +401,87 @@ export const compressionMethods = {
|
|||||||
if (needsRebuild) {
|
if (needsRebuild) {
|
||||||
debugLog('[TaskPolling] 需要从头重建 assistant 响应');
|
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 并设置标记,
|
// 关键修复:不要 pop assistant 消息,而是清空其 actions 并设置标记,
|
||||||
// 让 handleAiMessageStart 检测到 isRefreshRestore 并复用现有消息容器。
|
// 让 handleAiMessageStart 检测到 isRefreshRestore 并复用现有消息容器。
|
||||||
// 这样避免视觉闪回(消息不会突然消失)+ 新事件通过轮询增量追加,不会一次性同步处理几百个事件导致卡死。
|
// 这样避免视觉闪回(消息不会突然消失)+ 新事件通过轮询增量追加,不会一次性同步处理几百个事件导致卡死。
|
||||||
if (isAssistantMessage) {
|
if (rebuildContainer) {
|
||||||
debugLog('[TaskPolling] 复用现有 assistant 消息容器,清空 actions 等待事件重建', {
|
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),说明回复已经开始过,
|
// 如果事件流中已经有内容事件(tool/thinking/text),说明回复已经开始过,
|
||||||
// 重放事件会恢复内容,不显示等待提示。
|
// 重放事件会恢复内容,不显示等待提示。
|
||||||
// 如果还没有内容事件,说明回复还没开始,应该显示等待提示。
|
// 如果还没有内容事件,说明回复还没开始,应该显示等待提示。
|
||||||
lastMessage.awaitingFirstContent = !hasAssistantContentEvent;
|
rebuildContainer.awaitingFirstContent = !hasAssistantContentEvent;
|
||||||
lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...';
|
rebuildContainer.generatingLabel = rebuildContainer.generatingLabel || '思考中...';
|
||||||
// 清理旧的流式标记,确保新事件能正确设置
|
// 清理旧的流式标记,确保新事件能正确设置
|
||||||
if (typeof lastMessage.streaming === 'boolean') {
|
if (typeof rebuildContainer.streaming === 'boolean') {
|
||||||
lastMessage.streaming = true;
|
rebuildContainer.streaming = true;
|
||||||
}
|
}
|
||||||
|
// 绑定流式写入索引,确保重放事件追加进该容器(否则 ensureAssistantMessage
|
||||||
|
// 会因 currentMessageIndex=-1 新建 assistant,留下空壳并错位)。
|
||||||
|
this.currentMessageIndex = rebuildContainerIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.streamingMessage = true;
|
this.streamingMessage = true;
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
if (!hasAssistantContentEvent && !isAssistantMessage) {
|
if (!hasAssistantContentEvent && !rebuildContainer) {
|
||||||
this.ensureRunningAssistantPlaceholder?.(runningTask, 'restore:no-visible-content-yet');
|
this.ensureRunningAssistantPlaceholder?.(runningTask, 'restore:no-visible-content-yet');
|
||||||
}
|
}
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user