// @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 LiquidGlassWidget from './components/experiments/LiquidGlassWidget.vue'; import QuickMenu from './components/input/QuickMenu.vue'; import InputComposer from './components/input/InputComposer.vue'; import AppShell from './components/shell/AppShell.vue'; import ImagePicker from './components/overlay/ImagePicker.vue'; import ConversationReviewDialog from './components/overlay/ConversationReviewDialog.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 { useModelStore } from './stores/model'; import { useChatActionStore } from './stores/chatActions'; import { useMonitorStore } from './stores/monitor'; import { usePolicyStore } from './stores/policy'; 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 as baseGetToolStatusText, getToolDescription, cloneToolArguments, buildToolLabel, formatSearchTopic, formatSearchTime, formatSearchDomains, getLanguageClass } from './utils/chatDisplay'; import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, toggleScrollLock as toggleScrollLockHelper, normalizeScrollLock, scrollThinkingToBottom as scrollThinkingToBottomHelper } from './composables/useScrollControl'; import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from './composables/usePanelResize'; function normalizeShowImageSrc(src: string) { if (!src) return ''; const trimmed = src.trim(); if (/^https?:\/\//i.test(trimmed)) return trimmed; if (trimmed.startsWith('/user_upload/')) return trimmed; // 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx const idx = trimmed.toLowerCase().indexOf('/user_upload/'); if (idx >= 0) { return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length); } if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) { return trimmed; } return ''; } function isSafeImageSrc(src: string) { return !!normalizeShowImageSrc(src); } function escapeHtml(input: string) { return input .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderShowImages(root: ParentNode | null = document) { if (!root) return; // 处理因自闭合解析导致的嵌套:把子 show_image 平铺到父后面 const nested = Array.from(root.querySelectorAll('show_image show_image')).reverse(); nested.forEach(child => { const parent = child.parentElement; if (parent && parent !== root) { parent.after(child); } }); const nodes = Array.from(root.querySelectorAll('show_image:not([data-rendered])')).reverse(); nodes.forEach(node => { // 将 show_image 内误被包裹的内容移动到当前节点之后,保持原有顺序 if (node.parentNode && node.firstChild) { const parent = node.parentNode; const ref = node.nextSibling; // 可能为 null,insertBefore 会当 append const children = Array.from(node.childNodes); children.forEach(child => parent.insertBefore(child, ref)); } const rawSrc = node.getAttribute('src') || ''; const mappedSrc = normalizeShowImageSrc(rawSrc); if (!mappedSrc) { node.setAttribute('data-rendered', '1'); node.setAttribute('data-rendered-error', 'invalid-src'); return; } const alt = node.getAttribute('alt') || ''; const safeAlt = escapeHtml(alt.trim()); const figure = document.createElement('figure'); figure.className = 'chat-inline-image'; const img = document.createElement('img'); img.loading = 'lazy'; img.src = mappedSrc; img.alt = safeAlt; img.onerror = () => { figure.classList.add('chat-inline-image--error'); const tip = document.createElement('div'); tip.className = 'chat-inline-image__error'; tip.textContent = '图片加载失败'; figure.appendChild(tip); }; figure.appendChild(img); if (safeAlt) { const caption = document.createElement('figcaption'); caption.innerHTML = safeAlt; figure.appendChild(caption); } node.replaceChildren(figure); node.setAttribute('data-rendered', '1'); }); } let showImageObserver: MutationObserver | null = null; function setupShowImageObserver() { if (showImageObserver) return; const container = document.querySelector('.messages-area') || document.body; if (!container) return; renderShowImages(container); showImageObserver = new MutationObserver(() => renderShowImages(container)); showImageObserver.observe(container, { childList: true, subtree: true }); } function teardownShowImageObserver() { if (showImageObserver) { showImageObserver.disconnect(); showImageObserver = null; } } 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 = true; function debugLog(...args) { if (!ENABLE_APP_DEBUG_LOGS) return; try { console.log('[app]', ...args); } catch (e) { /* ignore logging errors */ } } // 临时排查对话切换问题的调试输出 const TRACE_CONV = true; const traceLog = (...args) => { if (!TRACE_CONV) return; try { console.log('[conv-trace]', ...args); } catch (e) { // ignore } }; const SEARCH_INITIAL_BATCH = 100; const SEARCH_MORE_BATCH = 50; const SEARCH_PREVIEW_LIMIT = 20; const appOptions = { data() { return { // 路由相关 initialRouteResolved: false, dropToolEvents: false, // 工具状态跟踪 preparingTools: new Map(), activeTools: new Map(), toolActionIndex: new Map(), toolStacks: new Map(), // 当前任务是否仍在进行中(用于保持输入区的“停止”状态) taskInProgress: false, // 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次 lastHistoryLoadedConversationId: null, // ========================================== // 对话管理相关状态 // ========================================== // 搜索功能 // ========================================== searchRequestSeq: 0, searchActiveQuery: '', searchResultIdSet: new Set(), searchPreviewCache: {}, // Token统计相关状态(修复版) // ========================================== // 对话压缩状态 compressing: false, skipConversationLoadedEvent: false, skipConversationHistoryReload: false, _scrollListenerReady: false, historyLoading: false, historyLoadingFor: null, historyLoadSeq: 0, blankHeroActive: false, blankHeroExiting: false, blankWelcomeText: '', lastBlankConversationId: null, // 对话标题打字效果 titleTypingText: '', titleTypingTarget: '', titleTypingTimer: null, titleReady: false, suppressTitleTyping: false, headerMenuOpen: false, blankWelcomePool: [ '有什么可以帮忙的?', '想了解些热点吗?', '要我帮你完成作业吗?', '整点代码?', '随便聊点什么?', '想让我帮你整理一下思路吗?', '要不要我帮你写个小工具?', '发我一句话,我来接着做。' ], mobileViewportQuery: null, modeMenuOpen: false, modelMenuOpen: false, imageEntries: [], imageLoading: false, videoEntries: [], videoLoading: false, conversationHasImages: false, conversationHasVideos: false, conversationListRequestSeq: 0, conversationListRefreshToken: 0, // 工具控制菜单 icons: ICONS, toolCategoryIcons: TOOL_CATEGORY_ICON_MAP, // 对话回顾 reviewDialogOpen: false, reviewSelectedConversationId: null, reviewSubmitting: false, reviewPreviewLines: [], reviewPreviewLoading: false, reviewPreviewError: null, reviewPreviewLimit: 20, reviewSendToModel: true, reviewGeneratedPath: null } }, 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); }); } // 并行启动路由解析与实时连接,避免等待路由加载阻塞思考模式同步 const routePromise = this.bootstrapRoute(); const socketPromise = this.initSocket(); await routePromise; await socketPromise; this.$nextTick(() => { this.ensureScrollListener(); // 刷新后若无输出,自动解锁滚动锁定 normalizeScrollLock(this); }); setupShowImageObserver(); // 立即加载初始数据(并行获取状态,优先同步运行模式) this.loadInitialData(); document.addEventListener('click', this.handleClickOutsideQuickMenu); document.addEventListener('click', this.handleClickOutsidePanelMenu); document.addEventListener('click', this.handleClickOutsideHeaderMenu); 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.resourceBindContainerVisibilityWatcher(); this.resourceStartContainerStatsPolling(); this.resourceStartProjectStoragePolling(); this.resourceStartUsageQuotaPolling(); }, computed: { ...mapWritableState(useConnectionStore, [ 'isConnected', 'socket', 'stopRequested', 'projectPath', 'agentVersion', 'thinkingMode', 'runMode' ]), ...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']), ...mapWritableState(useUiStore, [ 'sidebarCollapsed', 'workspaceCollapsed', 'chatDisplayMode', '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', 'searchResults', 'searchActive', 'searchInProgress', 'searchMoreAvailable', 'searchOffset', 'searchTotal', 'conversationsOffset', 'conversationsLimit' ]), ...mapWritableState(useModelStore, ['currentModelKey']), ...mapState(useModelStore, ['models']), ...mapWritableState(useChatStore, [ 'messages', 'currentMessageIndex', 'streamingMessage', 'expandedBlocks', 'autoScrollEnabled', 'userScrolling', 'thinkingScrollLocks' ]), ...mapWritableState(useInputStore, [ 'inputMessage', 'inputLineCount', 'inputIsMultiline', 'inputIsFocused', 'quickMenuOpen', 'toolMenuOpen', 'settingsOpen', 'imagePickerOpen', 'videoPickerOpen', 'selectedImages', 'selectedVideos' ]), resolvedRunMode() { const allowed = ['fast', 'thinking', 'deep']; if (allowed.includes(this.runMode)) { return this.runMode; } return this.thinkingMode ? 'thinking' : 'fast'; }, headerRunModeOptions() { return [ { value: 'fast', label: '快速模式', desc: '低思考,响应更快' }, { value: 'thinking', label: '思考模式', desc: '更长思考,综合回答' }, { value: 'deep', label: '深度思考', desc: '持续推理,适合复杂任务' } ]; }, headerRunModeLabel() { const current = this.headerRunModeOptions.find((o) => o.value === this.resolvedRunMode); return current ? current.label : '快速模式'; }, currentModelLabel() { const modelStore = useModelStore(); return modelStore.currentModel?.label || 'Kimi-k2.5'; }, policyUiBlocks() { const store = usePolicyStore(); return store.uiBlocks || {}; }, adminDisabledModels() { const store = usePolicyStore(); return store.disabledModelSet; }, modelOptions() { const disabledSet = this.adminDisabledModels || new Set(); const options = this.models || []; return options.map((opt) => ({ ...opt, disabled: disabledSet.has(opt.key) })).filter((opt) => true); }, titleRibbonVisible() { return !this.isMobileViewport && this.chatDisplayMode === 'chat'; }, ...mapWritableState(useToolStore, [ 'preparingTools', 'activeTools', 'toolActionIndex', 'toolStacks', 'toolSettings', 'toolSettingsLoading' ]), ...mapWritableState(useResourceStore, [ 'tokenPanelCollapsed', 'currentContextTokens', 'currentConversationTokens', 'projectStorage', 'containerStatus', 'containerNetRate', 'usageQuota' ]), ...mapWritableState(useFocusStore, ['focusedFiles']), ...mapWritableState(useUploadStore, ['uploading', 'mediaUploading']) , ...mapState(useMonitorStore, { monitorIsLocked: (store) => store.isLocked }) , displayModeSwitchDisabled() { return !!this.policyUiBlocks.block_virtual_monitor; }, displayLockEngaged() { return false; }, streamingUi() { return this.streamingMessage || this.hasPendingToolActions(); }, composerBusy() { const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor'; return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested; }, composerHeroActive() { return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive; }, composerInteractionActive() { const hasText = !!(this.inputMessage && this.inputMessage.trim().length > 0); const hasImages = Array.isArray(this.selectedImages) && this.selectedImages.length > 0; return this.quickMenuOpen || hasText || hasImages; } }, beforeUnmount() { document.removeEventListener('click', this.handleClickOutsideQuickMenu); document.removeEventListener('click', this.handleClickOutsidePanelMenu); document.removeEventListener('click', this.handleClickOutsideHeaderMenu); 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(); teardownShowImageObserver(); if (this.titleTypingTimer) { clearInterval(this.titleTypingTimer); this.titleTypingTimer = null; } const cleanup = this.destroyEasterEggEffect(true); if (cleanup && typeof cleanup.catch === 'function') { cleanup.catch(() => {}); } }, watch: { inputMessage() { this.autoResizeInput(); }, messages: { deep: true, handler() { this.refreshBlankHeroState(); } }, currentConversationTitle(newVal, oldVal) { const target = (newVal && newVal.trim()) || ''; if (this.suppressTitleTyping) { this.titleTypingText = target; this.titleTypingTarget = target; return; } const previous = (oldVal && oldVal.trim()) || (this.titleTypingText && this.titleTypingText.trim()) || ''; const placeholderPrev = !previous || previous === '新对话'; const placeholderTarget = !target || target === '新对话'; const animate = placeholderPrev && !placeholderTarget; // 仅从空/占位切换到真实标题时动画 this.startTitleTyping(target, { animate }); }, currentConversationId: { immediate: false, handler(newValue, oldValue) { debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); traceLog('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload, historyLoading: this.historyLoading, historyLoadingFor: this.historyLoadingFor, historyLoadSeq: this.historyLoadSeq }); this.refreshBlankHeroState(); 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(); } } , fileTree: { immediate: true, handler(newValue) { this.monitorSyncDesktop(newValue); } } }, 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(); }, applyPolicyUiLocks() { const policyStore = usePolicyStore(); const blocks = policyStore.uiBlocks; if (blocks.collapse_workspace) { this.uiSetWorkspaceCollapsed(true); } if (blocks.block_virtual_monitor && this.chatDisplayMode === 'monitor') { this.uiSetChatDisplayMode('chat'); } }, isPolicyBlocked(key: string, message?: string) { const policyStore = usePolicyStore(); if (policyStore.uiBlocks[key]) { this.uiPushToast({ title: '已被管理员禁用', message: message || '被管理员强制禁用', type: 'warning' }); return true; } return false; }, 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; } if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) { this.uiSetWorkspaceCollapsed(true); return; } const nextState = !this.workspaceCollapsed; this.uiSetWorkspaceCollapsed(nextState); if (nextState) { this.uiSetPanelMenuOpen(false); } }, handleDisplayModeToggle() { if (this.displayModeSwitchDisabled) { // 显式提示管理员禁用 this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用'); return; } if (this.chatDisplayMode === 'chat' && this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')) { return; } const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat'; this.uiSetChatDisplayMode(next); }, 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', uiSetChatDisplayMode: 'setChatDisplayMode', 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(useModelStore, { modelSet: 'setModel' }), ...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', inputSetImagePickerOpen: 'setImagePickerOpen', inputSetSelectedImages: 'setSelectedImages', inputAddSelectedImage: 'addSelectedImage', inputClearSelectedImages: 'clearSelectedImages', inputRemoveSelectedImage: 'removeSelectedImage', inputSetVideoPickerOpen: 'setVideoPickerOpen', inputSetSelectedVideos: 'setSelectedVideos', inputAddSelectedVideo: 'addSelectedVideo', inputClearSelectedVideos: 'clearSelectedVideos', inputRemoveSelectedVideo: 'removeSelectedVideo' }), ...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', resourceBindContainerVisibilityWatcher: 'bindContainerVisibilityWatcher', resourcePollProjectStorage: 'pollProjectStorage', resourceFetchUsageQuota: 'fetchUsageQuota', resourceResetTokenStatistics: 'resetTokenStatistics', resourceSetUsageQuota: 'setUsageQuota' }), ...mapActions(useUploadStore, { uploadHandleSelected: 'handleSelectedFiles', uploadBatchFiles: 'uploadFiles' }), ...mapActions(useFileStore, { fileFetchTree: 'fetchFileTree', fileSetTreeFromResponse: 'setFileTreeFromResponse', fileFetchTodoList: 'fetchTodoList', fileSetTodoList: 'setTodoList', fileHideContextMenu: 'hideContextMenu', fileMarkTreeUnavailable: 'markFileTreeUnavailable' }), ...mapActions(useMonitorStore, { monitorSyncDesktop: 'syncDesktopFromTree', monitorResetVisual: 'resetVisualState', monitorResetSpeech: 'resetSpeechBuffer', monitorShowSpeech: 'enqueueModelSpeech', monitorShowThinking: 'enqueueModelThinking', monitorEndModelOutput: 'endModelOutput', monitorShowPendingReply: 'showPendingReply', monitorPreviewTool: 'previewToolIntent', monitorQueueTool: 'enqueueToolEvent', monitorResolveTool: 'resolveToolResult' }), ...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; }, isOutputActive() { return !!(this.streamingMessage || this.taskInProgress || this.hasPendingToolActions()); }, 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() { if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) { return; } 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() { // 在路由解析期间抑制标题动画,避免预置“新对话”闪烁 this.suppressTitleTyping = true; this.titleReady = false; this.currentConversationTitle = ''; this.titleTypingText = ''; const path = window.location.pathname.replace(/^\/+/, ''); if (!path || path === 'new') { this.currentConversationId = null; this.currentConversationTitle = '新对话'; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping('新对话', { animate: false }); 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 || ''; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping(this.currentConversationTitle, { animate: false }); history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`); } else { history.replaceState({}, '', '/new'); this.currentConversationId = null; this.currentConversationTitle = '新对话'; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping('新对话', { animate: false }); } } catch (error) { console.warn('初始化路由失败:', error); history.replaceState({}, '', '/new'); this.currentConversationId = null; this.currentConversationTitle = '新对话'; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping('新对话', { animate: false }); } 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; const activeLock = this.autoScrollEnabled && this.isOutputActive(); // 锁定且当前有输出时,强制贴底 if (activeLock) { if (!isAtBottom) { if (typeof this._setScrollingFlag === 'function') { this._setScrollingFlag(true); } messagesArea.scrollTop = messagesArea.scrollHeight; if (typeof this._setScrollingFlag === 'function') { setTimeout(() => this._setScrollingFlag && this._setScrollingFlag(false), 16); } } // 保持锁定状态下 userScrolling 为 false this.chatSetScrollState({ userScrolling: false }); return; } // 未锁定或无输出:允许自由滚动,仅记录位置 this.chatSetScrollState({ userScrolling: !isAtBottom }); }); }, 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', options: { preserveMonitorWindows?: boolean } = {}) { debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId }); this.logMessageState('resetAllStates:before-cleanup', { reason }); this.fileHideContextMenu(); this.monitorResetVisual({ preserveBubble: true, preservePointer: true, preserveWindows: !!options?.preserveMonitorWindows, preserveQueue: !!options?.preserveMonitorWindows }); // 重置消息和流状态 this.streamingMessage = false; this.currentMessageIndex = -1; this.stopRequested = false; this.taskInProgress = false; this.dropToolEvents = 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'; } }); } }); // 清理Markdown缓存 if (this.markdownCache) { this.markdownCache.clear(); } this.chatClearThinkingLocks(); // 强制更新视图 this.$forceUpdate(); this.inputSetSettingsOpen(false); this.inputSetToolMenuOpen(false); this.inputSetQuickMenuOpen(false); this.modeMenuOpen = false; this.inputSetLineCount(1); this.inputSetMultiline(false); this.inputClearMessage(); this.inputClearSelectedImages(); this.inputSetImagePickerOpen(false); this.imageEntries = []; this.imageLoading = false; this.conversationHasImages = false; this.toolSetSettingsLoading(false); this.toolSetSettings([]); debugLog('前端状态重置完成'); this._scrollListenerReady = false; this.$nextTick(() => { this.ensureScrollListener(); }); // 重置已加载对话标记,便于后续重新加载新对话历史 this.lastHistoryLoadedConversationId = null; this.logMessageState('resetAllStates:after-cleanup', { reason }); }, scheduleResetAfterTask(reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {}) { const start = Date.now(); const maxWait = 4000; const interval = 200; const tryReset = () => { if (!this.monitorIsLocked || Date.now() - start >= maxWait) { this.resetAllStates(reason, options); return; } setTimeout(tryReset, interval); }; tryReset(); }, // 重置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('加载初始数据...'); 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); // 立即更新配额和运行模式,避免等待其他慢接口 this.fetchUsageQuota(); // 拉取管理员策略 const policyStore = usePolicyStore(); await policyStore.fetchPolicy(); this.applyPolicyUiLocks(); const focusPromise = this.focusFetchFiles(); const todoPromise = this.fileFetchTodoList(); let treePromise: Promise | null = null; const isHostMode = statusData?.container?.mode === 'host'; if (isHostMode) { this.fileMarkTreeUnavailable('宿主机模式下文件树不可用'); } else { treePromise = this.fileFetchTree(); } // 获取当前对话信息 const statusConversationId = statusData.conversation && statusData.conversation.current_id; if (statusConversationId && !this.currentConversationId) { this.skipConversationHistoryReload = true; // 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载 this.skipConversationLoadedEvent = true; this.suppressTitleTyping = true; this.titleReady = false; this.currentConversationTitle = ''; this.titleTypingText = ''; 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; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping(this.currentConversationTitle, { animate: false }); } else { this.titleReady = true; this.suppressTitleTyping = false; const fallbackTitle = this.currentConversationTitle || '新对话'; this.currentConversationTitle = fallbackTitle; this.startTitleTyping(fallbackTitle, { animate: false }); } // 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发 if ( this.lastHistoryLoadedConversationId !== this.currentConversationId || !Array.isArray(this.messages) || this.messages.length === 0 ) { await this.fetchAndDisplayHistory(); } // 获取当前对话的Token统计 this.fetchConversationTokenStatistics(); this.updateCurrentContextTokens(); } catch (e) { console.warn('获取当前对话标题失败:', e); this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping(this.currentConversationTitle || '新对话', ''); } } // 等待其他加载项完成(允许部分失败不阻塞模式切换) const pendingPromises = [focusPromise, todoPromise]; if (treePromise) { pendingPromises.push(treePromise); } await Promise.allSettled(pendingPromises); 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); if (status && typeof status.thinking_mode !== 'undefined') { this.thinkingMode = !!status.thinking_mode; } if (status && typeof status.run_mode === 'string') { this.runMode = status.run_mode; } else if (status && typeof status.thinking_mode !== 'undefined') { this.runMode = status.thinking_mode ? 'thinking' : 'fast'; } if (status && typeof status.model_key === 'string') { this.modelSet(status.model_key); } if (status && typeof status.has_images !== 'undefined') { this.conversationHasImages = !!status.has_images; } if (status && typeof status.has_videos !== 'undefined') { this.conversationHasVideos = !!status.has_videos; } }, 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() { const queryOffset = this.conversationsOffset; const queryLimit = this.conversationsLimit; const refreshToken = queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken; const requestSeq = ++this.conversationListRequestSeq; this.conversationsLoading = true; try { const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}`); const data = await response.json(); if (data.success) { if (refreshToken !== this.conversationListRefreshToken) { debugLog('忽略已过期的对话列表响应', { requestSeq, responseOffset: queryOffset }); return; } if (queryOffset === 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 { if (refreshToken === this.conversationListRefreshToken) { 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, options = {}) { const force = Boolean(options.force); debugLog('加载对话:', conversationId); traceLog('loadConversation:start', { conversationId, currentConversationId: this.currentConversationId, force }); this.logMessageState('loadConversation:start', { conversationId, force }); this.suppressTitleTyping = true; this.titleReady = false; this.currentConversationTitle = ''; this.titleTypingText = ''; if (!force && conversationId === this.currentConversationId) { debugLog('已是当前对话,跳过加载'); traceLog('loadConversation:skip-same', { conversationId }); this.suppressTitleTyping = false; this.titleReady = true; 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); traceLog('loadConversation:api-success', { conversationId, title: result.title }); // 2. 更新当前对话信息 this.skipConversationHistoryReload = true; this.currentConversationId = conversationId; this.currentConversationTitle = result.title; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping(this.currentConversationTitle, { animate: false }); this.promoteConversationToTop(conversationId); history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`); this.skipConversationLoadedEvent = true; // 3. 重置UI状态 this.resetAllStates(`loadConversation:${conversationId}`); this.subAgentFetch(); this.fetchTodoList(); // 4. 立即加载历史和统计,确保列表切换后界面同步更新 await this.fetchAndDisplayHistory(); this.fetchConversationTokenStatistics(); this.updateCurrentContextTokens(); traceLog('loadConversation:after-history', { conversationId, messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a' }); } else { console.error('对话加载失败:', result.message); this.suppressTitleTyping = false; this.titleReady = true; this.uiPushToast({ title: '加载对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { console.error('加载对话异常:', error); traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) }); this.suppressTitleTyping = false; this.titleReady = true; 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(options = {}) { const { force = false } = options as { force?: boolean }; const targetConversationId = this.currentConversationId; if (!targetConversationId || targetConversationId.startsWith('temp_')) { debugLog('没有当前对话ID,跳过历史加载'); this.refreshBlankHeroState(); return; } // 若同一对话正在加载,直接复用;若是切换对话则允许并发但后来的请求会赢 if (this.historyLoading && this.historyLoadingFor === targetConversationId) { debugLog('同一对话历史正在加载,跳过重复请求'); return; } // 已经有完整历史且非强制刷新时,避免重复加载导致动画播放两次 const alreadyHydrated = !force && this.lastHistoryLoadedConversationId === targetConversationId && Array.isArray(this.messages) && this.messages.length > 0; if (alreadyHydrated) { debugLog('历史已加载,跳过重复请求'); this.logMessageState('fetchAndDisplayHistory:skip-duplicate', { conversationId: targetConversationId }); return; } const loadSeq = ++this.historyLoadSeq; this.historyLoading = true; this.historyLoadingFor = targetConversationId; try { debugLog('开始获取历史对话内容...'); this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId }); try { // 使用专门的API获取对话消息历史 const messagesResponse = await fetch(`/api/conversations/${targetConversationId}/messages`); if (!messagesResponse.ok) { console.warn('无法获取消息历史,尝试备用方法'); // 备用方案:通过状态API获取 const statusResponse = await fetch('/api/status'); const status = await statusResponse.json(); debugLog('系统状态:', status); this.applyStatusSnapshot(status); // 如果状态中有对话历史字段 if (status.conversation_history && Array.isArray(status.conversation_history)) { this.renderHistoryMessages(status.conversation_history); return; } debugLog('备用方案也无法获取历史消息'); return; } // 如果在等待期间用户已切换到其他对话,则丢弃结果 if (loadSeq !== this.historyLoadSeq || this.currentConversationId !== targetConversationId) { debugLog('检测到对话已切换,丢弃过期的历史加载结果'); return; } const messagesData = await messagesResponse.json(); debugLog('获取到消息数据:', messagesData); if (messagesData.success && messagesData.data && messagesData.data.messages) { const messages = messagesData.data.messages; debugLog(`发现 ${messages.length} 条历史消息`); if (messages.length > 0) { // 清空当前显示的消息 this.logMessageState('fetchAndDisplayHistory:before-clear-existing'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:after-clear-existing'); // 渲染历史消息 - 这是关键功能 this.renderHistoryMessages(messages); // 滚动到底部 this.$nextTick(() => { this.scrollToBottom(); }); debugLog('历史对话内容显示完成'); } else { debugLog('对话存在但没有历史消息'); this.logMessageState('fetchAndDisplayHistory:no-history-clear'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:no-history-cleared'); } } else { debugLog('消息数据格式不正确:', messagesData); this.logMessageState('fetchAndDisplayHistory:invalid-data-clear'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared'); } } catch (error) { console.error('获取历史对话失败:', error); debugLog('尝试不显示错误弹窗,仅在控制台记录'); // 不显示alert,避免打断用户体验 this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) }); this.messages = []; this.logMessageState('fetchAndDisplayHistory:error-cleared'); } } finally { // 仅在本次加载仍是最新请求时清除 loading 状态 if (loadSeq === this.historyLoadSeq) { this.historyLoading = false; this.historyLoadingFor = null; } this.refreshBlankHeroState(); } }, // ========================================== // 关键功能:渲染历史消息 // ========================================== renderHistoryMessages(historyMessages) { debugLog('开始渲染历史消息...', historyMessages); debugLog('历史消息数量:', historyMessages.length); this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length }); if (!Array.isArray(historyMessages)) { console.error('历史消息不是数组格式'); return; } let currentAssistantMessage = null; let historyHasImages = false; let historyHasVideos = false; historyMessages.forEach((message, index) => { debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message); const meta = message.metadata || {}; if (message.role === 'user' && (meta.system_injected_image || meta.system_injected_video)) { debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)'); return; } if (message.role === 'user') { // 用户消息 - 先结束之前的assistant消息 if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { this.messages.push(currentAssistantMessage); currentAssistantMessage = null; } const images = message.images || (message.metadata && message.metadata.images) || []; const videos = message.videos || (message.metadata && message.metadata.videos) || []; if (Array.isArray(images) && images.length) { historyHasImages = true; } if (Array.isArray(videos) && videos.length) { historyHasVideos = true; } this.messages.push({ role: 'user', content: message.content || '', images, videos }); debugLog('添加用户消息:', message.content?.substring(0, 50) + '...'); } else if (message.role === 'assistant') { // AI消息 - 如果没有当前assistant消息,创建一个 if (!currentAssistantMessage) { currentAssistantMessage = { role: 'assistant', actions: [], streamingThinking: '', streamingText: '', currentStreamingType: null, activeThinkingId: null, awaitingFirstContent: false, generatingLabel: '' }; } const content = message.content || ''; const reasoningText = (message.reasoning_content || '').trim(); if (reasoningText) { const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`; currentAssistantMessage.actions.push({ id: `history-think-${Date.now()}-${Math.random()}`, type: 'thinking', content: reasoningText, streaming: false, 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 || ''); const textContent = content.trim(); if (appendPayloadMeta) { currentAssistantMessage.actions.push({ id: `history-append-payload-${Date.now()}-${Math.random()}`, type: 'append_payload', append: { path: appendPayloadMeta.path || '未知文件', forced: !!appendPayloadMeta.forced, success: appendPayloadMeta.success === undefined ? true : !!appendPayloadMeta.success, lines: appendPayloadMeta.lines ?? null, bytes: appendPayloadMeta.bytes ?? null }, timestamp: Date.now() }); debugLog('添加append占位信息:', appendPayloadMeta.path); } else if (modifyPayloadMeta) { currentAssistantMessage.actions.push({ id: `history-modify-payload-${Date.now()}-${Math.random()}`, type: 'modify_payload', modify: { path: modifyPayloadMeta.path || '未知文件', total: modifyPayloadMeta.total_blocks ?? null, completed: modifyPayloadMeta.completed || [], failed: modifyPayloadMeta.failed || [], forced: !!modifyPayloadMeta.forced, details: modifyPayloadMeta.details || [] }, timestamp: Date.now() }); debugLog('添加modify占位信息:', modifyPayloadMeta.path); } if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) { currentAssistantMessage.actions.push({ id: `history-text-${Date.now()}-${Math.random()}`, type: 'text', content: textContent, streaming: false, timestamp: Date.now() }); debugLog('添加文本内容:', textContent.substring(0, 50) + '...'); } // 处理工具调用 if (message.tool_calls && Array.isArray(message.tool_calls)) { message.tool_calls.forEach((toolCall, tcIndex) => { let arguments_obj = {}; try { arguments_obj = typeof toolCall.function.arguments === 'string' ? JSON.parse(toolCall.function.arguments || '{}') : (toolCall.function.arguments || {}); } catch (e) { console.warn('解析工具参数失败:', e); arguments_obj = {}; } const action = { id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`, type: 'tool', tool: { id: toolCall.id, name: toolCall.function.name, arguments: arguments_obj, intent_full: arguments_obj.intent || '', intent_rendered: arguments_obj.intent || '', status: 'preparing', result: null }, timestamp: Date.now() }; // 如果是历史加载的动作且状态仍为进行中,标记为 stale,避免刷新后按钮卡死 if (['preparing', 'running', 'awaiting_content'].includes(action.tool.status)) { action.tool.status = 'stale'; action.tool.awaiting_content = false; action.streaming = false; } currentAssistantMessage.actions.push(action); debugLog('添加工具调用:', toolCall.function.name); }); } } else if (message.role === 'tool') { // 工具结果 - 更新当前assistant消息中对应的工具 if (currentAssistantMessage) { // 查找对应的工具action - 使用更灵活的匹配 let toolAction = null; // 优先按tool_call_id匹配 if (message.tool_call_id) { toolAction = currentAssistantMessage.actions.find(action => action.type === 'tool' && action.tool.id === message.tool_call_id ); } // 如果找不到,按name匹配最后一个同名工具 if (!toolAction && message.name) { const sameNameTools = currentAssistantMessage.actions.filter(action => action.type === 'tool' && action.tool.name === message.name ); toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个 } if (toolAction) { // 解析工具结果(优先使用JSON,其次使用元数据的 tool_payload,以保证搜索结果在刷新后仍可展示) let result; try { result = JSON.parse(message.content); } catch (e) { if (message.metadata && message.metadata.tool_payload) { result = message.metadata.tool_payload; } else { result = { output: message.content, success: true }; } } toolAction.tool.status = 'completed'; toolAction.tool.result = result; if (result && typeof result === 'object') { if (result.error) { toolAction.tool.message = result.error; } else if (result.message && !toolAction.tool.message) { toolAction.tool.message = result.message; } } if (message.name === 'append_to_file' && result && result.message) { toolAction.tool.message = result.message; } debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`); // append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复 } else { console.warn('找不到对应的工具调用:', message.name, message.tool_call_id); } } } else { // 其他类型消息(如system)- 先结束当前assistant消息 if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { this.messages.push(currentAssistantMessage); currentAssistantMessage = null; } debugLog('处理其他类型消息:', message.role); this.messages.push({ role: message.role, content: message.content || '' }); } }); // 处理最后一个assistant消息 if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { this.messages.push(currentAssistantMessage); } this.conversationHasImages = historyHasImages; this.conversationHasVideos = historyHasVideos; debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`); this.logMessageState('renderHistoryMessages:after-render'); this.lastHistoryLoadedConversationId = this.currentConversationId || null; // 强制更新视图 this.$forceUpdate(); // 确保滚动到底部 this.$nextTick(() => { this.scrollToBottom(); setTimeout(() => { const blockCount = this.$el && this.$el.querySelectorAll ? this.$el.querySelectorAll('.message-block').length : 'N/A'; debugLog('[Messages] DOM 渲染统计', { blocks: blockCount, conversationId: this.currentConversationId }); }, 0); }); }, async createNewConversation() { debugLog('创建新对话...'); traceLog('createNewConversation:start', { currentConversationId: this.currentConversationId, convCount: Array.isArray(this.conversations) ? this.conversations.length : 'n/a' }); 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, mode: this.runMode }) }); const result = await response.json(); if (result.success) { const newConversationId = result.conversation_id; debugLog('新对话创建成功:', newConversationId); traceLog('createNewConversation:created', { newConversationId }); // 在本地列表插入占位,避免等待刷新 const placeholder = { id: newConversationId, title: '新对话', updated_at: new Date().toISOString(), total_messages: 0, total_tools: 0 }; this.conversations = [ placeholder, ...this.conversations.filter(conv => conv && conv.id !== newConversationId) ]; // 直接加载新对话,确保状态一致 // 如果 socket 事件已把 currentConversationId 设置为新ID,则强制加载一次以同步状态 await this.loadConversation(newConversationId, { force: true }); traceLog('createNewConversation:after-load', { newConversationId, currentConversationId: this.currentConversationId }); // 刷新对话列表获取最新统计 this.conversationsOffset = 0; await this.loadConversationsList(); traceLog('createNewConversation:after-refresh', { newConversationId, conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a' }); } 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' }); } }, handleSidebarSearchInput(value) { this.searchQuery = value; }, handleSidebarSearchSubmit(value) { this.searchQuery = value; const trimmed = String(value || '').trim(); if (!trimmed) { this.exitConversationSearch(); return; } this.startConversationSearch(trimmed); }, exitConversationSearch() { this.searchActive = false; this.searchInProgress = false; this.searchMoreAvailable = false; this.searchOffset = 0; this.searchTotal = 0; this.searchResults = []; this.searchActiveQuery = ''; this.searchResultIdSet = new Set(); this.conversationsOffset = 0; this.loadConversationsList(); }, async startConversationSearch(query) { const trimmed = String(query || '').trim(); if (!trimmed) { return; } const requestSeq = ++this.searchRequestSeq; this.searchActiveQuery = trimmed; this.searchActive = true; this.searchInProgress = true; this.searchMoreAvailable = false; this.searchOffset = 0; this.searchTotal = 0; this.searchResults = []; this.searchResultIdSet = new Set(); await this.searchNextConversationBatch(SEARCH_INITIAL_BATCH, requestSeq); }, async loadMoreSearchResults() { if (!this.searchActive || this.searchInProgress || !this.searchMoreAvailable) { return; } const requestSeq = this.searchRequestSeq; this.searchInProgress = true; await this.searchNextConversationBatch(SEARCH_MORE_BATCH, requestSeq); }, async searchNextConversationBatch(batchSize, requestSeq) { const query = this.searchActiveQuery; if (!query) { if (requestSeq === this.searchRequestSeq) { this.searchInProgress = false; } return; } try { const response = await fetch(`/api/conversations?limit=${batchSize}&offset=${this.searchOffset}`); const payload = await response.json(); if (requestSeq !== this.searchRequestSeq) { return; } if (!payload.success) { console.error('搜索对话失败:', payload.error || payload.message); this.searchInProgress = false; return; } const data = payload.data || {}; const conversations = data.conversations || []; if (!this.searchTotal) { this.searchTotal = data.total || 0; } for (const conv of conversations) { if (requestSeq !== this.searchRequestSeq) { return; } await this.matchConversation(conv, query, requestSeq); } this.searchOffset += conversations.length; this.searchMoreAvailable = this.searchOffset < (this.searchTotal || 0); } catch (error) { console.error('搜索对话异常:', error); } finally { if (requestSeq === this.searchRequestSeq) { this.searchInProgress = false; } } }, async matchConversation(conversation, query, requestSeq) { if (!conversation || !conversation.id) { return; } if (this.searchResultIdSet && this.searchResultIdSet.has(conversation.id)) { return; } const firstSentence = await this.getConversationFirstUserSentence(conversation.id, requestSeq); if (requestSeq !== this.searchRequestSeq) { return; } const queryLower = String(query || '').toLowerCase(); const combined = `${conversation.title || ''} ${firstSentence || ''}`.toLowerCase(); if (queryLower && combined.includes(queryLower)) { this.searchResults.push(conversation); this.searchResultIdSet.add(conversation.id); } }, async getConversationFirstUserSentence(conversationId, requestSeq) { if (!conversationId) { return ''; } if (this.searchPreviewCache && Object.prototype.hasOwnProperty.call(this.searchPreviewCache, conversationId)) { return this.searchPreviewCache[conversationId]; } try { const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${SEARCH_PREVIEW_LIMIT}`); const payload = await resp.json(); if (requestSeq !== this.searchRequestSeq) { return ''; } const lines = payload?.data?.preview || []; let firstUserLine = ''; for (const line of lines) { if (typeof line === 'string' && line.startsWith('user:')) { firstUserLine = line.slice('user:'.length).trim(); break; } } const firstSentence = this.extractFirstSentence(firstUserLine); const cached = firstSentence || firstUserLine || ''; if (!this.searchPreviewCache) { this.searchPreviewCache = {}; } this.searchPreviewCache[conversationId] = cached; return cached; } catch (error) { console.error('获取对话预览失败:', error); return ''; } }, extractFirstSentence(text) { if (!text) { return ''; } const normalized = String(text).replace(/\s+/g, ' ').trim(); const match = normalized.match(/(.+?[。!?.!?])/); if (match) { return match[1]; } return normalized; }, 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() { if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) { return; } this.personalizationOpenDrawer(); }, fetchTodoList() { return this.fileFetchTodoList(); }, fetchSubAgents() { return this.subAgentFetch(); }, async toggleThinkingMode() { await this.handleCycleRunMode(); }, handleQuickModeToggle() { if (!this.isConnected || this.streamingMessage) { return; } this.handleCycleRunMode(); }, triggerFileUpload() { if (this.uploading) { return; } const input = this.getComposerElement('fileUploadInput'); if (input) { input.click(); } }, handleFileSelected(files) { const policyStore = usePolicyStore(); if (policyStore.uiBlocks?.block_upload) { this.uiPushToast({ title: '上传被禁用', message: '已被管理员禁用上传功能', type: 'warning' }); return; } this.uploadHandleSelected(files); }, normalizeLocalFiles(files) { if (!files) return []; const list = Array.isArray(files) ? files : Array.from(files); return list.filter(Boolean); }, isImageFile(file) { const name = file?.name || ''; const type = file?.type || ''; return type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(name); }, isVideoFile(file) { const name = file?.name || ''; const type = file?.type || ''; return type.startsWith('video/') || /\.(mp4|mov|m4v|webm|avi|mkv|flv|mpg|mpeg)$/i.test(name); }, upsertImageEntry(path, filename) { if (!path) return; const name = filename || path.split('/').pop() || path; const list = Array.isArray(this.imageEntries) ? this.imageEntries : []; if (list.some((item) => item.path === path)) { return; } this.imageEntries = [{ name, path }, ...list]; }, upsertVideoEntry(path, filename) { if (!path) return; const name = filename || path.split('/').pop() || path; const list = Array.isArray(this.videoEntries) ? this.videoEntries : []; if (list.some((item) => item.path === path)) { return; } this.videoEntries = [{ name, path }, ...list]; }, async handleLocalImageFiles(files) { if (!this.isConnected) { return; } if (this.mediaUploading) { this.uiPushToast({ title: '上传中', message: '请等待当前图片上传完成', type: 'info' }); return; } const list = this.normalizeLocalFiles(files); if (!list.length) { return; } const existingCount = Array.isArray(this.selectedImages) ? this.selectedImages.length : 0; const remaining = Math.max(0, 9 - existingCount); if (!remaining) { this.uiPushToast({ title: '已达上限', message: '最多只能选择 9 张图片', type: 'warning' }); return; } const valid = list.filter((file) => this.isImageFile(file)); if (!valid.length) { this.uiPushToast({ title: '无法上传', message: '仅支持图片文件', type: 'warning' }); return; } if (valid.length < list.length) { this.uiPushToast({ title: '已忽略', message: '已跳过非图片文件', type: 'info' }); } const limited = valid.slice(0, remaining); if (valid.length > remaining) { this.uiPushToast({ title: '已超出数量', message: `最多还能添加 ${remaining} 张图片,已自动截断`, type: 'warning' }); } const uploaded = await this.uploadBatchFiles(limited, { markUploading: true, markMediaUploading: true }); if (!uploaded.length) { return; } uploaded.forEach((item) => { if (!item?.path) return; this.inputAddSelectedImage(item.path); this.upsertImageEntry(item.path, item.filename); }); }, async handleLocalVideoFiles(files) { if (!this.isConnected) { return; } if (this.mediaUploading) { this.uiPushToast({ title: '上传中', message: '请等待当前视频上传完成', type: 'info' }); return; } const list = this.normalizeLocalFiles(files); if (!list.length) { return; } const valid = list.filter((file) => this.isVideoFile(file)); if (!valid.length) { this.uiPushToast({ title: '无法上传', message: '仅支持视频文件', type: 'warning' }); return; } if (valid.length < list.length) { this.uiPushToast({ title: '已忽略', message: '已跳过非视频文件', type: 'info' }); } if (valid.length > 1) { this.uiPushToast({ title: '视频数量过多', message: '一次只能选择 1 个视频,已使用第一个', type: 'warning' }); } const [file] = valid; if (!file) { return; } const uploaded = await this.uploadBatchFiles([file], { markUploading: true, markMediaUploading: true }); const [item] = uploaded; if (!item?.path) { return; } this.inputSetSelectedVideos([item.path]); this.inputClearSelectedImages(); this.upsertVideoEntry(item.path, item.filename); }, handleSendOrStop() { if (this.composerBusy) { this.stopTask(); } else { this.sendMessage(); } }, sendMessage() { if (this.streamingUi || !this.isConnected) { return; } if (this.mediaUploading) { this.uiPushToast({ title: '上传中', message: '请等待图片/视频上传完成后再发送', type: 'info' }); return; } const text = (this.inputMessage || '').trim(); const images = Array.isArray(this.selectedImages) ? this.selectedImages.slice(0, 9) : []; const videos = Array.isArray(this.selectedVideos) ? this.selectedVideos.slice(0, 1) : []; const hasText = text.length > 0; const hasImages = images.length > 0; const hasVideos = videos.length > 0; if (!hasText && !hasImages && !hasVideos) { return; } const quotaType = this.thinkingMode ? 'thinking' : 'fast'; if (this.isQuotaExceeded(quotaType)) { this.showQuotaToast({ type: quotaType }); return; } if (hasImages && !['qwen3-vl-plus', 'kimi-k2.5'].includes(this.currentModelKey)) { this.uiPushToast({ title: '当前模型不支持图片', message: '请切换到 Qwen3.5 或 Kimi-k2.5 再发送图片', type: 'error' }); return; } if (hasVideos && !['qwen3-vl-plus', 'kimi-k2.5'].includes(this.currentModelKey)) { this.uiPushToast({ title: '当前模型不支持视频', message: '请切换到 Qwen3.5 或 Kimi-k2.5 后再发送视频', type: 'error' }); return; } if (hasVideos && hasImages) { this.uiPushToast({ title: '请勿同时发送', message: '视频与图片需分开发送,每条仅包含一种媒体', type: 'warning' }); return; } if (hasVideos) { this.uiPushToast({ title: '视频处理中', message: '读取视频需要较长时间,请耐心等待', type: 'info', duration: 5000 }); } const message = text; const isCommand = hasText && !hasImages && !hasVideos && message.startsWith('/'); if (isCommand) { this.socket.emit('send_command', { command: message }); this.inputClearMessage(); this.inputClearSelectedImages(); this.inputClearSelectedVideos(); this.autoResizeInput(); return; } const wasBlank = this.isConversationBlank(); if (wasBlank) { this.blankHeroExiting = true; this.blankHeroActive = true; setTimeout(() => { this.blankHeroExiting = false; this.blankHeroActive = false; }, 320); } // 标记任务进行中,直到任务完成或用户手动停止 this.taskInProgress = true; this.chatAddUserMessage(message, images, videos); this.socket.emit('send_message', { message: message, images, videos, conversation_id: this.currentConversationId }); if (typeof this.monitorShowPendingReply === 'function') { this.monitorShowPendingReply(); } this.inputClearMessage(); this.inputClearSelectedImages(); this.inputClearSelectedVideos(); this.inputSetImagePickerOpen(false); this.inputSetVideoPickerOpen(false); this.inputSetLineCount(1); this.inputSetMultiline(false); if (hasImages) { this.conversationHasImages = true; this.conversationHasVideos = false; } if (hasVideos) { this.conversationHasVideos = true; this.conversationHasImages = false; } if (this.autoScrollEnabled) { this.scrollToBottom(); } this.autoResizeInput(); // 发送消息后延迟更新当前上下文Token(关键修复:恢复原逻辑) setTimeout(() => { if (this.currentConversationId) { this.updateCurrentContextTokens(); } }, 1000); }, // 新增:停止任务方法 stopTask() { const canStop = this.composerBusy && !this.stopRequested; if (!canStop) { return; } const shouldDropToolEvents = this.streamingUi; this.stopRequested = true; this.dropToolEvents = shouldDropToolEvents; if (this.socket) { this.socket.emit('stop_task'); debugLog('发送停止请求'); } // 立即清理前端状态,避免出现“不可输入也不可停止”的卡死状态 this.clearPendingTools('user_stop'); this.streamingMessage = false; this.taskInProgress = false; this.forceUnlockMonitor('user_stop'); }, forceUnlockMonitor(reason = 'unspecified') { try { this.monitorResetVisual({ preserveBubble: true, preservePointer: true, preserveWindows: true, preserveQueue: false, preservePendingResults: false, preserveAwaitingTools: false }); debugLog('Monitor unlocked', { reason }); } catch (error) { console.warn('强制解锁监控面板失败', error); } }, clearPendingTools(reason = 'unspecified') { debugLog('清理未完成工具', { reason }); if (Array.isArray(this.messages)) { this.messages.forEach(msg => { if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) { return; } msg.actions.forEach(action => { if (action && action.type === 'tool' && action.tool) { action.tool.status = action.tool.status || 'cancelled'; action.tool.awaiting_content = false; action.streaming = false; } }); }); } this.toolResetTracking(); this.preparingTools.clear(); this.activeTools.clear(); this.toolActionIndex.clear(); this.toolStacks.clear(); this.stopRequested = false; this.taskInProgress = false; }, 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; } if (this.isPolicyBlocked('block_tool_toggle', '工具启用/禁用已被管理员锁定')) { return; } this.modeMenuOpen = false; this.modelMenuOpen = false; 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; } const opened = this.inputToggleQuickMenu(); if (!opened) { this.modeMenuOpen = false; this.modelMenuOpen = false; } }, closeQuickMenu() { this.inputCloseMenus(); this.modeMenuOpen = false; this.modelMenuOpen = false; }, async openImagePicker() { if (!['qwen3-vl-plus', 'kimi-k2.5'].includes(this.currentModelKey)) { this.uiPushToast({ title: '当前模型不支持图片', message: '请选择 Qwen3.5 或 Kimi-k2.5 后再发送图片', type: 'error' }); return; } this.closeQuickMenu(); this.inputSetImagePickerOpen(true); await this.loadWorkspaceImages(); }, closeImagePicker() { this.inputSetImagePickerOpen(false); }, async openVideoPicker() { if (!['qwen3-vl-plus', 'kimi-k2.5'].includes(this.currentModelKey)) { this.uiPushToast({ title: '当前模型不支持视频', message: '请切换到 Qwen3.5 或 Kimi-k2.5 后再发送视频', type: 'error' }); return; } this.closeQuickMenu(); this.inputSetVideoPickerOpen(true); await this.loadWorkspaceVideos(); }, closeVideoPicker() { this.inputSetVideoPickerOpen(false); }, async loadWorkspaceImages() { this.imageLoading = true; try { const entries = await this.fetchAllImageEntries(''); this.imageEntries = entries; if (!entries.length) { this.uiPushToast({ title: '未找到图片', message: '工作区内没有可用的图片文件', type: 'info' }); } } catch (error) { console.error('加载图片列表失败', error); this.uiPushToast({ title: '加载图片失败', message: error?.message || '请稍后重试', type: 'error' }); } finally { this.imageLoading = false; } }, async fetchAllImageEntries(startPath = '') { const queue: string[] = [startPath || '']; const visited = new Set(); const results: Array<{ name: string; path: string }> = []; const exts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg']); const maxFolders = 120; while (queue.length && visited.size < maxFolders) { const path = queue.shift() || ''; if (visited.has(path)) { continue; } visited.add(path); try { const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' } }); const data = await resp.json().catch(() => null); if (!data?.success) { continue; } const items = Array.isArray(data?.data?.items) ? data.data.items : []; for (const item of items) { const rawPath = item?.path || [path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/'); const type = String(item?.type || '').toLowerCase(); if (type === 'directory' || type === 'folder') { queue.push(rawPath); continue; } const ext = String(item?.extension || '').toLowerCase() || (rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : ''); if (exts.has(ext)) { results.push({ name: item?.name || rawPath.split('/').pop() || rawPath, path: rawPath }); if (results.length >= 400) { return results; } } } } catch (error) { console.warn('遍历文件夹失败', path, error); } } return results; }, async fetchAllVideoEntries(startPath = '') { const queue: string[] = [startPath || '']; const visited = new Set(); const results: Array<{ name: string; path: string }> = []; const exts = new Set(['.mp4', '.mov', '.mkv', '.avi', '.webm']); const maxFolders = 120; while (queue.length && visited.size < maxFolders) { const path = queue.shift() || ''; if (visited.has(path)) { continue; } visited.add(path); try { const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' } }); const data = await resp.json().catch(() => null); if (!data?.success) { continue; } const items = Array.isArray(data?.data?.items) ? data.data.items : []; for (const item of items) { const rawPath = item?.path || [path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/'); const type = String(item?.type || '').toLowerCase(); if (type === 'directory' || type === 'folder') { queue.push(rawPath); continue; } const ext = String(item?.extension || '').toLowerCase() || (rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : ''); if (exts.has(ext)) { results.push({ name: item?.name || rawPath.split('/').pop() || rawPath, path: rawPath }); if (results.length >= 200) { return results; } } } } catch (error) { console.warn('遍历文件夹失败', path, error); } } return results; }, async loadWorkspaceVideos() { this.videoLoading = true; try { const entries = await this.fetchAllVideoEntries(''); this.videoEntries = entries; if (!entries.length) { this.uiPushToast({ title: '未找到视频', message: '工作区内没有可用的视频文件', type: 'info' }); } } catch (error) { console.error('加载视频列表失败', error); this.uiPushToast({ title: '加载视频失败', message: error?.message || '请稍后重试', type: 'error' }); } finally { this.videoLoading = false; } }, handleImagesConfirmed(list) { this.inputSetSelectedImages(Array.isArray(list) ? list : []); this.inputSetImagePickerOpen(false); }, handleRemoveImage(path) { this.inputRemoveSelectedImage(path); }, handleVideosConfirmed(list) { const arr = Array.isArray(list) ? list.slice(0, 1) : []; this.inputSetSelectedVideos(arr); this.inputSetVideoPickerOpen(false); if (arr.length) { this.inputClearSelectedImages(); } }, handleRemoveVideo(path) { this.inputRemoveSelectedVideo(path); }, handleQuickUpload() { if (this.uploading || !this.isConnected) { return; } if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) { return; } this.triggerFileUpload(); }, toggleModeMenu() { if (!this.isConnected || this.streamingMessage) { return; } const next = !this.modeMenuOpen; this.modeMenuOpen = next; if (next) { this.modelMenuOpen = false; } if (next) { this.inputSetToolMenuOpen(false); this.inputSetSettingsOpen(false); if (!this.quickMenuOpen) { this.inputOpenQuickMenu(); } } }, toggleModelMenu() { if (!this.isConnected || this.streamingMessage) { return; } const next = !this.modelMenuOpen; this.modelMenuOpen = next; if (next) { this.modeMenuOpen = false; this.inputSetToolMenuOpen(false); this.inputSetSettingsOpen(false); if (!this.quickMenuOpen) { this.inputOpenQuickMenu(); } } }, toggleHeaderMenu() { if (!this.isConnected) return; this.headerMenuOpen = !this.headerMenuOpen; if (this.headerMenuOpen) { this.closeQuickMenu(); this.inputCloseMenus(); } }, async handleModeSelect(mode) { if (!this.isConnected || this.streamingMessage) { return; } await this.setRunMode(mode); }, async handleHeaderRunModeSelect(mode) { await this.handleModeSelect(mode); this.closeHeaderMenu(); }, async handleModelSelect(key) { if (!this.isConnected || this.streamingMessage) { return; } const policyStore = usePolicyStore(); if (policyStore.isModelDisabled(key)) { this.uiPushToast({ title: '模型被禁用', message: '被管理员强制禁用', type: 'warning' }); return; } if (this.conversationHasImages && !['qwen3-vl-plus', 'kimi-k2.5'].includes(key)) { this.uiPushToast({ title: '切换失败', message: '当前对话包含图片,仅支持 Qwen3.5 或 Kimi-k2.5', type: 'error' }); return; } const modelStore = useModelStore(); const prev = this.currentModelKey; try { const resp = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_key: key }) }); const payload = await resp.json(); if (!resp.ok || !payload.success) { throw new Error(payload.error || payload.message || '切换失败'); } const data = payload.data || {}; modelStore.setModel(data.model_key || key); if (data.run_mode) { this.runMode = data.run_mode; this.thinkingMode = data.thinking_mode ?? (data.run_mode !== 'fast'); } else { // 前端兼容策略:根据模型特性自动调整运行模式 const currentModel = modelStore.currentModel; if (currentModel?.deepOnly) { this.runMode = 'deep'; this.thinkingMode = true; } else if (currentModel?.fastOnly) { this.runMode = 'fast'; this.thinkingMode = false; } else { this.thinkingMode = this.runMode !== 'fast'; } } this.uiPushToast({ title: '模型已切换', message: modelStore.currentModel?.label || key, type: 'success' }); } catch (error) { modelStore.setModel(prev); const msg = error instanceof Error ? error.message : String(error || '切换失败'); this.uiPushToast({ title: '切换模型失败', message: msg, type: 'error' }); } finally { this.modelMenuOpen = false; this.inputCloseMenus(); this.inputSetQuickMenuOpen(false); } }, async handleHeaderModelSelect(key, disabled) { if (disabled) return; await this.handleModelSelect(key); this.closeHeaderMenu(); }, async handleCycleRunMode() { const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep']; const currentMode = this.resolvedRunMode; const currentIndex = modes.indexOf(currentMode); const nextMode = modes[(currentIndex + 1) % modes.length]; await this.setRunMode(nextMode); }, async setRunMode(mode, options = {}) { if (!this.isConnected || this.streamingMessage) { this.modeMenuOpen = false; return; } const modelStore = useModelStore(); const fastOnly = modelStore.currentModel?.fastOnly; const deepOnly = modelStore.currentModel?.deepOnly; if (fastOnly && mode !== 'fast') { if (!options.suppressToast) { this.uiPushToast({ title: '模式不可用', message: '当前模型仅支持快速模式', type: 'warning' }); } this.modeMenuOpen = false; this.inputCloseMenus(); return; } if (deepOnly && mode !== 'deep') { if (!options.suppressToast) { this.uiPushToast({ title: '模式不可用', message: '当前模型仅支持深度思考模式', type: 'warning' }); } this.modeMenuOpen = false; this.inputCloseMenus(); return; } if (mode === this.resolvedRunMode) { this.modeMenuOpen = false; this.closeQuickMenu(); return; } try { const response = await fetch('/api/thinking-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode }) }); const payload = await response.json(); if (!response.ok || !payload.success) { throw new Error(payload.message || payload.error || '切换失败'); } const data = payload.data || {}; this.thinkingMode = typeof data.thinking_mode === 'boolean' ? data.thinking_mode : mode !== 'fast'; this.runMode = data.mode || mode; } catch (error) { console.error('切换运行模式失败:', error); const message = error instanceof Error ? error.message : String(error || '未知错误'); this.uiPushToast({ title: '切换思考模式失败', message: message || '请稍后重试', type: 'error' }); } finally { this.modeMenuOpen = false; this.inputCloseMenus(); } }, handleInputChange() { this.autoResizeInput(); }, handleInputFocus() { this.inputSetFocused(true); this.closeQuickMenu(); }, handleInputBlur() { this.inputSetFocused(false); }, handleRealtimeTerminalClick() { if (!this.isConnected) { return; } if (this.isPolicyBlocked('block_realtime_terminal', '实时终端已被管理员禁用')) { return; } this.openRealtimeTerminal(); }, handleFocusPanelToggleClick() { if (!this.isConnected) { return; } if (this.isPolicyBlocked('block_focus_panel', '聚焦面板已被管理员禁用')) { return; } this.toggleFocusPanel(); }, handleTokenPanelToggleClick() { if (!this.currentConversationId) { return; } if (this.isPolicyBlocked('block_token_panel', '用量统计已被管理员禁用')) { return; } this.toggleTokenPanel(); }, handleCompressConversationClick() { if (this.compressing || this.streamingMessage || !this.isConnected) { return; } if (this.isPolicyBlocked('block_compress_conversation', '压缩对话已被管理员禁用')) { return; } this.compressConversation(); }, openReviewDialog() { if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) { return; } if (!this.isConnected) { this.uiPushToast({ title: '无法使用', message: '当前未连接,无法生成回顾文件', type: 'warning' }); return; } if (!this.conversations.length && !this.conversationsLoading) { this.loadConversationsList(); } const fallback = this.conversations.find((c) => c.id !== this.currentConversationId); if (!fallback) { this.uiPushToast({ title: '暂无可用对话', message: '没有可供回顾的其他对话记录', type: 'info' }); return; } this.reviewSelectedConversationId = fallback.id; this.reviewDialogOpen = true; this.reviewPreviewLines = []; this.reviewPreviewError = null; this.reviewGeneratedPath = null; this.loadReviewPreview(fallback.id); this.closeQuickMenu(); }, closeHeaderMenu() { this.headerMenuOpen = false; }, handleReviewSelect(id) { if (id === this.currentConversationId) { this.uiPushToast({ title: '无法引用当前对话', message: '请选择其他对话生成回顾', type: 'warning' }); return; } this.reviewSelectedConversationId = id; this.loadReviewPreview(id); }, async handleConfirmReview() { if (this.reviewSubmitting) return; if (!this.reviewSelectedConversationId) { this.uiPushToast({ title: '请选择对话', message: '请选择要生成回顾的对话记录', type: 'info' }); return; } if (this.reviewSelectedConversationId === this.currentConversationId) { this.uiPushToast({ title: '无法引用当前对话', message: '请选择其他对话生成回顾', type: 'warning' }); return; } if (!this.currentConversationId) { this.uiPushToast({ title: '无法发送', message: '当前没有活跃对话,无法自动发送提示消息', type: 'warning' }); return; } this.reviewSubmitting = true; try { const { path, char_count } = await this.generateConversationReview(this.reviewSelectedConversationId); if (!path) { throw new Error('未获取到生成的文件路径'); } const count = typeof char_count === 'number' ? char_count : 0; this.reviewGeneratedPath = path; const suggestion = count && count <= 10000 ? '建议直接完整阅读。' : '建议使用 read 工具进行搜索或分段阅读。'; if (this.reviewSendToModel) { const message = `帮我继续这个任务,对话文件在 ${path},文件长 ${count || '未知'} 字符,${suggestion} 请阅读文件了解后,不要直接继续工作,而是向我汇报你的理解,然后等我做出指示。`; const sent = this.sendAutoUserMessage(message); if (sent) { this.reviewDialogOpen = false; } } else { this.uiPushToast({ title: '回顾文件已生成', message: path, type: 'success' }); } } catch (error) { const msg = error instanceof Error ? error.message : String(error || '生成失败'); this.uiPushToast({ title: '生成回顾失败', message: msg, type: 'error' }); } finally { this.reviewSubmitting = false; } }, async loadReviewPreview(conversationId) { this.reviewPreviewLoading = true; this.reviewPreviewError = null; this.reviewPreviewLines = []; try { const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${this.reviewPreviewLimit}`); const payload = await resp.json().catch(() => ({})); if (!resp.ok || !payload?.success) { const msg = payload?.message || payload?.error || '获取预览失败'; throw new Error(msg); } this.reviewPreviewLines = payload?.data?.preview || []; } catch (error) { const msg = error instanceof Error ? error.message : String(error || '获取预览失败'); this.reviewPreviewError = msg; } finally { this.reviewPreviewLoading = false; } }, async generateConversationReview(conversationId) { const response = await fetch(`/api/conversations/${conversationId}/review`, { method: 'POST' }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { const msg = payload?.message || payload?.error || '生成失败'; throw new Error(msg); } const data = payload.data || payload; return { path: data.path || data.file_path || data.relative_path, char_count: data.char_count ?? data.length ?? data.size ?? 0 }; }, sendAutoUserMessage(text) { const message = (text || '').trim(); if (!message || !this.isConnected) { return false; } const quotaType = this.thinkingMode ? 'thinking' : 'fast'; if (this.isQuotaExceeded(quotaType)) { this.showQuotaToast({ type: quotaType }); return false; } this.taskInProgress = true; this.chatAddUserMessage(message, []); if (this.socket) { this.socket.emit('send_message', { message, images: [], conversation_id: this.currentConversationId }); } if (typeof this.monitorShowPendingReply === 'function') { this.monitorShowPendingReply(); } if (this.autoScrollEnabled) { this.scrollToBottom(); } this.autoResizeInput(); setTimeout(() => { if (this.currentConversationId) { this.updateCurrentContextTokens(); } }, 1000); return true; }, 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(); }, handleClickOutsideHeaderMenu(event) { if (!this.headerMenuOpen) return; const ribbon = this.$refs.titleRibbon as HTMLElement | undefined; const menu = this.$refs.headerMenu as HTMLElement | undefined; if ((ribbon && ribbon.contains(event.target)) || (menu && menu.contains(event.target))) { return; } this.closeHeaderMenu(); }, 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); }, isConversationBlank() { if (!Array.isArray(this.messages) || !this.messages.length) return true; return !this.messages.some( (msg) => msg && (msg.role === 'user' || msg.role === 'assistant') ); }, pickWelcomeText() { const pool = this.blankWelcomePool; if (!Array.isArray(pool) || !pool.length) { this.blankWelcomeText = '有什么可以帮忙的?'; return; } const idx = Math.floor(Math.random() * pool.length); this.blankWelcomeText = pool[idx]; }, refreshBlankHeroState() { const isBlank = this.isConversationBlank(); const currentConv = this.currentConversationId || 'temp'; const needNewWelcome = !this.blankHeroActive || this.lastBlankConversationId !== currentConv; if (isBlank) { if (needNewWelcome && !this.blankHeroExiting) { this.pickWelcomeText(); } this.blankHeroActive = true; this.lastBlankConversationId = currentConv; } else { this.blankHeroActive = false; this.blankHeroExiting = false; this.lastBlankConversationId = null; } }, 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 : [], locked: !!item.locked, locked_state: typeof item.locked_state === 'boolean' ? item.locked_state : null })); 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 policyStore = usePolicyStore(); if (policyStore.isCategoryLocked(categoryId)) { this.uiPushToast({ title: '无法修改', message: '该工具类别被管理员强制设置', type: 'warning' }); 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); if (data && (data.message || data.error)) { this.uiPushToast({ title: '无法切换工具', message: data.message || data.error, type: 'warning' }); } this.toolSetSettings(previousSnapshot); } } catch (error) { console.error('更新工具设置异常:', error); this.toolSetSettings(previousSnapshot); } this.toolSetSettingsLoading(false); }, toggleSettings() { if (!this.isConnected) { return; } this.modeMenuOpen = false; this.modelMenuOpen = false; 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(); }, startTitleTyping(title: string, options: { animate?: boolean } = {}) { if (this.titleTypingTimer) { clearInterval(this.titleTypingTimer); this.titleTypingTimer = null; } const target = (title || '').trim(); const animate = options.animate ?? true; if (!animate) { this.titleTypingTarget = target; this.titleTypingText = target; return; } const previous = (this.titleTypingText || '').trim(); if (previous === target) { this.titleTypingTarget = target; this.titleTypingText = target; return; } this.titleTypingTarget = target; this.titleTypingText = previous; const frames: string[] = []; for (let i = previous.length; i >= 0; i--) { frames.push(previous.slice(0, i)); } for (let j = 1; j <= target.length; j++) { frames.push(target.slice(0, j)); } let index = 0; this.titleTypingTimer = window.setInterval(() => { if (index >= frames.length) { clearInterval(this.titleTypingTimer!); this.titleTypingTimer = null; this.titleTypingText = target; return; } this.titleTypingText = frames[index]; index += 1; }, 32); }, 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(tool: any) { const personalization = usePersonalizationStore(); const intentEnabled = personalization?.form?.tool_intent_enabled ?? personalization?.tool_intent_enabled ?? true; return baseGetToolStatusText(tool, { intentEnabled }); }, getToolDescription, cloneToolArguments, buildToolLabel, formatSearchTopic, formatSearchTime, formatSearchDomains, 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, LiquidGlassWidget, QuickMenu, InputComposer, AppShell, ImagePicker, ConversationReviewDialog }; export default appOptions;