2082 lines
70 KiB
TypeScript
2082 lines
70 KiB
TypeScript
// @ts-nocheck
|
||
import { debugLog } from './common';
|
||
|
||
const debugNotifyLog = (...args: any[]) => {
|
||
void args;
|
||
};
|
||
const keyNotifyLog = (...args: any[]) => {
|
||
console.log(...args);
|
||
};
|
||
const jsonDebug = (...args: any[]) => {
|
||
console.log('[JSONDEBUG]', ...args);
|
||
};
|
||
const userMDebug = (...args: any[]) => {
|
||
console.log('[USERMDEBUG]', ...args);
|
||
};
|
||
const RESTORE_DEBUG_PREFIX = '[RESTORE_DEBUG]';
|
||
let restoreDebugCount = 0;
|
||
const RESTORE_DEBUG_MAX = 800;
|
||
const RESTORE_DEBUG_EVENTS = new Set([
|
||
'restore:start',
|
||
'restore:running-task-found',
|
||
'restore:history-empty',
|
||
'restore:task-detail-events',
|
||
'restore:rebuild-decision',
|
||
'restore:rebuild-polling-started',
|
||
'restore:polling-started-follow',
|
||
'restore:thinking-chunk-auto-start',
|
||
'restore:text-chunk-auto-start',
|
||
'restore:error',
|
||
'event:drop-duplicate',
|
||
'event:drop-conversation-mismatch'
|
||
]);
|
||
|
||
function isRestoreDebugEnabled() {
|
||
if (typeof window === 'undefined') return true;
|
||
try {
|
||
const explicit = (window as any).__RESTORE_DEBUG__;
|
||
if (explicit === false || explicit === '0') return false;
|
||
if (explicit === true || explicit === '1') return true;
|
||
const localFlag = window.localStorage?.getItem('restoreDebug');
|
||
if (localFlag === '0' || localFlag === 'false') return false;
|
||
if (localFlag === '1' || localFlag === 'true') return true;
|
||
return true;
|
||
} catch {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
function restoreDebugLog(event: string, payload: Record<string, any> = {}) {
|
||
if (!isRestoreDebugEnabled()) return;
|
||
if (!RESTORE_DEBUG_EVENTS.has(event)) return;
|
||
if (restoreDebugCount >= RESTORE_DEBUG_MAX) return;
|
||
restoreDebugCount += 1;
|
||
if (restoreDebugCount === RESTORE_DEBUG_MAX) {
|
||
console.warn(RESTORE_DEBUG_PREFIX, 'log-limit-reached', { max: RESTORE_DEBUG_MAX });
|
||
return;
|
||
}
|
||
console.log(RESTORE_DEBUG_PREFIX, event, payload);
|
||
}
|
||
|
||
function isSystemAutoUserMessagePayload(data: any): boolean {
|
||
if (!data || typeof data !== 'object') {
|
||
return false;
|
||
}
|
||
const meta = data.metadata || {};
|
||
return !!(
|
||
data.is_auto_generated ||
|
||
meta.is_auto_generated ||
|
||
data.auto_message_type ||
|
||
meta.auto_message_type ||
|
||
data.sub_agent_notice ||
|
||
meta.sub_agent_notice ||
|
||
data.background_command_notice ||
|
||
meta.background_command_notice
|
||
);
|
||
}
|
||
|
||
function isEmptyAssistantPlaceholderMessage(message: any): boolean {
|
||
if (!message || message.role !== 'assistant') {
|
||
return false;
|
||
}
|
||
const actions = Array.isArray(message.actions) ? message.actions : [];
|
||
return actions.length === 0 && !!message.awaitingFirstContent;
|
||
}
|
||
|
||
function getOptimisticUserEchoTarget(messages: any[]): any | null {
|
||
if (!Array.isArray(messages) || messages.length === 0) {
|
||
return null;
|
||
}
|
||
const lastIndex = messages.length - 1;
|
||
const last = messages[lastIndex];
|
||
if (last?.role === 'user') {
|
||
return last;
|
||
}
|
||
if (isEmptyAssistantPlaceholderMessage(last) && lastIndex > 0) {
|
||
const prev = messages[lastIndex - 1];
|
||
if (prev?.role === 'user') {
|
||
return prev;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 任务轮询事件处理器
|
||
* 将从 REST API 轮询获取的事件转换为前端状态更新
|
||
*/
|
||
export const taskPollingMethods = {
|
||
scheduleTodoListRefresh(delayMs = 120) {
|
||
const delay = Number.isFinite(delayMs) ? Math.max(0, Number(delayMs)) : 120;
|
||
if (this._todoRefreshTimer) {
|
||
clearTimeout(this._todoRefreshTimer);
|
||
this._todoRefreshTimer = null;
|
||
}
|
||
this._todoRefreshTimer = setTimeout(() => {
|
||
this._todoRefreshTimer = null;
|
||
if (typeof this.fetchTodoList === 'function') {
|
||
Promise.resolve(this.fetchTodoList()).catch(() => {});
|
||
}
|
||
}, delay);
|
||
},
|
||
|
||
cleanupTrailingEmptyAssistantPlaceholder(reason = 'unspecified') {
|
||
if (!Array.isArray(this.messages) || !this.messages.length) {
|
||
return false;
|
||
}
|
||
const last = this.messages[this.messages.length - 1];
|
||
if (!isEmptyAssistantPlaceholderMessage(last)) {
|
||
return false;
|
||
}
|
||
this.messages.pop();
|
||
if (typeof this.currentMessageIndex === 'number') {
|
||
this.currentMessageIndex = this.messages.length - 1;
|
||
}
|
||
userMDebug('taskPolling.cleanupTrailingEmptyAssistantPlaceholder:removed', {
|
||
reason,
|
||
messagesLengthAfter: this.messages.length,
|
||
currentMessageIndex: this.currentMessageIndex
|
||
});
|
||
return true;
|
||
},
|
||
|
||
stopWaitingTaskProbe() {
|
||
if (this.waitingTaskProbeTimer) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] stopWaitingTaskProbe');
|
||
clearInterval(this.waitingTaskProbeTimer);
|
||
this.waitingTaskProbeTimer = null;
|
||
}
|
||
},
|
||
|
||
async tryResumeWaitingNoticeTask() {
|
||
if (!this.waitingForSubAgent || !this.currentConversationId) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:skip', {
|
||
waitingForSubAgent: this.waitingForSubAgent,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
return false;
|
||
}
|
||
try {
|
||
const { useTaskStore } = await import('../../stores/task');
|
||
const taskStore = useTaskStore();
|
||
if (taskStore.hasActiveTask) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:already-active', {
|
||
taskId: taskStore.currentTaskId,
|
||
status: taskStore.taskStatus
|
||
});
|
||
return true;
|
||
}
|
||
|
||
const resp = await fetch('/api/tasks');
|
||
if (!resp.ok) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:tasks-api-not-ok', {
|
||
status: resp.status
|
||
});
|
||
return false;
|
||
}
|
||
const result = await resp.json().catch(() => ({}));
|
||
const tasks = Array.isArray(result?.data) ? result.data : [];
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:tasks', {
|
||
currentConversationId: this.currentConversationId,
|
||
count: tasks.length,
|
||
running: tasks
|
||
.filter((t: any) => t?.status === 'running')
|
||
.map((t: any) => ({
|
||
task_id: t?.task_id,
|
||
conversation_id: t?.conversation_id,
|
||
status: t?.status
|
||
}))
|
||
});
|
||
const runningTask = tasks.find(
|
||
(task: any) =>
|
||
task?.status === 'running' && task?.conversation_id === this.currentConversationId
|
||
);
|
||
if (!runningTask?.task_id) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:no-matched-task');
|
||
return false;
|
||
}
|
||
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:resume', {
|
||
task_id: runningTask.task_id
|
||
});
|
||
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] tryResumeWaitingNoticeTask:resume', {
|
||
task_id: runningTask.task_id,
|
||
conversationId: this.currentConversationId
|
||
});
|
||
// 关键修复:切到新的通知任务前,清空旧任务事件去重索引。
|
||
// 否则新任务 event idx 与旧任务重叠时会被误判为“重复事件”而丢失 user_message。
|
||
if (typeof this.clearProcessedEvents === 'function') {
|
||
this.clearProcessedEvents();
|
||
}
|
||
taskStore.resumeTask(runningTask.task_id, {
|
||
status: 'running',
|
||
resetOffset: true,
|
||
eventHandler: (event: any) => this.handleTaskEvent(event)
|
||
});
|
||
return true;
|
||
} catch (error) {
|
||
console.warn('[TaskPolling] 接管等待任务失败:', error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
startWaitingTaskProbe() {
|
||
if (this.waitingTaskProbeTimer) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] startWaitingTaskProbe:already-started');
|
||
return;
|
||
}
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] startWaitingTaskProbe');
|
||
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] startWaitingTaskProbe', {
|
||
conversationId: this.currentConversationId
|
||
});
|
||
this.waitingTaskProbeTimer = setInterval(async () => {
|
||
if (!this.waitingForSubAgent) {
|
||
this.stopWaitingTaskProbe();
|
||
return;
|
||
}
|
||
const resumed = await this.tryResumeWaitingNoticeTask();
|
||
if (resumed) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] waitingTaskProbe:resumed-and-stop');
|
||
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] waitingTaskProbe:resumed-and-stop');
|
||
this.stopWaitingTaskProbe();
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
/**
|
||
* 处理任务事件(从轮询获取)
|
||
*/
|
||
handleTaskEvent(event: any) {
|
||
if (!event || !event.type) {
|
||
return;
|
||
}
|
||
|
||
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' ||
|
||
(eventType === 'user_message' && eventData?.sub_agent_notice)
|
||
) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][event] 捕获关键事件', {
|
||
eventType,
|
||
eventIdx,
|
||
eventData
|
||
});
|
||
keyNotifyLog('[DEBUG_NOTIFY_KEY][event] key-event', {
|
||
eventType,
|
||
idx: eventIdx,
|
||
task_id: eventData?.task_id,
|
||
sub_agent_notice: !!eventData?.sub_agent_notice,
|
||
has_running_sub_agents: eventData?.has_running_sub_agents,
|
||
has_running_background_commands: eventData?.has_running_background_commands
|
||
});
|
||
}
|
||
|
||
// 事件去重检查
|
||
if (typeof eventIdx === 'number') {
|
||
if (!this._processedEventIndices) {
|
||
this._processedEventIndices = new Set();
|
||
}
|
||
|
||
if (this._processedEventIndices.has(eventIdx)) {
|
||
if (
|
||
[
|
||
'ai_message_start',
|
||
'thinking_start',
|
||
'thinking_chunk',
|
||
'thinking_end',
|
||
'text_start',
|
||
'text_chunk',
|
||
'text_end'
|
||
].includes(eventType)
|
||
) {
|
||
restoreDebugLog('event:drop-duplicate', {
|
||
eventType,
|
||
eventIdx,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
}
|
||
debugLog(`[TaskPolling] 跳过重复事件 #${eventIdx}`);
|
||
return;
|
||
}
|
||
|
||
this._processedEventIndices.add(eventIdx);
|
||
|
||
// 限制 Set 大小(保留最近 1000 个)
|
||
if (this._processedEventIndices.size > 1000) {
|
||
const firstIdx = Math.min(...this._processedEventIndices);
|
||
this._processedEventIndices.delete(firstIdx);
|
||
}
|
||
}
|
||
|
||
// 检查事件的 conversation_id 是否匹配当前对话
|
||
// 如果不匹配,忽略该事件(避免切换对话后旧任务的事件显示到新对话中)
|
||
const crossConversationAllowed = new Set([
|
||
'shallow_compression',
|
||
'conversation_resolved',
|
||
'compression_finished'
|
||
]);
|
||
if (
|
||
!crossConversationAllowed.has(eventType) &&
|
||
eventData.conversation_id &&
|
||
this.currentConversationId
|
||
) {
|
||
if (eventData.conversation_id !== this.currentConversationId) {
|
||
if (
|
||
[
|
||
'ai_message_start',
|
||
'thinking_start',
|
||
'thinking_chunk',
|
||
'thinking_end',
|
||
'text_start',
|
||
'text_chunk',
|
||
'text_end'
|
||
].includes(eventType)
|
||
) {
|
||
restoreDebugLog('event:drop-conversation-mismatch', {
|
||
eventType,
|
||
eventIdx,
|
||
eventConversationId: eventData.conversation_id,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
}
|
||
console.log(`[DEBUG_AWAITING] 忽略不匹配的事件`, {
|
||
eventType,
|
||
eventIdx,
|
||
eventConversationId: eventData.conversation_id,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
debugLog(
|
||
`[TaskPolling] 忽略不匹配的事件 #${eventIdx}: ${eventType}, 事件对话=${eventData.conversation_id}, 当前对话=${this.currentConversationId}`
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
debugLog(`[TaskPolling] 处理事件 #${eventIdx}: ${eventType}`, eventData);
|
||
|
||
console.log(`[DEBUG_AWAITING] 处理事件`, {
|
||
eventType,
|
||
eventIdx,
|
||
eventConversationId: eventData.conversation_id,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
|
||
// 根据事件类型调用对应的处理方法
|
||
switch (eventType) {
|
||
case 'ai_message_start':
|
||
this.handleAiMessageStart(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'thinking_start':
|
||
this.handleThinkingStart(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'thinking_chunk':
|
||
this.handleThinkingChunk(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'thinking_end':
|
||
this.handleThinkingEnd(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'text_start':
|
||
this.handleTextStart(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'text_chunk':
|
||
this.handleTextChunk(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'text_end':
|
||
this.handleTextEnd(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'tool_preparing':
|
||
this.handleToolPreparing(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'tool_start':
|
||
this.handleToolStart(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'tool_intent':
|
||
this.handleToolIntent(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'tool_update_action':
|
||
case 'update_action':
|
||
this.handleToolUpdateAction(eventData, eventIdx);
|
||
break;
|
||
case 'tool_approval_required':
|
||
this.handleToolApprovalRequired(eventData, eventIdx);
|
||
break;
|
||
case 'tool_approval_resolved':
|
||
this.handleToolApprovalResolved(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'append_payload':
|
||
this.handleAppendPayload(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'modify_payload':
|
||
this.handleModifyPayload(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'task_complete':
|
||
this.handleTaskComplete(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'task_stopped':
|
||
this.handleTaskStopped(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'error':
|
||
this.handleTaskError(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'token_update':
|
||
this.handleTokenUpdate(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'conversation_changed':
|
||
this.handleConversationChanged(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'conversation_resolved':
|
||
this.handleConversationResolved(eventData, eventIdx);
|
||
break;
|
||
case 'compression_state':
|
||
this.handleCompressionState(eventData, eventIdx);
|
||
break;
|
||
case 'compression_finished':
|
||
this.handleCompressionFinished(eventData, eventIdx);
|
||
break;
|
||
case 'shallow_compression':
|
||
this.handleShallowCompression(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'user_message':
|
||
debugNotifyLog('[DEBUG_NOTIFY][event] dispatch:user_message', {
|
||
idx: eventIdx,
|
||
data: eventData
|
||
});
|
||
this.handleUserMessage(eventData, eventIdx);
|
||
break;
|
||
|
||
case 'system_message':
|
||
this.handleSystemMessage(eventData, eventIdx);
|
||
break;
|
||
|
||
default:
|
||
debugLog(`[TaskPolling] 未知事件类型: ${eventType}`);
|
||
}
|
||
|
||
// 注意:不要在这里清除 _rebuildingFromScratch 标记
|
||
// 该标记应该在恢复完所有历史事件后才清除
|
||
},
|
||
|
||
handleAiMessageStart(data: any, eventIdx: number) {
|
||
const lastMessage = this.messages[this.messages.length - 1];
|
||
console.log('[DEBUG_AWAITING] ===== handleAiMessageStart =====', {
|
||
eventIdx,
|
||
lastMessageRole: lastMessage?.role,
|
||
lastMessageAwaitingFirstContent: lastMessage?.awaitingFirstContent,
|
||
lastMessageGeneratingLabel: lastMessage?.generatingLabel
|
||
});
|
||
|
||
debugLog('[TaskPolling] AI消息开始, idx:', eventIdx);
|
||
console.log('[AiMessageStart] 开始处理', {
|
||
eventIdx,
|
||
messagesLength: this.messages.length,
|
||
lastMessageRole: this.messages[this.messages.length - 1]?.role,
|
||
streamingMessage: this.streamingMessage,
|
||
taskInProgress: this.taskInProgress,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
|
||
if (this.waitingForSubAgent) {
|
||
this.waitingForSubAgent = false;
|
||
}
|
||
|
||
// 检查是否已经有 assistant 消息
|
||
const hasAssistantMessage = lastMessage && lastMessage.role === 'assistant';
|
||
|
||
console.log('[AiMessageStart] 最后一条消息状态', {
|
||
hasAssistantMessage,
|
||
lastMessageActionsLength: lastMessage?.actions?.length || 0,
|
||
lastMessageAwaitingFirstContent: lastMessage?.awaitingFirstContent,
|
||
lastMessageGeneratingLabel: lastMessage?.generatingLabel
|
||
});
|
||
|
||
// 判断是否是刷新恢复的情况:
|
||
// 1. 有assistant消息
|
||
// 2. 且最后一条 assistant 仍处于“未完成流式状态”(而不是普通已完成消息)
|
||
// 3. 且任务确实仍在进行中
|
||
const lastActions = Array.isArray(lastMessage?.actions) ? lastMessage.actions : [];
|
||
const hasUnfinishedAction = lastActions.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();
|
||
return ['preparing', 'running', 'pending', 'queued', 'awaiting_approval'].includes(status);
|
||
}
|
||
return false;
|
||
});
|
||
const hasNoActionsYet = !lastActions.length;
|
||
const isRefreshRestore =
|
||
hasAssistantMessage &&
|
||
(hasNoActionsYet || hasUnfinishedAction || lastMessage.awaitingFirstContent === true) &&
|
||
(this.taskInProgress || this.streamingMessage);
|
||
|
||
userMDebug('taskPolling.handleAiMessageStart:decision', {
|
||
eventIdx,
|
||
hasAssistantMessage,
|
||
hasNoActionsYet,
|
||
hasUnfinishedAction,
|
||
awaitingFirstContent: !!lastMessage?.awaitingFirstContent,
|
||
taskInProgress: this.taskInProgress,
|
||
streamingMessage: this.streamingMessage,
|
||
isRefreshRestore
|
||
});
|
||
|
||
console.log('[AiMessageStart] 场景判断', {
|
||
isRefreshRestore,
|
||
streamingMessage: this.streamingMessage,
|
||
hasActions: !!(lastMessage?.actions && lastMessage.actions.length > 0),
|
||
awaitingFirstContent: lastMessage?.awaitingFirstContent,
|
||
taskInProgress: this.taskInProgress
|
||
});
|
||
|
||
if (isRefreshRestore) {
|
||
console.log('[AiMessageStart] 场景=刷新恢复,复用现有assistant消息');
|
||
debugLog('[TaskPolling] 刷新恢复场景,复用现有 assistant 消息');
|
||
// 只更新状态,不创建新消息
|
||
this.taskInProgress = true;
|
||
this.stopRequested = false;
|
||
this.streamingMessage = true;
|
||
|
||
// 确保 awaitingFirstContent 被正确设置
|
||
const hasContent = lastMessage.actions && lastMessage.actions.length > 0;
|
||
if (!hasContent) {
|
||
lastMessage.awaitingFirstContent = true;
|
||
lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...';
|
||
console.log('[AiMessageStart] 设置等待动画', {
|
||
awaitingFirstContent: true,
|
||
generatingLabel: lastMessage.generatingLabel
|
||
});
|
||
} else {
|
||
// 如果已有内容,确保等待动画不显示
|
||
lastMessage.awaitingFirstContent = false;
|
||
lastMessage.generatingLabel = '';
|
||
console.log('[AiMessageStart] 关闭等待动画(已有内容)');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 其他情况:创建新的 assistant 消息
|
||
console.log('[AiMessageStart] 场景=创建新消息');
|
||
debugLog('[TaskPolling] 创建新的 assistant 消息');
|
||
this.monitorResetSpeech();
|
||
this.cleanupStaleToolActions();
|
||
this.taskInProgress = true;
|
||
this.chatStartAssistantMessage();
|
||
this.stopRequested = false;
|
||
this.streamingMessage = true;
|
||
|
||
const newMessage = this.messages[this.messages.length - 1];
|
||
console.log('[AiMessageStart] 新消息创建完成', {
|
||
role: newMessage?.role,
|
||
awaitingFirstContent: newMessage?.awaitingFirstContent,
|
||
generatingLabel: newMessage?.generatingLabel,
|
||
actionsLength: newMessage?.actions?.length || 0,
|
||
messagesLength: this.messages.length
|
||
});
|
||
|
||
// 如果是从头重建,标记消息为静默恢复
|
||
if (this._rebuildingFromScratch) {
|
||
if (newMessage && newMessage.role === 'assistant') {
|
||
debugLog('[TaskPolling] 标记消息为静默恢复(从头重建)');
|
||
newMessage.awaitingFirstContent = false;
|
||
newMessage.generatingLabel = '';
|
||
console.log('[AiMessageStart] 从头重建,关闭等待动画');
|
||
}
|
||
}
|
||
|
||
// 强制触发Vue响应式更新
|
||
this.$forceUpdate();
|
||
console.log('[AiMessageStart] 已调用$forceUpdate');
|
||
|
||
this.$nextTick(() => {
|
||
console.log('[AiMessageStart] nextTick回调执行');
|
||
this.conditionalScrollToBottom();
|
||
});
|
||
},
|
||
|
||
handleThinkingStart(data: any, eventIdx: number) {
|
||
console.log('[ThinkingStart] 开始处理', { eventIdx });
|
||
debugLog('[TaskPolling] 思考开始, idx:', eventIdx);
|
||
const ignoreThinking = this.runMode === 'fast' || this.thinkingMode === false;
|
||
if (ignoreThinking) {
|
||
this.monitorEndModelOutput();
|
||
console.log('[ThinkingStart] 忽略思考(fast模式或关闭思考)');
|
||
return;
|
||
}
|
||
|
||
// 防御性检查:如果没有assistant消息,先创建一个
|
||
const lastMessage = this.messages[this.messages.length - 1];
|
||
if (!lastMessage || lastMessage.role !== 'assistant') {
|
||
console.log('[DEBUG_AWAITING] ThinkingStart 检测到缺少assistant消息,自动创建');
|
||
this.chatStartAssistantMessage();
|
||
}
|
||
|
||
this.monitorShowThinking();
|
||
|
||
const result = this.chatStartThinkingAction();
|
||
|
||
if (result && result.blockId) {
|
||
const blockId = result.blockId;
|
||
|
||
const lastMessage = this.messages[this.messages.length - 1];
|
||
|
||
// 有内容了,关闭等待动画
|
||
if (lastMessage && lastMessage.role === 'assistant') {
|
||
console.log('[ThinkingStart] 关闭等待动画', {
|
||
beforeAwaitingFirstContent: lastMessage.awaitingFirstContent,
|
||
beforeGeneratingLabel: lastMessage.generatingLabel
|
||
});
|
||
lastMessage.awaitingFirstContent = false;
|
||
lastMessage.generatingLabel = '';
|
||
}
|
||
|
||
// 只在非历史恢复时展开思考块
|
||
if (!this._rebuildingFromScratch) {
|
||
this.chatExpandBlock(blockId);
|
||
}
|
||
|
||
this.conditionalScrollToBottom();
|
||
this.chatSetThinkingLock(blockId, true);
|
||
|
||
// 强制触发 actions 数组的响应式更新
|
||
if (lastMessage && lastMessage.actions) {
|
||
lastMessage.actions = [...lastMessage.actions];
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
|
||
handleThinkingChunk(data: any) {
|
||
if (this.runMode === 'fast' || this.thinkingMode === false) {
|
||
return;
|
||
}
|
||
let thinkingAction = this.chatAppendThinkingChunk(data.content);
|
||
// 兜底:刷新恢复时可能只能拿到 chunk 事件(start 事件已被服务端窗口截断)
|
||
// 此时补建一个 thinking action,避免思考内容丢失。
|
||
if (!thinkingAction && data?.content) {
|
||
const started = this.chatStartThinkingAction();
|
||
thinkingAction = this.chatAppendThinkingChunk(data.content);
|
||
restoreDebugLog('restore:thinking-chunk-auto-start', {
|
||
startedBlockId: started?.blockId || null,
|
||
contentLength: String(data.content || '').length,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
}
|
||
if (thinkingAction) {
|
||
this.$forceUpdate();
|
||
this.$nextTick(() => {
|
||
if (thinkingAction && thinkingAction.blockId) {
|
||
this.scrollThinkingToBottom(thinkingAction.blockId);
|
||
}
|
||
this.conditionalScrollToBottom();
|
||
});
|
||
}
|
||
this.monitorShowThinking();
|
||
},
|
||
|
||
handleThinkingEnd(data: any) {
|
||
debugLog('[TaskPolling] 思考结束');
|
||
if (this.runMode === 'fast' || this.thinkingMode === false) {
|
||
return;
|
||
}
|
||
const blockId = this.chatCompleteThinkingAction(data.full_content);
|
||
if (blockId) {
|
||
// 解锁思考块
|
||
this.chatSetThinkingLock(blockId, false);
|
||
|
||
// 只在非历史恢复时延迟折叠思考块
|
||
if (!this._rebuildingFromScratch) {
|
||
// 延迟折叠思考块(给用户一点时间看到思考完成)
|
||
setTimeout(() => {
|
||
this.chatCollapseBlock(blockId);
|
||
this.$forceUpdate();
|
||
}, 1000);
|
||
} else {
|
||
// 历史恢复时立即折叠,不需要动画
|
||
this.chatCollapseBlock(blockId);
|
||
}
|
||
|
||
this.$nextTick(() => this.scrollThinkingToBottom(blockId));
|
||
}
|
||
this.$forceUpdate();
|
||
this.monitorEndModelOutput();
|
||
},
|
||
|
||
handleTextStart() {
|
||
console.log('[TextStart] 开始处理');
|
||
debugLog('[TaskPolling] 文本开始');
|
||
|
||
// 防御性检查:如果没有assistant消息,先创建一个
|
||
const lastMessage = this.messages[this.messages.length - 1];
|
||
if (!lastMessage || lastMessage.role !== 'assistant') {
|
||
console.log('[DEBUG_AWAITING] TextStart 检测到缺少assistant消息,自动创建');
|
||
this.chatStartAssistantMessage();
|
||
}
|
||
|
||
this.chatStartTextAction();
|
||
|
||
// 有内容了,关闭等待动画
|
||
const currentMessage = this.messages[this.messages.length - 1];
|
||
if (currentMessage && currentMessage.role === 'assistant') {
|
||
console.log('[TextStart] 关闭等待动画', {
|
||
beforeAwaitingFirstContent: currentMessage.awaitingFirstContent,
|
||
beforeGeneratingLabel: currentMessage.generatingLabel
|
||
});
|
||
currentMessage.awaitingFirstContent = false;
|
||
currentMessage.generatingLabel = '';
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
handleTextChunk(data: any) {
|
||
if (data && typeof data.content === 'string' && data.content.length) {
|
||
let textAction = this.chatAppendTextChunk(data.content);
|
||
// 兜底:刷新恢复时可能只有 text_chunk(缺少 text_start),需要自动补建 text action
|
||
if (!textAction) {
|
||
this.chatStartTextAction();
|
||
textAction = this.chatAppendTextChunk(data.content);
|
||
restoreDebugLog('restore:text-chunk-auto-start', {
|
||
contentLength: data.content.length,
|
||
appendedAfterStart: !!textAction,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
}
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
const speech = data.content.replace(/\r/g, '');
|
||
if (speech) {
|
||
this.monitorShowSpeech(speech);
|
||
}
|
||
}
|
||
},
|
||
|
||
handleTextEnd(data: any) {
|
||
debugLog('[TaskPolling] 文本结束');
|
||
const full = data?.full_content || '';
|
||
this.chatCompleteTextAction(full);
|
||
this.$forceUpdate();
|
||
this.monitorEndModelOutput();
|
||
},
|
||
|
||
handleToolPreparing(data: any) {
|
||
console.log('[ToolPreparing] 开始处理', { name: data.name, id: data.id });
|
||
debugLog('[TaskPolling] 工具准备中:', data.name);
|
||
|
||
if (this.dropToolEvents) {
|
||
console.log('[ToolPreparing] dropToolEvents=true,跳过');
|
||
return;
|
||
}
|
||
|
||
const msg = this.chatEnsureAssistantMessage();
|
||
if (!msg) {
|
||
console.log('[ToolPreparing] 无法获取assistant消息');
|
||
return;
|
||
}
|
||
|
||
if (msg.awaitingFirstContent) {
|
||
console.log('[ToolPreparing] 关闭等待动画', {
|
||
beforeAwaitingFirstContent: msg.awaitingFirstContent,
|
||
beforeGeneratingLabel: msg.generatingLabel
|
||
});
|
||
msg.awaitingFirstContent = false;
|
||
msg.generatingLabel = '';
|
||
}
|
||
|
||
const action = {
|
||
id: data.id,
|
||
type: 'tool',
|
||
tool: {
|
||
id: data.id,
|
||
name: data.name,
|
||
arguments: {},
|
||
argumentSnapshot: null,
|
||
argumentLabel: '',
|
||
status: 'preparing',
|
||
result: null,
|
||
message: data.message || `准备调用 ${data.name}...`,
|
||
intent_full: data.intent || '',
|
||
intent_rendered: data.intent || ''
|
||
},
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
msg.actions.push(action);
|
||
this.preparingTools.set(data.id, action);
|
||
this.toolRegisterAction(action, data.id);
|
||
this.toolTrackAction(data.name, action);
|
||
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
|
||
if (this.monitorPreviewTool) {
|
||
this.monitorPreviewTool(data);
|
||
}
|
||
},
|
||
|
||
handleToolStart(data: any) {
|
||
debugLog('[TaskPolling] 工具开始:', data.name);
|
||
|
||
if (this.dropToolEvents) {
|
||
return;
|
||
}
|
||
|
||
let action = null;
|
||
if (data.preparing_id && this.preparingTools.has(data.preparing_id)) {
|
||
action = this.preparingTools.get(data.preparing_id);
|
||
this.preparingTools.delete(data.preparing_id);
|
||
} else {
|
||
action = this.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||
}
|
||
|
||
if (!action) {
|
||
const msg = this.chatEnsureAssistantMessage();
|
||
if (!msg) {
|
||
return;
|
||
}
|
||
action = {
|
||
id: data.id,
|
||
type: 'tool',
|
||
tool: {
|
||
id: data.id,
|
||
name: data.name,
|
||
arguments: {},
|
||
argumentSnapshot: null,
|
||
argumentLabel: '',
|
||
status: 'running',
|
||
result: null
|
||
},
|
||
timestamp: Date.now()
|
||
};
|
||
msg.actions.push(action);
|
||
}
|
||
|
||
action.tool.status = 'running';
|
||
action.tool.arguments = data.arguments;
|
||
action.tool.argumentSnapshot = this.cloneToolArguments(data.arguments);
|
||
action.tool.argumentLabel = this.buildToolLabel(action.tool.argumentSnapshot);
|
||
action.tool.message = null;
|
||
action.tool.id = data.id;
|
||
action.tool.executionId = data.id;
|
||
|
||
this.toolRegisterAction(action, data.id);
|
||
this.toolTrackAction(data.name, action);
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
|
||
if (this.monitorQueueTool) {
|
||
this.monitorQueueTool(data);
|
||
}
|
||
},
|
||
|
||
handleToolIntent(data: any) {
|
||
debugLog('[TaskPolling] 工具意图:', data.name, data.intent);
|
||
|
||
if (this.dropToolEvents) {
|
||
return;
|
||
}
|
||
|
||
// 查找对应的工具 action
|
||
const action = this.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||
|
||
if (action && action.tool) {
|
||
const newIntent = data.intent || '';
|
||
|
||
// 如果 intent 没有变化,跳过
|
||
if (action.tool.intent_full === newIntent) {
|
||
return;
|
||
}
|
||
|
||
// 停止之前的打字机效果
|
||
if (action.tool._intentTyping) {
|
||
action.tool._intentTyping = false;
|
||
if (action.tool._intentTimer) {
|
||
clearTimeout(action.tool._intentTimer);
|
||
action.tool._intentTimer = null;
|
||
}
|
||
}
|
||
|
||
// 更新完整 intent
|
||
action.tool.intent_full = newIntent;
|
||
|
||
// 判断是否是历史恢复:只有从头重建时才直接显示
|
||
const isHistoryRestore = this._rebuildingFromScratch;
|
||
|
||
if (isHistoryRestore) {
|
||
// 历史恢复,直接显示完整 intent
|
||
action.tool.intent_rendered = newIntent;
|
||
debugLog('[TaskPolling] 历史恢复,直接显示 intent');
|
||
} else {
|
||
// 新工具块,逐字符显示
|
||
debugLog('[TaskPolling] 新工具块,开始打字机效果');
|
||
action.tool.intent_rendered = '';
|
||
action.tool._intentTyping = true;
|
||
|
||
// 计算每个字符的间隔时间
|
||
// 总时长1秒,但最少0.5秒
|
||
const totalDuration = Math.max(500, Math.min(1000, newIntent.length * 50));
|
||
const charInterval = totalDuration / newIntent.length;
|
||
|
||
let charIndex = 0;
|
||
const typeNextChar = () => {
|
||
if (charIndex < newIntent.length && action.tool._intentTyping) {
|
||
action.tool.intent_rendered += newIntent[charIndex];
|
||
charIndex++;
|
||
this.$forceUpdate();
|
||
action.tool._intentTimer = setTimeout(typeNextChar, charInterval);
|
||
} else {
|
||
action.tool._intentTyping = false;
|
||
action.tool.intent_rendered = newIntent; // 确保完整
|
||
action.tool._intentTimer = null;
|
||
this.$forceUpdate();
|
||
}
|
||
};
|
||
|
||
action.tool._intentTimer = setTimeout(typeNextChar, 50); // 延迟50ms开始
|
||
}
|
||
|
||
// 更新 arguments 和 label
|
||
if (action.tool.arguments) {
|
||
action.tool.arguments.intent = newIntent;
|
||
action.tool.argumentSnapshot = this.cloneToolArguments(action.tool.arguments);
|
||
action.tool.argumentLabel = this.buildToolLabel(action.tool.argumentSnapshot);
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
debugLog('[TaskPolling] 已更新工具意图:', data.name);
|
||
} else {
|
||
debugLog('[TaskPolling] 未找到对应的工具 action:', data.id);
|
||
}
|
||
},
|
||
|
||
handleToolUpdateAction(data: any) {
|
||
if (this.dropToolEvents) {
|
||
return;
|
||
}
|
||
|
||
debugLog('[TaskPolling] 更新action:', data.id, 'status:', data.status);
|
||
|
||
let targetAction = this.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||
if (!targetAction && data.preparing_id && this.preparingTools.has(data.preparing_id)) {
|
||
targetAction = this.preparingTools.get(data.preparing_id);
|
||
}
|
||
|
||
if (!targetAction) {
|
||
return;
|
||
}
|
||
|
||
if (data.status) {
|
||
targetAction.tool.status = data.status;
|
||
}
|
||
if (data.result !== undefined) {
|
||
targetAction.tool.result = data.result;
|
||
|
||
// 处理个性化设置工具 - 刷新个人空间数据
|
||
if (targetAction.tool && targetAction.tool.name === 'manage_personalization') {
|
||
let result = data.result;
|
||
if (typeof result === 'string') {
|
||
try {
|
||
result = JSON.parse(result);
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
// 处理主题变更
|
||
if (result?.theme_changed === true && result.new_theme) {
|
||
const theme = result.new_theme;
|
||
if (typeof window !== 'undefined' && window.localStorage) {
|
||
window.localStorage.setItem('agents_ui_theme', theme);
|
||
}
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
document.body.setAttribute('data-theme', theme);
|
||
}
|
||
// 任何字段更新后都刷新个人空间数据
|
||
(async () => {
|
||
try {
|
||
const { usePersonalizationStore } = await import('../../stores/personalization');
|
||
const personalizationStore = usePersonalizationStore();
|
||
// 强制刷新个人空间数据
|
||
await personalizationStore.fetchPersonalization();
|
||
} catch (e) {
|
||
// 静默处理,不影响主流程
|
||
console.debug('[TaskPolling] 刷新个人空间数据失败:', e);
|
||
}
|
||
})();
|
||
}
|
||
}
|
||
if (data.message !== undefined) {
|
||
targetAction.tool.message = data.message;
|
||
}
|
||
if (data.content !== undefined) {
|
||
targetAction.tool.content = data.content;
|
||
}
|
||
|
||
// 待办工具执行完成后主动刷新左侧待办列表(网页端不再依赖 websocket todo_updated)
|
||
if (data.status === 'completed') {
|
||
const toolName = String(targetAction?.tool?.name || '').toLowerCase();
|
||
if (
|
||
toolName.startsWith('todo_') ||
|
||
toolName === 'todo_create' ||
|
||
toolName === 'todo_update_task'
|
||
) {
|
||
this.scheduleTodoListRefresh(80);
|
||
}
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
|
||
if (this.monitorResolveTool && data.status === 'completed') {
|
||
this.monitorResolveTool(data);
|
||
}
|
||
},
|
||
|
||
handleToolApprovalRequired(data: any) {
|
||
const approval = data?.approval;
|
||
if (!approval || !approval.approval_id) {
|
||
return;
|
||
}
|
||
if (!Array.isArray(this.pendingToolApprovals)) {
|
||
this.pendingToolApprovals = [];
|
||
}
|
||
const idx = this.pendingToolApprovals.findIndex(
|
||
(item: any) => item && item.approval_id === approval.approval_id
|
||
);
|
||
if (idx >= 0) {
|
||
this.pendingToolApprovals.splice(idx, 1, approval);
|
||
} else {
|
||
this.pendingToolApprovals.push(approval);
|
||
}
|
||
this.rightCollapsed = false;
|
||
if (this.rightWidth < this.minPanelWidth) {
|
||
this.rightWidth = this.minPanelWidth;
|
||
}
|
||
if (this.isMobileViewport && this.activeMobileOverlay !== 'approval') {
|
||
this.openMobileOverlay('approval');
|
||
}
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
handleToolApprovalResolved(data: any) {
|
||
const approvalId = data?.approval_id;
|
||
if (!approvalId || !Array.isArray(this.pendingToolApprovals)) {
|
||
return;
|
||
}
|
||
this.pendingToolApprovals = this.pendingToolApprovals.filter(
|
||
(item: any) => item && item.approval_id !== approvalId
|
||
);
|
||
// 电脑端:审批完成后自动折叠面板
|
||
if (!this.pendingToolApprovals.length && !this.isMobileViewport) {
|
||
this.rightCollapsed = true;
|
||
}
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
handleAppendPayload(data: any) {
|
||
debugLog('[TaskPolling] 文件追加:', data.path);
|
||
this.chatAddAppendPayloadAction(data);
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
},
|
||
|
||
handleModifyPayload(data: any) {
|
||
debugLog('[TaskPolling] 文件修改:', data.path);
|
||
this.chatAddModifyPayloadAction(data);
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
},
|
||
|
||
markLatestUserWorkCompleted() {
|
||
if (!Array.isArray(this.messages) || this.messages.length === 0) {
|
||
return;
|
||
}
|
||
for (let i = this.messages.length - 1; i >= 0; i -= 1) {
|
||
const msg = this.messages[i];
|
||
if (!msg || msg.role !== 'user') {
|
||
continue;
|
||
}
|
||
msg.metadata = msg.metadata || {};
|
||
msg.metadata.work_timer = msg.metadata.work_timer || {};
|
||
const timer = msg.metadata.work_timer;
|
||
if (timer.status === 'completed' && typeof timer.duration_ms === 'number') {
|
||
return;
|
||
}
|
||
const nowIso = new Date().toISOString();
|
||
const startedAt = timer.started_at || msg.timestamp || nowIso;
|
||
const startMs = Date.parse(startedAt);
|
||
const endMs = Date.now();
|
||
const durationMs = Number.isFinite(startMs) ? Math.max(0, endMs - startMs) : 0;
|
||
timer.status = 'completed';
|
||
timer.started_at = startedAt;
|
||
timer.finished_at = nowIso;
|
||
timer.duration_ms = durationMs;
|
||
return;
|
||
}
|
||
},
|
||
|
||
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) {
|
||
debugLog('[TaskPolling] 任务完成,但仍有后台子智能体运行');
|
||
} else {
|
||
debugLog('[TaskPolling] 任务完成');
|
||
}
|
||
|
||
// 同步处理状态更新
|
||
this.streamingMessage = false;
|
||
this.stopRequested = false;
|
||
if (!hasRunningSubAgents) {
|
||
this.markLatestUserWorkCompleted();
|
||
}
|
||
|
||
if (hasRunningSubAgents) {
|
||
this.taskInProgress = true;
|
||
this.waitingForSubAgent = true;
|
||
this.waitingForBackgroundCommand = hasRunningBackgroundCommands;
|
||
// 关键修复:主任务结束后将切到新的通知任务,先清空旧任务事件索引去重表。
|
||
this.clearProcessedEvents();
|
||
this.startWaitingTaskProbe();
|
||
} else {
|
||
this.cleanupTrailingEmptyAssistantPlaceholder('task_complete');
|
||
// 主任务已结束:若有遗留工具块处于 running/preparing,会导致发送按钮继续显示“停止”。
|
||
// 这里统一清理遗留中的工具状态,避免前端忙碌态卡死。
|
||
if (typeof this.clearPendingTools === 'function') {
|
||
this.clearPendingTools('task_complete');
|
||
}
|
||
this.taskInProgress = false;
|
||
this.waitingForSubAgent = false;
|
||
this.waitingForBackgroundCommand = false;
|
||
this.stopWaitingTaskProbe();
|
||
this.clearTaskState(); // 清理任务状态
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
|
||
// 只更新统计,不重新加载历史
|
||
if (this.currentConversationId) {
|
||
setTimeout(() => {
|
||
this.fetchConversationTokenStatistics();
|
||
this.updateCurrentContextTokens();
|
||
}, 500);
|
||
}
|
||
this.scheduleTodoListRefresh(100);
|
||
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();
|
||
this.cleanupTrailingEmptyAssistantPlaceholder('task_stopped');
|
||
this.streamingMessage = false;
|
||
this.taskInProgress = false;
|
||
this.stopRequested = false;
|
||
this.waitingForSubAgent = false;
|
||
this.waitingForBackgroundCommand = false;
|
||
this.stopWaitingTaskProbe();
|
||
|
||
if (typeof this.clearPendingTools === 'function') {
|
||
this.clearPendingTools('task_stopped');
|
||
}
|
||
this.scheduleTodoListRefresh(100);
|
||
this.$forceUpdate();
|
||
|
||
this.clearTaskState();
|
||
jsonDebug('handleTaskStopped:after', {
|
||
taskInProgress: this.taskInProgress,
|
||
streamingMessage: this.streamingMessage,
|
||
stopRequested: this.stopRequested
|
||
});
|
||
},
|
||
|
||
// 新增统一清理方法
|
||
clearTaskState() {
|
||
this.stopWaitingTaskProbe();
|
||
(async () => {
|
||
const { useTaskStore } = await import('../../stores/task');
|
||
const taskStore = useTaskStore();
|
||
taskStore.clearTask(); // 使用 clearTask 而不是 stopPolling
|
||
})();
|
||
|
||
if (typeof this.clearProcessedEvents === 'function') {
|
||
this.clearProcessedEvents();
|
||
}
|
||
},
|
||
|
||
handleUserMessage(data: any) {
|
||
const message = (data?.message || data?.content || '').trim();
|
||
if (!message) {
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] handleUserMessage:empty', { data });
|
||
return;
|
||
}
|
||
debugLog('[TaskPolling] 收到用户消息事件');
|
||
debugNotifyLog('[DEBUG_NOTIFY][ui] handleUserMessage', {
|
||
messagePreview: message.slice(0, 120),
|
||
sub_agent_notice: !!data?.sub_agent_notice,
|
||
background_command_notice: !!data?.background_command_notice,
|
||
task_id: data?.task_id,
|
||
has_running_sub_agents: data?.has_running_sub_agents,
|
||
has_running_background_commands: data?.has_running_background_commands
|
||
});
|
||
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] handleUserMessage', {
|
||
messagePreview: message.slice(0, 80),
|
||
task_id: data?.task_id,
|
||
sub_agent_notice: !!data?.sub_agent_notice,
|
||
background_command_notice: !!data?.background_command_notice
|
||
});
|
||
const isAutoUserMessage = isSystemAutoUserMessagePayload(data);
|
||
if (!isAutoUserMessage) {
|
||
const incomingImages = Array.isArray(data?.images) ? data.images : [];
|
||
const incomingVideos = Array.isArray(data?.videos) ? data.videos : [];
|
||
const incomingMediaRefs = Array.isArray(data?.media_refs)
|
||
? data.media_refs
|
||
: Array.isArray(data?.mediaRefs)
|
||
? data.mediaRefs
|
||
: [];
|
||
const last = getOptimisticUserEchoTarget(this.messages || []);
|
||
const isLikelyOptimisticEcho = !!(
|
||
last &&
|
||
last.role === 'user' &&
|
||
String(last.content || '').trim() === message &&
|
||
JSON.stringify(last.images || []) === JSON.stringify(incomingImages || []) &&
|
||
JSON.stringify(last.videos || []) === JSON.stringify(incomingVideos || [])
|
||
);
|
||
if (isLikelyOptimisticEcho) {
|
||
last.media_refs = incomingMediaRefs;
|
||
last.metadata = {
|
||
...(last.metadata || {}),
|
||
media_refs: incomingMediaRefs
|
||
};
|
||
} else {
|
||
this.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs);
|
||
}
|
||
this.taskInProgress = true;
|
||
this.streamingMessage = false;
|
||
this.stopRequested = false;
|
||
} else {
|
||
debugLog('[TaskPolling] 跳过自动代发 user_message 渲染', {
|
||
messagePreview: message.slice(0, 80),
|
||
data
|
||
});
|
||
}
|
||
if (data?.sub_agent_notice) {
|
||
if (typeof data?.has_running_sub_agents === 'boolean') {
|
||
this.waitingForSubAgent = data.has_running_sub_agents;
|
||
} else if (typeof data?.remaining_count === 'number') {
|
||
this.waitingForSubAgent = data.remaining_count > 0;
|
||
}
|
||
if (typeof data?.has_running_background_commands === 'boolean') {
|
||
this.waitingForBackgroundCommand = data.has_running_background_commands;
|
||
} else if (data?.background_command_notice) {
|
||
this.waitingForBackgroundCommand = this.waitingForSubAgent;
|
||
}
|
||
if (this.waitingForSubAgent) {
|
||
this.startWaitingTaskProbe();
|
||
} else {
|
||
this.stopWaitingTaskProbe();
|
||
}
|
||
}
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
},
|
||
|
||
handleSystemMessage(data: any) {
|
||
const content = (data?.content || data?.message || '').trim();
|
||
userMDebug('taskPolling.handleSystemMessage:incoming', {
|
||
raw: data,
|
||
normalizedContent: content,
|
||
currentConversationId: this.currentConversationId,
|
||
currentMessagesLength: Array.isArray(this.messages) ? this.messages.length : -1
|
||
});
|
||
if (!content) {
|
||
userMDebug('taskPolling.handleSystemMessage:empty-skip');
|
||
return;
|
||
}
|
||
this.cleanupTrailingEmptyAssistantPlaceholder('before_system_message_append');
|
||
this.appendSystemAction(content);
|
||
userMDebug('taskPolling.handleSystemMessage:after-append', {
|
||
currentMessagesLength: Array.isArray(this.messages) ? this.messages.length : -1,
|
||
lastRole: this.messages?.[this.messages.length - 1]?.role || null,
|
||
lastActions:
|
||
this.messages?.[this.messages.length - 1]?.actions?.map((a: any) => a?.type) || []
|
||
});
|
||
},
|
||
|
||
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;
|
||
const attempt = Number(data?.attempt) || 1;
|
||
const maxAttempts = Number(data?.max_attempts) || attempt;
|
||
|
||
debugLog('[TaskPolling] API错误,等待自动重试', {
|
||
retryIn,
|
||
attempt,
|
||
maxAttempts,
|
||
message: data?.message
|
||
});
|
||
|
||
this.stopRequested = false;
|
||
this.taskInProgress = true;
|
||
this.streamingMessage = true;
|
||
this.$forceUpdate();
|
||
|
||
this.uiPushToast({
|
||
title: '即将重试',
|
||
message: `将在 ${retryIn} 秒后重试(第 ${attempt}/${maxAttempts} 次)`,
|
||
type: 'info',
|
||
duration: Math.max(retryIn, 1) * 1000
|
||
});
|
||
return;
|
||
}
|
||
|
||
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;
|
||
|
||
// 根据错误类型提供友好提示
|
||
if (errorType === 'api_error') {
|
||
title = 'API 调用失败';
|
||
message = `模型服务异常:${errorMessage}`;
|
||
} else if (errorType === 'timeout') {
|
||
title = '任务超时';
|
||
message = '任务执行时间过长,已自动停止';
|
||
} else if (errorType === 'quota_exceeded') {
|
||
title = '配额不足';
|
||
message = '您的使用配额已用尽';
|
||
}
|
||
|
||
console.error('[TaskPolling] 任务错误:', data.message);
|
||
this.uiPushToast({
|
||
title,
|
||
message,
|
||
type: 'error',
|
||
duration: 8000
|
||
});
|
||
|
||
// 清理状态
|
||
this.markLatestUserWorkCompleted();
|
||
this.streamingMessage = false;
|
||
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 () => {
|
||
const { useTaskStore } = await import('../../stores/task');
|
||
const taskStore = useTaskStore();
|
||
taskStore.stopPolling();
|
||
})();
|
||
},
|
||
|
||
handleTokenUpdate(data: any) {
|
||
if (data.conversation_id === this.currentConversationId) {
|
||
this.currentConversationTokens.cumulative_input_tokens = data.cumulative_input_tokens || 0;
|
||
this.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
|
||
this.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
|
||
|
||
if (typeof data.current_context_tokens === 'number') {
|
||
this.resourceSetCurrentContextTokens(data.current_context_tokens);
|
||
} else {
|
||
this.updateCurrentContextTokens();
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
|
||
handleConversationChanged(data: any, eventIdx: number) {
|
||
debugLog('[TaskPolling] 对话标题已更新, idx:', eventIdx, data);
|
||
|
||
if (data && data.conversation_id === this.currentConversationId) {
|
||
// 更新当前对话标题
|
||
if (data.title) {
|
||
this.currentConversationTitle = data.title;
|
||
debugLog('[TaskPolling] 更新当前对话标题:', data.title);
|
||
}
|
||
|
||
// 更新对话列表中的标题
|
||
if (data.title && Array.isArray(this.conversations)) {
|
||
const conv = this.conversations.find((c) => c && c.id === data.conversation_id);
|
||
if (conv) {
|
||
conv.title = data.title;
|
||
debugLog('[TaskPolling] 更新对话列表中的标题:', data.title);
|
||
}
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
|
||
handleConversationResolved(data: any) {
|
||
if (data && data.conversation_id) {
|
||
this.currentConversationId = data.conversation_id;
|
||
if (data.title) {
|
||
this.currentConversationTitle = data.title;
|
||
}
|
||
this.promoteConversationToTop(data.conversation_id);
|
||
|
||
const pathFragment = this.stripConversationPrefix(data.conversation_id);
|
||
const currentPath = window.location.pathname.replace(/^\/+/, '');
|
||
if (data.created) {
|
||
history.pushState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
|
||
} else if (currentPath !== pathFragment) {
|
||
history.replaceState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
|
||
}
|
||
}
|
||
},
|
||
|
||
handleCompressionState(data: any) {
|
||
if (!data || typeof data !== 'object') {
|
||
return;
|
||
}
|
||
const wasInProgress = !!this.compressionInProgress;
|
||
this.compressionInProgress = !!data.in_progress;
|
||
this.compressionMode = data.mode || '';
|
||
this.compressionStage = data.stage || '';
|
||
if (this.compressionInProgress && !wasInProgress) {
|
||
const modeLabel = this.compressionMode === 'manual' ? '手动' : '自动';
|
||
if (this.compressionToastId) {
|
||
this.uiDismissToast(this.compressionToastId);
|
||
this.compressionToastId = null;
|
||
}
|
||
this.compressionToastId = this.uiPushToast({
|
||
title: '压缩中',
|
||
message: `对话正在${modeLabel}压缩,请稍候…`,
|
||
type: 'info',
|
||
duration: null,
|
||
closable: false
|
||
});
|
||
}
|
||
if (!this.compressionInProgress) {
|
||
this.compressionError = data.error || '';
|
||
if (this.compressionToastId) {
|
||
this.uiDismissToast(this.compressionToastId);
|
||
this.compressionToastId = null;
|
||
}
|
||
}
|
||
},
|
||
|
||
async handleCompressionFinished(data: any) {
|
||
if (this.compressionToastId) {
|
||
this.uiDismissToast(this.compressionToastId);
|
||
this.compressionToastId = null;
|
||
}
|
||
this.compressionInProgress = false;
|
||
this.compressionMode = '';
|
||
this.compressionStage = '';
|
||
this.compressionError = '';
|
||
const newId = data?.conversation_id;
|
||
if (newId && newId !== this.currentConversationId) {
|
||
await this.loadConversation(newId, { force: true });
|
||
}
|
||
this.conversationsOffset = 0;
|
||
if (typeof this.loadConversationsList === 'function') {
|
||
await this.loadConversationsList();
|
||
}
|
||
this.uiPushToast({
|
||
title: '压缩完成',
|
||
message: '已自动切换到压缩后的新对话',
|
||
type: 'success',
|
||
duration: 2400
|
||
});
|
||
},
|
||
|
||
handleShallowCompression(data: any, eventIdx?: number) {
|
||
const count = Number(data?.compressed_count || 0);
|
||
if (count <= 0) {
|
||
return;
|
||
}
|
||
debugLog('[TaskPolling] 自动浅层压缩触发, idx:', eventIdx, data);
|
||
this.uiPushToast({
|
||
title: '自动浅层压缩',
|
||
message: `已自动压缩 ${count} 条较早工具结果`,
|
||
type: 'info',
|
||
duration: 2500
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 清理已处理的事件索引
|
||
*/
|
||
clearProcessedEvents() {
|
||
if (this._processedEventIndices) {
|
||
this._processedEventIndices.clear();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 恢复任务状态(页面刷新后调用)
|
||
*/
|
||
async restoreTaskState() {
|
||
// 清理已处理的事件索引
|
||
this.clearProcessedEvents();
|
||
restoreDebugLog('restore:start', {
|
||
currentConversationId: this.currentConversationId,
|
||
streamingMessage: this.streamingMessage,
|
||
taskInProgress: this.taskInProgress,
|
||
historyLoading: this.historyLoading,
|
||
historyLoadingFor: this.historyLoadingFor,
|
||
messagesLen: Array.isArray(this.messages) ? this.messages.length : -1
|
||
});
|
||
|
||
try {
|
||
const { useTaskStore } = await import('../../stores/task');
|
||
const taskStore = useTaskStore();
|
||
|
||
// 如果已经在流式输出中,不重复恢复
|
||
if (this.streamingMessage || this.taskInProgress) {
|
||
restoreDebugLog('restore:skip-already-running', {
|
||
streamingMessage: this.streamingMessage,
|
||
taskInProgress: this.taskInProgress,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
debugLog('[TaskPolling] 任务已在进行中,跳过恢复', {
|
||
streamingMessage: this.streamingMessage,
|
||
taskInProgress: this.taskInProgress,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 查找运行中的任务
|
||
const runningTask = await taskStore.loadRunningTask(this.currentConversationId);
|
||
|
||
if (!runningTask) {
|
||
restoreDebugLog('restore:no-running-task', {
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
debugLog('[TaskPolling] 没有运行中的任务', {
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
this._restoreTaskStateWaitCount = 0;
|
||
await this.restoreSubAgentWaitingState();
|
||
return;
|
||
}
|
||
|
||
debugLog('[TaskPolling] 发现运行中的任务,开始恢复状态', {
|
||
taskId: runningTask?.task_id,
|
||
status: runningTask?.status,
|
||
conversationId: runningTask?.conversation_id
|
||
});
|
||
restoreDebugLog('restore:running-task-found', {
|
||
taskId: runningTask?.task_id,
|
||
status: runningTask?.status,
|
||
taskConversationId: runningTask?.conversation_id,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
|
||
// 检查历史是否已加载
|
||
const hasMessages = Array.isArray(this.messages) && this.messages.length > 0;
|
||
const historyLoadingSameConversation =
|
||
!!this.historyLoading && this.historyLoadingFor === this.currentConversationId;
|
||
|
||
if (!hasMessages) {
|
||
this._restoreTaskStateWaitCount = (this._restoreTaskStateWaitCount || 0) + 1;
|
||
const waitedTooLong = this._restoreTaskStateWaitCount >= 8; // ~4s
|
||
restoreDebugLog('restore:history-empty', {
|
||
waitCount: this._restoreTaskStateWaitCount,
|
||
waitedTooLong,
|
||
historyLoading: this.historyLoading,
|
||
historyLoadingFor: this.historyLoadingFor,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
if (historyLoadingSameConversation && !waitedTooLong) {
|
||
debugLog('[TaskPolling] 历史未加载,等待历史加载完成', {
|
||
waitCount: this._restoreTaskStateWaitCount,
|
||
historyLoading: this.historyLoading,
|
||
historyLoadingFor: this.historyLoadingFor
|
||
});
|
||
setTimeout(() => {
|
||
this.restoreTaskState();
|
||
}, 500);
|
||
return;
|
||
}
|
||
// 兜底:即使历史未成功拉取,也允许直接通过任务事件重放恢复界面,
|
||
// 避免 show_html 流式阶段刷新后“本次请求内容完全消失”。
|
||
debugLog('[TaskPolling] 历史为空,启用事件重放兜底恢复', {
|
||
waitCount: this._restoreTaskStateWaitCount,
|
||
historyLoading: this.historyLoading,
|
||
historyLoadingFor: this.historyLoadingFor
|
||
});
|
||
} else {
|
||
this._restoreTaskStateWaitCount = 0;
|
||
}
|
||
|
||
debugLog('[TaskPolling] 开始精细恢复', {
|
||
hasMessages,
|
||
historyLoading: this.historyLoading,
|
||
historyLoadingFor: this.historyLoadingFor
|
||
});
|
||
|
||
// 获取任务的所有事件
|
||
const detailResponse = await fetch(`/api/tasks/${taskStore.currentTaskId}`);
|
||
if (!detailResponse.ok) {
|
||
debugLog('[TaskPolling] 获取任务详情失败');
|
||
return;
|
||
}
|
||
|
||
const detailResult = await detailResponse.json();
|
||
if (!detailResult.success || !detailResult.data.events) {
|
||
restoreDebugLog('restore:task-detail-invalid', {
|
||
ok: !!detailResult?.success,
|
||
hasData: !!detailResult?.data,
|
||
hasEvents: !!detailResult?.data?.events
|
||
});
|
||
debugLog('[TaskPolling] 任务详情无效');
|
||
return;
|
||
}
|
||
|
||
const allEvents = detailResult.data.events;
|
||
debugLog(`[TaskPolling] 获取到 ${allEvents.length} 个事件`);
|
||
restoreDebugLog('restore:task-detail-events', {
|
||
total: allEvents.length,
|
||
firstIdx: allEvents[0]?.idx,
|
||
firstType: allEvents[0]?.type,
|
||
lastIdx: allEvents[allEvents.length - 1]?.idx,
|
||
lastType: allEvents[allEvents.length - 1]?.type
|
||
});
|
||
|
||
// 找到最后一条消息
|
||
const lastMessage = this.messages[this.messages.length - 1];
|
||
const isAssistantMessage = lastMessage && lastMessage.role === 'assistant';
|
||
|
||
// console.log('[TaskPolling] 最后一条消息:', {
|
||
// exists: !!lastMessage,
|
||
// role: lastMessage?.role,
|
||
// actionsCount: lastMessage?.actions?.length || 0,
|
||
// isAssistant: isAssistantMessage
|
||
// });
|
||
|
||
// 先分析事件状态(用于判定是否强制重建)
|
||
let inThinking = false;
|
||
let inText = false;
|
||
let hasTextChunkEvent = false;
|
||
|
||
for (let i = 0; i < allEvents.length; i++) {
|
||
const event = allEvents[i];
|
||
if (event.type === 'thinking_start') {
|
||
inThinking = true;
|
||
} else if (event.type === 'thinking_end') {
|
||
inThinking = false;
|
||
}
|
||
if (event.type === 'text_start') {
|
||
inText = true;
|
||
} else if (event.type === 'text_end') {
|
||
inText = false;
|
||
}
|
||
if (event.type === 'text_chunk') {
|
||
hasTextChunkEvent = true;
|
||
}
|
||
}
|
||
|
||
// 检查是否需要从头重建
|
||
// 1. 最后一条不是 assistant 消息
|
||
// 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 forceRebuildOnRefresh = true;
|
||
|
||
const needsRebuild =
|
||
forceRebuildOnRefresh ||
|
||
!isAssistantMessage ||
|
||
(isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) ||
|
||
historyIncomplete ||
|
||
forceRebuildForStreamingText;
|
||
restoreDebugLog('restore:rebuild-decision', {
|
||
needsRebuild,
|
||
forceRebuildOnRefresh,
|
||
isAssistantMessage,
|
||
historyActionsCount,
|
||
eventCount,
|
||
historyIncomplete,
|
||
inText,
|
||
hasTextChunkEvent,
|
||
forceRebuildForStreamingText
|
||
});
|
||
|
||
if (needsRebuild) {
|
||
// if (historyIncomplete) {
|
||
// console.log('[TaskPolling] 历史不完整,从头重建:', {
|
||
// historyActionsCount,
|
||
// eventCount,
|
||
// diff: eventCount - historyActionsCount
|
||
// });
|
||
// }
|
||
debugLog('[TaskPolling] 需要从头重建 assistant 响应');
|
||
|
||
// 清空所有消息,准备从头重建
|
||
// 保留用户消息,只删除最后的 assistant 消息
|
||
if (isAssistantMessage) {
|
||
debugLog('[TaskPolling] 删除不完整的 assistant 消息');
|
||
this.messages.pop();
|
||
}
|
||
|
||
this.streamingMessage = true;
|
||
this.taskInProgress = true;
|
||
this.$forceUpdate();
|
||
|
||
// 重置偏移量为 0,从头获取所有事件来重建 assistant 消息
|
||
taskStore.lastEventIndex = 0;
|
||
debugLog('[TaskPolling] 重置偏移量为 0,从头开始轮询');
|
||
|
||
// 标记正在从头重建,用于后续处理
|
||
this._rebuildingFromScratch = true;
|
||
this._rebuildingEventCount = allEvents.length; // 记录当前事件总数
|
||
|
||
(window as any).__taskEventHandler = (event: any) => {
|
||
this.handleTaskEvent(event);
|
||
};
|
||
|
||
taskStore.startPolling((event: any) => {
|
||
this.handleTaskEvent(event);
|
||
});
|
||
restoreDebugLog('restore:rebuild-polling-started', {
|
||
lastEventIndex: taskStore.lastEventIndex,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
|
||
// 延迟清除重建标记,确保所有历史事件都处理完毕
|
||
setTimeout(() => {
|
||
debugLog('[TaskPolling] 历史事件处理完毕,清除重建标记');
|
||
this._rebuildingFromScratch = false;
|
||
this._rebuildingEventCount = 0;
|
||
}, 2000);
|
||
|
||
return;
|
||
}
|
||
|
||
// console.log('[TaskPolling] 分析结果:', {
|
||
// inThinking,
|
||
// inText,
|
||
// totalEvents: allEvents.length,
|
||
// lastEventType: allEvents[allEvents.length - 1]?.type,
|
||
// lastEventIdx: allEvents[allEvents.length - 1]?.idx
|
||
// });
|
||
|
||
// 恢复思考块状态
|
||
if (lastMessage.actions) {
|
||
// console.log('[TaskPolling] 历史中的 actions 详情:', lastMessage.actions.map((a, idx) => ({
|
||
// index: idx,
|
||
// type: a.type,
|
||
// id: a.id,
|
||
// hasContent: !!a.content,
|
||
// contentLength: a.content?.length || 0,
|
||
// toolName: a.tool?.name,
|
||
// hasBlockId: !!a.blockId,
|
||
// blockId: a.blockId,
|
||
// collapsed: a.collapsed,
|
||
// streaming: a.streaming
|
||
// })));
|
||
|
||
const thinkingActions = lastMessage.actions.filter((a) => a.type === 'thinking');
|
||
// console.log('[TaskPolling] 思考块数量:', thinkingActions.length);
|
||
|
||
if (inThinking && thinkingActions.length > 0) {
|
||
// 正在思考中,检查最后一个思考块是否正在流式输出
|
||
const lastThinking = thinkingActions[thinkingActions.length - 1];
|
||
|
||
// 只有当思考块正在流式输出时才设置锁定状态(但不展开)
|
||
if (lastThinking.streaming && lastThinking.blockId) {
|
||
// console.log('[TaskPolling] 找到正在流式输出的思考块,设置锁定状态:', lastThinking.blockId);
|
||
this.$nextTick(() => {
|
||
this.chatSetThinkingLock(lastThinking.blockId, true);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 确保所有思考块都是折叠状态
|
||
for (const thinking of thinkingActions) {
|
||
if (thinking.blockId) {
|
||
thinking.collapsed = true;
|
||
}
|
||
}
|
||
|
||
// 检查思考块状态(在设置之后)(已禁用)
|
||
// thinkingActions.forEach((thinking, idx) => {
|
||
// console.log(`[TaskPolling] 思考块 ${idx} (设置后):`, {
|
||
// hasBlockId: !!thinking.blockId,
|
||
// blockId: thinking.blockId,
|
||
// collapsed: thinking.collapsed,
|
||
// contentLength: thinking.content?.length || 0
|
||
// });
|
||
// });
|
||
|
||
// 恢复文本块状态
|
||
const textActions = lastMessage.actions.filter((a) => a.type === 'text');
|
||
// console.log('[TaskPolling] 文本块数量:', textActions.length);
|
||
|
||
if (inText && textActions.length > 0) {
|
||
const lastText = textActions[textActions.length - 1];
|
||
// console.log('[TaskPolling] 标记文本块为流式状态');
|
||
lastText.streaming = true;
|
||
}
|
||
|
||
// 注册历史中的工具块到 toolActionIndex
|
||
// 这样后续的 update_action 事件可以找到对应的块进行状态更新
|
||
const toolActions = lastMessage.actions.filter((a) => a.type === 'tool');
|
||
// console.log('[TaskPolling] 工具块数量:', toolActions.length);
|
||
|
||
for (const toolAction of toolActions) {
|
||
if (toolAction.tool && toolAction.tool.id) {
|
||
// console.log('[TaskPolling] 注册工具块:', {
|
||
// id: toolAction.tool.id,
|
||
// name: toolAction.tool.name,
|
||
// status: toolAction.tool.status
|
||
// });
|
||
// 注册到 toolActionIndex
|
||
this.toolRegisterAction(toolAction, toolAction.tool.id);
|
||
// 如果有 executionId,也注册
|
||
if (toolAction.tool.executionId) {
|
||
this.toolRegisterAction(toolAction, toolAction.tool.executionId);
|
||
}
|
||
// 追踪工具调用
|
||
if (toolAction.tool.name) {
|
||
this.toolTrackAction(toolAction.tool.name, toolAction);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 标记状态为进行中
|
||
this.streamingMessage = true;
|
||
this.taskInProgress = true;
|
||
|
||
// 设置 currentMessageIndex 指向最后一条 assistant 消息
|
||
// 这样后续添加的 action 会添加到正确的消息中
|
||
const lastMessageIndex = this.messages.length - 1;
|
||
if (lastMessage && lastMessage.role === 'assistant') {
|
||
this.currentMessageIndex = lastMessageIndex;
|
||
// console.log('[TaskPolling] 设置 currentMessageIndex 为:', lastMessageIndex);
|
||
}
|
||
|
||
// 强制更新界面
|
||
this.$forceUpdate();
|
||
|
||
// 滚动到底部
|
||
this.$nextTick(() => {
|
||
this.conditionalScrollToBottom();
|
||
});
|
||
|
||
// 注册事件处理器到全局
|
||
(window as any).__taskEventHandler = (event: any) => {
|
||
this.handleTaskEvent(event);
|
||
};
|
||
|
||
// 启动轮询(从当前偏移量开始,只处理新事件)
|
||
debugLog('[TaskPolling] 启动轮询,起始偏移量:', taskStore.lastEventIndex);
|
||
|
||
taskStore.startPolling((event: any) => {
|
||
this.handleTaskEvent(event);
|
||
});
|
||
restoreDebugLog('restore:polling-started-follow', {
|
||
lastEventIndex: taskStore.lastEventIndex,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
|
||
this.uiPushToast({
|
||
title: '任务恢复',
|
||
message: '检测到进行中的任务,已恢复连接',
|
||
type: 'info',
|
||
duration: 3000
|
||
});
|
||
} catch (error) {
|
||
restoreDebugLog('restore:error', {
|
||
message: error?.message || String(error)
|
||
});
|
||
console.error('[TaskPolling] 恢复任务状态失败:', error);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 恢复子智能体等待状态(页面刷新后调用)
|
||
*/
|
||
async restoreSubAgentWaitingState(retry = 0) {
|
||
try {
|
||
if (!this.currentConversationId) {
|
||
if (retry < 5) {
|
||
setTimeout(() => {
|
||
this.restoreSubAgentWaitingState(retry + 1);
|
||
}, 300);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const response = await fetch('/api/sub_agents');
|
||
if (!response.ok) {
|
||
debugLog('[TaskPolling] 获取子智能体状态失败');
|
||
return;
|
||
}
|
||
const result = await response.json();
|
||
if (!result.success) {
|
||
debugLog('[TaskPolling] 子智能体状态响应无效');
|
||
return;
|
||
}
|
||
|
||
const tasks = Array.isArray(result.data) ? result.data : [];
|
||
debugLog('[TaskPolling] 子智能体状态响应', {
|
||
total: tasks.length,
|
||
currentConversationId: this.currentConversationId,
|
||
tasks: tasks.map((task: any) => ({
|
||
task_id: task?.task_id,
|
||
status: task?.status,
|
||
run_in_background: task?.run_in_background,
|
||
notice_pending: task?.notice_pending,
|
||
conversation_id: task?.conversation_id
|
||
}))
|
||
});
|
||
const terminalStatuses = new Set(['completed', 'failed', 'timeout', 'terminated']);
|
||
const relevant = tasks.filter((task: any) => {
|
||
if (task && task.conversation_id && task.conversation_id !== this.currentConversationId) {
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
const running = relevant.filter(
|
||
(task: any) => task?.run_in_background && !terminalStatuses.has(task?.status)
|
||
);
|
||
const pendingNotice = relevant.filter((task: any) => task?.notice_pending);
|
||
|
||
if (running.length || pendingNotice.length) {
|
||
debugLog('[TaskPolling] 恢复子智能体等待状态', {
|
||
running: running.length,
|
||
pendingNotice: pendingNotice.length,
|
||
runningTasks: running.map((task: any) => ({
|
||
task_id: task?.task_id,
|
||
status: task?.status
|
||
})),
|
||
pendingTasks: pendingNotice.map((task: any) => ({
|
||
task_id: task?.task_id,
|
||
status: task?.status
|
||
}))
|
||
});
|
||
this.waitingForSubAgent = true;
|
||
this.waitingForBackgroundCommand = false;
|
||
this.taskInProgress = true;
|
||
this.streamingMessage = false;
|
||
this.stopRequested = false;
|
||
this.startWaitingTaskProbe();
|
||
this.$forceUpdate();
|
||
}
|
||
} catch (error) {
|
||
console.error('[TaskPolling] 恢复子智能体等待状态失败:', error);
|
||
}
|
||
}
|
||
};
|