// static/app-enhanced.js - 修复版,正确实现Token实时更新 function updateViewportHeightVar() { const docEl = document.documentElement; const visualViewport = window.visualViewport; if (visualViewport) { const vh = visualViewport.height; const bottomInset = Math.max( 0, (window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop ); docEl.style.setProperty('--app-viewport', `${vh}px`); docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`); } else { const height = window.innerHeight || docEl.clientHeight; if (height) { docEl.style.setProperty('--app-viewport', `${height}px`); } docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)'); } } updateViewportHeightVar(); window.addEventListener('resize', updateViewportHeightVar); window.addEventListener('orientationchange', updateViewportHeightVar); window.addEventListener('pageshow', updateViewportHeightVar); if (window.visualViewport) { window.visualViewport.addEventListener('resize', updateViewportHeightVar); window.visualViewport.addEventListener('scroll', updateViewportHeightVar); } const SOCKET_IO_CDN_SOURCES = [ 'https://cdn.socket.io/4.7.5/socket.io.min.js', 'https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js' ]; function injectScriptSequentially(urls, onSuccess, onFailure) { let index = 0; const tryLoad = () => { if (index >= urls.length) { onFailure(); return; } const url = urls[index]; const script = document.createElement('script'); script.src = url; script.async = false; script.onload = () => { if (typeof io !== 'undefined') { console.log(`Socket.IO 已从 ${url} 加载`); onSuccess(); } else { index += 1; tryLoad(); } }; script.onerror = () => { console.warn(`无法从 ${url} 加载 Socket.IO,尝试下一个源`); index += 1; tryLoad(); }; document.head.appendChild(script); }; tryLoad(); } async function ensureSocketIOLoaded() { if (typeof io !== 'undefined') { return true; } return await new Promise((resolve) => { injectScriptSequentially( SOCKET_IO_CDN_SOURCES, () => resolve(true), () => resolve(false) ); }); } async function bootstrapApp() { // 检查必要的库是否加载 if (typeof Vue === 'undefined') { console.error('错误:Vue.js 未加载'); document.body.innerHTML = '

Vue.js 加载失败,请刷新页面

'; return; } if (typeof io === 'undefined') { const loaded = await ensureSocketIOLoaded(); if (!loaded || typeof io === 'undefined') { console.error('错误:Socket.IO 未加载'); document.body.innerHTML = '

Socket.IO 加载失败,请检查网络后刷新页面

