[\s\S]*?<\/thinking>/g, '')
.trim();
} else {
textContent = textContent.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()
});
console.log('添加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()
});
console.log('添加modify占位信息:', modifyPayloadMeta.path);
}
if (textContent && !appendPayloadMeta && !modifyPayloadMeta) {
currentAssistantMessage.actions.push({
id: `history-text-${Date.now()}-${Math.random()}`,
type: 'text',
content: textContent,
streaming: false,
timestamp: Date.now()
});
console.log('添加文本内容:', 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 = {};
}
currentAssistantMessage.actions.push({
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
type: 'tool',
tool: {
id: toolCall.id,
name: toolCall.function.name,
arguments: arguments_obj,
status: 'preparing',
result: null
},
timestamp: Date.now()
});
console.log('添加工具调用:', 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) {
// 解析工具结果
let result;
try {
// 尝试解析为JSON
result = JSON.parse(message.content);
} catch (e) {
// 如果不是JSON,就作为纯文本
result = {
output: message.content,
success: true
};
}
toolAction.tool.status = 'completed';
toolAction.tool.result = result;
if (message.name === 'append_to_file' && result && result.message) {
toolAction.tool.message = result.message;
}
console.log(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
if (message.name === 'append_to_file' && result && typeof result === 'object') {
const appendSummary = {
path: result.path || '未知文件',
success: result.success !== false,
summary: result.message || (result.success === false ? '追加失败' : '追加完成'),
lines: result.lines || 0,
bytes: result.bytes || 0,
forced: !!result.forced
};
currentAssistantMessage.actions.push({
id: `history-append-${Date.now()}-${Math.random()}`,
type: 'append',
append: appendSummary,
timestamp: Date.now()
});
}
} else {
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
}
}
} else {
// 其他类型消息(如system)- 先结束当前assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
currentAssistantMessage = null;
}
console.log('处理其他类型消息:', message.role);
this.messages.push({
role: message.role,
content: message.content || ''
});
}
});
// 处理最后一个assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
}
console.log(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
// 强制更新视图
this.$forceUpdate();
// 确保滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
async createNewConversation() {
console.log('创建新对话...');
try {
const response = await fetch('/api/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
thinking_mode: this.thinkingMode
})
});
const result = await response.json();
if (result.success) {
console.log('新对话创建成功:', result.conversation_id);
// 清空当前消息
this.messages = [];
this.currentConversationId = result.conversation_id;
this.currentConversationTitle = '新对话';
history.pushState({ conversationId: this.currentConversationId }, '', `/${this.stripConversationPrefix(this.currentConversationId)}`);
// 重置Token统计
this.resetTokenStatistics();
// 重置状态
this.resetAllStates();
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
console.error('创建对话失败:', result.message);
alert(`创建对话失败: ${result.message}`);
}
} catch (error) {
console.error('创建对话异常:', error);
alert(`创建对话异常: ${error.message}`);
}
},
async deleteConversation(conversationId) {
if (!confirm('确定要删除这个对话吗?删除后无法恢复。')) {
return;
}
console.log('删除对话:', conversationId);
try {
const response = await fetch(`/api/conversations/${conversationId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
console.log('对话删除成功');
// 如果删除的是当前对话,清空界面
if (conversationId === this.currentConversationId) {
this.messages = [];
this.currentConversationId = null;
this.currentConversationTitle = '';
this.resetAllStates();
this.resetTokenStatistics();
history.replaceState({}, '', '/new');
}
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
console.error('删除对话失败:', result.message);
alert(`删除对话失败: ${result.message}`);
}
} catch (error) {
console.error('删除对话异常:', error);
alert(`删除对话异常: ${error.message}`);
}
},
async duplicateConversation(conversationId) {
console.log('复制对话:', conversationId);
try {
const response = await fetch(`/api/conversations/${conversationId}/duplicate`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
const newId = result.duplicate_conversation_id;
if (newId) {
this.currentConversationId = newId;
}
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
const message = result.message || result.error || '复制失败';
alert(`复制失败: ${message}`);
}
} catch (error) {
console.error('复制对话异常:', error);
alert(`复制对话异常: ${error.message}`);
}
},
searchConversations() {
// 简单的搜索功能,实际实现可以调用搜索API
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
this.searchTimer = setTimeout(() => {
if (this.searchQuery.trim()) {
console.log('搜索对话:', this.searchQuery);
// TODO: 实现搜索API调用
// this.searchConversationsAPI(this.searchQuery);
} else {
// 清空搜索,重新加载全部对话
this.conversationsOffset = 0;
this.loadConversationsList();
}
}, 300);
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
async toggleThinkingMode() {
try {
const resp = await fetch('/api/thinking-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ thinking_mode: !this.thinkingMode })
});
const data = await resp.json();
if (data.success && data.data) {
if (typeof data.data === 'object') {
this.thinkingMode = !!data.data.enabled;
} else {
this.thinkingMode = data.data === '思考模式';
}
} else {
throw new Error(data.message || data.error || '切换失败');
}
} catch (error) {
console.error('切换思考模式失败:', error);
alert(`切换思考模式失败: ${error.message}`);
}
},
formatTime(timeString) {
if (!timeString) return '';
const date = new Date(timeString);
const now = new Date();
const diffMs = now - date;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) {
return '刚刚';
} else if (diffHours < 24) {
return `${diffHours}小时前`;
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else {
return date.toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric'
});
}
},
// ==========================================
// 原有功能保持不变
// ==========================================
updateFileTree(structure) {
const treeDictionary = structure && structure.tree ? structure.tree : {};
const buildNodes = (treeMap) => {
if (!treeMap) {
return [];
}
const entries = Object.keys(treeMap).map((name) => {
const node = treeMap[name] || {};
if (node.type === 'folder') {
return {
type: 'folder',
name,
path: node.path || name,
children: buildNodes(node.children)
};
}
return {
type: 'file',
name,
path: node.path || name,
annotation: node.annotation || ''
};
});
entries.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name, 'zh-CN');
});
return entries;
};
const nodes = buildNodes(treeDictionary);
const expanded = { ...this.expandedFolders };
const validFolderPaths = new Set();
const ensureExpansion = (list, depth = 0) => {
list.forEach((item) => {
if (item.type === 'folder') {
validFolderPaths.add(item.path);
if (expanded[item.path] === undefined) {
expanded[item.path] = false;
}
ensureExpansion(item.children || [], depth + 1);
}
});
};
ensureExpansion(nodes);
Object.keys(expanded).forEach((path) => {
if (!validFolderPaths.has(path)) {
delete expanded[path];
}
});
this.expandedFolders = expanded;
this.fileTree = nodes;
},
toggleFolder(path) {
this.hideContextMenu();
if (!path) {
return;
}
const current = !!this.expandedFolders[path];
this.expandedFolders = {
...this.expandedFolders,
[path]: !current
};
},
cycleSidebarPanel() {
const order = ['files', 'todo', 'subAgents'];
const nextIndex = (order.indexOf(this.panelMode) + 1) % order.length;
this.panelMode = order[nextIndex];
if (this.panelMode === 'subAgents') {
this.fetchSubAgents();
}
},
togglePanelMenu() {
this.panelMenuOpen = !this.panelMenuOpen;
},
selectPanelMode(mode) {
if (this.panelMode === mode) {
this.panelMenuOpen = false;
return;
}
this.panelMode = mode;
this.panelMenuOpen = false;
if (mode === 'todo') {
this.fetchTodoList();
} else if (mode === 'subAgents') {
this.fetchSubAgents();
}
},
formatTaskStatus(task) {
if (!task) {
return '';
}
return task.status === 'done'
? `${this.todoDoneEmoji} 完成`
: `${this.todoPendingEmoji} 未完成`;
},
toolCategoryEmoji(categoryId) {
return this.toolCategoryEmojis[categoryId] || '⚙️';
},
async fetchSubAgents() {
try {
const resp = await fetch('/api/sub_agents');
if (!resp.ok) {
throw new Error(await resp.text());
}
const data = await resp.json();
if (data.success) {
this.subAgents = Array.isArray(data.data) ? data.data : [];
}
} catch (error) {
console.error('获取子智能体列表失败:', error);
}
},
openSubAgent(agent) {
if (!agent || !agent.task_id) {
return;
}
const { protocol, hostname } = window.location;
const base = `${protocol}//${hostname}:8092`;
const parentConv = agent.conversation_id || this.currentConversationId || '';
const convSegment = this.stripConversationPrefix(parentConv);
const agentLabel = agent.agent_id ? `sub_agent${agent.agent_id}` : agent.task_id;
const pathSuffix = convSegment
? `/${convSegment}+${agentLabel}`
: `/sub_agent/${agent.task_id}`;
const url = `${base}${pathSuffix}`;
window.open(url, '_blank');
},
async fetchTodoList() {
try {
const response = await fetch('/api/todo-list');
const data = await response.json();
if (data.success) {
this.todoList = data.data || null;
}
} catch (error) {
console.error('获取待办列表失败:', error);
}
},
triggerFileUpload() {
if (this.uploading) {
return;
}
const input = this.$refs.fileUploadInput;
if (input) {
input.click();
}
},
handleFileSelected(event) {
const fileInput = event?.target;
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
return;
}
const [file] = fileInput.files;
this.uploadSelectedFile(file);
},
resetFileInput() {
const input = this.$refs.fileUploadInput;
if (input) {
input.value = '';
}
},
async uploadSelectedFile(file) {
if (!file || this.uploading) {
this.resetFileInput();
return;
}
this.uploading = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name || '');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
let result = {};
try {
result = await response.json();
} catch (parseError) {
throw new Error('服务器响应无法解析');
}
if (!response.ok || !result.success) {
const message = result.error || result.message || '上传失败';
throw new Error(message);
}
await this.refreshFileTree();
alert(`上传成功:${result.path || file.name}`);
} catch (error) {
console.error('文件上传失败:', error);
alert(`文件上传失败:${error.message}`);
} finally {
this.uploading = false;
this.resetFileInput();
}
},
handleSendOrStop() {
if (this.streamingMessage) {
this.stopTask();
} else {
this.sendMessage();
}
},
sendMessage() {
if (this.streamingMessage || !this.isConnected) {
return;
}
if (!this.inputMessage.trim()) {
return;
}
const message = this.inputMessage;
if (message.startsWith('/')) {
this.socket.emit('send_command', { command: message });
this.inputMessage = '';
this.settingsOpen = false;
return;
}
this.messages.push({
role: 'user',
content: message
});
this.currentMessageIndex = -1;
this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId });
this.inputMessage = '';
this.autoScrollEnabled = true;
this.scrollToBottom();
this.settingsOpen = false;
// 发送消息后延迟更新当前上下文Token(关键修复:恢复原逻辑)
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
},
// 新增:停止任务方法
stopTask() {
if (this.streamingMessage && !this.stopRequested) {
this.socket.emit('stop_task');
this.stopRequested = true;
console.log('发送停止请求');
}
this.settingsOpen = false;
},
clearChat() {
if (confirm('确定要清除所有对话记录吗?')) {
this.socket.emit('send_command', { command: '/clear' });
}
this.settingsOpen = false;
},
async compressConversation() {
if (!this.currentConversationId) {
alert('当前没有可压缩的对话。');
return;
}
if (this.compressing) {
return;
}
const confirmed = confirm('确定要压缩当前对话记录吗?压缩后会生成新的对话副本。');
if (!confirmed) {
return;
}
this.settingsOpen = false;
this.compressing = true;
try {
const response = await fetch(`/api/conversations/${this.currentConversationId}/compress`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
const newId = result.compressed_conversation_id;
if (newId) {
this.currentConversationId = newId;
}
console.log('对话压缩完成:', result);
} else {
const message = result.message || result.error || '压缩失败';
alert(`压缩失败: ${message}`);
}
} catch (error) {
console.error('压缩对话异常:', error);
alert(`压缩对话异常: ${error.message}`);
} finally {
this.compressing = false;
}
},
toggleToolMenu() {
if (!this.isConnected) {
return;
}
const nextState = !this.toolMenuOpen;
this.toolMenuOpen = nextState;
if (nextState) {
this.settingsOpen = false;
this.loadToolSettings();
}
},
handleClickOutsideToolMenu(event) {
if (!this.toolMenuOpen) {
return;
}
const dropdown = this.$refs.toolDropdown;
if (dropdown && !dropdown.contains(event.target)) {
this.toolMenuOpen = false;
}
},
handleClickOutsidePanelMenu(event) {
if (!this.panelMenuOpen) {
return;
}
const wrapper = this.$refs.panelMenuWrapper;
if (wrapper && wrapper.contains(event.target)) {
return;
}
this.panelMenuOpen = false;
},
applyToolSettingsSnapshot(categories) {
if (!Array.isArray(categories)) {
return;
}
this.toolSettings = categories.map((item) => ({
id: item.id,
label: item.label || item.id,
enabled: !!item.enabled,
tools: Array.isArray(item.tools) ? item.tools : []
}));
this.toolSettingsLoading = false;
},
async loadToolSettings(force = false) {
if (!this.isConnected) {
return;
}
if (this.toolSettingsLoading) {
return;
}
if (!force && this.toolSettings.length > 0) {
return;
}
this.toolSettingsLoading = true;
try {
const response = await fetch('/api/tool-settings');
const data = await response.json();
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn('获取工具设置失败:', data);
this.toolSettingsLoading = false;
}
} catch (error) {
console.error('获取工具设置异常:', error);
this.toolSettingsLoading = false;
}
},
async updateToolCategory(categoryId, enabled) {
if (!this.isConnected) {
return;
}
if (this.toolSettingsLoading) {
return;
}
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
this.toolSettings = this.toolSettings.map((item) => {
if (item.id === categoryId) {
return { ...item, enabled };
}
return item;
});
try {
const response = await fetch('/api/tool-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
category: categoryId,
enabled
})
});
const data = await response.json();
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn('更新工具设置失败:', data);
this.toolSettings = previousSnapshot;
}
} catch (error) {
console.error('更新工具设置异常:', error);
this.toolSettings = previousSnapshot;
}
this.toolSettingsLoading = false;
},
toggleSettings() {
if (!this.isConnected) {
return;
}
this.settingsOpen = !this.settingsOpen;
if (this.settingsOpen) {
this.toolMenuOpen = false;
}
},
toggleFocusPanel() {
this.rightCollapsed = !this.rightCollapsed;
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
this.settingsOpen = false;
},
handleClickOutsideSettings(event) {
if (!this.settingsOpen) {
return;
}
const dropdown = this.$refs.settingsDropdown;
if (dropdown && !dropdown.contains(event.target)) {
this.settingsOpen = false;
}
},
addSystemMessage(content) {
this.appendSystemAction(content);
},
appendSystemAction(content) {
const msg = this.ensureAssistantMessage();
msg.actions.push({
id: `system-${Date.now()}-${Math.random()}`,
type: 'system',
content: content,
timestamp: Date.now()
});
this.$forceUpdate();
this.conditionalScrollToBottom();
},
toggleBlock(id) {
if (this.expandedBlocks.has(id)) {
this.expandedBlocks.delete(id);
} else {
this.expandedBlocks.add(id);
}
this.$forceUpdate();
},
// 修复:工具相关方法 - 接收tool对象而不是name
getToolIcon(tool) {
const toolName = typeof tool === 'string' ? tool : tool.name;
const icons = {
'create_file': '📄',
'sleep': '⏱️',
'read_file': '📖',
'delete_file': '🗑️',
'rename_file': '✏️',
'modify_file': '✏️',
'append_to_file': '✏️',
'create_folder': '📁',
'focus_file': '👁️',
'unfocus_file': '👁️',
'web_search': '🔍',
'extract_webpage': '🌐',
'save_webpage': '💾',
'run_python': '🐍',
'run_command': '$',
'update_memory': '🧠',
'terminal_session': '💻',
'terminal_input': '⌨️',
'terminal_snapshot': '📋',
'terminal_reset': '♻️',
'todo_create': '🗒️',
'todo_update_task': '☑️',
'todo_finish': '🏁',
'todo_finish_confirm': '❗',
'create_sub_agent': '🤖',
'wait_sub_agent': '⏳',
'close_sub_agent': '🛑'
};
return icons[toolName] || '⚙️';
},
getToolAnimationClass(tool) {
// 根据工具状态返回不同的动画类
if (tool.status === 'hinted') {
return 'hint-animation pulse-slow';
} else if (tool.status === 'preparing') {
return 'preparing-animation';
} else if (tool.status === 'running') {
const animations = {
'create_file': 'file-animation',
'read_file': 'read-animation',
'delete_file': 'file-animation',
'rename_file': 'file-animation',
'modify_file': 'file-animation',
'append_to_file': 'file-animation',
'create_folder': 'file-animation',
'focus_file': 'focus-animation',
'unfocus_file': 'focus-animation',
'web_search': 'search-animation',
'extract_webpage': 'search-animation',
'save_webpage': 'file-animation',
'run_python': 'code-animation',
'run_command': 'terminal-animation',
'update_memory': 'memory-animation',
'sleep': 'wait-animation',
'terminal_session': 'terminal-animation',
'terminal_input': 'terminal-animation',
'terminal_snapshot': 'terminal-animation',
'terminal_reset': 'terminal-animation',
'todo_create': 'file-animation',
'todo_update_task': 'file-animation',
'todo_finish': 'file-animation',
'todo_finish_confirm': 'file-animation',
'create_sub_agent': 'terminal-animation',
'wait_sub_agent': 'wait-animation'
};
return animations[tool.name] || 'default-animation';
}
return '';
},
// 修复:获取工具状态文本
getToolStatusText(tool) {
// 优先使用自定义消息
if (tool.message) {
return tool.message;
}
if (tool.status === 'hinted') {
return `可能需要 ${tool.name}...`;
} else if (tool.status === 'preparing') {
return `准备调用 ${tool.name}...`;
} else if (tool.status === 'running') {
const texts = {
'create_file': '正在创建文件...',
'sleep': '正在等待...',
'delete_file': '正在删除文件...',
'rename_file': '正在重命名文件...',
'modify_file': '正在修改文件...',
'append_to_file': '正在追加文件...',
'create_folder': '正在创建文件夹...',
'focus_file': '正在聚焦文件...',
'unfocus_file': '正在取消聚焦...',
'web_search': '正在搜索网络...',
'extract_webpage': '正在提取网页...',
'save_webpage': '正在保存网页...',
'run_python': '正在执行Python代码...',
'run_command': '正在执行命令...',
'update_memory': '正在更新记忆...',
'terminal_session': '正在管理终端会话...',
'terminal_input': '正在发送终端输入...',
'terminal_snapshot': '正在获取终端快照...',
'terminal_reset': '正在重置终端...'
};
if (tool.name === 'read_file') {
const readType = ((tool.argumentSnapshot && tool.argumentSnapshot.type) ||
(tool.arguments && tool.arguments.type) || 'read').toLowerCase();
const runningMap = {
'read': '正在读取文件...',
'search': '正在执行搜索...',
'extract': '正在提取内容...'
};
return runningMap[readType] || '正在读取文件...';
}
return texts[tool.name] || '正在执行...';
} else if (tool.status === 'completed') {
// 修复:完成状态的文本
const texts = {
'create_file': '文件创建成功',
'delete_file': '文件删除成功',
'sleep': '等待完成',
'rename_file': '文件重命名成功',
'modify_file': '文件修改成功',
'append_to_file': '文件追加完成',
'create_folder': '文件夹创建成功',
'focus_file': '文件聚焦成功',
'unfocus_file': '取消聚焦成功',
'web_search': '搜索完成',
'extract_webpage': '网页提取完成',
'save_webpage': '网页保存完成(纯文本)',
'run_python': '代码执行完成',
'run_command': '命令执行完成',
'update_memory': '记忆更新成功',
'terminal_session': '终端操作完成',
'terminal_input': '终端输入完成',
'terminal_snapshot': '终端快照已返回',
'terminal_reset': '终端已重置'
};
if (tool.name === 'read_file' && tool.result && typeof tool.result === 'object') {
const readType = (tool.result.type || 'read').toLowerCase();
if (readType === 'search') {
const query = tool.result.query ? `「${tool.result.query}」` : '';
const count = typeof tool.result.returned_matches === 'number'
? tool.result.returned_matches
: (tool.result.actual_matches || 0);
return `搜索${query},得到${count}个结果`;
} else if (readType === 'extract') {
const segments = Array.isArray(tool.result.segments) ? tool.result.segments : [];
const totalLines = segments.reduce((sum, seg) => {
const start = Number(seg.line_start) || 0;
const end = Number(seg.line_end) || 0;
if (!start || !end || end < start) return sum;
return sum + (end - start + 1);
}, 0);
const displayLines = totalLines || tool.result.char_count || 0;
return `提取了${displayLines}行`;
}
return '文件读取完成';
}
return texts[tool.name] || '执行完成';
} else {
// 其他状态
return `${tool.name} - ${tool.status}`;
}
},
getToolDescription(tool) {
const args = tool.argumentSnapshot || tool.arguments;
const argumentLabel = tool.argumentLabel || this.buildToolLabel(args);
if (argumentLabel) {
return argumentLabel;
}
if (tool.statusDetail) {
return tool.statusDetail;
}
if (tool.result && typeof tool.result === 'object') {
if (tool.result.path) {
return tool.result.path.split('/').pop();
}
}
return '';
},
cloneToolArguments(args) {
if (!args || typeof args !== 'object') {
return null;
}
try {
return JSON.parse(JSON.stringify(args));
} catch (error) {
console.warn('无法克隆工具参数:', error);
return { ...args };
}
},
buildToolLabel(args) {
if (!args || typeof args !== 'object') {
return '';
}
if (args.command) {
return args.command;
}
if (args.path) {
return args.path.split('/').pop();
}
if (args.target_path) {
return args.target_path.split('/').pop();
}
if (args.query) {
return `"${args.query}"`;
}
if (args.seconds !== undefined) {
return `${args.seconds} 秒`;
}
if (args.name) {
return args.name;
}
return '';
},
formatSearchTopic(filters) {
const mapping = {
'general': '通用',
'news': '新闻',
'finance': '金融'
};
const topic = (filters && filters.topic) ? String(filters.topic).toLowerCase() : 'general';
return mapping[topic] || '通用';
},
formatSearchTime(filters) {
if (!filters) {
return '未限定时间';
}
if (filters.time_range) {
const mapping = {
'day': '过去24小时',
'week': '过去7天',
'month': '过去30天',
'year': '过去365天'
};
return mapping[filters.time_range] || `相对范围:${filters.time_range}`;
}
if (typeof filters.days === 'number') {
return `过去${filters.days}天`;
}
if (filters.start_date && filters.end_date) {
return `${filters.start_date} 至 ${filters.end_date}`;
}
return '未限定时间';
},
renderMarkdown(text, isStreaming = false) {
if (!text) return '';
if (typeof marked === 'undefined') {
return text;
}
marked.setOptions({
breaks: true,
gfm: true,
sanitize: false
});
if (!isStreaming) {
if (!this.markdownCache) {
this.markdownCache = new Map();
}
const cacheKey = `${text.length}_${text.substring(0, 100)}`;
if (this.markdownCache.has(cacheKey)) {
return this.markdownCache.get(cacheKey);
}
}
let html = marked.parse(text);
html = this.wrapCodeBlocks(html, isStreaming);
if (!isStreaming && text.length < 10000) {
if (!this.markdownCache) {
this.markdownCache = new Map();
}
this.markdownCache.set(`${text.length}_${text.substring(0, 100)}`, html);
if (this.markdownCache.size > 20) {
const firstKey = this.markdownCache.keys().next().value;
this.markdownCache.delete(firstKey);
}
}
// 只在非流式状态处理(流式状态由renderLatexInRealtime处理)
if (!isStreaming) {
setTimeout(() => {
// 代码高亮
if (typeof Prism !== 'undefined') {
const codeBlocks = document.querySelectorAll('.code-block-wrapper pre code:not([data-highlighted])');
codeBlocks.forEach(block => {
try {
Prism.highlightElement(block);
block.setAttribute('data-highlighted', 'true');
} catch (e) {
console.warn('代码高亮失败:', e);
}
});
}
// LaTeX最终渲染
if (typeof renderMathInElement !== 'undefined') {
const elements = document.querySelectorAll('.text-output .text-content:not(.streaming-text)');
elements.forEach(element => {
if (element.hasAttribute('data-math-rendered')) return;
try {
renderMathInElement(element, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\[', right: '\\]', display: true},
{left: '\\(', right: '\\)', display: false}
],
throwOnError: false,
trust: true
});
element.setAttribute('data-math-rendered', 'true');
} catch (e) {
console.warn('LaTeX渲染失败:', e);
}
});
}
}, 100);
}
return html;
},
// 实时LaTeX渲染(用于流式输出)
renderLatexInRealtime() {
if (typeof renderMathInElement === 'undefined') {
return;
}
// 使用requestAnimationFrame优化性能
if (this._latexRenderTimer) {
cancelAnimationFrame(this._latexRenderTimer);
}
this._latexRenderTimer = requestAnimationFrame(() => {
const elements = document.querySelectorAll('.text-output .streaming-text');
elements.forEach(element => {
try {
renderMathInElement(element, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\[', right: '\\]', display: true},
{left: '\\(', right: '\\)', display: false}
],
throwOnError: false,
trust: true
});
} catch (e) {
// 忽略错误,继续渲染
}
});
});
},
// 用字符串替换包装代码块
// 用字符串替换包装代码块 - 添加streaming参数
wrapCodeBlocks(html, isStreaming = false) {
// 如果是流式输出,不包装代码块,保持原样
if (isStreaming) {
return html;
}
let counter = 0;
// 匹配 ...
return html.replace(/]*)>([\s\S]*?)<\/code><\/pre>/g, (match, attributes, content) => {
// 提取语言
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
const language = langMatch ? langMatch[1] : 'text';
// 生成唯一ID
const blockId = `code-${Date.now()}-${counter++}`;
// 转义引号用于data属性
const escapedContent = content
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
// 构建新的HTML,保持code元素原样
return `
`;
});
},
getLanguageClass(path) {
const ext = path.split('.').pop().toLowerCase();
const langMap = {
'py': 'language-python',
'js': 'language-javascript',
'html': 'language-html',
'css': 'language-css',
'json': 'language-json',
'md': 'language-markdown',
'txt': 'language-plain'
};
return langMap[ext] || 'language-plain';
},
scrollToBottom() {
setTimeout(() => {
const messagesArea = this.$refs.messagesArea;
if (messagesArea) {
// 标记为程序触发的滚动
if (this._setScrollingFlag) {
this._setScrollingFlag(true);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
// 滚动完成后重置标记
setTimeout(() => {
if (this._setScrollingFlag) {
this._setScrollingFlag(false);
}
}, 100);
}
}, 50);
},
conditionalScrollToBottom() {
// 严格检查:只在明确允许时才滚动
if (this.autoScrollEnabled === true && this.userScrolling === false) {
this.scrollToBottom();
}
},
toggleScrollLock() {
const currentlyLocked = this.autoScrollEnabled && !this.userScrolling;
if (currentlyLocked) {
this.autoScrollEnabled = false;
this.userScrolling = true;
} else {
this.autoScrollEnabled = true;
this.userScrolling = false;
this.scrollToBottom();
}
},
// 面板调整方法
startResize(panel, event) {
this.isResizing = true;
this.resizingPanel = panel;
if (panel === 'right' && this.rightCollapsed) {
this.rightCollapsed = false;
if (this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
}
document.addEventListener('mousemove', this.handleResize);
document.addEventListener('mouseup', this.stopResize);
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
event.preventDefault();
},
handleResize(event) {
if (!this.isResizing) return;
const containerWidth = document.querySelector('.main-container').offsetWidth;
if (this.resizingPanel === 'left') {
let newWidth = event.clientX - (this.sidebarCollapsed ? 60 : 300);
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.leftWidth = newWidth;
} else if (this.resizingPanel === 'right') {
let newWidth = containerWidth - event.clientX;
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.rightWidth = newWidth;
} else if (this.resizingPanel === 'conversation') {
// 对话侧边栏宽度调整
let newWidth = event.clientX;
newWidth = Math.max(200, Math.min(newWidth, 400));
// 这里可以动态调整对话侧边栏宽度,暂时不实现
}
},
stopResize() {
this.isResizing = false;
this.resizingPanel = null;
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.stopResize);
document.body.style.userSelect = '';
document.body.style.cursor = '';
},
// 格式化token显示(修复NaN问题)
formatTokenCount(tokens) {
// 确保tokens是数字,防止NaN
const num = Number(tokens) || 0;
if (num < 1000) {
return num.toString();
} else if (num < 1000000) {
return (num / 1000).toFixed(1) + 'K';
} else {
return (num / 1000000).toFixed(1) + 'M';
}
}
}
});
app.component('file-node', {
name: 'FileNode',
props: {
node: {
type: Object,
required: true
},
level: {
type: Number,
default: 0
},
expandedFolders: {
type: Object,
required: true
}
},
emits: ['toggle-folder', 'context-menu'],
computed: {
isExpanded() {
if (this.node.type !== 'folder') {
return false;
}
const value = this.expandedFolders[this.node.path];
return value === undefined ? true : value;
},
folderPadding() {
return {
paddingLeft: `${12 + this.level * 16}px`
};
},
filePadding() {
return {
paddingLeft: `${40 + this.level * 16}px`
};
}
},
methods: {
toggle() {
if (this.node.type === 'folder') {
this.$emit('toggle-folder', this.node.path);
}
}
},
template: `
📄
{{ node.name }}
{{ node.annotation }}
`
});
app.mount('#app');
console.log('Vue应用初始化完成');
}
window.addEventListener('load', () => {
bootstrapApp();
});