542 lines
21 KiB
TypeScript
542 lines
21 KiB
TypeScript
// @ts-nocheck
|
||
import { debugLog } from './common';
|
||
const jsonDebug = (...args: any[]) => {
|
||
console.log('[JSONDEBUG]', ...args);
|
||
};
|
||
const RESTORE_DEBUG_PREFIX = '[RESTORE_DEBUG]';
|
||
const RESTORE_DEBUG_EVENTS = new Set([
|
||
'history:fetch:start',
|
||
'history:fetch:response',
|
||
'history:render:done'
|
||
]);
|
||
|
||
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;
|
||
console.log(RESTORE_DEBUG_PREFIX, event, payload);
|
||
}
|
||
|
||
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
||
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
|
||
|
||
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
||
const content = (rawContent || '').toString().trim();
|
||
if (!content) return null;
|
||
const match = content.match(SUB_AGENT_DONE_PREFIX_RE);
|
||
if (!match || !/已完成/.test(content)) return null;
|
||
return `子智能体${match[1]} 任务完成`;
|
||
}
|
||
|
||
function parseBackgroundRunCommandDoneLabel(rawContent: any): string | null {
|
||
const content = (rawContent || '').toString().trim();
|
||
if (!content) return null;
|
||
if (!BG_RUN_COMMAND_DONE_PREFIX_RE.test(content)) return null;
|
||
return '后台 run_command 完成';
|
||
}
|
||
|
||
function parseSystemNoticeLabel(rawContent: any): string | null {
|
||
return parseSubAgentDoneLabel(rawContent) || parseBackgroundRunCommandDoneLabel(rawContent);
|
||
}
|
||
|
||
function isSystemAutoUserMessageMeta(meta: any): boolean {
|
||
if (!meta || typeof meta !== 'object') {
|
||
return false;
|
||
}
|
||
return !!(
|
||
meta.is_auto_generated ||
|
||
meta.auto_message_type ||
|
||
meta.sub_agent_notice ||
|
||
meta.background_command_notice
|
||
);
|
||
}
|
||
|
||
export const historyMethods = {
|
||
// ==========================================
|
||
// 关键功能:获取并显示历史对话内容
|
||
// ==========================================
|
||
async fetchAndDisplayHistory(options = {}) {
|
||
const { force = false } = options as { force?: boolean };
|
||
const targetConversationId = this.currentConversationId;
|
||
if (!targetConversationId || targetConversationId.startsWith('temp_')) {
|
||
debugLog('没有当前对话ID,跳过历史加载');
|
||
this.refreshBlankHeroState();
|
||
return;
|
||
}
|
||
|
||
// 若同一对话正在加载,直接复用;若是切换对话则允许并发但后来的请求会赢
|
||
if (this.historyLoading && this.historyLoadingFor === targetConversationId) {
|
||
debugLog('同一对话历史正在加载,跳过重复请求');
|
||
return;
|
||
}
|
||
|
||
// 已经有完整历史且非强制刷新时,避免重复加载导致动画播放两次
|
||
const alreadyHydrated =
|
||
!force &&
|
||
this.lastHistoryLoadedConversationId === targetConversationId &&
|
||
Array.isArray(this.messages) &&
|
||
this.messages.length > 0;
|
||
if (alreadyHydrated) {
|
||
debugLog('历史已加载,跳过重复请求');
|
||
this.logMessageState('fetchAndDisplayHistory:skip-duplicate', {
|
||
conversationId: targetConversationId
|
||
});
|
||
return;
|
||
}
|
||
|
||
const loadSeq = ++this.historyLoadSeq;
|
||
restoreDebugLog('history:fetch:start', {
|
||
loadSeq,
|
||
targetConversationId,
|
||
currentConversationId: this.currentConversationId,
|
||
force
|
||
});
|
||
jsonDebug('history.fetch:start', {
|
||
loadSeq,
|
||
force,
|
||
targetConversationId,
|
||
currentConversationId: this.currentConversationId,
|
||
currentMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1
|
||
});
|
||
this.historyLoading = true;
|
||
this.historyLoadingFor = targetConversationId;
|
||
try {
|
||
debugLog('开始获取历史对话内容...');
|
||
this.logMessageState('fetchAndDisplayHistory:start', {
|
||
conversationId: this.currentConversationId
|
||
});
|
||
|
||
try {
|
||
// 使用专门的API获取对话消息历史
|
||
const messagesResponse = await fetch(`/api/conversations/${targetConversationId}/messages`);
|
||
|
||
if (!messagesResponse.ok) {
|
||
console.warn('无法获取消息历史,尝试备用方法');
|
||
// 备用方案:通过状态API获取
|
||
const statusResponse = await fetch('/api/status');
|
||
const status = await statusResponse.json();
|
||
debugLog('系统状态:', status);
|
||
this.applyStatusSnapshot(status);
|
||
|
||
// 如果状态中有对话历史字段
|
||
if (status.conversation_history && Array.isArray(status.conversation_history)) {
|
||
this.renderHistoryMessages(status.conversation_history);
|
||
return;
|
||
}
|
||
|
||
debugLog('备用方案也无法获取历史消息');
|
||
return;
|
||
}
|
||
|
||
// 如果在等待期间用户已切换到其他对话,则丢弃结果
|
||
if (
|
||
loadSeq !== this.historyLoadSeq ||
|
||
this.currentConversationId !== targetConversationId
|
||
) {
|
||
debugLog('检测到对话已切换,丢弃过期的历史加载结果');
|
||
return;
|
||
}
|
||
|
||
const messagesData = await messagesResponse.json();
|
||
const rawMessages = Array.isArray(messagesData?.data?.messages) ? messagesData.data.messages : [];
|
||
const lastAssistantRaw = [...rawMessages].reverse().find((m: any) => m?.role === 'assistant');
|
||
restoreDebugLog('history:fetch:response', {
|
||
loadSeq,
|
||
success: !!messagesData?.success,
|
||
rawCount: rawMessages.length,
|
||
responseConversationId: messagesData?.data?.conversation_id,
|
||
lastAssistantContentLength: (lastAssistantRaw?.content || '').length,
|
||
lastAssistantReasoningLength: (lastAssistantRaw?.reasoning_content || '').length,
|
||
lastAssistantHasShowHtml: /<show_html/i.test(lastAssistantRaw?.content || '')
|
||
});
|
||
jsonDebug('history.fetch:response', {
|
||
loadSeq,
|
||
success: !!messagesData?.success,
|
||
responseConversationId: messagesData?.data?.conversation_id,
|
||
messagesCount: Array.isArray(messagesData?.data?.messages)
|
||
? messagesData.data.messages.length
|
||
: -1
|
||
});
|
||
debugLog('获取到消息数据:', messagesData);
|
||
|
||
if (messagesData.success && messagesData.data && messagesData.data.messages) {
|
||
const messages = messagesData.data.messages;
|
||
debugLog(`发现 ${messages.length} 条历史消息`);
|
||
|
||
if (messages.length > 0) {
|
||
// 清空当前显示的消息
|
||
this.logMessageState('fetchAndDisplayHistory:before-clear-existing');
|
||
this.messages = [];
|
||
this.logMessageState('fetchAndDisplayHistory:after-clear-existing');
|
||
|
||
// 渲染历史消息 - 这是关键功能
|
||
this.renderHistoryMessages(messages);
|
||
jsonDebug('history.fetch:rendered', {
|
||
loadSeq,
|
||
renderedMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1,
|
||
lastRole: this.messages?.[this.messages.length - 1]?.role || null
|
||
});
|
||
|
||
// 滚动到底部
|
||
this.$nextTick(() => {
|
||
this.scrollToBottom();
|
||
});
|
||
|
||
debugLog('历史对话内容显示完成');
|
||
} else {
|
||
debugLog('对话存在但没有历史消息');
|
||
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
|
||
this.messages = [];
|
||
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
|
||
}
|
||
} else {
|
||
debugLog('消息数据格式不正确:', messagesData);
|
||
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
|
||
this.messages = [];
|
||
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取历史对话失败:', error);
|
||
debugLog('尝试不显示错误弹窗,仅在控制台记录');
|
||
// 不显示alert,避免打断用户体验
|
||
this.logMessageState('fetchAndDisplayHistory:error-clear', {
|
||
error: error?.message || String(error)
|
||
});
|
||
this.messages = [];
|
||
this.logMessageState('fetchAndDisplayHistory:error-cleared');
|
||
}
|
||
} finally {
|
||
// 仅在本次加载仍是最新请求时清除 loading 状态
|
||
if (loadSeq === this.historyLoadSeq) {
|
||
this.historyLoading = false;
|
||
this.historyLoadingFor = null;
|
||
}
|
||
this.refreshBlankHeroState();
|
||
}
|
||
},
|
||
|
||
// ==========================================
|
||
// 关键功能:渲染历史消息
|
||
// ==========================================
|
||
renderHistoryMessages(historyMessages) {
|
||
debugLog('开始渲染历史消息...', historyMessages);
|
||
debugLog('历史消息数量:', historyMessages.length);
|
||
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
|
||
|
||
if (!Array.isArray(historyMessages)) {
|
||
console.error('历史消息不是数组格式');
|
||
return;
|
||
}
|
||
|
||
let currentAssistantMessage = null;
|
||
let historyHasImages = false;
|
||
let historyHasVideos = false;
|
||
let toolArgsParseFailCount = 0;
|
||
let toolResultParseFailCount = 0;
|
||
|
||
historyMessages.forEach((message, index) => {
|
||
debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
|
||
const meta = message.metadata || {};
|
||
if (message.role === 'user' && (meta.system_injected_image || meta.system_injected_video)) {
|
||
debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)');
|
||
return;
|
||
}
|
||
if (message.role === 'user' && isSystemAutoUserMessageMeta(meta)) {
|
||
debugLog('跳过系统自动代发的 user 消息(前端不渲染)', {
|
||
auto_message_type: meta.auto_message_type || null,
|
||
sub_agent_notice: !!meta.sub_agent_notice,
|
||
background_command_notice: !!meta.background_command_notice
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (message.role === 'user') {
|
||
// 用户消息 - 先结束之前的assistant消息
|
||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||
this.messages.push(currentAssistantMessage);
|
||
currentAssistantMessage = null;
|
||
}
|
||
const images = message.images || (message.metadata && message.metadata.images) || [];
|
||
const videos = message.videos || (message.metadata && message.metadata.videos) || [];
|
||
if (Array.isArray(images) && images.length) {
|
||
historyHasImages = true;
|
||
}
|
||
if (Array.isArray(videos) && videos.length) {
|
||
historyHasVideos = true;
|
||
}
|
||
this.messages.push({
|
||
role: 'user',
|
||
content: message.content || '',
|
||
images,
|
||
videos,
|
||
metadata: message.metadata || {}
|
||
});
|
||
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
|
||
} else if (message.role === 'assistant') {
|
||
// AI消息 - 如果没有当前assistant消息,创建一个
|
||
if (!currentAssistantMessage) {
|
||
currentAssistantMessage = {
|
||
role: 'assistant',
|
||
actions: [],
|
||
streamingThinking: '',
|
||
streamingText: '',
|
||
currentStreamingType: null,
|
||
activeThinkingId: null,
|
||
awaitingFirstContent: false, // 历史消息不应该显示等待动画
|
||
generatingLabel: ''
|
||
};
|
||
}
|
||
|
||
const content = message.content || '';
|
||
const reasoningText = (message.reasoning_content || '').trim();
|
||
|
||
if (reasoningText) {
|
||
const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||
currentAssistantMessage.actions.push({
|
||
id: `history-think-${Date.now()}-${Math.random()}`,
|
||
type: 'thinking',
|
||
content: reasoningText,
|
||
streaming: false,
|
||
collapsed: true,
|
||
timestamp: Date.now(),
|
||
blockId
|
||
});
|
||
debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...');
|
||
}
|
||
|
||
// 处理普通文本内容(移除思考标签后的内容)
|
||
const textContent = content.trim();
|
||
if (textContent) {
|
||
currentAssistantMessage.actions.push({
|
||
id: `history-text-${Date.now()}-${Math.random()}`,
|
||
type: 'text',
|
||
content: textContent,
|
||
streaming: false,
|
||
timestamp: Date.now()
|
||
});
|
||
debugLog('添加文本内容:', textContent.substring(0, 50) + '...');
|
||
}
|
||
|
||
// 处理工具调用
|
||
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
||
message.tool_calls.forEach((toolCall, tcIndex) => {
|
||
let arguments_obj = {};
|
||
try {
|
||
arguments_obj =
|
||
typeof toolCall.function.arguments === 'string'
|
||
? JSON.parse(toolCall.function.arguments || '{}')
|
||
: toolCall.function.arguments || {};
|
||
} catch (e) {
|
||
console.warn('解析工具参数失败:', e);
|
||
arguments_obj = {};
|
||
toolArgsParseFailCount += 1;
|
||
jsonDebug('history.render:tool-args-parse-fail', {
|
||
toolName: toolCall?.function?.name,
|
||
toolCallId: toolCall?.id,
|
||
error: e?.message || String(e)
|
||
});
|
||
}
|
||
|
||
const action = {
|
||
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
|
||
type: 'tool',
|
||
tool: {
|
||
id: toolCall.id,
|
||
name: toolCall.function.name,
|
||
arguments: arguments_obj,
|
||
argumentSnapshot: this.cloneToolArguments(arguments_obj),
|
||
argumentLabel: this.buildToolLabel(arguments_obj),
|
||
intent_full: arguments_obj.intent || '',
|
||
intent_rendered: arguments_obj.intent || '',
|
||
status: 'preparing',
|
||
result: null
|
||
},
|
||
timestamp: Date.now()
|
||
};
|
||
// 如果是历史加载的动作且状态仍为进行中,标记为 stale,避免刷新后按钮卡死
|
||
if (['preparing', 'running', 'awaiting_content'].includes(action.tool.status)) {
|
||
action.tool.status = 'stale';
|
||
action.tool.awaiting_content = false;
|
||
action.streaming = false;
|
||
}
|
||
currentAssistantMessage.actions.push(action);
|
||
debugLog('添加工具调用:', toolCall.function.name);
|
||
});
|
||
}
|
||
} else if (message.role === 'tool') {
|
||
// 工具结果 - 更新当前assistant消息中对应的工具
|
||
if (currentAssistantMessage) {
|
||
// 查找对应的工具action - 使用更灵活的匹配
|
||
let toolAction = null;
|
||
|
||
// 优先按tool_call_id匹配
|
||
if (message.tool_call_id) {
|
||
toolAction = currentAssistantMessage.actions.find(
|
||
(action) => action.type === 'tool' && action.tool.id === message.tool_call_id
|
||
);
|
||
}
|
||
|
||
// 如果找不到,按name匹配最后一个同名工具
|
||
if (!toolAction && message.name) {
|
||
const sameNameTools = currentAssistantMessage.actions.filter(
|
||
(action) => action.type === 'tool' && action.tool.name === message.name
|
||
);
|
||
toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个
|
||
}
|
||
|
||
if (toolAction) {
|
||
// 解析工具结果(优先使用JSON,其次使用元数据的 tool_payload,以保证搜索结果在刷新后仍可展示)
|
||
let result;
|
||
try {
|
||
result = JSON.parse(message.content);
|
||
} catch (e) {
|
||
toolResultParseFailCount += 1;
|
||
jsonDebug('history.render:tool-result-parse-fail', {
|
||
toolName: message?.name,
|
||
toolCallId: message?.tool_call_id,
|
||
error: e?.message || String(e)
|
||
});
|
||
if (message.metadata && message.metadata.tool_payload) {
|
||
result = message.metadata.tool_payload;
|
||
} else {
|
||
result = {
|
||
output: message.content,
|
||
success: true
|
||
};
|
||
}
|
||
}
|
||
|
||
toolAction.tool.status = 'completed';
|
||
toolAction.tool.result = result;
|
||
if (result && typeof result === 'object') {
|
||
if (result.error) {
|
||
toolAction.tool.message = result.error;
|
||
} else if (result.message && !toolAction.tool.message) {
|
||
toolAction.tool.message = result.message;
|
||
}
|
||
}
|
||
debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
|
||
} else {
|
||
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
|
||
}
|
||
}
|
||
} else {
|
||
// 其他类型消息(如system)- 先结束当前assistant消息
|
||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||
this.messages.push(currentAssistantMessage);
|
||
currentAssistantMessage = null;
|
||
}
|
||
if (message.role === 'system') {
|
||
const rawContent = message.content || '';
|
||
const label = parseSystemNoticeLabel(rawContent);
|
||
console.log('[DEBUG_SYSTEM][history] 命中 role=system 历史消息', {
|
||
rawContent,
|
||
matchedDoneNotice: !!label
|
||
});
|
||
if (label) {
|
||
// 历史中的 system 通知转换为 assistant system action,避免被 role=system 过滤掉
|
||
this.messages.push({
|
||
role: 'assistant',
|
||
actions: [
|
||
{
|
||
id: `history-system-${Date.now()}-${Math.random()}`,
|
||
type: 'system',
|
||
content: label,
|
||
variant: 'sub_agent_done',
|
||
streaming: false,
|
||
timestamp: Date.now()
|
||
}
|
||
],
|
||
streamingThinking: '',
|
||
streamingText: '',
|
||
currentStreamingType: null,
|
||
activeThinkingId: null,
|
||
awaitingFirstContent: false,
|
||
generatingLabel: ''
|
||
});
|
||
} else {
|
||
console.log('[DEBUG_SYSTEM][history] role=system 被历史加载拦截(非子智能体完成)', {
|
||
rawContent
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
debugLog('处理其他类型消息:', message.role);
|
||
this.messages.push({
|
||
role: message.role,
|
||
content: message.content || ''
|
||
});
|
||
}
|
||
});
|
||
|
||
// 处理最后一个assistant消息
|
||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||
this.messages.push(currentAssistantMessage);
|
||
}
|
||
|
||
this.conversationHasImages = historyHasImages;
|
||
this.conversationHasVideos = historyHasVideos;
|
||
|
||
debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
|
||
const lastRendered = this.messages?.[this.messages.length - 1] || null;
|
||
const lastRenderedActions = Array.isArray(lastRendered?.actions) ? lastRendered.actions : [];
|
||
const lastRenderedText = lastRenderedActions
|
||
.filter((a: any) => a?.type === 'text')
|
||
.map((a: any) => String(a?.content || ''))
|
||
.join('\n');
|
||
restoreDebugLog('history:render:done', {
|
||
currentConversationId: this.currentConversationId,
|
||
historyInputCount: Array.isArray(historyMessages) ? historyMessages.length : -1,
|
||
finalMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1,
|
||
lastRole: lastRendered?.role || null,
|
||
lastActionsCount: lastRenderedActions.length,
|
||
lastHasThinkingAction: lastRenderedActions.some((a: any) => a?.type === 'thinking'),
|
||
lastHasTextAction: lastRenderedActions.some((a: any) => a?.type === 'text'),
|
||
lastTextLength: lastRenderedText.length,
|
||
lastTextHasShowHtml: /<show_html/i.test(lastRenderedText)
|
||
});
|
||
jsonDebug('history.render:done', {
|
||
historyInputCount: Array.isArray(historyMessages) ? historyMessages.length : -1,
|
||
finalMessagesCount: Array.isArray(this.messages) ? this.messages.length : -1,
|
||
lastRole: this.messages?.[this.messages.length - 1]?.role || null,
|
||
toolArgsParseFailCount,
|
||
toolResultParseFailCount
|
||
});
|
||
this.logMessageState('renderHistoryMessages:after-render');
|
||
this.lastHistoryLoadedConversationId = this.currentConversationId || null;
|
||
|
||
// 强制更新视图
|
||
this.$forceUpdate();
|
||
|
||
// 确保滚动到底部
|
||
this.$nextTick(() => {
|
||
this.scrollToBottom();
|
||
setTimeout(() => {
|
||
const blockCount =
|
||
this.$el && this.$el.querySelectorAll
|
||
? this.$el.querySelectorAll('.message-block').length
|
||
: 'N/A';
|
||
debugLog('[Messages] DOM 渲染统计', {
|
||
blocks: blockCount,
|
||
conversationId: this.currentConversationId
|
||
});
|
||
}, 0);
|
||
});
|
||
}
|
||
};
|