'; return; } } console.log('所有依赖加载成功,初始化Vue应用...'); const { createApp } = Vue; const urlParams = new URLSearchParams(window.location.search || ''); const pathName = window.location.pathname || ''; const plusRouteMatch = pathName.match(/\/?([^\/+]+)\+([^\/]+)/i); const slashRouteMatch = pathName.match(/sub_agent[_/](.+)$/i); let detectedVariant = window.APP_VARIANT || urlParams.get('variant') || null; let detectedTaskId = window.SUB_AGENT_TASK_ID || urlParams.get('task_id') || null; let detectedConversationId = window.SUB_AGENT_CONVERSATION_ID || urlParams.get('conversation_id') || null; if (!detectedVariant) { if (plusRouteMatch || slashRouteMatch) { detectedVariant = 'sub_agent'; } else { detectedVariant = 'main'; } } if (!detectedTaskId) { if (plusRouteMatch) { detectedTaskId = slashRouteMatch ? slashRouteMatch[1] : plusRouteMatch[2]; } else if (slashRouteMatch) { detectedTaskId = slashRouteMatch[1]; } } if (!detectedConversationId && plusRouteMatch) { detectedConversationId = plusRouteMatch[1]; } const initialVariant = detectedVariant; const initialSubAgentTaskId = detectedTaskId; const initialSubAgentConversationId = detectedConversationId; const app = createApp({ data() { return { // 子智能体模式 variant: initialVariant, subAgentTaskId: initialSubAgentTaskId || '', subAgentConversationId: initialSubAgentConversationId ? (initialSubAgentConversationId.startsWith('conv_') ? initialSubAgentConversationId : `conv_${initialSubAgentConversationId}`) : null, subAgentTaskInfo: { task_id: initialSubAgentTaskId || '', agent_id: '', summary: '', status: '', message: '', workspace_dir: '', references_dir: '', deliverables_dir: '', target_project_dir: '', reference_manifest: [], last_tool: '' }, readonlyNoticeShown: false, // 连接状态 isConnected: false, socket: null, // 系统信息 projectPath: '', agentVersion: '', thinkingMode: '未知', // 消息相关 messages: [], inputMessage: '', // 当前消息索引 currentMessageIndex: -1, streamingMessage: false, // 停止功能状态 stopRequested: false, terminating: false, // 路由相关 initialRouteResolved: false, // 文件相关 fileTree: [], focusedFiles: {}, expandedFolders: {}, // 展开状态管理 expandedBlocks: new Set(), // 滚动控制 userScrolling: false, autoScrollEnabled: true, scrollListenerAttached: false, scrollListenerRetryWarned: false, // 面板宽度控制 leftWidth: 280, rightWidth: 420, rightCollapsed: true, isResizing: false, resizingPanel: null, minPanelWidth: 200, maxPanelWidth: 600, // 工具状态跟踪 preparingTools: new Map(), activeTools: new Map(), toolActionIndex: new Map(), toolActionIndex: new Map(), toolStacks: new Map(), // ========================================== // 对话管理相关状态 // ========================================== // 对话历史侧边栏 sidebarCollapsed: true, // 默认收起对话侧边栏 panelMode: 'files', // files | todo | subAgents subAgents: [], subAgentPollTimer: null, conversations: [], conversationsLoading: false, hasMoreConversations: false, loadingMoreConversations: false, currentConversationId: null, currentConversationTitle: '当前对话', // 搜索功能 searchQuery: '', searchTimer: null, // 分页 conversationsOffset: 0, conversationsLimit: 20, // 对话压缩状态 compressing: false, // 设置菜单状态 settingsOpen: false, thinkingScrollLocks: new Map(), // 工具控制菜单 toolMenuOpen: false, toolSettings: [], toolSettingsLoading: false, // 文件上传状态 uploading: false, // TODO 列表 todoList: null, todoEmoji: '🗒️', fileEmoji: '📁', todoDoneEmoji: '☑️', todoPendingEmoji: '⬜️', toolCategoryEmojis: { network: '🌐', file_edit: '📝', read_focus: '🔍', terminal_realtime: '🖥️', terminal_command: '⌨️', memory: '🧠', todo: '🗒️', sub_agent: '🤖' }, // 右键菜单相关 contextMenu: { visible: false, x: 0, y: 0, node: null }, onDocumentClick: null, onWindowScroll: null, onKeydownListener: null } }, async mounted() { console.log('Vue应用已挂载'); document.addEventListener('click', this.handleClickOutsideSettings); document.addEventListener('click', this.handleClickOutsideToolMenu); window.addEventListener('popstate', this.handlePopState); this.onDocumentClick = (event) => { if (!this.contextMenu.visible) { return; } if (event.target && event.target.closest && event.target.closest('.context-menu')) { return; } this.hideContextMenu(); }; this.onWindowScroll = () => { if (this.contextMenu.visible) { this.hideContextMenu(); } }; this.onKeydownListener = (event) => { if (event.key === 'Escape' && this.contextMenu.visible) { this.hideContextMenu(); } }; document.addEventListener('click', this.onDocumentClick); window.addEventListener('scroll', this.onWindowScroll, true); document.addEventListener('keydown', this.onKeydownListener); if (this.isSubAgentView) { document.documentElement.classList.add('sub-agent-view'); document.body.classList.add('sub-agent-view'); await this.bootstrapSubAgentRoute(); this.initSocket(); this.initScrollListener(); this.loadSubAgentInitialData(); this.subAgentPollTimer = setInterval(() => { this.fetchSubAgentTaskInfo(true); }, 4000); } else { await this.bootstrapRoute(); this.initSocket(); this.initScrollListener(); setTimeout(() => { this.loadInitialData(); }, 500); this.fetchSubAgents(); this.subAgentPollTimer = setInterval(() => { if (this.panelMode === 'subAgents') { this.fetchSubAgents(); } }, 5000); } }, beforeUnmount() { document.removeEventListener('click', this.handleClickOutsideSettings); document.removeEventListener('click', this.handleClickOutsideToolMenu); window.removeEventListener('popstate', this.handlePopState); if (this.onDocumentClick) { document.removeEventListener('click', this.onDocumentClick); this.onDocumentClick = null; } if (this.onWindowScroll) { window.removeEventListener('scroll', this.onWindowScroll, true); this.onWindowScroll = null; } if (this.onKeydownListener) { document.removeEventListener('keydown', this.onKeydownListener); this.onKeydownListener = null; } if (this.subAgentPollTimer) { clearInterval(this.subAgentPollTimer); this.subAgentPollTimer = null; } if (this.isSubAgentView) { document.documentElement.classList.remove('sub-agent-view'); document.body.classList.remove('sub-agent-view'); } }, computed: { isSubAgentView() { return this.variant === 'sub_agent'; } }, methods: { openGuiFileManager() { window.open('/file-manager', '_blank'); }, findMessageByAction(action) { if (!action) { return null; } for (const message of this.messages) { if (!message.actions) { continue; } if (message.actions.includes(action)) { return message; } } return null; }, async bootstrapSubAgentRoute() { if (!this.isSubAgentView) { await this.bootstrapRoute(); return; } if (this.subAgentConversationId) { this.currentConversationId = this.subAgentConversationId; this.currentConversationTitle = this.subAgentTaskInfo.summary || '子智能体对话'; this.initialRouteResolved = true; return; } if (!this.subAgentTaskId) { this.initialRouteResolved = true; return; } const info = await this.fetchSubAgentTaskInfo(true); if (info && info.sub_conversation_id) { const convId = info.sub_conversation_id; this.subAgentConversationId = convId; this.currentConversationId = convId; this.currentConversationTitle = info.summary || this.currentConversationTitle; } this.initialRouteResolved = true; }, async loadSubAgentInitialData() { if (!this.isSubAgentView) { this.loadInitialData(); return; } const info = await this.fetchSubAgentTaskInfo(); await this.fetchSubAgentConversation(); await this.fetchSubAgentFileTree(); await this.fetchTodoList(); if (!this.socket || !this.socket.connected) { setTimeout(() => { if (!this.socket || !this.socket.connected) { console.warn('WebSocket尚未建立,启用只读回放模式。'); this.isConnected = true; if (!this.readonlyNoticeShown) { this.readonlyNoticeShown = true; const status = (info && info.status) ? info.status : 'unknown'; this.addSystemMessage(`🔗 子智能体连接尚未建立,已进入只读模式(当前状态:${status})。`); } } }, 1200); } }, async fetchSubAgentTaskInfo(silent = false) { if (!this.subAgentTaskId) { return null; } try { const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}`); if (!resp.ok) { let payload; try { payload = await resp.json(); } catch (err) { payload = { error: await resp.text() }; } if (resp.status === 404) { const fallback = { task_id: this.subAgentTaskId, status: 'archived', summary: this.currentConversationTitle || '', message: payload && payload.message ? payload.message : '任务不存在' }; this.subAgentTaskInfo = { ...this.subAgentTaskInfo, ...fallback }; return fallback; } throw new Error(payload && (payload.error || payload.message) ? (payload.error || payload.message) : '加载失败'); } const data = await resp.json(); if (!data.success) { throw new Error(data.error || '加载失败'); } this.subAgentTaskInfo = { ...this.subAgentTaskInfo, ...data }; if (data.task_id && data.task_id !== this.subAgentTaskId) { this.subAgentTaskId = data.task_id; if (this.socket && this.socket.connected) { this.socket.emit('sub_agent_join', { task_id: data.task_id }); } } if (data.workspace_dir) { this.projectPath = data.workspace_dir; } if (data.sub_conversation_id && !this.subAgentConversationId) { const convId = data.sub_conversation_id; this.subAgentConversationId = convId; this.currentConversationId = convId; } if (!silent && data.summary) { this.currentConversationTitle = data.summary; } return data; } catch (error) { if (!silent) { console.warn('获取子智能体任务信息失败:', error); } if (!this.isConnected) { this.addSystemMessage(`⚠️ 无法获取子智能体任务信息:${error.message || error}`); } return null; } }, async fetchSubAgentConversation() { if (!this.subAgentTaskId) { return; } try { const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/conversation`); if (!resp.ok) { const fallbackHandled = await this.tryFetchArchivedConversation(); if (!fallbackHandled) { throw new Error(await resp.text()); } return; } const data = await resp.json(); if (!data.success) { const fallbackHandled = await this.tryFetchArchivedConversation(); if (!fallbackHandled) { throw new Error(data.error || '加载失败'); } return; } if (data.conversation_id) { this.currentConversationId = data.conversation_id; } const transformed = this.transformHistoryMessages(data.messages || []); this.messages = transformed; this.$nextTick(() => this.scrollToBottom(true)); if (!this.currentConversationTitle && this.subAgentTaskInfo.summary) { this.currentConversationTitle = this.subAgentTaskInfo.summary; } } catch (error) { console.warn('加载子智能体对话失败:', error); if (!this.isConnected) { this.isConnected = true; } this.addSystemMessage(`⚠️ 加载子智能体对话失败:${error.message || error}`); } }, async tryFetchArchivedConversation() { if (!this.currentConversationId) { return false; } try { const response = await fetch(`/sub_agent/conversations/${this.currentConversationId}`); if (!response.ok) { return false; } const payload = await response.json(); if (!payload.success) { return false; } const transformed = this.transformHistoryMessages(payload.messages || []); this.messages = transformed; this.$nextTick(() => this.scrollToBottom(true)); return true; } catch (err) { console.warn('加载归档子智能体对话失败:', err); return false; } }, async fetchSubAgentFileTree() { if (!this.isSubAgentView || !this.subAgentTaskId) { return; } try { const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/files`); if (!resp.ok) { throw new Error(await resp.text()); } const data = await resp.json(); if (data.success && data.data) { this.updateFileTree(data.data); } else { console.warn('获取子智能体文件树失败:', data.error || data.message); } } catch (error) { console.warn('加载子智能体文件树失败:', error); } }, async sendSubAgentMessage() { if (!this.subAgentTaskId || this.sending) { return; } const text = (this.inputMessage || '').trim(); if (!text) { return; } const userMessage = { role: 'user', content: text, metadata: {}, actions: [], streamingText: '', streamingThinking: '', currentStreamingType: null, timestamp: new Date().toISOString() }; this.messages.push(userMessage); this.currentMessageIndex = -1; this.inputMessage = ''; this.sending = true; this.autoScrollEnabled = true; this.scrollToBottom(true); try { await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }) }); } catch (error) { console.warn('发送子智能体消息失败:', error); } finally { this.sending = false; } }, async stopSubAgentTask() { if (!this.subAgentTaskId || this.stopping) { return; } this.stopping = true; this.stopRequested = true; try { await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/stop`, { method: 'POST' }); } catch (error) { console.warn('停止子智能体任务失败:', error); } finally { this.stopping = false; this.stopRequested = false; } }, async terminateSubAgentTask() { if (!this.subAgentTaskId || this.terminating) { return; } const shouldTerminate = window.confirm('确定要立即关闭子智能体吗?此操作无法撤销。'); if (!shouldTerminate) { return; } this.terminating = true; try { const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/terminate`, { method: 'POST' }); let data = null; try { data = await resp.json(); } catch (err) { data = { success: false, error: await resp.text() }; } if (!resp.ok || !data.success) { const message = (data && (data.error || data.message)) || '关闭失败'; alert(`关闭子智能体失败:${message}`); return; } this.addSystemMessage('🛑 子智能体已被手动关闭。'); } catch (error) { console.warn('关闭子智能体失败:', error); alert(`关闭子智能体失败:${error.message || error}`); } finally { this.terminating = false; } }, formatPathDisplay(path) { if (!path) { return '—'; } return path.replace(this.projectPath, '.'); }, formatSubAgentStatus(status) { if (!status) return '未知'; const map = { running: '进行中', pending: '等待中', completed: '已完成', failed: '失败', timeout: '超时', terminated: '已关闭' }; return map[status] || status; }, async bootstrapRoute() { if (this.isSubAgentView) { await this.bootstrapSubAgentRoute(); return; } const path = window.location.pathname.replace(/^\/+/, ''); if (!path || path === 'new') { this.currentConversationId = null; this.currentConversationTitle = '新对话'; this.initialRouteResolved = true; return; } const convId = path.startsWith('conv_') ? path : `conv_${path}`; try { const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' }); const result = await resp.json(); if (result.success) { this.currentConversationId = convId; this.currentConversationTitle = result.title || '对话'; history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`); } else { history.replaceState({}, '', '/new'); this.currentConversationId = null; this.currentConversationTitle = '新对话'; } } catch (error) { console.warn('初始化路由失败:', error); history.replaceState({}, '', '/new'); this.currentConversationId = null; this.currentConversationTitle = '新对话'; } finally { this.initialRouteResolved = true; } }, handlePopState(event) { const state = event.state || {}; const convId = state.conversationId; if (!convId) { this.currentConversationId = null; this.currentConversationTitle = '新对话'; this.messages = []; this.resetAllStates(); return; } this.loadConversation(convId); }, stripConversationPrefix(conversationId) { if (!conversationId) return ''; return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId; }, showContextMenu(payload) { if (!payload || !payload.node) { return; } const { node, event } = payload; console.log('context menu', node.path, node.type); if (event && typeof event.preventDefault === 'function') { event.preventDefault(); } if (event && typeof event.stopPropagation === 'function') { event.stopPropagation(); } if (!node.path && node.path !== '') { this.hideContextMenu(); return; } if (node.type !== 'file' && node.type !== 'folder') { this.hideContextMenu(); return; } this.hideContextMenu(); let x = (event && event.clientX) || 0; let y = (event && event.clientY) || 0; const menuWidth = 200; const menuHeight = 50; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (x + menuWidth > viewportWidth) { x = viewportWidth - menuWidth - 8; } if (y + menuHeight > viewportHeight) { y = viewportHeight - menuHeight - 8; } this.contextMenu.visible = true; this.contextMenu.x = Math.max(8, x); this.contextMenu.y = Math.max(8, y); this.contextMenu.node = node; }, hideContextMenu() { if (!this.contextMenu.visible) { return; } this.contextMenu.visible = false; this.contextMenu.node = null; }, async downloadFile(path) { if (!path) { this.hideContextMenu(); return; } const url = `/api/download/file?path=${encodeURIComponent(path)}`; const name = path.split('/').pop() || 'file'; await this.downloadResource(url, name); }, async downloadFolder(path) { if (!path) { this.hideContextMenu(); return; } const url = `/api/download/folder?path=${encodeURIComponent(path)}`; const segments = path.split('/').filter(Boolean); const folderName = segments.length ? segments.pop() : 'folder'; await this.downloadResource(url, `${folderName}.zip`); }, async downloadResource(url, filename) { try { const response = await fetch(url); if (!response.ok) { let message = response.statusText; try { const errorData = await response.json(); message = errorData.error || errorData.message || message; } catch (err) { message = await response.text(); } alert(`下载失败: ${message}`); return; } const blob = await response.blob(); const downloadName = filename || 'download'; const link = document.createElement('a'); const href = URL.createObjectURL(blob); link.href = href; link.download = downloadName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(href); } catch (error) { console.error('下载失败:', error); alert(`下载失败: ${error.message || error}`); } finally { this.hideContextMenu(); } }, initScrollListener() { if (this.scrollListenerAttached) { return; } const attach = () => { const messagesArea = this.$refs.messagesArea; if (!messagesArea) { if (!this.scrollListenerRetryWarned) { console.warn('消息区域未找到,等待渲染后重试'); this.scrollListenerRetryWarned = true; } setTimeout(attach, 200); return; } this.scrollListenerRetryWarned = false; this.scrollListenerAttached = true; let isProgrammaticScroll = false; const bottomThreshold = 12; this._setScrollingFlag = (flag) => { isProgrammaticScroll = !!flag; }; messagesArea.addEventListener('scroll', () => { if (isProgrammaticScroll) { return; } const scrollTop = messagesArea.scrollTop; const scrollHeight = messagesArea.scrollHeight; const clientHeight = messagesArea.clientHeight; const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold; if (isAtBottom) { this.userScrolling = false; this.autoScrollEnabled = true; } else { this.userScrolling = true; this.autoScrollEnabled = false; } }); }; this.$nextTick(attach); }, initSocket() { try { console.log('初始化WebSocket连接...'); const usePollingOnly = window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1'; this.socket = io('/', usePollingOnly ? { transports: ['polling'], upgrade: false } : { transports: ['websocket', 'polling'] }); // 连接事件 this.socket.on('connect', () => { this.isConnected = true; this.readonlyNoticeShown = false; console.log('WebSocket已连接'); if (this.isSubAgentView) { if (this.subAgentTaskId) { this.socket.emit('sub_agent_join', { task_id: this.subAgentTaskId }); } } else { this.resetAllStates(); } }); this.socket.on('disconnect', () => { this.isConnected = false; console.log('WebSocket已断开'); if (!this.isSubAgentView) { this.resetAllStates(); } }); this.socket.on('connect_error', (error) => { console.error('WebSocket连接错误:', error.message); }); this.socket.on('todo_updated', (data) => { console.log('收到todo更新事件:', data); if (!this.isSubAgentView && data && data.conversation_id) { this.currentConversationId = data.conversation_id; } if (this.isSubAgentView && data && data.conversation_id && this.subAgentConversationId && data.conversation_id !== this.subAgentConversationId) { return; } this.todoList = data && data.todo_list ? data.todo_list : null; }); // 系统就绪 if (!this.isSubAgentView) { this.socket.on('system_ready', (data) => { this.projectPath = data.project_path || ''; this.agentVersion = data.version || this.agentVersion; this.thinkingMode = data.thinking_mode || '未知'; console.log('系统就绪:', data); // 系统就绪后立即加载对话列表 this.loadConversationsList(); }); this.socket.on('tool_settings_updated', (data) => { console.log('收到工具设置更新:', data); if (data && Array.isArray(data.categories)) { this.applyToolSettingsSnapshot(data.categories); } }); } // ========================================== // 对话管理相关Socket事件 // ========================================== // 监听对话变更事件 if (!this.isSubAgentView) { this.socket.on('conversation_changed', (data) => { console.log('对话已切换:', data); this.currentConversationId = data.conversation_id; this.currentConversationTitle = data.title || ''; if (data.cleared) { this.messages = []; this.currentConversationId = null; this.currentConversationTitle = ''; history.replaceState({}, '', '/new'); } this.loadConversationsList(); this.fetchTodoList(); }); this.socket.on('conversation_resolved', (data) => { if (!data || !data.conversation_id) { return; } const convId = data.conversation_id; this.currentConversationId = convId; if (data.title) { this.currentConversationTitle = data.title; } const pathFragment = this.stripConversationPrefix(convId); const currentPath = window.location.pathname.replace(/^\/+/, ''); if (data.created) { history.pushState({ conversationId: convId }, '', `/${pathFragment}`); } else if (currentPath !== pathFragment) { history.replaceState({ conversationId: convId }, '', `/${pathFragment}`); } }); } else { this.socket.on('sub_agent_status', (data) => { if (data && data.task_id === this.subAgentTaskId) { this.subAgentTaskInfo.status = data.status || this.subAgentTaskInfo.status; if (data.reason) { this.subAgentTaskInfo.message = data.reason; } else if (data.message) { this.subAgentTaskInfo.message = data.message; } } }); } // 监听对话加载事件 this.socket.on('conversation_loaded', (data) => { console.log('对话已加载:', data); if (data.clear_ui) { // 清理当前UI状态,准备显示历史内容 this.resetAllStates(); } // 延迟获取并显示历史对话内容 setTimeout(() => { this.fetchAndDisplayHistory(); }, 300); // 延迟获取Token统计(累计+当前上下文) setTimeout(() => { this.fetchTodoList(); }, 500); }); // 监听对话列表更新事件 this.socket.on('conversation_list_update', (data) => { console.log('对话列表已更新:', data); // 刷新对话列表 this.loadConversationsList(); }); // 监听状态更新事件 this.socket.on('status_update', (status) => { // 更新系统状态信息 if (status.conversation && status.conversation.current_id) { this.currentConversationId = status.conversation.current_id; } }); // AI消息开始 this.socket.on('ai_message_start', () => { console.log('AI消息开始'); this.cleanupStaleToolActions(); const newMessage = { role: 'assistant', actions: [], streamingThinking: '', streamingText: '', currentStreamingType: null }; this.messages.push(newMessage); this.currentMessageIndex = this.messages.length - 1; this.streamingMessage = true; this.stopRequested = false; this.autoScrollEnabled = true; this.scrollToBottom(); }); // 思考流开始 this.socket.on('thinking_start', () => { console.log('思考开始'); if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; msg.streamingThinking = ''; msg.currentStreamingType = 'thinking'; const action = { id: Date.now() + Math.random(), type: 'thinking', content: '', streaming: true, timestamp: Date.now() }; msg.actions.push(action); const blockId = `${this.currentMessageIndex}-thinking-${msg.actions.length - 1}`; action.blockId = blockId; this.expandedBlocks.add(blockId); this.autoScrollEnabled = true; this.userScrolling = false; this.scrollToBottom(); this.thinkingScrollLocks.set(blockId, true); this.$forceUpdate(); } }); // 思考内容块 this.socket.on('thinking_chunk', (data) => { if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; msg.streamingThinking += data.content; const lastAction = msg.actions[msg.actions.length - 1]; if (lastAction && lastAction.type === 'thinking') { lastAction.content += data.content; } this.$forceUpdate(); if (lastAction && lastAction.blockId) { this.$nextTick(() => this.scrollThinkingToBottom(lastAction.blockId)); } else { this.conditionalScrollToBottom(); } } }); // 思考结束 this.socket.on('thinking_end', (data) => { console.log('思考结束'); if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; const lastAction = msg.actions[msg.actions.length - 1]; if (lastAction && lastAction.type === 'thinking') { lastAction.streaming = false; lastAction.content = data.full_content; if (lastAction.blockId) { setTimeout(() => { this.expandedBlocks.delete(lastAction.blockId); this.$forceUpdate(); }, 1000); this.$nextTick(() => this.scrollThinkingToBottom(lastAction.blockId)); } } msg.streamingThinking = ''; msg.currentStreamingType = null; this.$forceUpdate(); } }); // 文本流开始 this.socket.on('text_start', () => { console.log('文本开始'); if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; msg.streamingText = ''; msg.currentStreamingType = 'text'; const action = { id: Date.now() + Math.random(), type: 'text', content: '', streaming: true, timestamp: Date.now() }; msg.actions.push(action); this.$forceUpdate(); } }); // 文本内容块 this.socket.on('text_chunk', (data) => { if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; msg.streamingText += data.content; const lastAction = msg.actions[msg.actions.length - 1]; if (lastAction && lastAction.type === 'text') { lastAction.content += data.content; } this.$forceUpdate(); this.conditionalScrollToBottom(); // 实时渲染LaTeX this.renderLatexInRealtime(); } }); // 文本结束 this.socket.on('text_end', (data) => { console.log('文本结束'); if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; // 查找当前流式文本的action for (let i = msg.actions.length - 1; i >= 0; i--) { const action = msg.actions[i]; if (action.type === 'text' && action.streaming) { action.streaming = false; action.content = data.full_content; console.log('文本action已更新为完成状态'); break; } } msg.streamingText = ''; msg.currentStreamingType = null; this.$forceUpdate(); } }); // 工具提示事件(可选) this.socket.on('tool_hint', (data) => { console.log('工具提示:', data.name); // 可以在这里添加提示UI }); // 工具准备中事件 - 实时显示 this.socket.on('tool_preparing', (data) => { console.log('工具准备中:', data.name); const msg = this.ensureAssistantMessage(); if (!msg) { return; } 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}...` }, timestamp: Date.now() }; msg.actions.push(action); this.preparingTools.set(data.id, action); this.registerToolAction(action, data.id); this.trackToolAction(data.name, action); this.$forceUpdate(); this.conditionalScrollToBottom(); }); // 工具状态更新事件 - 实时显示详细状态 this.socket.on('tool_status', (data) => { console.log('工具状态:', data); const target = this.findToolAction(data.id, data.preparing_id, data.execution_id); if (target) { target.tool.statusDetail = data.detail; target.tool.statusType = data.status; this.$forceUpdate(); return; } const fallbackAction = this.getLatestActiveToolAction(data.tool); if (fallbackAction) { fallbackAction.tool.statusDetail = data.detail; fallbackAction.tool.statusType = data.status; this.registerToolAction(fallbackAction, data.execution_id || data.id || data.preparing_id); this.$forceUpdate(); } }); // 工具开始(从准备转为执行) this.socket.on('tool_start', (data) => { console.log('工具开始执行:', data.name); 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.findToolAction(data.id, data.preparing_id, data.execution_id); } if (!action) { const msg = this.ensureAssistantMessage(); 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.registerToolAction(action, data.id); this.trackToolAction(data.name, action); this.$forceUpdate(); this.conditionalScrollToBottom(); }); // 更新action(工具完成) this.socket.on('update_action', (data) => { console.log('更新action:', data.id, 'status:', data.status); let targetAction = this.findToolAction(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) { outer: for (const message of this.messages) { if (!message.actions) continue; for (const action of message.actions) { if (action.type !== 'tool') continue; const matchByExecution = action.tool.executionId && action.tool.executionId === data.id; const matchByToolId = action.tool.id === data.id; const matchByPreparingId = action.id === data.preparing_id; if (matchByExecution || matchByToolId || matchByPreparingId) { targetAction = action; break outer; } } } } if (targetAction) { 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.awaiting_content) { targetAction.tool.awaiting_content = true; } else if (data.status === 'completed') { targetAction.tool.awaiting_content = false; } if (!targetAction.tool.executionId && (data.execution_id || data.id)) { targetAction.tool.executionId = data.execution_id || data.id; } if (data.arguments) { targetAction.tool.arguments = data.arguments; targetAction.tool.argumentSnapshot = this.cloneToolArguments(data.arguments); targetAction.tool.argumentLabel = this.buildToolLabel(targetAction.tool.argumentSnapshot); } this.registerToolAction(targetAction, data.execution_id || data.id); if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) { this.unregisterToolAction(targetAction); if (data.id) { this.preparingTools.delete(data.id); } if (data.preparing_id) { this.preparingTools.delete(data.preparing_id); } } this.$forceUpdate(); this.conditionalScrollToBottom(); } // 关键修复:每个工具完成后都更新当前上下文Token if (data.status === 'completed') { setTimeout(() => { }, 500); } }); this.socket.on('append_payload', (data) => { console.log('收到append_payload事件:', data); if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; const action = { id: `append-payload-${Date.now()}-${Math.random()}`, type: 'append_payload', append: { path: data.path || '未知文件', forced: !!data.forced, success: data.success === undefined ? true : !!data.success, lines: data.lines ?? null, bytes: data.bytes ?? null }, timestamp: Date.now() }; msg.actions.push(action); this.$forceUpdate(); this.conditionalScrollToBottom(); } }); this.socket.on('modify_payload', (data) => { console.log('收到modify_payload事件:', data); if (this.currentMessageIndex >= 0) { const msg = this.messages[this.currentMessageIndex]; const action = { id: `modify-payload-${Date.now()}-${Math.random()}`, type: 'modify_payload', modify: { path: data.path || '未知文件', total: data.total ?? null, completed: data.completed || [], failed: data.failed || [], forced: !!data.forced }, timestamp: Date.now() }; msg.actions.push(action); this.$forceUpdate(); this.conditionalScrollToBottom(); } }); // 停止请求确认 this.socket.on('stop_requested', (data) => { console.log('停止请求已接收:', data.message); // 可以显示提示信息 }); // 任务停止 this.socket.on('task_stopped', (data) => { console.log('任务已停止:', data.message); this.resetAllStates(); }); // 任务完成(重点:更新Token统计) this.socket.on('task_complete', (data) => { console.log('任务完成', data); this.resetAllStates(); // 任务完成后立即更新Token统计(关键修复) if (this.currentConversationId) { } }); // 聚焦文件更新 this.socket.on('focused_files_update', (data) => { this.focusedFiles = data || {}; // 聚焦文件变化时更新当前上下文Token(关键修复) if (this.currentConversationId) { setTimeout(() => { }, 500); } }); // 文件树更新 this.socket.on('file_tree_update', (data) => { this.updateFileTree(data); // 文件树变化时也可能影响上下文 if (this.currentConversationId) { setTimeout(() => { }, 500); } }); // 系统消息 this.socket.on('system_message', (data) => { if (!data || !data.content) { return; } this.appendSystemAction(data.content); }); // 错误处理 this.socket.on('error', (data) => { this.addSystemMessage(`错误: ${data.message}`); // 仅标记当前流结束,避免状态错乱 this.streamingMessage = false; this.stopRequested = false; }); // 命令结果 this.socket.on('command_result', (data) => { if (data.command === 'clear' && data.success) { this.messages = []; this.currentMessageIndex = -1; this.expandedBlocks.clear(); } else if (data.command === 'status' && data.success) { this.addSystemMessage(`系统状态:\n${JSON.stringify(data.data, null, 2)}`); } else if (!data.success) { this.addSystemMessage(`命令失败: ${data.message}`); } }); } catch (error) { console.error('Socket初始化失败:', error); } }, registerToolAction(action, executionId = null) { if (!action || action.type !== 'tool') { return; } const keys = new Set(); if (action.id) { keys.add(action.id); } if (action.tool && action.tool.id) { keys.add(action.tool.id); } if (executionId) { keys.add(executionId); } if (action.tool && action.tool.executionId) { keys.add(action.tool.executionId); } keys.forEach(key => { if (!key) { return; } this.toolActionIndex.set(key, action); }); }, unregisterToolAction(action) { if (!action || action.type !== 'tool') { return; } const keysToRemove = []; for (const [key, stored] of this.toolActionIndex.entries()) { if (stored === action) { keysToRemove.push(key); } } keysToRemove.forEach(key => this.toolActionIndex.delete(key)); if (action.tool && action.tool.name) { this.releaseToolAction(action.tool.name, action); } }, findToolAction(id, preparingId, executionId) { if (!this.toolActionIndex) { return null; } const candidates = [executionId, id, preparingId]; for (const key of candidates) { if (key && this.toolActionIndex.has(key)) { return this.toolActionIndex.get(key); } } return null; }, trackToolAction(toolName, action) { if (!toolName || !action) { return; } if (!this.toolStacks.has(toolName)) { this.toolStacks.set(toolName, []); } const stack = this.toolStacks.get(toolName); if (!stack.includes(action)) { stack.push(action); } }, releaseToolAction(toolName, action) { if (!toolName || !this.toolStacks.has(toolName)) { return; } const stack = this.toolStacks.get(toolName); const index = stack.indexOf(action); if (index !== -1) { stack.splice(index, 1); } if (stack.length === 0) { this.toolStacks.delete(toolName); } }, getLatestActiveToolAction(toolName) { if (!toolName || !this.toolStacks.has(toolName)) { return null; } const stack = this.toolStacks.get(toolName); for (let i = stack.length - 1; i >= 0; i--) { const action = stack[i]; if (!action || action.type !== 'tool' || !action.tool) { continue; } if (['preparing', 'running', 'stale'].includes(action.tool.status)) { return action; } } return stack[stack.length - 1] || null; }, cleanupStaleToolActions() { this.messages.forEach(msg => { if (!msg.actions) { return; } msg.actions.forEach(action => { if (action.type !== 'tool' || !action.tool) { return; } if (['running', 'preparing'].includes(action.tool.status)) { action.tool.status = 'stale'; action.tool.message = action.tool.message || '已被新的响应中断'; this.unregisterToolAction(action); } }); }); this.preparingTools.clear(); this.toolActionIndex.clear(); }, ensureAssistantMessage() { if (this.currentMessageIndex >= 0) { return this.messages[this.currentMessageIndex]; } const message = { role: 'assistant', actions: [], streamingThinking: '', streamingText: '', currentStreamingType: null }; this.messages.push(message); this.currentMessageIndex = this.messages.length - 1; return message; }, // 完整重置所有状态 resetAllStates() { console.log('重置所有前端状态'); this.hideContextMenu(); // 重置消息和流状态 this.streamingMessage = false; this.currentMessageIndex = -1; this.stopRequested = false; // 清理工具状态 this.preparingTools.clear(); this.activeTools.clear(); this.toolActionIndex.clear(); // ✨ 新增:将所有未完成的工具标记为已完成 this.messages.forEach(msg => { if (msg.role === 'assistant' && msg.actions) { msg.actions.forEach(action => { if (action.type === 'tool' && (action.tool.status === 'preparing' || action.tool.status === 'running')) { action.tool.status = 'completed'; } }); } }); // 重置滚动状态 this.userScrolling = false; this.autoScrollEnabled = true; // 清理Markdown缓存 if (this.markdownCache) { this.markdownCache.clear(); } this.thinkingScrollLocks.clear(); // 强制更新视图 this.$forceUpdate(); this.settingsOpen = false; this.toolMenuOpen = false; this.toolSettingsLoading = false; this.toolSettings = []; console.log('前端状态重置完成'); }, async loadInitialData() { if (this.isSubAgentView) { await this.loadSubAgentInitialData(); return; } try { console.log('加载初始数据...'); const filesResponse = await fetch('/api/files'); const filesData = await filesResponse.json(); this.updateFileTree(filesData); const focusedResponse = await fetch('/api/focused'); const focusedData = await focusedResponse.json(); this.focusedFiles = focusedData || {}; await this.fetchTodoList(); const statusResponse = await fetch('/api/status'); const statusData = await statusResponse.json(); this.projectPath = statusData.project_path || ''; this.agentVersion = statusData.version || this.agentVersion; this.thinkingMode = statusData.thinking_mode || '未知'; // 获取当前对话信息 const statusConversationId = statusData.conversation && statusData.conversation.current_id; if (statusConversationId) { if (!this.currentConversationId) { this.currentConversationId = statusConversationId; } // 如果有当前对话,尝试获取标题和Token统计 try { const convResponse = await fetch(`/api/conversations/current`); const convData = await convResponse.json(); if (convData.success && convData.data) { this.currentConversationTitle = convData.data.title; } await this.fetchAndDisplayHistory(); // 获取当前对话的Token统计 } catch (e) { console.warn('获取当前对话标题失败:', e); } } await this.loadToolSettings(true); console.log('初始数据加载完成'); } catch (error) { console.error('加载初始数据失败:', error); } }, async refreshFileTree() { try { const response = await fetch('/api/files'); const data = await response.json(); this.updateFileTree(data); } catch (error) { console.error('刷新文件树失败:', error); } }, // ========================================== // 对话管理核心功能 // ========================================== async loadConversationsList() { this.conversationsLoading = true; try { const response = await fetch(`/api/conversations?limit=${this.conversationsLimit}&offset=${this.conversationsOffset}`); const data = await response.json(); if (data.success) { if (this.conversationsOffset === 0) { this.conversations = data.data.conversations; } else { this.conversations.push(...data.data.conversations); } this.hasMoreConversations = data.data.has_more; console.log(`已加载 ${this.conversations.length} 个对话`); if (this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0) { const latestConversation = this.conversations[0]; if (latestConversation && latestConversation.id) { await this.loadConversation(latestConversation.id); } } } else { console.error('加载对话列表失败:', data.error); } } catch (error) { console.error('加载对话列表异常:', error); } finally { this.conversationsLoading = false; } }, async loadMoreConversations() { if (this.loadingMoreConversations || !this.hasMoreConversations) return; this.loadingMoreConversations = true; this.conversationsOffset += this.conversationsLimit; await this.loadConversationsList(); this.loadingMoreConversations = false; }, async loadConversation(conversationId) { console.log('加载对话:', conversationId); if (conversationId === this.currentConversationId) { console.log('已是当前对话,跳过加载'); return; } try { // 1. 调用加载API const response = await fetch(`/api/conversations/${conversationId}/load`, { method: 'PUT' }); const result = await response.json(); if (result.success) { console.log('对话加载API成功:', result); // 2. 更新当前对话信息 this.currentConversationId = conversationId; this.currentConversationTitle = result.title; history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`); // 3. 重置UI状态 this.resetAllStates(); // 4. 延迟获取并显示历史对话内容(关键功能) setTimeout(() => { this.fetchAndDisplayHistory(); }, 300); // 5. 获取Token统计(重点:加载历史累计统计+当前上下文) setTimeout(() => { }, 500); } else { console.error('对话加载失败:', result.message); alert(`加载对话失败: ${result.message}`); } } catch (error) { console.error('加载对话异常:', error); alert(`加载对话异常: ${error.message}`); } }, // ========================================== // 关键功能:获取并显示历史对话内容 // ========================================== async fetchAndDisplayHistory() { console.log('开始获取历史对话内容...'); if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) { console.log('没有当前对话ID,跳过历史加载'); return; } try { const response = await fetch(`/api/conversations/${this.currentConversationId}/messages`); if (!response.ok) { console.warn('无法获取消息历史,尝试备用方法'); const statusResponse = await fetch('/api/status'); const status = await statusResponse.json(); if (status.conversation_history && Array.isArray(status.conversation_history)) { this.renderHistoryMessages(status.conversation_history); } return; } const payload = await response.json(); if (payload.success && payload.data && Array.isArray(payload.data.messages)) { this.renderHistoryMessages(payload.data.messages); } else { console.warn('历史消息结构异常:', payload); this.messages = []; } } catch (error) { console.error('获取历史对话失败:', error); this.messages = []; } }, renderHistoryMessages(historyMessages) { const transformed = this.transformHistoryMessages(historyMessages); this.messages = transformed; this.$forceUpdate(); this.$nextTick(() => this.scrollToBottom()); }, transformHistoryMessages(historyMessages = []) { if (!Array.isArray(historyMessages)) { return []; } const output = []; let currentAssistantMessage = null; const flushAssistant = () => { if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { output.push(currentAssistantMessage); } currentAssistantMessage = null; }; const ensureAssistantMessage = () => { if (!currentAssistantMessage) { currentAssistantMessage = { role: 'assistant', actions: [], streamingThinking: '', streamingText: '', currentStreamingType: null }; } return currentAssistantMessage; }; const parseToolArguments = (toolCall) => { if (!toolCall || !toolCall.function) { return {}; } const raw = toolCall.function.arguments; if (!raw) { return {}; } try { return typeof raw === 'string' ? JSON.parse(raw || '{}') : raw; } catch (error) { console.warn('解析工具参数失败:', error); return {}; } }; const attachToolResult = (message, assistantMsg) => { if (!assistantMsg || !assistantMsg.actions) { return false; } const actions = assistantMsg.actions; let toolAction = null; if (message.tool_call_id) { toolAction = actions.find(action => action.type === 'tool' && action.tool.id === message.tool_call_id); } if (!toolAction && message.name) { const candidates = actions.filter(action => action.type === 'tool' && action.tool.name === message.name); toolAction = candidates[candidates.length - 1]; } if (!toolAction) { return false; } let resultObj; try { resultObj = JSON.parse(message.content); } catch (_) { resultObj = message.content ? { output: message.content } : {}; } toolAction.tool.status = resultObj.success === false ? 'failed' : 'completed'; toolAction.tool.result = resultObj; if (message.name === 'append_to_file' && resultObj && typeof resultObj === 'object') { const summary = { path: resultObj.path || '未知文件', success: resultObj.success !== false, summary: resultObj.message || (resultObj.success === false ? '追加失败' : '追加完成'), lines: resultObj.lines || 0, bytes: resultObj.bytes || 0, forced: !!resultObj.forced }; assistantMsg.actions.push({ id: `history-append-${Date.now()}-${Math.random()}`, type: 'append', append: summary, timestamp: Date.now() }); } return true; }; const isToolResultNotice = (message) => { if (!message || typeof message.content !== 'string') { return false; } return message.content.trim().startsWith('[工具结果]'); }; const applyToolResultNotice = (message, assistantMsg) => { if (!isToolResultNotice(message) || !assistantMsg) { return false; } const trimmed = message.content.trim(); const headerMatch = trimmed.match(/\[工具结果\]\s*([^\s(]+)(?:\s*\(tool_call_id=([^)]+)\))?/); const remainder = headerMatch ? trimmed.slice(headerMatch[0].length).trim() : ''; const synthetic = { name: headerMatch ? headerMatch[1] : message.name, tool_call_id: message.tool_call_id || (headerMatch ? headerMatch[2] : undefined), content: remainder || '{}' }; return attachToolResult(synthetic, assistantMsg); }; historyMessages.forEach((message) => { const role = (message.role || 'assistant').toLowerCase(); if (role === 'user') { flushAssistant(); output.push({ role: 'user', content: message.content || '', metadata: message.metadata || {}, timestamp: message.timestamp }); return; } if (role === 'assistant') { const assistant = ensureAssistantMessage(); const content = message.content || ''; const metadata = message.metadata || {}; const appendPayloadMeta = metadata.append_payload; const modifyPayloadMeta = metadata.modify_payload; const reasoningText = (message.reasoning_content || '').trim(); if (reasoningText) { assistant.actions.push({ id: `history-think-${Date.now()}-${Math.random()}`, type: 'thinking', content: reasoningText, streaming: false, timestamp: Date.now() }); } const textContent = content.trim(); if (appendPayloadMeta) { assistant.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() }); } else if (modifyPayloadMeta) { assistant.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() }); } if (textContent && !appendPayloadMeta && !modifyPayloadMeta) { assistant.actions.push({ id: `history-text-${Date.now()}-${Math.random()}`, type: 'text', content: textContent, streaming: false, timestamp: Date.now() }); } if (message.tool_calls && Array.isArray(message.tool_calls)) { message.tool_calls.forEach((toolCall, tcIndex) => { assistant.actions.push({ id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`, type: 'tool', tool: { id: toolCall.id, name: toolCall.function?.name, arguments: parseToolArguments(toolCall), status: 'preparing', result: null }, timestamp: Date.now() }); }); } return; } if (role === 'tool') { if (!attachToolResult(message, currentAssistantMessage)) { flushAssistant(); output.push({ role: 'system', content: message.content || '', metadata: message.metadata || {}, timestamp: message.timestamp }); } return; } if (applyToolResultNotice(message, currentAssistantMessage)) { return; } flushAssistant(); output.push({ role: message.role || 'system', content: message.content || '', metadata: message.metadata || {}, timestamp: message.timestamp }); }); flushAssistant(); return output; }, 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)}`); // 重置状态 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(); 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; }, 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() { if (this.isSubAgentView) { this.panelMode = this.panelMode === 'files' ? 'todo' : 'files'; if (this.panelMode === 'files') { this.fetchSubAgentFileTree(); } else { this.fetchTodoList(); } return; } const order = ['files', 'todo', 'subAgents']; const nextIndex = (order.indexOf(this.panelMode) + 1) % order.length; this.panelMode = order[nextIndex]; if (this.panelMode === 'subAgents') { this.fetchSubAgents(); } }, formatTaskStatus(task) { if (!task) { return ''; } return task.status === 'done' ? `${this.todoDoneEmoji} 完成` : `${this.todoPendingEmoji} 未完成`; }, toolCategoryEmoji(categoryId) { return this.toolCategoryEmojis[categoryId] || '⚙️'; }, async fetchSubAgents() { if (this.isSubAgentView) { return; } 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); } }, getSubAgentBaseUrl() { const override = window.SUB_AGENT_BASE_URL || window.__SUB_AGENT_BASE_URL__; if (override && typeof override === 'string') { return override.replace(/\/$/, ''); } const { protocol, hostname } = window.location; if (hostname && hostname.includes('agent.')) { const mappedHost = hostname.replace('agent.', 'subagent.'); return `${protocol}//${mappedHost}`; } return `${protocol}//${hostname}:8092`; }, openSubAgent(agent) { if (this.isSubAgentView || !agent || !agent.task_id) { return; } 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 base = this.getSubAgentBaseUrl(); const pathSuffix = convSegment ? `/${convSegment}+${agentLabel}` : `/sub_agent/${agent.task_id}`; const params = new URLSearchParams(); params.set('task_id', agent.task_id); if (agent.conversation_id) { params.set('conversation_id', agent.conversation_id); } const url = `${base}${pathSuffix}?${params.toString()}`; window.open(url, '_blank'); }, async fetchTodoList() { if (this.isSubAgentView) { return; } 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(); } }, async sendMessage() { if (this.isSubAgentView) { await this.sendSubAgentMessage(); return; } 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) { } }, 1000); }, // 新增:停止任务方法 stopTask() { if (this.isSubAgentView) { this.stopSubAgentTask(); return; } 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; } }, 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': '📖', 'ocr_image': '📸', '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': '⏳' }; 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 `
            
${language}
${content}
`; }); }, 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(); } }, scrollThinkingToBottom(blockId) { if (!this.thinkingScrollLocks.get(blockId)) return; const refName = `thinkingContent-${blockId}`; const elRef = this.$refs[refName]; const el = Array.isArray(elRef) ? elRef[0] : elRef; if (el) { el.scrollTop = el.scrollHeight; } }, handleThinkingScroll(blockId, event) { const el = event.target; const threshold = 12; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold; this.thinkingScrollLocks.set(blockId, atBottom); }, // 面板调整方法 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 = ''; } } }); 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(); });