fix(前端): 目标模式刷新/切换对话稳定性修复
- 切换/停止任务后立即 stopPolling,防止旧任务目标事件污染新对话 - 目标进度/完成/停止回调增加 conversation_id 归属校验 - 刷新恢复改为复用现有 assistant 容器+增量追加事件,去掉无条件强制重建,避免视觉闪回与同步重放卡死 - 压缩消息不再另起 work segment,继续前一段 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ea4e5dc14b
commit
1da269e2aa
@ -935,6 +935,8 @@ export const messageMethods = {
|
||||
|
||||
if (taskStore.currentTaskId) {
|
||||
await taskStore.cancelTask();
|
||||
// 立即停止轮询,防止旧任务的目标模式事件在切换对话后仍被处理
|
||||
taskStore.stopPolling('user_stop');
|
||||
}
|
||||
|
||||
// 等待后端确认
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user