- 在历史加载时给思考块设置 collapsed: true 默认折叠 - 在任务恢复时跳过思考块的展开/折叠动画 - 延迟清除 _rebuildingFromScratch 标记,确保所有历史事件处理完毕 - 删除过早清除重建标记的逻辑,避免后续思考块被展开 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
400 lines
19 KiB
TypeScript
400 lines
19 KiB
TypeScript
// @ts-nocheck
|
||
import { debugLog } from './common';
|
||
|
||
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;
|
||
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();
|
||
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);
|
||
|
||
// 滚动到底部
|
||
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;
|
||
|
||
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') {
|
||
// 用户消息 - 先结束之前的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
|
||
});
|
||
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 metadata = message.metadata || {};
|
||
const appendPayloadMeta = metadata.append_payload;
|
||
const modifyPayloadMeta = metadata.modify_payload;
|
||
const isAppendMessage = message.name === 'append_to_file';
|
||
const isModifyMessage = message.name === 'modify_file';
|
||
const containsAppendMarkers = /<<<\s*(APPEND|MODIFY)/i.test(content || '') || /<<<END_\s*(APPEND|MODIFY)>>>/i.test(content || '');
|
||
|
||
const textContent = content.trim();
|
||
|
||
if (appendPayloadMeta) {
|
||
currentAssistantMessage.actions.push({
|
||
id: `history-append-payload-${Date.now()}-${Math.random()}`,
|
||
type: 'append_payload',
|
||
append: {
|
||
path: appendPayloadMeta.path || '未知文件',
|
||
forced: !!appendPayloadMeta.forced,
|
||
success: appendPayloadMeta.success === undefined ? true : !!appendPayloadMeta.success,
|
||
lines: appendPayloadMeta.lines ?? null,
|
||
bytes: appendPayloadMeta.bytes ?? null
|
||
},
|
||
timestamp: Date.now()
|
||
});
|
||
debugLog('添加append占位信息:', appendPayloadMeta.path);
|
||
} else if (modifyPayloadMeta) {
|
||
currentAssistantMessage.actions.push({
|
||
id: `history-modify-payload-${Date.now()}-${Math.random()}`,
|
||
type: 'modify_payload',
|
||
modify: {
|
||
path: modifyPayloadMeta.path || '未知文件',
|
||
total: modifyPayloadMeta.total_blocks ?? null,
|
||
completed: modifyPayloadMeta.completed || [],
|
||
failed: modifyPayloadMeta.failed || [],
|
||
forced: !!modifyPayloadMeta.forced,
|
||
details: modifyPayloadMeta.details || []
|
||
},
|
||
timestamp: Date.now()
|
||
});
|
||
debugLog('添加modify占位信息:', modifyPayloadMeta.path);
|
||
}
|
||
|
||
if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) {
|
||
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 = {};
|
||
}
|
||
|
||
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) {
|
||
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;
|
||
}
|
||
}
|
||
if (message.name === 'append_to_file' && result && result.message) {
|
||
toolAction.tool.message = result.message;
|
||
}
|
||
debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
|
||
|
||
// append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复
|
||
} else {
|
||
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
|
||
}
|
||
}
|
||
|
||
} else {
|
||
// 其他类型消息(如system)- 先结束当前assistant消息
|
||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||
this.messages.push(currentAssistantMessage);
|
||
currentAssistantMessage = null;
|
||
}
|
||
|
||
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} 条消息`);
|
||
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);
|
||
});
|
||
}
|
||
};
|