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

# Conflicts:
#	frontend/src/views/MAAUserEdit.vue
This commit is contained in:
MoeSnowyFox
2025-09-14 23:34:12 +08:00
9 changed files with 1656 additions and 541 deletions

View File

@@ -124,22 +124,37 @@ class GlobalConfig(ConfigBase):
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
Data_LastStatisticsUpload = ConfigItem(
"Data", "LastStatisticsUpload", "2000-01-01 00:00:00", DateTimeValidator()
"Data",
"LastStatisticsUpload",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_LastStageUpdated = ConfigItem(
"Data", "LastStageUpdated", "2000-01-01 00:00:00", DateTimeValidator()
"Data",
"LastStageUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_StageTimeStamp = ConfigItem(
"Data", "StageTimeStamp", "2000-01-01 00:00:00", DateTimeValidator()
"Data",
"StageTimeStamp",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_Stage = ConfigItem("Data", "Stage", "{ }", JSONValidator())
Data_LastNoticeUpdated = ConfigItem(
"Data", "LastNoticeUpdated", "2000-01-01 00:00:00", DateTimeValidator()
"Data",
"LastNoticeUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator())
Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator())
Data_LastWebConfigUpdated = ConfigItem(
"Data", "LastWebConfigUpdated", "2000-01-01 00:00:00", DateTimeValidator()
"Data",
"LastWebConfigUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }", JSONValidator())
@@ -265,11 +280,15 @@ class MaaUserConfig(ConfigBase):
"Info", "SklandToken", "", EncryptValidator()
)
self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01")
self.Data_LastAnnihilationDate = ConfigItem(
"Data", "LastAnnihilationDate", "2000-01-01"
self.Data_LastProxyDate = ConfigItem(
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_LastAnnihilationDate = ConfigItem(
"Data", "LastAnnihilationDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_LastSklandDate = ConfigItem(
"Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_LastSklandDate = ConfigItem("Data", "LastSklandDate", "2000-01-01")
self.Data_ProxyTimes = ConfigItem(
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
)

View File

@@ -30,7 +30,7 @@ from typing import List, Any, Dict, Union, Optional
from app.utils import dpapi_encrypt, dpapi_decrypt
from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS
from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS, DEFAULT_DATETIME
class ConfigValidator:
@@ -114,24 +114,30 @@ class UUIDValidator(ConfigValidator):
class DateTimeValidator(ConfigValidator):
"""日期时间验证器"""
def __init__(self, date_format: str) -> None:
if not date_format:
raise ValueError("日期时间格式不能为空")
self.date_format = date_format
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
datetime.strptime(value, self.date_format)
return True
except ValueError:
return False
def correct(self, value: Any) -> str:
if not isinstance(value, str):
return "2000-01-01 00:00:00"
return DEFAULT_DATETIME.strftime(self.date_format)
try:
datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
datetime.strptime(value, self.date_format)
return value
except ValueError:
return "2000-01-01 00:00:00"
return DEFAULT_DATETIME.strftime(self.date_format)
class JSONValidator(ConfigValidator):
@@ -302,6 +308,8 @@ class ConfigItem:
要设置的值, 可以是任何合法类型
"""
print(self.group, self.name, value, self.value)
if (
dpapi_decrypt(self.value)
if isinstance(self.validator, EncryptValidator)

View File

@@ -1339,7 +1339,7 @@ class MaaManager:
shutil.copy(
(
Path.cwd()
/ f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json"
/ f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/ConfigFile/gui.json"
),
self.maa_set_path,
)
@@ -1597,7 +1597,7 @@ class MaaManager:
if (
Path.cwd()
/ f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json"
/ f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/Infrastructure/infrastructure.json"
).exists():
data["Configurations"]["Default"][
@@ -1618,7 +1618,7 @@ class MaaManager:
"Infrast.CustomInfrastFile"
] = str(
Path.cwd()
/ f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json"
/ f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/Infrastructure/infrastructure.json"
) # 自定义基建配置文件地址
else:
logger.warning(

View File

@@ -20,6 +20,8 @@
# Contact: DLmaster_361@163.com
from datetime import datetime
RESOURCE_STAGE_INFO = [
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},
@@ -270,3 +272,6 @@ MIRROR_ERROR_INFO = {
1: "未知错误类型",
}
"""MirrorChyan错误代码映射表"""
DEFAULT_DATETIME = datetime.strptime("2000-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
"""默认日期时间"""

View File

@@ -0,0 +1,388 @@
# AUTO-MAS 后端任务调度逻辑与WebSocket消息格式说明
## 1. 任务调度架构概览
AUTO-MAS 后端采用基于 AsyncIO 的异步任务调度系统,主要由以下核心组件构成:
### 1.1 核心组件
- **TaskManager**: 任务调度器,负责任务的创建、运行、停止和清理
- **Broadcast**: 消息广播系统,负责在不同组件间传递消息
- **WebSocket**: 与前端的实时通信通道
- **Config**: 配置管理系统,包含脚本配置、队列配置等
### 1.2 任务类型
系统支持三种主要任务模式:
1. **设置脚本** - 直接执行单个脚本配置
2. **自动代理** - 按队列顺序自动执行多个脚本
3. **人工排查** - 手动排查和执行任务
## 2. 任务调度流程
### 2.1 任务创建流程
```
前端请求 → API接口 → TaskManager.add_task() → 任务验证 → 创建异步任务 → 返回任务ID
```
**具体步骤:**
1. **任务验证**: 根据模式和UID验证任务配置是否存在
2. **重复检查**: 确保相同任务未在运行中
3. **任务创建**: 使用`asyncio.create_task()`创建异步任务
4. **回调设置**: 添加任务完成回调用于清理工作
### 2.2 任务执行流程
#### 设置脚本模式
```
获取脚本配置 → 确定脚本类型(MAA/General) → 创建对应Manager → 执行任务
```
#### 自动代理模式
```
获取队列配置 → 构建任务列表 → 逐个执行脚本 → 更新状态 → 发送完成信号
```
### 2.3 任务状态管理
- **等待**: 任务已加入队列但未开始执行
- **运行**: 任务正在执行中
- **跳过**: 任务因重复或其他原因被跳过
- **完成**: 任务执行完毕
## 3. WebSocket 消息系统
### 3.1 消息基础结构
所有WebSocket消息都遵循统一的JSON格式
```json
{
"id": "消息ID或任务ID",
"type": "消息类型",
"data": {
"具体数据": "根据类型而定"
}
}
```
### 3.2 消息类型详解
#### 3.2.1 Update 类型 - 数据更新
**用途**: 通知前端更新界面数据
**常见数据格式:**
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"user_list": [
{
"name": "用户名",
"status": "运行状态",
"config": "配置信息"
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"task_list": [
{
"script_id": "脚本ID",
"status": "等待/运行/完成/跳过",
"name": "脚本名称"
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"log": "任务执行日志内容"
}
}
```
#### 3.2.2 Info 类型 - 信息显示
**用途**: 向前端发送需要显示的信息,包括普通信息、警告和错误
**数据格式:**
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Error": "错误信息内容"
}
}
```
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Warning": "警告信息内容"
}
}
```
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Info": "普通信息内容"
}
}
```
#### 3.2.3 Message 类型 - 对话框请求
**用途**: 请求前端弹出对话框显示重要信息
**数据格式:**
```json
{
"id": "task-uuid",
"type": "Message",
"data": {
"title": "对话框标题",
"content": "对话框内容",
"type": "info/warning/error"
}
}
```
#### 3.2.4 Signal 类型 - 程序信号
**用途**: 发送程序控制信号和状态通知
**常见信号:**
**任务完成信号:**
```json
{
"id": "task-uuid",
"type": "Signal",
"data": {
"Accomplish": "任务完成后调度台显示的日志内容"
}
}
```
**电源操作信号:**
```json
{
"id": "task-uuid",
"type": "Signal",
"data": {
"power": "NoAction/KillSelf/Sleep/Hibernate/Shutdown/ShutdownForce",
}
}
```
**心跳信号:**
```json
{
"id": "Main",
"type": "Signal",
"data": {
"Ping": "无描述"
}
}
```
```json
{
"id": "Main",
"type": "Signal",
"data": {
"Pong": "无描述"
}
}
```
## 4. 任务管理器详细说明
### 4.1 TaskManager 核心方法
#### add_task(mode: str, uid: str)
- **功能**: 添加新任务到调度队列
- **参数**:
- `mode`: 任务模式 ("设置脚本", "自动代理", "人工排查")
- `uid`: 任务唯一标识符
- **返回**: 任务UUID
#### stop_task(task_id: str)
- **功能**: 停止指定任务
- **参数**:
- `task_id`: 任务ID支持 "ALL" 停止所有任务
#### run_task(mode: str, task_id: UUID, actual_id: Optional[UUID])
- **功能**: 执行具体任务逻辑
- **流程**: 根据模式选择相应的执行策略
### 4.2 任务执行器
#### GeneralManager
- **用途**: 处理通用脚本任务
- **特点**: 支持自定义脚本路径和参数
- **配置**: 基于 GeneralConfig 和 GeneralUserConfig
#### MaaManager
- **用途**: 处理MAA (明日方舟助手) 专用任务
- **特点**: 支持模拟器控制、ADB连接、游戏自动化
- **配置**: 基于 MaaConfig 和 MaaUserConfig
## 5. 消息广播系统
### 5.1 Broadcast 机制
- **设计模式**: 发布-订阅模式
- **功能**: 实现组件间解耦的消息传递
- **特点**: 支持多个订阅者同时接收消息
### 5.2 消息流向
```
任务执行器 → Broadcast → WebSocket → 前端界面
其他订阅者
```
## 6. 配置管理系统
### 6.1 配置类型
- **ScriptConfig**: 脚本配置包含MAA和General两种类型
- **QueueConfig**: 队列配置,定义自动代理任务的执行顺序
- **GlobalConfig**: 全局系统配置
### 6.2 配置操作
- **锁机制**: 防止配置在使用时被修改
- **实时更新**: 支持动态加载配置变更
- **类型验证**: 确保配置数据的正确性
## 7. API 接口说明
### 7.1 任务控制接口
**创建任务**
- **端点**: `POST /api/dispatch/start`
- **请求体**:
```json
{
"mode": "自动代理|人工排查|设置脚本",
"taskId": "目标任务ID"
}
```
- **响应**:
```json
{
"code": 200,
"status": "success",
"message": "操作成功",
"websocketId": "新任务ID"
}
```
**停止任务**
- **端点**: `POST /api/dispatch/stop`
- **请求体**:
```json
{
"taskId": "要停止的任务ID"
}
```
**电源操作**
- **端点**: `POST /api/dispatch/power`
- **请求体**:
```json
{
"signal": "NoAction|Shutdown|ShutdownForce|Hibernate|Sleep|KillSelf"
}
```
### 7.2 WebSocket 连接
**端点**: `WS /api/core/ws`
**连接特性**:
- 同时只允许一个WebSocket连接
- 自动心跳检测 (15秒超时)
- 连接断开时自动清理资源
## 8. 错误处理机制
### 8.1 异常类型
- **ValueError**: 配置验证失败
- **RuntimeError**: 任务状态冲突
- **TimeoutError**: 操作超时
- **ConnectionError**: 连接相关错误
### 8.2 错误响应格式
```json
{
"id": "相关任务ID",
"type": "Info",
"data": {
"Error": "具体错误描述"
}
}
```
## 9. 性能和监控
### 9.1 日志系统
- **分层日志**: 按模块划分日志记录器
- **实时监控**: 支持日志实时推送到前端
- **文件轮转**: 自动管理日志文件大小
### 9.2 资源管理
- **进程管理**: 自动清理子进程
- **内存监控**: 防止内存泄漏
- **连接池**: 复用数据库和网络连接
## 10. 安全考虑
### 10.1 输入验证
- **参数校验**: 使用 Pydantic 模型验证
- **路径安全**: 防止路径遍历攻击
- **命令注入**: 严格控制执行的命令参数
### 10.2 权限控制
- **单一连接**: 限制WebSocket连接数量
- **操作限制**: 防止重复或冲突操作
- **资源保护**: 防止资源滥用
---
*此文档基于 AUTO-MAS v5.0.0 版本编写详细的API文档和配置说明请参考相关配置文件和源代码注释。*

View File

@@ -590,12 +590,125 @@ ipcMain.handle('download-git', async () => {
ipcMain.handle('check-git-update', async () => {
try {
// 这里可以实现检查Git仓库更新的逻辑
// 暂时返回false表示没有更新
return { hasUpdate: false }
const appRoot = getAppRoot()
// 检查是否为Git仓库
const gitDir = path.join(appRoot, '.git')
if (!fs.existsSync(gitDir)) {
log.info('不是Git仓库跳过更新检查')
return { hasUpdate: false }
}
// 检查Git可执行文件是否存在
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
if (!fs.existsSync(gitPath)) {
log.warn('Git可执行文件不存在无法检查更新')
return { hasUpdate: false, error: 'Git可执行文件不存在' }
}
// 获取Git环境变量
const gitEnv = {
...process.env,
PATH: `${path.join(appRoot, 'environment', 'git', 'bin')};${path.join(appRoot, 'environment', 'git', 'mingw64', 'bin')};${process.env.PATH}`,
GIT_EXEC_PATH: path.join(appRoot, 'environment', 'git', 'mingw64', 'libexec', 'git-core'),
HOME: process.env.USERPROFILE || process.env.HOME,
GIT_CONFIG_NOSYSTEM: '1',
GIT_TERMINAL_PROMPT: '0',
GIT_ASKPASS: '',
}
log.info('开始检查Git仓库更新...')
// 执行 git fetch 获取最新的远程信息
await new Promise<void>((resolve, reject) => {
const fetchProc = spawn(gitPath, ['fetch', 'origin'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
fetchProc.stdout?.on('data', (data) => {
log.info('git fetch output:', data.toString())
})
fetchProc.stderr?.on('data', (data) => {
log.info('git fetch stderr:', data.toString())
})
fetchProc.on('close', (code) => {
if (code === 0) {
resolve()
} else {
reject(new Error(`git fetch失败退出码: ${code}`))
}
})
fetchProc.on('error', reject)
})
// 检查本地分支是否落后于远程分支
const hasUpdate = await new Promise<boolean>((resolve, reject) => {
const statusProc = spawn(gitPath, ['status', '-uno', '--porcelain=v1'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
let output = ''
statusProc.stdout?.on('data', (data) => {
output += data.toString()
})
statusProc.stderr?.on('data', (data) => {
log.info('git status stderr:', data.toString())
})
statusProc.on('close', (code) => {
if (code === 0) {
// 检查是否有 "Your branch is behind" 的信息
// 使用 git rev-list 来比较本地和远程分支
const revListProc = spawn(gitPath, ['rev-list', '--count', 'HEAD..origin/feature/refactor'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
let revOutput = ''
revListProc.stdout?.on('data', (data) => {
revOutput += data.toString()
})
revListProc.on('close', (revCode) => {
if (revCode === 0) {
const commitsBehind = parseInt(revOutput.trim())
const hasUpdates = commitsBehind > 0
log.info(`本地分支落后远程分支 ${commitsBehind} 个提交hasUpdate: ${hasUpdates}`)
resolve(hasUpdates)
} else {
log.warn('无法比较本地和远程分支,假设有更新')
resolve(true) // 如果无法确定,假设有更新
}
})
revListProc.on('error', () => {
log.warn('git rev-list执行失败假设有更新')
resolve(true)
})
} else {
reject(new Error(`git status失败退出码: ${code}`))
}
})
statusProc.on('error', reject)
})
log.info(`Git更新检查完成hasUpdate: ${hasUpdate}`)
return { hasUpdate }
} catch (error) {
log.error('检查Git更新失败:', error)
return { hasUpdate: false, error: error instanceof Error ? error.message : String(error) }
// 如果检查失败返回true以触发更新流程确保代码是最新的
return { hasUpdate: true, error: error instanceof Error ? error.message : String(error) }
}
})

View File

@@ -37,24 +37,24 @@
type="primary"
ghost
size="middle"
@click="handleMAAConfig(script)"
@click="handleStartMAAConfig(script)"
>
<template #icon>
<SettingOutlined />
</template>
置MAA全局配置
置MAA
</a-button>
<a-button
v-if="script.type === 'MAA' && props.activeConnections.has(script.id)"
type="primary"
danger
size="middle"
@click="handleDisconnectMAA(script)"
style="background: #52c41a; border-color: #52c41a;"
@click="handleSaveMAAConfig(script)"
>
<template #icon>
<StopOutlined />
<SaveOutlined />
</template>
断开配置连接
保存配置
</a-button>
<a-button type="default" size="middle" @click="handleEdit(script)">
<template #icon>
@@ -291,8 +291,8 @@ import type { Script, User } from '../types/script'
import {
DeleteOutlined,
EditOutlined,
SaveOutlined,
SettingOutlined,
StopOutlined,
UserAddOutlined,
} from '@ant-design/icons-vue'
@@ -312,9 +312,9 @@ interface Emits {
(e: 'deleteUser', user: User): void
(e: 'maaConfig', script: Script): void
(e: 'startMaaConfig', script: Script): void
(e: 'disconnectMaa', script: Script): void
(e: 'saveMaaConfig', script: Script): void
(e: 'toggleUserStatus', user: User): void
}
@@ -350,12 +350,12 @@ const handleDeleteUser = (user: User) => {
emit('deleteUser', user)
}
const handleMAAConfig = (script: Script) => {
emit('maaConfig', script)
const handleStartMAAConfig = (script: Script) => {
emit('startMaaConfig', script)
}
const handleDisconnectMAA = (script: Script) => {
emit('disconnectMaa', script)
const handleSaveMAAConfig = (script: Script) => {
emit('saveMaaConfig', script)
}
const handleToggleUserStatus = (user: User) => {

File diff suppressed because it is too large Load Diff

View File

@@ -40,8 +40,8 @@
@add-user="handleAddUser"
@edit-user="handleEditUser"
@delete-user="handleDeleteUser"
@maa-config="handleMAAConfig"
@disconnect-maa="handleDisconnectMAA"
@start-maa-config="handleStartMAAConfig"
@save-maa-config="handleSaveMAAConfig"
@toggle-user-status="handleToggleUserStatus"
/>
@@ -232,6 +232,8 @@ import { useScriptApi } from '@/composables/useScriptApi'
import { useUserApi } from '@/composables/useUserApi'
import { useWebSocket } from '@/composables/useWebSocket'
import { useTemplateApi, type WebConfigTemplate } from '@/composables/useTemplateApi'
import { Service } from '@/api/services/Service'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
import MarkdownIt from 'markdown-it'
const router = useRouter()
@@ -494,51 +496,89 @@ const handleDeleteUser = async (user: User) => {
}
}
const handleMAAConfig = async (script: Script) => {
const handleStartMAAConfig = async (script: Script) => {
try {
// 检查是否已有连接
const existingConnection = activeConnections.value.get(script.id)
if (existingConnection) {
message.warning('该脚本已在配置中,请先断开连接')
message.warning('该脚本已在配置中,请先保存配置')
return
}
// 新订阅
subscribe(script.id, {
onError: error => {
console.error(`脚本 ${script.name} 连接错误:`, error)
message.error(`MAA配置连接失败: ${error}`)
activeConnections.value.delete(script.id)
},
// 调用启动配置任务API
const response = await Service.addTaskApiDispatchStartPost({
taskId: script.id,
mode: TaskCreateIn.mode.SettingScriptMode
})
// 记录连接
activeConnections.value.set(script.id, script.id)
message.success(`已开始配置 ${script.name}`)
// 可选设置自动断开连接的定时器比如30分钟后
setTimeout(
() => {
if (activeConnections.value.has(script.id)) {
unsubscribe(script.id)
if (response.code === 200) {
// 订阅WebSocket消息
subscribe(response.websocketId, {
onError: error => {
console.error(`脚本 ${script.name} 连接错误:`, error)
message.error(`MAA配置连接失败: ${error}`)
activeConnections.value.delete(script.id)
message.info(`${script.name} 配置会话已超时断开`)
},
onResult: (data: any) => {
// 处理配置完成消息(兼容任何结构)
if (data.Accomplish) {
message.success(`${script.name} 配置已完成`)
activeConnections.value.delete(script.id)
}
}
},
30 * 60 * 1000
) // 30分钟
})
// 记录连接和websocketId
activeConnections.value.set(script.id, response.websocketId)
message.success(`已启动 ${script.name} 的MAA配置`)
// 设置自动断开连接的定时器30分钟后
setTimeout(
() => {
if (activeConnections.value.has(script.id)) {
const wsId = activeConnections.value.get(script.id)
if (wsId) {
unsubscribe(wsId)
}
activeConnections.value.delete(script.id)
message.info(`${script.name} 配置会话已超时断开`)
}
},
30 * 60 * 1000
) // 30分钟
} else {
message.error(response.message || '启动MAA配置失败')
}
} catch (error) {
console.error('MAA配置失败:', error)
message.error('MAA配置失败')
console.error('启动MAA配置失败:', error)
message.error('启动MAA配置失败')
}
}
const handleDisconnectMAA = (script: Script) => {
const connectionId = activeConnections.value.get(script.id)
if (connectionId) {
unsubscribe(script.id)
activeConnections.value.delete(script.id)
message.success(`已断开 ${script.name} 的配置连接`)
const handleSaveMAAConfig = async (script: Script) => {
try {
const websocketId = activeConnections.value.get(script.id)
if (!websocketId) {
message.error('未找到活动的配置会话')
return
}
// 调用停止配置任务API
const response = await Service.stopTaskApiDispatchStopPost({
taskId: websocketId
})
if (response.code === 200) {
// 取消订阅
unsubscribe(websocketId)
activeConnections.value.delete(script.id)
message.success(`${script.name} 的配置已保存`)
} else {
message.error(response.message || '保存配置失败')
}
} catch (error) {
console.error('保存MAA配置失败:', error)
message.error('保存MAA配置失败')
}
}