diff --git a/static/src/app/methods/message.ts b/static/src/app/methods/message.ts index e2aa7c3..840b192 100644 --- a/static/src/app/methods/message.ts +++ b/static/src/app/methods/message.ts @@ -935,6 +935,8 @@ export const messageMethods = { if (taskStore.currentTaskId) { await taskStore.cancelTask(); + // 立即停止轮询,防止旧任务的目标模式事件在切换对话后仍被处理 + taskStore.stopPolling('user_stop'); } // 等待后端确认 diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index 5911d0a..287cc3e 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -783,6 +783,8 @@ export const taskPollingMethods = { this.handleSystemMessage(eventData, eventIdx); break; case 'runtime_queue_sync': + // 从头重建期间跳过 runtime_queue_sync,避免与已加载的历史冲突导致 UI 闪回卡死 + if (this._rebuildingFromScratch) break; this.handleRuntimeQueueSync(eventData); break; @@ -1905,6 +1907,11 @@ export const taskPollingMethods = { // 目标模式:运行中进度 handleGoalProgress(data: any) { + // 安全检查:确保目标事件属于当前对话,防止切换对话后旧任务事件污染 + if (data?.conversation_id && this.currentConversationId && data.conversation_id !== this.currentConversationId) { + debugLog('[Goal] 忽略不匹配对话的目标进度事件', { eventCid: data.conversation_id, currentCid: this.currentConversationId }); + return; + } debugLog('[Goal] 目标进度更新', data); const status = String(data?.status || 'running').toLowerCase(); this.goalRunning = status === 'running'; @@ -1958,6 +1965,11 @@ export const taskPollingMethods = { // 目标模式:目标达成 handleGoalCompleted(data: any) { + // 安全检查:确保目标事件属于当前对话 + if (data?.conversation_id && this.currentConversationId && data.conversation_id !== this.currentConversationId) { + debugLog('[Goal] 忽略不匹配对话的目标完成事件', { eventCid: data.conversation_id, currentCid: this.currentConversationId }); + return; + } debugLog('[Goal] 目标已达成', data); this.goalRunning = false; this.goalModeArmed = false; @@ -1968,6 +1980,11 @@ export const taskPollingMethods = { // 目标模式:目标停止(空转/轮数/token/用户取消) handleGoalStopped(data: any) { + // 安全检查:确保目标事件属于当前对话 + if (data?.conversation_id && this.currentConversationId && data.conversation_id !== this.currentConversationId) { + debugLog('[Goal] 忽略不匹配对话的目标停止事件', { eventCid: data.conversation_id, currentCid: this.currentConversationId }); + return; + } debugLog('[Goal] 目标已停止', data); this.goalRunning = false; this.goalModeArmed = false; @@ -2721,54 +2738,57 @@ export const taskPollingMethods = { // 2. 最后一条是空的 assistant 消息 // 3. 事件数量远大于历史中的 actions 数量(说明历史不完整) // 4. 任务正处于文本流式阶段(刷新时易丢分段内容,强制重放全部事件) - const historyActionsCount = lastMessage?.actions?.length || 0; - const eventCount = allEvents.length; - const historyIncomplete = eventCount > historyActionsCount + 5; // 允许5个事件的误差 - const forceRebuildForStreamingText = inText || hasTextChunkEvent; + const forceRebuildForStreamingText = inText || hasTextChunkEvent || inThinking; - // 刷新恢复场景下,优先强制从头重放当前任务事件,避免“有运行中任务但界面无输出” - const forceRebuildOnRefresh = true; + // 检查 assistant 消息的 actions 是否有仍在进行中的 + const hasInProgressActions = isAssistantMessage && Array.isArray(lastMessage.actions) && lastMessage.actions.some((action: any) => { + if (!action) return false; + if (action.streaming) return true; + if (action.type === 'tool' && action.tool) { + const status = String(action.tool.status || '').toLowerCase(); + if (['preparing', 'running', 'pending', 'queued'].includes(status)) return true; + } + return false; + }); + // 仅当确实需要时才重建,避免引导消息等场景下不必要的 assistant 消息移除和事件重放 const needsRebuild = - forceRebuildOnRefresh || !isAssistantMessage || (isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) || - historyIncomplete || - forceRebuildForStreamingText; + forceRebuildForStreamingText || + hasInProgressActions; restoreDebugLog('restore:rebuild-decision', { needsRebuild, - forceRebuildOnRefresh, isAssistantMessage, - historyActionsCount, - eventCount, hasAssistantResponseEvent, hasAssistantContentEvent, - historyIncomplete, inText, hasTextChunkEvent, - forceRebuildForStreamingText + inThinking, + forceRebuildForStreamingText, + hasInProgressActions }); if (needsRebuild) { - // if (historyIncomplete) { - // console.log('[TaskPolling] 历史不完整,从头重建:', { - // historyActionsCount, - // eventCount, - // diff: eventCount - historyActionsCount - // }); - // } debugLog('[TaskPolling] 需要从头重建 assistant 响应'); - // 清空所有消息,准备从头重建 - // 保留用户消息,只删除最后的 assistant 消息 + // 关键修复:不要 pop assistant 消息,而是清空其 actions 并设置标记, + // 让 handleAiMessageStart 检测到 isRefreshRestore 并复用现有消息容器。 + // 这样避免视觉闪回(消息不会突然消失)+ 新事件通过轮询增量追加,不会一次性同步处理几百个事件导致卡死。 if (isAssistantMessage) { - debugLog('[TaskPolling] 删除不完整的 assistant 消息'); - this.messages.pop(); + debugLog('[TaskPolling] 复用现有 assistant 消息容器,清空 actions 等待事件重建'); + lastMessage.actions = []; + lastMessage.awaitingFirstContent = true; + lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...'; + // 清理旧的流式标记,确保新事件能正确设置 + if (typeof lastMessage.streaming === 'boolean') { + lastMessage.streaming = true; + } } this.streamingMessage = true; this.taskInProgress = true; - if (!hasAssistantContentEvent) { + if (!hasAssistantContentEvent && !isAssistantMessage) { this.ensureRunningAssistantPlaceholder?.(runningTask, 'restore:no-visible-content-yet'); } this.$forceUpdate(); diff --git a/static/src/utils/messageVisibility.ts b/static/src/utils/messageVisibility.ts index b1f207c..fbcf2f3 100644 --- a/static/src/utils/messageVisibility.ts +++ b/static/src/utils/messageVisibility.ts @@ -73,8 +73,12 @@ export function messageStartsWork(message: any): boolean { if (source === 'guidance' || source === 'goal_prompt' || source === 'skill') { return false; } - if (source === 'goal_review' || source === 'compression' || source === 'compression_handoff' || isLegacyGoalReview(message)) { + if (source === 'goal_review' || isLegacyGoalReview(message)) { return true; } + // 压缩消息应该继续前一个 work segment,不开启新的回复头与计时器 + if (source === 'compression' || source === 'compression_handoff') { + return false; + } return LEGACY_STARTS_WORK_SOURCES.has(source || 'user'); }