From 7952e88885b2d80934eefdf919edd4a0cb41ac39 Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Thu, 2 Oct 2025 15:51:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BC=BA=E5=88=B6=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=97=B6=EF=BC=8C=E4=BB=8D=E5=B0=9D=E8=AF=95?= =?UTF-8?q?=E5=BB=BA=E7=AB=8Bws=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/initialization/AutoMode.vue | 7 +- .../initialization/EnvironmentIncomplete.vue | 6 +- frontend/src/composables/useWebSocket.ts | 235 ++++++++++++---- frontend/src/utils/appEntry.ts | 81 ++++++ frontend/src/views/Initialization.vue | 13 +- websocket_force_test.html | 254 ++++++++++++++++++ 6 files changed, 540 insertions(+), 56 deletions(-) create mode 100644 frontend/src/utils/appEntry.ts create mode 100644 websocket_force_test.html diff --git a/frontend/src/components/initialization/AutoMode.vue b/frontend/src/components/initialization/AutoMode.vue index dfbbfb4..230dbb6 100644 --- a/frontend/src/components/initialization/AutoMode.vue +++ b/frontend/src/components/initialization/AutoMode.vue @@ -71,9 +71,9 @@ import { ref, onMounted, onUnmounted } from 'vue' import { getConfig } from '@/utils/config' import { mirrorManager } from '@/utils/mirrorManager' -import router from '@/router' import { useUpdateChecker } from '@/composables/useUpdateChecker' import { connectAfterBackendStart } from '@/composables/useWebSocket' +import { forceEnterApp } from '@/utils/appEntry' import { message } from 'ant-design-vue' // Props @@ -154,11 +154,12 @@ function handleForceEnter() { } // 确认弹窗中的"我知道我在做什么"按钮,直接进入应用 -function handleForceEnterConfirm() { +async function handleForceEnterConfirm() { clearTimers() aborted.value = true forceEnterVisible.value = false - router.push('/home') + + await forceEnterApp('自动模式-强行进入确认') } // 事件处理 - 增强重新配置环境按钮功能 diff --git a/frontend/src/components/initialization/EnvironmentIncomplete.vue b/frontend/src/components/initialization/EnvironmentIncomplete.vue index 6ba0f55..dc10184 100644 --- a/frontend/src/components/initialization/EnvironmentIncomplete.vue +++ b/frontend/src/components/initialization/EnvironmentIncomplete.vue @@ -63,7 +63,7 @@ diff --git a/frontend/src/composables/useWebSocket.ts b/frontend/src/composables/useWebSocket.ts index 1483ad9..05f3af9 100644 --- a/frontend/src/composables/useWebSocket.ts +++ b/frontend/src/composables/useWebSocket.ts @@ -5,9 +5,9 @@ import { Modal } from 'ant-design-vue' // ====== 配置项 ====== const BASE_WS_URL = 'ws://localhost:36163/api/core/ws' -const HEARTBEAT_INTERVAL = 15000 -const HEARTBEAT_TIMEOUT = 5000 -const BACKEND_CHECK_INTERVAL = 3000 +const HEARTBEAT_INTERVAL = 30000 // 30秒心跳间隔,与后端保持一致 +const HEARTBEAT_TIMEOUT = 45000 // 45秒超时,给网络延迟留够时间 +const BACKEND_CHECK_INTERVAL = 6000 // 6秒检查间隔 const MAX_RESTART_ATTEMPTS = 3 const RESTART_DELAY = 2000 const MAX_QUEUE_SIZE = 50 // 每个 ID 或全局 type 队列最大条数 @@ -111,7 +111,7 @@ const initGlobalStorage = (): GlobalWSStorage => ({ const getGlobalStorage = (): GlobalWSStorage => { if (!(window as any)[WS_STORAGE_KEY]) { - ;(window as any)[WS_STORAGE_KEY] = initGlobalStorage() + ; (window as any)[WS_STORAGE_KEY] = initGlobalStorage() } return (window as any)[WS_STORAGE_KEY] } @@ -166,7 +166,7 @@ const handleBackendFailure = async () => { okText: '重启应用', onOk: () => { if ((window.electronAPI as any)?.windowClose) { - ;(window.electronAPI as any).windowClose() + ; (window.electronAPI as any).windowClose() } else { window.location.reload() } @@ -178,13 +178,26 @@ const handleBackendFailure = async () => { setTimeout(async () => { const success = await restartBackend() if (success) { + // 统一在一个地方管理连接权限 setConnectionPermission(true, '后端重启后重连') - setTimeout(() => { - connectGlobalWebSocket('后端重启后重连').then(() => { - setConnectionPermission(false, '正常运行中') - }) + + // 等待后端完全启动 + setTimeout(async () => { + try { + const connected = await connectGlobalWebSocket('后端重启后重连') + if (connected) { + // 连接成功后再禁用权限 + setTimeout(() => { + setConnectionPermission(false, '正常运行中') + }, 1000) + } + } catch (e) { + warn('重启后重连失败:', e) + setConnectionPermission(false, '连接失败') + } }, RESTART_DELAY) } else { + // 重启失败,继续尝试 setTimeout(handleBackendFailure, RESTART_DELAY) } }, RESTART_DELAY) @@ -208,12 +221,15 @@ const startBackendMonitoring = () => { setBackendStatus('stopped') } - if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT * 2) { + // 检查心跳超时:如果超过心跳超时时间且连接仍然打开,说明后端可能有问题 + if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT) { if (global.wsRef?.readyState === WebSocket.OPEN) { setBackendStatus('error') + // 主动关闭可能有问题的连接 + global.wsRef.close(1000, '心跳超时') } } - }, BACKEND_CHECK_INTERVAL * 2) + }, BACKEND_CHECK_INTERVAL) } // ====== 心跳 ====== @@ -239,7 +255,7 @@ const startGlobalHeartbeat = (ws: WebSocket) => { data: { Ping: pingTime, connectionId: global.connectionId }, }) ) - } catch {} + } catch { } } }, HEARTBEAT_INTERVAL) } @@ -256,13 +272,13 @@ const cleanupExpiredMessages = (now: number) => { const messageMatchesFilter = (message: WebSocketBaseMessage, filter: SubscriptionFilter): boolean => { // 如果都不指定,匹配所有消息 if (!filter.type && !filter.id) return true - + // 如果只指定type if (filter.type && !filter.id) return message.type === filter.type - + // 如果只指定id if (!filter.type && filter.id) return message.id === filter.id - + // 如果同时指定type和id,必须都匹配 return message.type === filter.type && message.id === filter.id } @@ -278,11 +294,11 @@ const getCacheMarkerKey = (filter: SubscriptionFilter): string => { // 添加缓存标记 const addCacheMarker = (filter: SubscriptionFilter) => { if (!filter.needCache) return - + const global = getGlobalStorage() const key = getCacheMarkerKey(filter) const existing = global.cacheMarkers.value.get(key) - + if (existing) { existing.refCount++ } else { @@ -292,18 +308,18 @@ const addCacheMarker = (filter: SubscriptionFilter) => { refCount: 1 }) } - + log(`缓存标记 ${key} 引用计数: ${global.cacheMarkers.value.get(key)?.refCount}`) } // 移除缓存标记 const removeCacheMarker = (filter: SubscriptionFilter) => { if (!filter.needCache) return - + const global = getGlobalStorage() const key = getCacheMarkerKey(filter) const existing = global.cacheMarkers.value.get(key) - + if (existing) { existing.refCount-- if (existing.refCount <= 0) { @@ -318,7 +334,7 @@ const removeCacheMarker = (filter: SubscriptionFilter) => { // 检查消息是否需要缓存 const shouldCacheMessage = (message: WebSocketBaseMessage): boolean => { const global = getGlobalStorage() - + for (const [, marker] of global.cacheMarkers.value) { const filter = { type: marker.type, id: marker.id } if (messageMatchesFilter(message, filter)) { @@ -361,8 +377,8 @@ const handleMessage = (raw: WebSocketBaseMessage) => { log(`消息已缓存: type=${raw.type}, id=${raw.id}`) } - // 定期清理过期消息(每 10 条触发一次,避免频繁) - if (Math.random() < 0.1) { + // 定期清理过期消息(每处理50条消息触发一次,避免频繁且更可预测) + if (global.cachedMessages.value.length > 0 && global.cachedMessages.value.length % 50 === 0) { cleanupExpiredMessages(now) } @@ -378,23 +394,23 @@ export const subscribe = ( ): string => { const global = getGlobalStorage() const subscriptionId = `sub_${++global.subscriptionCounter}_${Date.now()}` - + const subscription: WebSocketSubscription = { subscriptionId, filter, handler } - + global.subscriptions.value.set(subscriptionId, subscription) - + // 添加缓存标记 addCacheMarker(filter) - + // 回放匹配的缓存消息 - const matchingMessages = global.cachedMessages.value.filter(cached => + const matchingMessages = global.cachedMessages.value.filter(cached => messageMatchesFilter(cached.message, filter) ) - + if (matchingMessages.length > 0) { log(`回放 ${matchingMessages.length} 条缓存消息给订阅 ${subscriptionId}`) matchingMessages.forEach(cached => { @@ -405,7 +421,7 @@ export const subscribe = ( } }) } - + log(`新订阅创建: ${subscriptionId}`, filter) return subscriptionId } @@ -413,14 +429,14 @@ export const subscribe = ( export const unsubscribe = (subscriptionId: string): void => { const global = getGlobalStorage() const subscription = global.subscriptions.value.get(subscriptionId) - + if (subscription) { // 移除缓存标记 removeCacheMarker(subscription.filter) - + // 清理缓存中没有任何标记的消息 cleanupUnmarkedCache() - + global.subscriptions.value.delete(subscriptionId) log(`订阅已取消: ${subscriptionId}`) } else { @@ -431,7 +447,7 @@ export const unsubscribe = (subscriptionId: string): void => { // 清理没有标记的缓存消息 const cleanupUnmarkedCache = () => { const global = getGlobalStorage() - + global.cachedMessages.value = global.cachedMessages.value.filter(cached => { // 检查是否还有标记需要这条消息 for (const [, marker] of global.cacheMarkers.value) { @@ -455,7 +471,7 @@ const releaseConnectionLock = () => { isGlobalConnectingLock = false } -const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连'] +const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连', '系统初始化', '手动重连', '强制连接'] const isValidConnectionReason = (reason: string) => allowedConnectionReasons.includes(reason) const checkConnectionPermission = () => getGlobalStorage().allowNewConnection const setConnectionPermission = (allow: boolean, reason: string) => { @@ -466,9 +482,19 @@ const setConnectionPermission = (allow: boolean, reason: string) => { const createGlobalWebSocket = (): WebSocket => { const global = getGlobalStorage() + + // 清理旧连接 if (global.wsRef) { - if (global.wsRef.readyState === WebSocket.OPEN) return global.wsRef - if (global.wsRef.readyState === WebSocket.CONNECTING) return global.wsRef + if (global.wsRef.readyState === WebSocket.OPEN) { + log('警告:尝试创建新连接但当前连接仍有效') + return global.wsRef + } + if (global.wsRef.readyState === WebSocket.CONNECTING) { + log('警告:尝试创建新连接但当前连接正在建立中') + return global.wsRef + } + // 清理已关闭或错误状态的连接 + global.wsRef = null } const ws = new WebSocket(BASE_WS_URL) @@ -480,7 +506,11 @@ const createGlobalWebSocket = (): WebSocket => { global.reconnectAttempts = 0 setGlobalStatus('已连接') startGlobalHeartbeat(ws) - setConnectionPermission(false, '正常运行中') + + // 只有在特殊连接原因下才设置为正常运行 + if (global.connectionReason !== '系统初始化') { + setConnectionPermission(false, '正常运行中') + } try { ws.send( @@ -495,35 +525,52 @@ const createGlobalWebSocket = (): WebSocket => { data: { Pong: Date.now(), connectionId: global.connectionId }, }) ) - } catch {} + } catch (e) { + warn('发送初始信号失败:', e) + } initializeGlobalSubscriptions() + log('WebSocket连接已建立并初始化完成') } ws.onmessage = ev => { try { const raw = JSON.parse(ev.data) as WebSocketBaseMessage handleMessage(raw) - } catch {} + } catch (e) { + warn('解析WebSocket消息失败:', e, '原始数据:', ev.data) + } + } + + ws.onerror = (error) => { + setGlobalStatus('连接错误') + warn('WebSocket错误:', error) } - ws.onerror = () => setGlobalStatus('连接错误') ws.onclose = event => { setGlobalStatus('已断开') stopGlobalHeartbeat() global.isConnecting = false + log(`WebSocket连接关闭: code=${event.code}, reason="${event.reason}"`) + + // 根据关闭原因决定是否需要处理后端故障 if (event.code === 1000 && event.reason === 'Ping超时') { handleBackendFailure().catch(e => warn('handleBackendFailure error:', e)) + } else if (event.code === 1000 && event.reason === '心跳超时') { + handleBackendFailure().catch(e => warn('handleBackendFailure error:', e)) } } return ws } -const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promise => { +const connectGlobalWebSocket = async (reason: string = '手动重连'): Promise => { const global = getGlobalStorage() - if (!checkConnectionPermission() || !isValidConnectionReason(reason)) return false + if (!checkConnectionPermission() || !isValidConnectionReason(reason)) { + warn(`连接被拒绝: 权限=${checkConnectionPermission()}, 原因="${reason}"是否有效=${isValidConnectionReason(reason)}`) + return false + } if (!acquireConnectionLock()) return false try { @@ -573,6 +620,72 @@ export const connectAfterBackendStart = async (): Promise => { } } +// 强制连接模式,用于强行进入应用时 +export const forceConnectWebSocket = async (): Promise => { + log('强制WebSocket连接模式开始') + + const global = getGlobalStorage() + + // 显示当前状态 + log('当前连接状态:', { + status: global.status.value, + wsReadyState: global.wsRef?.readyState, + allowNewConnection: global.allowNewConnection, + connectionReason: global.connectionReason + }) + + // 设置连接权限 + setConnectionPermission(true, '强制连接') + log('已设置强制连接权限') + + try { + // 尝试连接,最多重试3次 + let connected = false + let attempts = 0 + const maxAttempts = 3 + + while (!connected && attempts < maxAttempts) { + attempts++ + log(`强制连接尝试 ${attempts}/${maxAttempts}`) + + try { + connected = await connectGlobalWebSocket('强制连接') + if (connected) { + startBackendMonitoring() + log('强制WebSocket连接成功') + break + } else { + warn(`强制连接尝试 ${attempts} 失败`) + if (attempts < maxAttempts) { + // 等待1秒后重试 + await new Promise(resolve => setTimeout(resolve, 1000)) + } + } + } catch (attemptError) { + warn(`强制连接尝试 ${attempts} 异常:`, attemptError) + if (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 1000)) + } + } + } + + if (!connected) { + warn('所有强制连接尝试均失败,但不阻止应用启动') + } + + return connected + } catch (error) { + warn('强制WebSocket连接异常:', error) + return false + } finally { + // 稍后重置连接权限,给连接时间 + setTimeout(() => { + setConnectionPermission(false, '强制连接完成') + log('强制连接权限已重置') + }, 2000) // 增加到2秒 + } +} + // ====== 全局处理器 ====== let _defaultHandlersLoaded = true let _defaultTaskManagerHandler = schedulerHandlers.handleTaskManagerMessage @@ -627,7 +740,7 @@ const initializeGlobalSubscriptions = () => { data: { Pong: msg.data.Ping, connectionId: global.connectionId }, }) ) - } catch {} + } catch { } } return } @@ -648,8 +761,22 @@ export function useWebSocket() { const ws = global.wsRef if (ws?.readyState === WebSocket.OPEN) { try { - ws.send(JSON.stringify({ id, type, data })) - } catch {} + const message = { id, type, data } + ws.send(JSON.stringify(message)) + if (DEBUG && type !== 'Signal') { // 避免心跳消息spam日志 + log('发送消息:', message) + } + return true + } catch (e) { + warn('发送消息失败:', e, { id, type, data }) + return false + } + } else { + warn('WebSocket未连接,无法发送消息:', { + readyState: ws?.readyState, + message: { id, type, data } + }) + return false } } @@ -679,6 +806,21 @@ export function useWebSocket() { lastCheck: global.lastBackendCheck, }) + // 调试功能 + const debug = { + forceConnect: forceConnectWebSocket, + normalConnect: connectAfterBackendStart, + getGlobalStorage, + setConnectionPermission, + checkConnectionPermission, + allowedReasons: allowedConnectionReasons + } + + // 在开发模式下暴露调试功能到全局 + if (DEBUG && typeof window !== 'undefined') { + ; (window as any).wsDebug = debug + } + return { subscribe, unsubscribe, @@ -688,6 +830,7 @@ export function useWebSocket() { backendStatus: global.backendStatus, restartBackend: restartBackendManually, getBackendStatus, + debug: DEBUG ? debug : undefined } } diff --git a/frontend/src/utils/appEntry.ts b/frontend/src/utils/appEntry.ts new file mode 100644 index 0000000..6973cc1 --- /dev/null +++ b/frontend/src/utils/appEntry.ts @@ -0,0 +1,81 @@ +// appEntry.ts - 统一的应用进入逻辑 +import router from '@/router' +import { connectAfterBackendStart, forceConnectWebSocket } from '@/composables/useWebSocket' + +/** + * 统一的进入应用函数,会自动尝试建立WebSocket连接 + * @param reason 进入应用的原因,用于日志记录 + * @param forceEnter 是否强制进入(即使WebSocket连接失败) + * @returns Promise 是否成功进入应用 + */ +export async function enterApp(reason: string = '正常进入', forceEnter: boolean = true): Promise { + console.log(`${reason}:开始进入应用流程,尝试建立WebSocket连接...`) + + let wsConnected = false + + try { + // 尝试建立WebSocket连接 + wsConnected = await connectAfterBackendStart() + if (wsConnected) { + console.log(`${reason}:WebSocket连接建立成功`) + } else { + console.warn(`${reason}:WebSocket连接建立失败`) + } + } catch (error) { + console.error(`${reason}:WebSocket连接尝试失败:`, error) + } + + // 决定是否进入应用 + if (wsConnected || forceEnter) { + if (!wsConnected && forceEnter) { + console.warn(`${reason}:WebSocket连接失败,但强制进入应用`) + } + + // 跳转到主页 + router.push('/home') + console.log(`${reason}:已进入应用`) + return true + } else { + console.error(`${reason}:WebSocket连接失败且不允许强制进入`) + return false + } +} + +/** + * 强行进入应用(忽略WebSocket连接状态) + * @param reason 进入原因 + */ +export async function forceEnterApp(reason: string = '强行进入'): Promise { + console.log(`🚀 ${reason}:强行进入应用流程开始`) + console.log(`📡 ${reason}:尝试强制建立WebSocket连接...`) + + try { + // 使用强制连接模式 + const wsConnected = await forceConnectWebSocket() + if (wsConnected) { + console.log(`✅ ${reason}:强制WebSocket连接成功!`) + } else { + console.warn(`⚠️ ${reason}:强制WebSocket连接失败,但继续进入应用`) + } + + // 等待一下确保连接状态稳定 + await new Promise(resolve => setTimeout(resolve, 500)) + + } catch (error) { + console.error(`❌ ${reason}:强制WebSocket连接异常:`, error) + } + + // 无论WebSocket是否成功,都进入应用 + console.log(`🏠 ${reason}:跳转到主页...`) + router.push('/home') + console.log(`✨ ${reason}:已强行进入应用`) +} + +/** + * 正常进入应用(需要WebSocket连接成功) + * @param reason 进入原因 + * @returns 是否成功进入 + */ +export async function normalEnterApp(reason: string = '正常进入'): Promise { + return await enterApp(reason, false) +} \ No newline at end of file diff --git a/frontend/src/views/Initialization.vue b/frontend/src/views/Initialization.vue index 20caf00..0f0bd52 100644 --- a/frontend/src/views/Initialization.vue +++ b/frontend/src/views/Initialization.vue @@ -43,6 +43,7 @@ import ManualMode from '@/components/initialization/ManualMode.vue' import EnvironmentIncomplete from '@/components/initialization/EnvironmentIncomplete.vue' import type { DownloadProgress } from '@/types/initialization' import { mirrorManager } from '@/utils/mirrorManager' +import { forceEnterApp } from '@/utils/appEntry' const router = useRouter() @@ -69,8 +70,8 @@ const mirrorConfigStatus = ref({ const manualModeRef = ref() // 基础功能函数 -function skipToHome() { - router.push('/home') +async function skipToHome() { + await forceEnterApp('跳过初始化直接进入') } function switchToManualMode() { @@ -84,10 +85,14 @@ async function enterApp() { try { // 设置初始化完成标记 await setInitialized(true) - console.log('设置初始化完成标记,跳转到首页') - router.push('/home') + console.log('设置初始化完成标记,准备进入应用...') + + // 使用统一的进入应用函数 + await forceEnterApp('初始化完成后进入') } catch (error) { console.error('进入应用失败:', error) + // 即使出错也强制进入 + await forceEnterApp('初始化失败后强制进入') } } diff --git a/websocket_force_test.html b/websocket_force_test.html new file mode 100644 index 0000000..59345a7 --- /dev/null +++ b/websocket_force_test.html @@ -0,0 +1,254 @@ + + + + + + + WebSocket强制连接测试 + + + + +
+

WebSocket强制连接测试工具

+ +
状态:未知
+ +
+ + + + + +
+ +
+
+ + + + + \ No newline at end of file