// @ts-nocheck // static/app-enhanced.js - 修复版,正确实现Token实时更新 import katex from 'katex'; import { mapActions, mapState, mapWritableState } from 'pinia'; import ChatArea from './components/chat/ChatArea.vue'; import ConversationSidebar from './components/sidebar/ConversationSidebar.vue'; import LeftPanel from './components/panels/LeftPanel.vue'; import FocusPanel from './components/panels/FocusPanel.vue'; import TokenDrawer from './components/token/TokenDrawer.vue'; import PersonalizationDrawer from './components/personalization/PersonalizationDrawer.vue'; import QuickMenu from './components/input/QuickMenu.vue'; import InputComposer from './components/input/InputComposer.vue'; import AppShell from './components/shell/AppShell.vue'; import { useUiStore } from './stores/ui'; import { useConversationStore } from './stores/conversation'; import { useChatStore } from './stores/chat'; import { useInputStore } from './stores/input'; import { useToolStore } from './stores/tool'; import { useResourceStore } from './stores/resource'; import { useUploadStore } from './stores/upload'; import { useFileStore } from './stores/file'; import { useSubAgentStore } from './stores/subAgent'; import { useFocusStore } from './stores/focus'; import { usePersonalizationStore } from './stores/personalization'; import { useChatActionStore } from './stores/chatActions'; import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons'; import { initializeLegacySocket } from './composables/useLegacySocket'; import { useConnectionStore } from './stores/connection'; import { useEasterEgg } from './composables/useEasterEgg'; import { renderMarkdown as renderMarkdownHelper } from './composables/useMarkdownRenderer'; import { formatTokenCount, formatBytes, formatPercentage, formatRate, formatResetTime, formatQuotaValue, quotaTypeLabel, buildQuotaResetSummary, isQuotaExceeded as isQuotaExceededUtil, buildQuotaToastMessage } from './utils/formatters'; import { getToolIcon, getToolAnimationClass, getToolStatusText, getToolDescription, cloneToolArguments, buildToolLabel, formatSearchTopic, formatSearchTime, getLanguageClass } from './utils/chatDisplay'; import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, toggleScrollLock as toggleScrollLockHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from './composables/useScrollControl'; import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from './composables/usePanelResize'; window.katex = katex; 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 ENABLE_APP_DEBUG_LOGS = false; function debugLog(...args) { if (!ENABLE_APP_DEBUG_LOGS) { return; } debugLog(...args); } const appOptions = { data() { return { // 路由相关 initialRouteResolved: false, // 工具状态跟踪 preparingTools: new Map(), activeTools: new Map(), toolActionIndex: new Map(), toolStacks: new Map(), // ========================================== // 对话管理相关状态 // ========================================== // 搜索功能 // ========================================== // Token统计相关状态(修复版) // ========================================== // 对话压缩状态 compressing: false, skipConversationLoadedEvent: false, skipConversationHistoryReload: false, _scrollListenerReady: false, historyLoading: false, mobileViewportQuery: null, // 工具控制菜单 icons: ICONS, toolCategoryIcons: TOOL_CATEGORY_ICON_MAP } }, created() { const actionStore = useChatActionStore(); actionStore.registerDependencies({ pushToast: (payload) => this.uiPushToast(payload), autoResizeInput: () => this.autoResizeInput(), focusComposer: () => { const inputEl = this.getComposerElement('stadiumInput'); if (inputEl && typeof inputEl.focus === 'function') { inputEl.focus(); } }, isConnected: () => this.isConnected, getSocket: () => this.socket, downloadResource: (url, filename) => this.downloadResource(url, filename) }); }, async mounted() { debugLog('Vue应用已挂载'); if (window.ensureCsrfToken) { window.ensureCsrfToken().catch((err) => { console.warn('CSRF token 初始化失败:', err); }); } await this.bootstrapRoute(); await this.initSocket(); this.$nextTick(() => { this.ensureScrollListener(); }); // 延迟加载初始数据 setTimeout(() => { this.loadInitialData(); }, 500); document.addEventListener('click', this.handleClickOutsideQuickMenu); document.addEventListener('click', this.handleClickOutsidePanelMenu); document.addEventListener('click', this.handleClickOutsideMobileMenu); window.addEventListener('popstate', this.handlePopState); window.addEventListener('keydown', this.handleMobileOverlayEscape); this.setupMobileViewportWatcher(); this.subAgentFetch(); this.subAgentStartPolling(); this.$nextTick(() => { this.autoResizeInput(); }); this.resourceStartContainerStatsPolling(); this.resourceStartProjectStoragePolling(); this.resourceStartUsageQuotaPolling(); }, computed: { ...mapWritableState(useConnectionStore, [ 'isConnected', 'socket', 'stopRequested', 'projectPath', 'agentVersion', 'thinkingMode' ]), ...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']), ...mapWritableState(useUiStore, [ 'sidebarCollapsed', 'workspaceCollapsed', 'panelMode', 'panelMenuOpen', 'leftWidth', 'rightWidth', 'rightCollapsed', 'isResizing', 'resizingPanel', 'minPanelWidth', 'maxPanelWidth', 'quotaToast', 'toastQueue', 'confirmDialog', 'easterEgg', 'isMobileViewport', 'mobileOverlayMenuOpen', 'activeMobileOverlay' ]), ...mapWritableState(useConversationStore, [ 'conversations', 'conversationsLoading', 'hasMoreConversations', 'loadingMoreConversations', 'currentConversationId', 'currentConversationTitle', 'searchQuery', 'searchTimer', 'conversationsOffset', 'conversationsLimit' ]), ...mapWritableState(useChatStore, [ 'messages', 'currentMessageIndex', 'streamingMessage', 'expandedBlocks', 'autoScrollEnabled', 'userScrolling', 'thinkingScrollLocks' ]), ...mapWritableState(useInputStore, [ 'inputMessage', 'inputLineCount', 'inputIsMultiline', 'inputIsFocused', 'quickMenuOpen', 'toolMenuOpen', 'settingsOpen' ]), ...mapWritableState(useToolStore, [ 'preparingTools', 'activeTools', 'toolActionIndex', 'toolStacks', 'toolSettings', 'toolSettingsLoading' ]), ...mapWritableState(useResourceStore, [ 'tokenPanelCollapsed', 'currentContextTokens', 'currentConversationTokens', 'projectStorage', 'containerStatus', 'containerNetRate', 'usageQuota' ]), ...mapWritableState(useFocusStore, ['focusedFiles']), ...mapWritableState(useUploadStore, ['uploading']) }, beforeUnmount() { document.removeEventListener('click', this.handleClickOutsideQuickMenu); document.removeEventListener('click', this.handleClickOutsidePanelMenu); document.removeEventListener('click', this.handleClickOutsideMobileMenu); window.removeEventListener('popstate', this.handlePopState); window.removeEventListener('keydown', this.handleMobileOverlayEscape); this.teardownMobileViewportWatcher(); this.subAgentStopPolling(); this.resourceStopContainerStatsPolling(); this.resourceStopProjectStoragePolling(); this.resourceStopUsageQuotaPolling(); const cleanup = this.destroyEasterEggEffect(true); if (cleanup && typeof cleanup.catch === 'function') { cleanup.catch(() => {}); } }, watch: { inputMessage() { this.autoResizeInput(); }, currentConversationId: { immediate: false, handler(newValue, oldValue) { debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { return; } if (this.skipConversationHistoryReload) { this.skipConversationHistoryReload = false; return; } if (oldValue && newValue === oldValue) { return; } this.fetchAndDisplayHistory(); this.fetchConversationTokenStatistics(); this.updateCurrentContextTokens(); } } }, methods: { ensureScrollListener() { if (this._scrollListenerReady) { return; } const area = this.getMessagesAreaElement(); if (!area) { return; } this.initScrollListener(); this._scrollListenerReady = true; }, setupMobileViewportWatcher() { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { this.updateMobileViewportState(false); return; } const query = window.matchMedia('(max-width: 768px)'); this.mobileViewportQuery = query; this.updateMobileViewportState(query.matches); if (typeof query.addEventListener === 'function') { query.addEventListener('change', this.handleMobileViewportQueryChange); } else if (typeof query.addListener === 'function') { query.addListener(this.handleMobileViewportQueryChange); } }, teardownMobileViewportWatcher() { const query = this.mobileViewportQuery; if (!query) { return; } if (typeof query.removeEventListener === 'function') { query.removeEventListener('change', this.handleMobileViewportQueryChange); } else if (typeof query.removeListener === 'function') { query.removeListener(this.handleMobileViewportQueryChange); } this.mobileViewportQuery = null; }, handleMobileViewportQueryChange(event) { this.updateMobileViewportState(event.matches); }, updateMobileViewportState(isMobile) { this.uiSetMobileViewport(!!isMobile); if (!isMobile) { this.uiSetMobileOverlayMenuOpen(false); this.closeMobileOverlay(); } }, toggleMobileOverlayMenu() { if (!this.isMobileViewport) { return; } this.uiToggleMobileOverlayMenu(); }, openMobileOverlay(target) { if (!this.isMobileViewport) { return; } if (this.activeMobileOverlay === target) { this.closeMobileOverlay(); return; } if (this.activeMobileOverlay === 'conversation') { this.uiSetSidebarCollapsed(true); } if (target === 'conversation') { this.uiSetSidebarCollapsed(false); } this.uiSetActiveMobileOverlay(target); this.uiSetMobileOverlayMenuOpen(false); }, closeMobileOverlay() { if (!this.activeMobileOverlay) { this.uiCloseMobileOverlay(); return; } if (this.activeMobileOverlay === 'conversation') { this.uiSetSidebarCollapsed(true); } this.uiCloseMobileOverlay(); }, handleClickOutsideMobileMenu(event) { if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) { return; } const trigger = this.$refs.mobilePanelTrigger; if (trigger && typeof trigger.contains === 'function' && trigger.contains(event.target)) { return; } this.uiSetMobileOverlayMenuOpen(false); }, handleWorkspaceToggle() { if (this.isMobileViewport) { return; } const nextState = !this.workspaceCollapsed; this.uiSetWorkspaceCollapsed(nextState); if (nextState) { this.uiSetPanelMenuOpen(false); } }, handleMobileOverlayEscape(event) { if (event.key !== 'Escape' || !this.isMobileViewport) { return; } if (this.mobileOverlayMenuOpen) { this.uiSetMobileOverlayMenuOpen(false); return; } if (this.activeMobileOverlay) { this.closeMobileOverlay(); } }, ...mapActions(useUiStore, { uiToggleSidebar: 'toggleSidebar', uiSetSidebarCollapsed: 'setSidebarCollapsed', uiSetWorkspaceCollapsed: 'setWorkspaceCollapsed', uiToggleWorkspaceCollapsed: 'toggleWorkspaceCollapsed', uiSetPanelMode: 'setPanelMode', uiSetPanelMenuOpen: 'setPanelMenuOpen', uiTogglePanelMenu: 'togglePanelMenu', uiSetMobileViewport: 'setIsMobileViewport', uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen', uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu', uiSetActiveMobileOverlay: 'setActiveMobileOverlay', uiCloseMobileOverlay: 'closeMobileOverlay', uiPushToast: 'pushToast', uiUpdateToast: 'updateToast', uiDismissToast: 'dismissToast', uiShowQuotaToastMessage: 'showQuotaToastMessage', uiDismissQuotaToast: 'dismissQuotaToast', uiRequestConfirm: 'requestConfirm', uiResolveConfirm: 'resolveConfirm' }), ...mapActions(useChatStore, { chatExpandBlock: 'expandBlock', chatCollapseBlock: 'collapseBlock', chatClearExpandedBlocks: 'clearExpandedBlocks', chatSetThinkingLock: 'setThinkingLock', chatClearThinkingLocks: 'clearThinkingLocks', chatSetScrollState: 'setScrollState', chatEnableAutoScroll: 'enableAutoScroll', chatDisableAutoScroll: 'disableAutoScroll', chatToggleScrollLockState: 'toggleScrollLockState', chatAddUserMessage: 'addUserMessage', chatStartAssistantMessage: 'startAssistantMessage', chatStartThinkingAction: 'startThinkingAction', chatAppendThinkingChunk: 'appendThinkingChunk', chatCompleteThinkingAction: 'completeThinking', chatStartTextAction: 'startTextAction', chatAppendTextChunk: 'appendTextChunk', chatCompleteTextAction: 'completeText', chatAddSystemMessage: 'addSystemMessage', chatAddAppendPayloadAction: 'addAppendPayloadAction', chatAddModifyPayloadAction: 'addModifyPayloadAction', chatEnsureAssistantMessage: 'ensureAssistantMessage' }), ...mapActions(useInputStore, { inputSetFocused: 'setInputFocused', inputToggleQuickMenu: 'toggleQuickMenu', inputCloseMenus: 'closeMenus', inputOpenQuickMenu: 'openQuickMenu', inputSetQuickMenuOpen: 'setQuickMenuOpen', inputToggleToolMenu: 'toggleToolMenu', inputSetToolMenuOpen: 'setToolMenuOpen', inputToggleSettingsMenu: 'toggleSettingsMenu', inputSetSettingsOpen: 'setSettingsOpen', inputSetMessage: 'setInputMessage', inputClearMessage: 'clearInputMessage', inputSetLineCount: 'setInputLineCount', inputSetMultiline: 'setInputMultiline' }), ...mapActions(useToolStore, { toolRegisterAction: 'registerToolAction', toolUnregisterAction: 'unregisterToolAction', toolFindAction: 'findToolAction', toolTrackAction: 'trackToolAction', toolReleaseAction: 'releaseToolAction', toolGetLatestAction: 'getLatestActiveToolAction', toolResetTracking: 'resetToolTracking', toolSetSettings: 'setToolSettings', toolSetSettingsLoading: 'setToolSettingsLoading' }), ...mapActions(useResourceStore, { resourceUpdateCurrentContextTokens: 'updateCurrentContextTokens', resourceFetchConversationTokenStatistics: 'fetchConversationTokenStatistics', resourceSetCurrentContextTokens: 'setCurrentContextTokens', resourceToggleTokenPanel: 'toggleTokenPanel', resourceApplyStatusSnapshot: 'applyStatusSnapshot', resourceUpdateContainerStatus: 'updateContainerStatus', resourceStartContainerStatsPolling: 'startContainerStatsPolling', resourceStopContainerStatsPolling: 'stopContainerStatsPolling', resourceStartProjectStoragePolling: 'startProjectStoragePolling', resourceStopProjectStoragePolling: 'stopProjectStoragePolling', resourceStartUsageQuotaPolling: 'startUsageQuotaPolling', resourceStopUsageQuotaPolling: 'stopUsageQuotaPolling', resourcePollContainerStats: 'pollContainerStats', resourcePollProjectStorage: 'pollProjectStorage', resourceFetchUsageQuota: 'fetchUsageQuota', resourceResetTokenStatistics: 'resetTokenStatistics', resourceSetUsageQuota: 'setUsageQuota' }), ...mapActions(useUploadStore, { uploadHandleSelected: 'handleSelectedFiles' }), ...mapActions(useFileStore, { fileFetchTree: 'fetchFileTree', fileSetTreeFromResponse: 'setFileTreeFromResponse', fileFetchTodoList: 'fetchTodoList', fileSetTodoList: 'setTodoList', fileHideContextMenu: 'hideContextMenu' }), ...mapActions(useSubAgentStore, { subAgentFetch: 'fetchSubAgents', subAgentStartPolling: 'startPolling', subAgentStopPolling: 'stopPolling' }), ...mapActions(useFocusStore, { focusFetchFiles: 'fetchFocusedFiles', focusSetFiles: 'setFocusedFiles' }), ...mapActions(usePersonalizationStore, { personalizationOpenDrawer: 'openDrawer' }), getInputComposerRef() { return this.$refs.inputComposer || null; }, getComposerElement(field) { const composer = this.getInputComposerRef(); const unwrap = (value: any) => { if (!value) { return null; } if (value instanceof HTMLElement) { return value; } if (typeof value === 'object' && 'value' in value && !(value instanceof Window)) { return value.value; } return value; }; if (composer && composer[field]) { return unwrap(composer[field]); } if (this.$refs && this.$refs[field]) { return unwrap(this.$refs[field]); } return null; }, getMessagesAreaElement() { const ref = this.$refs.messagesArea; if (!ref) { return null; } if (ref instanceof HTMLElement) { return ref; } if (ref.rootEl) { return ref.rootEl.value || ref.rootEl; } if (ref.$el && ref.$el.querySelector) { const el = ref.$el.querySelector('.messages-area'); if (el) { return el; } } return null; }, getThinkingContentElement(blockId) { const chatArea = this.$refs.messagesArea; if (chatArea && typeof chatArea.getThinkingRef === 'function') { const el = chatArea.getThinkingRef(blockId); if (el) { return el; } } const refName = `thinkingContent-${blockId}`; const elRef = this.$refs[refName]; if (Array.isArray(elRef)) { return elRef[0] || null; } return elRef || null; }, logMessageState(action, extra = {}) { const count = Array.isArray(this.messages) ? this.messages.length : 'N/A'; debugLog('[Messages]', { action, count, conversationId: this.currentConversationId, streaming: this.streamingMessage, ...extra }); }, iconStyle(iconKey, size) { const iconPath = this.icons ? this.icons[iconKey] : null; if (!iconPath) { return {}; } const style = { '--icon-src': `url(${iconPath})` }; if (size) { style['--icon-size'] = size; } return style; }, toolCategoryIcon(categoryId) { return this.toolCategoryIcons[categoryId] || 'settings'; }, 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; }, renderMarkdown(content, isStreaming = false) { return renderMarkdownHelper(content, isStreaming); }, hasContainerStats() { return !!( this.containerStatus && this.containerStatus.mode === 'docker' && this.containerStatus.stats ); }, containerStatusClass() { if (!this.containerStatus) { return 'status-pill--host'; } if (this.containerStatus.mode !== 'docker') { return 'status-pill--host'; } const rawStatus = ( this.containerStatus.state && (this.containerStatus.state.status || this.containerStatus.state.Status) ) || ''; const status = String(rawStatus).toLowerCase(); if (status.includes('running')) { return 'status-pill--running'; } if (status.includes('paused')) { return 'status-pill--stopped'; } if (status.includes('exited') || status.includes('dead')) { return 'status-pill--stopped'; } return 'status-pill--running'; }, containerStatusText() { if (!this.containerStatus) { return '未知'; } if (this.containerStatus.mode !== 'docker') { return '宿主机模式'; } const rawStatus = ( this.containerStatus.state && (this.containerStatus.state.status || this.containerStatus.state.Status) ) || ''; const status = String(rawStatus).toLowerCase(); if (status.includes('running')) { return '运行中'; } if (status.includes('paused')) { return '已暂停'; } if (status.includes('exited') || status.includes('dead')) { return '已停止'; } return rawStatus || '容器模式'; }, formatTime(value) { if (!value) { return '未知时间'; } let date; if (typeof value === 'number') { date = new Date(value); } else if (typeof value === 'string') { const parsed = Date.parse(value); if (!Number.isNaN(parsed)) { date = new Date(parsed); } else { const numeric = Number(value); if (!Number.isNaN(numeric)) { date = new Date(numeric); } } } else if (value instanceof Date) { date = value; } if (!date || Number.isNaN(date.getTime())) { return String(value); } const now = Date.now(); const diff = now - date.getTime(); if (diff < 60000) { return '刚刚'; } if (diff < 3600000) { const mins = Math.floor(diff / 60000); return `${mins} 分钟前`; } if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours} 小时前`; } const formatter = new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); return formatter.format(date); }, async bootstrapRoute() { 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.logMessageState('handlePopState:clear-messages-no-conversation'); this.messages = []; this.logMessageState('handlePopState:after-clear-no-conversation'); this.resetAllStates('handlePopState:no-conversation'); this.resetTokenStatistics(); return; } this.loadConversation(convId); }, stripConversationPrefix(conversationId) { if (!conversationId) return ''; return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId; }, async downloadFile(path) { if (!path) { this.fileHideContextMenu(); 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.fileHideContextMenu(); 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(); } this.uiPushToast({ title: '下载失败', message: message || '无法完成下载', type: 'error' }); 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); this.uiPushToast({ title: '下载失败', message: error.message || String(error), type: 'error' }); } finally { this.fileHideContextMenu(); } }, initScrollListener() { const messagesArea = this.getMessagesAreaElement(); if (!messagesArea) { console.warn('消息区域未找到'); return; } this._scrollListenerReady = 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.chatSetScrollState({ userScrolling: false, autoScrollEnabled: true }); } else { this.chatSetScrollState({ userScrolling: true, autoScrollEnabled: false }); } }); }, async initSocket() { await initializeLegacySocket(this); }, 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.toolUnregisterAction(action); } }); }); this.preparingTools.clear(); this.toolActionIndex.clear(); }, hasPendingToolActions() { const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0; if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) { return true; } if (!Array.isArray(this.messages)) { return false; } return this.messages.some(msg => { if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) { return false; } return msg.actions.some(action => { if (!action || action.type !== 'tool' || !action.tool) { return false; } if (action.tool.awaiting_content) { return true; } const status = typeof action.tool.status === 'string' ? action.tool.status.toLowerCase() : ''; return !status || ['preparing', 'running', 'pending', 'queued'].includes(status); }); }); }, maybeResetStreamingState(reason = 'unspecified') { if (!this.streamingMessage) { return false; } if (this.hasPendingToolActions()) { return false; } this.streamingMessage = false; this.stopRequested = false; debugLog('流式状态已结束', { reason }); return true; }, // 完整重置所有状态 resetAllStates(reason = 'unspecified') { debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId }); this.logMessageState('resetAllStates:before-cleanup', { reason }); this.fileHideContextMenu(); // 重置消息和流状态 this.streamingMessage = false; this.currentMessageIndex = -1; this.stopRequested = false; // 清理工具状态 this.toolResetTracking(); // 新增:将所有未完成的工具标记为已完成 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.chatEnableAutoScroll(); // 清理Markdown缓存 if (this.markdownCache) { this.markdownCache.clear(); } this.chatClearThinkingLocks(); // 强制更新视图 this.$forceUpdate(); this.inputSetSettingsOpen(false); this.inputSetToolMenuOpen(false); this.inputSetQuickMenuOpen(false); this.inputSetLineCount(1); this.inputSetMultiline(false); this.inputClearMessage(); this.toolSetSettingsLoading(false); this.toolSetSettings([]); debugLog('前端状态重置完成'); this._scrollListenerReady = false; this.$nextTick(() => { this.ensureScrollListener(); }); this.logMessageState('resetAllStates:after-cleanup', { reason }); }, // 重置Token统计 openRealtimeTerminal() { const { protocol, hostname, port } = window.location; const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`; window.open(target, '_blank'); }, resetTokenStatistics() { this.resourceResetTokenStatistics(); }, async loadInitialData() { try { debugLog('加载初始数据...'); await this.fileFetchTree(); await this.focusFetchFiles(); await this.fileFetchTodoList(); 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; this.applyStatusSnapshot(statusData); await this.fetchUsageQuota(); // 获取当前对话信息 const statusConversationId = statusData.conversation && statusData.conversation.current_id; if (statusConversationId) { if (!this.currentConversationId) { this.skipConversationHistoryReload = true; this.currentConversationId = statusConversationId; // 如果有当前对话,尝试获取标题和历史 try { const convResponse = await fetch(`/api/conversations/current`); const convData = await convResponse.json(); if (convData.success && convData.data) { this.currentConversationTitle = convData.data.title; } // 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发 await this.fetchAndDisplayHistory(); // 获取当前对话的Token统计 this.fetchConversationTokenStatistics(); this.updateCurrentContextTokens(); } catch (e) { console.warn('获取当前对话标题失败:', e); } } } await this.loadToolSettings(true); debugLog('初始数据加载完成'); } catch (error) { console.error('加载初始数据失败:', error); } }, // ========================================== // Token / 资源状态封装(Pinia store) // ========================================== async updateCurrentContextTokens() { await this.resourceUpdateCurrentContextTokens(this.currentConversationId); }, async fetchConversationTokenStatistics() { await this.resourceFetchConversationTokenStatistics(this.currentConversationId); }, toggleTokenPanel() { this.resourceToggleTokenPanel(); }, applyStatusSnapshot(status) { this.resourceApplyStatusSnapshot(status); }, updateContainerStatus(status) { this.resourceUpdateContainerStatus(status); }, pollContainerStats() { return this.resourcePollContainerStats(); }, startContainerStatsPolling() { this.resourceStartContainerStatsPolling(); }, stopContainerStatsPolling() { this.resourceStopContainerStatsPolling(); }, pollProjectStorage() { return this.resourcePollProjectStorage(); }, startProjectStoragePolling() { this.resourceStartProjectStoragePolling(); }, stopProjectStoragePolling() { this.resourceStopProjectStoragePolling(); }, fetchUsageQuota() { return this.resourceFetchUsageQuota(); }, startUsageQuotaPolling() { this.resourceStartUsageQuotaPolling(); }, stopUsageQuotaPolling() { this.resourceStopUsageQuotaPolling(); }, // ========================================== // 对话管理核心功能 // ========================================== 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); } if (this.currentConversationId) { this.promoteConversationToTop(this.currentConversationId); } this.hasMoreConversations = data.data.has_more; debugLog(`已加载 ${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) { debugLog('加载对话:', conversationId); this.logMessageState('loadConversation:start', { conversationId }); if (conversationId === this.currentConversationId) { debugLog('已是当前对话,跳过加载'); return; } try { // 1. 调用加载API const response = await fetch(`/api/conversations/${conversationId}/load`, { method: 'PUT' }); const result = await response.json(); if (result.success) { debugLog('对话加载API成功:', result); // 2. 更新当前对话信息 this.skipConversationHistoryReload = true; this.currentConversationId = conversationId; this.currentConversationTitle = result.title; this.promoteConversationToTop(conversationId); history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`); this.skipConversationLoadedEvent = true; // 3. 重置UI状态 this.resetAllStates(`loadConversation:${conversationId}`); this.subAgentFetch(); this.fetchTodoList(); // 4. 历史对话内容和Token统计由后端的 conversation_loaded 事件触发 // 不在此处重复调用,避免双重加载 // Socket.IO 的 conversation_loaded 事件会处理: // - fetchAndDisplayHistory() // - fetchConversationTokenStatistics() // - updateCurrentContextTokens() } else { console.error('对话加载失败:', result.message); this.uiPushToast({ title: '加载对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { console.error('加载对话异常:', error); this.uiPushToast({ title: '加载对话异常', message: error.message || String(error), type: 'error' }); } }, promoteConversationToTop(conversationId) { if (!Array.isArray(this.conversations) || !conversationId) { return; } const index = this.conversations.findIndex(conv => conv && conv.id === conversationId); if (index > 0) { const [selected] = this.conversations.splice(index, 1); this.conversations.unshift(selected); } }, // ========================================== // 关键功能:获取并显示历史对话内容 // ========================================== async fetchAndDisplayHistory() { if (this.historyLoading) { debugLog('历史消息正在加载,跳过重复请求'); return; } this.historyLoading = true; try { debugLog('开始获取历史对话内容...'); this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId }); if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) { debugLog('没有当前对话ID,跳过历史加载'); return; } try { // 使用专门的API获取对话消息历史 const messagesResponse = await fetch(`/api/conversations/${this.currentConversationId}/messages`); if (!messagesResponse.ok) { console.warn('无法获取消息历史,尝试备用方法'); // 备用方案:通过状态API获取 const statusResponse = await fetch('/api/status'); const status = await statusResponse.json(); debugLog('系统状态:', status); this.applyStatusSnapshot(status); // 如果状态中有对话历史字段 if (status.conversation_history && Array.isArray(status.conversation_history)) { this.renderHistoryMessages(status.conversation_history); return; } debugLog('备用方案也无法获取历史消息'); return; } const messagesData = await messagesResponse.json(); debugLog('获取到消息数据:', messagesData); if (messagesData.success && messagesData.data && messagesData.data.messages) { const messages = messagesData.data.messages; debugLog(`发现 ${messages.length} 条历史消息`); if (messages.length > 0) { // 清空当前显示的消息 this.logMessageState('fetchAndDisplayHistory:before-clear-existing'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:after-clear-existing'); // 渲染历史消息 - 这是关键功能 this.renderHistoryMessages(messages); // 滚动到底部 this.$nextTick(() => { this.scrollToBottom(); }); debugLog('历史对话内容显示完成'); } else { debugLog('对话存在但没有历史消息'); this.logMessageState('fetchAndDisplayHistory:no-history-clear'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:no-history-cleared'); } } else { debugLog('消息数据格式不正确:', messagesData); this.logMessageState('fetchAndDisplayHistory:invalid-data-clear'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared'); } } catch (error) { console.error('获取历史对话失败:', error); debugLog('尝试不显示错误弹窗,仅在控制台记录'); // 不显示alert,避免打断用户体验 this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) }); this.messages = []; this.logMessageState('fetchAndDisplayHistory:error-cleared'); } } finally { this.historyLoading = false; } }, // ========================================== // 关键功能:渲染历史消息 // ========================================== renderHistoryMessages(historyMessages) { debugLog('开始渲染历史消息...', historyMessages); debugLog('历史消息数量:', historyMessages.length); this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length }); if (!Array.isArray(historyMessages)) { console.error('历史消息不是数组格式'); return; } let currentAssistantMessage = null; historyMessages.forEach((message, index) => { debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message); if (message.role === 'user') { // 用户消息 - 先结束之前的assistant消息 if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { this.messages.push(currentAssistantMessage); currentAssistantMessage = null; } this.messages.push({ role: 'user', content: message.content || '' }); debugLog('添加用户消息:', message.content?.substring(0, 50) + '...'); } else if (message.role === 'assistant') { // AI消息 - 如果没有当前assistant消息,创建一个 if (!currentAssistantMessage) { currentAssistantMessage = { role: 'assistant', actions: [], streamingThinking: '', streamingText: '', currentStreamingType: null, activeThinkingId: null, awaitingFirstContent: false, generatingLabel: '' }; } const content = message.content || ''; let reasoningText = (message.reasoning_content || '').trim(); if (!reasoningText) { const thinkPatterns = [ /([\s\S]*?)<\/think>/g, /([\s\S]*?)<\/thinking>/g ]; let extracted = ''; for (const pattern of thinkPatterns) { let match; while ((match = pattern.exec(content)) !== null) { extracted += (match[1] || '').trim() + '\n'; } } reasoningText = extracted.trim(); } if (reasoningText) { const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`; currentAssistantMessage.actions.push({ id: `history-think-${Date.now()}-${Math.random()}`, type: 'thinking', content: reasoningText, streaming: false, timestamp: Date.now(), blockId }); debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...'); } // 处理普通文本内容(移除思考标签后的内容) const metadata = message.metadata || {}; const appendPayloadMeta = metadata.append_payload; const modifyPayloadMeta = metadata.modify_payload; const isAppendMessage = message.name === 'append_to_file'; const isModifyMessage = message.name === 'modify_file'; const containsAppendMarkers = /<<<\s*(APPEND|MODIFY)/i.test(content || '') || /<<>>/i.test(content || ''); let textContent = content; if (!message.reasoning_content) { textContent = textContent .replace(/[\s\S]*?<\/think>/g, '') .replace(/[\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() }); debugLog('添加append占位信息:', appendPayloadMeta.path); } else if (modifyPayloadMeta) { currentAssistantMessage.actions.push({ id: `history-modify-payload-${Date.now()}-${Math.random()}`, type: 'modify_payload', modify: { path: modifyPayloadMeta.path || '未知文件', total: modifyPayloadMeta.total_blocks ?? null, completed: modifyPayloadMeta.completed || [], failed: modifyPayloadMeta.failed || [], forced: !!modifyPayloadMeta.forced, details: modifyPayloadMeta.details || [] }, timestamp: Date.now() }); debugLog('添加modify占位信息:', modifyPayloadMeta.path); } if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) { currentAssistantMessage.actions.push({ id: `history-text-${Date.now()}-${Math.random()}`, type: 'text', content: textContent, streaming: false, timestamp: Date.now() }); debugLog('添加文本内容:', textContent.substring(0, 50) + '...'); } // 处理工具调用 if (message.tool_calls && Array.isArray(message.tool_calls)) { message.tool_calls.forEach((toolCall, tcIndex) => { let arguments_obj = {}; try { arguments_obj = typeof toolCall.function.arguments === 'string' ? JSON.parse(toolCall.function.arguments || '{}') : (toolCall.function.arguments || {}); } catch (e) { console.warn('解析工具参数失败:', e); arguments_obj = {}; } 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() }); debugLog('添加工具调用:', toolCall.function.name); }); } } else if (message.role === 'tool') { // 工具结果 - 更新当前assistant消息中对应的工具 if (currentAssistantMessage) { // 查找对应的工具action - 使用更灵活的匹配 let toolAction = null; // 优先按tool_call_id匹配 if (message.tool_call_id) { toolAction = currentAssistantMessage.actions.find(action => action.type === 'tool' && action.tool.id === message.tool_call_id ); } // 如果找不到,按name匹配最后一个同名工具 if (!toolAction && message.name) { const sameNameTools = currentAssistantMessage.actions.filter(action => action.type === 'tool' && action.tool.name === message.name ); toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个 } if (toolAction) { // 解析工具结果 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 (result && typeof result === 'object') { if (result.error) { toolAction.tool.message = result.error; } else if (result.message && !toolAction.tool.message) { toolAction.tool.message = result.message; } } if (message.name === 'append_to_file' && result && result.message) { toolAction.tool.message = result.message; } debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`); // append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复 } else { console.warn('找不到对应的工具调用:', message.name, message.tool_call_id); } } } else { // 其他类型消息(如system)- 先结束当前assistant消息 if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { this.messages.push(currentAssistantMessage); currentAssistantMessage = null; } debugLog('处理其他类型消息:', message.role); this.messages.push({ role: message.role, content: message.content || '' }); } }); // 处理最后一个assistant消息 if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { this.messages.push(currentAssistantMessage); } debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`); this.logMessageState('renderHistoryMessages:after-render'); // 强制更新视图 this.$forceUpdate(); // 确保滚动到底部 this.$nextTick(() => { this.scrollToBottom(); setTimeout(() => { const blockCount = this.$el && this.$el.querySelectorAll ? this.$el.querySelectorAll('.message-block').length : 'N/A'; debugLog('[Messages] DOM 渲染统计', { blocks: blockCount, conversationId: this.currentConversationId }); }, 0); }); }, async createNewConversation() { debugLog('创建新对话...'); this.logMessageState('createNewConversation:start'); 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) { debugLog('新对话创建成功:', result.conversation_id); // 清空当前消息 this.logMessageState('createNewConversation:before-clear'); this.messages = []; this.logMessageState('createNewConversation:after-clear'); this.currentConversationId = result.conversation_id; this.currentConversationTitle = '新对话'; history.pushState({ conversationId: this.currentConversationId }, '', `/${this.stripConversationPrefix(this.currentConversationId)}`); // 重置Token统计 this.resetTokenStatistics(); // 重置状态 this.resetAllStates('createNewConversation'); // 刷新对话列表 this.conversationsOffset = 0; await this.loadConversationsList(); } else { console.error('创建对话失败:', result.message); this.uiPushToast({ title: '创建对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { console.error('创建对话异常:', error); this.uiPushToast({ title: '创建对话异常', message: error.message || String(error), type: 'error' }); } }, async deleteConversation(conversationId) { const confirmed = await this.confirmAction({ title: '删除对话', message: '确定要删除这个对话吗?删除后无法恢复。', confirmText: '删除', cancelText: '取消' }); if (!confirmed) { return; } debugLog('删除对话:', conversationId); this.logMessageState('deleteConversation:start', { conversationId }); try { const response = await fetch(`/api/conversations/${conversationId}`, { method: 'DELETE' }); const result = await response.json(); if (result.success) { debugLog('对话删除成功'); // 如果删除的是当前对话,清空界面 if (conversationId === this.currentConversationId) { this.logMessageState('deleteConversation:before-clear', { conversationId }); this.messages = []; this.logMessageState('deleteConversation:after-clear', { conversationId }); this.currentConversationId = null; this.currentConversationTitle = ''; this.resetAllStates(`deleteConversation:${conversationId}`); this.resetTokenStatistics(); history.replaceState({}, '', '/new'); } // 刷新对话列表 this.conversationsOffset = 0; await this.loadConversationsList(); } else { console.error('删除对话失败:', result.message); this.uiPushToast({ title: '删除对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { console.error('删除对话异常:', error); this.uiPushToast({ title: '删除对话异常', message: error.message || String(error), type: 'error' }); } }, async duplicateConversation(conversationId) { debugLog('复制对话:', 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 || '复制失败'; this.uiPushToast({ title: '复制对话失败', message, type: 'error' }); } } catch (error) { console.error('复制对话异常:', error); this.uiPushToast({ title: '复制对话异常', message: error.message || String(error), type: 'error' }); } }, searchConversations() { // 简单的搜索功能,实际实现可以调用搜索API if (this.searchTimer) { clearTimeout(this.searchTimer); } this.searchTimer = setTimeout(() => { if (this.searchQuery.trim()) { debugLog('搜索对话:', this.searchQuery); // TODO: 实现搜索API调用 // this.searchConversationsAPI(this.searchQuery); } else { // 清空搜索,重新加载全部对话 this.conversationsOffset = 0; this.loadConversationsList(); } }, 300); }, handleSidebarSearch(value) { this.searchQuery = value; this.searchConversations(); }, handleMobileOverlaySelect(conversationId) { this.loadConversation(conversationId); this.closeMobileOverlay(); }, handleMobilePersonalClick() { this.closeMobileOverlay(); this.uiSetMobileOverlayMenuOpen(false); this.openPersonalPage(); }, toggleSidebar() { if (this.isMobileViewport && this.activeMobileOverlay === 'conversation') { this.closeMobileOverlay(); return; } this.uiToggleSidebar(); }, togglePanelMenu() { this.uiTogglePanelMenu(); }, selectPanelMode(mode) { this.uiSetPanelMode(mode); this.uiSetPanelMenuOpen(false); }, openPersonalPage() { this.personalizationOpenDrawer(); }, fetchTodoList() { return this.fileFetchTodoList(); }, fetchSubAgents() { return this.subAgentFetch(); }, async toggleThinkingMode() { const nextMode = !this.thinkingMode; try { const response = await fetch('/api/thinking-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thinking_mode: nextMode }) }); const data = await response.json(); if (response.ok && data.success) { const actual = typeof data.data === 'boolean' ? data.data : nextMode; this.thinkingMode = actual; return; } throw new Error(data.message || data.error || '切换失败'); } catch (error) { console.error('切换思考模式失败:', error); this.uiPushToast({ title: '切换思考模式失败', message: error.message || '请稍后重试', type: 'error' }); } }, triggerFileUpload() { if (this.uploading) { return; } const input = this.getComposerElement('fileUploadInput'); if (input) { input.click(); } }, handleFileSelected(files) { this.uploadHandleSelected(files); }, handleSendOrStop() { if (this.streamingMessage) { this.stopTask(); } else { this.sendMessage(); } }, sendMessage() { if (this.streamingMessage || !this.isConnected) { return; } if (!this.inputMessage.trim()) { return; } const quotaType = this.thinkingMode ? 'thinking' : 'fast'; if (this.isQuotaExceeded(quotaType)) { this.showQuotaToast({ type: quotaType }); return; } const message = this.inputMessage; if (message.startsWith('/')) { this.socket.emit('send_command', { command: message }); this.inputClearMessage(); this.autoResizeInput(); return; } this.chatAddUserMessage(message); this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId }); this.inputClearMessage(); this.inputSetLineCount(1); this.inputSetMultiline(false); this.chatEnableAutoScroll(); this.scrollToBottom(); this.autoResizeInput(); // 发送消息后延迟更新当前上下文Token(关键修复:恢复原逻辑) setTimeout(() => { if (this.currentConversationId) { this.updateCurrentContextTokens(); } }, 1000); }, // 新增:停止任务方法 stopTask() { if (this.streamingMessage && !this.stopRequested) { this.socket.emit('stop_task'); this.stopRequested = true; debugLog('发送停止请求'); } }, async clearChat() { const confirmed = await this.confirmAction({ title: '清除对话', message: '确定要清除所有对话记录吗?该操作不可撤销。', confirmText: '清除', cancelText: '取消' }); if (confirmed) { this.socket.emit('send_command', { command: '/clear' }); } }, async compressConversation() { if (!this.currentConversationId) { this.uiPushToast({ title: '无法压缩', message: '当前没有可压缩的对话。', type: 'info' }); return; } if (this.compressing) { return; } const confirmed = await this.confirmAction({ title: '压缩对话', message: '确定要压缩当前对话记录吗?压缩后会生成新的对话副本。', confirmText: '压缩', cancelText: '取消' }); if (!confirmed) { return; } 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; } debugLog('对话压缩完成:', result); } else { const message = result.message || result.error || '压缩失败'; this.uiPushToast({ title: '压缩失败', message, type: 'error' }); } } catch (error) { console.error('压缩对话异常:', error); this.uiPushToast({ title: '压缩对话异常', message: error.message || '请稍后重试', type: 'error' }); } finally { this.compressing = false; } }, toggleToolMenu() { if (!this.isConnected) { return; } const nextState = this.inputToggleToolMenu(); if (nextState) { this.inputSetSettingsOpen(false); if (!this.quickMenuOpen) { this.inputOpenQuickMenu(); } this.loadToolSettings(true); } else { this.inputSetToolMenuOpen(false); } }, toggleQuickMenu() { if (!this.isConnected) { return; } this.inputToggleQuickMenu(); }, closeQuickMenu() { this.inputCloseMenus(); }, handleQuickUpload() { if (this.uploading || !this.isConnected) { return; } this.triggerFileUpload(); }, handleQuickModeToggle() { if (!this.isConnected || this.streamingMessage) { return; } this.toggleThinkingMode(); }, handleInputChange() { this.autoResizeInput(); }, handleInputFocus() { this.inputSetFocused(true); this.closeQuickMenu(); }, handleInputBlur() { this.inputSetFocused(false); }, handleRealtimeTerminalClick() { if (!this.isConnected) { return; } this.openRealtimeTerminal(); }, handleFocusPanelToggleClick() { if (!this.isConnected) { return; } this.toggleFocusPanel(); }, handleTokenPanelToggleClick() { if (!this.currentConversationId) { return; } this.toggleTokenPanel(); }, handleCompressConversationClick() { if (this.compressing || this.streamingMessage || !this.isConnected) { return; } this.compressConversation(); }, autoResizeInput() { this.$nextTick(() => { const textarea = this.getComposerElement('stadiumInput'); if (!textarea || !(textarea instanceof HTMLTextAreaElement)) { return; } const previousHeight = textarea.offsetHeight; textarea.style.height = 'auto'; const computedStyle = window.getComputedStyle(textarea); const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20; const maxHeight = lineHeight * 6; const targetHeight = Math.min(textarea.scrollHeight, maxHeight); this.inputSetLineCount(Math.max(1, Math.round(targetHeight / lineHeight))); this.inputSetMultiline(targetHeight > lineHeight * 1.4); if (Math.abs(targetHeight - previousHeight) <= 0.5) { textarea.style.height = `${targetHeight}px`; return; } textarea.style.height = `${previousHeight}px`; void textarea.offsetHeight; requestAnimationFrame(() => { textarea.style.height = `${targetHeight}px`; }); }); }, handleClickOutsideQuickMenu(event) { if (!this.quickMenuOpen) { return; } const shell = this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell'); if (shell && shell.contains(event.target)) { return; } this.closeQuickMenu(); }, handleClickOutsidePanelMenu(event) { if (!this.panelMenuOpen) { return; } const leftPanel = this.$refs.leftPanel; const wrapper = leftPanel && leftPanel.panelMenuWrapper ? leftPanel.panelMenuWrapper : null; if (wrapper && wrapper.contains(event.target)) { return; } this.uiSetPanelMenuOpen(false); }, applyToolSettingsSnapshot(categories) { if (!Array.isArray(categories)) { console.warn('[ToolSettings] Snapshot skipped: categories not array', categories); return; } const normalized = categories.map((item) => ({ id: item.id, label: item.label || item.id, enabled: !!item.enabled, tools: Array.isArray(item.tools) ? item.tools : [] })); debugLog('[ToolSettings] Snapshot applied', { received: categories.length, normalized, anyEnabled: normalized.some(cat => cat.enabled), toolExamples: normalized.slice(0, 3) }); this.toolSetSettings(normalized); this.toolSetSettingsLoading(false); }, async loadToolSettings(force = false) { if (!this.isConnected && !force) { debugLog('[ToolSettings] Skip load: disconnected & not forced'); return; } if (this.toolSettingsLoading) { debugLog('[ToolSettings] Skip load: already loading'); return; } if (!force && this.toolSettings.length > 0) { debugLog('[ToolSettings] Skip load: already have settings'); return; } debugLog('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected }); this.toolSetSettingsLoading(true); try { const response = await fetch('/api/tool-settings'); const data = await response.json(); debugLog('[ToolSettings] Fetch response', { status: response.status, data }); if (response.ok && data.success && Array.isArray(data.categories)) { this.applyToolSettingsSnapshot(data.categories); } else { console.warn('获取工具设置失败:', data); this.toolSetSettingsLoading(false); } } catch (error) { console.error('获取工具设置异常:', error); this.toolSetSettingsLoading(false); } }, async updateToolCategory(categoryId, enabled) { if (!this.isConnected) { return; } if (this.toolSettingsLoading) { return; } const previousSnapshot = this.toolSettings.map((item) => ({ ...item })); const updatedSettings = this.toolSettings.map((item) => { if (item.id === categoryId) { return { ...item, enabled }; } return item; }); this.toolSetSettings(updatedSettings); 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.toolSetSettings(previousSnapshot); } } catch (error) { console.error('更新工具设置异常:', error); this.toolSetSettings(previousSnapshot); } this.toolSetSettingsLoading(false); }, toggleSettings() { if (!this.isConnected) { return; } const nextState = this.inputToggleSettingsMenu(); if (nextState) { this.inputSetToolMenuOpen(false); if (!this.quickMenuOpen) { this.inputOpenQuickMenu(); } } }, toggleFocusPanel() { this.rightCollapsed = !this.rightCollapsed; if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) { this.rightWidth = this.minPanelWidth; } }, addSystemMessage(content) { this.chatAddSystemMessage(content); this.$forceUpdate(); this.conditionalScrollToBottom(); }, toggleBlock(blockId) { if (!blockId) { return; } if (this.expandedBlocks && this.expandedBlocks.has(blockId)) { this.chatCollapseBlock(blockId); } else { this.chatExpandBlock(blockId); } }, handleThinkingScroll(blockId, event) { if (!blockId || !event || !event.target) { return; } const el = event.target; const threshold = 12; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold; this.chatSetThinkingLock(blockId, atBottom); }, getToolIcon, getToolAnimationClass, getToolStatusText, getToolDescription, cloneToolArguments, buildToolLabel, formatSearchTopic, formatSearchTime, getLanguageClass, scrollToBottom() { scrollToBottomHelper(this); }, conditionalScrollToBottom() { conditionalScrollToBottomHelper(this); }, toggleScrollLock() { toggleScrollLockHelper(this); }, scrollThinkingToBottom(blockId) { scrollThinkingToBottomHelper(this, blockId); }, // 面板调整方法 startResize(panel, event) { startPanelResize(this, panel, event); }, handleResize(event) { handlePanelResize(this, event); }, stopResize() { stopPanelResize(this); }, async handleEasterEggPayload(payload) { const controller = useEasterEgg(); await controller.handlePayload(payload, this); }, async startEasterEggEffect(effectName, payload = {}) { const controller = useEasterEgg(); await controller.startEffect(effectName, payload, this); }, destroyEasterEggEffect(forceImmediate = false) { const controller = useEasterEgg(); return controller.destroyEffect(forceImmediate); }, finishEasterEggCleanup() { const controller = useEasterEgg(); controller.finishCleanup(); }, formatTokenCount, formatBytes, formatPercentage, formatRate, quotaTypeLabel, formatResetTime, formatQuotaValue, quotaResetSummary() { return buildQuotaResetSummary(this.usageQuota); }, isQuotaExceeded(type) { return isQuotaExceededUtil(this.usageQuota, type); }, showQuotaToast(payload) { if (!payload) { return; } const type = payload.type || 'fast'; const message = buildQuotaToastMessage(type, this.usageQuota, payload.reset_at); this.uiShowQuotaToastMessage(message, type); }, confirmAction(options = {}) { return this.uiRequestConfirm(options); } } }; (appOptions as any).components = { ChatArea, ConversationSidebar, LeftPanel, FocusPanel, TokenDrawer, PersonalizationDrawer, QuickMenu, InputComposer, AppShell }; export default appOptions;