fix: stabilize task completion and chat auto-scroll behavior
This commit is contained in:
parent
757718c492
commit
49bd994cfa
@ -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;
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user