fix(static): 修复运行期加载对话进度块周期性重复显示

对账循环把 pending/cancel_requested 的活任务误判为「缺轮询」(服务端活跃
集合含 pending/cancel_requested,而 hasActiveTask 只认 running),每 2.5s
触发一次「清去重 + 偏移归零」的全量事件重放;思考/文本/tool_preparing 块
创建无幂等,每轮重放整份追加,十几份后页面卡死。

A: 对账 resume 增加 alreadyTrackingMain 判定(同任务且轮询在跑则不介入,
   仅幂等对齐 taskInProgress 标志位)
B: resumeTask 收编 resetOffset——同任务且轮询在跑时拒绝归零事件偏移,
   仅冷启动恢复(不同任务或轮询已停)允许归零
D: startThinkingAction/startTextAction/handleToolPreparing 幂等创建,
   已有流式块/同 id 块时复用而非新建,作为去重集合被清后的最后防线
This commit is contained in:
JOJO 2026-07-21 14:39:42 +08:00
parent c9c3eedd82
commit 265f71e297
4 changed files with 58 additions and 2 deletions

View File

@ -73,7 +73,15 @@ export const probeMethods = {
if (status.is_truly_active) {
this._runningStateIdleStreak = 0;
// 恢复方向(立即执行,宁多勿漏):服务端活跃但本地缺轮询/缺标志位
if (status.is_main_running && status.main_task_id && !taskStore.hasActiveTask) {
// 幂等约束:本地已在轮询同一任务时不介入。服务端把 pending/cancel_requested
// 也算活跃,而 hasActiveTask 只认 'running',旧判定(!hasActiveTask会把
// 排队中/停止收尾中的活任务误判为「缺轮询」,每 2.5s 触发一次「清去重 +
// 偏移归零」的全量事件重放,导致进度块周期性整份重复追加(页面卡死根因)。
const alreadyTrackingMain =
!!status.main_task_id &&
taskStore.currentTaskId === status.main_task_id &&
taskStore.isPolling;
if (status.is_main_running && status.main_task_id && !alreadyTrackingMain) {
debugNotifyLog('[DEBUG_NOTIFY][ui] reconcile:resume-main-task', {
taskId: status.main_task_id,
conversationId
@ -87,6 +95,9 @@ export const probeMethods = {
eventHandler: (event: any) => this.handleTaskEvent(event)
});
this.taskInProgress = true;
} else if (status.is_main_running && alreadyTrackingMain) {
// 已在跟踪:仅幂等对齐标志位,绝不触碰去重集合与事件偏移
this.taskInProgress = true;
}
if (status.has_running_sub_agents) {
this.waitingForSubAgent = true;

View File

@ -43,6 +43,26 @@ export const toolMethods = {
msg.generatingLabel = '';
}
// 幂等兜底:同 id 的工具块已存在(重复/重放事件)时复用,不重复 push。
// 若块已进入 runningtool_start 已处理),不得把状态回退为 preparing。
let existingAction = null;
if (data.id && this.preparingTools.has(data.id)) {
existingAction = this.preparingTools.get(data.id);
} else if (data.id) {
existingAction = this.toolFindAction(data.id, data.preparing_id, data.execution_id);
}
if (existingAction) {
if (String(existingAction.tool?.status || '').toLowerCase() === 'preparing') {
existingAction.tool.message = data.message || existingAction.tool.message;
if (data.intent) {
existingAction.tool.intent_full = data.intent;
existingAction.tool.intent_rendered = data.intent;
}
this.$forceUpdate();
}
return;
}
const action = {
id: data.id,
type: 'tool',

View File

@ -232,6 +232,13 @@ export const useChatStore = defineStore('chat', {
startThinkingAction() {
const msg = this.ensureAssistantMessage();
clearAwaitingFirstContent(msg);
// 幂等兜底:已有「流式中」的思考块时复用而非新建。正常流程
// thinking_start/thinking_end 成对出现end 会把 streaming 置 false
// 因此命中流式块只可能来自重复/重放事件(去重集合被清后的最后防线)。
const existingThinking = this.getActiveThinkingAction(msg);
if (existingThinking && existingThinking.streaming === true) {
return { action: existingThinking, blockId: existingThinking.blockId || existingThinking.id };
}
msg.streamingThinking = '';
msg.currentStreamingType = 'thinking';
const actionId = randomId('thinking');
@ -279,6 +286,13 @@ export const useChatStore = defineStore('chat', {
return null;
}
clearAwaitingFirstContent(msg);
// 幂等兜底:末尾已有「流式中」的文本块时复用而非新建。正常流程
// text_start/text_end 成对出现end 会把 streaming 置 false
// 因此命中流式块只可能来自重复/重放事件(去重集合被清后的最后防线)。
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'text' && lastAction.streaming === true) {
return lastAction;
}
msg.streamingText = '';
msg.currentStreamingType = 'text';
(msg as any).__splitByShowHtml = false;

View File

@ -499,13 +499,24 @@ export const useTaskStore = defineStore('task', {
currentTaskId: this.currentTaskId,
resetOffset: options?.resetOffset !== false
});
const isSameTask = !!this.currentTaskId && this.currentTaskId === taskId;
const wasPolling = this.isPolling;
if (this.currentTaskId && this.currentTaskId !== taskId) {
this.stopPolling();
}
this.currentTaskId = taskId;
this.taskStatus = options.status || 'running';
if (options.resetOffset !== false) {
// resetOffset 收编:仅允许「真正的冷启动恢复」(不同任务或轮询已停)归零偏移。
// 同任务且轮询在跑时归零,下一次轮询会从 0 全量重拉事件;若去重集合恰好
// 被清(对账/恢复路径),进度块会整份重复追加。
if (options.resetOffset !== false && !(isSameTask && wasPolling)) {
this.lastEventIndex = 0;
} else if (options.resetOffset !== false) {
goalModeDebugLog('taskStore.resumeTask_resetOffset_skipped', {
taskId,
reason: 'same-task-polling-active',
lastEventIndex: this.lastEventIndex
});
}
this.pollingError = null;
this.runtimeQueueSnapshotKey = '';