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/app/core/config.py b/app/core/config.py index 1b5894f..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): """全局配置""" @@ -118,20 +127,25 @@ class GlobalConfig(ConfigBase): Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) Data_LastStatisticsUpload = ConfigItem( - "Data", "LastStatisticsUpload", "2000-01-01 00:00:00" + "Data", "LastStatisticsUpload", "2000-01-01 00:00:00", DateTimeValidator() ) Data_LastStageUpdated = ConfigItem( - "Data", "LastStageUpdated", "2000-01-01 00:00:00" + "Data", "LastStageUpdated", "2000-01-01 00:00:00", DateTimeValidator() + ) + Data_StageTimeStamp = ConfigItem( + "Data", "StageTimeStamp", "2000-01-01 00:00:00", DateTimeValidator() ) - Data_StageTimeStamp = ConfigItem("Data", "StageTimeStamp", "2000-01-01 00:00:00") Data_Stage = ConfigItem("Data", "Stage", "{ }") Data_LastNoticeUpdated = ConfigItem( - "Data", "LastNoticeUpdated", "2000-01-01 00:00:00" + "Data", "LastNoticeUpdated", "2000-01-01 00:00:00", DateTimeValidator() ) Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator()) Data_Notice = ConfigItem("Data", "Notice", "{ }") Data_LastWebConfigUpdated = ConfigItem( - "Data", "LastWebConfigUpdated", "2000-01-01 00:00:00" + "Data", "LastWebConfigUpdated", "2000-01-01 00:00:00", DateTimeValidator() + ) + Data_LastCheckVersion = ConfigItem( + "Data", "LastCheckVersion", "2000-01-01 00:00:00", DateTimeValidator() ) Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }") @@ -597,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] = {} @@ -910,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"] diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py index d3770ed..973eaed 100644 --- a/app/models/ConfigBase.py +++ b/app/models/ConfigBase.py @@ -24,6 +24,7 @@ import json import uuid import win32com.client from copy import deepcopy +from datetime import datetime from pathlib import Path from typing import List, Any, Dict, Union, Optional @@ -112,6 +113,27 @@ class UUIDValidator(ConfigValidator): return value if self.validate(value) else str(uuid.uuid4()) +class DateTimeValidator(ConfigValidator): + + def validate(self, value: Any) -> bool: + if not isinstance(value, str): + return False + try: + datetime.strptime(value, "%Y-%m-%d %H:%M:%S") + return True + except ValueError: + return False + + def correct(self, value: Any) -> str: + if not isinstance(value, str): + return "2000-01-01 00:00:00" + try: + datetime.strptime(value, "%Y-%m-%d %H:%M:%S") + return value + except ValueError: + return "2000-01-01 00:00:00" + + class EncryptValidator(ConfigValidator): """加密数据验证器""" diff --git a/app/services/update.py b/app/services/update.py index e4d7b14..51aa877 100644 --- a/app/services/update.py +++ b/app/services/update.py @@ -27,6 +27,7 @@ import zipfile import requests import subprocess from packaging import version +from datetime import datetime, timedelta from typing import List, Dict, Optional from pathlib import Path @@ -49,6 +50,23 @@ class _UpdateHandler: self, current_version: str ) -> tuple[bool, str, Dict[str, List[str]]]: + if datetime.now() - timedelta(hours=4) < datetime.strptime( + Config.get("Data", "LastCheckVersion"), "%Y-%m-%d %H:%M:%S" + ): + logger.info("四小时内已进行过一次检查, 直接使用缓存的版本更新信息") + return ( + ( + False + if self.remote_version is None + else bool( + version.parse(self.remote_version) + > version.parse(current_version) + ) + ), + current_version if self.remote_version is None else self.remote_version, + {}, + ) + logger.info("开始检查更新") response = requests.get( @@ -72,6 +90,9 @@ class _UpdateHandler: ) logger.success("获取版本信息成功") + await Config.set( + "Data", "LastCheckVersion", datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ) remote_version = version_info["data"]["version_name"] self.remote_version = remote_version 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..97173fd 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, Modal, notification } 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 MAX_RESTART_ATTEMPTS = 3 // 最大重启尝试次数 +const RESTART_DELAY = 2000 // 重启延迟 // 类型定义 export type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误' @@ -47,44 +45,12 @@ export interface WebSocketSubscriber { onResult?: (data: ResultMessage) => void onError?: (err: ErrorMessage) => void onNotify?: (n: NotifyMessage) => void - // 兼容旧版 API - onMessage?: (raw: WebSocketBaseMessage) => void - onStatusChange?: (status: WebSocketStatus) => void } -// 兼容旧版 connect(config) 接口 -export interface WebSocketConfig { - taskId: string - mode?: string - showNotifications?: boolean - onProgress?: (data: ProgressMessage) => void - onResult?: (data: ResultMessage) => void - onError?: (err: ErrorMessage | string) => void - onNotify?: (n: NotifyMessage) => void - onMessage?: (raw: WebSocketBaseMessage) => void - onStatusChange?: (status: WebSocketStatus) => void -} +// 后端状态类型 +export type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error' -// 日志工具 -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) -} - -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 +62,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') @@ -110,41 +87,171 @@ 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, - 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 + return (window as any)[WS_STORAGE_KEY] as GlobalWSStorage } // 设置全局状态 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 => { - sub.onStatusChange?.(status) - }) +// 设置后端状态 +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) // 降低检查频率 } // 停止心跳 @@ -153,7 +260,6 @@ const stopGlobalHeartbeat = () => { if (global.heartbeatTimer) { clearInterval(global.heartbeatTimer) global.heartbeatTimer = undefined - wsLog('心跳检测已停止') } } @@ -162,68 +268,62 @@ 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({ - type: 'Signal', - data: pingData - })) - - // 心跳超时检测 - 但不主动断开连接 + ws.send( + JSON.stringify({ + type: 'Signal', + data: { Ping: pingTime, connectionId: global.connectionId }, + }) + ) 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 状态') - } + } catch { + /* ignore */ } - } 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 { + ws.send( + JSON.stringify({ + type: 'Signal', + data: { Pong: raw.data.Ping, connectionId: global.connectionId }, + }) + ) + } catch (e) { + // Pong发送失败,静默处理 + } + } + return + } } const dispatch = (sub: WebSocketSubscriber) => { if (msgType === 'Signal') return - // 兼容旧版:先调用通用 onMessage 回调 - sub.onMessage?.(raw) - if (msgType === 'Progress') return sub.onProgress?.(raw.data as ProgressMessage) if (msgType === 'Result') return sub.onResult?.(raw.data as ResultMessage) if (msgType === 'Error') { @@ -238,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 @@ -250,8 +350,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,177 +357,226 @@ 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}次尝试)`) +// 后端启动后建立连接的公开函数 +export const connectAfterBackendStart = async (): Promise => { + setConnectionPermission(true, '后端启动后连接') - setTimeout(() => { - global.reconnectAttempts++ - createGlobalWebSocket() - }, delay) + try { + const connected = await connectGlobalWebSocket('后端启动后连接') + if (connected) { + startBackendMonitoring() + // 连接完成后禁止新连接 + setConnectionPermission(false, '正常运行中') + return true + } else { + return false + } + } catch (error) { + return false + } } -// 创建 WebSocket 连接 - 移除销毁检查,确保永不放弃连接 +// 创建 WebSocket 连接 const createGlobalWebSocket = (): WebSocket => { const global = getGlobalStorage() // 检查现有连接状态 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 // 重置重连计数 + global.reconnectAttempts = 0 setGlobalStatus('已连接') + startGlobalHeartbeat(ws) - // 发送连接确认 + // 连接成功后禁止新连接 + setConnectionPermission(false, '正常运行中') + + // 发送连接确认和初始pong try { - const connectData = { Connect: true, connectionId: global.connectionId } - wsLog('发送连接确认信号', connectData) - ws.send(JSON.stringify({ - type: 'Signal', - data: connectData - })) - } catch (e) { - wsError('发送连接确认失败', e) + 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) } 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}`) - + ws.onclose = event => { setGlobalStatus('已断开') stopGlobalHeartbeat() global.isConnecting = false - // 永不放弃:立即安排重连 - wsLog('连接断开,安排自动重连以保持永久连接') - scheduleReconnect(global) + // 检查是否是后端自杀导致的关闭 + if (event.code === 1000 && event.reason === 'Ping超时') { + handleBackendFailure().catch(error => { + // 忽略错误,或者可以添加适当的错误处理 + console.warn('handleBackendFailure error:', error) + }) + } 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 模块开始初始化 - 永久连接模式 ===') +// 连接权限控制函数 +const setConnectionPermission = (allow: boolean, reason: string) => { + const global = getGlobalStorage() + global.allowNewConnection = allow + global.connectionReason = reason +} + +const checkConnectionPermission = (): boolean => { + const global = getGlobalStorage() + return global.allowNewConnection +} + +// 只在后端启动/重启时允许创建连接 +const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连'] + +const isValidConnectionReason = (reason: string): boolean => + 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,140 +585,62 @@ 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('消息发送成功') + ws.send(JSON.stringify({ id, type, data })) } catch (e) { - wsError('发送消息失败', e) + // 发送失败,静默处理 } - } else { - wsWarn(`WebSocket 未准备就绪: ${ws ? `状态=${ws.readyState}` : '连接为null'}`) - wsWarn('消息将在连接恢复后可用') } } - const startTaskRaw = (params: any) => { - wsLog('发送启动任务请求', params) - sendRaw('StartTask', params) + const getConnectionInfo = () => ({ + connectionId: global.connectionId, + status: global.status.value, + subscriberCount: global.subscribers.value.size, + moduleLoadCount: global.moduleLoadCount, + wsReadyState: global.wsRef ? global.wsRef.readyState : null, + isConnecting: global.isConnecting, + hasHeartbeat: !!global.heartbeatTimer, + hasEverConnected: global.hasEverConnected, + reconnectAttempts: global.reconnectAttempts, + isPersistentMode: true, // 标识为永久连接模式 + }) + + const restartBackendManually = async () => { + const global = getGlobalStorage() + global.backendRestartAttempts = 0 + return await restartBackend() } - // 移除 destroy 功能,确保连接永不断开 - const forceReconnect = () => { - wsLog('手动触发重连') - if (global.wsRef) { - // 不关闭现有连接,直接尝试创建新连接 - global.isConnecting = false - connectGlobalWebSocket() + const getBackendStatus = () => { + const global = getGlobalStorage() + return { + status: global.backendStatus.value, + restartAttempts: global.backendRestartAttempts, + isRestarting: global.isRestartingBackend, + lastCheck: global.lastBackendCheck, } - return true - } - - const getConnectionInfo = () => { - const info = { - connectionId: global.connectionId, - status: global.status.value, - subscriberCount: global.subscribers.value.size, - moduleLoadCount: global.moduleLoadCount, - wsReadyState: global.wsRef ? global.wsRef.readyState : null, - isConnecting: global.isConnecting, - 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 - } - - // 兼容旧版 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)') } return { - // 兼容 API - connect, - disconnect, - disconnectAll, - // 原有 API & 工具 subscribe, unsubscribe, sendRaw, - startTaskRaw, - forceReconnect, 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/GeneralUserEdit.vue b/frontend/src/views/GeneralUserEdit.vue index bd708bc..9e0cdf6 100644 --- a/frontend/src/views/GeneralUserEdit.vue +++ b/frontend/src/views/GeneralUserEdit.vue @@ -360,7 +360,7 @@ 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) @@ -513,47 +513,28 @@ const loadUserData = async () => { const handleSubmit = async () => { try { await formRef.value?.validate() - - // 确保扁平化字段同步到嵌套数据 formData.Info.Name = formData.userName - - console.log('提交前的表单数据:', { - userName: formData.userName, - InfoName: formData.Info.Name, - isEdit: isEdit.value, - }) - - // 构建提交数据,移除通用脚本不需要的MAA专用字段 - const { IfSendSixStar, ...generalNotify } = formData.Notify - const userData = { Info: { ...formData.Info }, - Notify: generalNotify, + Notify: { ...formData.Notify }, Data: { ...formData.Data }, } - if (isEdit.value) { - // 编辑模式 const result = await updateUser(scriptId, userId, userData) if (result) { message.success('用户更新成功') handleCancel() } } else { - // 添加模式 const result = await addUser(scriptId) if (result) { - // 创建成功后立即更新用户数据 try { const updateResult = await updateUser(scriptId, result.userId, userData) - console.log('用户数据更新结果:', updateResult) - if (updateResult) { message.success('用户创建成功') handleCancel() } else { message.error('用户创建成功,但数据更新失败,请手动编辑用户信息') - // 不跳转,让用户可以重新保存 } } catch (updateError) { console.error('更新用户数据时发生错误:', updateError) @@ -575,35 +556,23 @@ const handleGeneralConfig = async () => { try { generalConfigLoading.value = true - // 如果已有连接,先断开 if (generalWebsocketId.value) { - disconnect(generalWebsocketId.value) + unsubscribe(generalWebsocketId.value) generalWebsocketId.value = null } - // 建立WebSocket连接进行通用配置 - const websocketId = await connect({ - taskId: userId, // 使用用户ID进行配置 - mode: '设置脚本', - showNotifications: true, - onStatusChange: status => { - console.log(`用户 ${formData.userName} 通用配置状态: ${status}`) - }, - onMessage: data => { - console.log(`用户 ${formData.userName} 通用配置消息:`, data) - // 这里可以根据需要处理特定的消息 - }, + const subId = userId + + subscribe(subId, { onError: error => { console.error(`用户 ${formData.userName} 通用配置错误:`, error) message.error(`通用配置连接失败: ${error}`) generalWebsocketId.value = null - }, + } }) - if (websocketId) { - generalWebsocketId.value = websocketId - message.success(`已开始配置用户 ${formData.userName} 的通用设置`) - } + generalWebsocketId.value = subId + message.success(`已开始配置用户 ${formData.userName} 的通用设置`) } catch (error) { console.error('通用配置失败:', error) message.error('通用配置失败') @@ -650,9 +619,8 @@ const selectScriptAfterTask = async () => { } const handleCancel = () => { - // 清理WebSocket连接 if (generalWebsocketId.value) { - disconnect(generalWebsocketId.value) + unsubscribe(generalWebsocketId.value) generalWebsocketId.value = null } router.push('/scripts') @@ -945,8 +913,8 @@ onMounted(() => { } .path-button:disabled { - background: var(--ant-color-bg-container-disabled); - color: var(--ant-color-text-disabled); + background: var(--ant-color-bg-container); + color: var(--ant-color-text-tertiary); cursor: not-allowed; } \ 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/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))) +})