// @ts-nocheck import { debugLog } from '../common'; import { usePolicyStore } from '../../../stores/policy'; import { useModelStore } from '../../../stores/model'; import { usePersonalizationStore } from '../../../stores/personalization'; import { useTutorialStore } from '../../../stores/tutorial'; import { renderMarkdown as renderMarkdownHelper } from '../../../composables/useMarkdownRenderer'; import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from '../../../composables/useScrollControl'; import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from '../../../composables/usePanelResize'; import { SUB_AGENT_DONE_PREFIX_RE, BG_RUN_COMMAND_DONE_PREFIX_RE, userMDebug, UI_BOUNCE_TRACE_MAX, uiBounceTraceLastTsByKey, isUiBounceTraceEnabled, uiBounceTrace, isConnectionDiagEnabled, pushConnectionDiagRecord, connectionDiag, parseSubAgentDoneLabel, parseBackgroundRunCommandDoneLabel, parseSystemNoticeLabel, } from './shared'; export const socketMethods = { async initSocket() { // 主网页端已切换为 REST + 轮询模式,这里保留空实现用于兼容旧调用 this.socket = null; }, async checkConnectionHealth() { if (this.connectionHeartbeatInFlight) { connectionDiag('log', 'health-skip-inflight', { seq: this.connectionHeartbeatSeq, isConnected: !!this.isConnected, failCount: this.connectionHeartbeatFailCount }); return; } this.connectionHeartbeatInFlight = true; const seq = Number(this.connectionHeartbeatSeq || 0) + 1; this.connectionHeartbeatSeq = seq; const requestId = `${Date.now()}-${seq}`; const startedAt = Date.now(); const diagEnabled = isConnectionDiagEnabled(); const healthUrl = diagEnabled ? '/api/health?diag=1' : '/api/health'; const wasConnected = !!this.isConnected; let responseStatus: number | null = null; const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; const timeoutMs = typeof this.connectionHeartbeatRequestTimeoutMs === 'number' && this.connectionHeartbeatRequestTimeoutMs > 0 ? this.connectionHeartbeatRequestTimeoutMs : 5000; const timeoutId = controller ? window.setTimeout(() => controller.abort(), timeoutMs) : null; try { const response = await fetch(healthUrl, { method: 'GET', cache: 'no-store', headers: { 'X-Connection-Heartbeat': requestId }, signal: controller?.signal }); responseStatus = response.status; if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const failCountBeforeRecover = this.connectionHeartbeatFailCount || 0; this.isConnected = true; this.connectionHeartbeatFailCount = 0; this.connectionHeartbeatLastLatencyMs = Date.now() - startedAt; this.connectionHeartbeatLastStatusCode = responseStatus; this.connectionHeartbeatLastError = ''; if (!wasConnected) { this.connectionHeartbeatLastChangeAt = Date.now(); connectionDiag( 'warn', 'health-recovered', { requestId, seq, elapsedMs: this.connectionHeartbeatLastLatencyMs, failCountBeforeRecover, status: responseStatus, diagEnabled, endpoint: healthUrl, conversationId: this.currentConversationId || null, taskInProgress: !!this.taskInProgress, streamingMessage: !!this.streamingMessage, visibility: document?.visibilityState || 'unknown', online: typeof navigator !== 'undefined' ? navigator.onLine : null }, { force: true } ); } else if ( isConnectionDiagEnabled() && (this.connectionHeartbeatLastLatencyMs >= 700 || seq <= 3 || seq % 60 === 0) ) { connectionDiag('log', 'health-ok', { requestId, seq, elapsedMs: this.connectionHeartbeatLastLatencyMs, status: responseStatus, endpoint: healthUrl, visibility: document?.visibilityState || 'unknown' }); } } catch (error) { const nextFailCount = (this.connectionHeartbeatFailCount || 0) + 1; this.connectionHeartbeatFailCount = nextFailCount; this.connectionHeartbeatLastLatencyMs = Date.now() - startedAt; this.connectionHeartbeatLastStatusCode = responseStatus; const errName = error?.name || 'Error'; const errMessage = error?.message || String(error); this.connectionHeartbeatLastError = `${errName}: ${errMessage}`; const failThreshold = typeof this.connectionHeartbeatFailThreshold === 'number' && this.connectionHeartbeatFailThreshold > 0 ? this.connectionHeartbeatFailThreshold : 3; const shouldDisconnect = nextFailCount >= failThreshold; // 改为连续失败阈值后再置灰,避免偶发请求超时导致“假断连” if (shouldDisconnect) { this.isConnected = false; } if (wasConnected && shouldDisconnect) { this.connectionHeartbeatLastChangeAt = Date.now(); } const shouldLogFail = wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0; connectionDiag( shouldLogFail ? 'warn' : 'log', 'health-failed', { requestId, seq, elapsedMs: this.connectionHeartbeatLastLatencyMs, failCount: nextFailCount, status: responseStatus, endpoint: healthUrl, diagEnabled, wasConnected, nowConnected: !!this.isConnected, failThreshold, shouldDisconnect, errorName: errName, errorMessage: errMessage, timeout: errName === 'AbortError', visibility: document?.visibilityState || 'unknown', online: typeof navigator !== 'undefined' ? navigator.onLine : null, conversationId: this.currentConversationId || null, taskInProgress: !!this.taskInProgress, streamingMessage: !!this.streamingMessage, hasPendingTools: typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null }, { force: shouldLogFail } ); } finally { this.connectionHeartbeatInFlight = false; if (timeoutId) { clearTimeout(timeoutId); } } }, startConnectionHeartbeat() { if (this.connectionHeartbeatActive) { return; } this.connectionHeartbeatActive = true; this.connectionHeartbeatFailCount = 0; connectionDiag( 'log', 'heartbeat-start', { connectedIntervalMs: this.connectionHeartbeatIntervalMs, disconnectedIntervalMs: this.connectionHeartbeatDisconnectedIntervalMs }, { force: true } ); const runHeartbeat = async () => { if (!this.connectionHeartbeatActive) { return; } await this.checkConnectionHealth(); if (!this.connectionHeartbeatActive) { return; } const connectedInterval = typeof this.connectionHeartbeatIntervalMs === 'number' && this.connectionHeartbeatIntervalMs > 0 ? this.connectionHeartbeatIntervalMs : 8000; const disconnectedInterval = typeof this.connectionHeartbeatDisconnectedIntervalMs === 'number' && this.connectionHeartbeatDisconnectedIntervalMs > 0 ? this.connectionHeartbeatDisconnectedIntervalMs : 1000; const nextInterval = this.isConnected ? connectedInterval : disconnectedInterval; if ( isConnectionDiagEnabled() && (!this.isConnected || (this.connectionHeartbeatSeq || 0) <= 3) ) { connectionDiag('log', 'heartbeat-next', { seq: this.connectionHeartbeatSeq, isConnected: !!this.isConnected, failCount: this.connectionHeartbeatFailCount, nextInterval }); } this.connectionHeartbeatTimer = window.setTimeout(() => { runHeartbeat(); }, nextInterval); }; // 先做一次即时探活,再根据连接状态动态设置下次轮询间隔 runHeartbeat(); }, stopConnectionHeartbeat() { this.connectionHeartbeatActive = false; if (this.connectionHeartbeatTimer) { clearTimeout(this.connectionHeartbeatTimer); this.connectionHeartbeatTimer = null; } connectionDiag('log', 'heartbeat-stop', {}, { force: true }); }, async loadInitialData() { try { debugLog('加载初始数据...'); const statusResponse = await fetch('/api/status'); if (!statusResponse.ok) { throw new Error(`状态接口请求失败: ${statusResponse.status}`); } const statusData = await statusResponse.json(); this.socket = null; this.projectPath = statusData.project_path || ''; this.agentVersion = statusData.version || this.agentVersion; this.thinkingMode = !!statusData.thinking_mode; this.applyStatusSnapshot(statusData); this.fetchPermissionMode(); this.fetchExecutionMode(); this.fetchNetworkPermission(); this.fetchPendingToolApprovals(); // 立即更新配额和运行模式,避免等待其他慢接口 this.fetchUsageQuota(); const modelStore = useModelStore(); await modelStore.fetchModels().catch((err) => { console.warn('加载模型列表失败,使用前端默认列表:', err); }); if (statusData && typeof statusData.model_key === 'string') { modelStore.setModel(statusData.model_key); this.currentModelKey = modelStore.currentModelKey; } // 拉取管理员策略 const policyStore = usePolicyStore(); await policyStore.fetchPolicy(); this.applyPolicyUiLocks(); // 加载个性化设置 const personalizationStore = usePersonalizationStore(); if (!personalizationStore.loaded && !personalizationStore.loading) { await personalizationStore.fetchPersonalization().catch((err) => { console.warn('加载个性化设置失败:', err); }); } // 仅在“无当前对话”的新会话入口应用默认模型/模式; // 对于已存在对话(含刷新恢复/URL指定对话),必须保留对话自身设置。 const statusConversationIdForDefaults = statusData?.conversation?.current_id; const hasActiveConversation = typeof statusConversationIdForDefaults === 'string' && statusConversationIdForDefaults.length > 0 && !statusConversationIdForDefaults.startsWith('temp_'); if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) { const defaultRunMode = personalizationStore.form.default_run_mode; const defaultModel = personalizationStore.form.default_model; if (defaultRunMode) { this.runMode = defaultRunMode; debugLog('应用默认运行模式:', defaultRunMode); } if (defaultModel) { modelStore.setModel(defaultModel); this.currentModelKey = modelStore.currentModelKey; debugLog('应用默认模型:', defaultModel); } // 根据默认运行模式设置思考模式 if (defaultRunMode === 'thinking') { this.thinkingMode = true; } else if (defaultRunMode === 'fast') { this.thinkingMode = false; } } this.isConnected = true; const focusPromise = this.focusFetchFiles(); let treePromise: Promise | null = null; const isHostMode = statusData?.container?.mode === 'host'; this.versioningHostMode = !!isHostMode; this.dockerProjectMode = !isHostMode; if (isHostMode) { this.fileMarkTreeUnavailable('宿主机模式下文件树不可用'); await this.fetchHostWorkspaces(); } else { this.fileMarkTreeUnavailable('Docker 模式下文件区已改为项目列表'); await this.fetchHostWorkspaces(); this.hostWorkspaceCreateDialogOpen = false; this.hostWorkspaceCreatePath = ''; this.hostWorkspaceCreateLabel = ''; this.hostWorkspaceCreateError = ''; this.hostWorkspaceCreateSubmitting = false; } // 获取当前对话信息 const isExplicitNewRoute = this.isExplicitNewConversationRoute(); const runningConversationId = await this.getRunningTaskConversationId(); const statusConversationId = statusData.conversation && statusData.conversation.current_id; const resumeConversationId = statusConversationId || runningConversationId; const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId; if ( resumeConversationId && !this.currentConversationId && (!isExplicitNewRoute || shouldResumeOnNewRoute) ) { this.skipConversationHistoryReload = true; // 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载 this.skipConversationLoadedEvent = true; this.suppressTitleTyping = true; this.titleReady = false; this.currentConversationTitle = ''; this.titleTypingText = ''; this.currentConversationId = resumeConversationId; const pathFragment = this.stripConversationPrefix(resumeConversationId); const currentPath = window.location.pathname.replace(/^\/+/, ''); if (currentPath !== pathFragment) { history.replaceState({ conversationId: resumeConversationId }, '', `/${pathFragment}`); } // 如果有当前对话,尝试获取标题和历史 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(); await this.fetchVersioningStatus(this.currentConversationId, { silent: true }); } catch (e) { console.warn('获取当前对话标题失败:', e); this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping(this.currentConversationTitle || '新对话', ''); } } // 待办数据依赖当前会话上下文,放在会话恢复/切换之后拉取,避免初始化早期拿到空快照 const todoPromise = this.fileFetchTodoList(); // 等待其他加载项完成(允许部分失败不阻塞模式切换) const pendingPromises = [focusPromise, todoPromise]; if (treePromise) { pendingPromises.push(treePromise); } await Promise.allSettled(pendingPromises); await this.loadToolSettings(true); // 加载对话列表(修复:初始化时未加载对话列表的bug) this.conversationsOffset = 0; await this.loadConversationsList(); debugLog('初始数据加载完成'); } catch (error) { console.error('加载初始数据失败:', error); this.isConnected = false; } } };