// @ts-nocheck import { usePolicyStore } from '../../stores/policy'; import { useModelStore } from '../../stores/model'; import { initializeLegacySocket } from '../../composables/useLegacySocket'; import { renderMarkdown as renderMarkdownHelper } from '../../composables/useMarkdownRenderer'; import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, toggleScrollLock as toggleScrollLockHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from '../../composables/useScrollControl'; import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from '../../composables/usePanelResize'; import { debugLog } from './common'; export const uiMethods = { 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(); } }, 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(); }, 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; }, 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 }; }, 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; } }, 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); }, 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); }, 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; }, openGuiFileManager() { if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) { return; } window.open('/file-manager', '_blank'); }, renderMarkdown(content, isStreaming = false) { return renderMarkdownHelper(content, isStreaming); }, 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; }, 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); }, openRealtimeTerminal() { const { protocol, hostname, port } = window.location; const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`; window.open(target, '_blank'); }, 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); } }, confirmAction(options = {}) { return this.uiRequestConfirm(options); }, async handleCopyCodeClick(event) { const target = event.target; if (!target || !target.classList || !target.classList.contains('copy-code-btn')) { return; } const blockId = target.getAttribute('data-code'); if (!blockId) { return; } // 防止重复点击 if (target.classList.contains('copied')) { return; } const selector = `[data-code-id="${blockId.replace(/"/g, '\\"')}"]`; const codeEl = document.querySelector(selector); if (!codeEl) { return; } const encoded = codeEl.getAttribute('data-original-code'); const content = encoded ? this.decodeHtmlEntities(encoded) : codeEl.textContent || ''; if (!content.trim()) { return; } try { await navigator.clipboard.writeText(content); // 添加 copied 类,切换为对勾图标 target.classList.add('copied'); // 5秒后恢复 setTimeout(() => { target.classList.remove('copied'); }, 5000); } catch (error) { console.warn('复制失败:', error); } }, decodeHtmlEntities(text) { const textarea = document.createElement('textarea'); textarea.innerHTML = text; return textarea.value; } };