From 49bd994cfa444aa6841b700df81c6712eedf33df Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 12 Apr 2026 19:45:01 +0800 Subject: [PATCH] fix: stabilize task completion and chat auto-scroll behavior --- static/src/app/methods/history.ts | 44 +++++++++++ static/src/app/methods/taskPolling.ts | 103 +++++++++++++++++++++++++- static/src/app/methods/ui.ts | 4 +- static/src/stores/task.ts | 53 +++++++++++++ 4 files changed, 201 insertions(+), 3 deletions(-) diff --git a/static/src/app/methods/history.ts b/static/src/app/methods/history.ts index 5f24de3..e5c0d88 100644 --- a/static/src/app/methods/history.ts +++ b/static/src/app/methods/history.ts @@ -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; diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index e00a96e..78bbac1 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -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 () => { diff --git a/static/src/app/methods/ui.ts b/static/src/app/methods/ui.ts index 95319d5..bc525a0 100644 --- a/static/src/app/methods/ui.ts +++ b/static/src/app/methods/ui.ts @@ -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) { diff --git a/static/src/stores/task.ts b/static/src/stores/task.ts index 7b19d1d..2b124ff 100644 --- a/static/src/stores/task.ts +++ b/static/src/stores/task.ts @@ -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);