Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 初始化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,7 +933,13 @@ class AppConfig(GlobalConfig):
|
||||
await Config.websocket.send_json(data)
|
||||
|
||||
async def get_git_version(self) -> tuple[bool, str, str]:
|
||||
"""获取Git版本信息,如果Git不可用则返回默认值"""
|
||||
|
||||
if self.repo is None:
|
||||
logger.warning("Git仓库不可用,返回默认版本信息")
|
||||
return False, "unknown", "unknown"
|
||||
|
||||
try:
|
||||
# 获取当前 commit
|
||||
current_commit = self.repo.head.commit
|
||||
|
||||
@@ -928,6 +957,9 @@ class AppConfig(GlobalConfig):
|
||||
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"]
|
||||
|
||||
@@ -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):
|
||||
"""加密数据验证器"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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连接建立成功')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时开始自动流程
|
||||
|
||||
@@ -7,7 +7,34 @@
|
||||
>
|
||||
|
||||
<!-- <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-->
|
||||
@@ -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'
|
||||
|
||||
@@ -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<WebSocketStatus>
|
||||
@@ -96,7 +62,18 @@ interface GlobalWSStorage {
|
||||
moduleLoadCount: number
|
||||
createdAt: number
|
||||
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')
|
||||
@@ -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<BackendStatus>('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<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) {
|
||||
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({
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'Signal',
|
||||
data: pingData
|
||||
}))
|
||||
|
||||
// 心跳超时检测 - 但不主动断开连接
|
||||
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
|
||||
|
||||
// 优先处理Signal类型的ping-pong消息,不受id限制
|
||||
if (msgType === 'Signal') {
|
||||
// 处理心跳响应
|
||||
if (msgType === 'Signal' && raw.data && raw.data.Pong) {
|
||||
const pongTime = raw.data.Pong
|
||||
const latency = Date.now() - pongTime
|
||||
wsLog(`收到心跳pong响应,延迟: ${latency}ms`)
|
||||
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<boolean> => {
|
||||
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({
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'Signal',
|
||||
data: connectData
|
||||
}))
|
||||
} catch (e) {
|
||||
wsError('发送连接确认失败', e)
|
||||
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<boolean> => {
|
||||
// 连接全局 WebSocket
|
||||
const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promise<boolean> => {
|
||||
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 (!checkConnectionPermission()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (global.wsRef.readyState === WebSocket.CONNECTING) {
|
||||
wsLog('WebSocket 正在连接中')
|
||||
return true
|
||||
}
|
||||
// 验证连接原因是否合法
|
||||
if (!isValidConnectionReason(reason)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (global.isConnecting) {
|
||||
wsLog('全局连接标志显示正在连接中,等待连接完成')
|
||||
return true
|
||||
// 尝试获取全局连接锁
|
||||
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,50 +585,25 @@ export function useWebSocket() {
|
||||
|
||||
const subscribe = (id: string, handlers: Omit<WebSocketSubscriber, 'id'>) => {
|
||||
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)
|
||||
}
|
||||
|
||||
// 移除 destroy 功能,确保连接永不断开
|
||||
const forceReconnect = () => {
|
||||
wsLog('手动触发重连')
|
||||
if (global.wsRef) {
|
||||
// 不关闭现有连接,直接尝试创建新连接
|
||||
global.isConnecting = false
|
||||
connectGlobalWebSocket()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const getConnectionInfo = () => {
|
||||
const info = {
|
||||
const getConnectionInfo = () => ({
|
||||
connectionId: global.connectionId,
|
||||
status: global.status.value,
|
||||
subscriberCount: global.subscribers.value.size,
|
||||
@@ -491,87 +613,34 @@ export function useWebSocket() {
|
||||
hasHeartbeat: !!global.heartbeatTimer,
|
||||
hasEverConnected: global.hasEverConnected,
|
||||
reconnectAttempts: global.reconnectAttempts,
|
||||
wsDevEnabled: WS_DEV,
|
||||
isPersistentMode: true // 标识为永久连接模式
|
||||
}
|
||||
wsLog('连接信息查询', info)
|
||||
return info
|
||||
}
|
||||
|
||||
// 兼容旧版 API:connect 重载
|
||||
async function connect(): Promise<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
|
||||
isPersistentMode: true, // 标识为永久连接模式
|
||||
})
|
||||
|
||||
// 立即推送当前状态
|
||||
config.onStatusChange?.(global.status.value)
|
||||
|
||||
// 可根据 mode 发送一个初始信号(可选)
|
||||
if (config.mode) {
|
||||
sendRaw('Mode', { mode: config.mode }, config.taskId)
|
||||
const restartBackendManually = async () => {
|
||||
const global = getGlobalStorage()
|
||||
global.backendRestartAttempts = 0
|
||||
return await restartBackend()
|
||||
}
|
||||
|
||||
return config.taskId
|
||||
const getBackendStatus = () => {
|
||||
const global = getGlobalStorage()
|
||||
return {
|
||||
status: global.backendStatus.value,
|
||||
restartAttempts: global.backendRestartAttempts,
|
||||
isRestarting: global.isRestartingBackend,
|
||||
lastCheck: global.lastBackendCheck,
|
||||
}
|
||||
|
||||
// 兼容旧版 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,
|
||||
}
|
||||
}
|
||||
|
||||
3
frontend/src/types/electron.d.ts
vendored
3
frontend/src/types/electron.d.ts
vendored
@@ -18,7 +18,8 @@ export interface ElectronAPI {
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (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>
|
||||
|
||||
@@ -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<FormInstance>()
|
||||
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
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
@@ -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<FormInstance>()
|
||||
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
|
||||
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')
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -236,9 +236,9 @@ import MarkdownIt from 'markdown-it'
|
||||
|
||||
const router = useRouter()
|
||||
const { addScript, deleteScript, getScriptsWithUsers, loading } = useScriptApi()
|
||||
const { addUser, updateUser, deleteUser, loading: userLoading } = useUserApi()
|
||||
const { connect, disconnect, disconnectAll } = useWebSocket()
|
||||
const { getWebConfigTemplates, importScriptFromWeb, loading: templateApiLoading } = useTemplateApi()
|
||||
const { updateUser, deleteUser } = useUserApi()
|
||||
const { subscribe, unsubscribe } = useWebSocket()
|
||||
const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi()
|
||||
|
||||
// 初始化markdown解析器
|
||||
const md = new MarkdownIt({
|
||||
@@ -497,49 +497,36 @@ const handleDeleteUser = async (user: User) => {
|
||||
const handleMAAConfig = async (script: Script) => {
|
||||
try {
|
||||
// 检查是否已有连接
|
||||
const existingWebsocketId = activeConnections.value.get(script.id)
|
||||
if (existingWebsocketId) {
|
||||
const existingConnection = activeConnections.value.get(script.id)
|
||||
if (existingConnection) {
|
||||
message.warning('该脚本已在配置中,请先断开连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 建立WebSocket连接进行MAA配置
|
||||
const websocketId = await connect({
|
||||
taskId: script.id,
|
||||
mode: '设置脚本',
|
||||
showNotifications: true,
|
||||
onStatusChange: status => {
|
||||
console.log(`脚本 ${script.name} 连接状态: ${status}`)
|
||||
},
|
||||
onMessage: data => {
|
||||
console.log(`脚本 ${script.name} 收到消息:`, data)
|
||||
// 这里可以根据需要处理特定的消息
|
||||
},
|
||||
// 新订阅
|
||||
subscribe(script.id, {
|
||||
onError: error => {
|
||||
console.error(`脚本 ${script.name} 连接错误:`, error)
|
||||
message.error(`MAA配置连接失败: ${error}`)
|
||||
// 清理连接记录
|
||||
activeConnections.value.delete(script.id)
|
||||
},
|
||||
})
|
||||
|
||||
if (websocketId) {
|
||||
// 记录连接
|
||||
activeConnections.value.set(script.id, websocketId)
|
||||
activeConnections.value.set(script.id, script.id)
|
||||
message.success(`已开始配置 ${script.name}`)
|
||||
|
||||
// 可选:设置自动断开连接的定时器(比如30分钟后)
|
||||
setTimeout(
|
||||
() => {
|
||||
if (activeConnections.value.has(script.id)) {
|
||||
disconnect(websocketId)
|
||||
unsubscribe(script.id)
|
||||
activeConnections.value.delete(script.id)
|
||||
message.info(`${script.name} 配置会话已超时断开`)
|
||||
}
|
||||
},
|
||||
30 * 60 * 1000
|
||||
) // 30分钟
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MAA配置失败:', error)
|
||||
message.error('MAA配置失败')
|
||||
@@ -547,9 +534,9 @@ const handleMAAConfig = async (script: Script) => {
|
||||
}
|
||||
|
||||
const handleDisconnectMAA = (script: Script) => {
|
||||
const websocketId = activeConnections.value.get(script.id)
|
||||
if (websocketId) {
|
||||
disconnect(websocketId)
|
||||
const connectionId = activeConnections.value.get(script.id)
|
||||
if (connectionId) {
|
||||
unsubscribe(script.id)
|
||||
activeConnections.value.delete(script.id)
|
||||
message.success(`已断开 ${script.name} 的配置连接`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user