From 84f03688117e7f4e7f1f573462f4d94516f08e03 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:19:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0ws=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/services/ssh.service.ts | 4 +- packages/backend/src/websocket.ts | 2 +- .../frontend/src/components/StatusMonitor.vue | 2 +- .../src/composables/useWebSocketConnection.ts | 67 ++++++++++++++++++- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/services/ssh.service.ts b/packages/backend/src/services/ssh.service.ts index 7f8f057..565fceb 100644 --- a/packages/backend/src/services/ssh.service.ts +++ b/packages/backend/src/services/ssh.service.ts @@ -102,8 +102,8 @@ export const establishSshConnection = ( privateKey: connDetails.privateKey, passphrase: connDetails.passphrase, readyTimeout: timeout, - keepaliveInterval: 30000, // 保持连接 - keepaliveCountMax: 3, + keepaliveInterval: 10000, // 保持连接 + keepaliveCountMax: 10, }; const readyHandler = () => { diff --git a/packages/backend/src/websocket.ts b/packages/backend/src/websocket.ts index 11653dd..f25c39f 100644 --- a/packages/backend/src/websocket.ts +++ b/packages/backend/src/websocket.ts @@ -92,7 +92,7 @@ export const initializeWebSocket = (server: http.Server, sessionParser: RequestH extWs.isAlive = false; extWs.ping(() => {}); }); - }, 30000); // 30 秒心跳间隔 + }, 60000); // 增加到 60 秒心跳间隔 // --- WebSocket 升级处理 (认证) --- server.on('upgrade', (request: Request, socket, head) => { diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index acf5f1c..96ee25f 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -224,7 +224,7 @@ const swapDisplay = computed(() => { .status-item { display: grid; /* Simplified grid columns: Label | Value Area - Further increased label width */ - grid-template-columns: 100px 1fr; + grid-template-columns: 75px 1fr; align-items: center; gap: 0.8rem; /* Keep increased gap */ } diff --git a/packages/frontend/src/composables/useWebSocketConnection.ts b/packages/frontend/src/composables/useWebSocketConnection.ts index f546088..bdfbdd9 100644 --- a/packages/frontend/src/composables/useWebSocketConnection.ts +++ b/packages/frontend/src/composables/useWebSocketConnection.ts @@ -24,6 +24,11 @@ export function createWebSocketConnectionManager(sessionId: string, dbConnection const messageHandlers = new Map>(); // 此实例的消息处理器注册表 const instanceSessionId = sessionId; // 保存会话 ID 用于日志 const instanceDbConnectionId = dbConnectionId; // 保存数据库连接 ID + let reconnectAttempts = 0; // 重连尝试次数 + const maxReconnectAttempts = 5; // 最大重连次数 + let reconnectTimeoutId: ReturnType | null = null; // 重连定时器 ID + let lastUrl = ''; // 保存上次连接的 URL + let intentionalDisconnect = false; // 标记是否为用户主动断开 // --- End Instance State --- /** @@ -60,11 +65,47 @@ export function createWebSocketConnectionManager(sessionId: string, dbConnection } }; + /** + * 安排 WebSocket 重连尝试 + */ + const scheduleReconnect = () => { + if (intentionalDisconnect) return; // 如果是主动断开,则不重连 + + if (reconnectAttempts >= maxReconnectAttempts) { + console.log(`[WebSocket ${instanceSessionId}] 已达到最大重连次数 (${maxReconnectAttempts}),停止重连。`); + statusMessage.value = getStatusText('reconnectFailed'); + connectionStatus.value = 'error'; // 标记为错误状态 + return; + } + + reconnectAttempts++; + // 指数退避延迟 (例如: 2s, 4s, 8s, 16s, 32s) + const delay = Math.pow(2, reconnectAttempts) * 1000; + console.log(`[WebSocket ${instanceSessionId}] 连接丢失,将在 ${delay / 1000} 秒后尝试第 ${reconnectAttempts} 次重连...`); + statusMessage.value = getStatusText('reconnecting', { attempt: reconnectAttempts, delay: delay / 1000 }); + connectionStatus.value = 'connecting'; // 更新状态为正在连接 + + if (reconnectTimeoutId) clearTimeout(reconnectTimeoutId); // 清除旧的定时器 + + reconnectTimeoutId = setTimeout(() => { + if (!intentionalDisconnect && lastUrl) { // 再次检查是否主动断开 + connect(lastUrl); + } + }, delay); + }; + /** * 建立 WebSocket 连接 * @param {string} url - WebSocket 服务器 URL */ const connect = (url: string) => { + lastUrl = url; // 保存 URL 以便重连 + intentionalDisconnect = false; // 重置主动断开标记 + if (reconnectTimeoutId) { + clearTimeout(reconnectTimeoutId); // 清除可能存在的重连定时器 + reconnectTimeoutId = null; + } + // 防止重复连接同一实例 if (ws.value && (ws.value.readyState === WebSocket.OPEN || ws.value.readyState === WebSocket.CONNECTING)) { console.warn(`[WebSocket ${instanceSessionId}] 连接已打开或正在连接中。`); @@ -81,6 +122,7 @@ export function createWebSocketConnectionManager(sessionId: string, dbConnection ws.value.onopen = () => { console.log(`[WebSocket ${instanceSessionId}] 连接已打开。`); + reconnectAttempts = 0; // 连接成功,重置尝试次数 statusMessage.value = getStatusText('wsConnected'); // 状态保持 'connecting' 直到收到 ssh:connected // 发送后端所需的初始连接消息,包含数据库连接 ID @@ -140,18 +182,32 @@ export function createWebSocketConnectionManager(sessionId: string, dbConnection dispatchMessage('internal:error', event, { type: 'internal:error' }); isSftpReady.value = false; ws.value = null; // 清理实例 + // 如果不是主动断开,尝试重连 + if (!intentionalDisconnect) { + scheduleReconnect(); + } }; ws.value.onclose = (event) => { console.log(`[WebSocket ${instanceSessionId}] 连接已关闭: Code=${event.code}, Reason=${event.reason}`); - if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') { + // 只有在非错误状态下才更新为 disconnected + if (connectionStatus.value !== 'error') { connectionStatus.value = 'disconnected'; - statusMessage.value = getStatusText('wsClosed', { code: event.code }); + // 如果不是主动断开,显示尝试重连的消息 + if (!intentionalDisconnect && event.code !== 1000) { + statusMessage.value = getStatusText('wsClosedWillRetry', { code: event.code }); + } else { + statusMessage.value = getStatusText('wsClosed', { code: event.code }); + } } dispatchMessage('internal:closed', { code: event.code, reason: event.reason }, { type: 'internal:closed' }); isSftpReady.value = false; ws.value = null; // 清理实例引用 - // 不自动清除处理器,以便在重连时可能复用 + + // 如果不是主动断开 (code 1000),尝试重连 + if (!intentionalDisconnect && event.code !== 1000) { + scheduleReconnect(); + } }; } catch (err) { console.error(`[WebSocket ${instanceSessionId}] 创建 WebSocket 实例失败:`, err); @@ -166,6 +222,11 @@ export function createWebSocketConnectionManager(sessionId: string, dbConnection * 手动断开此 WebSocket 连接 */ const disconnect = () => { + intentionalDisconnect = true; // 标记为主动断开 + if (reconnectTimeoutId) { + clearTimeout(reconnectTimeoutId); // 清除重连定时器 + reconnectTimeoutId = null; + } if (ws.value) { console.log(`[WebSocket ${instanceSessionId}] 手动关闭连接...`); if (connectionStatus.value !== 'disconnected') {