agent-Specialization/static/src/app/methods/taskPolling.ts

2082 lines
70 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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);
}
}
};