Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor

This commit is contained in:
2025-09-12 00:27:36 +08:00
14 changed files with 686 additions and 526 deletions

View File

@@ -41,13 +41,14 @@ async def connect_websocket(websocket: WebSocket):
await websocket.accept() await websocket.accept()
Config.websocket = websocket Config.websocket = websocket
last_pong = time.monotonic() last_pong = time.monotonic()
last_ping = time.monotonic()
data = {} data = {}
while True: while True:
try: 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", {}): if data.get("type") == "Signal" and "Pong" in data.get("data", {}):
last_pong = time.monotonic() last_pong = time.monotonic()
elif data.get("type") == "Signal" and "Ping" in data.get("data", {}): elif data.get("type") == "Signal" and "Ping" in data.get("data", {}):
@@ -61,7 +62,7 @@ async def connect_websocket(websocket: WebSocket):
except asyncio.TimeoutError: except asyncio.TimeoutError:
if time.monotonic() - last_pong > 15: if last_pong < last_ping:
await websocket.close(code=1000, reason="Ping超时") await websocket.close(code=1000, reason="Ping超时")
break break
await websocket.send_json( await websocket.send_json(
@@ -69,6 +70,7 @@ async def connect_websocket(websocket: WebSocket):
id="Main", type="Signal", data={"Ping": "无描述"} id="Main", type="Signal", data={"Ping": "无描述"}
).model_dump() ).model_dump()
) )
last_ping = time.monotonic()
except WebSocketDisconnect: except WebSocketDisconnect:
break break

View File

@@ -19,7 +19,7 @@
# Contact: DLmaster_361@163.com # Contact: DLmaster_361@163.com
import os
import re import re
import shutil import shutil
import asyncio import asyncio
@@ -28,7 +28,6 @@ import sqlite3
import calendar import calendar
import requests import requests
import truststore import truststore
from git import Repo
from pathlib import Path from pathlib import Path
from fastapi import WebSocket from fastapi import WebSocket
from collections import defaultdict from collections import defaultdict
@@ -41,6 +40,16 @@ from app.utils import get_logger
logger = 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): class GlobalConfig(ConfigBase):
"""全局配置""" """全局配置"""
@@ -118,20 +127,25 @@ class GlobalConfig(ConfigBase):
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
Data_LastStatisticsUpload = ConfigItem( 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 = 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_Stage = ConfigItem("Data", "Stage", "{ }")
Data_LastNoticeUpdated = ConfigItem( 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_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator())
Data_Notice = ConfigItem("Data", "Notice", "{ }") Data_Notice = ConfigItem("Data", "Notice", "{ }")
Data_LastWebConfigUpdated = ConfigItem( 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", "{ }") Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }")
@@ -597,7 +611,16 @@ class AppConfig(GlobalConfig):
self.config_path.mkdir(parents=True, exist_ok=True) self.config_path.mkdir(parents=True, exist_ok=True)
self.history_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.server: Optional[uvicorn.Server] = None
self.websocket: Optional[WebSocket] = None self.websocket: Optional[WebSocket] = None
self.silence_dict: Dict[Path, datetime] = {} self.silence_dict: Dict[Path, datetime] = {}
@@ -910,24 +933,33 @@ class AppConfig(GlobalConfig):
await Config.websocket.send_json(data) await Config.websocket.send_json(data)
async def get_git_version(self) -> tuple[bool, str, str]: async def get_git_version(self) -> tuple[bool, str, str]:
"""获取Git版本信息如果Git不可用则返回默认值"""
# 获取当前 commit if self.repo is None:
current_commit = self.repo.head.commit logger.warning("Git仓库不可用返回默认版本信息")
return False, "unknown", "unknown"
# 获取 commit 哈希 try:
commit_hash = current_commit.hexsha # 获取当前 commit
current_commit = self.repo.head.commit
# 获取 commit 时间 # 获取 commit 哈希
commit_time = datetime.fromtimestamp(current_commit.committed_date) commit_hash = current_commit.hexsha
# 检查是否为最新 commit # 获取 commit 时间
# 获取远程分支的最新 commit commit_time = datetime.fromtimestamp(current_commit.committed_date)
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") # 检查是否为最新 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( async def add_script(
self, script: Literal["MAA", "General"] self, script: Literal["MAA", "General"]

View File

@@ -24,6 +24,7 @@ import json
import uuid import uuid
import win32com.client import win32com.client
from copy import deepcopy from copy import deepcopy
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List, Any, Dict, Union, Optional 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()) 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): class EncryptValidator(ConfigValidator):
"""加密数据验证器""" """加密数据验证器"""

View File

