1263 lines
47 KiB
TypeScript
1263 lines
47 KiB
TypeScript
// @ts-nocheck
|
||
import { debugLog } from './common';
|
||
|
||
/**
|
||
* 任务轮询事件处理器
|
||
* 将从 REST API 轮询获取的事件转换为前端状态更新
|
||
*/
|
||
export const taskPollingMethods = {
|
||
/**
|
||
* 处理任务事件(从轮询获取)
|
||
*/
|
||
handleTaskEvent(event: any) {
|
||
if (!event || !event.type) {
|
||
return;
|
||
}
|
||
|
||
const eventType = event.type;
|
||
const eventData = event.data || {};
|
||
const eventIdx = event.idx;
|
||
|
||
// 事件去重检查
|
||
if (typeof eventIdx === 'number') {
|
||
if (!this._processedEventIndices) {
|
||
this._processedEventIndices = new Set();
|
||
}
|
||
|
||
if (this._processedEventIndices.has(eventIdx)) {
|
||
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 是否匹配当前对话
|
||
// 如果不匹配,忽略该事件(避免切换对话后旧任务的事件显示到新对话中)
|
||
if (eventData.conversation_id && this.currentConversationId) {
|
||
if (eventData.conversation_id !== 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 '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 'user_message':
|
||
this.handleUserMessage(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. 且该消息正在streaming或者没有任何内容(说明是刚创建的)
|
||
// 3. 且不是历史加载完成的消息(历史消息的awaitingFirstContent应该是false)
|
||
const isRefreshRestore = hasAssistantMessage &&
|
||
(this.streamingMessage || !lastMessage.actions || lastMessage.actions.length === 0) &&
|
||
(lastMessage.awaitingFirstContent === true || this.taskInProgress);
|
||
|
||
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.scrollToBottom();
|
||
});
|
||
},
|
||
|
||
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.scrollToBottom();
|
||
this.chatSetThinkingLock(blockId, true);
|
||
|
||
// 强制触发 actions 数组的响应式更新
|
||
if (lastMessage && lastMessage.actions) {
|
||
lastMessage.actions = [...lastMessage.actions];
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
|
||
handleThinkingChunk(data: any, eventIdx: number) {
|
||
if (this.runMode === 'fast' || this.thinkingMode === false) {
|
||
return;
|
||
}
|
||
const thinkingAction = this.chatAppendThinkingChunk(data.content);
|
||
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(data: any) {
|
||
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) {
|
||
this.chatAppendTextChunk(data.content);
|
||
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 (data.message !== undefined) {
|
||
targetAction.tool.message = data.message;
|
||
}
|
||
if (data.content !== undefined) {
|
||
targetAction.tool.content = data.content;
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
|
||
if (this.monitorResolveTool && data.status === 'completed') {
|
||
this.monitorResolveTool(data);
|
||
}
|
||
},
|
||
|
||
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 hasRunningSubAgents = !!data?.has_running_sub_agents;
|
||
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;
|
||
} else {
|
||
this.taskInProgress = false;
|
||
this.waitingForSubAgent = false;
|
||
this.clearTaskState(); // 清理任务状态
|
||
}
|
||
|
||
this.$forceUpdate();
|
||
|
||
// 只更新统计,不重新加载历史
|
||
if (this.currentConversationId) {
|
||
setTimeout(() => {
|
||
this.fetchConversationTokenStatistics();
|
||
this.updateCurrentContextTokens();
|
||
}, 500);
|
||
}
|
||
},
|
||
|
||
handleTaskStopped(data: any, eventIdx: number) {
|
||
debugLog('[TaskPolling] 任务已停止, idx:', eventIdx, data);
|
||
|
||
this.markLatestUserWorkCompleted();
|
||
this.streamingMessage = false;
|
||
this.taskInProgress = false;
|
||
this.stopRequested = false;
|
||
this.waitingForSubAgent = false;
|
||
|
||
if (typeof this.clearPendingTools === 'function') {
|
||
this.clearPendingTools('task_stopped');
|
||
}
|
||
this.$forceUpdate();
|
||
|
||
this.clearTaskState();
|
||
},
|
||
|
||
// 新增统一清理方法
|
||
clearTaskState() {
|
||
(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) {
|
||
return;
|
||
}
|
||
debugLog('[TaskPolling] 收到用户消息事件');
|
||
this.chatAddUserMessage(message, data?.images || [], data?.videos || []);
|
||
this.taskInProgress = true;
|
||
this.streamingMessage = false;
|
||
this.stopRequested = false;
|
||
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;
|
||
}
|
||
}
|
||
this.$forceUpdate();
|
||
this.conditionalScrollToBottom();
|
||
},
|
||
|
||
handleTaskError(data: any) {
|
||
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';
|
||
|
||
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();
|
||
|
||
// 停止轮询
|
||
(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}`);
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 清理已处理的事件索引
|
||
*/
|
||
clearProcessedEvents() {
|
||
if (this._processedEventIndices) {
|
||
this._processedEventIndices.clear();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 恢复任务状态(页面刷新后调用)
|
||
*/
|
||
async restoreTaskState() {
|
||
// 清理已处理的事件索引
|
||
this.clearProcessedEvents();
|
||
|
||
try {
|
||
const { useTaskStore } = await import('../../stores/task');
|
||
const taskStore = useTaskStore();
|
||
|
||
// 如果已经在流式输出中,不重复恢复
|
||
if (this.streamingMessage || this.taskInProgress) {
|
||
debugLog('[TaskPolling] 任务已在进行中,跳过恢复', {
|
||
streamingMessage: this.streamingMessage,
|
||
taskInProgress: this.taskInProgress,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 查找运行中的任务
|
||
const runningTask = await taskStore.loadRunningTask(this.currentConversationId);
|
||
|
||
if (!runningTask) {
|
||
debugLog('[TaskPolling] 没有运行中的任务', {
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
await this.restoreSubAgentWaitingState();
|
||
return;
|
||
}
|
||
|
||
debugLog('[TaskPolling] 发现运行中的任务,开始恢复状态', {
|
||
taskId: runningTask?.task_id,
|
||
status: runningTask?.status,
|
||
conversationId: runningTask?.conversation_id
|
||
});
|
||
|
||
// 检查历史是否已加载
|
||
const hasMessages = Array.isArray(this.messages) && this.messages.length > 0;
|
||
|
||
if (!hasMessages) {
|
||
debugLog('[TaskPolling] 历史未加载,等待历史加载完成');
|
||
setTimeout(() => {
|
||
this.restoreTaskState();
|
||
}, 500);
|
||
return;
|
||
}
|
||
|
||
debugLog('[TaskPolling] 历史已加载,开始精细恢复');
|
||
|
||
// 获取任务的所有事件
|
||
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) {
|
||
debugLog('[TaskPolling] 任务详情无效');
|
||
return;
|
||
}
|
||
|
||
const allEvents = detailResult.data.events;
|
||
debugLog(`[TaskPolling] 获取到 ${allEvents.length} 个事件`);
|
||
|
||
// 找到最后一条消息
|
||
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
|
||
// });
|
||
|
||
// 检查是否需要从头重建
|
||
// 1. 最后一条不是 assistant 消息
|
||
// 2. 最后一条是空的 assistant 消息
|
||
// 3. 事件数量远大于历史中的 actions 数量(说明历史不完整)
|
||
const historyActionsCount = lastMessage?.actions?.length || 0;
|
||
const eventCount = allEvents.length;
|
||
const historyIncomplete = eventCount > historyActionsCount + 5; // 允许5个事件的误差
|
||
|
||
const needsRebuild = !isAssistantMessage ||
|
||
(isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) ||
|
||
historyIncomplete;
|
||
|
||
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);
|
||
});
|
||
|
||
// 延迟清除重建标记,确保所有历史事件都处理完毕
|
||
setTimeout(() => {
|
||
debugLog('[TaskPolling] 历史事件处理完毕,清除重建标记');
|
||
this._rebuildingFromScratch = false;
|
||
this._rebuildingEventCount = 0;
|
||
}, 2000);
|
||
|
||
return;
|
||
}
|
||
|
||
// 分析事件,找到当前正在进行的操作
|
||
let inThinking = false;
|
||
let inText = 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;
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
});
|
||
|
||
this.uiPushToast({
|
||
title: '任务恢复',
|
||
message: '检测到进行中的任务,已恢复连接',
|
||
type: 'info',
|
||
duration: 3000
|
||
});
|
||
} catch (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.taskInProgress = true;
|
||
this.streamingMessage = false;
|
||
this.stopRequested = false;
|
||
this.$forceUpdate();
|
||
}
|
||
} catch (error) {
|
||
console.error('[TaskPolling] 恢复子智能体等待状态失败:', error);
|
||
}
|
||
},
|
||
};
|