fix: stabilize task completion and chat auto-scroll behavior

This commit is contained in:
JOJO 2026-04-12 19:45:01 +08:00
parent 757718c492
commit 49bd994cfa
4 changed files with 201 additions and 3 deletions

View File

@ -1,5 +1,8 @@
// @ts-nocheck
import { debugLog } from './common';
const jsonDebug = (...args: any[]) => {
console.log('[JSONDEBUG]', ...args);
};
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[:]/;
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
@ -57,6 +60,13 @@ export const historyMethods = {
}
const loadSeq = ++this.historyLoadSeq;
jsonDebug('history.fetch:start', {
loadSeq,
force,
targetConversationId,
currentConversationId: this.currentConversationId,
currentMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1
});
this.historyLoading = true;
this.historyLoadingFor = targetConversationId;
try {
@ -97,6 +107,14 @@ export const historyMethods = {
}
const messagesData = await messagesResponse.json();
jsonDebug('history.fetch:response', {
loadSeq,
success: !!messagesData?.success,
responseConversationId: messagesData?.data?.conversation_id,
messagesCount: Array.isArray(messagesData?.data?.messages)
? messagesData.data.messages.length
: -1
});
debugLog('获取到消息数据:', messagesData);
if (messagesData.success && messagesData.data && messagesData.data.messages) {
@ -111,6 +129,11 @@ export const historyMethods = {
// 渲染历史消息 - 这是关键功能
this.renderHistoryMessages(messages);
jsonDebug('history.fetch:rendered', {
loadSeq,
renderedMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1,
lastRole: this.messages?.[this.messages.length - 1]?.role || null
});
// 滚动到底部
this.$nextTick(() => {
@ -166,6 +189,8 @@ export const historyMethods = {
let currentAssistantMessage = null;
let historyHasImages = false;
let historyHasVideos = false;
let toolArgsParseFailCount = 0;
let toolResultParseFailCount = 0;
historyMessages.forEach((message, index) => {
debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
@ -254,6 +279,12 @@ export const historyMethods = {
} catch (e) {
console.warn('解析工具参数失败:', e);
arguments_obj = {};
toolArgsParseFailCount += 1;
jsonDebug('history.render:tool-args-parse-fail', {
toolName: toolCall?.function?.name,
toolCallId: toolCall?.id,
error: e?.message || String(e)
});
}
const action = {
@ -309,6 +340,12 @@ export const historyMethods = {
try {
result = JSON.parse(message.content);
} catch (e) {
toolResultParseFailCount += 1;
jsonDebug('history.render:tool-result-parse-fail', {
toolName: message?.name,
toolCallId: message?.tool_call_id,
error: e?.message || String(e)
});
if (message.metadata && message.metadata.tool_payload) {
result = message.metadata.tool_payload;
} else {
@ -392,6 +429,13 @@ export const historyMethods = {
this.conversationHasVideos = historyHasVideos;
debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
jsonDebug('history.render:done', {
historyInputCount: Array.isArray(historyMessages) ? historyMessages.length : -1,
finalMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1,
lastRole: this.messages?.[this.messages.length - 1]?.role || null,
toolArgsParseFailCount,
toolResultParseFailCount
});
this.logMessageState('renderHistoryMessages:after-render');
this.lastHistoryLoadedConversationId = this.currentConversationId || null;

View File

@ -7,6 +7,9 @@ const debugNotifyLog = (...args: any[]) => {
const keyNotifyLog = (...args: any[]) => {
console.log(...args);
};
const jsonDebug = (...args: any[]) => {
console.log('[JSONDEBUG]', ...args);
};
/**
*
@ -127,6 +130,21 @@ export const taskPollingMethods = {
const eventType = event.type;
const eventData = event.data || {};
const eventIdx = event.idx;
if (eventType === 'task_complete' || eventType === 'task_stopped' || eventType === 'error') {
jsonDebug('task-event', {
eventType,
eventIdx,
currentConversationId: this.currentConversationId,
eventConversationId: eventData?.conversation_id,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested,
waitingForSubAgent: this.waitingForSubAgent,
waitingForBackgroundCommand: this.waitingForBackgroundCommand,
errorType: eventData?.error_type,
errorMessage: eventData?.message
});
}
if (
eventType === 'system_message' ||
eventType === 'sub_agent_waiting' ||
@ -427,7 +445,7 @@ export const taskPollingMethods = {
this.$nextTick(() => {
console.log('[AiMessageStart] nextTick回调执行');
this.scrollToBottom();
this.conditionalScrollToBottom();
});
},
@ -472,7 +490,7 @@ export const taskPollingMethods = {
this.chatExpandBlock(blockId);
}
this.scrollToBottom();
this.conditionalScrollToBottom();
this.chatSetThinkingLock(blockId, true);
// 强制触发 actions 数组的响应式更新
@ -888,6 +906,19 @@ export const taskPollingMethods = {
},
handleTaskComplete(data: any) {
const pendingToolsBefore =
typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null;
jsonDebug('handleTaskComplete:before', {
data,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested,
pendingToolsBefore,
preparingToolsSize: this.preparingTools?.size ?? null,
activeToolsSize: this.activeTools?.size ?? null,
waitingForSubAgent: this.waitingForSubAgent,
waitingForBackgroundCommand: this.waitingForBackgroundCommand
});
const hasRunningSubAgents = !!data?.has_running_sub_agents;
const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
if (hasRunningSubAgents) {
@ -911,6 +942,11 @@ export const taskPollingMethods = {
this.clearProcessedEvents();
this.startWaitingTaskProbe();
} else {
// 主任务已结束:若有遗留工具块处于 running/preparing会导致发送按钮继续显示“停止”。
// 这里统一清理遗留中的工具状态,避免前端忙碌态卡死。
if (typeof this.clearPendingTools === 'function') {
this.clearPendingTools('task_complete');
}
this.taskInProgress = false;
this.waitingForSubAgent = false;
this.waitingForBackgroundCommand = false;
@ -927,9 +963,28 @@ export const taskPollingMethods = {
this.updateCurrentContextTokens();
}, 500);
}
const pendingToolsAfter =
typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null;
jsonDebug('handleTaskComplete:after', {
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested,
pendingToolsAfter,
preparingToolsSize: this.preparingTools?.size ?? null,
activeToolsSize: this.activeTools?.size ?? null,
waitingForSubAgent: this.waitingForSubAgent,
waitingForBackgroundCommand: this.waitingForBackgroundCommand
});
},
handleTaskStopped(data: any, eventIdx: number) {
jsonDebug('handleTaskStopped:before', {
eventIdx,
data,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested
});
debugLog('[TaskPolling] 任务已停止, idx:', eventIdx, data);
this.markLatestUserWorkCompleted();
@ -946,6 +1001,11 @@ export const taskPollingMethods = {
this.$forceUpdate();
this.clearTaskState();
jsonDebug('handleTaskStopped:after', {
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested
});
},
// 新增统一清理方法
@ -1023,6 +1083,12 @@ export const taskPollingMethods = {
},
handleTaskError(data: any) {
jsonDebug('handleTaskError:incoming', {
data,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested
});
const shouldRetry = Boolean(data?.retry);
if (shouldRetry) {
const retryIn = Number(data?.retry_in) || 5;
@ -1052,6 +1118,32 @@ export const taskPollingMethods = {
const errorMessage = data.message || '未知错误';
const errorType = data.error_type || 'unknown';
const isToolArgumentParseError =
errorType === 'parameter_format_error' ||
/工具参数解析失败/.test(String(errorMessage || ''));
// 工具参数解析失败属于“单个工具调用失败”,后端会继续执行主任务。
// 这里不能停止轮询,否则会出现“后端继续跑、前端不再更新”的假死状态。
if (isToolArgumentParseError) {
console.warn('[TaskPolling] 工具参数解析失败(非致命),继续轮询任务:', data);
jsonDebug('handleTaskError:tool-args-parse-error-ignored', {
errorType,
errorMessage,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested
});
this.uiPushToast({
title: '工具调用失败',
message: errorMessage,
type: 'warning',
duration: 6000
});
// 保持当前运行态,不在这里强制置为“工作中”,避免后续任务结束时状态粘住
this.stopRequested = false;
this.$forceUpdate();
return;
}
let title = '任务执行失败';
let message = errorMessage;
@ -1082,6 +1174,13 @@ export const taskPollingMethods = {
this.taskInProgress = false;
this.stopRequested = false;
this.$forceUpdate();
jsonDebug('handleTaskError:fatal-after-state-update', {
errorType,
errorMessage,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
stopRequested: this.stopRequested
});
// 停止轮询
(async () => {

View File

@ -1452,7 +1452,9 @@ export const uiMethods = {
const clientHeight = messagesArea.clientHeight;
const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold;
const activeLock = this.autoScrollEnabled && this.isOutputActive();
// 仅在“已锁定autoScrollEnabled=true 且 userScrolling=false+ 有输出”时强制贴底
// 避免用户手动上滑后,仍被误判成锁定状态而被拉回到底部。
const activeLock = this.autoScrollEnabled && !this.userScrolling && this.isOutputActive();
// 锁定且当前有输出时,强制贴底
if (activeLock) {

View File

@ -8,6 +8,9 @@ const debugNotifyLog = (...args: any[]) => {
const keyNotifyLog = (...args: any[]) => {
console.log(...args);
};
const jsonDebug = (...args: any[]) => {
console.log('[JSONDEBUG]', ...args);
};
export const useTaskStore = defineStore('task', {
state: () => ({
@ -110,10 +113,18 @@ export const useTaskStore = defineStore('task', {
}
const data = result.data;
jsonDebug('taskStore.poll:response', {
taskId: this.currentTaskId,
from: this.lastEventIndex,
status: data?.status,
nextOffset: data?.next_offset,
eventsCount: Array.isArray(data?.events) ? data.events.length : 0
});
// 更新任务状态
this.taskStatus = data.status;
this.taskUpdatedAt = data.updated_at;
let sawTaskCompleteEvent = false;
let sawTaskStoppedEvent = false;
// 处理新事件
@ -121,6 +132,20 @@ export const useTaskStore = defineStore('task', {
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
for (const event of data.events) {
if (
event?.type === 'task_complete' ||
event?.type === 'task_stopped' ||
event?.type === 'error'
) {
jsonDebug('taskStore.poll:event', {
idx: event?.idx,
type: event?.type,
data: event?.data
});
}
if (event?.type === 'task_complete') {
sawTaskCompleteEvent = true;
}
if (event?.type === 'task_stopped') {
sawTaskStoppedEvent = true;
}
@ -163,6 +188,10 @@ export const useTaskStore = defineStore('task', {
// 补发一个本地停止事件,确保前端解除“停止中”状态。
if (data.status === 'canceled' && !sawTaskStoppedEvent) {
debugLog('[Task] 状态已取消但未收到 task_stopped补发本地事件');
jsonDebug('taskStore.poll:emit-synthetic-task-stopped', {
taskId: data.task_id || this.currentTaskId,
conversation_id: data.conversation_id || null
});
try {
eventHandler({
type: 'task_stopped',
@ -177,6 +206,30 @@ export const useTaskStore = defineStore('task', {
}
}
// 兜底:某些异常流程(如工具参数解析失败后恢复执行)可能没有 task_complete 事件,
// 但任务状态已经 succeeded。补发一次完成事件避免前端一直显示“工作中”。
if (data.status === 'succeeded' && !sawTaskCompleteEvent) {
debugLog('[Task] 状态已成功但未收到 task_complete补发本地事件');
jsonDebug('taskStore.poll:emit-synthetic-task-complete', {
taskId: data.task_id || this.currentTaskId,
conversation_id: data.conversation_id || null
});
try {
eventHandler({
type: 'task_complete',
data: {
task_id: data.task_id || this.currentTaskId,
conversation_id: data.conversation_id || null,
synthetic: true,
has_running_sub_agents: false,
has_running_background_commands: false
}
});
} catch (err) {
console.error('[Task] 处理补发 task_complete 失败:', err);
}
}
// 如果任务已完成,停止轮询
if (this.isTaskCompleted) {
debugLog('[Task] 任务已完成,停止轮询:', this.taskStatus);