From a33e2675ddb882e417e4b224915ce78f6753ba89 Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Thu, 11 Sep 2025 21:57:24 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dws=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/core.py | 6 +- frontend/electron/main.ts | 4 + frontend/electron/preload.ts | 1 + .../components/initialization/AutoMode.vue | 10 + .../components/initialization/ManualMode.vue | 43 +- frontend/src/composables/useWebSocket.ts | 615 +++++++++++------- frontend/src/types/electron.d.ts | 3 +- frontend/src/views/Scheduler.vue | 156 +++-- frontend/src/views/Scripts.vue | 49 +- frontend/src/views/WebSocketTest.vue | 0 10 files changed, 542 insertions(+), 345 deletions(-) create mode 100644 frontend/src/views/WebSocketTest.vue diff --git a/app/api/core.py b/app/api/core.py index 402ff3a..839329b 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -41,13 +41,14 @@ async def connect_websocket(websocket: WebSocket): await websocket.accept() Config.websocket = websocket last_pong = time.monotonic() + last_ping = time.monotonic() data = {} while True: try: - data = await asyncio.wait_for(websocket.receive_json(), timeout=30.0) + data = await asyncio.wait_for(websocket.receive_json(), timeout=15.0) if data.get("type") == "Signal" and "Pong" in data.get("data", {}): last_pong = time.monotonic() elif data.get("type") == "Signal" and "Ping" in data.get("data", {}): @@ -61,7 +62,7 @@ async def connect_websocket(websocket: WebSocket): except asyncio.TimeoutError: - if time.monotonic() - last_pong > 15: + if last_pong < last_ping: await websocket.close(code=1000, reason="Ping超时") break await websocket.send_json( @@ -69,6 +70,7 @@ async def connect_websocket(websocket: WebSocket): id="Main", type="Signal", data={"Ping": "无描述"} ).model_dump() ) + last_ping = time.monotonic() except WebSocketDisconnect: break diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 95298ad..72bcb66 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -537,6 +537,10 @@ ipcMain.handle('start-backend', async () => { return startBackend(appRoot) }) +ipcMain.handle('stop-backend', async () => { + return stopBackend() +}) + // Git相关 ipcMain.handle('download-git', async () => { const appRoot = getAppRoot() diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 4e64079..3866a77 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('electronAPI', { cloneBackend: (repoUrl?: string) => ipcRenderer.invoke('clone-backend', repoUrl), updateBackend: (repoUrl?: string) => ipcRenderer.invoke('update-backend', repoUrl), startBackend: () => ipcRenderer.invoke('start-backend'), + stopBackend: () => ipcRenderer.invoke('stop-backend'), // 管理员权限相关 checkAdmin: () => ipcRenderer.invoke('check-admin'), diff --git a/frontend/src/components/initialization/AutoMode.vue b/frontend/src/components/initialization/AutoMode.vue index 9d03d2c..11fa8d3 100644 --- a/frontend/src/components/initialization/AutoMode.vue +++ b/frontend/src/components/initialization/AutoMode.vue @@ -42,6 +42,7 @@ import { ref, onMounted } from 'vue' import { getConfig } from '@/utils/config' import { getMirrorUrl } from '@/config/mirrors' import router from '@/router' +import { connectAfterBackendStart } from '@/composables/useWebSocket' @@ -185,6 +186,15 @@ async function startBackendService() { if (!result.success) { throw new Error(`后端服务启动失败: ${result.error}`) } + + // 后端启动成功,建立WebSocket连接 + console.log('后端启动成功,正在建立WebSocket连接...') + const wsConnected = await connectAfterBackendStart() + if (!wsConnected) { + console.warn('WebSocket连接建立失败,但继续进入应用') + } else { + console.log('WebSocket连接建立成功') + } } // 组件挂载时开始自动流程 diff --git a/frontend/src/components/initialization/ManualMode.vue b/frontend/src/components/initialization/ManualMode.vue index 876feaa..ad1bad8 100644 --- a/frontend/src/components/initialization/ManualMode.vue +++ b/frontend/src/components/initialization/ManualMode.vue @@ -7,7 +7,34 @@ > - + @@ -119,6 +146,7 @@ import GitStep from './GitStep.vue' import BackendStep from './BackendStep.vue' import DependenciesStep from './DependenciesStep.vue' import ServiceStep from './ServiceStep.vue' +import { connectAfterBackendStart } from '@/composables/useWebSocket' @@ -408,6 +436,19 @@ async function startBackendService() { if (result.success) { if (serviceStepRef.value) { serviceStepRef.value.serviceProgress = 100 + serviceStepRef.value.serviceStatus = '后端服务启动成功,正在建立WebSocket连接...' + } + + // 后端启动成功,建立WebSocket连接 + console.log('后端手动启动成功,正在建立WebSocket连接...') + const wsConnected = await connectAfterBackendStart() + if (!wsConnected) { + console.warn('WebSocket连接建立失败,但继续进入应用') + } else { + console.log('WebSocket连接建立成功') + } + + if (serviceStepRef.value) { serviceStepRef.value.serviceStatus = '后端服务启动成功,即将进入主页...' } stepStatus.value = 'finish' diff --git a/frontend/src/composables/useWebSocket.ts b/frontend/src/composables/useWebSocket.ts index 8a2da6f..7882319 100644 --- a/frontend/src/composables/useWebSocket.ts +++ b/frontend/src/composables/useWebSocket.ts @@ -1,15 +1,13 @@ import { ref, type Ref } from 'vue' -import { message, notification } from 'ant-design-vue' - -// WebSocket 调试开关 -const WS_DEV = true -const WS_VERSION = 'v2.5-PERSISTENT-' + Date.now() -console.log(`🚀 WebSocket 模块已加载: ${WS_VERSION} - 永久连接模式`) +import { message, notification, Modal } from 'ant-design-vue' // 基础配置 const BASE_WS_URL = 'ws://localhost:36163/api/core/ws' -const HEARTBEAT_INTERVAL = 15000 -const HEARTBEAT_TIMEOUT = 5000 +const HEARTBEAT_INTERVAL = 60000 // 60秒,确保后端有机会在30秒时发送ping +const HEARTBEAT_TIMEOUT = 15000 +const BACKEND_CHECK_INTERVAL = 3000 // 后端检查间隔 +const MAX_RESTART_ATTEMPTS = 3 // 最大重启尝试次数 +const RESTART_DELAY = 2000 // 重启延迟 // 类型定义 export type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误' @@ -65,26 +63,10 @@ export interface WebSocketConfig { onStatusChange?: (status: WebSocketStatus) => void } -// 日志工具 -const wsLog = (message: string, ...args: any[]) => { - if (!WS_DEV) return - const timestamp = new Date().toISOString().split('T')[1].split('.')[0] - console.log(`[WS ${timestamp}] ${message}`, ...args) -} +// 后端状态类型 +export type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error' -const wsWarn = (message: string, ...args: any[]) => { - if (!WS_DEV) return - const timestamp = new Date().toISOString().split('T')[1].split('.')[0] - console.warn(`[WS ${timestamp}] ${message}`, ...args) -} - -const wsError = (message: string, ...args: any[]) => { - if (!WS_DEV) return - const timestamp = new Date().toISOString().split('T')[1].split('.')[0] - console.error(`[WS ${timestamp}] ${message}`, ...args) -} - -// 全局存储接口 - 移除销毁相关字段 +// 全局存储接口 - 添加后端管理和连接控制 interface GlobalWSStorage { wsRef: WebSocket | null status: Ref @@ -96,7 +78,18 @@ interface GlobalWSStorage { moduleLoadCount: number createdAt: number hasEverConnected: boolean - reconnectAttempts: number // 新增:重连尝试次数 + reconnectAttempts: number + // 新增:后端管理 + backendStatus: Ref + backendCheckTimer?: number + backendRestartAttempts: number + isRestartingBackend: boolean + lastBackendCheck: number + // 新增:连接保护 + lastConnectAttempt: number + // 新增:连接权限控制 + allowNewConnection: boolean + connectionReason: string } const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT') @@ -114,32 +107,35 @@ const initGlobalStorage = (): GlobalWSStorage => { moduleLoadCount: 0, createdAt: Date.now(), hasEverConnected: false, - reconnectAttempts: 0 + reconnectAttempts: 0, + // 后端管理 + backendStatus: ref('unknown'), + backendCheckTimer: undefined, + backendRestartAttempts: 0, + isRestartingBackend: false, + lastBackendCheck: 0, + // 连接保护 + lastConnectAttempt: 0, + // 连接权限控制 + allowNewConnection: true, // 初始化时允许创建连接 + connectionReason: '系统初始化' } } // 获取全局存储 const getGlobalStorage = (): GlobalWSStorage => { if (!(window as any)[WS_STORAGE_KEY]) { - wsLog('首次初始化全局 WebSocket 存储 - 永久连接模式') ;(window as any)[WS_STORAGE_KEY] = initGlobalStorage() } const storage = (window as any)[WS_STORAGE_KEY] as GlobalWSStorage - storage.moduleLoadCount++ - - const uptime = ((Date.now() - storage.createdAt) / 1000).toFixed(1) - wsLog(`模块加载第${storage.moduleLoadCount}次,存储运行时间: ${uptime}s,连接状态: ${storage.status.value}`) - return storage } // 设置全局状态 const setGlobalStatus = (status: WebSocketStatus) => { const global = getGlobalStorage() - const oldStatus = global.status.value global.status.value = status - wsLog(`状态变更: ${oldStatus} -> ${status} [连接ID: ${global.connectionId}]`) // 广播状态变化给所有订阅者(兼容 onStatusChange) global.subscribers.value.forEach(sub => { @@ -147,13 +143,145 @@ const setGlobalStatus = (status: WebSocketStatus) => { }) } +// 设置后端状态 +const setBackendStatus = (status: BackendStatus) => { + const global = getGlobalStorage() + global.backendStatus.value = status +} + +// 检查后端是否运行(通过WebSocket连接状态判断) +const checkBackendStatus = (): boolean => { + const global = getGlobalStorage() + + // 如果WebSocket存在且状态为OPEN,说明后端运行正常 + if (global.wsRef && global.wsRef.readyState === WebSocket.OPEN) { + return true + } + + // 如果WebSocket不存在或状态不是OPEN,说明后端可能有问题 + return false +} + +// 重启后端 +const restartBackend = async (): Promise => { + const global = getGlobalStorage() + + if (global.isRestartingBackend) { + return false + } + + try { + global.isRestartingBackend = true + global.backendRestartAttempts++ + + setBackendStatus('starting') + + // 调用 Electron API 重启后端 + if ((window.electronAPI as any)?.startBackend) { + const result = await (window.electronAPI as any).startBackend() + if (result.success) { + setBackendStatus('running') + global.backendRestartAttempts = 0 + return true + } else { + setBackendStatus('error') + return false + } + } else { + setBackendStatus('error') + return false + } + } catch (error) { + setBackendStatus('error') + return false + } finally { + global.isRestartingBackend = false + } +} + +// 后端监控和重启逻辑 +const handleBackendFailure = async () => { + const global = getGlobalStorage() + + if (global.backendRestartAttempts >= MAX_RESTART_ATTEMPTS) { + // 弹窗提示用户重启整个应用 + Modal.error({ + title: '后端服务异常', + content: '后端服务多次重启失败,请重启整个应用程序。', + okText: '重启应用', + onOk: () => { + if ((window.electronAPI as any)?.windowClose) { + (window.electronAPI as any).windowClose() + } else { + window.location.reload() + } + } + }) + return + } + + // 尝试重启后端 + setTimeout(async () => { + const success = await restartBackend() + if (success) { + // 重启成功,允许重连并等待一段时间后重新连接 WebSocket + setConnectionPermission(true, '后端重启后重连') + setTimeout(() => { + connectGlobalWebSocket('后端重启后重连').then(() => { + // 连接完成后禁止新连接 + setConnectionPermission(false, '正常运行中') + }) + }, RESTART_DELAY) + } else { + // 重启失败,继续监控 + setTimeout(handleBackendFailure, RESTART_DELAY) + } + }, RESTART_DELAY) +} + +// 启动后端监控(仅基于WebSocket状态) +const startBackendMonitoring = () => { + const global = getGlobalStorage() + + if (global.backendCheckTimer) { + clearInterval(global.backendCheckTimer) + } + + global.backendCheckTimer = window.setInterval(() => { + const isRunning = checkBackendStatus() + const now = Date.now() + global.lastBackendCheck = now + + // 基于 WebSocket 状态判断后端运行状态 + if (isRunning) { + // WebSocket连接正常 + if (global.backendStatus.value !== 'running') { + setBackendStatus('running') + global.backendRestartAttempts = 0 // 重置重启计数 + } + } else { + // WebSocket连接异常,但不频繁报告 + const shouldReportStatus = global.backendStatus.value === 'running' + if (shouldReportStatus) { + setBackendStatus('stopped') + } + } + + // 仅在必要时检查心跳超时 + if (global.lastPingTime > 0 && (now - global.lastPingTime) > HEARTBEAT_TIMEOUT * 2) { + if (global.wsRef && global.wsRef.readyState === WebSocket.OPEN) { + setBackendStatus('error') + } + } + }, BACKEND_CHECK_INTERVAL * 2) // 降低检查频率 +} + // 停止心跳 const stopGlobalHeartbeat = () => { const global = getGlobalStorage() if (global.heartbeatTimer) { clearInterval(global.heartbeatTimer) global.heartbeatTimer = undefined - wsLog('心跳检测已停止') } } @@ -162,60 +290,64 @@ const startGlobalHeartbeat = (ws: WebSocket) => { const global = getGlobalStorage() stopGlobalHeartbeat() - wsLog('启动心跳检测,间隔15秒') global.heartbeatTimer = window.setInterval(() => { - wsLog(`心跳检测 - WebSocket状态: ${ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`) - if (ws.readyState === WebSocket.OPEN) { try { const pingTime = Date.now() global.lastPingTime = pingTime const pingData = { Ping: pingTime, connectionId: global.connectionId } - wsLog('发送心跳ping', pingData) - ws.send(JSON.stringify({ + const pingMessage = JSON.stringify({ type: 'Signal', data: pingData - })) + }) + + ws.send(pingMessage) // 心跳超时检测 - 但不主动断开连接 setTimeout(() => { if (global.lastPingTime === pingTime && ws.readyState === WebSocket.OPEN) { - wsWarn(`心跳超时 - 发送时间: ${pingTime}, 当前lastPingTime: ${global.lastPingTime}, 连接状态: ${ws.readyState}`) - wsWarn('心跳超时但保持连接,等待网络层或服务端处理') + // 心跳超时但保持连接,等待网络层或服务端处理 } }, HEARTBEAT_TIMEOUT) } catch (e) { - wsError('心跳发送失败', e) - if (ws.readyState !== WebSocket.OPEN) { - wsWarn('心跳发送失败,当前连接已不再是 OPEN 状态') - } + // 心跳发送失败,静默处理 } - } else { - wsWarn(`心跳检测时连接状态异常: ${ws.readyState},但不主动断开连接`) } }, HEARTBEAT_INTERVAL) } -// 处理消息 const handleMessage = (raw: WebSocketBaseMessage) => { const global = getGlobalStorage() const msgType = String(raw.type) const id = raw.id - // 处理心跳响应 - if (msgType === 'Signal' && raw.data && raw.data.Pong) { - const pongTime = raw.data.Pong - const latency = Date.now() - pongTime - wsLog(`收到心跳pong响应,延迟: ${latency}ms`) - global.lastPingTime = 0 // 重置ping时间,表示收到了响应 - return - } + // 优先处理Signal类型的ping-pong消息,不受id限制 + if (msgType === 'Signal') { + // 处理心跳响应 + if (raw.data && raw.data.Pong) { + global.lastPingTime = 0 // 重置ping时间,表示收到了响应 + return + } - // 记录其他类型的消息 - if (msgType !== 'Signal') { - wsLog(`收到消息: type=${msgType}, id=${id || 'broadcast'}`) + // 处理后端发送的Ping,回复Pong + if (raw.data && raw.data.Ping) { + const ws = global.wsRef + if (ws && ws.readyState === WebSocket.OPEN) { + try { + const pongMessage = { + type: 'Signal', + data: { Pong: raw.data.Ping, connectionId: global.connectionId } + } + const pongJson = JSON.stringify(pongMessage) + ws.send(pongJson) + } catch (e) { + // Pong发送失败,静默处理 + } + } + return + } } const dispatch = (sub: WebSocketSubscriber) => { @@ -250,8 +382,6 @@ const handleMessage = (raw: WebSocketBaseMessage) => { const sub = global.subscribers.value.get(id) if (sub) { dispatch(sub) - } else { - wsWarn(`未找到 ws_id=${id} 的订阅者, type=${msgType}`) } } else { // 无 id 的消息广播给所有订阅者 @@ -259,15 +389,23 @@ const handleMessage = (raw: WebSocketBaseMessage) => { } } -// 延迟重连函数 -const scheduleReconnect = (global: GlobalWSStorage) => { - const delay = Math.min(1000 * Math.pow(2, global.reconnectAttempts), 30000) // 最大30秒 - wsLog(`计划在 ${delay}ms 后重连 (第${global.reconnectAttempts + 1}次尝试)`) - - setTimeout(() => { - global.reconnectAttempts++ - createGlobalWebSocket() - }, delay) +// 后端启动后建立连接的公开函数 +export const connectAfterBackendStart = async (): Promise => { + setConnectionPermission(true, '后端启动后连接') + + try { + const connected = await connectGlobalWebSocket('后端启动后连接') + if (connected) { + startBackendMonitoring() + // 连接完成后禁止新连接 + setConnectionPermission(false, '正常运行中') + return true + } else { + return false + } + } catch (error) { + return false + } } // 创建 WebSocket 连接 - 移除销毁检查,确保永不放弃连接 @@ -276,45 +414,46 @@ const createGlobalWebSocket = (): WebSocket => { // 检查现有连接状态 if (global.wsRef) { - wsLog(`检查现有连接状态: ${global.wsRef.readyState}`) - if (global.wsRef.readyState === WebSocket.OPEN) { - wsLog('检测到已有活跃连接,直接返回现有连接') return global.wsRef } if (global.wsRef.readyState === WebSocket.CONNECTING) { - wsLog('检测到正在连接的 WebSocket,返回现有连接实例') return global.wsRef } - - wsLog('现有连接状态为 CLOSING 或 CLOSED,将创建新连接') } - wsLog(`开始创建新的 WebSocket 连接到: ${BASE_WS_URL}`) const ws = new WebSocket(BASE_WS_URL) - // 记录连接创建 - wsLog(`WebSocket 实例已创建 [连接ID: ${global.connectionId}]`) - ws.onopen = () => { - wsLog(`WebSocket 连接已建立 [连接ID: ${global.connectionId}]`) global.isConnecting = false global.hasEverConnected = true global.reconnectAttempts = 0 // 重置重连计数 setGlobalStatus('已连接') + startGlobalHeartbeat(ws) - // 发送连接确认 + // 连接成功后禁止新连接 + setConnectionPermission(false, '正常运行中') + + // 发送连接确认和初始pong try { const connectData = { Connect: true, connectionId: global.connectionId } - wsLog('发送连接确认信号', connectData) - ws.send(JSON.stringify({ + const connectMessage = JSON.stringify({ type: 'Signal', data: connectData - })) + }) + + ws.send(connectMessage) + + // 发送初始pong以重置后端last_pong时间 + const initialPongMessage = JSON.stringify({ + type: 'Signal', + data: { Pong: Date.now(), connectionId: global.connectionId } + }) + ws.send(initialPongMessage) } catch (e) { - wsError('发送连接确认失败', e) + // 连接确认发送失败,静默处理 } } @@ -323,113 +462,161 @@ const createGlobalWebSocket = (): WebSocket => { const raw = JSON.parse(ev.data) as WebSocketBaseMessage handleMessage(raw) } catch (e) { - wsError('解析 WebSocket 消息失败', e, '原始数据:', ev.data) + // 消息解析失败,静默处理 } } - ws.onerror = (event) => { - wsError(`WebSocket 连接错误 [连接ID: ${global.connectionId}]`, event) - wsError(`错误发生时连接状态: ${ws.readyState}`) + ws.onerror = () => { setGlobalStatus('连接错误') } ws.onclose = (event) => { - wsLog(`WebSocket 连接已关闭 [连接ID: ${global.connectionId}]`) - wsLog(`关闭码: ${event.code}, 关闭原因: "${event.reason}", 是否干净关闭: ${event.wasClean}`) - - // 详细分析关闭原因 - const closeReasons: { [key: number]: string } = { - 1000: '正常关闭', - 1001: '终端离开(如页面关闭)', - 1002: '协议错误', - 1003: '不支持的数据类型', - 1005: '未收到状态码', - 1006: '连接异常关闭', - 1007: '数据格式错误', - 1008: '策略违规', - 1009: '消息过大', - 1010: '扩展协商失败', - 1011: '服务器意外错误', - 1015: 'TLS握手失败' - } - - const reasonDesc = closeReasons[event.code] || '未知原因' - wsLog(`关闭详情: ${reasonDesc}`) - setGlobalStatus('已断开') stopGlobalHeartbeat() global.isConnecting = false - // 永不放弃:立即安排重连 - wsLog('连接断开,安排自动重连以保持永久连接') - scheduleReconnect(global) + // 检查是否是后端自杀导致的关闭 + if (event.code === 1000 && event.reason === 'Ping超时') { + handleBackendFailure() + } else { + // 连接断开,不自动重连,等待后端重启 + setGlobalStatus('已断开') + } } // 为新创建的 WebSocket 设置引用 global.wsRef = ws - wsLog(`WebSocket 引用已设置到全局存储`) return ws } -// 连接全局 WebSocket - 简化逻辑,移除销毁检查 -const connectGlobalWebSocket = async (): Promise => { +// 连接全局 WebSocket - 确保单一连接 +const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promise => { const global = getGlobalStorage() - // 详细检查连接状态 - if (global.wsRef) { - wsLog(`检查现有连接: readyState=${global.wsRef.readyState}, isConnecting=${global.isConnecting}`) - - if (global.wsRef.readyState === WebSocket.OPEN) { - wsLog('WebSocket 已连接,直接返回') - return true - } - - if (global.wsRef.readyState === WebSocket.CONNECTING) { - wsLog('WebSocket 正在连接中') - return true - } + // 首先检查连接权限 + if (!checkConnectionPermission()) { + return false } - if (global.isConnecting) { - wsLog('全局连接标志显示正在连接中,等待连接完成') - return true + // 验证连接原因是否合法 + if (!isValidConnectionReason(reason)) { + return false + } + + // 尝试获取全局连接锁 + if (!acquireConnectionLock()) { + return false } try { - wsLog('开始建立 WebSocket 连接流程') + // 严格检查现有连接,避免重复创建 + if (global.wsRef) { + const state = global.wsRef.readyState + + if (state === WebSocket.OPEN) { + setGlobalStatus('已连接') + return true + } + + if (state === WebSocket.CONNECTING) { + return true + } + + // CLOSING 或 CLOSED 状态才允许创建新连接 + if (state === WebSocket.CLOSING) { + return false + } + } + + // 检查全局连接标志 - 增强防重复逻辑 + if (global.isConnecting) { + return false + } + + // 额外保护:检查最近连接尝试时间,避免过于频繁的连接 + const now = Date.now() + const MIN_CONNECT_INTERVAL = 2000 // 最小连接间隔2秒 + if (global.lastConnectAttempt && (now - global.lastConnectAttempt) < MIN_CONNECT_INTERVAL) { + return false + } + global.isConnecting = true + global.lastConnectAttempt = now + + // 清理旧连接引用(如果存在且已关闭) + if (global.wsRef && global.wsRef.readyState === WebSocket.CLOSED) { + global.wsRef = null + } + global.wsRef = createGlobalWebSocket() setGlobalStatus('连接中') - wsLog('WebSocket 连接流程已启动') return true } catch (e) { - wsError('创建 WebSocket 失败', e) setGlobalStatus('连接错误') global.isConnecting = false - - // 即使创建失败也要安排重连 - scheduleReconnect(global) return false + } finally { + // 确保始终释放连接锁 + releaseConnectionLock() } } -// 模块初始化逻辑 -wsLog('=== WebSocket 模块开始初始化 - 永久连接模式 ===') +// 移除未使用的函数,已改为外部调用 connectAfterBackendStart + +// 连接权限控制函数 +const setConnectionPermission = (allow: boolean, reason: string) => { + const global = getGlobalStorage() + global.allowNewConnection = allow + global.connectionReason = reason +} + +const checkConnectionPermission = (): boolean => { + const global = getGlobalStorage() + if (!global.allowNewConnection) { + return false + } + return true +} + +// 只在后端启动/重启时允许创建连接 +const allowedConnectionReasons = [ + '后端启动后连接', + '后端重启后重连' +] + +const isValidConnectionReason = (reason: string): boolean => { + return allowedConnectionReasons.includes(reason) +} + +// 全局连接锁 - 防止多个模块实例同时连接 +let isGlobalConnectingLock = false + +// 获取全局连接锁 +const acquireConnectionLock = (): boolean => { + if (isGlobalConnectingLock) { + return false + } + isGlobalConnectingLock = true + return true +} + +// 释放全局连接锁 +const releaseConnectionLock = () => { + isGlobalConnectingLock = false +} + +// 模块初始化逻辑 - 不自动建立连接 const global = getGlobalStorage() -if (global.moduleLoadCount > 1) { - wsLog(`检测到模块热更新重载 (第${global.moduleLoadCount}次)`) - wsLog(`当前连接状态: ${global.wsRef ? global.wsRef.readyState : 'null'}`) - wsLog('保持现有连接,不重新建立连接') -} else { - wsLog('首次加载模块,建立永久 WebSocket 连接') - connectGlobalWebSocket() +// 只在模块真正加载时计数一次 +if (global.moduleLoadCount === 0) { + global.moduleLoadCount = 1 } // 页面卸载时不关闭连接,保持永久连接 window.addEventListener('beforeunload', () => { - wsLog('页面即将卸载,但保持 WebSocket 连接') + // 保持连接 }) // 主要 Hook 函数 @@ -438,46 +625,28 @@ export function useWebSocket() { const subscribe = (id: string, handlers: Omit) => { global.subscribers.value.set(id, { id, ...handlers }) - wsLog(`添加订阅者: ${id},当前订阅者总数: ${global.subscribers.value.size}`) } const unsubscribe = (id: string) => { - const existed = global.subscribers.value.delete(id) - wsLog(`移除订阅者: ${id},是否存在: ${existed},剩余订阅者: ${global.subscribers.value.size}`) + global.subscribers.value.delete(id) } const sendRaw = (type: string, data?: any, id?: string) => { const ws = global.wsRef - wsLog(`尝试发送消息: type=${type}, id=${id || 'broadcast'}`) if (ws && ws.readyState === WebSocket.OPEN) { try { const messageData = { id, type, data } ws.send(JSON.stringify(messageData)) - wsLog('消息发送成功') } catch (e) { - wsError('发送消息失败', e) + // 发送失败,静默处理 } - } else { - wsWarn(`WebSocket 未准备就绪: ${ws ? `状态=${ws.readyState}` : '连接为null'}`) - wsWarn('消息将在连接恢复后可用') } } - const startTaskRaw = (params: any) => { - wsLog('发送启动任务请求', params) - sendRaw('StartTask', params) - } - - // 移除 destroy 功能,确保连接永不断开 - const forceReconnect = () => { - wsLog('手动触发重连') - if (global.wsRef) { - // 不关闭现有连接,直接尝试创建新连接 - global.isConnecting = false - connectGlobalWebSocket() - } - return true + // 移除 forceReconnect 功能,现在只能通过后端重启建立连接 + const ensureConnection = () => { + return Promise.resolve(false) } const getConnectionInfo = () => { @@ -491,87 +660,43 @@ export function useWebSocket() { hasHeartbeat: !!global.heartbeatTimer, hasEverConnected: global.hasEverConnected, reconnectAttempts: global.reconnectAttempts, - wsDevEnabled: WS_DEV, isPersistentMode: true // 标识为永久连接模式 } - wsLog('连接信息查询', info) return info } - // 兼容旧版 API:connect 重载 - async function connect(): Promise - async function connect(config: WebSocketConfig): Promise - async function connect(config?: WebSocketConfig): Promise { - if (!config) { - // 无参数调用:返回连接状态 - return connectGlobalWebSocket() - } - - // 有参数调用:建立订阅,复用现有连接 - const ok = await connectGlobalWebSocket() - if (!ok) { - // 即使连接失败也要建立订阅,等待连接恢复 - wsLog('连接暂时不可用,但仍建立订阅等待连接恢复') - } - - // 先移除旧订阅避免重复 - if (global.subscribers.value.has(config.taskId)) { - unsubscribe(config.taskId) - } - - subscribe(config.taskId, { - onProgress: config.onProgress, - onResult: config.onResult, - onError: (e) => { - if (typeof config.onError === 'function') config.onError(e) - }, - onNotify: (n) => { - config.onNotify?.(n) - if (config.showNotifications && n?.title) { - notification.info({ message: n.title, description: n.content }) - } - }, - onMessage: config.onMessage, - onStatusChange: config.onStatusChange - }) - - // 立即推送当前状态 - config.onStatusChange?.(global.status.value) - - // 可根据 mode 发送一个初始信号(可选) - if (config.mode) { - sendRaw('Mode', { mode: config.mode }, config.taskId) - } - - return config.taskId + // 手动重启后端 + const restartBackendManually = async () => { + const global = getGlobalStorage() + global.backendRestartAttempts = 0 // 重置重启计数 + return await restartBackend() } - // 兼容旧版 API:disconnect / disconnectAll - 只取消订阅,不断开连接 - const disconnect = (taskId: string) => { - if (!taskId) return - unsubscribe(taskId) - wsLog(`兼容模式取消订阅: ${taskId}`) - } - - const disconnectAll = () => { - const ids = Array.from(global.subscribers.value.keys()) - ids.forEach((id: string) => unsubscribe(id)) - wsLog('已取消所有订阅 (disconnectAll)') + // 获取后端状态 + const getBackendStatus = () => { + const global = getGlobalStorage() + return { + status: global.backendStatus.value, + restartAttempts: global.backendRestartAttempts, + isRestarting: global.isRestartingBackend, + lastCheck: global.lastBackendCheck + } } return { - // 兼容 API - connect, - disconnect, - disconnectAll, - // 原有 API & 工具 + // 新的订阅 API subscribe, unsubscribe, sendRaw, - startTaskRaw, - forceReconnect, + // 连接管理 + ensureConnection, getConnectionInfo, + // 状态 status: global.status, - subscribers: global.subscribers + subscribers: global.subscribers, + // 后端管理 + backendStatus: global.backendStatus, + restartBackend: restartBackendManually, + getBackendStatus } } diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index 832e76a..5cb20bb 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -18,7 +18,8 @@ export interface ElectronAPI { installDependencies: (mirror?: string) => Promise cloneBackend: (repoUrl?: string) => Promise updateBackend: (repoUrl?: string) => Promise - startBackend: () => Promise + startBackend: () => Promise<{ success: boolean; error?: string }> + stopBackend?: () => Promise<{ success: boolean; error?: string }> // 管理员权限相关 checkAdmin: () => Promise diff --git a/frontend/src/views/Scheduler.vue b/frontend/src/views/Scheduler.vue index 5dbdab0..e6da198 100644 --- a/frontend/src/views/Scheduler.vue +++ b/frontend/src/views/Scheduler.vue @@ -206,7 +206,7 @@ import { PlayCircleOutlined, StopOutlined } from '@ant-design/icons-vue' import { Service } from '@/api/services/Service' import type { ComboBoxItem } from '@/api/models/ComboBoxItem' import { TaskCreateIn } from '@/api/models/TaskCreateIn' -import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket' +import { useWebSocket } from '@/composables/useWebSocket' // 类型定义 interface RunningTask { @@ -246,8 +246,8 @@ const messageResponse = ref('') const taskForm = reactive<{ taskId: string | null; mode: TaskCreateIn.mode }>({ taskId: null, mode: TaskCreateIn.mode.AutoMode }) const quickTaskForm = reactive<{ taskId: string | null; mode: TaskCreateIn.mode }>({ taskId: null, mode: TaskCreateIn.mode.AutoMode }) -// WebSocket API -const { connect: wsConnect, disconnect: wsDisconnect, sendRaw } = useWebSocket() +// WebSocket API - 更新为新的订阅机制 +const { subscribe, unsubscribe } = useWebSocket() // Tab 事件 const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'remove') => { @@ -263,7 +263,7 @@ const addSchedulerTab = () => { const removeSchedulerTab = (key: string) => { const idx = schedulerTabs.value.findIndex(t => t.key === key) if (idx === -1) return - schedulerTabs.value[idx].runningTasks.forEach(t => wsDisconnect(t.websocketId)) + schedulerTabs.value[idx].runningTasks.forEach(t => unsubscribe(t.websocketId)) schedulerTabs.value.splice(idx, 1) if (activeSchedulerTab.value === key) activeSchedulerTab.value = schedulerTabs.value[Math.max(0, idx - 1)]?.key || 'main' } @@ -321,7 +321,7 @@ const startQuickTask = async () => { const idx = currentTab.value.runningTasks.findIndex(t => t.taskName === name) if (idx >= 0) { const existing = currentTab.value.runningTasks[idx] - wsDisconnect(existing.websocketId) + unsubscribe(existing.websocketId) const oldId = existing.websocketId existing.websocketId = r.websocketId existing.status = '连接中' @@ -347,18 +347,18 @@ const startQuickTask = async () => { } } -// 订阅任务 +// 订阅任务 - 已重构为新的WebSocket订阅机制 const subscribeTask = (task: RunningTask, mode: TaskCreateIn.mode) => { - wsConnect({ - taskId: task.websocketId, - mode, - onMessage: raw => handleWebSocketMessage(task, raw), - onStatusChange: st => { - if (st === '已连接' && task.status === '连接中') task.status = '运行中' - if (st === '已断开' && task.status === '运行中') task.status = '已断开' - if (st === '连接错误') task.status = '连接错误' - } + // 使用已有的WebSocket订阅API实例 + subscribe(task.websocketId, { + onProgress: (data) => handleTaskProgress(task, data), + onResult: (data) => handleTaskResult(task, data), + onError: (data) => handleTaskError(task, data), + onNotify: (data) => handleTaskNotify(task, data) }) + + task.status = '运行中' + addTaskLog(task, `任务 ${task.taskName} 已开始执行 (模式: ${mode})`, 'info') } // 取消添加 @@ -393,6 +393,65 @@ const checkAllTasksCompleted = () => { message.success(`所有任务结束,准备执行动作: ${action}`) } +// 新的WebSocket消息处理函数 +const handleTaskProgress = (task: RunningTask, data: any) => { + if (data?.task_list) { + const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId) + if (idx >= 0) { + currentTab.value.runningTasks[idx].userQueue = data.task_list.map((i: any) => ({ + name: i.name || '未知任务', + status: i.status || '未知' + })) + } + } + if (data) { + Object.entries(data).forEach(([k, v]) => { + if (k !== 'task_list') addTaskLog(task, `${k}: ${v}`, 'info') + }) + } +} + +const handleTaskResult = (task: RunningTask, data: any) => { + const isSuccess = !data?.Error + const content = data?.Error || data?.message || data?.val || '任务完成' + addTaskLog(task, content, isSuccess ? 'success' : 'error') + task.status = isSuccess ? '已完成' : '已失败' + + if (isSuccess) { + notification.success({ message: '任务完成', description: content }) + } else { + notification.error({ message: '任务失败', description: content }) + } + checkAllTasksCompleted() +} + +const handleTaskError = (task: RunningTask, data: any) => { + const content = data?.message || data?.Error || data?.val || '任务发生错误' + addTaskLog(task, content, 'error') + task.status = '已失败' + notification.error({ message: '任务错误', description: content }) + checkAllTasksCompleted() +} + +const handleTaskNotify = (task: RunningTask, data: any) => { + if (data?.needInput || data?.messageId) { + // 需要用户输入的消息 + currentMessage.value = { + title: '任务消息', + content: data?.message || data?.val || '任务需要您的输入', + needInput: true, + messageId: data?.messageId, + taskId: task.websocketId + } + messageModalVisible.value = true + } else { + // 普通通知消息 + const content = data?.message || data?.val || '任务通知' + addTaskLog(task, content, 'info') + notification.info({ message: '任务信息', description: content }) + } +} + // 消息弹窗控制 const cancelMessage = () => { messageModalVisible.value = false @@ -400,67 +459,26 @@ const cancelMessage = () => { currentMessage.value = null } -// WebSocket 消息处理 -const handleWebSocketMessage = (task: RunningTask, raw: WebSocketBaseMessage) => { - const type = raw.type - const payload: any = raw.data - const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId) - if (idx === -1) return - switch (type) { - case 'Update': { - if (payload?.task_list) { - currentTab.value.runningTasks[idx].userQueue = payload.task_list.map((i: any) => ({ name: i.name || '未知任务', status: i.status || '未知' })) - } - if (payload) Object.entries(payload).forEach(([k, v]) => { if (k !== 'task_list') addTaskLog(currentTab.value.runningTasks[idx], `${k}: ${v}`, 'info') }) - break - } - case 'Message': { - currentMessage.value = { title: '任务消息', content: payload?.message || payload?.val || '任务需要您的输入', needInput: true, messageId: payload?.messageId || (raw as any).messageId, taskId: task.websocketId } - messageModalVisible.value = true - break - } - case 'Info': { - const isErr = !!payload?.Error - const content = payload?.Error || payload?.val || payload?.message || '未知通知' - addTaskLog(task, content, isErr ? 'error' : 'info') - if (isErr) notification.error({ message: '任务错误', description: content }) - else notification.info({ message: '任务信息', description: content }) - break - } - case 'Signal': { - if (payload?.Accomplish !== undefined) { - const done = !!payload.Accomplish - currentTab.value.runningTasks[idx].status = done ? '已完成' : '已失败' - addTaskLog(currentTab.value.runningTasks[idx], `任务${done ? '已完成' : '已失败'}`, done ? 'success' : 'error') - checkAllTasksCompleted() - wsDisconnect(task.websocketId) - } - break - } - default: - addTaskLog(task, `收到未知消息类型: ${type}`, 'warning') - } -} - -// 回复消息 +// 回复消息 - 待重构为WebSocket消息发送 const sendMessageResponse = () => { if (!currentMessage.value?.taskId) return const task = schedulerTabs.value.flatMap(t => t.runningTasks).find(t => t.websocketId === currentMessage.value!.taskId) if (task) { - sendRaw('MessageResponse', { messageId: currentMessage.value!.messageId, response: messageResponse.value }, task.websocketId) + // TODO: 实现WebSocket消息回复机制 addTaskLog(task, `用户回复: ${messageResponse.value}`, 'info') + message.warning('消息回复功能待重构为WebSocket发送') } messageModalVisible.value = false messageResponse.value = '' currentMessage.value = null } -// 停止任务 +// 停止任务 - 已重构为新的WebSocket取消订阅 const stopTask = (id: string) => { const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === id) if (idx >= 0) { const task = currentTab.value.runningTasks[idx] - wsDisconnect(task.websocketId) + unsubscribe(task.websocketId) currentTab.value.runningTasks.splice(idx, 1) const p = currentTab.value.activeTaskPanels.indexOf(id) if (p >= 0) currentTab.value.activeTaskPanels.splice(p, 1) @@ -468,15 +486,15 @@ const stopTask = (id: string) => { } } -// 清空日志(按钮已注释,可保留) -const clearTaskOutput = (id: string) => { - const t = currentTab.value.runningTasks.find(x => x.websocketId === id) - if (t) t.logs = [] -} - // 生命周期 -onMounted(() => { wsConnect(); loadTaskOptions() }) -onUnmounted(() => { schedulerTabs.value.forEach(tab => tab.runningTasks.forEach(t => wsDisconnect(t.websocketId))) }) +onMounted(() => { + // WebSocket 连接由 useWebSocket 模块自动管理,这里只加载任务选项 + loadTaskOptions() +}) +onUnmounted(() => { + // 清理订阅,但不断开全局连接 + schedulerTabs.value.forEach(tab => tab.runningTasks.forEach(t => unsubscribe(t.websocketId))) +}) \ No newline at end of file diff --git a/frontend/src/views/MAAUserEdit.vue b/frontend/src/views/MAAUserEdit.vue index 7f42b60..713a463 100644 --- a/frontend/src/views/MAAUserEdit.vue +++ b/frontend/src/views/MAAUserEdit.vue @@ -898,12 +898,14 @@ import { useUserApi } from '@/composables/useUserApi' import { useScriptApi } from '@/composables/useScriptApi' import { useWebSocket } from '@/composables/useWebSocket' import { Service } from '@/api' +import { GetStageIn } from '@/api/models/GetStageIn' +import { defineComponent } from 'vue' const router = useRouter() const route = useRoute() const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi() const { getScript } = useScriptApi() -const { connect, disconnect } = useWebSocket() +const { subscribe, unsubscribe } = useWebSocket() const formRef = ref() const loading = computed(() => userLoading.value) @@ -1129,19 +1131,17 @@ const loadUserData = async () => { const loadStageOptions = async () => { try { const response = await Service.getStageComboxApiInfoComboxStagePost({ - type: 'Today', + type: GetStageIn.type.TODAY }) if (response && response.code === 200 && response.data) { - const sorted = [...response.data].sort((a, b) => { + stageOptions.value = [...response.data].sort((a, b) => { if (a.value === '-') return -1 if (b.value === '-') return 1 return 0 }) - stageOptions.value = sorted } } catch (error) { console.error('加载关卡选项失败:', error) - // 保持默认选项 } } @@ -1157,14 +1157,21 @@ const loadStageModeOptions = async () => { } } +// 替换 VNodes 组件定义 +const VNodes = defineComponent({ + props: { vnodes: { type: Object, required: true } }, + setup(props) { + return () => props.vnodes as any + } +}) + // 选择基建配置文件 const selectInfrastructureConfig = async () => { try { - const path = await window.electronAPI?.selectFile([ + const path = await (window as any).electronAPI?.selectFile([ { name: 'JSON 文件', extensions: ['json'] }, { name: '所有文件', extensions: ['*'] }, ]) - if (path && path.length > 0) { infrastructureConfigPath.value = path formData.Info.InfrastPath = path[0] @@ -1182,28 +1189,22 @@ const importInfrastructureConfig = async () => { message.warning('请先选择配置文件') return } - if (!isEdit.value) { message.warning('请先保存用户后再导入配置') return } - try { infrastructureImporting.value = true - - // 调用API导入基建配置 const result = await Service.importInfrastructureApiScriptsUserInfrastructurePost({ scriptId: scriptId, userId: userId, jsonFile: infrastructureConfigPath.value[0], }) - if (result && result.code === 200) { message.success('基建配置导入成功') - // 清空文件路径 infrastructureConfigPath.value = '' } else { - message.error(result?.msg || '基建配置导入失败') + message.error('基建配置导入失败') } } catch (error) { console.error('基建配置导入失败:', error) @@ -1285,33 +1286,22 @@ const handleMAAConfig = async () => { // 如果已有连接,先断开 if (maaWebsocketId.value) { - disconnect(maaWebsocketId.value) + unsubscribe(maaWebsocketId.value) maaWebsocketId.value = null } - // 建立WebSocket连接进行MAA配置 - const websocketId = await connect({ - taskId: userId, // 使用用户ID进行配置 - mode: '设置脚本', - showNotifications: true, - onStatusChange: status => { - console.log(`用户 ${formData.userName} MAA配置状态: ${status}`) - }, - onMessage: data => { - console.log(`用户 ${formData.userName} MAA配置消息:`, data) - // 这里可以根据需要处理特定的消息 - }, + // 直接订阅(旧 connect 参数移除) + const subId = userId + subscribe(subId, { onError: error => { console.error(`用户 ${formData.userName} MAA配置错误:`, error) message.error(`MAA配置连接失败: ${error}`) maaWebsocketId.value = null - }, + } }) - if (websocketId) { - maaWebsocketId.value = websocketId - message.success(`已开始配置用户 ${formData.userName} 的MAA设置`) - } + maaWebsocketId.value = subId + message.success(`已开始配置用户 ${formData.userName} 的MAA设置`) } catch (error) { console.error('MAA配置失败:', error) message.error('MAA配置失败') @@ -1335,17 +1325,12 @@ const stage3InputRef = ref() const stageRemainInputRef = ref() // VNodes 组件,用于渲染下拉菜单内容 -const VNodes = { - props: { - vnodes: { - type: Object, - required: true, - }, - }, - render() { - return this.vnodes - }, -} +const VNodes = defineComponent({ + props: { vnodes: { type: Object, required: true } }, + setup(props) { + return () => props.vnodes as any + } +}) // 验证关卡名称格式 const validateStageName = (stageName: string): boolean => { @@ -1465,9 +1450,8 @@ const addCustomStageRemain = () => { } const handleCancel = () => { - // 清理WebSocket连接 if (maaWebsocketId.value) { - disconnect(maaWebsocketId.value) + unsubscribe(maaWebsocketId.value) maaWebsocketId.value = null } router.push('/scripts') diff --git a/frontend/src/views/Scripts.vue b/frontend/src/views/Scripts.vue index 867c025..717b5f5 100644 --- a/frontend/src/views/Scripts.vue +++ b/frontend/src/views/Scripts.vue @@ -236,9 +236,9 @@ import MarkdownIt from 'markdown-it' const router = useRouter() const { addScript, deleteScript, getScriptsWithUsers, loading } = useScriptApi() -const { addUser, updateUser, deleteUser, loading: userLoading } = useUserApi() +const { updateUser, deleteUser } = useUserApi() const { subscribe, unsubscribe } = useWebSocket() -const { getWebConfigTemplates, importScriptFromWeb, loading: templateApiLoading } = useTemplateApi() +const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi() // 初始化markdown解析器 const md = new MarkdownIt({ @@ -503,19 +503,11 @@ const handleMAAConfig = async (script: Script) => { return } - // 建立WebSocket订阅进行MAA配置 + // 新订阅 subscribe(script.id, { - onStatusChange: status => { - console.log(`脚本 ${script.name} 连接状态: ${status}`) - }, - onMessage: data => { - console.log(`脚本 ${script.name} 收到消息:`, data) - // 这里可以根据需要处理特定的消息 - }, onError: error => { console.error(`脚本 ${script.name} 连接错误:`, error) message.error(`MAA配置连接失败: ${error}`) - // 清理连接记录 activeConnections.value.delete(script.id) }, }) From 47e72918858415e220a7f28de14a91b16e0ff9b4 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Thu, 11 Sep 2025 23:35:42 +0800 Subject: [PATCH 5/6] =?UTF-8?q?:wastebasket:=20=E4=BF=AE=E5=A4=8D=E8=AD=A6?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/composables/useWebSocket.ts | 115 ++++++++++++++--------- 1 file changed, 73 insertions(+), 42 deletions(-) diff --git a/frontend/src/composables/useWebSocket.ts b/frontend/src/composables/useWebSocket.ts index 255f1db..97173fd 100644 --- a/frontend/src/composables/useWebSocket.ts +++ b/frontend/src/composables/useWebSocket.ts @@ -1,5 +1,5 @@ import { ref, type Ref } from 'vue' -import { message, notification, Modal } from 'ant-design-vue' +import { message, Modal, notification } from 'ant-design-vue' // 基础配置 const BASE_WS_URL = 'ws://localhost:36163/api/core/ws' @@ -87,7 +87,7 @@ const initGlobalStorage = (): GlobalWSStorage => { heartbeatTimer: undefined, isConnecting: false, lastPingTime: 0, - connectionId: Math.random().toString(36).substr(2, 9), + connectionId: Math.random().toString(36).substring(2, 9), moduleLoadCount: 0, createdAt: Date.now(), hasEverConnected: false, @@ -102,7 +102,7 @@ const initGlobalStorage = (): GlobalWSStorage => { lastConnectAttempt: 0, // 连接权限控制 allowNewConnection: true, // 初始化时允许创建连接 - connectionReason: '系统初始化' + connectionReason: '系统初始化', } } @@ -112,8 +112,7 @@ const getGlobalStorage = (): GlobalWSStorage => { ;(window as any)[WS_STORAGE_KEY] = initGlobalStorage() } - const storage = (window as any)[WS_STORAGE_KEY] as GlobalWSStorage - return storage + return (window as any)[WS_STORAGE_KEY] as GlobalWSStorage } // 设置全局状态 @@ -131,12 +130,12 @@ const setBackendStatus = (status: BackendStatus) => { // 检查后端是否运行(通过WebSocket连接状态判断) const checkBackendStatus = (): boolean => { const global = getGlobalStorage() - + // 如果WebSocket存在且状态为OPEN,说明后端运行正常 if (global.wsRef && global.wsRef.readyState === WebSocket.OPEN) { return true } - + // 如果WebSocket不存在或状态不是OPEN,说明后端可能有问题 return false } @@ -144,7 +143,7 @@ const checkBackendStatus = (): boolean => { // 重启后端 const restartBackend = async (): Promise => { const global = getGlobalStorage() - + if (global.isRestartingBackend) { return false } @@ -152,7 +151,7 @@ const restartBackend = async (): Promise => { try { global.isRestartingBackend = true global.backendRestartAttempts++ - + setBackendStatus('starting') // 调用 Electron API 重启后端 @@ -181,7 +180,7 @@ const restartBackend = async (): Promise => { // 后端监控和重启逻辑 const handleBackendFailure = async () => { const global = getGlobalStorage() - + if (global.backendRestartAttempts >= MAX_RESTART_ATTEMPTS) { // 弹窗提示用户重启整个应用 Modal.error({ @@ -190,11 +189,11 @@ 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() } - } + }, }) return } @@ -221,16 +220,16 @@ const handleBackendFailure = async () => { // 启动后端监控(仅基于WebSocket状态) const startBackendMonitoring = () => { const global = getGlobalStorage() - + if (global.backendCheckTimer) { clearInterval(global.backendCheckTimer) } - + global.backendCheckTimer = window.setInterval(() => { const isRunning = checkBackendStatus() const now = Date.now() global.lastBackendCheck = now - + // 基于 WebSocket 状态判断后端运行状态 if (isRunning) { // WebSocket连接正常 @@ -245,9 +244,9 @@ const startBackendMonitoring = () => { setBackendStatus('stopped') } } - + // 仅在必要时检查心跳超时 - if (global.lastPingTime > 0 && (now - global.lastPingTime) > HEARTBEAT_TIMEOUT * 2) { + if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT * 2) { if (global.wsRef && global.wsRef.readyState === WebSocket.OPEN) { setBackendStatus('error') } @@ -274,9 +273,18 @@ const startGlobalHeartbeat = (ws: WebSocket) => { try { const pingTime = Date.now() global.lastPingTime = pingTime - ws.send(JSON.stringify({ type: 'Signal', data: { Ping: pingTime, connectionId: global.connectionId } })) - setTimeout(() => { /* 心跳超时不主动断开 */ }, HEARTBEAT_TIMEOUT) - } catch { /* ignore */ } + ws.send( + JSON.stringify({ + type: 'Signal', + data: { Ping: pingTime, connectionId: global.connectionId }, + }) + ) + setTimeout(() => { + /* 心跳超时不主动断开 */ + }, HEARTBEAT_TIMEOUT) + } catch { + /* ignore */ + } } }, HEARTBEAT_INTERVAL) } @@ -299,7 +307,12 @@ const handleMessage = (raw: WebSocketBaseMessage) => { const ws = global.wsRef if (ws && ws.readyState === WebSocket.OPEN) { try { - ws.send(JSON.stringify({ type: 'Signal', data: { Pong: raw.data.Ping, connectionId: global.connectionId } })) + ws.send( + JSON.stringify({ + type: 'Signal', + data: { Pong: raw.data.Ping, connectionId: global.connectionId }, + }) + ) } catch (e) { // Pong发送失败,静默处理 } @@ -325,7 +338,7 @@ const handleMessage = (raw: WebSocketBaseMessage) => { if (raw.data && (raw.data as NotifyMessage).title) { notification.info({ message: (raw.data as NotifyMessage).title, - description: (raw.data as NotifyMessage).content + description: (raw.data as NotifyMessage).content, }) } return @@ -347,7 +360,7 @@ const handleMessage = (raw: WebSocketBaseMessage) => { // 后端启动后建立连接的公开函数 export const connectAfterBackendStart = async (): Promise => { setConnectionPermission(true, '后端启动后连接') - + try { const connected = await connectGlobalWebSocket('后端启动后连接') if (connected) { @@ -385,7 +398,7 @@ const createGlobalWebSocket = (): WebSocket => { global.hasEverConnected = true global.reconnectAttempts = 0 setGlobalStatus('已连接') - + startGlobalHeartbeat(ws) // 连接成功后禁止新连接 @@ -393,12 +406,24 @@ const createGlobalWebSocket = (): WebSocket => { // 发送连接确认和初始pong try { - ws.send(JSON.stringify({ type: 'Signal', data: { Connect: true, connectionId: global.connectionId } })) - ws.send(JSON.stringify({ type: 'Signal', data: { Pong: Date.now(), connectionId: global.connectionId } })) - } catch { /* ignore */ } + ws.send( + JSON.stringify({ + type: 'Signal', + data: { Connect: true, connectionId: global.connectionId }, + }) + ) + ws.send( + JSON.stringify({ + type: 'Signal', + data: { Pong: Date.now(), connectionId: global.connectionId }, + }) + ) + } catch { + /* ignore */ + } } - ws.onmessage = (ev) => { + ws.onmessage = ev => { try { const raw = JSON.parse(ev.data) as WebSocketBaseMessage handleMessage(raw) @@ -411,14 +436,17 @@ const createGlobalWebSocket = (): WebSocket => { setGlobalStatus('连接错误') } - ws.onclose = (event) => { + ws.onclose = event => { setGlobalStatus('已断开') stopGlobalHeartbeat() global.isConnecting = false // 检查是否是后端自杀导致的关闭 if (event.code === 1000 && event.reason === 'Ping超时') { - handleBackendFailure() + handleBackendFailure().catch(error => { + // 忽略错误,或者可以添加适当的错误处理 + console.warn('handleBackendFailure error:', error) + }) } else { // 连接断开,不自动重连,等待后端重启 setGlobalStatus('已断开') @@ -478,18 +506,18 @@ const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promi // 额外保护:检查最近连接尝试时间,避免过于频繁的连接 const now = Date.now() const MIN_CONNECT_INTERVAL = 2000 // 最小连接间隔2秒 - if (global.lastConnectAttempt && (now - global.lastConnectAttempt) < MIN_CONNECT_INTERVAL) { + if (global.lastConnectAttempt && now - global.lastConnectAttempt < MIN_CONNECT_INTERVAL) { return false } global.isConnecting = true global.lastConnectAttempt = now - + // 清理旧连接引用(如果存在且已关闭) if (global.wsRef && global.wsRef.readyState === WebSocket.CLOSED) { global.wsRef = null } - + global.wsRef = createGlobalWebSocket() setGlobalStatus('连接中') return true @@ -512,16 +540,14 @@ const setConnectionPermission = (allow: boolean, reason: string) => { const checkConnectionPermission = (): boolean => { const global = getGlobalStorage() - return !!global.allowNewConnection + return global.allowNewConnection } // 只在后端启动/重启时允许创建连接 -const allowedConnectionReasons = [ - '后端启动后连接', - '后端重启后重连' -] +const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连'] -const isValidConnectionReason = (reason: string): boolean => allowedConnectionReasons.includes(reason) +const isValidConnectionReason = (reason: string): boolean => + allowedConnectionReasons.includes(reason) // 全局连接锁 - 防止多个模块实例同时连接 let isGlobalConnectingLock = false @@ -587,7 +613,7 @@ export function useWebSocket() { hasHeartbeat: !!global.heartbeatTimer, hasEverConnected: global.hasEverConnected, reconnectAttempts: global.reconnectAttempts, - isPersistentMode: true // 标识为永久连接模式 + isPersistentMode: true, // 标识为永久连接模式 }) const restartBackendManually = async () => { @@ -598,7 +624,12 @@ export function useWebSocket() { const getBackendStatus = () => { const global = getGlobalStorage() - return { status: global.backendStatus.value, restartAttempts: global.backendRestartAttempts, isRestarting: global.isRestartingBackend, lastCheck: global.lastBackendCheck } + return { + status: global.backendStatus.value, + restartAttempts: global.backendRestartAttempts, + isRestarting: global.isRestartingBackend, + lastCheck: global.lastBackendCheck, + } } return { @@ -610,6 +641,6 @@ export function useWebSocket() { subscribers: global.subscribers, backendStatus: global.backendStatus, restartBackend: restartBackendManually, - getBackendStatus + getBackendStatus, } } From b16bf0fe3f3a06ad8935eaa7d205b83c04d69532 Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Fri, 12 Sep 2025 00:10:38 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E7=9A=84git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 59 +++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 4ab59b5..5e7293c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -19,7 +19,7 @@ # Contact: DLmaster_361@163.com - +import os import re import shutil import asyncio @@ -28,7 +28,6 @@ import sqlite3 import calendar import requests import truststore -from git import Repo from pathlib import Path from fastapi import WebSocket from collections import defaultdict @@ -41,6 +40,16 @@ from app.utils import get_logger logger = get_logger("配置管理") +if (Path.cwd() / "environment/git/bin/git.exe").exists(): + os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = str( + Path.cwd() / "environment/git/bin/git.exe" + ) + +try: + from git import Repo +except ImportError: + Repo = None + class GlobalConfig(ConfigBase): """全局配置""" @@ -602,7 +611,16 @@ class AppConfig(GlobalConfig): self.config_path.mkdir(parents=True, exist_ok=True) self.history_path.mkdir(parents=True, exist_ok=True) - self.repo = Repo(Path.cwd()) + # 初始化Git仓库(如果可用) + try: + if Repo is not None: + self.repo = Repo(Path.cwd()) + else: + self.repo = None + except Exception as e: + logger.warning(f"Git仓库初始化失败: {e}") + self.repo = None + self.server: Optional[uvicorn.Server] = None self.websocket: Optional[WebSocket] = None self.silence_dict: Dict[Path, datetime] = {} @@ -915,24 +933,33 @@ class AppConfig(GlobalConfig): await Config.websocket.send_json(data) async def get_git_version(self) -> tuple[bool, str, str]: + """获取Git版本信息,如果Git不可用则返回默认值""" - # 获取当前 commit - current_commit = self.repo.head.commit + if self.repo is None: + logger.warning("Git仓库不可用,返回默认版本信息") + return False, "unknown", "unknown" - # 获取 commit 哈希 - commit_hash = current_commit.hexsha + try: + # 获取当前 commit + current_commit = self.repo.head.commit - # 获取 commit 时间 - commit_time = datetime.fromtimestamp(current_commit.committed_date) + # 获取 commit 哈希 + commit_hash = current_commit.hexsha - # 检查是否为最新 commit - # 获取远程分支的最新 commit - origin = self.repo.remotes.origin - origin.fetch() # 拉取最新信息 - remote_commit = self.repo.commit(f"origin/{self.repo.active_branch.name}") - is_latest = bool(current_commit.hexsha == remote_commit.hexsha) + # 获取 commit 时间 + commit_time = datetime.fromtimestamp(current_commit.committed_date) - return is_latest, commit_hash, commit_time.strftime("%Y-%m-%d %H:%M:%S") + # 检查是否为最新 commit + # 获取远程分支的最新 commit + origin = self.repo.remotes.origin + origin.fetch() # 拉取最新信息 + remote_commit = self.repo.commit(f"origin/{self.repo.active_branch.name}") + is_latest = bool(current_commit.hexsha == remote_commit.hexsha) + + return is_latest, commit_hash, commit_time.strftime("%Y-%m-%d %H:%M:%S") + except Exception as e: + logger.warning(f"获取Git版本信息失败: {e}") + return False, "error", "error" async def add_script( self, script: Literal["MAA", "General"]