@@ -27,6 +27,7 @@ import zipfile
import requests import requests
import subprocess import subprocess
from packaging import version from packaging import version
from datetime import datetime, timedelta
from typing import List, Dict, Optional from typing import List, Dict, Optional
from pathlib import Path from pathlib import Path
@@ -49,6 +50,23 @@ class _UpdateHandler:
self, current_version: str self, current_version: str
) -> tuple[bool, str, Dict[str, List[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("开始检查更新") logger.info("开始检查更新")
response = requests.get( response = requests.get(
@@ -72,6 +90,9 @@ class _UpdateHandler:
) )
logger.success("获取版本信息成功") logger.success("获取版本信息成功")
await Config.set(
"Data", "LastCheckVersion", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
remote_version = version_info["data"]["version_name"] remote_version = version_info["data"]["version_name"]
self.remote_version = remote_version self.remote_version = remote_version

View File

@@ -537,6 +537,10 @@ ipcMain.handle('start-backend', async () => {
return startBackend(appRoot) return startBackend(appRoot)
}) })
ipcMain.handle('stop-backend', async () => {
return stopBackend()
})
// Git相关 // Git相关
ipcMain.handle('download-git', async () => { ipcMain.handle('download-git', async () => {
const appRoot = getAppRoot() const appRoot = getAppRoot()

View File

@@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
cloneBackend: (repoUrl?: string) => ipcRenderer.invoke('clone-backend', repoUrl), cloneBackend: (repoUrl?: string) => ipcRenderer.invoke('clone-backend', repoUrl),
updateBackend: (repoUrl?: string) => ipcRenderer.invoke('update-backend', repoUrl), updateBackend: (repoUrl?: string) => ipcRenderer.invoke('update-backend', repoUrl),
startBackend: () => ipcRenderer.invoke('start-backend'), startBackend: () => ipcRenderer.invoke('start-backend'),
stopBackend: () => ipcRenderer.invoke('stop-backend'),
// 管理员权限相关 // 管理员权限相关
checkAdmin: () => ipcRenderer.invoke('check-admin'), checkAdmin: () => ipcRenderer.invoke('check-admin'),

View File

@@ -42,6 +42,7 @@ import { ref, onMounted } from 'vue'
import { getConfig } from '@/utils/config' import { getConfig } from '@/utils/config'
import { getMirrorUrl } from '@/config/mirrors' import { getMirrorUrl } from '@/config/mirrors'
import router from '@/router' import router from '@/router'
import { connectAfterBackendStart } from '@/composables/useWebSocket'
@@ -185,6 +186,15 @@ async function startBackendService() {
if (!result.success) { if (!result.success) {
throw new Error(`后端服务启动失败: ${result.error}`) throw new Error(`后端服务启动失败: ${result.error}`)
} }
// 后端启动成功建立WebSocket连接
console.log('后端启动成功正在建立WebSocket连接...')
const wsConnected = await connectAfterBackendStart()
if (!wsConnected) {
console.warn('WebSocket连接建立失败但继续进入应用')
} else {
console.log('WebSocket连接建立成功')
}
} }
// 组件挂载时开始自动流程 // 组件挂载时开始自动流程

View File

@@ -7,7 +7,34 @@
> >
<!-- <div class="header-actions">--> <!-- <div class="header-actions">-->
<!-- <a-button size="large" type="primary" @click="handleSkipToHome">--> <!-- <a-button size="large" type="prim try {
const result = await window.electronAPI.startBackend()
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'
console.log('后端服务启动成功延迟1秒后自动进入主页')
// 延迟1秒后自动进入主页
setTimeout(() => {
handleEnterApp()
}, 1000)
} else {ndleSkipToHome">-->
<!-- 跳转至首页仅开发用--> <!-- 跳转至首页仅开发用-->
<!-- </a-button>--> <!-- </a-button>-->
<!-- <a-button--> <!-- <a-button-->
@@ -119,6 +146,7 @@ import GitStep from './GitStep.vue'
import BackendStep from './BackendStep.vue' import BackendStep from './BackendStep.vue'
import DependenciesStep from './DependenciesStep.vue' import DependenciesStep from './DependenciesStep.vue'
import ServiceStep from './ServiceStep.vue' import ServiceStep from './ServiceStep.vue'
import { connectAfterBackendStart } from '@/composables/useWebSocket'
@@ -408,6 +436,19 @@ async function startBackendService() {
if (result.success) { if (result.success) {
if (serviceStepRef.value) { if (serviceStepRef.value) {
serviceStepRef.value.serviceProgress = 100 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 = '后端服务启动成功,即将进入主页...' serviceStepRef.value.serviceStatus = '后端服务启动成功,即将进入主页...'
} }
stepStatus.value = 'finish' stepStatus.value = 'finish'

View File

@@ -1,15 +1,13 @@
import { ref, type Ref } from 'vue' import { ref, type Ref } from 'vue'
import { message, notification } from 'ant-design-vue' import { message, Modal, notification } from 'ant-design-vue'
// WebSocket 调试开关
const WS_DEV = true
const WS_VERSION = 'v2.5-PERSISTENT-' + Date.now()
console.log(`🚀 WebSocket 模块已加载: ${WS_VERSION} - 永久连接模式`)
// 基础配置 // 基础配置
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws' const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
const HEARTBEAT_INTERVAL = 15000 const HEARTBEAT_INTERVAL = 15000
const HEARTBEAT_TIMEOUT = 5000 const HEARTBEAT_TIMEOUT = 5000
const BACKEND_CHECK_INTERVAL = 3000 // 后端检查间隔
const MAX_RESTART_ATTEMPTS = 3 // 最大重启尝试次数
const RESTART_DELAY = 2000 // 重启延迟
// 类型定义 // 类型定义
export type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误' export type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误'
@@ -47,44 +45,12 @@ export interface WebSocketSubscriber {
onResult?: (data: ResultMessage) => void onResult?: (data: ResultMessage) => void
onError?: (err: ErrorMessage) => void onError?: (err: ErrorMessage) => void
onNotify?: (n: NotifyMessage) => void onNotify?: (n: NotifyMessage) => void
// 兼容旧版 API
onMessage?: (raw: WebSocketBaseMessage) => void
onStatusChange?: (status: WebSocketStatus) => void
} }
// 兼容旧版 connect(config) 接口 // 后端状态类型
export interface WebSocketConfig { export type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
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
}
// 日志工具 // 全局存储接口 - 添加后端管理和连接控制
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 { interface GlobalWSStorage {
wsRef: WebSocket | null wsRef: WebSocket | null
status: Ref<WebSocketStatus> status: Ref<WebSocketStatus>
@@ -96,7 +62,18 @@ interface GlobalWSStorage {
moduleLoadCount: number moduleLoadCount: number
createdAt: number createdAt: number
hasEverConnected: boolean hasEverConnected: boolean
reconnectAttempts: number // 新增:重连尝试次数 reconnectAttempts: number
// 新增:后端管理
backendStatus: Ref<BackendStatus>
backendCheckTimer?: number
backendRestartAttempts: number
isRestartingBackend: boolean
lastBackendCheck: number
// 新增:连接保护
lastConnectAttempt: number
// 新增:连接权限控制
allowNewConnection: boolean
connectionReason: string
} }
const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT') const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')
@@ -110,41 +87,171 @@ const initGlobalStorage = (): GlobalWSStorage => {
heartbeatTimer: undefined, heartbeatTimer: undefined,
isConnecting: false, isConnecting: false,
lastPingTime: 0, lastPingTime: 0,
connectionId: Math.random().toString(36).substr(2, 9), connectionId: Math.random().toString(36).substring(2, 9),
moduleLoadCount: 0, moduleLoadCount: 0,
createdAt: Date.now(), createdAt: Date.now(),
hasEverConnected: false, hasEverConnected: false,
reconnectAttempts: 0 reconnectAttempts: 0,
// 后端管理
backendStatus: ref<BackendStatus>('unknown'),
backendCheckTimer: undefined,
backendRestartAttempts: 0,
isRestartingBackend: false,
lastBackendCheck: 0,
// 连接保护
lastConnectAttempt: 0,
// 连接权限控制
allowNewConnection: true, // 初始化时允许创建连接
connectionReason: '系统初始化',
} }
} }
// 获取全局存储 // 获取全局存储
const getGlobalStorage = (): GlobalWSStorage => { const getGlobalStorage = (): GlobalWSStorage => {
if (!(window as any)[WS_STORAGE_KEY]) { if (!(window as any)[WS_STORAGE_KEY]) {
wsLog('首次初始化全局 WebSocket 存储 - 永久连接模式')
;(window as any)[WS_STORAGE_KEY] = initGlobalStorage() ;(window as any)[WS_STORAGE_KEY] = initGlobalStorage()
} }
const storage = (window as any)[WS_STORAGE_KEY] as GlobalWSStorage return (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 setGlobalStatus = (status: WebSocketStatus) => {
const global = getGlobalStorage() const global = getGlobalStorage()
const oldStatus = global.status.value
global.status.value = status global.status.value = status
wsLog(`状态变更: ${oldStatus} -> ${status} [连接ID: ${global.connectionId}]`) }
// 广播状态变化给所有订阅者(兼容 onStatusChange // 设置后端状态
global.subscribers.value.forEach(sub => { const setBackendStatus = (status: BackendStatus) => {
sub.onStatusChange?.(status) 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<boolean> => {
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) { if (global.heartbeatTimer) {
clearInterval(global.heartbeatTimer) clearInterval(global.heartbeatTimer)
global.heartbeatTimer = undefined global.heartbeatTimer = undefined
wsLog('心跳检测已停止')
} }
} }
@@ -162,68 +268,62 @@ const startGlobalHeartbeat = (ws: WebSocket) => {
const global = getGlobalStorage() const global = getGlobalStorage()
stopGlobalHeartbeat() stopGlobalHeartbeat()
wsLog('启动心跳检测间隔15秒')
global.heartbeatTimer = window.setInterval(() => { global.heartbeatTimer = window.setInterval(() => {
wsLog(`心跳检测 - WebSocket状态: ${ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`)
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
try { try {
const pingTime = Date.now() const pingTime = Date.now()
global.lastPingTime = pingTime global.lastPingTime = pingTime
const pingData = { Ping: pingTime, connectionId: global.connectionId } ws.send(
JSON.stringify({
wsLog('发送心跳ping', pingData) type: 'Signal',
ws.send(JSON.stringify({ data: { Ping: pingTime, connectionId: global.connectionId },
type: 'Signal', })
data: pingData )
}))
// 心跳超时检测 - 但不主动断开连接
setTimeout(() => { setTimeout(() => {
if (global.lastPingTime === pingTime && ws.readyState === WebSocket.OPEN) { /* 心跳超时不主动断开 */
wsWarn(`心跳超时 - 发送时间: ${pingTime}, 当前lastPingTime: ${global.lastPingTime}, 连接状态: ${ws.readyState}`)
wsWarn('心跳超时但保持连接,等待网络层或服务端处理')
}
}, HEARTBEAT_TIMEOUT) }, HEARTBEAT_TIMEOUT)
} catch {
} catch (e) { /* ignore */
wsError('心跳发送失败', e)
if (ws.readyState !== WebSocket.OPEN) {
wsWarn('心跳发送失败,当前连接已不再是 OPEN 状态')
}
} }
} else {
wsWarn(`心跳检测时连接状态异常: ${ws.readyState},但不主动断开连接`)
} }
}, HEARTBEAT_INTERVAL) }, HEARTBEAT_INTERVAL)
} }
// 处理消息
const handleMessage = (raw: WebSocketBaseMessage) => { const handleMessage = (raw: WebSocketBaseMessage) => {
const global = getGlobalStorage() const global = getGlobalStorage()
const msgType = String(raw.type) const msgType = String(raw.type)
const id = raw.id const id = raw.id
// 处理心跳响应 // 优先处理Signal类型的ping-pong消息不受id限制
if (msgType === 'Signal' && raw.data && raw.data.Pong) { if (msgType === 'Signal') {
const pongTime = raw.data.Pong // 处理心跳响应
const latency = Date.now() - pongTime if (raw.data && raw.data.Pong) {
wsLog(`收到心跳pong响应延迟: ${latency}ms`) global.lastPingTime = 0 // 重置ping时间表示收到了响应
global.lastPingTime = 0 // 重置ping时间表示收到了响应 return
return }
}
// 记录其他类型的消息 // 处理后端发送的Ping回复Pong
if (msgType !== 'Signal') { if (raw.data && raw.data.Ping) {
wsLog(`收到消息: type=${msgType}, id=${id || 'broadcast'}`) 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) => { const dispatch = (sub: WebSocketSubscriber) => {
if (msgType === 'Signal') return if (msgType === 'Signal') return
// 兼容旧版:先调用通用 onMessage 回调
sub.onMessage?.(raw)
if (msgType === 'Progress') return sub.onProgress?.(raw.data as ProgressMessage) if (msgType === 'Progress') return sub.onProgress?.(raw.data as ProgressMessage)
if (msgType === 'Result') return sub.onResult?.(raw.data as ResultMessage) if (msgType === 'Result') return sub.onResult?.(raw.data as ResultMessage)
if (msgType === 'Error') { if (msgType === 'Error') {
@@ -238,7 +338,7 @@ const handleMessage = (raw: WebSocketBaseMessage) => {
if (raw.data && (raw.data as NotifyMessage).title) { if (raw.data && (raw.data as NotifyMessage).title) {
notification.info({ notification.info({
message: (raw.data as NotifyMessage).title, message: (raw.data as NotifyMessage).title,
description: (raw.data as NotifyMessage).content description: (raw.data as NotifyMessage).content,
}) })
} }
return return
@@ -250,8 +350,6 @@ const handleMessage = (raw: WebSocketBaseMessage) => {
const sub = global.subscribers.value.get(id) const sub = global.subscribers.value.get(id)
if (sub) { if (sub) {
dispatch(sub) dispatch(sub)
} else {
wsWarn(`未找到 ws_id=${id} 的订阅者, type=${msgType}`)
} }
} else { } else {
// 无 id 的消息广播给所有订阅者 // 无 id 的消息广播给所有订阅者
@@ -259,177 +357,226 @@ const handleMessage = (raw: WebSocketBaseMessage) => {
} }
} }
// 延迟重连函数 // 后端启动后建立连接的公开函数
const scheduleReconnect = (global: GlobalWSStorage) => { export const connectAfterBackendStart = async (): Promise<boolean> => {
const delay = Math.min(1000 * Math.pow(2, global.reconnectAttempts), 30000) // 最大30秒 setConnectionPermission(true, '后端启动后连接')
wsLog(`计划在 ${delay}ms 后重连 (第${global.reconnectAttempts + 1}次尝试)`)
setTimeout(() => { try {
global.reconnectAttempts++ const connected = await connectGlobalWebSocket('后端启动后连接')
createGlobalWebSocket() if (connected) {
}, delay) startBackendMonitoring()
// 连接完成后禁止新连接
setConnectionPermission(false, '正常运行中')
return true
} else {
return false
}
} catch (error) {
return false
}
} }
// 创建 WebSocket 连接 - 移除销毁检查,确保永不放弃连接 // 创建 WebSocket 连接
const createGlobalWebSocket = (): WebSocket => { const createGlobalWebSocket = (): WebSocket => {
const global = getGlobalStorage() const global = getGlobalStorage()
// 检查现有连接状态 // 检查现有连接状态
if (global.wsRef) { if (global.wsRef) {
wsLog(`检查现有连接状态: ${global.wsRef.readyState}`)
if (global.wsRef.readyState === WebSocket.OPEN) { if (global.wsRef.readyState === WebSocket.OPEN) {
wsLog('检测到已有活跃连接,直接返回现有连接')
return global.wsRef return global.wsRef
} }
if (global.wsRef.readyState === WebSocket.CONNECTING) { if (global.wsRef.readyState === WebSocket.CONNECTING) {
wsLog('检测到正在连接的 WebSocket返回现有连接实例')
return global.wsRef return global.wsRef
} }
wsLog('现有连接状态为 CLOSING 或 CLOSED将创建新连接')
} }
wsLog(`开始创建新的 WebSocket 连接到: ${BASE_WS_URL}`)
const ws = new WebSocket(BASE_WS_URL) const ws = new WebSocket(BASE_WS_URL)
// 记录连接创建
wsLog(`WebSocket 实例已创建 [连接ID: ${global.connectionId}]`)
ws.onopen = () => { ws.onopen = () => {
wsLog(`WebSocket 连接已建立 [连接ID: ${global.connectionId}]`)
global.isConnecting = false global.isConnecting = false
global.hasEverConnected = true global.hasEverConnected = true
global.reconnectAttempts = 0 // 重置重连计数 global.reconnectAttempts = 0
setGlobalStatus('已连接') setGlobalStatus('已连接')
startGlobalHeartbeat(ws) startGlobalHeartbeat(ws)
// 发送连接确认 // 连接成功后禁止新连接
setConnectionPermission(false, '正常运行中')
// 发送连接确认和初始pong
try { try {
const connectData = { Connect: true, connectionId: global.connectionId } ws.send(
wsLog('发送连接确认信号', connectData) JSON.stringify({
ws.send(JSON.stringify({ type: 'Signal',
type: 'Signal', data: { Connect: true, connectionId: global.connectionId },
data: connectData })
})) )
} catch (e) { ws.send(
wsError('发送连接确认失败', e) JSON.stringify({
type: 'Signal',
data: { Pong: Date.now(), connectionId: global.connectionId },
})
)
} catch {
/* ignore */
} }
} }
ws.onmessage = (ev) => { ws.onmessage = ev => {
try { try {
const raw = JSON.parse(ev.data) as WebSocketBaseMessage const raw = JSON.parse(ev.data) as WebSocketBaseMessage
handleMessage(raw) handleMessage(raw)
} catch (e) { } catch (e) {
wsError('解析 WebSocket 消息失败', e, '原始数据:', ev.data) // 消息解析失败,静默处理
} }
} }
ws.onerror = (event) => { ws.onerror = () => {
wsError(`WebSocket 连接错误 [连接ID: ${global.connectionId}]`, event)
wsError(`错误发生时连接状态: ${ws.readyState}`)
setGlobalStatus('连接错误') setGlobalStatus('连接错误')
} }
ws.onclose = (event) => { 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('已断开') setGlobalStatus('已断开')
stopGlobalHeartbeat() stopGlobalHeartbeat()
global.isConnecting = false global.isConnecting = false
// 永不放弃:立即安排重连 // 检查是否是后端自杀导致的关闭
wsLog('连接断开,安排自动重连以保持永久连接') if (event.code === 1000 && event.reason === 'Ping超时') {
scheduleReconnect(global) handleBackendFailure().catch(error => {
// 忽略错误,或者可以添加适当的错误处理
console.warn('handleBackendFailure error:', error)
})
} else {
// 连接断开,不自动重连,等待后端重启
setGlobalStatus('已断开')
}
} }
// 为新创建的 WebSocket 设置引用 // 为新创建的 WebSocket 设置引用
global.wsRef = ws global.wsRef = ws
wsLog(`WebSocket 引用已设置到全局存储`)
return ws return ws
} }
// 连接全局 WebSocket - 简化逻辑,移除销毁检查 // 连接全局 WebSocket
const connectGlobalWebSocket = async (): Promise<boolean> => { const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promise<boolean> => {
const global = getGlobalStorage() const global = getGlobalStorage()
// 详细检查连接状态 // 首先检查连接权限
if (global.wsRef) { if (!checkConnectionPermission()) {
wsLog(`检查现有连接: readyState=${global.wsRef.readyState}, isConnecting=${global.isConnecting}`) return false
if (global.wsRef.readyState === WebSocket.OPEN) {
wsLog('WebSocket 已连接,直接返回')
return true
}
if (global.wsRef.readyState === WebSocket.CONNECTING) {
wsLog('WebSocket 正在连接中')
return true
}
} }
if (global.isConnecting) { // 验证连接原因是否合法
wsLog('全局连接标志显示正在连接中,等待连接完成') if (!isValidConnectionReason(reason)) {
return true return false
}
// 尝试获取全局连接锁
if (!acquireConnectionLock()) {
return false
} }
try { 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.isConnecting = true
global.lastConnectAttempt = now
// 清理旧连接引用(如果存在且已关闭)
if (global.wsRef && global.wsRef.readyState === WebSocket.CLOSED) {
global.wsRef = null
}
global.wsRef = createGlobalWebSocket() global.wsRef = createGlobalWebSocket()
setGlobalStatus('连接中') setGlobalStatus('连接中')
wsLog('WebSocket 连接流程已启动')
return true return true
} catch (e) { } catch (e) {
wsError('创建 WebSocket 失败', e)
setGlobalStatus('连接错误') setGlobalStatus('连接错误')
global.isConnecting = false global.isConnecting = false
// 即使创建失败也要安排重连
scheduleReconnect(global)
return false 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() const global = getGlobalStorage()
if (global.moduleLoadCount > 1) { // 只在模块真正加载时计数一次
wsLog(`检测到模块热更新重载 (第${global.moduleLoadCount}次)`) if (global.moduleLoadCount === 0) {
wsLog(`当前连接状态: ${global.wsRef ? global.wsRef.readyState : 'null'}`) global.moduleLoadCount = 1
wsLog('保持现有连接,不重新建立连接')
} else {
wsLog('首次加载模块,建立永久 WebSocket 连接')
connectGlobalWebSocket()
} }
// 页面卸载时不关闭连接,保持永久连接 // 页面卸载时不关闭连接,保持永久连接
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
wsLog('页面即将卸载,但保持 WebSocket 连接') // 保持连接
}) })
// 主要 Hook 函数 // 主要 Hook 函数
@@ -438,140 +585,62 @@ export function useWebSocket() {
const subscribe = (id: string, handlers: Omit<WebSocketSubscriber, 'id'>) => { const subscribe = (id: string, handlers: Omit<WebSocketSubscriber, 'id'>) => {
global.subscribers.value.set(id, { id, ...handlers }) global.subscribers.value.set(id, { id, ...handlers })
wsLog(`添加订阅者: ${id},当前订阅者总数: ${global.subscribers.value.size}`)
} }
const unsubscribe = (id: string) => { const unsubscribe = (id: string) => {
const existed = global.subscribers.value.delete(id) global.subscribers.value.delete(id)
wsLog(`移除订阅者: ${id},是否存在: ${existed},剩余订阅者: ${global.subscribers.value.size}`)
} }
const sendRaw = (type: string, data?: any, id?: string) => { const sendRaw = (type: string, data?: any, id?: string) => {
const ws = global.wsRef const ws = global.wsRef
wsLog(`尝试发送消息: type=${type}, id=${id || 'broadcast'}`)
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
try { try {
const messageData = { id, type, data } ws.send(JSON.stringify({ id, type, data }))
ws.send(JSON.stringify(messageData))
wsLog('消息发送成功')
} catch (e) { } catch (e) {
wsError('发送消息失败', e) // 发送失败,静默处理
} }
} else {
wsWarn(`WebSocket 未准备就绪: ${ws ? `状态=${ws.readyState}` : '连接为null'}`)
wsWarn('消息将在连接恢复后可用')
} }
} }
const startTaskRaw = (params: any) => { const getConnectionInfo = () => ({
wsLog('发送启动任务请求', params) connectionId: global.connectionId,
sendRaw('StartTask', params) 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 getBackendStatus = () => {
const forceReconnect = () => { const global = getGlobalStorage()
wsLog('手动触发重连') return {
if (global.wsRef) { status: global.backendStatus.value,
// 不关闭现有连接,直接尝试创建新连接 restartAttempts: global.backendRestartAttempts,
global.isConnecting = false isRestarting: global.isRestartingBackend,
connectGlobalWebSocket() 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
}
// 兼容旧版 APIconnect 重载
async function connect(): Promise<boolean>
async function connect(config: WebSocketConfig): Promise<string | null>
async function connect(config?: WebSocketConfig): Promise<boolean | string | null> {
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
}
// 兼容旧版 APIdisconnect / 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 { return {
// 兼容 API
connect,
disconnect,
disconnectAll,
// 原有 API & 工具
subscribe, subscribe,
unsubscribe, unsubscribe,
sendRaw, sendRaw,
startTaskRaw,
forceReconnect,
getConnectionInfo, getConnectionInfo,
status: global.status, status: global.status,
subscribers: global.subscribers subscribers: global.subscribers,
backendStatus: global.backendStatus,
restartBackend: restartBackendManually,
getBackendStatus,
} }
} }

View File

@@ -18,7 +18,8 @@ export interface ElectronAPI {
installDependencies: (mirror?: string) => Promise<any> installDependencies: (mirror?: string) => Promise<any>
cloneBackend: (repoUrl?: string) => Promise<any> cloneBackend: (repoUrl?: string) => Promise<any>
updateBackend: (repoUrl?: string) => Promise<any> updateBackend: (repoUrl?: string) => Promise<any>
startBackend: () => Promise<any> startBackend: () => Promise<{ success: boolean; error?: string }>
stopBackend?: () => Promise<{ success: boolean; error?: string }>
// 管理员权限相关 // 管理员权限相关
checkAdmin: () => Promise<boolean> checkAdmin: () => Promise<boolean>

View File

@@ -360,7 +360,7 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi() const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi()
const { getScript } = useScriptApi() const { getScript } = useScriptApi()
const { connect, disconnect } = useWebSocket() const { subscribe, unsubscribe } = useWebSocket()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const loading = computed(() => userLoading.value) const loading = computed(() => userLoading.value)
@@ -513,47 +513,28 @@ const loadUserData = async () => {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await formRef.value?.validate() await formRef.value?.validate()
// 确保扁平化字段同步到嵌套数据
formData.Info.Name = formData.userName 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 = { const userData = {
Info: { ...formData.Info }, Info: { ...formData.Info },
Notify: generalNotify, Notify: { ...formData.Notify },
Data: { ...formData.Data }, Data: { ...formData.Data },
} }
if (isEdit.value) { if (isEdit.value) {
// 编辑模式
const result = await updateUser(scriptId, userId, userData) const result = await updateUser(scriptId, userId, userData)
if (result) { if (result) {
message.success('用户更新成功') message.success('用户更新成功')
handleCancel() handleCancel()
} }
} else { } else {
// 添加模式
const result = await addUser(scriptId) const result = await addUser(scriptId)
if (result) { if (result) {
// 创建成功后立即更新用户数据
try { try {
const updateResult = await updateUser(scriptId, result.userId, userData) const updateResult = await updateUser(scriptId, result.userId, userData)
console.log('用户数据更新结果:', updateResult)
if (updateResult) { if (updateResult) {
message.success('用户创建成功') message.success('用户创建成功')
handleCancel() handleCancel()
} else { } else {
message.error('用户创建成功,但数据更新失败,请手动编辑用户信息') message.error('用户创建成功,但数据更新失败,请手动编辑用户信息')
// 不跳转,让用户可以重新保存
} }
} catch (updateError) { } catch (updateError) {
console.error('更新用户数据时发生错误:', updateError) console.error('更新用户数据时发生错误:', updateError)
@@ -575,35 +556,23 @@ const handleGeneralConfig = async () => {
try { try {
generalConfigLoading.value = true generalConfigLoading.value = true
// 如果已有连接,先断开
if (generalWebsocketId.value) { if (generalWebsocketId.value) {
disconnect(generalWebsocketId.value) unsubscribe(generalWebsocketId.value)
generalWebsocketId.value = null generalWebsocketId.value = null
} }
// 建立WebSocket连接进行通用配置 const subId = userId
const websocketId = await connect({
taskId: userId, // 使用用户ID进行配置 subscribe(subId, {
mode: '设置脚本',
showNotifications: true,
onStatusChange: status => {
console.log(`用户 ${formData.userName} 通用配置状态: ${status}`)
},
onMessage: data => {
console.log(`用户 ${formData.userName} 通用配置消息:`, data)
// 这里可以根据需要处理特定的消息
},
onError: error => { onError: error => {
console.error(`用户 ${formData.userName} 通用配置错误:`, error) console.error(`用户 ${formData.userName} 通用配置错误:`, error)
message.error(`通用配置连接失败: ${error}`) message.error(`通用配置连接失败: ${error}`)
generalWebsocketId.value = null generalWebsocketId.value = null
}, }
}) })
if (websocketId) { generalWebsocketId.value = subId
generalWebsocketId.value = websocketId message.success(`已开始配置用户 ${formData.userName} 的通用设置`)
message.success(`已开始配置用户 ${formData.userName} 的通用设置`)
}
} catch (error) { } catch (error) {
console.error('通用配置失败:', error) console.error('通用配置失败:', error)
message.error('通用配置失败') message.error('通用配置失败')
@@ -650,9 +619,8 @@ const selectScriptAfterTask = async () => {
} }
const handleCancel = () => { const handleCancel = () => {
// 清理WebSocket连接
if (generalWebsocketId.value) { if (generalWebsocketId.value) {
disconnect(generalWebsocketId.value) unsubscribe(generalWebsocketId.value)
generalWebsocketId.value = null generalWebsocketId.value = null
} }
router.push('/scripts') router.push('/scripts')
@@ -945,8 +913,8 @@ onMounted(() => {
} }
.path-button:disabled { .path-button:disabled {
background: var(--ant-color-bg-container-disabled); background: var(--ant-color-bg-container);
color: var(--ant-color-text-disabled); color: var(--ant-color-text-tertiary);
cursor: not-allowed; cursor: not-allowed;
} }
</style> </style>

View File

@@ -898,12 +898,14 @@ import { useUserApi } from '@/composables/useUserApi'
import { useScriptApi } from '@/composables/useScriptApi' import { useScriptApi } from '@/composables/useScriptApi'
import { useWebSocket } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
import { Service } from '@/api' import { Service } from '@/api'
import { GetStageIn } from '@/api/models/GetStageIn'
import { defineComponent } from 'vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi() const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi()
const { getScript } = useScriptApi() const { getScript } = useScriptApi()
const { connect, disconnect } = useWebSocket() const { subscribe, unsubscribe } = useWebSocket()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const loading = computed(() => userLoading.value) const loading = computed(() => userLoading.value)
@@ -1129,19 +1131,17 @@ const loadUserData = async () => {
const loadStageOptions = async () => { const loadStageOptions = async () => {
try { try {
const response = await Service.getStageComboxApiInfoComboxStagePost({ const response = await Service.getStageComboxApiInfoComboxStagePost({
type: 'Today', type: GetStageIn.type.TODAY
}) })
if (response && response.code === 200 && response.data) { 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 (a.value === '-') return -1
if (b.value === '-') return 1 if (b.value === '-') return 1
return 0 return 0
}) })
stageOptions.value = sorted
} }
} catch (error) { } catch (error) {
console.error('加载关卡选项失败:', 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 () => { const selectInfrastructureConfig = async () => {
try { try {
const path = await window.electronAPI?.selectFile([ const path = await (window as any).electronAPI?.selectFile([
{ name: 'JSON 文件', extensions: ['json'] }, { name: 'JSON 文件', extensions: ['json'] },
{ name: '所有文件', extensions: ['*'] }, { name: '所有文件', extensions: ['*'] },
]) ])
if (path && path.length > 0) { if (path && path.length > 0) {
infrastructureConfigPath.value = path infrastructureConfigPath.value = path
formData.Info.InfrastPath = path[0] formData.Info.InfrastPath = path[0]
@@ -1182,28 +1189,22 @@ const importInfrastructureConfig = async () => {
message.warning('请先选择配置文件') message.warning('请先选择配置文件')
return return
} }
if (!isEdit.value) { if (!isEdit.value) {
message.warning('请先保存用户后再导入配置') message.warning('请先保存用户后再导入配置')
return return
} }
try { try {
infrastructureImporting.value = true infrastructureImporting.value = true
// 调用API导入基建配置
const result = await Service.importInfrastructureApiScriptsUserInfrastructurePost({ const result = await Service.importInfrastructureApiScriptsUserInfrastructurePost({
scriptId: scriptId, scriptId: scriptId,
userId: userId, userId: userId,
jsonFile: infrastructureConfigPath.value[0], jsonFile: infrastructureConfigPath.value[0],
}) })
if (result && result.code === 200) { if (result && result.code === 200) {
message.success('基建配置导入成功') message.success('基建配置导入成功')
// 清空文件路径
infrastructureConfigPath.value = '' infrastructureConfigPath.value = ''
} else { } else {
message.error(result?.msg || '基建配置导入失败') message.error('基建配置导入失败')
} }
} catch (error) { } catch (error) {
console.error('基建配置导入失败:', error) console.error('基建配置导入失败:', error)
@@ -1285,33 +1286,22 @@ const handleMAAConfig = async () => {
// 如果已有连接,先断开 // 如果已有连接,先断开
if (maaWebsocketId.value) { if (maaWebsocketId.value) {
disconnect(maaWebsocketId.value) unsubscribe(maaWebsocketId.value)
maaWebsocketId.value = null maaWebsocketId.value = null
} }
// 建立WebSocket连接进行MAA配置 // 直接订阅(旧 connect 参数移除)
const websocketId = await connect({ const subId = userId
taskId: userId, // 使用用户ID进行配置 subscribe(subId, {
mode: '设置脚本',
showNotifications: true,
onStatusChange: status => {
console.log(`用户 ${formData.userName} MAA配置状态: ${status}`)
},
onMessage: data => {
console.log(`用户 ${formData.userName} MAA配置消息:`, data)
// 这里可以根据需要处理特定的消息
},
onError: error => { onError: error => {
console.error(`用户 ${formData.userName} MAA配置错误:`, error) console.error(`用户 ${formData.userName} MAA配置错误:`, error)
message.error(`MAA配置连接失败: ${error}`) message.error(`MAA配置连接失败: ${error}`)
maaWebsocketId.value = null maaWebsocketId.value = null
}, }
}) })
if (websocketId) { maaWebsocketId.value = subId
maaWebsocketId.value = websocketId message.success(`已开始配置用户 ${formData.userName} 的MAA设置`)
message.success(`已开始配置用户 ${formData.userName} 的MAA设置`)
}
} catch (error) { } catch (error) {
console.error('MAA配置失败:', error) console.error('MAA配置失败:', error)
message.error('MAA配置失败') message.error('MAA配置失败')
@@ -1335,17 +1325,12 @@ const stage3InputRef = ref()
const stageRemainInputRef = ref() const stageRemainInputRef = ref()
// VNodes 组件,用于渲染下拉菜单内容 // VNodes 组件,用于渲染下拉菜单内容
const VNodes = { const VNodes = defineComponent({
props: { props: { vnodes: { type: Object, required: true } },
vnodes: { setup(props) {
type: Object, return () => props.vnodes as any
required: true, }
}, })
},
render() {
return this.vnodes
},
}
// 验证关卡名称格式 // 验证关卡名称格式
const validateStageName = (stageName: string): boolean => { const validateStageName = (stageName: string): boolean => {
@@ -1465,9 +1450,8 @@ const addCustomStageRemain = () => {
} }
const handleCancel = () => { const handleCancel = () => {
// 清理WebSocket连接
if (maaWebsocketId.value) { if (maaWebsocketId.value) {
disconnect(maaWebsocketId.value) unsubscribe(maaWebsocketId.value)
maaWebsocketId.value = null maaWebsocketId.value = null
} }
router.push('/scripts') router.push('/scripts')

View File

@@ -206,7 +206,7 @@ import { PlayCircleOutlined, StopOutlined } from '@ant-design/icons-vue'
import { Service } from '@/api/services/Service' import { Service } from '@/api/services/Service'
import type { ComboBoxItem } from '@/api/models/ComboBoxItem' import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
import { TaskCreateIn } from '@/api/models/TaskCreateIn' import { TaskCreateIn } from '@/api/models/TaskCreateIn'
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
// 类型定义 // 类型定义
interface RunningTask { 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 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 }) const quickTaskForm = reactive<{ taskId: string | null; mode: TaskCreateIn.mode }>({ taskId: null, mode: TaskCreateIn.mode.AutoMode })
// WebSocket API // WebSocket API - 更新为新的订阅机制
const { connect: wsConnect, disconnect: wsDisconnect, sendRaw } = useWebSocket() const { subscribe, unsubscribe } = useWebSocket()
// Tab 事件 // Tab 事件
const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'remove') => { const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'remove') => {
@@ -263,7 +263,7 @@ const addSchedulerTab = () => {
const removeSchedulerTab = (key: string) => { const removeSchedulerTab = (key: string) => {
const idx = schedulerTabs.value.findIndex(t => t.key === key) const idx = schedulerTabs.value.findIndex(t => t.key === key)
if (idx === -1) return 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) schedulerTabs.value.splice(idx, 1)
if (activeSchedulerTab.value === key) activeSchedulerTab.value = schedulerTabs.value[Math.max(0, idx - 1)]?.key || 'main' 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) const idx = currentTab.value.runningTasks.findIndex(t => t.taskName === name)
if (idx >= 0) { if (idx >= 0) {
const existing = currentTab.value.runningTasks[idx] const existing = currentTab.value.runningTasks[idx]
wsDisconnect(existing.websocketId) unsubscribe(existing.websocketId)
const oldId = existing.websocketId const oldId = existing.websocketId
existing.websocketId = r.websocketId existing.websocketId = r.websocketId
existing.status = '连接中' existing.status = '连接中'
@@ -347,18 +347,18 @@ const startQuickTask = async () => {
} }
} }
// 订阅任务 // 订阅任务 - 已重构为新的WebSocket订阅机制
const subscribeTask = (task: RunningTask, mode: TaskCreateIn.mode) => { const subscribeTask = (task: RunningTask, mode: TaskCreateIn.mode) => {
wsConnect({ // 使用已有的WebSocket订阅API实例
taskId: task.websocketId, subscribe(task.websocketId, {
mode, onProgress: (data) => handleTaskProgress(task, data),
onMessage: raw => handleWebSocketMessage(task, raw), onResult: (data) => handleTaskResult(task, data),
onStatusChange: st => { onError: (data) => handleTaskError(task, data),
if (st === '已连接' && task.status === '连接中') task.status = '运行中' onNotify: (data) => handleTaskNotify(task, data)
if (st === '已断开' && task.status === '运行中') task.status = '已断开'
if (st === '连接错误') task.status = '连接错误'
}
}) })
task.status = '运行中'
addTaskLog(task, `任务 ${task.taskName} 已开始执行 (模式: ${mode})`, 'info')
} }
// 取消添加 // 取消添加
@@ -393,6 +393,65 @@ const checkAllTasksCompleted = () => {
message.success(`所有任务结束,准备执行动作: ${action}`) 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 = () => { const cancelMessage = () => {
messageModalVisible.value = false messageModalVisible.value = false
@@ -400,67 +459,26 @@ const cancelMessage = () => {
currentMessage.value = null currentMessage.value = null
} }
// WebSocket 消息处理 // 回复消息 - 待重构为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')
}
}
// 回复消息
const sendMessageResponse = () => { const sendMessageResponse = () => {
if (!currentMessage.value?.taskId) return if (!currentMessage.value?.taskId) return
const task = schedulerTabs.value.flatMap(t => t.runningTasks).find(t => t.websocketId === currentMessage.value!.taskId) const task = schedulerTabs.value.flatMap(t => t.runningTasks).find(t => t.websocketId === currentMessage.value!.taskId)
if (task) { if (task) {
sendRaw('MessageResponse', { messageId: currentMessage.value!.messageId, response: messageResponse.value }, task.websocketId) // TODO: 实现WebSocket消息回复机制
addTaskLog(task, `用户回复: ${messageResponse.value}`, 'info') addTaskLog(task, `用户回复: ${messageResponse.value}`, 'info')
message.warning('消息回复功能待重构为WebSocket发送')
} }
messageModalVisible.value = false messageModalVisible.value = false
messageResponse.value = '' messageResponse.value = ''
currentMessage.value = null currentMessage.value = null
} }
// 停止任务 // 停止任务 - 已重构为新的WebSocket取消订阅
const stopTask = (id: string) => { const stopTask = (id: string) => {
const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === id) const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === id)
if (idx >= 0) { if (idx >= 0) {
const task = currentTab.value.runningTasks[idx] const task = currentTab.value.runningTasks[idx]
wsDisconnect(task.websocketId) unsubscribe(task.websocketId)
currentTab.value.runningTasks.splice(idx, 1) currentTab.value.runningTasks.splice(idx, 1)
const p = currentTab.value.activeTaskPanels.indexOf(id) const p = currentTab.value.activeTaskPanels.indexOf(id)
if (p >= 0) currentTab.value.activeTaskPanels.splice(p, 1) 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() }) onMounted(() => {
onUnmounted(() => { schedulerTabs.value.forEach(tab => tab.runningTasks.forEach(t => wsDisconnect(t.websocketId))) }) // WebSocket 连接由 useWebSocket 模块自动管理,这里只加载任务选项
loadTaskOptions()
})
onUnmounted(() => {
// 清理订阅,但不断开全局连接
schedulerTabs.value.forEach(tab => tab.runningTasks.forEach(t => unsubscribe(t.websocketId)))
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -236,9 +236,9 @@ import MarkdownIt from 'markdown-it'
const router = useRouter() const router = useRouter()
const { addScript, deleteScript, getScriptsWithUsers, loading } = useScriptApi() const { addScript, deleteScript, getScriptsWithUsers, loading } = useScriptApi()
const { addUser, updateUser, deleteUser, loading: userLoading } = useUserApi() const { updateUser, deleteUser } = useUserApi()
const { connect, disconnect, disconnectAll } = useWebSocket() const { subscribe, unsubscribe } = useWebSocket()
const { getWebConfigTemplates, importScriptFromWeb, loading: templateApiLoading } = useTemplateApi() const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi()
// 初始化markdown解析器 // 初始化markdown解析器
const md = new MarkdownIt({ const md = new MarkdownIt({
@@ -497,49 +497,36 @@ const handleDeleteUser = async (user: User) => {
const handleMAAConfig = async (script: Script) => { const handleMAAConfig = async (script: Script) => {
try { try {
// 检查是否已有连接 // 检查是否已有连接
const existingWebsocketId = activeConnections.value.get(script.id) const existingConnection = activeConnections.value.get(script.id)
if (existingWebsocketId) { if (existingConnection) {
message.warning('该脚本已在配置中,请先断开连接') message.warning('该脚本已在配置中,请先断开连接')
return return
} }
// 建立WebSocket连接进行MAA配置 // 新订阅
const websocketId = await connect({ subscribe(script.id, {
taskId: script.id,
mode: '设置脚本',
showNotifications: true,
onStatusChange: status => {
console.log(`脚本 ${script.name} 连接状态: ${status}`)
},
onMessage: data => {
console.log(`脚本 ${script.name} 收到消息:`, data)
// 这里可以根据需要处理特定的消息
},
onError: error => { onError: error => {
console.error(`脚本 ${script.name} 连接错误:`, error) console.error(`脚本 ${script.name} 连接错误:`, error)
message.error(`MAA配置连接失败: ${error}`) message.error(`MAA配置连接失败: ${error}`)
// 清理连接记录
activeConnections.value.delete(script.id) activeConnections.value.delete(script.id)
}, },
}) })
if (websocketId) { // 记录连接
// 记录连接 activeConnections.value.set(script.id, script.id)
activeConnections.value.set(script.id, websocketId) message.success(`已开始配置 ${script.name}`)
message.success(`已开始配置 ${script.name}`)
// 可选设置自动断开连接的定时器比如30分钟后 // 可选设置自动断开连接的定时器比如30分钟后
setTimeout( setTimeout(
() => { () => {
if (activeConnections.value.has(script.id)) { if (activeConnections.value.has(script.id)) {
disconnect(websocketId) unsubscribe(script.id)
activeConnections.value.delete(script.id) activeConnections.value.delete(script.id)
message.info(`${script.name} 配置会话已超时断开`) message.info(`${script.name} 配置会话已超时断开`)
} }
}, },
30 * 60 * 1000 30 * 60 * 1000
) // 30分钟 ) // 30分钟
}
} catch (error) { } catch (error) {
console.error('MAA配置失败:', error) console.error('MAA配置失败:', error)
message.error('MAA配置失败') message.error('MAA配置失败')
@@ -547,9 +534,9 @@ const handleMAAConfig = async (script: Script) => {
} }
const handleDisconnectMAA = (script: Script) => { const handleDisconnectMAA = (script: Script) => {
const websocketId = activeConnections.value.get(script.id) const connectionId = activeConnections.value.get(script.id)
if (websocketId) { if (connectionId) {
disconnect(websocketId) unsubscribe(script.id)
activeConnections.value.delete(script.id) activeConnections.value.delete(script.id)
message.success(`已断开 ${script.name} 的配置连接`) message.success(`已断开 ${script.name} 的配置连接`)
} }