Compare commits
3 Commits
v5.0.0-alp
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e516d1d866 | ||
|
|
f4bcb73fe9 | ||
|
|
993524c4dd |
@@ -29,7 +29,7 @@ from fastapi import APIRouter, Body
|
||||
from app.core import Config
|
||||
from app.services import System, Notify
|
||||
from app.models.schema import *
|
||||
from app.models.config import Webhook as WebhookConfig
|
||||
import uuid
|
||||
|
||||
router = APIRouter(prefix="/api/setting", tags=["全局设置"])
|
||||
|
||||
@@ -100,99 +100,169 @@ async def test_notify() -> OutBase:
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/get",
|
||||
summary="查询 webhook 配置",
|
||||
response_model=WebhookGetOut,
|
||||
"/webhook/create",
|
||||
summary="创建自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut:
|
||||
async def create_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""创建自定义Webhook"""
|
||||
|
||||
try:
|
||||
index, data = await Config.get_webhook(None, None, webhook.webhookId)
|
||||
index = [WebhookIndexItem(**_) for _ in index]
|
||||
data = {uid: Webhook(**cfg) for uid, cfg in data.items()}
|
||||
# 生成唯一ID
|
||||
webhook_id = str(uuid.uuid4())
|
||||
|
||||
# 创建webhook配置
|
||||
webhook_config = {
|
||||
"id": webhook_id,
|
||||
"name": webhook_data.get("name", ""),
|
||||
"url": webhook_data.get("url", ""),
|
||||
"template": webhook_data.get("template", ""),
|
||||
"enabled": webhook_data.get("enabled", True),
|
||||
"headers": webhook_data.get("headers", {}),
|
||||
"method": webhook_data.get("method", "POST"),
|
||||
}
|
||||
|
||||
# 获取当前配置
|
||||
current_config = await Config.get_setting()
|
||||
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
|
||||
|
||||
# 添加新webhook
|
||||
custom_webhooks.append(webhook_config)
|
||||
|
||||
# 更新配置
|
||||
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
|
||||
await Config.update_setting(update_data)
|
||||
|
||||
return OutBase(message=f"Webhook '{webhook_config['name']}' 创建成功")
|
||||
|
||||
except Exception as e:
|
||||
return WebhookGetOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
index=[],
|
||||
data={},
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return WebhookGetOut(index=index, data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/add",
|
||||
summary="添加定时项",
|
||||
response_model=WebhookCreateOut,
|
||||
"/webhook/update",
|
||||
summary="更新自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def add_webhook() -> WebhookCreateOut:
|
||||
|
||||
uid, config = await Config.add_webhook(None, None)
|
||||
data = Webhook(**(await config.toDict()))
|
||||
return WebhookCreateOut(webhookId=str(uid), data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/update", summary="更新定时项", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase:
|
||||
async def update_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""更新自定义Webhook"""
|
||||
|
||||
try:
|
||||
await Config.update_webhook(
|
||||
None, None, webhook.webhookId, webhook.data.model_dump(exclude_unset=True)
|
||||
)
|
||||
webhook_id = webhook_data.get("id")
|
||||
if not webhook_id:
|
||||
return OutBase(code=400, status="error", message="缺少Webhook ID")
|
||||
|
||||
# 获取当前配置
|
||||
current_config = await Config.get_setting()
|
||||
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
|
||||
|
||||
# 查找并更新webhook
|
||||
updated = False
|
||||
for i, webhook in enumerate(custom_webhooks):
|
||||
if webhook.get("id") == webhook_id:
|
||||
custom_webhooks[i].update(
|
||||
{
|
||||
"name": webhook_data.get("name", webhook.get("name", "")),
|
||||
"url": webhook_data.get("url", webhook.get("url", "")),
|
||||
"template": webhook_data.get(
|
||||
"template", webhook.get("template", "")
|
||||
),
|
||||
"enabled": webhook_data.get(
|
||||
"enabled", webhook.get("enabled", True)
|
||||
),
|
||||
"headers": webhook_data.get(
|
||||
"headers", webhook.get("headers", {})
|
||||
),
|
||||
"method": webhook_data.get(
|
||||
"method", webhook.get("method", "POST")
|
||||
),
|
||||
}
|
||||
)
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
return OutBase(code=404, status="error", message="Webhook不存在")
|
||||
|
||||
# 更新配置
|
||||
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
|
||||
await Config.update_setting(update_data)
|
||||
|
||||
return OutBase(message="Webhook更新成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/delete", summary="删除定时项", response_model=OutBase, status_code=200
|
||||
"/webhook/delete",
|
||||
summary="删除自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase:
|
||||
async def delete_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""删除自定义Webhook"""
|
||||
|
||||
try:
|
||||
await Config.del_webhook(None, None, webhook.webhookId)
|
||||
webhook_id = webhook_data.get("id")
|
||||
if not webhook_id:
|
||||
return OutBase(code=400, status="error", message="缺少Webhook ID")
|
||||
|
||||
# 获取当前配置
|
||||
current_config = await Config.get_setting()
|
||||
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
|
||||
|
||||
# 查找并删除webhook
|
||||
original_length = len(custom_webhooks)
|
||||
custom_webhooks = [w for w in custom_webhooks if w.get("id") != webhook_id]
|
||||
|
||||
if len(custom_webhooks) == original_length:
|
||||
return OutBase(code=404, status="error", message="Webhook不存在")
|
||||
|
||||
# 更新配置
|
||||
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
|
||||
await Config.update_setting(update_data)
|
||||
|
||||
return OutBase(message="Webhook删除成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/order", summary="重新排序定时项", response_model=OutBase, status_code=200
|
||||
"/webhook/test",
|
||||
summary="测试自定义Webhook",
|
||||
response_model=OutBase,
|
||||
status_code=200,
|
||||
)
|
||||
async def reorder_webhook(webhook: WebhookReorderIn = Body(...)) -> OutBase:
|
||||
|
||||
try:
|
||||
await Config.reorder_webhook(None, None, webhook.indexList)
|
||||
except Exception as e:
|
||||
return OutBase(
|
||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||
)
|
||||
return OutBase()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/webhook/test", summary="测试Webhook配置", response_model=OutBase, status_code=200
|
||||
)
|
||||
async def test_webhook(webhook: WebhookTestIn = Body(...)) -> OutBase:
|
||||
async def test_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||
"""测试自定义Webhook"""
|
||||
|
||||
try:
|
||||
webhook_config = WebhookConfig()
|
||||
await webhook_config.load(webhook.data.model_dump())
|
||||
await Notify.WebhookPush(
|
||||
webhook_config = {
|
||||
"name": webhook_data.get("name", "测试Webhook"),
|
||||
"url": webhook_data.get("url", ""),
|
||||
"template": webhook_data.get("template", ""),
|
||||
"enabled": True,
|
||||
"headers": webhook_data.get("headers", {}),
|
||||
"method": webhook_data.get("method", "POST"),
|
||||
}
|
||||
|
||||
await Notify.CustomWebhookPush(
|
||||
"AUTO-MAS Webhook测试",
|
||||
"这是一条测试消息,如果您收到此消息,说明Webhook配置正确!",
|
||||
webhook_config,
|
||||
)
|
||||
|
||||
return OutBase(message="Webhook测试成功")
|
||||
|
||||
except Exception as e:
|
||||
return OutBase(code=500, status="error", message=f"Webhook测试失败: {str(e)}")
|
||||
return OutBase()
|
||||
|
||||
@@ -45,6 +45,7 @@ class _Broadcast:
|
||||
|
||||
async def put(self, item):
|
||||
"""向所有订阅者广播消息"""
|
||||
logger.debug(f"向所有订阅者广播消息: {item}")
|
||||
for subscriber in self.__subscribers:
|
||||
await subscriber.put(deepcopy(item))
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,10 @@ class _TaskManager:
|
||||
actual_id = None
|
||||
else:
|
||||
for script_id, script in Config.ScriptConfig.items():
|
||||
if actual_id in script.UserData:
|
||||
if (
|
||||
isinstance(script, (MaaConfig | GeneralConfig))
|
||||
and actual_id in script.UserData
|
||||
):
|
||||
task_id = script_id
|
||||
break
|
||||
else:
|
||||
@@ -139,26 +142,31 @@ class _TaskManager:
|
||||
# 初始化任务列表
|
||||
if task_id in Config.QueueConfig:
|
||||
|
||||
queue = Config.QueueConfig[task_id]
|
||||
if not isinstance(queue, QueueConfig):
|
||||
return
|
||||
|
||||
task_list = []
|
||||
for queue_item in Config.QueueConfig[task_id].QueueItem.values():
|
||||
for queue_item in queue.QueueItem.values():
|
||||
if queue_item.get("Info", "ScriptId") == "-":
|
||||
continue
|
||||
script_uid = uuid.UUID(queue_item.get("Info", "ScriptId"))
|
||||
|
||||
script_id = uuid.UUID(queue_item.get("Info", "ScriptId"))
|
||||
script = Config.ScriptConfig[script_id]
|
||||
if not isinstance(script, (MaaConfig | GeneralConfig)):
|
||||
logger.error(f"不支持的脚本类型: {type(script).__name__}")
|
||||
continue
|
||||
task_list.append(
|
||||
{
|
||||
"script_id": str(script_uid),
|
||||
"script_id": str(script_id),
|
||||
"status": "等待",
|
||||
"name": Config.ScriptConfig[script_uid].get("Info", "Name"),
|
||||
"name": script.get("Info", "Name"),
|
||||
"user_list": [
|
||||
{
|
||||
"user_id": str(user_id),
|
||||
"status": "等待",
|
||||
"name": config.get("Info", "Name"),
|
||||
}
|
||||
for user_id, config in Config.ScriptConfig[
|
||||
script_uid
|
||||
].UserData.items()
|
||||
for user_id, config in script.UserData.items()
|
||||
if config.get("Info", "Status")
|
||||
and config.get("Info", "RemainedDay") != 0
|
||||
],
|
||||
@@ -167,20 +175,23 @@ class _TaskManager:
|
||||
|
||||
elif actual_id is not None and actual_id in Config.ScriptConfig:
|
||||
|
||||
script = Config.ScriptConfig[actual_id]
|
||||
if not isinstance(script, (MaaConfig | GeneralConfig)):
|
||||
logger.error(f"不支持的脚本类型: {type(script).__name__}")
|
||||
return
|
||||
|
||||
task_list = [
|
||||
{
|
||||
"script_id": str(actual_id),
|
||||
"status": "等待",
|
||||
"name": Config.ScriptConfig[actual_id].get("Info", "Name"),
|
||||
"name": script.get("Info", "Name"),
|
||||
"user_list": [
|
||||
{
|
||||
"user_id": str(user_id),
|
||||
"status": "等待",
|
||||
"name": config.get("Info", "Name"),
|
||||
}
|
||||
for user_id, config in Config.ScriptConfig[
|
||||
actual_id
|
||||
].UserData.items()
|
||||
for user_id, config in script.UserData.items()
|
||||
if config.get("Info", "Status")
|
||||
and config.get("Info", "RemainedDay") != 0
|
||||
],
|
||||
|
||||
@@ -81,7 +81,9 @@ class _MainTimer:
|
||||
|
||||
for uid, queue in Config.QueueConfig.items():
|
||||
|
||||
if not queue.get("Info", "TimeEnabled"):
|
||||
if not isinstance(queue, QueueConfig) or not queue.get(
|
||||
"Info", "TimeEnabled"
|
||||
):
|
||||
continue
|
||||
|
||||
# 避免重复调起任务
|
||||
|
||||
@@ -25,10 +25,9 @@ import json
|
||||
import uuid
|
||||
import win32com.client
|
||||
from copy import deepcopy
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Any, Dict, Union, Optional, TypeVar, Generic, Type
|
||||
from typing import List, Any, Dict, Union, Optional
|
||||
|
||||
|
||||
from app.utils import dpapi_encrypt, dpapi_decrypt
|
||||
@@ -128,25 +127,17 @@ class DateTimeValidator(ConfigValidator):
|
||||
|
||||
class JSONValidator(ConfigValidator):
|
||||
|
||||
def __init__(self, tpye: type[dict] | type[list] = dict) -> None:
|
||||
self.type = tpye
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
try:
|
||||
data = json.loads(value)
|
||||
if isinstance(data, self.type):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
json.loads(value)
|
||||
return True
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
return (
|
||||
value if self.validate(value) else ("{ }" if self.type == dict else "[ ]")
|
||||
)
|
||||
return value if self.validate(value) else "{ }"
|
||||
|
||||
|
||||
class EncryptValidator(ConfigValidator):
|
||||
@@ -255,67 +246,6 @@ class UserNameValidator(ConfigValidator):
|
||||
return value
|
||||
|
||||
|
||||
class URLValidator(ConfigValidator):
|
||||
"""URL格式验证器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schemes: list[str] | None = None,
|
||||
require_netloc: bool = True,
|
||||
default: str = "",
|
||||
):
|
||||
"""
|
||||
:param schemes: 允许的协议列表, 若为 None 则允许任意协议
|
||||
:param require_netloc: 是否要求必须包含网络位置, 如域名或IP
|
||||
"""
|
||||
self.schemes = [s.lower() for s in schemes] if schemes else None
|
||||
self.require_netloc = require_netloc
|
||||
self.default = default
|
||||
|
||||
def validate(self, value: Any) -> bool:
|
||||
|
||||
if value == self.default:
|
||||
return True
|
||||
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
|
||||
try:
|
||||
parsed = urlparse(value)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# 检查协议
|
||||
if self.schemes is not None:
|
||||
if not parsed.scheme or parsed.scheme.lower() not in self.schemes:
|
||||
return False
|
||||
else:
|
||||
# 不限制协议仍要求有 scheme
|
||||
if not parsed.scheme:
|
||||
return False
|
||||
|
||||
# 检查是否包含网络位置
|
||||
if self.require_netloc and not parsed.netloc:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def correct(self, value: Any) -> str:
|
||||
|
||||
if self.validate(value):
|
||||
return value
|
||||
|
||||
if isinstance(value, str):
|
||||
# 简单尝试:若看起来像域名,加上 https://
|
||||
stripped = value.strip()
|
||||
if stripped and not stripped.startswith(("http://", "https://")):
|
||||
candidate = f"https://{stripped}"
|
||||
if self.validate(candidate):
|
||||
return candidate
|
||||
|
||||
return self.default
|
||||
|
||||
|
||||
class ConfigItem:
|
||||
"""配置项"""
|
||||
|
||||
@@ -607,10 +537,7 @@ class ConfigBase:
|
||||
await item.unlock()
|
||||
|
||||
|
||||
T = TypeVar("T", bound="ConfigBase")
|
||||
|
||||
|
||||
class MultipleConfig(Generic[T]):
|
||||
class MultipleConfig:
|
||||
"""
|
||||
多配置项管理类
|
||||
|
||||
@@ -623,7 +550,7 @@ class MultipleConfig(Generic[T]):
|
||||
子配置项的类型列表, 必须是 ConfigBase 的子类
|
||||
"""
|
||||
|
||||
def __init__(self, sub_config_type: List[Type[T]]):
|
||||
def __init__(self, sub_config_type: List[type]):
|
||||
|
||||
if not sub_config_type:
|
||||
raise ValueError("子配置项类型列表不能为空")
|
||||
@@ -634,13 +561,13 @@ class MultipleConfig(Generic[T]):
|
||||
f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
|
||||
)
|
||||
|
||||
self.sub_config_type: List[Type[T]] = sub_config_type
|
||||
self.file: Path | None = None
|
||||
self.sub_config_type = sub_config_type
|
||||
self.file: None | Path = None
|
||||
self.order: List[uuid.UUID] = []
|
||||
self.data: Dict[uuid.UUID, T] = {}
|
||||
self.data: Dict[uuid.UUID, ConfigBase] = {}
|
||||
self.is_locked = False
|
||||
|
||||
def __getitem__(self, key: uuid.UUID) -> T:
|
||||
def __getitem__(self, key: uuid.UUID) -> ConfigBase:
|
||||
"""允许通过 config[uuid] 访问配置项"""
|
||||
if key not in self.data:
|
||||
raise KeyError(f"配置项 '{key}' 不存在")
|
||||
@@ -738,9 +665,7 @@ class MultipleConfig(Generic[T]):
|
||||
if self.file:
|
||||
await self.save()
|
||||
|
||||
async def toDict(
|
||||
self, ignore_multi_config: bool = False, if_decrypt: bool = True
|
||||
) -> Dict[str, Union[list, dict]]:
|
||||
async def toDict(self) -> Dict[str, Union[list, dict]]:
|
||||
"""
|
||||
将配置项转换为字典
|
||||
|
||||
@@ -753,7 +678,7 @@ class MultipleConfig(Generic[T]):
|
||||
]
|
||||
}
|
||||
for uid, config in self.items():
|
||||
data[str(uid)] = await config.toDict(ignore_multi_config, if_decrypt)
|
||||
data[str(uid)] = await config.toDict()
|
||||
return data
|
||||
|
||||
async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
|
||||
@@ -796,7 +721,7 @@ class MultipleConfig(Generic[T]):
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
async def add(self, config_type: Type[T]) -> tuple[uuid.UUID, T]:
|
||||
async def add(self, config_type: type) -> tuple[uuid.UUID, ConfigBase]:
|
||||
"""
|
||||
添加一个新的配置项
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ __author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .ConfigBase import *
|
||||
from .config import *
|
||||
from .schema import *
|
||||
|
||||
__all__ = ["ConfigBase", "config", "schema"]
|
||||
__all__ = ["ConfigBase", "schema"]
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
|
||||
# Copyright © 2025 AUTO-MAS Team
|
||||
|
||||
# This file is part of AUTO-MAS.
|
||||
|
||||
# AUTO-MAS is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO-MAS is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO-MAS. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .ConfigBase import *
|
||||
|
||||
|
||||
class Webhook(ConfigBase):
|
||||
"""Webhook 配置"""
|
||||
|
||||
Info_Name = ConfigItem("Info", "Name", "")
|
||||
Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator())
|
||||
|
||||
Data_Url = ConfigItem("Data", "Url", "", URLValidator())
|
||||
Data_Template = ConfigItem("Data", "Template", "")
|
||||
Data_Headers = ConfigItem("Data", "Headers", "{ }", JSONValidator())
|
||||
Data_Method = ConfigItem(
|
||||
"Data", "Method", "POST", OptionsValidator(["POST", "GET"])
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig(ConfigBase):
|
||||
"""全局配置"""
|
||||
|
||||
Function_HistoryRetentionTime = ConfigItem(
|
||||
"Function",
|
||||
"HistoryRetentionTime",
|
||||
0,
|
||||
OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]),
|
||||
)
|
||||
Function_IfAllowSleep = ConfigItem(
|
||||
"Function", "IfAllowSleep", False, BoolValidator()
|
||||
)
|
||||
Function_IfSilence = ConfigItem("Function", "IfSilence", False, BoolValidator())
|
||||
Function_BossKey = ConfigItem("Function", "BossKey", "")
|
||||
Function_IfAgreeBilibili = ConfigItem(
|
||||
"Function", "IfAgreeBilibili", False, BoolValidator()
|
||||
)
|
||||
Function_IfSkipMumuSplashAds = ConfigItem(
|
||||
"Function", "IfSkipMumuSplashAds", False, BoolValidator()
|
||||
)
|
||||
|
||||
Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator())
|
||||
Voice_Type = ConfigItem(
|
||||
"Voice", "Type", "simple", OptionsValidator(["simple", "noisy"])
|
||||
)
|
||||
|
||||
Start_IfSelfStart = ConfigItem("Start", "IfSelfStart", False, BoolValidator())
|
||||
Start_IfMinimizeDirectly = ConfigItem(
|
||||
"Start", "IfMinimizeDirectly", False, BoolValidator()
|
||||
)
|
||||
|
||||
UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
|
||||
UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
|
||||
|
||||
Notify_SendTaskResultTime = ConfigItem(
|
||||
"Notify",
|
||||
"SendTaskResultTime",
|
||||
"不推送",
|
||||
OptionsValidator(["不推送", "任何时刻", "仅失败时"]),
|
||||
)
|
||||
Notify_IfSendStatistic = ConfigItem(
|
||||
"Notify", "IfSendStatistic", False, BoolValidator()
|
||||
)
|
||||
Notify_IfSendSixStar = ConfigItem("Notify", "IfSendSixStar", False, BoolValidator())
|
||||
Notify_IfPushPlyer = ConfigItem("Notify", "IfPushPlyer", False, BoolValidator())
|
||||
Notify_IfSendMail = ConfigItem("Notify", "IfSendMail", False, BoolValidator())
|
||||
Notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "")
|
||||
Notify_AuthorizationCode = ConfigItem(
|
||||
"Notify", "AuthorizationCode", "", EncryptValidator()
|
||||
)
|
||||
Notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
|
||||
Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
|
||||
Notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator())
|
||||
Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
|
||||
Notify_CustomWebhooks = MultipleConfig([Webhook])
|
||||
|
||||
Update_IfAutoUpdate = ConfigItem("Update", "IfAutoUpdate", False, BoolValidator())
|
||||
Update_Source = ConfigItem(
|
||||
"Update",
|
||||
"Source",
|
||||
"GitHub",
|
||||
OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]),
|
||||
)
|
||||
Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "")
|
||||
Update_MirrorChyanCDK = ConfigItem(
|
||||
"Update", "MirrorChyanCDK", "", EncryptValidator()
|
||||
)
|
||||
|
||||
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
|
||||
Data_LastStatisticsUpload = ConfigItem(
|
||||
"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("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_StageTimeStamp = ConfigItem(
|
||||
"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("%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("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }", JSONValidator())
|
||||
|
||||
|
||||
class QueueItem(ConfigBase):
|
||||
"""队列项配置"""
|
||||
|
||||
related_config: dict[str, MultipleConfig] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_ScriptId = ConfigItem(
|
||||
"Info",
|
||||
"ScriptId",
|
||||
"-",
|
||||
MultipleUIDValidator("-", self.related_config, "ScriptConfig"),
|
||||
)
|
||||
|
||||
|
||||
class TimeSet(ConfigBase):
|
||||
"""时间设置配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Enabled = ConfigItem("Info", "Enabled", False, BoolValidator())
|
||||
self.Info_Time = ConfigItem("Info", "Time", "00:00", DateTimeValidator("%H:%M"))
|
||||
|
||||
|
||||
class QueueConfig(ConfigBase):
|
||||
"""队列配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新队列")
|
||||
self.Info_TimeEnabled = ConfigItem(
|
||||
"Info", "TimeEnabled", False, BoolValidator()
|
||||
)
|
||||
self.Info_StartUpEnabled = ConfigItem(
|
||||
"Info", "StartUpEnabled", False, BoolValidator()
|
||||
)
|
||||
self.Info_AfterAccomplish = ConfigItem(
|
||||
"Info",
|
||||
"AfterAccomplish",
|
||||
"NoAction",
|
||||
OptionsValidator(
|
||||
[
|
||||
"NoAction",
|
||||
"KillSelf",
|
||||
"Sleep",
|
||||
"Hibernate",
|
||||
"Shutdown",
|
||||
"ShutdownForce",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
self.Data_LastTimedStart = ConfigItem(
|
||||
"Data",
|
||||
"LastTimedStart",
|
||||
"2000-01-01 00:00",
|
||||
DateTimeValidator("%Y-%m-%d %H:%M"),
|
||||
)
|
||||
|
||||
self.TimeSet = MultipleConfig([TimeSet])
|
||||
self.QueueItem = MultipleConfig([QueueItem])
|
||||
|
||||
|
||||
class MaaUserConfig(ConfigBase):
|
||||
"""MAA用户配置"""
|
||||
|
||||
related_config: dict[str, MultipleConfig] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
|
||||
self.Info_Id = ConfigItem("Info", "Id", "")
|
||||
self.Info_Mode = ConfigItem(
|
||||
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
|
||||
)
|
||||
self.Info_StageMode = ConfigItem(
|
||||
"Info",
|
||||
"StageMode",
|
||||
"Fixed",
|
||||
MultipleUIDValidator("Fixed", self.related_config, "PlanConfig"),
|
||||
)
|
||||
self.Info_Server = ConfigItem(
|
||||
"Info",
|
||||
"Server",
|
||||
"Official",
|
||||
OptionsValidator(
|
||||
["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"]
|
||||
),
|
||||
)
|
||||
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
|
||||
self.Info_RemainedDay = ConfigItem(
|
||||
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
|
||||
)
|
||||
self.Info_Annihilation = ConfigItem(
|
||||
"Info",
|
||||
"Annihilation",
|
||||
"Annihilation",
|
||||
OptionsValidator(
|
||||
[
|
||||
"Close",
|
||||
"Annihilation",
|
||||
"Chernobog@Annihilation",
|
||||
"LungmenOutskirts@Annihilation",
|
||||
"LungmenDowntown@Annihilation",
|
||||
]
|
||||
),
|
||||
)
|
||||
self.Info_Routine = ConfigItem("Info", "Routine", True, BoolValidator())
|
||||
self.Info_InfrastMode = ConfigItem(
|
||||
"Info",
|
||||
"InfrastMode",
|
||||
"Normal",
|
||||
OptionsValidator(["Normal", "Rotation", "Custom"]),
|
||||
)
|
||||
self.Info_InfrastPath = ConfigItem(
|
||||
"Info", "InfrastPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator())
|
||||
self.Info_Notes = ConfigItem("Info", "Notes", "无")
|
||||
self.Info_MedicineNumb = ConfigItem(
|
||||
"Info", "MedicineNumb", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Info_SeriesNumb = ConfigItem(
|
||||
"Info",
|
||||
"SeriesNumb",
|
||||
"0",
|
||||
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
|
||||
)
|
||||
self.Info_Stage = ConfigItem("Info", "Stage", "-")
|
||||
self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-")
|
||||
self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-")
|
||||
self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-")
|
||||
self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-")
|
||||
self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator())
|
||||
self.Info_SklandToken = ConfigItem(
|
||||
"Info", "SklandToken", "", EncryptValidator()
|
||||
)
|
||||
|
||||
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_ProxyTimes = ConfigItem(
|
||||
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator())
|
||||
self.Data_CustomInfrastPlanIndex = ConfigItem(
|
||||
"Data", "CustomInfrastPlanIndex", "0"
|
||||
)
|
||||
|
||||
self.Task_IfWakeUp = ConfigItem("Task", "IfWakeUp", True, BoolValidator())
|
||||
self.Task_IfRecruiting = ConfigItem(
|
||||
"Task", "IfRecruiting", True, BoolValidator()
|
||||
)
|
||||
self.Task_IfBase = ConfigItem("Task", "IfBase", True, BoolValidator())
|
||||
self.Task_IfCombat = ConfigItem("Task", "IfCombat", True, BoolValidator())
|
||||
self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator())
|
||||
self.Task_IfMission = ConfigItem("Task", "IfMission", True, BoolValidator())
|
||||
self.Task_IfAutoRoguelike = ConfigItem(
|
||||
"Task", "IfAutoRoguelike", False, BoolValidator()
|
||||
)
|
||||
self.Task_IfReclamation = ConfigItem(
|
||||
"Task", "IfReclamation", False, BoolValidator()
|
||||
)
|
||||
|
||||
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
|
||||
self.Notify_IfSendStatistic = ConfigItem(
|
||||
"Notify", "IfSendStatistic", False, BoolValidator()
|
||||
)
|
||||
self.Notify_IfSendSixStar = ConfigItem(
|
||||
"Notify", "IfSendSixStar", False, BoolValidator()
|
||||
)
|
||||
self.Notify_IfSendMail = ConfigItem(
|
||||
"Notify", "IfSendMail", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
|
||||
self.Notify_IfServerChan = ConfigItem(
|
||||
"Notify", "IfServerChan", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
|
||||
self.Notify_CustomWebhooks = MultipleConfig([Webhook])
|
||||
|
||||
|
||||
class MaaConfig(ConfigBase):
|
||||
"""MAA配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本")
|
||||
self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator())
|
||||
|
||||
self.Run_TaskTransitionMethod = ConfigItem(
|
||||
"Run",
|
||||
"TaskTransitionMethod",
|
||||
"ExitEmulator",
|
||||
OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]),
|
||||
)
|
||||
self.Run_ProxyTimesLimit = ConfigItem(
|
||||
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Run_ADBSearchRange = ConfigItem(
|
||||
"Run", "ADBSearchRange", 0, RangeValidator(0, 3)
|
||||
)
|
||||
self.Run_RunTimesLimit = ConfigItem(
|
||||
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_AnnihilationTimeLimit = ConfigItem(
|
||||
"Run", "AnnihilationTimeLimit", 40, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_RoutineTimeLimit = ConfigItem(
|
||||
"Run", "RoutineTimeLimit", 10, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_AnnihilationWeeklyLimit = ConfigItem(
|
||||
"Run", "AnnihilationWeeklyLimit", True, BoolValidator()
|
||||
)
|
||||
|
||||
self.UserData = MultipleConfig([MaaUserConfig])
|
||||
|
||||
|
||||
class MaaPlanConfig(ConfigBase):
|
||||
"""MAA计划表配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新 MAA 计划表")
|
||||
self.Info_Mode = ConfigItem(
|
||||
"Info", "Mode", "ALL", OptionsValidator(["ALL", "Weekly"])
|
||||
)
|
||||
|
||||
self.config_item_dict: dict[str, Dict[str, ConfigItem]] = {}
|
||||
|
||||
for group in [
|
||||
"ALL",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
]:
|
||||
self.config_item_dict[group] = {}
|
||||
|
||||
self.config_item_dict[group]["MedicineNumb"] = ConfigItem(
|
||||
group, "MedicineNumb", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.config_item_dict[group]["SeriesNumb"] = ConfigItem(
|
||||
group,
|
||||
"SeriesNumb",
|
||||
"0",
|
||||
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
|
||||
)
|
||||
self.config_item_dict[group]["Stage"] = ConfigItem(group, "Stage", "-")
|
||||
self.config_item_dict[group]["Stage_1"] = ConfigItem(group, "Stage_1", "-")
|
||||
self.config_item_dict[group]["Stage_2"] = ConfigItem(group, "Stage_2", "-")
|
||||
self.config_item_dict[group]["Stage_3"] = ConfigItem(group, "Stage_3", "-")
|
||||
self.config_item_dict[group]["Stage_Remain"] = ConfigItem(
|
||||
group, "Stage_Remain", "-"
|
||||
)
|
||||
|
||||
for name in [
|
||||
"MedicineNumb",
|
||||
"SeriesNumb",
|
||||
"Stage",
|
||||
"Stage_1",
|
||||
"Stage_2",
|
||||
"Stage_3",
|
||||
"Stage_Remain",
|
||||
]:
|
||||
setattr(self, f"{group}_{name}", self.config_item_dict[group][name])
|
||||
|
||||
def get_current_info(self, name: str) -> ConfigItem:
|
||||
"""获取当前的计划表配置项"""
|
||||
|
||||
if self.get("Info", "Mode") == "ALL":
|
||||
|
||||
return self.config_item_dict["ALL"][name]
|
||||
|
||||
elif self.get("Info", "Mode") == "Weekly":
|
||||
|
||||
dt = datetime.now()
|
||||
if dt.time() < datetime.min.time().replace(hour=4):
|
||||
dt = dt - timedelta(days=1)
|
||||
today = dt.strftime("%A")
|
||||
|
||||
if today in self.config_item_dict:
|
||||
return self.config_item_dict[today][name]
|
||||
else:
|
||||
return self.config_item_dict["ALL"][name]
|
||||
|
||||
else:
|
||||
raise ValueError("非法的计划表模式")
|
||||
|
||||
|
||||
class GeneralUserConfig(ConfigBase):
|
||||
"""通用脚本用户配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
|
||||
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
|
||||
self.Info_RemainedDay = ConfigItem(
|
||||
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
|
||||
)
|
||||
self.Info_IfScriptBeforeTask = ConfigItem(
|
||||
"Info", "IfScriptBeforeTask", False, BoolValidator()
|
||||
)
|
||||
self.Info_ScriptBeforeTask = ConfigItem(
|
||||
"Info", "ScriptBeforeTask", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Info_IfScriptAfterTask = ConfigItem(
|
||||
"Info", "IfScriptAfterTask", False, BoolValidator()
|
||||
)
|
||||
self.Info_ScriptAfterTask = ConfigItem(
|
||||
"Info", "ScriptAfterTask", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Info_Notes = ConfigItem("Info", "Notes", "无")
|
||||
|
||||
self.Data_LastProxyDate = ConfigItem(
|
||||
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
|
||||
)
|
||||
self.Data_ProxyTimes = ConfigItem(
|
||||
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
|
||||
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
|
||||
self.Notify_IfSendStatistic = ConfigItem(
|
||||
"Notify", "IfSendStatistic", False, BoolValidator()
|
||||
)
|
||||
self.Notify_IfSendMail = ConfigItem(
|
||||
"Notify", "IfSendMail", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
|
||||
self.Notify_IfServerChan = ConfigItem(
|
||||
"Notify", "IfServerChan", False, BoolValidator()
|
||||
)
|
||||
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
|
||||
self.Notify_CustomWebhooks = MultipleConfig([Webhook])
|
||||
|
||||
|
||||
class GeneralConfig(ConfigBase):
|
||||
"""通用配置"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.Info_Name = ConfigItem("Info", "Name", "新通用脚本")
|
||||
self.Info_RootPath = ConfigItem(
|
||||
"Info", "RootPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
|
||||
self.Script_ScriptPath = ConfigItem(
|
||||
"Script", "ScriptPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Script_Arguments = ConfigItem("Script", "Arguments", "")
|
||||
self.Script_IfTrackProcess = ConfigItem(
|
||||
"Script", "IfTrackProcess", False, BoolValidator()
|
||||
)
|
||||
self.Script_ConfigPath = ConfigItem(
|
||||
"Script", "ConfigPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Script_ConfigPathMode = ConfigItem(
|
||||
"Script", "ConfigPathMode", "File", OptionsValidator(["File", "Folder"])
|
||||
)
|
||||
self.Script_UpdateConfigMode = ConfigItem(
|
||||
"Script",
|
||||
"UpdateConfigMode",
|
||||
"Never",
|
||||
OptionsValidator(["Never", "Success", "Failure", "Always"]),
|
||||
)
|
||||
self.Script_LogPath = ConfigItem(
|
||||
"Script", "LogPath", str(Path.cwd()), FileValidator()
|
||||
)
|
||||
self.Script_LogPathFormat = ConfigItem("Script", "LogPathFormat", "%Y-%m-%d")
|
||||
self.Script_LogTimeStart = ConfigItem(
|
||||
"Script", "LogTimeStart", 1, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Script_LogTimeEnd = ConfigItem(
|
||||
"Script", "LogTimeEnd", 1, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Script_LogTimeFormat = ConfigItem(
|
||||
"Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
self.Script_SuccessLog = ConfigItem("Script", "SuccessLog", "")
|
||||
self.Script_ErrorLog = ConfigItem("Script", "ErrorLog", "")
|
||||
|
||||
self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator())
|
||||
self.Game_Type = ConfigItem(
|
||||
"Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client"])
|
||||
)
|
||||
self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator())
|
||||
self.Game_Arguments = ConfigItem("Game", "Arguments", "")
|
||||
self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999))
|
||||
self.Game_IfForceClose = ConfigItem(
|
||||
"Game", "IfForceClose", False, BoolValidator()
|
||||
)
|
||||
|
||||
self.Run_ProxyTimesLimit = ConfigItem(
|
||||
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
|
||||
)
|
||||
self.Run_RunTimesLimit = ConfigItem(
|
||||
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
|
||||
)
|
||||
self.Run_RunTimeLimit = ConfigItem(
|
||||
"Run", "RunTimeLimit", 10, RangeValidator(1, 9999)
|
||||
)
|
||||
|
||||
self.UserData = MultipleConfig([GeneralUserConfig])
|
||||
|
||||
|
||||
CLASS_BOOK = {"MAA": MaaConfig, "MaaPlan": MaaPlanConfig, "General": GeneralConfig}
|
||||
"""配置类映射表"""
|
||||
@@ -75,30 +75,6 @@ class GetStageIn(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class WebhookIndexItem(BaseModel):
|
||||
uid: str = Field(..., description="唯一标识符")
|
||||
type: Literal["Webhook"] = Field(..., description="配置类型")
|
||||
|
||||
|
||||
class Webhook_Info(BaseModel):
|
||||
Name: Optional[str] = Field(default=None, description="Webhook名称")
|
||||
Enabled: Optional[bool] = Field(default=None, description="是否启用")
|
||||
|
||||
|
||||
class Webhook_Data(BaseModel):
|
||||
Url: Optional[str] = Field(default=None, description="Webhook URL")
|
||||
Template: Optional[str] = Field(default=None, description="消息模板")
|
||||
Headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
|
||||
Method: Optional[Literal["POST", "GET"]] = Field(
|
||||
default=None, description="请求方法"
|
||||
)
|
||||
|
||||
|
||||
class Webhook(BaseModel):
|
||||
Info: Optional[Webhook_Info] = Field(default=None, description="Webhook基础信息")
|
||||
Data: Optional[Webhook_Data] = Field(default=None, description="Webhook配置数据")
|
||||
|
||||
|
||||
class GlobalConfig_Function(BaseModel):
|
||||
HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
|
||||
None, description="历史记录保留时间, 0表示永久保存"
|
||||
@@ -135,6 +111,18 @@ class GlobalConfig_UI(BaseModel):
|
||||
IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘")
|
||||
|
||||
|
||||
class CustomWebhook(BaseModel):
|
||||
id: str = Field(..., description="Webhook唯一标识")
|
||||
name: str = Field(..., description="Webhook名称")
|
||||
url: str = Field(..., description="Webhook URL")
|
||||
template: str = Field(..., description="消息模板")
|
||||
enabled: bool = Field(default=True, description="是否启用")
|
||||
headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
|
||||
method: Optional[Literal["POST", "GET"]] = Field(
|
||||
default="POST", description="请求方法"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Notify(BaseModel):
|
||||
SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
|
||||
default=None, description="任务结果推送时机"
|
||||
@@ -155,6 +143,9 @@ class GlobalConfig_Notify(BaseModel):
|
||||
default=None, description="是否使用ServerChan推送"
|
||||
)
|
||||
ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥")
|
||||
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
|
||||
default=None, description="自定义Webhook列表"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfig_Update(BaseModel):
|
||||
@@ -322,6 +313,9 @@ class MaaUserConfig_Notify(BaseModel):
|
||||
default=None, description="是否使用Server酱推送"
|
||||
)
|
||||
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
|
||||
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
|
||||
default=None, description="用户自定义Webhook列表"
|
||||
)
|
||||
|
||||
|
||||
class GeneralUserConfig_Notify(BaseModel):
|
||||
@@ -624,50 +618,6 @@ class UserSetIn(UserInBase):
|
||||
jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件")
|
||||
|
||||
|
||||
class WebhookInBase(BaseModel):
|
||||
scriptId: Optional[str] = Field(
|
||||
default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带"
|
||||
)
|
||||
userId: Optional[str] = Field(
|
||||
default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带"
|
||||
)
|
||||
|
||||
|
||||
class WebhookGetIn(WebhookInBase):
|
||||
webhookId: Optional[str] = Field(
|
||||
default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据"
|
||||
)
|
||||
|
||||
|
||||
class WebhookGetOut(OutBase):
|
||||
index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表")
|
||||
data: Dict[str, Webhook] = Field(
|
||||
..., description="Webhook数据字典, key来自于index列表的uid"
|
||||
)
|
||||
|
||||
|
||||
class WebhookCreateOut(OutBase):
|
||||
webhookId: str = Field(..., description="新创建的Webhook ID")
|
||||
data: Webhook = Field(..., description="Webhook配置数据")
|
||||
|
||||
|
||||
class WebhookUpdateIn(WebhookInBase):
|
||||
webhookId: str = Field(..., description="Webhook ID")
|
||||
data: Webhook = Field(..., description="Webhook更新数据")
|
||||
|
||||
|
||||
class WebhookDeleteIn(WebhookInBase):
|
||||
webhookId: str = Field(..., description="Webhook ID")
|
||||
|
||||
|
||||
class WebhookReorderIn(WebhookInBase):
|
||||
indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列")
|
||||
|
||||
|
||||
class WebhookTestIn(WebhookInBase):
|
||||
data: Webhook = Field(..., description="Webhook配置数据")
|
||||
|
||||
|
||||
class PlanCreateIn(BaseModel):
|
||||
type: Literal["MaaPlan"]
|
||||
|
||||
|
||||
@@ -21,20 +21,17 @@
|
||||
|
||||
|
||||
import re
|
||||
import json
|
||||
import smtplib
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from plyer import notification
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from plyer import notification
|
||||
|
||||
from app.core import Config
|
||||
from app.models.config import Webhook
|
||||
from app.utils import get_logger, ImageUtils
|
||||
|
||||
logger = get_logger("通知服务")
|
||||
@@ -42,76 +39,71 @@ logger = get_logger("通知服务")
|
||||
|
||||
class Notification:
|
||||
|
||||
async def push_plyer(self, title: str, message: str, ticker: str, t: int) -> None:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
async def push_plyer(self, title, message, ticker, t) -> bool:
|
||||
"""
|
||||
推送系统通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
通知标题
|
||||
message: str
|
||||
通知内容
|
||||
ticker: str
|
||||
通知横幅
|
||||
t: int
|
||||
通知持续时间
|
||||
:param title: 通知标题
|
||||
:param message: 通知内容
|
||||
:param ticker: 通知横幅
|
||||
:param t: 通知持续时间
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if not Config.get("Notify", "IfPushPlyer"):
|
||||
return
|
||||
if Config.get("Notify", "IfPushPlyer"):
|
||||
|
||||
logger.info(f"推送系统通知: {title}")
|
||||
logger.info(f"推送系统通知: {title}")
|
||||
|
||||
if notification.notify is not None:
|
||||
notification.notify(
|
||||
title=title,
|
||||
message=message,
|
||||
app_name="AUTO-MAS",
|
||||
app_icon=(Path.cwd() / "res/icons/AUTO-MAS.ico").as_posix(),
|
||||
timeout=t,
|
||||
ticker=ticker,
|
||||
toast=True,
|
||||
)
|
||||
else:
|
||||
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
|
||||
if notification.notify is not None:
|
||||
notification.notify(
|
||||
title=title,
|
||||
message=message,
|
||||
app_name="AUTO-MAS",
|
||||
app_icon=(Path.cwd() / "res/icons/AUTO-MAS.ico").as_posix(),
|
||||
timeout=t,
|
||||
ticker=ticker,
|
||||
toast=True,
|
||||
)
|
||||
else:
|
||||
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
|
||||
|
||||
async def send_mail(
|
||||
self, mode: Literal["文本", "网页"], title: str, content: str, to_address: str
|
||||
) -> None:
|
||||
return True
|
||||
|
||||
async def send_mail(self, mode, title, content, to_address) -> None:
|
||||
"""
|
||||
推送邮件通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode: Literal["文本", "网页"]
|
||||
邮件内容模式, 支持 "文本" 和 "网页"
|
||||
title: str
|
||||
邮件标题
|
||||
content: str
|
||||
邮件内容
|
||||
to_address: str
|
||||
收件人地址
|
||||
:param mode: 邮件内容模式, 支持 "文本" 和 "网页"
|
||||
:param title: 邮件标题
|
||||
:param content: 邮件内容
|
||||
:param to_address: 收件人地址
|
||||
"""
|
||||
|
||||
if Config.get("Notify", "SMTPServerAddress") == "":
|
||||
raise ValueError("邮件通知的SMTP服务器地址不能为空")
|
||||
if Config.get("Notify", "AuthorizationCode") == "":
|
||||
raise ValueError("邮件通知的授权码不能为空")
|
||||
if not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get("Notify", "FromAddress"),
|
||||
if (
|
||||
Config.get("Notify", "SMTPServerAddress") == ""
|
||||
or Config.get("Notify", "AuthorizationCode") == ""
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get("Notify", "FromAddress"),
|
||||
)
|
||||
)
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
to_address,
|
||||
)
|
||||
)
|
||||
):
|
||||
raise ValueError("邮件通知的发送邮箱格式错误或为空")
|
||||
if not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
to_address,
|
||||
logger.error(
|
||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
|
||||
)
|
||||
raise ValueError(
|
||||
"邮件通知的SMTP服务器地址、授权码、发件人地址或收件人地址未正确配置"
|
||||
)
|
||||
):
|
||||
raise ValueError("邮件通知的接收邮箱格式错误或为空")
|
||||
|
||||
# 定义邮件正文
|
||||
if mode == "文本":
|
||||
@@ -143,21 +135,16 @@ class Notification:
|
||||
smtpObj.quit()
|
||||
logger.success(f"邮件发送成功: {title}")
|
||||
|
||||
async def ServerChanPush(self, title: str, content: str, send_key: str) -> None:
|
||||
async def ServerChanPush(self, title, content, send_key) -> None:
|
||||
"""
|
||||
使用Server酱推送通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
通知标题
|
||||
content: str
|
||||
通知内容
|
||||
send_key: str
|
||||
Server酱的SendKey
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param send_key: Server酱的SendKey
|
||||
"""
|
||||
|
||||
if send_key == "":
|
||||
if not send_key:
|
||||
raise ValueError("ServerChan SendKey 不能为空")
|
||||
|
||||
# 构造 URL
|
||||
@@ -184,33 +171,33 @@ class Notification:
|
||||
else:
|
||||
raise Exception(f"ServerChan 推送通知失败: {response.text}")
|
||||
|
||||
async def WebhookPush(self, title: str, content: str, webhook: Webhook) -> None:
|
||||
async def CustomWebhookPush(self, title, content, webhook_config) -> None:
|
||||
"""
|
||||
Webhook 推送通知
|
||||
自定义 Webhook 推送通知
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
通知标题
|
||||
content: str
|
||||
通知内容
|
||||
webhook: Webhook
|
||||
Webhook配置对象
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param webhook_config: Webhook配置对象
|
||||
"""
|
||||
if not webhook.get("Info", "Enabled"):
|
||||
return
|
||||
|
||||
if webhook.get("Data", "Url") == "":
|
||||
if not webhook_config.get("url"):
|
||||
raise ValueError("Webhook URL 不能为空")
|
||||
|
||||
if not webhook_config.get("enabled", True):
|
||||
logger.info(
|
||||
f"Webhook {webhook_config.get('name', 'Unknown')} 已禁用,跳过推送"
|
||||
)
|
||||
return
|
||||
|
||||
# 解析模板
|
||||
template = (
|
||||
webhook.get("Data", "Template")
|
||||
or '{"title": "{title}", "content": "{content}"}'
|
||||
template = webhook_config.get(
|
||||
"template", '{"title": "{title}", "content": "{content}"}'
|
||||
)
|
||||
|
||||
# 替换模板变量
|
||||
try:
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 准备模板变量
|
||||
template_vars = {
|
||||
@@ -277,55 +264,56 @@ class Notification:
|
||||
|
||||
# 准备请求头
|
||||
headers = {"Content-Type": "application/json"}
|
||||
headers.update(json.loads(webhook.get("Data", "Headers")))
|
||||
if webhook_config.get("headers"):
|
||||
headers.update(webhook_config["headers"])
|
||||
|
||||
if webhook.get("Data", "Method") == "POST":
|
||||
if isinstance(data, dict):
|
||||
response = requests.post(
|
||||
url=webhook.get("Data", "Url"),
|
||||
json=data,
|
||||
# 发送请求
|
||||
method = webhook_config.get("method", "POST").upper()
|
||||
|
||||
try:
|
||||
if method == "POST":
|
||||
if isinstance(data, dict):
|
||||
response = requests.post(
|
||||
url=webhook_config["url"],
|
||||
json=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
else:
|
||||
response = requests.post(
|
||||
url=webhook_config["url"],
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
else: # GET
|
||||
params = data if isinstance(data, dict) else {"message": data}
|
||||
response = requests.get(
|
||||
url=webhook_config["url"],
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
)
|
||||
elif isinstance(data, str):
|
||||
response = requests.post(
|
||||
url=webhook.get("Data", "Url"),
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
logger.success(
|
||||
f"自定义Webhook推送成功: {webhook_config.get('name', 'Unknown')} - {title}"
|
||||
)
|
||||
elif webhook.get("Data", "Method") == "GET":
|
||||
if isinstance(data, dict):
|
||||
# Flatten params to ensure all values are str or list of str
|
||||
params = {}
|
||||
for k, v in data.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
params[k] = json.dumps(v, ensure_ascii=False)
|
||||
else:
|
||||
params[k] = str(v)
|
||||
else:
|
||||
params = {"message": str(data)}
|
||||
response = requests.get(
|
||||
url=webhook.get("Data", "Url"),
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies=Config.get_proxies(),
|
||||
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"自定义Webhook推送失败 ({webhook_config.get('name', 'Unknown')}): {str(e)}"
|
||||
)
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
logger.success(
|
||||
f"自定义Webhook推送成功: {webhook.get('Info', 'Name')} - {title}"
|
||||
)
|
||||
else:
|
||||
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
||||
|
||||
async def _WebHookPush(self, title, content, webhook_url) -> None:
|
||||
async def WebHookPush(self, title, content, webhook_url) -> None:
|
||||
"""
|
||||
WebHook 推送通知 (即将弃用)
|
||||
WebHook 推送通知 (兼容旧版企业微信格式)
|
||||
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
@@ -352,7 +340,7 @@ class Notification:
|
||||
self, image_path: Path, webhook_url: str
|
||||
) -> None:
|
||||
"""
|
||||
使用企业微信群机器人推送图片通知(等待重新适配)
|
||||
使用企业微信群机器人推送图片通知
|
||||
|
||||
:param image_path: 图片文件路径
|
||||
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||
@@ -418,12 +406,23 @@ class Notification:
|
||||
)
|
||||
|
||||
# 发送自定义Webhook通知
|
||||
for webhook in Config.Notify_CustomWebhooks.values():
|
||||
await self.WebhookPush(
|
||||
"AUTO-MAS测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
webhook,
|
||||
)
|
||||
try:
|
||||
custom_webhooks = Config.get("Notify", "CustomWebhooks")
|
||||
except AttributeError:
|
||||
custom_webhooks = []
|
||||
if custom_webhooks:
|
||||
for webhook in custom_webhooks:
|
||||
if webhook.get("enabled", True):
|
||||
try:
|
||||
await self.CustomWebhookPush(
|
||||
"AUTO-MAS测试通知",
|
||||
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||
webhook,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"自定义Webhook测试失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||
)
|
||||
|
||||
logger.success("测试通知发送完成")
|
||||
|
||||
|
||||
1344
app/task/MAA.py
1344
app/task/MAA.py
File diff suppressed because it is too large
Load Diff
@@ -27,9 +27,10 @@ import shutil
|
||||
import asyncio
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from fastapi import WebSocket
|
||||
from datetime import datetime, timedelta
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from typing import List, Dict, Optional
|
||||
from typing import Union, List, Dict, Optional
|
||||
|
||||
|
||||
from app.core import Config, GeneralConfig, GeneralUserConfig
|
||||
@@ -43,7 +44,7 @@ logger = get_logger("通用调度器")
|
||||
|
||||
|
||||
class GeneralManager:
|
||||
"""通用脚本调度器"""
|
||||
"""通用脚本通用控制器"""
|
||||
|
||||
def __init__(
|
||||
self, mode: str, script_id: uuid.UUID, user_id: Optional[uuid.UUID], ws_id: str
|
||||
@@ -68,8 +69,9 @@ class GeneralManager:
|
||||
await Config.ScriptConfig[self.script_id].lock()
|
||||
|
||||
self.script_config = Config.ScriptConfig[self.script_id]
|
||||
self.user_config = MultipleConfig([GeneralUserConfig])
|
||||
await self.user_config.load(await self.script_config.UserData.toDict())
|
||||
if isinstance(self.script_config, GeneralConfig):
|
||||
self.user_config = MultipleConfig([GeneralUserConfig])
|
||||
await self.user_config.load(await self.script_config.UserData.toDict())
|
||||
|
||||
self.script_root_path = Path(self.script_config.get("Info", "RootPath"))
|
||||
self.script_path = Path(self.script_config.get("Script", "ScriptPath"))
|
||||
@@ -141,9 +143,6 @@ class GeneralManager:
|
||||
def check_config(self) -> str:
|
||||
"""检查配置是否可用"""
|
||||
|
||||
if not isinstance(Config.ScriptConfig[self.script_id], GeneralConfig):
|
||||
return "脚本配置类型错误, 不是通用脚本类型"
|
||||
|
||||
if self.mode == "人工排查":
|
||||
return "通用脚本不支持人工排查模式"
|
||||
if self.mode == "设置脚本" and self.user_id is None:
|
||||
@@ -222,319 +221,298 @@ class GeneralManager:
|
||||
# 开始代理
|
||||
for self.index, user in enumerate(self.user_list):
|
||||
|
||||
try:
|
||||
self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
|
||||
|
||||
self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
|
||||
if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
|
||||
self.cur_user_data.get("Data", "ProxyTimes")
|
||||
< self.script_config.get("Run", "ProxyTimesLimit")
|
||||
):
|
||||
user["status"] = "运行"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={"user_list": self.user_list},
|
||||
).model_dump()
|
||||
)
|
||||
else:
|
||||
user["status"] = "跳过"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={"user_list": self.user_list},
|
||||
).model_dump()
|
||||
)
|
||||
continue
|
||||
|
||||
if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
|
||||
self.cur_user_data.get("Data", "ProxyTimes")
|
||||
< self.script_config.get("Run", "ProxyTimesLimit")
|
||||
):
|
||||
user["status"] = "运行"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={"user_list": self.user_list},
|
||||
).model_dump()
|
||||
)
|
||||
else:
|
||||
user["status"] = "跳过"
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={"user_list": self.user_list},
|
||||
).model_dump()
|
||||
)
|
||||
continue
|
||||
logger.info(f"开始代理用户: {user['user_id']}")
|
||||
|
||||
logger.info(f"开始代理用户: {user['user_id']}")
|
||||
self.user_start_time = datetime.now()
|
||||
|
||||
self.user_start_time = datetime.now()
|
||||
self.run_book = False
|
||||
|
||||
self.run_book = False
|
||||
if not (
|
||||
Path.cwd() / f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||
).exists():
|
||||
|
||||
if not (
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||
).exists():
|
||||
|
||||
logger.error(f"用户: {user['user_id']} - 未找到配置文件")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Info",
|
||||
data={"Error": f"未找到 {user['user_id']} 的配置文件"},
|
||||
).model_dump()
|
||||
)
|
||||
self.run_book = False
|
||||
continue
|
||||
|
||||
# 尝试次数循环
|
||||
for i in range(self.script_config.get("Run", "RunTimesLimit")):
|
||||
|
||||
if self.run_book:
|
||||
break
|
||||
|
||||
logger.info(
|
||||
f"用户 {user['user_id']} - 尝试次数: {i + 1}/{self.script_config.get('Run','RunTimesLimit')}"
|
||||
)
|
||||
|
||||
# 配置脚本
|
||||
await self.set_general()
|
||||
# 记录当前时间
|
||||
self.log_start_time = datetime.now()
|
||||
|
||||
# 执行任务前脚本
|
||||
if (
|
||||
self.cur_user_data.get("Info", "IfScriptBeforeTask")
|
||||
and Path(
|
||||
self.cur_user_data.get("Info", "ScriptBeforeTask")
|
||||
).exists()
|
||||
):
|
||||
await self.execute_script_task(
|
||||
Path(
|
||||
self.cur_user_data.get("Info", "ScriptBeforeTask")
|
||||
),
|
||||
"脚本前任务",
|
||||
)
|
||||
|
||||
# 启动游戏/模拟器
|
||||
if self.script_config.get("Game", "Enabled"):
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"启动游戏/模拟器: {self.game_path}, 参数: {self.script_config.get('Game','Arguments')}"
|
||||
)
|
||||
await self.game_process_manager.open_process(
|
||||
self.game_path,
|
||||
str(
|
||||
self.script_config.get("Game", "Arguments")
|
||||
).split(" "),
|
||||
0,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"启动游戏/模拟器时出现异常: {e}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Info",
|
||||
data={
|
||||
"Error": f"启动游戏/模拟器时出现异常: {e}"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
self.general_result = "游戏/模拟器启动失败"
|
||||
break
|
||||
|
||||
# 更新静默进程标记有效时间
|
||||
if self.script_config.get("Game", "Type") == "Emulator":
|
||||
logger.info(
|
||||
f"更新静默进程标记: {self.game_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.script_config.get('Game', 'WaitTime') + 10)}"
|
||||
)
|
||||
Config.silence_dict[
|
||||
self.game_path
|
||||
] = datetime.now() + timedelta(
|
||||
seconds=self.script_config.get("Game", "WaitTime")
|
||||
+ 10
|
||||
)
|
||||
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={
|
||||
"log": f"正在等待游戏/模拟器完成启动\n请等待{self.script_config.get('Game', 'WaitTime')}s"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
await asyncio.sleep(
|
||||
self.script_config.get("Game", "WaitTime")
|
||||
)
|
||||
|
||||
# 运行脚本任务
|
||||
logger.info(
|
||||
f"运行脚本任务: {self.script_exe_path}, 参数: {self.script_arguments}"
|
||||
)
|
||||
await self.general_process_manager.open_process(
|
||||
self.script_exe_path,
|
||||
self.script_arguments,
|
||||
tracking_time=(
|
||||
60
|
||||
if self.script_config.get("Script", "IfTrackProcess")
|
||||
else 0
|
||||
),
|
||||
)
|
||||
|
||||
# 监测运行状态
|
||||
await self.general_log_monitor.start(
|
||||
self.script_log_path, self.log_start_time
|
||||
)
|
||||
self.wait_event.clear()
|
||||
await self.wait_event.wait()
|
||||
|
||||
await self.general_log_monitor.stop()
|
||||
|
||||
# 处理通用脚本结果
|
||||
if self.general_result == "Success!":
|
||||
|
||||
# 标记任务完成
|
||||
self.run_book = True
|
||||
|
||||
logger.info(
|
||||
f"用户: {user['user_id']} - 通用脚本进程完成代理任务"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={
|
||||
"log": "检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# 中止相关程序
|
||||
logger.info(f"中止相关程序: {self.script_exe_path}")
|
||||
await self.general_process_manager.kill()
|
||||
await System.kill_process(self.script_exe_path)
|
||||
if self.script_config.get("Game", "Enabled"):
|
||||
logger.info(
|
||||
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
|
||||
)
|
||||
await self.game_process_manager.kill()
|
||||
if self.script_config.get("Game", "IfForceClose"):
|
||||
await System.kill_process(self.game_path)
|
||||
|
||||
await asyncio.sleep(10)
|
||||
|
||||
# 更新脚本配置文件
|
||||
if self.script_config.get("Script", "UpdateConfigMode") in [
|
||||
"Success",
|
||||
"Always",
|
||||
]:
|
||||
|
||||
if (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "Folder"
|
||||
):
|
||||
shutil.copytree(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
elif (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "File"
|
||||
):
|
||||
shutil.copy(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||
/ self.script_config_path.name,
|
||||
)
|
||||
logger.success("通用脚本配置文件已更新")
|
||||
|
||||
else:
|
||||
logger.error(
|
||||
f"配置: {user['user_id']} - 代理任务异常: {self.general_result}"
|
||||
)
|
||||
# 打印中止信息
|
||||
# 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={
|
||||
"log": f"{self.general_result}\n正在中止相关程序\n请等待10s"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# 中止相关程序
|
||||
logger.info(f"中止相关程序: {self.script_exe_path}")
|
||||
await self.general_process_manager.kill()
|
||||
await System.kill_process(self.script_exe_path)
|
||||
if self.script_config.get("Game", "Enabled"):
|
||||
logger.info(
|
||||
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
|
||||
)
|
||||
await self.game_process_manager.kill()
|
||||
if self.script_config.get("Game", "IfForceClose"):
|
||||
await System.kill_process(self.game_path)
|
||||
|
||||
# 推送异常通知
|
||||
await Notify.push_plyer(
|
||||
"用户自动代理出现异常!",
|
||||
f"用户 {user['name']} 的自动代理出现一次异常",
|
||||
f"{user['name']} 的自动代理出现异常",
|
||||
3,
|
||||
)
|
||||
|
||||
await asyncio.sleep(10)
|
||||
|
||||
# 更新脚本配置文件
|
||||
if self.script_config.get("Script", "UpdateConfigMode") in [
|
||||
"Failure",
|
||||
"Always",
|
||||
]:
|
||||
|
||||
if (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "Folder"
|
||||
):
|
||||
shutil.copytree(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
elif (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "File"
|
||||
):
|
||||
shutil.copy(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||
/ self.script_config_path.name,
|
||||
)
|
||||
logger.success("通用脚本配置文件已更新")
|
||||
|
||||
# 执行任务后脚本
|
||||
if (
|
||||
self.cur_user_data.get("Info", "IfScriptAfterTask")
|
||||
and Path(
|
||||
self.cur_user_data.get("Info", "ScriptAfterTask")
|
||||
).exists()
|
||||
):
|
||||
await self.execute_script_task(
|
||||
Path(self.cur_user_data.get("Info", "ScriptAfterTask")),
|
||||
"脚本后任务",
|
||||
)
|
||||
|
||||
# 保存运行日志以及统计信息
|
||||
await Config.save_general_log(
|
||||
Path.cwd()
|
||||
/ f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
|
||||
self.general_logs,
|
||||
self.general_result,
|
||||
)
|
||||
|
||||
await self.result_record()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"用户 {user['user_id']} 代理时出现异常: {e}")
|
||||
user["status"] = "异常"
|
||||
logger.error(f"用户: {user['user_id']} - 未找到配置文件")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Info",
|
||||
data={"Error": f"代理用户 {user['name']} 时出现异常: {e}"},
|
||||
data={"Error": f"未找到 {user['user_id']} 的配置文件"},
|
||||
).model_dump()
|
||||
)
|
||||
self.run_book = False
|
||||
continue
|
||||
|
||||
# 尝试次数循环
|
||||
for i in range(self.script_config.get("Run", "RunTimesLimit")):
|
||||
|
||||
if self.run_book:
|
||||
break
|
||||
|
||||
logger.info(
|
||||
f"用户 {user['user_id']} - 尝试次数: {i + 1}/{self.script_config.get('Run','RunTimesLimit')}"
|
||||
)
|
||||
|
||||
# 配置脚本
|
||||
await self.set_general()
|
||||
# 记录当前时间
|
||||
self.log_start_time = datetime.now()
|
||||
|
||||
# 执行任务前脚本
|
||||
if (
|
||||
self.cur_user_data.get("Info", "IfScriptBeforeTask")
|
||||
and Path(
|
||||
self.cur_user_data.get("Info", "ScriptBeforeTask")
|
||||
).exists()
|
||||
):
|
||||
await self.execute_script_task(
|
||||
Path(self.cur_user_data.get("Info", "ScriptBeforeTask")),
|
||||
"脚本前任务",
|
||||
)
|
||||
|
||||
# 启动游戏/模拟器
|
||||
if self.script_config.get("Game", "Enabled"):
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"启动游戏/模拟器: {self.game_path}, 参数: {self.script_config.get('Game','Arguments')}"
|
||||
)
|
||||
await self.game_process_manager.open_process(
|
||||
self.game_path,
|
||||
str(self.script_config.get("Game", "Arguments")).split(
|
||||
" "
|
||||
),
|
||||
0,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"启动游戏/模拟器时出现异常: {e}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Info",
|
||||
data={"Error": f"启动游戏/模拟器时出现异常: {e}"},
|
||||
).model_dump()
|
||||
)
|
||||
self.general_result = "游戏/模拟器启动失败"
|
||||
break
|
||||
|
||||
# 更新静默进程标记有效时间
|
||||
if self.script_config.get("Game", "Type") == "Emulator":
|
||||
logger.info(
|
||||
f"更新静默进程标记: {self.game_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.script_config.get('Game', 'WaitTime') + 10)}"
|
||||
)
|
||||
Config.silence_dict[
|
||||
self.game_path
|
||||
] = datetime.now() + timedelta(
|
||||
seconds=self.script_config.get("Game", "WaitTime") + 10
|
||||
)
|
||||
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={
|
||||
"log": f"正在等待游戏/模拟器完成启动\n请等待{self.script_config.get('Game', 'WaitTime')}s"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
await asyncio.sleep(self.script_config.get("Game", "WaitTime"))
|
||||
|
||||
# 运行脚本任务
|
||||
logger.info(
|
||||
f"运行脚本任务: {self.script_exe_path}, 参数: {self.script_arguments}"
|
||||
)
|
||||
await self.general_process_manager.open_process(
|
||||
self.script_exe_path,
|
||||
self.script_arguments,
|
||||
tracking_time=(
|
||||
60
|
||||
if self.script_config.get("Script", "IfTrackProcess")
|
||||
else 0
|
||||
),
|
||||
)
|
||||
|
||||
# 监测运行状态
|
||||
await self.general_log_monitor.start(
|
||||
self.script_log_path, self.log_start_time
|
||||
)
|
||||
self.wait_event.clear()
|
||||
await self.wait_event.wait()
|
||||
|
||||
await self.general_log_monitor.stop()
|
||||
|
||||
# 处理通用脚本结果
|
||||
if self.general_result == "Success!":
|
||||
|
||||
# 标记任务完成
|
||||
self.run_book = True
|
||||
|
||||
logger.info(
|
||||
f"用户: {user['user_id']} - 通用脚本进程完成代理任务"
|
||||
)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={
|
||||
"log": "检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# 中止相关程序
|
||||
logger.info(f"中止相关程序: {self.script_exe_path}")
|
||||
await self.general_process_manager.kill()
|
||||
await System.kill_process(self.script_exe_path)
|
||||
if self.script_config.get("Game", "Enabled"):
|
||||
logger.info(
|
||||
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
|
||||
)
|
||||
await self.game_process_manager.kill()
|
||||
if self.script_config.get("Game", "IfForceClose"):
|
||||
await System.kill_process(self.game_path)
|
||||
|
||||
await asyncio.sleep(10)
|
||||
|
||||
# 更新脚本配置文件
|
||||
if self.script_config.get("Script", "UpdateConfigMode") in [
|
||||
"Success",
|
||||
"Always",
|
||||
]:
|
||||
|
||||
if (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "Folder"
|
||||
):
|
||||
shutil.copytree(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
elif (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "File"
|
||||
):
|
||||
shutil.copy(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||
/ self.script_config_path.name,
|
||||
)
|
||||
logger.success("通用脚本配置文件已更新")
|
||||
|
||||
else:
|
||||
logger.error(
|
||||
f"配置: {user['user_id']} - 代理任务异常: {self.general_result}"
|
||||
)
|
||||
# 打印中止信息
|
||||
# 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Update",
|
||||
data={
|
||||
"log": f"{self.general_result}\n正在中止相关程序\n请等待10s"
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# 中止相关程序
|
||||
logger.info(f"中止相关程序: {self.script_exe_path}")
|
||||
await self.general_process_manager.kill()
|
||||
await System.kill_process(self.script_exe_path)
|
||||
if self.script_config.get("Game", "Enabled"):
|
||||
logger.info(
|
||||
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
|
||||
)
|
||||
await self.game_process_manager.kill()
|
||||
if self.script_config.get("Game", "IfForceClose"):
|
||||
await System.kill_process(self.game_path)
|
||||
|
||||
# 推送异常通知
|
||||
await Notify.push_plyer(
|
||||
"用户自动代理出现异常!",
|
||||
f"用户 {user['name']} 的自动代理出现一次异常",
|
||||
f"{user['name']} 的自动代理出现异常",
|
||||
3,
|
||||
)
|
||||
|
||||
await asyncio.sleep(10)
|
||||
|
||||
# 更新脚本配置文件
|
||||
if self.script_config.get("Script", "UpdateConfigMode") in [
|
||||
"Failure",
|
||||
"Always",
|
||||
]:
|
||||
|
||||
if (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "Folder"
|
||||
):
|
||||
shutil.copytree(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
elif (
|
||||
self.script_config.get("Script", "ConfigPathMode")
|
||||
== "File"
|
||||
):
|
||||
shutil.copy(
|
||||
self.script_config_path,
|
||||
Path.cwd()
|
||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||
/ self.script_config_path.name,
|
||||
)
|
||||
logger.success("通用脚本配置文件已更新")
|
||||
|
||||
# 执行任务后脚本
|
||||
if (
|
||||
self.cur_user_data.get("Info", "IfScriptAfterTask")
|
||||
and Path(
|
||||
self.cur_user_data.get("Info", "ScriptAfterTask")
|
||||
).exists()
|
||||
):
|
||||
await self.execute_script_task(
|
||||
Path(self.cur_user_data.get("Info", "ScriptAfterTask")),
|
||||
"脚本后任务",
|
||||
)
|
||||
|
||||
# 保存运行日志以及统计信息
|
||||
await Config.save_general_log(
|
||||
Path.cwd()
|
||||
/ f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
|
||||
self.general_logs,
|
||||
self.general_result,
|
||||
)
|
||||
|
||||
await self.result_record()
|
||||
|
||||
# 设置通用脚本模式
|
||||
elif self.mode == "设置脚本":
|
||||
@@ -567,21 +545,11 @@ class GeneralManager:
|
||||
"end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"user_result": "代理成功" if self.run_book else self.general_result,
|
||||
}
|
||||
try:
|
||||
await self.push_notification(
|
||||
"统计信息",
|
||||
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
|
||||
statistics,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"推送统计信息失败: {e}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Info",
|
||||
data={"Error": f"推送统计信息失败: {e}"},
|
||||
).model_dump()
|
||||
)
|
||||
await self.push_notification(
|
||||
"统计信息",
|
||||
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
|
||||
statistics,
|
||||
)
|
||||
|
||||
if self.run_book:
|
||||
# 成功完成代理的用户修改相关参数
|
||||
@@ -691,10 +659,10 @@ class GeneralManager:
|
||||
if self.mode == "自动代理":
|
||||
|
||||
# 更新用户数据
|
||||
await Config.ScriptConfig[self.script_id].UserData.load(
|
||||
await self.user_config.toDict()
|
||||
)
|
||||
await Config.ScriptConfig.save()
|
||||
sc = Config.ScriptConfig[self.script_id]
|
||||
if isinstance(sc, GeneralConfig):
|
||||
await sc.UserData.load(await self.user_config.toDict())
|
||||
await Config.ScriptConfig.save()
|
||||
|
||||
error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
|
||||
over_user = [_["name"] for _ in self.user_list if _["status"] == "完成"]
|
||||
@@ -740,17 +708,7 @@ class GeneralManager:
|
||||
f"已完成配置数: {len(over_user)}, 未完成配置数: {len(error_user) + len(wait_user)}",
|
||||
10,
|
||||
)
|
||||
try:
|
||||
await self.push_notification("代理结果", title, result)
|
||||
except Exception as e:
|
||||
logger.exception(f"推送代理结果失败: {e}")
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id=self.ws_id,
|
||||
type="Info",
|
||||
data={"Error": f"推送代理结果失败: {e}"},
|
||||
).model_dump()
|
||||
)
|
||||
await self.push_notification("代理结果", title, result)
|
||||
|
||||
elif self.mode == "设置脚本":
|
||||
|
||||
@@ -1012,6 +970,7 @@ class GeneralManager:
|
||||
serverchan_message = message_text.replace("\n", "\n\n")
|
||||
|
||||
# 发送全局通知
|
||||
|
||||
if Config.get("Notify", "IfSendMail"):
|
||||
await Notify.send_mail(
|
||||
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
||||
@@ -1025,10 +984,21 @@ class GeneralManager:
|
||||
)
|
||||
|
||||
# 发送自定义Webhook通知
|
||||
for webhook in Config.Notify_CustomWebhooks.values():
|
||||
await Notify.WebhookPush(
|
||||
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||
)
|
||||
try:
|
||||
custom_webhooks = Config.get("Notify", "CustomWebhooks")
|
||||
except AttributeError:
|
||||
custom_webhooks = []
|
||||
if custom_webhooks:
|
||||
for webhook in custom_webhooks:
|
||||
if webhook.get("enabled", True):
|
||||
try:
|
||||
await Notify.CustomWebhookPush(
|
||||
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||
)
|
||||
|
||||
elif mode == "统计信息":
|
||||
|
||||
@@ -1061,10 +1031,21 @@ class GeneralManager:
|
||||
)
|
||||
|
||||
# 发送自定义Webhook通知
|
||||
for webhook in Config.Notify_CustomWebhooks.values():
|
||||
await Notify.WebhookPush(
|
||||
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||
)
|
||||
try:
|
||||
custom_webhooks = Config.get("Notify", "CustomWebhooks")
|
||||
except AttributeError:
|
||||
custom_webhooks = []
|
||||
if custom_webhooks:
|
||||
for webhook in custom_webhooks:
|
||||
if webhook.get("enabled", True):
|
||||
try:
|
||||
await Notify.CustomWebhookPush(
|
||||
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||
)
|
||||
|
||||
# 发送用户单独通知
|
||||
if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
|
||||
@@ -1097,7 +1078,22 @@ class GeneralManager:
|
||||
)
|
||||
|
||||
# 推送CompanyWebHookBot通知
|
||||
for webhook in self.cur_user_data.Notify_CustomWebhooks.values():
|
||||
await Notify.WebhookPush(
|
||||
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||
)
|
||||
# 发送用户自定义Webhook通知
|
||||
user_webhooks = self.cur_user_data.get("Notify", "CustomWebhooks")
|
||||
if user_webhooks:
|
||||
for webhook in user_webhooks:
|
||||
if webhook.get("enabled", True):
|
||||
try:
|
||||
await Notify.CustomWebhookPush(
|
||||
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"用户自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"用户CompanyWebHookBot密钥为空, 无法发送用户单独的CompanyWebHookBot通知"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -23,10 +23,6 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
|
||||
"""配置类型映射表"""
|
||||
|
||||
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]},
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
Menu,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
screen,
|
||||
dialog,
|
||||
shell,
|
||||
Tray,
|
||||
Menu,
|
||||
nativeImage,
|
||||
screen,
|
||||
} from 'electron'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { exec, spawn } from 'child_process'
|
||||
import { checkEnvironment, getAppRoot } from './services/environmentService'
|
||||
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
|
||||
import {
|
||||
downloadPython,
|
||||
installDependencies,
|
||||
installPipPackage,
|
||||
setMainWindow as setPythonMainWindow,
|
||||
startBackend,
|
||||
} from './services/pythonService'
|
||||
import { cloneBackend, downloadGit, setMainWindow as setGitMainWindow } from './services/gitService'
|
||||
import { cleanOldLogs, getLogFiles, getLogPath, log, setupLogger } from './services/logService'
|
||||
import { spawn, exec } from 'child_process'
|
||||
import { getAppRoot, checkEnvironment } from './services/environmentService'
|
||||
|
||||
// 强制清理相关进程的函数
|
||||
async function forceKillRelatedProcesses(): Promise<void> {
|
||||
@@ -33,15 +22,15 @@ async function forceKillRelatedProcesses(): Promise<void> {
|
||||
log.info('所有相关进程已清理')
|
||||
} catch (error) {
|
||||
log.error('清理进程时出错:', error)
|
||||
|
||||
|
||||
// 备用清理方法
|
||||
if (process.platform === 'win32') {
|
||||
const appRoot = getAppRoot()
|
||||
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 使用更简单的命令强制结束相关进程
|
||||
exec(`taskkill /f /im python.exe`, error => {
|
||||
exec(`taskkill /f /im python.exe`, (error) => {
|
||||
if (error) {
|
||||
log.warn('备用清理方法失败:', error.message)
|
||||
} else {
|
||||
@@ -53,6 +42,16 @@ async function forceKillRelatedProcesses(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
|
||||
import {
|
||||
setMainWindow as setPythonMainWindow,
|
||||
downloadPython,
|
||||
installPipPackage,
|
||||
installDependencies,
|
||||
startBackend,
|
||||
} from './services/pythonService'
|
||||
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
|
||||
import { setupLogger, log, getLogPath, getLogFiles, cleanOldLogs } from './services/logService'
|
||||
|
||||
// 检查是否以管理员权限运行
|
||||
function isRunningAsAdmin(): boolean {
|
||||
@@ -114,7 +113,6 @@ interface AppConfig {
|
||||
IfMinimizeDirectly: boolean
|
||||
IfSelfStart: boolean
|
||||
}
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -306,9 +304,7 @@ function updateTrayVisibility(config: AppConfig) {
|
||||
log.info('托盘图标已销毁')
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow: Electron.BrowserWindow | null = null
|
||||
|
||||
function createWindow() {
|
||||
log.info('开始创建主窗口')
|
||||
|
||||
@@ -446,7 +442,7 @@ function createWindow() {
|
||||
screen.removeListener('display-metrics-changed', recomputeMinSize)
|
||||
// 置空模块级引用
|
||||
mainWindow = null
|
||||
|
||||
|
||||
// 如果是正在退出,立即执行进程清理
|
||||
if (isQuitting) {
|
||||
log.info('窗口关闭,执行最终清理')
|
||||
@@ -602,19 +598,19 @@ ipcMain.handle('kill-all-processes', async () => {
|
||||
ipcMain.handle('force-exit', async () => {
|
||||
log.info('收到强制退出命令')
|
||||
isQuitting = true
|
||||
|
||||
|
||||
// 立即清理进程
|
||||
try {
|
||||
await forceKillRelatedProcesses()
|
||||
} catch (e) {
|
||||
log.error('强制清理失败:', e)
|
||||
}
|
||||
|
||||
|
||||
// 强制退出
|
||||
setTimeout(() => {
|
||||
process.exit(0)
|
||||
}, 500)
|
||||
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
@@ -750,232 +746,6 @@ ipcMain.handle('stop-backend', async () => {
|
||||
return stopBackend()
|
||||
})
|
||||
|
||||
// 获取当前主题信息
|
||||
ipcMain.handle('get-theme-info', async () => {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
|
||||
|
||||
let themeMode = 'system'
|
||||
let themeColor = 'blue'
|
||||
|
||||
// 尝试从配置文件读取主题设置
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const configData = fs.readFileSync(configPath, 'utf8')
|
||||
const config = JSON.parse(configData)
|
||||
themeMode = config.themeMode || 'system'
|
||||
themeColor = config.themeColor || 'blue'
|
||||
} catch (error) {
|
||||
log.warn('读取主题配置失败,使用默认值:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检测系统主题
|
||||
const systemTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
|
||||
|
||||
// 确定实际使用的主题
|
||||
let actualTheme = themeMode
|
||||
if (themeMode === 'system') {
|
||||
actualTheme = systemTheme
|
||||
}
|
||||
|
||||
const themeColors: Record<string, string> = {
|
||||
blue: '#1677ff',
|
||||
purple: '#722ed1',
|
||||
cyan: '#13c2c2',
|
||||
green: '#52c41a',
|
||||
magenta: '#eb2f96',
|
||||
pink: '#eb2f96',
|
||||
red: '#ff4d4f',
|
||||
orange: '#fa8c16',
|
||||
yellow: '#fadb14',
|
||||
volcano: '#fa541c',
|
||||
geekblue: '#2f54eb',
|
||||
lime: '#a0d911',
|
||||
gold: '#faad14',
|
||||
}
|
||||
|
||||
return {
|
||||
themeMode,
|
||||
themeColor,
|
||||
actualTheme,
|
||||
systemTheme,
|
||||
isDark: actualTheme === 'dark',
|
||||
primaryColor: themeColors[themeColor] || themeColors.blue
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('获取主题信息失败:', error)
|
||||
return {
|
||||
themeMode: 'system',
|
||||
themeColor: 'blue',
|
||||
actualTheme: 'light',
|
||||
systemTheme: 'light',
|
||||
isDark: false,
|
||||
primaryColor: '#1677ff'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 获取对话框专用的主题信息
|
||||
ipcMain.handle('get-theme', async () => {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
|
||||
|
||||
let themeMode = 'system'
|
||||
|
||||
// 尝试从配置文件读取主题设置
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const configData = fs.readFileSync(configPath, 'utf8')
|
||||
const config = JSON.parse(configData)
|
||||
themeMode = config.themeMode || 'system'
|
||||
} catch (error) {
|
||||
log.warn('读取主题配置失败,使用默认值:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检测系统主题
|
||||
const systemTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
|
||||
|
||||
// 确定实际使用的主题
|
||||
let actualTheme = themeMode
|
||||
if (themeMode === 'system') {
|
||||
actualTheme = systemTheme
|
||||
}
|
||||
|
||||
return actualTheme
|
||||
} catch (error) {
|
||||
log.error('获取对话框主题失败:', error)
|
||||
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
|
||||
}
|
||||
})
|
||||
|
||||
// 全局存储对话框窗口引用和回调
|
||||
let dialogWindows = new Map<string, BrowserWindow>()
|
||||
let dialogCallbacks = new Map<string, (result: boolean) => void>()
|
||||
|
||||
// 创建对话框窗口
|
||||
function createQuestionDialog(questionData: any): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const messageId = questionData.messageId || 'dialog_' + Date.now()
|
||||
|
||||
// 存储回调函数
|
||||
dialogCallbacks.set(messageId, resolve)
|
||||
|
||||
// 准备对话框数据
|
||||
const dialogData = {
|
||||
title: questionData.title || '操作确认',
|
||||
message: questionData.message || '是否要执行此操作?',
|
||||
options: questionData.options || ['确定', '取消'],
|
||||
messageId: messageId
|
||||
}
|
||||
|
||||
// 获取主窗口的尺寸用于全屏显示
|
||||
let windowBounds = { width: 800, height: 600, x: 100, y: 100 }
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
windowBounds = mainWindow.getBounds()
|
||||
}
|
||||
|
||||
// 创建对话框窗口 - 小尺寸可拖动窗口
|
||||
const dialogWindow = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 145,
|
||||
x: windowBounds.x + (windowBounds.width - 400) / 2, // 居中显示
|
||||
y: windowBounds.y + (windowBounds.height - 200) / 2,
|
||||
resizable: false, // 不允许改变大小
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
alwaysOnTop: true,
|
||||
show: false,
|
||||
frame: false,
|
||||
modal: mainWindow ? true : false,
|
||||
parent: mainWindow || undefined,
|
||||
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
})
|
||||
|
||||
// 存储窗口引用
|
||||
dialogWindows.set(messageId, dialogWindow)
|
||||
|
||||
// 编码对话框数据
|
||||
const encodedData = encodeURIComponent(JSON.stringify(dialogData))
|
||||
|
||||
// 加载对话框页面
|
||||
const dialogUrl = `file://${path.join(__dirname, '../public/dialog.html')}?data=${encodedData}`
|
||||
dialogWindow.loadURL(dialogUrl)
|
||||
|
||||
// 窗口准备好后显示
|
||||
dialogWindow.once('ready-to-show', () => {
|
||||
dialogWindow.show()
|
||||
dialogWindow.focus()
|
||||
})
|
||||
|
||||
// 窗口关闭时清理
|
||||
dialogWindow.on('closed', () => {
|
||||
dialogWindows.delete(messageId)
|
||||
const callback = dialogCallbacks.get(messageId)
|
||||
if (callback) {
|
||||
dialogCallbacks.delete(messageId)
|
||||
callback(false) // 默认返回 false (取消)
|
||||
}
|
||||
})
|
||||
|
||||
log.info(`全屏对话框窗口已创建: ${messageId}`)
|
||||
})
|
||||
}
|
||||
|
||||
// 显示问题对话框
|
||||
ipcMain.handle('show-question-dialog', async (_event, questionData) => {
|
||||
log.info('收到显示对话框请求:', questionData)
|
||||
try {
|
||||
const result = await createQuestionDialog(questionData)
|
||||
log.info(`对话框结果: ${result}`)
|
||||
return result
|
||||
} catch (error) {
|
||||
log.error('创建对话框失败:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// 处理对话框响应
|
||||
ipcMain.handle('dialog-response', async (_event, messageId: string, choice: boolean) => {
|
||||
log.info(`收到对话框响应: ${messageId} = ${choice}`)
|
||||
|
||||
const callback = dialogCallbacks.get(messageId)
|
||||
if (callback) {
|
||||
dialogCallbacks.delete(messageId)
|
||||
callback(choice)
|
||||
}
|
||||
|
||||
// 关闭对话框窗口
|
||||
const dialogWindow = dialogWindows.get(messageId)
|
||||
if (dialogWindow && !dialogWindow.isDestroyed()) {
|
||||
dialogWindow.close()
|
||||
}
|
||||
dialogWindows.delete(messageId)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// 移动对话框窗口
|
||||
ipcMain.handle('move-window', async (_event, deltaX: number, deltaY: number) => {
|
||||
// 获取当前活动的对话框窗口(最后创建的)
|
||||
const dialogWindow = Array.from(dialogWindows.values()).pop()
|
||||
if (dialogWindow && !dialogWindow.isDestroyed()) {
|
||||
const currentBounds = dialogWindow.getBounds()
|
||||
dialogWindow.setPosition(
|
||||
currentBounds.x + deltaX,
|
||||
currentBounds.y + deltaY
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Git相关
|
||||
ipcMain.handle('download-git', async () => {
|
||||
const appRoot = getAppRoot()
|
||||
@@ -1015,7 +785,7 @@ ipcMain.handle('check-git-update', async () => {
|
||||
|
||||
// 不执行fetch,直接检查本地状态
|
||||
// 这样避免了直接访问GitHub,而是在后续的pull操作中使用镜像站
|
||||
|
||||
|
||||
// 获取当前HEAD的commit hash
|
||||
const currentCommit = await new Promise<string>((resolve, reject) => {
|
||||
const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
|
||||
@@ -1041,13 +811,14 @@ ipcMain.handle('check-git-update', async () => {
|
||||
})
|
||||
|
||||
log.info(`当前本地commit: ${currentCommit}`)
|
||||
|
||||
|
||||
// 由于我们跳过了fetch步骤(避免直接访问GitHub),
|
||||
// 我们无法准确知道远程是否有更新
|
||||
// 因此返回true,让后续的pull操作通过镜像站来检查和获取更新
|
||||
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
||||
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
||||
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
||||
|
||||
} catch (error) {
|
||||
log.error('检查Git更新失败:', error)
|
||||
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
||||
@@ -1313,23 +1084,23 @@ app.on('before-quit', async event => {
|
||||
|
||||
// 立即开始强制清理,不等待优雅关闭
|
||||
log.info('开始强制清理所有相关进程')
|
||||
|
||||
|
||||
try {
|
||||
// 并行执行多种清理方法
|
||||
const cleanupPromises = [
|
||||
// 方法1: 使用我们的进程管理器
|
||||
forceKillRelatedProcesses(),
|
||||
|
||||
|
||||
// 方法2: 直接使用 taskkill 命令
|
||||
new Promise<void>(resolve => {
|
||||
new Promise<void>((resolve) => {
|
||||
if (process.platform === 'win32') {
|
||||
const appRoot = getAppRoot()
|
||||
const commands = [
|
||||
`taskkill /f /im python.exe`,
|
||||
`wmic process where "CommandLine like '%main.py%'" delete`,
|
||||
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`,
|
||||
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`
|
||||
]
|
||||
|
||||
|
||||
let completed = 0
|
||||
commands.forEach(cmd => {
|
||||
exec(cmd, () => {
|
||||
@@ -1339,26 +1110,26 @@ app.on('before-quit', async event => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// 2秒超时
|
||||
setTimeout(resolve, 2000)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}),
|
||||
})
|
||||
]
|
||||
|
||||
|
||||
// 最多等待3秒
|
||||
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000))
|
||||
await Promise.race([Promise.all(cleanupPromises), timeoutPromise])
|
||||
|
||||
|
||||
log.info('进程清理完成')
|
||||
} catch (e) {
|
||||
log.error('进程清理时出错:', e)
|
||||
}
|
||||
|
||||
log.info('应用强制退出')
|
||||
|
||||
|
||||
// 使用 process.exit 而不是 app.exit,更加强制
|
||||
setTimeout(() => {
|
||||
process.exit(0)
|
||||
|
||||
@@ -63,16 +63,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
||||
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
|
||||
|
||||
// 对话框相关
|
||||
showQuestionDialog: (questionData: any) => ipcRenderer.invoke('show-question-dialog', questionData),
|
||||
dialogResponse: (messageId: string, choice: boolean) => ipcRenderer.invoke('dialog-response', messageId, choice),
|
||||
resizeDialogWindow: (height: number) => ipcRenderer.invoke('resize-dialog-window', height),
|
||||
moveWindow: (deltaX: number, deltaY: number) => ipcRenderer.invoke('move-window', deltaX, deltaY),
|
||||
|
||||
// 主题信息获取
|
||||
getThemeInfo: () => ipcRenderer.invoke('get-theme-info'),
|
||||
getTheme: () => ipcRenderer.invoke('get-theme'),
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { BrowserWindow, app } from 'electron'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { downloadFile } from './downloadService'
|
||||
|
||||
@@ -13,116 +13,6 @@ export function setMainWindow(window: BrowserWindow) {
|
||||
|
||||
const gitDownloadUrl = 'https://download.auto-mas.top/d/AUTO_MAS/git.zip'
|
||||
|
||||
// 获取应用版本号
|
||||
function getAppVersion(appRoot: string): string {
|
||||
console.log('=== 开始获取应用版本号 ===')
|
||||
console.log(`应用根目录: ${appRoot}`)
|
||||
|
||||
try {
|
||||
// 方法1: 从 Electron app 获取版本号(打包后可用)
|
||||
try {
|
||||
const appVersion = app.getVersion()
|
||||
if (appVersion && appVersion !== '1.0.0') { // 避免使用默认版本
|
||||
console.log(`✅ 从 app.getVersion() 获取版本号: ${appVersion}`)
|
||||
return appVersion
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ app.getVersion() 获取失败:', error)
|
||||
}
|
||||
|
||||
// 方法2: 从预设的环境变量获取(如果在构建时注入了)
|
||||
if (process.env.VITE_APP_VERSION) {
|
||||
console.log(`✅ 从环境变量获取版本号: ${process.env.VITE_APP_VERSION}`)
|
||||
return process.env.VITE_APP_VERSION
|
||||
}
|
||||
|
||||
// 方法3: 开发环境下从 package.json 获取
|
||||
const packageJsonPath = path.join(appRoot, 'frontend', 'package.json')
|
||||
console.log(`尝试读取前端package.json: ${packageJsonPath}`)
|
||||
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
const version = packageJson.version || '获取版本失败!'
|
||||
console.log(`✅ 从前端package.json获取版本号: ${version}`)
|
||||
return version
|
||||
}
|
||||
|
||||
console.log('⚠️ 前端package.json不存在,尝试读取根目录package.json')
|
||||
|
||||
// 方法4: 从根目录 package.json 获取(开发环境)
|
||||
const currentPackageJsonPath = path.join(appRoot, 'package.json')
|
||||
console.log(`尝试读取根目录package.json: ${currentPackageJsonPath}`)
|
||||
|
||||
if (fs.existsSync(currentPackageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(currentPackageJsonPath, 'utf8'))
|
||||
const version = packageJson.version || '获取版本失败!'
|
||||
console.log(`✅ 从根目录package.json获取版本号: ${version}`)
|
||||
return version
|
||||
}
|
||||
|
||||
console.log('❌ 未找到任何版本信息源')
|
||||
return '获取版本失败!'
|
||||
} catch (error) {
|
||||
console.error('❌ 获取版本号失败:', error)
|
||||
return '获取版本失败!'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查分支是否存在
|
||||
async function checkBranchExists(
|
||||
gitPath: string,
|
||||
gitEnv: any,
|
||||
repoUrl: string,
|
||||
branchName: string
|
||||
): Promise<boolean> {
|
||||
console.log(`=== 检查分支是否存在: ${branchName} ===`)
|
||||
console.log(`Git路径: ${gitPath}`)
|
||||
console.log(`仓库URL: ${repoUrl}`)
|
||||
|
||||
try {
|
||||
return new Promise<boolean>(resolve => {
|
||||
const proc = spawn(gitPath, ['ls-remote', '--heads', repoUrl, branchName], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
})
|
||||
|
||||
let output = ''
|
||||
let errorOutput = ''
|
||||
|
||||
proc.stdout?.on('data', data => {
|
||||
const chunk = data.toString()
|
||||
output += chunk
|
||||
console.log(`git ls-remote stdout: ${chunk.trim()}`)
|
||||
})
|
||||
|
||||
proc.stderr?.on('data', data => {
|
||||
const chunk = data.toString()
|
||||
errorOutput += chunk
|
||||
console.log(`git ls-remote stderr: ${chunk.trim()}`)
|
||||
})
|
||||
|
||||
proc.on('close', code => {
|
||||
console.log(`git ls-remote 退出码: ${code}`)
|
||||
// 如果输出包含分支名,说明分支存在
|
||||
const branchExists = output.includes(`refs/heads/${branchName}`)
|
||||
console.log(`分支 ${branchName} ${branchExists ? '✅ 存在' : '❌ 不存在'}`)
|
||||
if (errorOutput) {
|
||||
console.log(`错误输出: ${errorOutput}`)
|
||||
}
|
||||
resolve(branchExists)
|
||||
})
|
||||
|
||||
proc.on('error', error => {
|
||||
console.error(`git ls-remote 进程错误:`, error)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`❌ 检查分支 ${branchName} 时出错:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 递归复制目录,包括文件和隐藏文件
|
||||
function copyDirSync(src: string, dest: string) {
|
||||
if (!fs.existsSync(dest)) {
|
||||
@@ -302,226 +192,69 @@ export async function cloneBackend(
|
||||
success: boolean
|
||||
error?: string
|
||||
}> {
|
||||
console.log('=== 开始克隆/更新后端代码 ===')
|
||||
console.log(`应用根目录: ${appRoot}`)
|
||||
console.log(`仓库URL: ${repoUrl}`)
|
||||
|
||||
try {
|
||||
const backendPath = appRoot
|
||||
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
|
||||
|
||||
console.log(`Git可执行文件路径: ${gitPath}`)
|
||||
console.log(`后端代码路径: ${backendPath}`)
|
||||
|
||||
if (!fs.existsSync(gitPath)) {
|
||||
const error = `Git可执行文件不存在: ${gitPath}`
|
||||
console.error(`❌ ${error}`)
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
console.log('✅ Git可执行文件存在')
|
||||
if (!fs.existsSync(gitPath)) throw new Error(`Git可执行文件不存在: ${gitPath}`)
|
||||
const gitEnv = getGitEnvironment(appRoot)
|
||||
console.log('✅ Git环境变量配置完成')
|
||||
|
||||
// 检查 git 是否可用
|
||||
console.log('=== 检查Git是否可用 ===')
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['--version'], { env: gitEnv })
|
||||
|
||||
proc.stdout?.on('data', data => {
|
||||
console.log(`git --version output: ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
proc.stderr?.on('data', data => {
|
||||
console.log(`git --version error: ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
proc.on('close', code => {
|
||||
console.log(`git --version 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ Git可用')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ Git无法正常运行')
|
||||
reject(new Error('git 无法正常运行'))
|
||||
}
|
||||
})
|
||||
|
||||
proc.on('error', error => {
|
||||
console.error('❌ Git进程启动失败:', error)
|
||||
reject(error)
|
||||
})
|
||||
proc.on('close', code => (code === 0 ? resolve() : reject(new Error('git 无法正常运行'))))
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
// 获取版本号并确定目标分支
|
||||
const version = getAppVersion(appRoot)
|
||||
console.log(`=== 分支选择逻辑 ===`)
|
||||
console.log(`当前应用版本: ${version}`)
|
||||
|
||||
let targetBranch = 'feature/refactor' // 默认分支
|
||||
console.log(`默认分支: ${targetBranch}`)
|
||||
|
||||
if (version !== '获取版本失败!') {
|
||||
// 检查版本对应的分支是否存在
|
||||
console.log(`开始检查版本分支是否存在...`)
|
||||
const versionBranchExists = await checkBranchExists(gitPath, gitEnv, repoUrl, version)
|
||||
if (versionBranchExists) {
|
||||
targetBranch = version
|
||||
console.log(`🎯 将使用版本分支: ${targetBranch}`)
|
||||
} else {
|
||||
console.log(`⚠️ 版本分支 ${version} 不存在,使用默认分支: ${targetBranch}`)
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 版本号获取失败,使用默认分支: feature/refactor')
|
||||
}
|
||||
|
||||
console.log(`=== 最终选择分支: ${targetBranch} ===`)
|
||||
|
||||
// 检查是否为Git仓库
|
||||
const isRepo = isGitRepository(backendPath)
|
||||
console.log(`检查是否为Git仓库: ${isRepo ? '✅ 是' : '❌ 否'}`)
|
||||
|
||||
// ==== 下面是关键逻辑 ====
|
||||
if (isRepo) {
|
||||
console.log('=== 更新现有Git仓库 ===')
|
||||
|
||||
if (isGitRepository(backendPath)) {
|
||||
// 已是 git 仓库,先更新远程URL为镜像站,然后 pull
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: `正在更新后端代码(分支: ${targetBranch})...`,
|
||||
message: '正在更新后端代码...',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 更新远程URL为镜像站URL,避免直接访问GitHub
|
||||
console.log(`📡 更新远程URL为镜像站: ${repoUrl}`)
|
||||
console.log(`更新远程URL为镜像站: ${repoUrl}`)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git remote set-url stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git remote set-url stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git remote set-url 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ 远程URL更新成功')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ 远程URL更新失败')
|
||||
reject(new Error(`git remote set-url失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git remote set-url 进程错误:', error)
|
||||
reject(error)
|
||||
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git remote set-url:', d.toString()))
|
||||
proc.stderr?.on('data', d => console.log('git remote set-url err:', d.toString()))
|
||||
proc.on('close', code =>
|
||||
code === 0 ? resolve() : reject(new Error(`git remote set-url失败,退出码: ${code}`))
|
||||
)
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
// 获取目标分支信息(显式 fetch 目标分支)
|
||||
console.log(`📥 显式获取远程分支: ${targetBranch} ...`)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['fetch', 'origin', targetBranch], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git fetch stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git fetch stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git fetch origin ${targetBranch} 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log(`✅ 成功获取远程分支: ${targetBranch}`)
|
||||
resolve()
|
||||
} else {
|
||||
console.error(`❌ 获取远程分支失败: ${targetBranch}`)
|
||||
reject(new Error(`git fetch origin ${targetBranch} 失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git fetch 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// 切换到目标分支
|
||||
console.log(`🔀 切换到目标分支: ${targetBranch}`)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['checkout', '-B', targetBranch, `origin/${targetBranch}`], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git checkout stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git checkout stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git checkout 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log(`✅ 成功切换到分支: ${targetBranch}`)
|
||||
resolve()
|
||||
} else {
|
||||
console.error(`❌ 切换分支失败: ${targetBranch}`)
|
||||
reject(new Error(`git checkout失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git checkout 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// 执行pull操作
|
||||
console.log('🔄 强制同步到远程分支最新提交...')
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(gitPath, ['reset', '--hard', `origin/${targetBranch}`], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: backendPath,
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git reset stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git reset stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git reset --hard 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ 代码已强制更新到远程最新版本')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ 代码重置失败')
|
||||
reject(new Error(`git reset --hard 失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git reset 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
const proc = spawn(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath })
|
||||
proc.stdout?.on('data', d => console.log('git pull:', d.toString()))
|
||||
proc.stderr?.on('data', d => console.log('git pull err:', d.toString()))
|
||||
proc.on('close', code =>
|
||||
code === 0 ? resolve() : reject(new Error(`git pull失败,退出码: ${code}`))
|
||||
)
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: `后端代码更新完成(分支: ${targetBranch})`,
|
||||
message: '后端代码更新完成',
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ 后端代码更新完成(分支: ${targetBranch})`)
|
||||
} else {
|
||||
console.log('=== 克隆新的Git仓库 ===')
|
||||
|
||||
// 不是 git 仓库,clone 到 tmp,再拷贝出来
|
||||
const tmpDir = path.join(appRoot, 'git_tmp')
|
||||
console.log(`临时目录: ${tmpDir}`)
|
||||
|
||||
if (fs.existsSync(tmpDir)) {
|
||||
console.log('🗑️ 清理现有临时目录...')
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log('📁 创建临时目录...')
|
||||
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
fs.mkdirSync(tmpDir, { recursive: true })
|
||||
|
||||
if (mainWindow) {
|
||||
@@ -529,13 +262,9 @@ export async function cloneBackend(
|
||||
type: 'backend',
|
||||
progress: 0,
|
||||
status: 'downloading',
|
||||
message: `正在克隆后端代码(分支: ${targetBranch})...`,
|
||||
message: '正在克隆后端代码...',
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`📥 开始克隆代码到临时目录...`)
|
||||
console.log(`克隆参数: --single-branch --depth 1 --branch ${targetBranch}`)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
gitPath,
|
||||
@@ -547,7 +276,7 @@ export async function cloneBackend(
|
||||
'--depth',
|
||||
'1',
|
||||
'--branch',
|
||||
targetBranch,
|
||||
'feature/refactor',
|
||||
repoUrl,
|
||||
tmpDir,
|
||||
],
|
||||
@@ -557,51 +286,26 @@ export async function cloneBackend(
|
||||
cwd: appRoot,
|
||||
}
|
||||
)
|
||||
proc.stdout?.on('data', d => console.log('git clone stdout:', d.toString().trim()))
|
||||
proc.stderr?.on('data', d => console.log('git clone stderr:', d.toString().trim()))
|
||||
proc.on('close', code => {
|
||||
console.log(`git clone 退出码: ${code}`)
|
||||
if (code === 0) {
|
||||
console.log('✅ 代码克隆成功')
|
||||
resolve()
|
||||
} else {
|
||||
console.error('❌ 代码克隆失败')
|
||||
reject(new Error(`git clone失败,退出码: ${code}`))
|
||||
}
|
||||
})
|
||||
proc.on('error', error => {
|
||||
console.error('❌ git clone 进程错误:', error)
|
||||
reject(error)
|
||||
})
|
||||
proc.stdout?.on('data', d => console.log('git clone:', d.toString()))
|
||||
proc.stderr?.on('data', d => console.log('git clone err:', d.toString()))
|
||||
proc.on('close', code =>
|
||||
code === 0 ? resolve() : reject(new Error(`git clone失败,退出码: ${code}`))
|
||||
)
|
||||
proc.on('error', reject)
|
||||
})
|
||||
|
||||
// 复制所有文件到 backendPath(appRoot),包含 .git
|
||||
console.log('📋 复制文件到目标目录...')
|
||||
const tmpFiles = fs.readdirSync(tmpDir)
|
||||
console.log(`临时目录中的文件: ${tmpFiles.join(', ')}`)
|
||||
|
||||
for (const file of tmpFiles) {
|
||||
const src = path.join(tmpDir, file)
|
||||
const dst = path.join(backendPath, file)
|
||||
|
||||
console.log(`复制: ${file}`)
|
||||
|
||||
if (fs.existsSync(dst)) {
|
||||
console.log(` - 删除现有文件/目录: ${dst}`)
|
||||
if (fs.statSync(dst).isDirectory()) fs.rmSync(dst, { recursive: true, force: true })
|
||||
else fs.unlinkSync(dst)
|
||||
}
|
||||
|
||||
if (fs.statSync(src).isDirectory()) {
|
||||
console.log(` - 复制目录: ${src} -> ${dst}`)
|
||||
copyDirSync(src, dst)
|
||||
} else {
|
||||
console.log(` - 复制文件: ${src} -> ${dst}`)
|
||||
fs.copyFileSync(src, dst)
|
||||
}
|
||||
if (fs.statSync(src).isDirectory()) copyDirSync(src, dst)
|
||||
else fs.copyFileSync(src, dst)
|
||||
}
|
||||
|
||||
console.log('🗑️ 清理临时目录...')
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
|
||||
if (mainWindow) {
|
||||
@@ -609,20 +313,14 @@ export async function cloneBackend(
|
||||
type: 'backend',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
message: `后端代码克隆完成(分支: ${targetBranch})`,
|
||||
message: '后端代码克隆完成',
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ 后端代码克隆完成(分支: ${targetBranch})`)
|
||||
}
|
||||
|
||||
console.log('=== 后端代码获取操作完成 ===')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('❌ 获取后端代码失败:', errorMessage)
|
||||
console.error('错误堆栈:', error instanceof Error ? error.stack : 'N/A')
|
||||
|
||||
console.error('获取后端代码失败:', errorMessage)
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
@@ -633,4 +331,4 @@ export async function cloneBackend(
|
||||
}
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "v5.0.0-alpha.3",
|
||||
"version": "5.0.0-alpha.2",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
||||
@@ -113,4 +113,4 @@
|
||||
"@types/node": "22.17.1"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>操作确认</title>
|
||||
<style>
|
||||
:root {
|
||||
/* 亮色模式变量 */
|
||||
--primary-color: #1677ff;
|
||||
--primary-hover: #4096ff;
|
||||
--primary-active: #0958d9;
|
||||
--danger-color: #ff4d4f;
|
||||
--danger-hover: #ff7875;
|
||||
--danger-active: #d9363e;
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
|
||||
--text-primary: #262626;
|
||||
--text-secondary: #595959;
|
||||
--text-tertiary: #8c8c8c;
|
||||
--text-disabled: #bfbfbf;
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-tertiary: #f5f5f5;
|
||||
--bg-quaternary: #f0f0f0;
|
||||
|
||||
--border-primary: #d9d9d9;
|
||||
--border-secondary: #f0f0f0;
|
||||
--border-hover: #4096ff;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
/* 暗色模式变量 */
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #1668dc;
|
||||
--primary-hover: #3c89e8;
|
||||
--primary-active: #1554ad;
|
||||
--danger-color: #ff4d4f;
|
||||
--danger-hover: #ff7875;
|
||||
--danger-active: #d9363e;
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #c9cdd4;
|
||||
--text-tertiary: #a6adb4;
|
||||
--text-disabled: #6c757d;
|
||||
|
||||
--bg-primary: #1f1f1f;
|
||||
--bg-secondary: #2a2a2a;
|
||||
--bg-tertiary: #373737;
|
||||
--bg-quaternary: #404040;
|
||||
|
||||
--border-primary: #434343;
|
||||
--border-secondary: #303030;
|
||||
--border-hover: #3c89e8;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 1px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-primary);
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 12px 16px 8px 16px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: move;
|
||||
-webkit-app-region: drag;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-primary);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 12px 16px 12px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
padding: 6px 12px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
min-width: 60px;
|
||||
outline: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
line-height: 1.5715;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: var(--bg-quaternary);
|
||||
border-color: var(--border-hover);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.dialog-button:focus {
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
|
||||
border-color: var(--border-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dialog-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.dialog-button.primary {
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
border-color: var(--primary-color);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-button.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-button.primary:active {
|
||||
background: var(--primary-active);
|
||||
border-color: var(--primary-active);
|
||||
}
|
||||
|
||||
.dialog-button.danger {
|
||||
background: var(--danger-color);
|
||||
color: #fff;
|
||||
border-color: var(--danger-color);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-button.danger:hover {
|
||||
background: var(--danger-hover);
|
||||
border-color: var(--danger-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-button.danger:active {
|
||||
background: var(--danger-active);
|
||||
border-color: var(--danger-active);
|
||||
}
|
||||
|
||||
/* 键盘导航样式 */
|
||||
.dialog-button:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.dialog-container {
|
||||
animation: dialogFadeIn 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
@keyframes dialogFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 350px) {
|
||||
.dialog-header {
|
||||
padding: 8px 12px 6px 12px;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 8px 12px 8px 12px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dialog-container" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-message">
|
||||
<div class="dialog-header">
|
||||
<h3 class="dialog-title" id="dialog-title">操作确认</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p class="dialog-message" id="dialog-message">是否要执行此操作?</p>
|
||||
</div>
|
||||
<div class="dialog-actions" id="dialog-actions" role="group" aria-label="对话框操作按钮">
|
||||
<!-- 按钮将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 主题管理
|
||||
const ThemeManager = {
|
||||
init() {
|
||||
this.applyTheme();
|
||||
this.listenForThemeChanges();
|
||||
},
|
||||
|
||||
applyTheme() {
|
||||
// 优先从 Electron 主进程获取软件内主题状态
|
||||
if (window.electronAPI && window.electronAPI.getTheme) {
|
||||
window.electronAPI.getTheme().then(theme => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}).catch(() => {
|
||||
// 如果获取失败,使用系统主题
|
||||
this.useSystemTheme();
|
||||
});
|
||||
} else {
|
||||
this.useSystemTheme();
|
||||
}
|
||||
},
|
||||
|
||||
useSystemTheme() {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
listenForThemeChanges() {
|
||||
// 监听系统主题变化
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
// 如果软件主题配置为系统跟随,则更新主题
|
||||
if (!window.electronAPI || !window.electronAPI.getTheme) {
|
||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||
} else {
|
||||
// 重新获取软件内主题状态
|
||||
this.applyTheme();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 Electron 主题变化
|
||||
if (window.electronAPI && window.electronAPI.onThemeChanged) {
|
||||
window.electronAPI.onThemeChanged((theme) => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 拖拽管理
|
||||
const DragManager = {
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
|
||||
init() {
|
||||
const header = document.querySelector('.dialog-header');
|
||||
if (!header) return;
|
||||
|
||||
header.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
|
||||
// 防止文本选择
|
||||
header.addEventListener('selectstart', (e) => e.preventDefault());
|
||||
},
|
||||
|
||||
handleMouseDown(e) {
|
||||
// 只有在头部区域才能拖拽
|
||||
if (!e.target.closest('.dialog-header')) return;
|
||||
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
|
||||
const container = document.querySelector('.dialog-container');
|
||||
container.style.transition = 'none';
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
handleMouseMove(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - this.startX;
|
||||
const deltaY = e.clientY - this.startY;
|
||||
|
||||
// 通知 Electron 移动窗口
|
||||
if (window.electronAPI && window.electronAPI.moveWindow) {
|
||||
window.electronAPI.moveWindow(deltaX, deltaY);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
handleMouseUp(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
this.isDragging = false;
|
||||
|
||||
const container = document.querySelector('.dialog-container');
|
||||
container.style.transition = '';
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口加载完成后的初始化
|
||||
window.addEventListener('load', () => {
|
||||
// 初始化主题管理
|
||||
ThemeManager.init();
|
||||
});
|
||||
|
||||
// 获取传递的参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const data = JSON.parse(decodeURIComponent(urlParams.get('data') || '{}'));
|
||||
|
||||
// 设置对话框内容
|
||||
document.getElementById('dialog-title').textContent = data.title || '操作确认';
|
||||
document.getElementById('dialog-message').textContent = data.message || '是否要执行此操作?';
|
||||
|
||||
// 创建按钮
|
||||
const actionsContainer = document.getElementById('dialog-actions');
|
||||
const options = data.options || ['确定', '取消'];
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'dialog-button';
|
||||
button.textContent = option;
|
||||
|
||||
// 根据按钮文本设置样式
|
||||
if (option.includes('确定') || option.includes('是') || option.includes('同意')) {
|
||||
button.className += ' primary';
|
||||
} else if (option.includes('删除') || option.includes('危险')) {
|
||||
button.className += ' danger';
|
||||
}
|
||||
|
||||
// 绑定点击事件
|
||||
button.addEventListener('click', () => {
|
||||
// 添加点击动画
|
||||
button.style.transform = 'scale(0.98)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 100);
|
||||
|
||||
// 发送结果到主进程
|
||||
if (window.electronAPI && window.electronAPI.dialogResponse) {
|
||||
const choice = index === 0; // 第一个选项为 true
|
||||
window.electronAPI.dialogResponse(data.messageId, choice);
|
||||
}
|
||||
});
|
||||
|
||||
actionsContainer.appendChild(button);
|
||||
});
|
||||
|
||||
// 自动聚焦第一个按钮
|
||||
setTimeout(() => {
|
||||
const firstButton = actionsContainer.querySelector('.dialog-button');
|
||||
if (firstButton) {
|
||||
firstButton.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 键盘事件处理
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
// ESC 键相当于取消
|
||||
if (window.electronAPI && window.electronAPI.dialogResponse) {
|
||||
window.electronAPI.dialogResponse(data.messageId, false);
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
// Enter 键相当于确定
|
||||
const focusedButton = document.activeElement;
|
||||
if (focusedButton && focusedButton.classList.contains('dialog-button')) {
|
||||
focusedButton.click();
|
||||
} else {
|
||||
// 如果没有聚焦按钮,默认点击第一个
|
||||
const firstButton = actionsContainer.querySelector('.dialog-button');
|
||||
if (firstButton) {
|
||||
firstButton.click();
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Tab') {
|
||||
// Tab 键在按钮间切换
|
||||
const buttons = Array.from(actionsContainer.querySelectorAll('.dialog-button'));
|
||||
const currentIndex = buttons.indexOf(document.activeElement);
|
||||
|
||||
if (event.shiftKey) {
|
||||
// Shift+Tab 向前切换
|
||||
const prevIndex = currentIndex <= 0 ? buttons.length - 1 : currentIndex - 1;
|
||||
buttons[prevIndex].focus();
|
||||
} else {
|
||||
// Tab 向后切换
|
||||
const nextIndex = currentIndex >= buttons.length - 1 ? 0 : currentIndex + 1;
|
||||
buttons[nextIndex].focus();
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口加载完成后的初始化
|
||||
window.addEventListener('load', () => {
|
||||
// 初始化主题管理
|
||||
ThemeManager.init();
|
||||
|
||||
// 初始化拖拽管理
|
||||
DragManager.init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -171,10 +171,45 @@
|
||||
基建: {{ user.Info.InfrastMode === 'Normal' ? '普通' : '自定义' }}
|
||||
</a-tag>
|
||||
|
||||
<!-- 关卡信息 - Stage固定展示 -->
|
||||
<a-tag v-if="user.Info.Stage" class="info-tag" color="blue">
|
||||
关卡: {{ user.Info.Stage === '-' ? '未选择' : user.Info.Stage }}
|
||||
</a-tag>
|
||||
<!-- 关卡信息 - 根据是否使用计划表配置显示不同内容 -->
|
||||
<template v-if="user.Info.Stage === '1-7' && props.currentPlanData">
|
||||
<!-- 计划表模式信息 -->
|
||||
<a-tag
|
||||
v-if="props.currentPlanData.Info?.Mode"
|
||||
class="info-tag"
|
||||
color="purple"
|
||||
>
|
||||
模式:
|
||||
{{ props.currentPlanData.Info.Mode === 'ALL' ? '全局' : '周计划' }}
|
||||
</a-tag>
|
||||
|
||||
<!-- 显示计划表中的所有关卡 -->
|
||||
<template v-for="(stageInfo, index) in getAllPlanStages()" :key="index">
|
||||
<a-tag class="info-tag" color="green">
|
||||
{{ stageInfo.label }}: {{ stageInfo.value }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 如果没有配置任何关卡,显示提示 -->
|
||||
<a-tag
|
||||
v-if="getAllPlanStages().length === 0"
|
||||
class="info-tag"
|
||||
color="orange"
|
||||
>
|
||||
关卡: 计划表未配置
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 用户自定义关卡 -->
|
||||
<template v-else>
|
||||
<a-tag
|
||||
v-if="user.Info.Stage"
|
||||
class="info-tag"
|
||||
:color="getStageTagColor(user.Info.Stage)"
|
||||
>
|
||||
关卡: {{ getDisplayStage(user.Info.Stage) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 额外关卡 - 只有不为-或空时才显示 -->
|
||||
<a-tag
|
||||
@@ -327,6 +362,7 @@ import { message } from 'ant-design-vue'
|
||||
interface Props {
|
||||
scripts: Script[]
|
||||
activeConnections: Map<string, { subscriptionId: string; websocketId: string }>
|
||||
currentPlanData?: Record<string, any> | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -419,6 +455,12 @@ const getRemainingDayColor = (remainedDay: number): string => {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
// 获取关卡标签颜色
|
||||
const getStageTagColor = (stage: string): string => {
|
||||
if (stage === '1-7') return 'green' // 使用计划表配置用绿色
|
||||
return 'blue' // 自定义关卡用蓝色
|
||||
}
|
||||
|
||||
// 获取剩余天数的显示文本
|
||||
const getRemainingDayText = (remainedDay: number): string => {
|
||||
if (remainedDay === -1) return '剩余天数: 长期有效'
|
||||
@@ -426,6 +468,102 @@ const getRemainingDayText = (remainedDay: number): string => {
|
||||
return `剩余天数: ${remainedDay}天`
|
||||
}
|
||||
|
||||
// 获取关卡的显示文本
|
||||
const getDisplayStage = (stage: string): string => {
|
||||
if (stage === '-') return '未选择'
|
||||
|
||||
// 如果是默认值且有计划表数据,显示计划表中的实际关卡
|
||||
if (stage === '1-7' && props.currentPlanData) {
|
||||
const planStage = getCurrentPlanStage()
|
||||
if (planStage && planStage !== '-') {
|
||||
return planStage
|
||||
}
|
||||
return '使用计划表配置'
|
||||
}
|
||||
|
||||
return stage
|
||||
}
|
||||
|
||||
// 从计划表获取当前关卡
|
||||
const getCurrentPlanStage = (): string => {
|
||||
if (!props.currentPlanData) return ''
|
||||
|
||||
// 根据当前时间确定使用哪个时间段的配置
|
||||
const planMode = props.currentPlanData.Info?.Mode || 'ALL'
|
||||
let timeKey = 'ALL'
|
||||
|
||||
if (planMode === 'Weekly') {
|
||||
// 如果是周模式,根据当前星期几获取对应配置
|
||||
const today = new Date().getDay() // 0=Sunday, 1=Monday, ...
|
||||
const dayMap = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||
timeKey = dayMap[today]
|
||||
}
|
||||
|
||||
// 从计划表获取关卡配置
|
||||
const timeConfig = props.currentPlanData[timeKey]
|
||||
if (!timeConfig) return ''
|
||||
|
||||
// 获取主要关卡
|
||||
if (timeConfig.Stage && timeConfig.Stage !== '-') {
|
||||
return timeConfig.Stage
|
||||
}
|
||||
|
||||
// 如果主要关卡为空,尝试获取第一个备选关卡
|
||||
const backupStages = [timeConfig.Stage_1, timeConfig.Stage_2, timeConfig.Stage_3]
|
||||
for (const stage of backupStages) {
|
||||
if (stage && stage !== '-') {
|
||||
return stage
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 从计划表获取所有配置的关卡
|
||||
const getAllPlanStages = (): Array<{ label: string; value: string }> => {
|
||||
if (!props.currentPlanData) return []
|
||||
|
||||
// 根据当前时间确定使用哪个时间段的配置
|
||||
const planMode = props.currentPlanData.Info?.Mode || 'ALL'
|
||||
let timeKey = 'ALL'
|
||||
|
||||
if (planMode === 'Weekly') {
|
||||
// 如果是周模式,根据当前星期几获取对应配置
|
||||
const today = new Date().getDay() // 0=Sunday, 1=Monday, ...
|
||||
const dayMap = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||
timeKey = dayMap[today]
|
||||
}
|
||||
|
||||
// 从计划表获取关卡配置
|
||||
const timeConfig = props.currentPlanData[timeKey]
|
||||
if (!timeConfig) return []
|
||||
|
||||
const stages: Array<{ label: string; value: string }> = []
|
||||
|
||||
// 主关卡
|
||||
if (timeConfig.Stage && timeConfig.Stage !== '-') {
|
||||
stages.push({ label: '关卡', value: timeConfig.Stage })
|
||||
}
|
||||
|
||||
// 备选关卡
|
||||
if (timeConfig.Stage_1 && timeConfig.Stage_1 !== '-') {
|
||||
stages.push({ label: '关卡1', value: timeConfig.Stage_1 })
|
||||
}
|
||||
if (timeConfig.Stage_2 && timeConfig.Stage_2 !== '-') {
|
||||
stages.push({ label: '关卡2', value: timeConfig.Stage_2 })
|
||||
}
|
||||
if (timeConfig.Stage_3 && timeConfig.Stage_3 !== '-') {
|
||||
stages.push({ label: '关卡3', value: timeConfig.Stage_3 })
|
||||
}
|
||||
|
||||
// 剩余关卡
|
||||
if (timeConfig.Stage_Remain && timeConfig.Stage_Remain !== '-') {
|
||||
stages.push({ label: '剩余关卡', value: timeConfig.Stage_Remain })
|
||||
}
|
||||
|
||||
return stages
|
||||
}
|
||||
|
||||
// 处理脚本拖拽结束
|
||||
const onScriptDragEnd = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,69 +1,97 @@
|
||||
<template>
|
||||
<div style="display: none">
|
||||
<!-- 这是一个隐藏的监听组件,不需要UI -->
|
||||
<!-- 现在使用系统级对话框窗口而不是应用内弹窗 -->
|
||||
</div>
|
||||
|
||||
<!-- 简单的自定义对话框 -->
|
||||
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h3>{{ dialogData.title }}</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p>{{ dialogData.message }}</p>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
v-for="(option, index) in dialogData.options"
|
||||
:key="index"
|
||||
class="dialog-button"
|
||||
@click="handleChoice(index)"
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
|
||||
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
// WebSocket hook
|
||||
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
||||
|
||||
// 对话框状态
|
||||
const showDialog = ref(false)
|
||||
const dialogData = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
options: ['确定', '取消'],
|
||||
messageId: ''
|
||||
})
|
||||
|
||||
// 存储订阅ID用于取消订阅
|
||||
let subscriptionId: string
|
||||
|
||||
// 检查是否在 Electron 环境中
|
||||
const isElectron = () => {
|
||||
return typeof window !== 'undefined' && (window as any).electronAPI
|
||||
}
|
||||
|
||||
// 发送用户选择结果到后端
|
||||
const sendResponse = (messageId: string, choice: boolean) => {
|
||||
const response = {"choice": choice}
|
||||
const response = {
|
||||
message_id: messageId,
|
||||
choice: choice,
|
||||
}
|
||||
|
||||
logger.info('[WebSocket消息监听器] 发送用户选择结果:', response)
|
||||
|
||||
// 发送响应消息到后端
|
||||
sendRaw('Response', response, messageId)
|
||||
sendRaw('Response', response)
|
||||
}
|
||||
|
||||
// 显示系统级问题对话框
|
||||
const showQuestion = async (questionData: any) => {
|
||||
|
||||
// 处理用户选择
|
||||
const handleChoice = (choiceIndex: number) => {
|
||||
const choice = choiceIndex === 0 // 第一个选项为true,其他为false
|
||||
sendResponse(dialogData.value.messageId, choice)
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
// 显示问题对话框
|
||||
const showQuestion = (questionData: any) => {
|
||||
const title = questionData.title || '操作提示'
|
||||
const message = questionData.message || ''
|
||||
const options = questionData.options || ['确定', '取消']
|
||||
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
||||
|
||||
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
|
||||
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
|
||||
|
||||
if (!isElectron()) {
|
||||
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
|
||||
// 在非 Electron 环境中,使用默认响应
|
||||
sendResponse(messageId, false)
|
||||
return
|
||||
// 设置对话框数据
|
||||
dialogData.value = {
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 Electron API 显示系统级对话框
|
||||
const result = await (window as any).electronAPI.showQuestionDialog({
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
})
|
||||
|
||||
logger.info('[WebSocket消息监听器] 系统级对话框返回结果:', result)
|
||||
|
||||
// 发送结果到后端
|
||||
sendResponse(messageId, result)
|
||||
} catch (error) {
|
||||
logger.error('[WebSocket消息监听器] 显示系统级对话框失败:', error)
|
||||
// 出错时发送默认响应
|
||||
sendResponse(messageId, false)
|
||||
}
|
||||
showDialog.value = true
|
||||
|
||||
// 在下一个tick自动聚焦第一个按钮
|
||||
nextTick(() => {
|
||||
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
|
||||
if (firstButton) {
|
||||
firstButton.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 消息处理函数
|
||||
@@ -109,14 +137,14 @@ const handleObjectMessage = (data: any) => {
|
||||
logger.info('[WebSocket消息监听器] 发现Question类型消息')
|
||||
|
||||
if (data.message_id) {
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示系统级对话框')
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示选择弹窗')
|
||||
showQuestion(data)
|
||||
return
|
||||
} else {
|
||||
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
||||
// 即使缺少message_id,也尝试显示对话框,使用当前时间戳作为ID
|
||||
// 即使缺少message_id,也尝试显示弹窗,使用当前时间戳作为ID
|
||||
const fallbackId = 'fallback_' + Date.now()
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
|
||||
showQuestion({
|
||||
...data,
|
||||
message_id: fallbackId
|
||||
@@ -182,4 +210,150 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 对话框遮罩层 */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 对话框容器 */
|
||||
.dialog-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
animation: dialogAppear 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* 对话框头部 */
|
||||
.dialog-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 对话框内容 */
|
||||
.dialog-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dialog-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 按钮区域 */
|
||||
.dialog-actions {
|
||||
padding: 12px 20px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.dialog-button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.dialog-button:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dialog-button:first-child {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.dialog-button:first-child:hover {
|
||||
background: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
/* 出现动画 */
|
||||
@keyframes dialogAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dialog-container {
|
||||
background: #2d2d2d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-content p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
background: #444;
|
||||
color: #fff;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: #555;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.dialog-button:first-child {
|
||||
background: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.dialog-button:first-child:hover {
|
||||
background: #0b5ed7;
|
||||
border-color: #0b5ed7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,630 +1 @@
|
||||
<template>
|
||||
<div class="backend-launch-page">
|
||||
<div class="section">
|
||||
<h3 class="section-title">🚀 后端服务控制</h3>
|
||||
|
||||
<!-- 后端状态显示 -->
|
||||
<div class="status-card" :class="{ running: isBackendRunning, stopped: !isBackendRunning }">
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" :class="{ active: isBackendRunning }"></span>
|
||||
<span class="status-text">
|
||||
{{ isBackendRunning ? '运行中' : '已停止' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="backendPid" class="pid-info">
|
||||
PID: {{ backendPid }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
@click="startBackend"
|
||||
:disabled="isLoading || isBackendRunning"
|
||||
class="action-btn start-btn"
|
||||
>
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<span v-else>▶️</span>
|
||||
启动后端
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="stopBackend"
|
||||
:disabled="isLoading || !isBackendRunning"
|
||||
class="action-btn stop-btn"
|
||||
>
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<span v-else>⏹️</span>
|
||||
停止后端
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="refreshStatus"
|
||||
:disabled="isLoading"
|
||||
class="action-btn refresh-btn"
|
||||
>
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<span v-else>🔄</span>
|
||||
刷新状态
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 操作结果显示 -->
|
||||
<div v-if="lastResult" class="result-card" :class="{ success: lastResult.success, error: !lastResult.success }">
|
||||
<div class="result-title">
|
||||
{{ lastResult.success ? '✅ 操作成功' : '❌ 操作失败' }}
|
||||
</div>
|
||||
<div v-if="lastResult.message" class="result-message">
|
||||
{{ lastResult.message }}
|
||||
</div>
|
||||
<div v-if="lastResult.error" class="result-error">
|
||||
错误: {{ lastResult.error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进程信息 -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">📊 进程信息</h3>
|
||||
|
||||
<div class="process-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Python路径:</span>
|
||||
<span class="info-value">{{ pythonPath || '未检测到' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">主文件:</span>
|
||||
<span class="info-value">{{ mainPyPath || '未检测到' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">工作目录:</span>
|
||||
<span class="info-value">{{ workingDir || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="getProcessInfo" :disabled="isLoading" class="action-btn info-btn">
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<span v-else>🔍</span>
|
||||
获取进程信息
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">⚡ 快速操作</h3>
|
||||
|
||||
<div class="quick-actions">
|
||||
<button @click="restartBackend" :disabled="isLoading" class="action-btn restart-btn">
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<span v-else>🔄</span>
|
||||
重启后端
|
||||
</button>
|
||||
|
||||
<button @click="forceKillProcesses" :disabled="isLoading" class="action-btn kill-btn">
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<span v-else>💀</span>
|
||||
强制结束所有进程
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志区域 -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">📝 操作日志</h3>
|
||||
|
||||
<div class="log-container">
|
||||
<div v-if="logs.length === 0" class="no-logs">
|
||||
暂无日志记录
|
||||
</div>
|
||||
<div v-else class="log-entries">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
class="log-entry"
|
||||
:class="log.type"
|
||||
>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="clearLogs" class="action-btn clear-btn">
|
||||
🗑️ 清空日志
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 临时的类型断言,确保能访问到完整的electronAPI
|
||||
const electronAPI = (window as any).electronAPI
|
||||
|
||||
// 状态管理
|
||||
const isBackendRunning = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const backendPid = ref<number | null>(null)
|
||||
const lastResult = ref<{ success: boolean; message?: string; error?: string } | null>(null)
|
||||
|
||||
// 进程信息
|
||||
const pythonPath = ref<string>('')
|
||||
const mainPyPath = ref<string>('')
|
||||
const workingDir = ref<string>('')
|
||||
|
||||
// 日志管理
|
||||
const logs = ref<Array<{ time: string; message: string; type: 'info' | 'success' | 'error' }>>([])
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message: string, type: 'info' | 'success' | 'error' = 'info') => {
|
||||
const now = new Date()
|
||||
const time = now.toLocaleTimeString()
|
||||
logs.value.unshift({ time, message, type })
|
||||
|
||||
// 限制日志数量
|
||||
if (logs.value.length > 50) {
|
||||
logs.value = logs.value.slice(0, 50)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
logs.value = []
|
||||
addLog('日志已清空', 'info')
|
||||
}
|
||||
|
||||
// 启动后端
|
||||
const startBackend = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
lastResult.value = null
|
||||
addLog('正在启动后端服务...', 'info')
|
||||
|
||||
try {
|
||||
const result = await electronAPI.startBackend()
|
||||
|
||||
if (result.success) {
|
||||
lastResult.value = { success: true, message: '后端服务启动成功' }
|
||||
addLog('✅ 后端服务启动成功', 'success')
|
||||
await refreshStatus()
|
||||
} else {
|
||||
lastResult.value = { success: false, error: result.error }
|
||||
addLog(`❌ 后端服务启动失败: ${result.error}`, 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
lastResult.value = { success: false, error: errorMsg }
|
||||
addLog(`❌ 启动后端时出现异常: ${errorMsg}`, 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停止后端
|
||||
const stopBackend = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
lastResult.value = null
|
||||
addLog('正在停止后端服务...', 'info')
|
||||
|
||||
try {
|
||||
// 检查stopBackend方法是否存在
|
||||
if (electronAPI.stopBackend) {
|
||||
const result = await electronAPI.stopBackend()
|
||||
|
||||
if (result.success) {
|
||||
lastResult.value = { success: true, message: '后端服务已停止' }
|
||||
addLog('✅ 后端服务已停止', 'success')
|
||||
await refreshStatus()
|
||||
} else {
|
||||
lastResult.value = { success: false, error: result.error }
|
||||
addLog(`❌ 停止后端服务失败: ${result.error}`, 'error')
|
||||
}
|
||||
} else {
|
||||
// 如果没有stopBackend方法,使用强制结束进程的方式
|
||||
addLog('ℹ️ 使用强制结束进程的方式停止后端', 'info')
|
||||
await forceKillProcesses()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
lastResult.value = { success: false, error: errorMsg }
|
||||
addLog(`❌ 停止后端时出现异常: ${errorMsg}`, 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重启后端
|
||||
const restartBackend = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
addLog('正在重启后端服务...', 'info')
|
||||
|
||||
// 先停止
|
||||
if (isBackendRunning.value) {
|
||||
await stopBackend()
|
||||
// 等待一秒确保完全停止
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
// 再启动
|
||||
await startBackend()
|
||||
}
|
||||
|
||||
// 强制结束所有相关进程
|
||||
const forceKillProcesses = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
addLog('正在强制结束所有相关进程...', 'info')
|
||||
|
||||
try {
|
||||
const result = await electronAPI.killAllProcesses()
|
||||
|
||||
if (result.success) {
|
||||
lastResult.value = { success: true, message: '所有相关进程已强制结束' }
|
||||
addLog('✅ 所有相关进程已强制结束', 'success')
|
||||
await refreshStatus()
|
||||
} else {
|
||||
lastResult.value = { success: false, error: result.error }
|
||||
addLog(`❌ 强制结束进程失败: ${result.error}`, 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
lastResult.value = { success: false, error: errorMsg }
|
||||
addLog(`❌ 强制结束进程时出现异常: ${errorMsg}`, 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
const refreshStatus = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
addLog('正在刷新后端状态...', 'info')
|
||||
|
||||
try {
|
||||
// 获取相关进程信息
|
||||
const processes = await electronAPI.getRelatedProcesses()
|
||||
|
||||
// 检查是否有Python进程在运行main.py
|
||||
const backendProcess = processes.find((proc: any) =>
|
||||
proc.command && proc.command.includes('main.py')
|
||||
)
|
||||
|
||||
if (backendProcess) {
|
||||
isBackendRunning.value = true
|
||||
backendPid.value = backendProcess.pid
|
||||
addLog(`✅ 检测到后端进程 (PID: ${backendProcess.pid})`, 'success')
|
||||
} else {
|
||||
isBackendRunning.value = false
|
||||
backendPid.value = null
|
||||
addLog('ℹ️ 未检测到后端进程', 'info')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
addLog(`❌ 刷新状态失败: ${errorMsg}`, 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进程信息
|
||||
const getProcessInfo = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
addLog('正在获取进程信息...', 'info')
|
||||
|
||||
try {
|
||||
// 这里可以调用一些API来获取Python路径等信息
|
||||
// 暂时使用模拟数据
|
||||
pythonPath.value = 'environment/python/python.exe'
|
||||
mainPyPath.value = 'main.py'
|
||||
workingDir.value = window.location.origin
|
||||
|
||||
addLog('✅ 进程信息获取完成', 'success')
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
addLog(`❌ 获取进程信息失败: ${errorMsg}`, 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 定时刷新状态
|
||||
let statusInterval: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
addLog('📱 后端控制面板已加载', 'info')
|
||||
|
||||
// 初始化时获取状态
|
||||
refreshStatus()
|
||||
getProcessInfo()
|
||||
|
||||
// 每5秒自动刷新状态
|
||||
statusInterval = setInterval(() => {
|
||||
refreshStatus()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backend-launch-page {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
/* 状态卡片 */
|
||||
.status-card {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-card.running {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-color: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.status-card.stopped {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-color: rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #f44336;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pid-info {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.start-btn:hover:not(:disabled) {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
border-color: rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
|
||||
.stop-btn:hover:not(:disabled) {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
border-color: rgba(244, 67, 54, 0.5);
|
||||
}
|
||||
|
||||
.restart-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 193, 7, 0.3);
|
||||
border-color: rgba(255, 193, 7, 0.5);
|
||||
}
|
||||
|
||||
.kill-btn:hover:not(:disabled) {
|
||||
background: rgba(156, 39, 176, 0.3);
|
||||
border-color: rgba(156, 39, 176, 0.5);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 结果卡片 */
|
||||
.result-card {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.result-card.success {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-color: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.result-card.error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-color: rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.result-message, .result-error {
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* 进程信息 */
|
||||
.process-info {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 60px;
|
||||
color: #888;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #fff;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 日志区域 */
|
||||
.log-container {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.no-logs {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.log-entries {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 2px 4px;
|
||||
font-size: 9px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #666;
|
||||
font-size: 8px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
.log-entry.error .log-message {
|
||||
|
||||
@@ -74,7 +74,7 @@ import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
const { subscribe, unsubscribe, getConnectionInfo } = useWebSocket()
|
||||
const { subscribe, unsubscribe, sendRaw, getConnectionInfo } = useWebSocket()
|
||||
|
||||
// 测试状态
|
||||
const isTesting = ref(false)
|
||||
@@ -165,39 +165,27 @@ const directTriggerModal = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 直接调用弹窗API测试功能
|
||||
// 发送WebSocket消息来模拟接收消息
|
||||
const simulateMessage = (messageData: any) => {
|
||||
logger.info('[调试工具] 直接测试弹窗功能:', messageData)
|
||||
logger.info('[调试工具] 发送模拟消息:', messageData)
|
||||
|
||||
// 检查连接状态
|
||||
const connInfo = getConnectionInfo()
|
||||
if (connInfo.status !== '已连接') {
|
||||
logger.warn('[调试工具] WebSocket未连接,无法发送消息')
|
||||
lastResponse.value = '发送失败: WebSocket未连接'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否在Electron环境
|
||||
if (typeof window !== 'undefined' && (window as any).electronAPI?.showQuestionDialog) {
|
||||
// 直接调用Electron的弹窗API进行测试
|
||||
(window as any).electronAPI.showQuestionDialog({
|
||||
title: messageData.title || '测试标题',
|
||||
message: messageData.message || '测试消息',
|
||||
options: messageData.options || ['确定', '取消'],
|
||||
messageId: messageData.message_id || 'test-' + Date.now()
|
||||
}).then((result: boolean) => {
|
||||
logger.info('[调试工具] 弹窗测试结果:', result)
|
||||
const choice = result ? '确认' : '取消'
|
||||
lastResponse.value = `用户选择: ${choice}`
|
||||
addTestHistory('弹窗测试', choice)
|
||||
}).catch((error: any) => {
|
||||
logger.error('[调试工具] 弹窗测试失败:', error)
|
||||
lastResponse.value = '弹窗测试失败: ' + (error?.message || '未知错误')
|
||||
})
|
||||
} else {
|
||||
logger.warn('[调试工具] 不在Electron环境中或API不可用,使用浏览器confirm作为备用')
|
||||
const result = confirm(`${messageData.title || '测试'}\n\n${messageData.message || '这是测试消息'}`)
|
||||
const choice = result ? '确认' : '取消'
|
||||
lastResponse.value = `用户选择: ${choice} (浏览器备用)`
|
||||
addTestHistory('浏览器备用测试', choice)
|
||||
}
|
||||
// 使用sendRaw直接发送Message类型的消息
|
||||
sendRaw('Message', messageData)
|
||||
|
||||
logger.info('[调试工具] 消息已发送到WebSocket')
|
||||
lastResponse.value = '消息已发送,等待弹窗显示...'
|
||||
} catch (error: any) {
|
||||
logger.error('[调试工具] 测试弹窗失败:', error)
|
||||
lastResponse.value = '测试失败: ' + (error?.message || '未知错误')
|
||||
logger.error('[调试工具] 发送消息失败:', error)
|
||||
lastResponse.value = '发送失败: ' + (error?.message || '未知错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,28 +44,22 @@ import RouteInfoPage from './RouteInfoPage.vue'
|
||||
import EnvironmentPage from './EnvironmentPage.vue'
|
||||
import QuickNavPage from './QuickNavPage.vue'
|
||||
import MessageTestPage from './MessageTestPage.vue'
|
||||
import BackendLaunchPage from './BackendLaunchPage.vue'
|
||||
|
||||
// 调试页面配置
|
||||
const tabs = [
|
||||
{ key: 'route', title: '路由', icon: '🛣️', component: RouteInfoPage },
|
||||
{ key: 'env', title: '环境', icon: '⚙️', component: EnvironmentPage },
|
||||
{ key: 'backend', title: '后端', icon: '🚀', component: BackendLaunchPage },
|
||||
{ key: 'nav', title: '导航', icon: '🧭', component: QuickNavPage },
|
||||
{ key: 'nav', title: '导航', icon: '🚀', component: QuickNavPage },
|
||||
{ key: 'message', title: '消息', icon: '💬', component: MessageTestPage },
|
||||
]
|
||||
|
||||
// 开发环境检测
|
||||
const isDev = ref(
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
(import.meta as any).env?.DEV === true ||
|
||||
window.location.hostname === 'localhost'
|
||||
)
|
||||
const isDev = ref(process.env.NODE_ENV === 'development' || import.meta.env?.DEV === true)
|
||||
|
||||
// 面板状态
|
||||
const isCollapsed = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const activeTab = ref('backend') // 默认显示后端页面
|
||||
const activeTab = ref('route')
|
||||
|
||||
// 面板位置
|
||||
const panelPosition = ref({
|
||||
|
||||
@@ -71,9 +71,9 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getConfig } from '@/utils/config'
|
||||
import { mirrorManager } from '@/utils/mirrorManager'
|
||||
import router from '@/router'
|
||||
import { useUpdateChecker } from '@/composables/useUpdateChecker'
|
||||
import { connectAfterBackendStart } from '@/composables/useWebSocket'
|
||||
import { forceEnterApp } from '@/utils/appEntry'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
// Props
|
||||
@@ -154,12 +154,11 @@ function handleForceEnter() {
|
||||
}
|
||||
|
||||
// 确认弹窗中的"我知道我在做什么"按钮,直接进入应用
|
||||
async function handleForceEnterConfirm() {
|
||||
function handleForceEnterConfirm() {
|
||||
clearTimers()
|
||||
aborted.value = true
|
||||
forceEnterVisible.value = false
|
||||
|
||||
await forceEnterApp('自动模式-强行进入确认')
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
// 事件处理 - 增强重新配置环境按钮功能
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { forceEnterApp } from '@/utils/appEntry'
|
||||
import router from '@/router'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
@@ -79,8 +79,8 @@ function handleReconfigure() {
|
||||
}
|
||||
|
||||
// 强行进入应用
|
||||
async function handleForceEnter() {
|
||||
await forceEnterApp('环境不完整-强行进入')
|
||||
function handleForceEnter() {
|
||||
router.push('/home')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
531
frontend/src/composables/usePlanDataCoordinator.ts
Normal file
531
frontend/src/composables/usePlanDataCoordinator.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* 计划表数据协调层
|
||||
*
|
||||
* 作为前端架构中的"交通指挥中心",负责:
|
||||
* 1. 统一管理数据流
|
||||
* 2. 协调视图间的同步
|
||||
* 3. 处理与后端的通信
|
||||
* 4. 提供统一的数据访问接口
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import type { MaaPlanConfig, MaaPlanConfig_Item } from '@/api'
|
||||
|
||||
// 时间维度常量
|
||||
export const TIME_KEYS = ['ALL', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] as const
|
||||
export type TimeKey = typeof TIME_KEYS[number]
|
||||
|
||||
// 关卡槽位常量
|
||||
export const STAGE_SLOTS = ['Stage', 'Stage_1', 'Stage_2', 'Stage_3'] as const
|
||||
export type StageSlot = typeof STAGE_SLOTS[number]
|
||||
|
||||
// 统一的数据结构
|
||||
export interface PlanDataState {
|
||||
// 基础信息
|
||||
info: {
|
||||
name: string
|
||||
mode: 'ALL' | 'Weekly'
|
||||
type: string
|
||||
}
|
||||
|
||||
// 时间维度的配置数据
|
||||
timeConfigs: Record<TimeKey, {
|
||||
medicineNumb: number
|
||||
seriesNumb: string
|
||||
stages: {
|
||||
primary: string // Stage
|
||||
backup1: string // Stage_1
|
||||
backup2: string // Stage_2
|
||||
backup3: string // Stage_3
|
||||
remain: string // Stage_Remain
|
||||
}
|
||||
}>
|
||||
|
||||
// 自定义关卡定义
|
||||
customStageDefinitions: {
|
||||
custom_stage_1: string
|
||||
custom_stage_2: string
|
||||
custom_stage_3: string
|
||||
}
|
||||
}
|
||||
|
||||
// 关卡可用性信息
|
||||
export interface StageAvailability {
|
||||
value: string
|
||||
text: string
|
||||
days: number[]
|
||||
}
|
||||
|
||||
export const STAGE_DAILY_INFO: StageAvailability[] = [
|
||||
{ value: '-', text: '当前/上次', days: [1, 2, 3, 4, 5, 6, 7] },
|
||||
{ value: '1-7', text: '1-7', days: [1, 2, 3, 4, 5, 6, 7] },
|
||||
{ value: 'R8-11', text: 'R8-11', days: [1, 2, 3, 4, 5, 6, 7] },
|
||||
{ value: '12-17-HARD', text: '12-17-HARD', days: [1, 2, 3, 4, 5, 6, 7] },
|
||||
{ value: 'LS-6', text: '经验-6/5', days: [1, 2, 3, 4, 5, 6, 7] },
|
||||
{ value: 'CE-6', text: '龙门币-6/5', days: [2, 4, 6, 7] },
|
||||
{ value: 'AP-5', text: '红票-5', days: [1, 4, 6, 7] },
|
||||
{ value: 'CA-5', text: '技能-5', days: [2, 3, 5, 7] },
|
||||
{ value: 'SK-5', text: '碳-5', days: [1, 3, 5, 6] },
|
||||
{ value: 'PR-A-1', text: '奶/盾芯片', days: [1, 4, 5, 7] },
|
||||
{ value: 'PR-A-2', text: '奶/盾芯片组', days: [1, 4, 5, 7] },
|
||||
{ value: 'PR-B-1', text: '术/狙芯片', days: [1, 2, 5, 6] },
|
||||
{ value: 'PR-B-2', text: '术/狙芯片组', days: [1, 2, 5, 6] },
|
||||
{ value: 'PR-C-1', text: '先/辅芯片', days: [3, 4, 6, 7] },
|
||||
{ value: 'PR-C-2', text: '先/辅芯片组', days: [3, 4, 6, 7] },
|
||||
{ value: 'PR-D-1', text: '近/特芯片', days: [2, 3, 6, 7] },
|
||||
{ value: 'PR-D-2', text: '近/特芯片组', days: [2, 3, 6, 7] },
|
||||
]
|
||||
|
||||
/**
|
||||
* 计划表数据协调器
|
||||
*/
|
||||
export function usePlanDataCoordinator() {
|
||||
// 当前计划表ID
|
||||
const currentPlanId = ref<string>('default')
|
||||
|
||||
// localStorage 相关函数
|
||||
const CUSTOM_STAGE_KEY_PREFIX = 'maa_custom_stage_definitions_'
|
||||
|
||||
const getStorageKey = (): string => {
|
||||
return `${CUSTOM_STAGE_KEY_PREFIX}${currentPlanId.value}`
|
||||
}
|
||||
|
||||
const getDefaultCustomStageDefinitions = () => ({
|
||||
custom_stage_1: '',
|
||||
custom_stage_2: '',
|
||||
custom_stage_3: '',
|
||||
})
|
||||
|
||||
const loadCustomStageDefinitionsFromStorage = () => {
|
||||
try {
|
||||
const storageKey = getStorageKey()
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
// 确保包含所有必需的键
|
||||
return {
|
||||
custom_stage_1: parsed.custom_stage_1 || '',
|
||||
custom_stage_2: parsed.custom_stage_2 || '',
|
||||
custom_stage_3: parsed.custom_stage_3 || '',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自定义关卡] localStorage 恢复失败:', error)
|
||||
}
|
||||
|
||||
return getDefaultCustomStageDefinitions()
|
||||
}
|
||||
|
||||
const saveCustomStageDefinitionsToStorage = (definitions: Record<string, string>) => {
|
||||
try {
|
||||
const storageKey = getStorageKey()
|
||||
localStorage.setItem(storageKey, JSON.stringify(definitions))
|
||||
// 只在开发环境输出详细日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[自定义关卡] 保存到 localStorage (${currentPlanId.value})`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自定义关卡] localStorage 保存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 单一数据源
|
||||
const planData = ref<PlanDataState>({
|
||||
info: {
|
||||
name: '',
|
||||
mode: 'ALL',
|
||||
type: 'MaaPlanConfig'
|
||||
},
|
||||
timeConfigs: {} as Record<TimeKey, any>,
|
||||
customStageDefinitions: loadCustomStageDefinitionsFromStorage()
|
||||
})
|
||||
|
||||
// 初始化时间配置
|
||||
const initializeTimeConfigs = () => {
|
||||
TIME_KEYS.forEach(timeKey => {
|
||||
planData.value.timeConfigs[timeKey] = {
|
||||
medicineNumb: 0,
|
||||
seriesNumb: '0',
|
||||
stages: {
|
||||
primary: '-',
|
||||
backup1: '-',
|
||||
backup2: '-',
|
||||
backup3: '-',
|
||||
remain: '-'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
initializeTimeConfigs()
|
||||
|
||||
// 从API数据转换为内部数据结构
|
||||
const fromApiData = (apiData: MaaPlanConfig) => {
|
||||
// 更新基础信息
|
||||
if (apiData.Info) {
|
||||
planData.value.info.name = apiData.Info.Name || ''
|
||||
planData.value.info.mode = apiData.Info.Mode || 'ALL'
|
||||
|
||||
// 如果API数据中包含计划表ID信息,更新当前planId
|
||||
// 注意:这里假设planId通过其他方式传入,API数据本身可能不包含ID
|
||||
}
|
||||
|
||||
// 更新时间配置
|
||||
TIME_KEYS.forEach(timeKey => {
|
||||
const timeData = apiData[timeKey] as MaaPlanConfig_Item
|
||||
if (timeData) {
|
||||
planData.value.timeConfigs[timeKey] = {
|
||||
medicineNumb: timeData.MedicineNumb || 0,
|
||||
seriesNumb: timeData.SeriesNumb || '0',
|
||||
stages: {
|
||||
primary: timeData.Stage || '-',
|
||||
backup1: timeData.Stage_1 || '-',
|
||||
backup2: timeData.Stage_2 || '-',
|
||||
backup3: timeData.Stage_3 || '-',
|
||||
remain: timeData.Stage_Remain || '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 更新自定义关卡定义
|
||||
const customStages = (apiData.ALL as any)?.customStageDefinitions
|
||||
if (customStages && typeof customStages === 'object') {
|
||||
// 只在开发环境输出详细日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[自定义关卡] 从后端数据恢复 (${currentPlanId.value})`)
|
||||
}
|
||||
const newDefinitions = {
|
||||
custom_stage_1: customStages.custom_stage_1 || '',
|
||||
custom_stage_2: customStages.custom_stage_2 || '',
|
||||
custom_stage_3: customStages.custom_stage_3 || '',
|
||||
}
|
||||
|
||||
// 只有当定义真的不同时才更新和保存
|
||||
const hasChanged = JSON.stringify(newDefinitions) !== JSON.stringify(planData.value.customStageDefinitions)
|
||||
|
||||
if (hasChanged) {
|
||||
planData.value.customStageDefinitions = newDefinitions
|
||||
// 同步到 localStorage
|
||||
saveCustomStageDefinitionsToStorage(planData.value.customStageDefinitions)
|
||||
}
|
||||
} else {
|
||||
// 只在开发环境输出日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[自定义关卡] 使用 localStorage 数据 (${currentPlanId.value})`)
|
||||
}
|
||||
// 如果后端没有自定义关卡定义,使用 localStorage 中的值
|
||||
const storedDefinitions = loadCustomStageDefinitionsFromStorage()
|
||||
planData.value.customStageDefinitions = storedDefinitions
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为API数据格式
|
||||
const toApiData = (): MaaPlanConfig => {
|
||||
const result: MaaPlanConfig = {
|
||||
Info: {
|
||||
Name: planData.value.info.name,
|
||||
Mode: planData.value.info.mode
|
||||
}
|
||||
}
|
||||
|
||||
TIME_KEYS.forEach(timeKey => {
|
||||
const config = planData.value.timeConfigs[timeKey]
|
||||
result[timeKey] = {
|
||||
MedicineNumb: config.medicineNumb,
|
||||
SeriesNumb: config.seriesNumb as any,
|
||||
Stage: config.stages.primary,
|
||||
Stage_1: config.stages.backup1,
|
||||
Stage_2: config.stages.backup2,
|
||||
Stage_3: config.stages.backup3,
|
||||
Stage_Remain: config.stages.remain
|
||||
}
|
||||
})
|
||||
|
||||
// 在ALL中包含自定义关卡定义
|
||||
if (result.ALL) {
|
||||
(result.ALL as any).customStageDefinitions = planData.value.customStageDefinitions
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 配置视图数据适配器
|
||||
const configViewData = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'MedicineNumb',
|
||||
taskName: '吃理智药',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.medicineNumb || 0
|
||||
])
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'SeriesNumb',
|
||||
taskName: '连战次数',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.seriesNumb || '0'
|
||||
])
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Stage',
|
||||
taskName: '关卡选择',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.stages.primary || '-'
|
||||
])
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Stage_1',
|
||||
taskName: '备选关卡-1',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.stages.backup1 || '-'
|
||||
])
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Stage_2',
|
||||
taskName: '备选关卡-2',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.stages.backup2 || '-'
|
||||
])
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Stage_3',
|
||||
taskName: '备选关卡-3',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.stages.backup3 || '-'
|
||||
])
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Stage_Remain',
|
||||
taskName: '剩余理智关卡',
|
||||
...Object.fromEntries(
|
||||
TIME_KEYS.map(timeKey => [
|
||||
timeKey,
|
||||
planData.value.timeConfigs[timeKey]?.stages.remain || '-'
|
||||
])
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 简化视图数据适配器
|
||||
const simpleViewData = computed(() => {
|
||||
const result: any[] = []
|
||||
|
||||
// 添加自定义关卡
|
||||
Object.entries(planData.value.customStageDefinitions).forEach(([, stageName]) => {
|
||||
if (stageName && stageName.trim()) {
|
||||
const stageStates: Record<string, boolean> = {}
|
||||
TIME_KEYS.forEach(timeKey => {
|
||||
const config = planData.value.timeConfigs[timeKey]
|
||||
stageStates[timeKey] = Object.values(config.stages).includes(stageName)
|
||||
})
|
||||
|
||||
result.push({
|
||||
key: stageName,
|
||||
taskName: stageName,
|
||||
isCustom: true,
|
||||
stageName: stageName,
|
||||
...stageStates
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 添加标准关卡
|
||||
STAGE_DAILY_INFO.filter(stage => stage.value !== '-').forEach(stage => {
|
||||
const stageStates: Record<string, boolean> = {}
|
||||
TIME_KEYS.forEach(timeKey => {
|
||||
const config = planData.value.timeConfigs[timeKey]
|
||||
stageStates[timeKey] = Object.values(config.stages).includes(stage.value)
|
||||
})
|
||||
|
||||
result.push({
|
||||
key: stage.value,
|
||||
taskName: stage.text,
|
||||
isCustom: false,
|
||||
stageName: stage.value,
|
||||
...stageStates
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 更新配置数据
|
||||
const updateConfig = (timeKey: TimeKey, field: string, value: any) => {
|
||||
if (field === 'MedicineNumb') {
|
||||
planData.value.timeConfigs[timeKey].medicineNumb = value
|
||||
} else if (field === 'SeriesNumb') {
|
||||
planData.value.timeConfigs[timeKey].seriesNumb = value
|
||||
} else if (field === 'Stage') {
|
||||
planData.value.timeConfigs[timeKey].stages.primary = value
|
||||
} else if (field === 'Stage_1') {
|
||||
planData.value.timeConfigs[timeKey].stages.backup1 = value
|
||||
} else if (field === 'Stage_2') {
|
||||
planData.value.timeConfigs[timeKey].stages.backup2 = value
|
||||
} else if (field === 'Stage_3') {
|
||||
planData.value.timeConfigs[timeKey].stages.backup3 = value
|
||||
} else if (field === 'Stage_Remain') {
|
||||
planData.value.timeConfigs[timeKey].stages.remain = value
|
||||
}
|
||||
}
|
||||
|
||||
// 切换关卡状态(简化视图用)
|
||||
const toggleStage = (stageName: string, timeKey: TimeKey, enabled: boolean) => {
|
||||
const config = planData.value.timeConfigs[timeKey]
|
||||
const stageSlots = ['primary', 'backup1', 'backup2', 'backup3'] as const
|
||||
|
||||
if (enabled) {
|
||||
// 找到第一个空槽位
|
||||
const emptySlot = stageSlots.find(slot =>
|
||||
!config.stages[slot] || config.stages[slot] === '-'
|
||||
)
|
||||
if (emptySlot) {
|
||||
config.stages[emptySlot] = stageName
|
||||
}
|
||||
// 启用后重新按简化视图顺序排列
|
||||
reassignSlotsBySimpleViewOrder(timeKey)
|
||||
} else {
|
||||
// 从所有槽位中移除
|
||||
stageSlots.forEach(slot => {
|
||||
if (config.stages[slot] === stageName) {
|
||||
config.stages[slot] = '-'
|
||||
}
|
||||
})
|
||||
// 移除后重新按简化视图顺序排列
|
||||
reassignSlotsBySimpleViewOrder(timeKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 按简化视图顺序重新分配槽位
|
||||
const reassignSlotsBySimpleViewOrder = (timeKey: TimeKey) => {
|
||||
const config = planData.value.timeConfigs[timeKey]
|
||||
const stageSlots = ['primary', 'backup1', 'backup2', 'backup3'] as const
|
||||
|
||||
// 收集当前已启用的关卡
|
||||
const enabledStages = Object.values(config.stages).filter(stage => stage && stage !== '-')
|
||||
|
||||
// 清空所有槽位
|
||||
stageSlots.forEach(slot => {
|
||||
config.stages[slot] = '-'
|
||||
})
|
||||
|
||||
// 按简化视图的实际显示顺序重新分配
|
||||
const sortedStages: string[] = []
|
||||
|
||||
// 1. 先添加自定义关卡(按 custom_stage_1, custom_stage_2, custom_stage_3 的顺序)
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const key = `custom_stage_${i}` as keyof typeof planData.value.customStageDefinitions
|
||||
const stageName = planData.value.customStageDefinitions[key]
|
||||
if (stageName && stageName.trim() && enabledStages.includes(stageName)) {
|
||||
sortedStages.push(stageName)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再添加标准关卡(按STAGE_DAILY_INFO的顺序,跳过'-')
|
||||
STAGE_DAILY_INFO.filter(stage => stage.value !== '-').forEach(stage => {
|
||||
if (enabledStages.includes(stage.value)) {
|
||||
sortedStages.push(stage.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 3. 按顺序分配到槽位:第1个→primary,第2个→backup1,第3个→backup2,第4个→backup3
|
||||
sortedStages.forEach((stageName, index) => {
|
||||
if (index < stageSlots.length) {
|
||||
config.stages[stageSlots[index]] = stageName
|
||||
}
|
||||
})
|
||||
|
||||
// 只在开发环境输出排序日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[关卡排序] ${timeKey}:`, sortedStages.join(' → '))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 更新自定义关卡定义
|
||||
const updateCustomStageDefinition = (index: 1 | 2 | 3, name: string) => {
|
||||
const key = `custom_stage_${index}` as keyof typeof planData.value.customStageDefinitions
|
||||
const oldName = planData.value.customStageDefinitions[key]
|
||||
|
||||
// 只在开发环境输出详细日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[自定义关卡] 保存关卡-${index}: "${oldName}" -> "${name}"`)
|
||||
}
|
||||
|
||||
planData.value.customStageDefinitions[key] = name
|
||||
|
||||
// 保存到 localStorage
|
||||
saveCustomStageDefinitionsToStorage(planData.value.customStageDefinitions)
|
||||
|
||||
// 如果名称改变了,需要更新所有引用
|
||||
if (oldName !== name) {
|
||||
TIME_KEYS.forEach(timeKey => {
|
||||
const config = planData.value.timeConfigs[timeKey]
|
||||
Object.keys(config.stages).forEach(stageKey => {
|
||||
if (config.stages[stageKey as keyof typeof config.stages] === oldName) {
|
||||
config.stages[stageKey as keyof typeof config.stages] = name || '-'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新计划表ID
|
||||
const updatePlanId = (newPlanId: string) => {
|
||||
if (currentPlanId.value !== newPlanId) {
|
||||
// 只在开发环境输出日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[自定义关卡] 计划表切换: ${currentPlanId.value} -> ${newPlanId}`)
|
||||
}
|
||||
|
||||
currentPlanId.value = newPlanId
|
||||
|
||||
// 重新加载自定义关卡定义
|
||||
const newDefinitions = loadCustomStageDefinitionsFromStorage()
|
||||
planData.value.customStageDefinitions = newDefinitions
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 数据
|
||||
planData: planData.value,
|
||||
|
||||
// 视图适配器
|
||||
configViewData,
|
||||
simpleViewData,
|
||||
|
||||
// 数据转换
|
||||
fromApiData,
|
||||
toApiData,
|
||||
|
||||
// 数据操作
|
||||
updateConfig,
|
||||
toggleStage,
|
||||
updateCustomStageDefinition,
|
||||
updatePlanId,
|
||||
|
||||
// 工具函数
|
||||
initializeTimeConfigs
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import { Modal } from 'ant-design-vue'
|
||||
|
||||
// ====== 配置项 ======
|
||||
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
|
||||
const HEARTBEAT_INTERVAL = 30000 // 30秒心跳间隔,与后端保持一致
|
||||
const HEARTBEAT_TIMEOUT = 45000 // 45秒超时,给网络延迟留够时间
|
||||
const BACKEND_CHECK_INTERVAL = 6000 // 6秒检查间隔
|
||||
const HEARTBEAT_INTERVAL = 15000
|
||||
const HEARTBEAT_TIMEOUT = 5000
|
||||
const BACKEND_CHECK_INTERVAL = 3000
|
||||
const MAX_RESTART_ATTEMPTS = 3
|
||||
const RESTART_DELAY = 2000
|
||||
const MAX_QUEUE_SIZE = 50 // 每个 ID 或全局 type 队列最大条数
|
||||
@@ -111,7 +111,7 @@ const initGlobalStorage = (): GlobalWSStorage => ({
|
||||
|
||||
const getGlobalStorage = (): GlobalWSStorage => {
|
||||
if (!(window as any)[WS_STORAGE_KEY]) {
|
||||
; (window as any)[WS_STORAGE_KEY] = initGlobalStorage()
|
||||
;(window as any)[WS_STORAGE_KEY] = initGlobalStorage()
|
||||
}
|
||||
return (window as any)[WS_STORAGE_KEY]
|
||||
}
|
||||
@@ -166,7 +166,7 @@ const handleBackendFailure = async () => {
|
||||
okText: '重启应用',
|
||||
onOk: () => {
|
||||
if ((window.electronAPI as any)?.windowClose) {
|
||||
; (window.electronAPI as any).windowClose()
|
||||
;(window.electronAPI as any).windowClose()
|
||||
} else {
|
||||
window.location.reload()
|
||||
}
|
||||
@@ -178,26 +178,13 @@ const handleBackendFailure = async () => {
|
||||
setTimeout(async () => {
|
||||
const success = await restartBackend()
|
||||
if (success) {
|
||||
// 统一在一个地方管理连接权限
|
||||
setConnectionPermission(true, '后端重启后重连')
|
||||
|
||||
// 等待后端完全启动
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const connected = await connectGlobalWebSocket('后端重启后重连')
|
||||
if (connected) {
|
||||
// 连接成功后再禁用权限
|
||||
setTimeout(() => {
|
||||
setConnectionPermission(false, '正常运行中')
|
||||
}, 1000)
|
||||
}
|
||||
} catch (e) {
|
||||
warn('重启后重连失败:', e)
|
||||
setConnectionPermission(false, '连接失败')
|
||||
}
|
||||
setTimeout(() => {
|
||||
connectGlobalWebSocket('后端重启后重连').then(() => {
|
||||
setConnectionPermission(false, '正常运行中')
|
||||
})
|
||||
}, RESTART_DELAY)
|
||||
} else {
|
||||
// 重启失败,继续尝试
|
||||
setTimeout(handleBackendFailure, RESTART_DELAY)
|
||||
}
|
||||
}, RESTART_DELAY)
|
||||
@@ -221,15 +208,12 @@ const startBackendMonitoring = () => {
|
||||
setBackendStatus('stopped')
|
||||
}
|
||||
|
||||
// 检查心跳超时:如果超过心跳超时时间且连接仍然打开,说明后端可能有问题
|
||||
if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT) {
|
||||
if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT * 2) {
|
||||
if (global.wsRef?.readyState === WebSocket.OPEN) {
|
||||
setBackendStatus('error')
|
||||
// 主动关闭可能有问题的连接
|
||||
global.wsRef.close(1000, '心跳超时')
|
||||
}
|
||||
}
|
||||
}, BACKEND_CHECK_INTERVAL)
|
||||
}, BACKEND_CHECK_INTERVAL * 2)
|
||||
}
|
||||
|
||||
// ====== 心跳 ======
|
||||
@@ -255,7 +239,7 @@ const startGlobalHeartbeat = (ws: WebSocket) => {
|
||||
data: { Ping: pingTime, connectionId: global.connectionId },
|
||||
})
|
||||
)
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
}
|
||||
@@ -272,13 +256,13 @@ const cleanupExpiredMessages = (now: number) => {
|
||||
const messageMatchesFilter = (message: WebSocketBaseMessage, filter: SubscriptionFilter): boolean => {
|
||||
// 如果都不指定,匹配所有消息
|
||||
if (!filter.type && !filter.id) return true
|
||||
|
||||
|
||||
// 如果只指定type
|
||||
if (filter.type && !filter.id) return message.type === filter.type
|
||||
|
||||
|
||||
// 如果只指定id
|
||||
if (!filter.type && filter.id) return message.id === filter.id
|
||||
|
||||
|
||||
// 如果同时指定type和id,必须都匹配
|
||||
return message.type === filter.type && message.id === filter.id
|
||||
}
|
||||
@@ -294,11 +278,11 @@ const getCacheMarkerKey = (filter: SubscriptionFilter): string => {
|
||||
// 添加缓存标记
|
||||
const addCacheMarker = (filter: SubscriptionFilter) => {
|
||||
if (!filter.needCache) return
|
||||
|
||||
|
||||
const global = getGlobalStorage()
|
||||
const key = getCacheMarkerKey(filter)
|
||||
const existing = global.cacheMarkers.value.get(key)
|
||||
|
||||
|
||||
if (existing) {
|
||||
existing.refCount++
|
||||
} else {
|
||||
@@ -308,18 +292,18 @@ const addCacheMarker = (filter: SubscriptionFilter) => {
|
||||
refCount: 1
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
log(`缓存标记 ${key} 引用计数: ${global.cacheMarkers.value.get(key)?.refCount}`)
|
||||
}
|
||||
|
||||
// 移除缓存标记
|
||||
const removeCacheMarker = (filter: SubscriptionFilter) => {
|
||||
if (!filter.needCache) return
|
||||
|
||||
|
||||
const global = getGlobalStorage()
|
||||
const key = getCacheMarkerKey(filter)
|
||||
const existing = global.cacheMarkers.value.get(key)
|
||||
|
||||
|
||||
if (existing) {
|
||||
existing.refCount--
|
||||
if (existing.refCount <= 0) {
|
||||
@@ -334,7 +318,7 @@ const removeCacheMarker = (filter: SubscriptionFilter) => {
|
||||
// 检查消息是否需要缓存
|
||||
const shouldCacheMessage = (message: WebSocketBaseMessage): boolean => {
|
||||
const global = getGlobalStorage()
|
||||
|
||||
|
||||
for (const [, marker] of global.cacheMarkers.value) {
|
||||
const filter = { type: marker.type, id: marker.id }
|
||||
if (messageMatchesFilter(message, filter)) {
|
||||
@@ -377,8 +361,8 @@ const handleMessage = (raw: WebSocketBaseMessage) => {
|
||||
log(`消息已缓存: type=${raw.type}, id=${raw.id}`)
|
||||
}
|
||||
|
||||
// 定期清理过期消息(每处理50条消息触发一次,避免频繁且更可预测)
|
||||
if (global.cachedMessages.value.length > 0 && global.cachedMessages.value.length % 50 === 0) {
|
||||
// 定期清理过期消息(每 10 条触发一次,避免频繁)
|
||||
if (Math.random() < 0.1) {
|
||||
cleanupExpiredMessages(now)
|
||||
}
|
||||
|
||||
@@ -394,23 +378,23 @@ export const subscribe = (
|
||||
): string => {
|
||||
const global = getGlobalStorage()
|
||||
const subscriptionId = `sub_${++global.subscriptionCounter}_${Date.now()}`
|
||||
|
||||
|
||||
const subscription: WebSocketSubscription = {
|
||||
subscriptionId,
|
||||
filter,
|
||||
handler
|
||||
}
|
||||
|
||||
|
||||
global.subscriptions.value.set(subscriptionId, subscription)
|
||||
|
||||
|
||||
// 添加缓存标记
|
||||
addCacheMarker(filter)
|
||||
|
||||
|
||||
// 回放匹配的缓存消息
|
||||
const matchingMessages = global.cachedMessages.value.filter(cached =>
|
||||
const matchingMessages = global.cachedMessages.value.filter(cached =>
|
||||
messageMatchesFilter(cached.message, filter)
|
||||
)
|
||||
|
||||
|
||||
if (matchingMessages.length > 0) {
|
||||
log(`回放 ${matchingMessages.length} 条缓存消息给订阅 ${subscriptionId}`)
|
||||
matchingMessages.forEach(cached => {
|
||||
@@ -421,7 +405,7 @@ export const subscribe = (
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
log(`新订阅创建: ${subscriptionId}`, filter)
|
||||
return subscriptionId
|
||||
}
|
||||
@@ -429,14 +413,14 @@ export const subscribe = (
|
||||
export const unsubscribe = (subscriptionId: string): void => {
|
||||
const global = getGlobalStorage()
|
||||
const subscription = global.subscriptions.value.get(subscriptionId)
|
||||
|
||||
|
||||
if (subscription) {
|
||||
// 移除缓存标记
|
||||
removeCacheMarker(subscription.filter)
|
||||
|
||||
|
||||
// 清理缓存中没有任何标记的消息
|
||||
cleanupUnmarkedCache()
|
||||
|
||||
|
||||
global.subscriptions.value.delete(subscriptionId)
|
||||
log(`订阅已取消: ${subscriptionId}`)
|
||||
} else {
|
||||
@@ -447,7 +431,7 @@ export const unsubscribe = (subscriptionId: string): void => {
|
||||
// 清理没有标记的缓存消息
|
||||
const cleanupUnmarkedCache = () => {
|
||||
const global = getGlobalStorage()
|
||||
|
||||
|
||||
global.cachedMessages.value = global.cachedMessages.value.filter(cached => {
|
||||
// 检查是否还有标记需要这条消息
|
||||
for (const [, marker] of global.cacheMarkers.value) {
|
||||
@@ -471,7 +455,7 @@ const releaseConnectionLock = () => {
|
||||
isGlobalConnectingLock = false
|
||||
}
|
||||
|
||||
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连', '系统初始化', '手动重连', '强制连接']
|
||||
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连']
|
||||
const isValidConnectionReason = (reason: string) => allowedConnectionReasons.includes(reason)
|
||||
const checkConnectionPermission = () => getGlobalStorage().allowNewConnection
|
||||
const setConnectionPermission = (allow: boolean, reason: string) => {
|
||||
@@ -482,19 +466,9 @@ const setConnectionPermission = (allow: boolean, reason: string) => {
|
||||
|
||||
const createGlobalWebSocket = (): WebSocket => {
|
||||
const global = getGlobalStorage()
|
||||
|
||||
// 清理旧连接
|
||||
if (global.wsRef) {
|
||||
if (global.wsRef.readyState === WebSocket.OPEN) {
|
||||
log('警告:尝试创建新连接但当前连接仍有效')
|
||||
return global.wsRef
|
||||
}
|
||||
if (global.wsRef.readyState === WebSocket.CONNECTING) {
|
||||
log('警告:尝试创建新连接但当前连接正在建立中')
|
||||
return global.wsRef
|
||||
}
|
||||
// 清理已关闭或错误状态的连接
|
||||
global.wsRef = null
|
||||
if (global.wsRef.readyState === WebSocket.OPEN) return global.wsRef
|
||||
if (global.wsRef.readyState === WebSocket.CONNECTING) return global.wsRef
|
||||
}
|
||||
|
||||
const ws = new WebSocket(BASE_WS_URL)
|
||||
@@ -506,11 +480,7 @@ const createGlobalWebSocket = (): WebSocket => {
|
||||
global.reconnectAttempts = 0
|
||||
setGlobalStatus('已连接')
|
||||
startGlobalHeartbeat(ws)
|
||||
|
||||
// 只有在特殊连接原因下才设置为正常运行
|
||||
if (global.connectionReason !== '系统初始化') {
|
||||
setConnectionPermission(false, '正常运行中')
|
||||
}
|
||||
setConnectionPermission(false, '正常运行中')
|
||||
|
||||
try {
|
||||
ws.send(
|
||||
@@ -525,52 +495,35 @@ const createGlobalWebSocket = (): WebSocket => {
|
||||
data: { Pong: Date.now(), connectionId: global.connectionId },
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
warn('发送初始信号失败:', e)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
initializeGlobalSubscriptions()
|
||||
log('WebSocket连接已建立并初始化完成')
|
||||
}
|
||||
|
||||
ws.onmessage = ev => {
|
||||
try {
|
||||
const raw = JSON.parse(ev.data) as WebSocketBaseMessage
|
||||
handleMessage(raw)
|
||||
} catch (e) {
|
||||
warn('解析WebSocket消息失败:', e, '原始数据:', ev.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
setGlobalStatus('连接错误')
|
||||
warn('WebSocket错误:', error)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
ws.onerror = () => setGlobalStatus('连接错误')
|
||||
ws.onclose = event => {
|
||||
setGlobalStatus('已断开')
|
||||
stopGlobalHeartbeat()
|
||||
global.isConnecting = false
|
||||
|
||||
log(`WebSocket连接关闭: code=${event.code}, reason="${event.reason}"`)
|
||||
|
||||
// 根据关闭原因决定是否需要处理后端故障
|
||||
if (event.code === 1000 && event.reason === 'Ping超时') {
|
||||
handleBackendFailure().catch(e => warn('handleBackendFailure error:', e))
|
||||
} else if (event.code === 1000 && event.reason === '心跳超时') {
|
||||
handleBackendFailure().catch(e => warn('handleBackendFailure error:', e))
|
||||
}
|
||||
}
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
const connectGlobalWebSocket = async (reason: string = '手动重连'): Promise<boolean> => {
|
||||
const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promise<boolean> => {
|
||||
const global = getGlobalStorage()
|
||||
if (!checkConnectionPermission() || !isValidConnectionReason(reason)) {
|
||||
warn(`连接被拒绝: 权限=${checkConnectionPermission()}, 原因="${reason}"是否有效=${isValidConnectionReason(reason)}`)
|
||||
return false
|
||||
}
|
||||
if (!checkConnectionPermission() || !isValidConnectionReason(reason)) return false
|
||||
if (!acquireConnectionLock()) return false
|
||||
|
||||
try {
|
||||
@@ -620,72 +573,6 @@ export const connectAfterBackendStart = async (): Promise<boolean> => {
|
||||
}
|
||||
}
|
||||
|
||||
// 强制连接模式,用于强行进入应用时
|
||||
export const forceConnectWebSocket = async (): Promise<boolean> => {
|
||||
log('强制WebSocket连接模式开始')
|
||||
|
||||
const global = getGlobalStorage()
|
||||
|
||||
// 显示当前状态
|
||||
log('当前连接状态:', {
|
||||
status: global.status.value,
|
||||
wsReadyState: global.wsRef?.readyState,
|
||||
allowNewConnection: global.allowNewConnection,
|
||||
connectionReason: global.connectionReason
|
||||
})
|
||||
|
||||
// 设置连接权限
|
||||
setConnectionPermission(true, '强制连接')
|
||||
log('已设置强制连接权限')
|
||||
|
||||
try {
|
||||
// 尝试连接,最多重试3次
|
||||
let connected = false
|
||||
let attempts = 0
|
||||
const maxAttempts = 3
|
||||
|
||||
while (!connected && attempts < maxAttempts) {
|
||||
attempts++
|
||||
log(`强制连接尝试 ${attempts}/${maxAttempts}`)
|
||||
|
||||
try {
|
||||
connected = await connectGlobalWebSocket('强制连接')
|
||||
if (connected) {
|
||||
startBackendMonitoring()
|
||||
log('强制WebSocket连接成功')
|
||||
break
|
||||
} else {
|
||||
warn(`强制连接尝试 ${attempts} 失败`)
|
||||
if (attempts < maxAttempts) {
|
||||
// 等待1秒后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
} catch (attemptError) {
|
||||
warn(`强制连接尝试 ${attempts} 异常:`, attemptError)
|
||||
if (attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
warn('所有强制连接尝试均失败,但不阻止应用启动')
|
||||
}
|
||||
|
||||
return connected
|
||||
} catch (error) {
|
||||
warn('强制WebSocket连接异常:', error)
|
||||
return false
|
||||
} finally {
|
||||
// 稍后重置连接权限,给连接时间
|
||||
setTimeout(() => {
|
||||
setConnectionPermission(false, '强制连接完成')
|
||||
log('强制连接权限已重置')
|
||||
}, 2000) // 增加到2秒
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 全局处理器 ======
|
||||
let _defaultHandlersLoaded = true
|
||||
let _defaultTaskManagerHandler = schedulerHandlers.handleTaskManagerMessage
|
||||
@@ -740,7 +627,7 @@ const initializeGlobalSubscriptions = () => {
|
||||
data: { Pong: msg.data.Ping, connectionId: global.connectionId },
|
||||
})
|
||||
)
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -761,22 +648,8 @@ export function useWebSocket() {
|
||||
const ws = global.wsRef
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
const message = { id, type, data }
|
||||
ws.send(JSON.stringify(message))
|
||||
if (DEBUG && type !== 'Signal') { // 避免心跳消息spam日志
|
||||
log('发送消息:', message)
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
warn('发送消息失败:', e, { id, type, data })
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
warn('WebSocket未连接,无法发送消息:', {
|
||||
readyState: ws?.readyState,
|
||||
message: { id, type, data }
|
||||
})
|
||||
return false
|
||||
ws.send(JSON.stringify({ id, type, data }))
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,21 +679,6 @@ export function useWebSocket() {
|
||||
lastCheck: global.lastBackendCheck,
|
||||
})
|
||||
|
||||
// 调试功能
|
||||
const debug = {
|
||||
forceConnect: forceConnectWebSocket,
|
||||
normalConnect: connectAfterBackendStart,
|
||||
getGlobalStorage,
|
||||
setConnectionPermission,
|
||||
checkConnectionPermission,
|
||||
allowedReasons: allowedConnectionReasons
|
||||
}
|
||||
|
||||
// 在开发模式下暴露调试功能到全局
|
||||
if (DEBUG && typeof window !== 'undefined') {
|
||||
; (window as any).wsDebug = debug
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
@@ -830,7 +688,6 @@ export function useWebSocket() {
|
||||
backendStatus: global.backendStatus,
|
||||
restartBackend: restartBackendManually,
|
||||
getBackendStatus,
|
||||
debug: DEBUG ? debug : undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
140
frontend/src/types/electron.d.ts
vendored
140
frontend/src/types/electron.d.ts
vendored
@@ -1,84 +1,64 @@
|
||||
export interface ElectronAPI {
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: any[]) => Promise<string[]>
|
||||
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
downloadPython: (mirror?: string) => Promise<any>
|
||||
installPip: () => Promise<any>
|
||||
downloadGit: () => Promise<any>
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (repoUrl?: string) => Promise<any>
|
||||
updateBackend: (repoUrl?: string) => Promise<any>
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
stopBackend?: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => Promise<boolean>
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => Promise<void>
|
||||
loadConfig: () => Promise<any>
|
||||
resetConfig: () => Promise<void>
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => Promise<void>
|
||||
loadLogsFromFile: () => Promise<string | null>
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (filePath: string) => Promise<void>
|
||||
showItemInFolder: (filePath: string) => Promise<void>
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface ElectronAPI {
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: any[]) => Promise<string[]>
|
||||
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
downloadPython: (mirror?: string) => Promise<any>
|
||||
installPip: () => Promise<any>
|
||||
downloadGit: () => Promise<any>
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (repoUrl?: string) => Promise<any>
|
||||
updateBackend: (repoUrl?: string) => Promise<any>
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
stopBackend?: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => Promise<boolean>
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => Promise<void>
|
||||
loadConfig: () => Promise<any>
|
||||
resetConfig: () => Promise<void>
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => Promise<void>
|
||||
loadLogsFromFile: () => Promise<string | null>
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (filePath: string) => Promise<void>
|
||||
showItemInFolder: (filePath: string) => Promise<void>
|
||||
|
||||
// 对话框相关
|
||||
showQuestionDialog: (questionData: {
|
||||
title?: string
|
||||
message?: string
|
||||
options?: string[]
|
||||
messageId?: string
|
||||
}) => Promise<boolean>
|
||||
dialogResponse: (messageId: string, choice: boolean) => Promise<boolean>
|
||||
resizeDialogWindow: (height: number) => Promise<void>
|
||||
|
||||
// 主题信息获取
|
||||
getThemeInfo: () => Promise<{
|
||||
themeMode: string
|
||||
themeColor: string
|
||||
actualTheme: string
|
||||
systemTheme: string
|
||||
isDark: boolean
|
||||
primaryColor: string
|
||||
}>
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
|
||||
78
frontend/src/types/electron.ts
Normal file
78
frontend/src/types/electron.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Electron API 类型定义
|
||||
export interface ElectronAPI {
|
||||
// 开发工具
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
|
||||
// 管理员权限检查
|
||||
checkAdmin: () => Promise<boolean>
|
||||
|
||||
// 重启为管理员
|
||||
restartAsAdmin: () => Promise<void>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
// 环境检查
|
||||
checkEnvironment: () => Promise<{
|
||||
pythonExists: boolean
|
||||
gitExists: boolean
|
||||
backendExists: boolean
|
||||
dependenciesInstalled: boolean
|
||||
isInitialized: boolean
|
||||
}>
|
||||
|
||||
// 关键文件检查
|
||||
checkCriticalFiles: () => Promise<{
|
||||
pythonExists: boolean
|
||||
pipExists: boolean
|
||||
gitExists: boolean
|
||||
mainPyExists: boolean
|
||||
}>
|
||||
|
||||
// Python相关
|
||||
downloadPython: (mirror: string) => Promise<{ success: boolean; error?: string }>
|
||||
deletePython: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// pip相关
|
||||
installPip: () => Promise<{ success: boolean; error?: string }>
|
||||
deletePip: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// Git相关
|
||||
downloadGit: () => Promise<{ success: boolean; error?: string }>
|
||||
deleteGit: () => Promise<{ success: boolean; error?: string }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
|
||||
// 后端代码相关
|
||||
cloneBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
|
||||
updateBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 依赖安装
|
||||
installDependencies: (mirror: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 后端服务
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 下载进度监听
|
||||
onDownloadProgress: (
|
||||
callback: (progress: { progress: number; status: string; message: string }) => void
|
||||
) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,81 +0,0 @@
|
||||
// appEntry.ts - 统一的应用进入逻辑
|
||||
import router from '@/router'
|
||||
import { connectAfterBackendStart, forceConnectWebSocket } from '@/composables/useWebSocket'
|
||||
|
||||
/**
|
||||
* 统一的进入应用函数,会自动尝试建立WebSocket连接
|
||||
* @param reason 进入应用的原因,用于日志记录
|
||||
* @param forceEnter 是否强制进入(即使WebSocket连接失败)
|
||||
* @returns Promise<boolean> 是否成功进入应用
|
||||
*/
|
||||
export async function enterApp(reason: string = '正常进入', forceEnter: boolean = true): Promise<boolean> {
|
||||
console.log(`${reason}:开始进入应用流程,尝试建立WebSocket连接...`)
|
||||
|
||||
let wsConnected = false
|
||||
|
||||
try {
|
||||
// 尝试建立WebSocket连接
|
||||
wsConnected = await connectAfterBackendStart()
|
||||
if (wsConnected) {
|
||||
console.log(`${reason}:WebSocket连接建立成功`)
|
||||
} else {
|
||||
console.warn(`${reason}:WebSocket连接建立失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${reason}:WebSocket连接尝试失败:`, error)
|
||||
}
|
||||
|
||||
// 决定是否进入应用
|
||||
if (wsConnected || forceEnter) {
|
||||
if (!wsConnected && forceEnter) {
|
||||
console.warn(`${reason}:WebSocket连接失败,但强制进入应用`)
|
||||
}
|
||||
|
||||
// 跳转到主页
|
||||
router.push('/home')
|
||||
console.log(`${reason}:已进入应用`)
|
||||
return true
|
||||
} else {
|
||||
console.error(`${reason}:WebSocket连接失败且不允许强制进入`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强行进入应用(忽略WebSocket连接状态)
|
||||
* @param reason 进入原因
|
||||
*/
|
||||
export async function forceEnterApp(reason: string = '强行进入'): Promise<void> {
|
||||
console.log(`🚀 ${reason}:强行进入应用流程开始`)
|
||||
console.log(`📡 ${reason}:尝试强制建立WebSocket连接...`)
|
||||
|
||||
try {
|
||||
// 使用强制连接模式
|
||||
const wsConnected = await forceConnectWebSocket()
|
||||
if (wsConnected) {
|
||||
console.log(`✅ ${reason}:强制WebSocket连接成功!`)
|
||||
} else {
|
||||
console.warn(`⚠️ ${reason}:强制WebSocket连接失败,但继续进入应用`)
|
||||
}
|
||||
|
||||
// 等待一下确保连接状态稳定
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${reason}:强制WebSocket连接异常:`, error)
|
||||
}
|
||||
|
||||
// 无论WebSocket是否成功,都进入应用
|
||||
console.log(`🏠 ${reason}:跳转到主页...`)
|
||||
router.push('/home')
|
||||
console.log(`✨ ${reason}:已强行进入应用`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常进入应用(需要WebSocket连接成功)
|
||||
* @param reason 进入原因
|
||||
* @returns 是否成功进入
|
||||
*/
|
||||
export async function normalEnterApp(reason: string = '正常进入'): Promise<boolean> {
|
||||
return await enterApp(reason, false)
|
||||
}
|
||||
@@ -1,13 +1,24 @@
|
||||
// 渲染进程日志工具
|
||||
const LogLevel = {
|
||||
DEBUG: 'DEBUG',
|
||||
INFO: 'INFO',
|
||||
WARN: 'WARN',
|
||||
ERROR: 'ERROR'
|
||||
} as const
|
||||
interface ElectronAPI {
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
}
|
||||
|
||||
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
|
||||
export { LogLevel }
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 'DEBUG',
|
||||
INFO = 'INFO',
|
||||
WARN = 'WARN',
|
||||
ERROR = 'ERROR'
|
||||
}
|
||||
|
||||
class Logger {
|
||||
// 直接使用原生console,主进程会自动处理日志记录
|
||||
@@ -29,32 +40,32 @@ class Logger {
|
||||
|
||||
// 获取日志文件路径
|
||||
async getLogPath(): Promise<string> {
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogPath()
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogPath()
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 获取日志文件列表
|
||||
async getLogFiles(): Promise<string[]> {
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogFiles()
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogFiles()
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 获取日志内容
|
||||
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogs(lines, fileName)
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogs(lines, fileName)
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
async clearLogs(fileName?: string): Promise<void> {
|
||||
if ((window as any).electronAPI) {
|
||||
await (window as any).electronAPI.clearLogs(fileName)
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.clearLogs(fileName)
|
||||
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
||||
} else {
|
||||
throw new Error('Electron API not available')
|
||||
@@ -63,8 +74,8 @@ class Logger {
|
||||
|
||||
// 清理旧日志
|
||||
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
||||
if ((window as any).electronAPI) {
|
||||
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.cleanOldLogs(daysToKeep)
|
||||
console.info(`已清理${daysToKeep}天前的旧日志`)
|
||||
} else {
|
||||
throw new Error('Electron API not available')
|
||||
|
||||
91
frontend/src/utils/planNameUtils.ts
Normal file
91
frontend/src/utils/planNameUtils.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 计划表名称管理工具函数
|
||||
*/
|
||||
|
||||
export interface PlanNameValidationResult {
|
||||
isValid: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的计划表名称(使用数字后缀)
|
||||
* @param planType 计划表类型
|
||||
* @param existingNames 已存在的名称列表
|
||||
* @returns 唯一的计划表名称
|
||||
*/
|
||||
export function generateUniquePlanName(planType: string, existingNames: string[]): string {
|
||||
const baseNames = {
|
||||
MaaPlanConfig: '新 MAA 计划表',
|
||||
GeneralPlan: '新通用计划表',
|
||||
CustomPlan: '新自定义计划表',
|
||||
} as Record<string, string>
|
||||
|
||||
const baseName = baseNames[planType] || '新计划表'
|
||||
|
||||
// 如果基础名称没有被使用,直接返回
|
||||
if (!existingNames.includes(baseName)) {
|
||||
return baseName
|
||||
}
|
||||
|
||||
// 查找可用的编号
|
||||
let counter = 2
|
||||
let candidateName = `${baseName} ${counter}`
|
||||
|
||||
while (existingNames.includes(candidateName)) {
|
||||
counter++
|
||||
candidateName = `${baseName} ${counter}`
|
||||
}
|
||||
|
||||
return candidateName
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证计划表名称是否可用
|
||||
* @param newName 新名称
|
||||
* @param existingNames 已存在的名称列表
|
||||
* @param currentName 当前名称(编辑时排除自己)
|
||||
* @returns 验证结果
|
||||
*/
|
||||
export function validatePlanName(
|
||||
newName: string,
|
||||
existingNames: string[],
|
||||
currentName?: string
|
||||
): PlanNameValidationResult {
|
||||
// 检查名称是否为空
|
||||
if (!newName || !newName.trim()) {
|
||||
return { isValid: false, message: '计划表名称不能为空' }
|
||||
}
|
||||
|
||||
const trimmedName = newName.trim()
|
||||
|
||||
// 检查名称长度
|
||||
if (trimmedName.length > 50) {
|
||||
return { isValid: false, message: '计划表名称不能超过50个字符' }
|
||||
}
|
||||
|
||||
// 检查是否与其他计划表重名(排除当前名称)
|
||||
const isDuplicate = existingNames.some(name =>
|
||||
name === trimmedName && name !== currentName
|
||||
)
|
||||
|
||||
if (isDuplicate) {
|
||||
return { isValid: false, message: '计划表名称已存在,请使用其他名称' }
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划表类型的显示标签
|
||||
* @param planType 计划表类型
|
||||
* @returns 显示标签
|
||||
*/
|
||||
export function getPlanTypeLabel(planType: string): string {
|
||||
const labels = {
|
||||
MaaPlanConfig: 'MAA计划表',
|
||||
GeneralPlan: '通用计划表',
|
||||
CustomPlan: '自定义计划表',
|
||||
} as Record<string, string>
|
||||
|
||||
return labels[planType] || '计划表'
|
||||
}
|
||||
@@ -16,7 +16,11 @@
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<a-space size="middle">
|
||||
// 如果已有连接,先断开
|
||||
if (generalSubscriptionId.value) {
|
||||
unsubscribe(generalSubscriptionId.value)
|
||||
generalSubscriptionId.value = null
|
||||
generalWebsocketId.value = nulla-space size="middle">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
@@ -371,15 +375,6 @@ import { Service } from '@/api'
|
||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||
import WebhookManager from '@/components/WebhookManager.vue'
|
||||
|
||||
// 扩展 Window 接口以支持 Electron API
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
selectFile: (filters: Array<{ name: string; extensions: string[] }>) => Promise<string[]>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi()
|
||||
@@ -586,9 +581,8 @@ const handleGeneralConfig = async () => {
|
||||
showGeneralConfigMask.value = true
|
||||
|
||||
// 如果已有连接,先断开并清理
|
||||
if (generalSubscriptionId.value) {
|
||||
unsubscribe(generalSubscriptionId.value)
|
||||
generalSubscriptionId.value = null
|
||||
if (generalWebsocketId.value) {
|
||||
unsubscribe(generalWebsocketId.value)
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
if (generalConfigTimeout) {
|
||||
@@ -696,7 +690,7 @@ const handleSaveGeneralConfig = async () => {
|
||||
// 文件选择方法
|
||||
const selectScriptBeforeTask = async () => {
|
||||
try {
|
||||
const path = await window.electronAPI?.selectFile([
|
||||
const path = await (window.electronAPI as any)?.selectFile([
|
||||
{ name: '可执行文件', extensions: ['exe', 'bat', 'cmd', 'ps1'] },
|
||||
{ name: '脚本文件', extensions: ['py', 'js', 'sh'] },
|
||||
{ name: '所有文件', extensions: ['*'] },
|
||||
@@ -714,7 +708,7 @@ const selectScriptBeforeTask = async () => {
|
||||
|
||||
const selectScriptAfterTask = async () => {
|
||||
try {
|
||||
const path = await window.electronAPI?.selectFile([
|
||||
const path = await (window.electronAPI as any)?.selectFile([
|
||||
{ name: '可执行文件', extensions: ['exe', 'bat', 'cmd', 'ps1'] },
|
||||
{ name: '脚本文件', extensions: ['py', 'js', 'sh'] },
|
||||
{ name: '所有文件', extensions: ['*'] },
|
||||
|
||||
@@ -43,7 +43,6 @@ import ManualMode from '@/components/initialization/ManualMode.vue'
|
||||
import EnvironmentIncomplete from '@/components/initialization/EnvironmentIncomplete.vue'
|
||||
import type { DownloadProgress } from '@/types/initialization'
|
||||
import { mirrorManager } from '@/utils/mirrorManager'
|
||||
import { forceEnterApp } from '@/utils/appEntry'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -70,8 +69,8 @@ const mirrorConfigStatus = ref({
|
||||
const manualModeRef = ref()
|
||||
|
||||
// 基础功能函数
|
||||
async function skipToHome() {
|
||||
await forceEnterApp('跳过初始化直接进入')
|
||||
function skipToHome() {
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
function switchToManualMode() {
|
||||
@@ -85,14 +84,10 @@ async function enterApp() {
|
||||
try {
|
||||
// 设置初始化完成标记
|
||||
await setInitialized(true)
|
||||
console.log('设置初始化完成标记,准备进入应用...')
|
||||
|
||||
// 使用统一的进入应用函数
|
||||
await forceEnterApp('初始化完成后进入')
|
||||
console.log('设置初始化完成标记,跳转到首页')
|
||||
router.push('/home')
|
||||
} catch (error) {
|
||||
console.error('进入应用失败:', error)
|
||||
// 即使出错也强制进入
|
||||
await forceEnterApp('初始化失败后强制进入')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<ScriptTable
|
||||
:scripts="scripts"
|
||||
:active-connections="activeConnections"
|
||||
:current-plan-data="currentPlanData"
|
||||
@edit="handleEditScript"
|
||||
@delete="handleDeleteScript"
|
||||
@add-user="handleAddUser"
|
||||
@@ -253,6 +254,7 @@ import { useScriptApi } from '@/composables/useScriptApi'
|
||||
import { useUserApi } from '@/composables/useUserApi'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
import { useTemplateApi, type WebConfigTemplate } from '@/composables/useTemplateApi'
|
||||
import { usePlanApi } from '@/composables/usePlanApi'
|
||||
import { Service } from '@/api/services/Service'
|
||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
@@ -262,6 +264,7 @@ const { addScript, deleteScript, getScriptsWithUsers } = useScriptApi()
|
||||
const { updateUser, deleteUser } = useUserApi()
|
||||
const { subscribe, unsubscribe } = useWebSocket()
|
||||
const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi()
|
||||
const { getPlans } = usePlanApi()
|
||||
|
||||
// 初始化markdown解析器
|
||||
const md = new MarkdownIt({
|
||||
@@ -273,6 +276,8 @@ const md = new MarkdownIt({
|
||||
const scripts = ref<Script[]>([])
|
||||
// 增加:标记是否已经完成过一次脚本列表加载(成功或失败都算一次)
|
||||
const loadedOnce = ref(false)
|
||||
// 当前计划表数据
|
||||
const currentPlanData = ref<Record<string, any> | null>(null)
|
||||
const typeSelectVisible = ref(false)
|
||||
const generalModeSelectVisible = ref(false)
|
||||
const templateSelectVisible = ref(false)
|
||||
@@ -312,6 +317,7 @@ const filteredTemplates = computed(() => {
|
||||
|
||||
onMounted(() => {
|
||||
loadScripts()
|
||||
loadCurrentPlan()
|
||||
})
|
||||
|
||||
const loadScripts = async () => {
|
||||
@@ -336,6 +342,21 @@ const loadScripts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载当前计划表数据
|
||||
const loadCurrentPlan = async () => {
|
||||
try {
|
||||
const response = await getPlans()
|
||||
if (response.data && response.index && response.index.length > 0) {
|
||||
// 获取第一个计划表的数据
|
||||
const firstPlanId = response.index[0].uid
|
||||
currentPlanData.value = response.data[firstPlanId] || null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载计划表数据失败:', error)
|
||||
// 不显示错误消息,因为计划表数据是可选的
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddScript = () => {
|
||||
selectedType.value = 'MAA'
|
||||
typeSelectVisible.value = true
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
:current-mode="currentMode"
|
||||
:view-mode="viewMode"
|
||||
:options-loaded="!loading"
|
||||
:plan-id="activePlanId"
|
||||
@update-table-data="handleTableDataUpdate"
|
||||
/>
|
||||
</PlanConfig>
|
||||
@@ -70,6 +71,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { usePlanApi } from '@/composables/usePlanApi'
|
||||
import { generateUniquePlanName, validatePlanName, getPlanTypeLabel } from '@/utils/planNameUtils'
|
||||
import PlanHeader from './components/PlanHeader.vue'
|
||||
import PlanSelector from './components/PlanSelector.vue'
|
||||
import PlanConfig from './components/PlanConfig.vue'
|
||||
@@ -100,6 +102,7 @@ const viewMode = ref<'config' | 'simple'>('config')
|
||||
|
||||
const isEditingPlanName = ref<boolean>(false)
|
||||
const loading = ref(true)
|
||||
const switching = ref(false) // 添加切换状态
|
||||
|
||||
// Use a record to match child component expectations
|
||||
const tableData = ref<Record<string, any>>({})
|
||||
@@ -128,13 +131,18 @@ const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): T =
|
||||
const handleAddPlan = async (planType: string = 'MaaPlanConfig') => {
|
||||
try {
|
||||
const response = await createPlan(planType)
|
||||
const defaultName = getDefaultPlanName(planType)
|
||||
const newPlan = { id: response.planId, name: defaultName, type: planType }
|
||||
const uniqueName = getDefaultPlanName(planType)
|
||||
const newPlan = { id: response.planId, name: uniqueName, type: planType }
|
||||
planList.value.push(newPlan)
|
||||
activePlanId.value = newPlan.id
|
||||
currentPlanName.value = defaultName
|
||||
currentPlanName.value = uniqueName
|
||||
await loadPlanData(newPlan.id)
|
||||
message.info(`已创建新的${getPlanTypeLabel(planType)},建议您修改为更有意义的名称`, 3)
|
||||
// 如果生成的名称包含数字,说明有重名,提示用户
|
||||
if (uniqueName.match(/\s\d+$/)) {
|
||||
message.info(`已创建新的${getPlanTypeLabel(planType)}:"${uniqueName}",建议您修改为更有意义的名称`, 4)
|
||||
} else {
|
||||
message.success(`已创建新的${getPlanTypeLabel(planType)}:"${uniqueName}"`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加计划失败:', error)
|
||||
}
|
||||
@@ -186,6 +194,7 @@ const saveInBackground = async (planId: string) => {
|
||||
const planData: Record<string, any> = { ...(tableData.value || {}) }
|
||||
planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType }
|
||||
|
||||
console.log(`[计划表] 保存数据 (${planId}):`, planData)
|
||||
await updatePlan(planId, planData)
|
||||
} catch (error) {
|
||||
console.error('后台保存计划数据失败:', error)
|
||||
@@ -214,20 +223,27 @@ const handleSave = async () => {
|
||||
await debouncedSave()
|
||||
}
|
||||
|
||||
// 优化计划切换逻辑
|
||||
// 优化计划切换逻辑 - 异步保存,立即切换
|
||||
const onPlanChange = async (planId: string) => {
|
||||
if (planId === activePlanId.value) return
|
||||
|
||||
// 触发当前计划的异步保存,但不等待完成
|
||||
if (activePlanId.value) {
|
||||
saveInBackground(activePlanId.value).catch(error => {
|
||||
console.warn('切换时保存当前计划失败:', error)
|
||||
})
|
||||
}
|
||||
switching.value = true
|
||||
try {
|
||||
// 异步保存当前计划,不等待完成
|
||||
if (activePlanId.value) {
|
||||
saveInBackground(activePlanId.value).catch(error => {
|
||||
console.error('切换时保存当前计划失败:', error)
|
||||
message.warning('保存当前计划时出现问题,请检查数据是否完整')
|
||||
})
|
||||
}
|
||||
|
||||
// 立即切换到新计划
|
||||
activePlanId.value = planId
|
||||
await loadPlanData(planId)
|
||||
// 立即切换到新计划,提升响应速度
|
||||
console.log(`[计划表] 切换到新计划: ${planId}`)
|
||||
activePlanId.value = planId
|
||||
await loadPlanData(planId)
|
||||
} finally {
|
||||
switching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startEditPlanName = () => {
|
||||
@@ -242,13 +258,29 @@ const startEditPlanName = () => {
|
||||
}
|
||||
|
||||
const finishEditPlanName = () => {
|
||||
isEditingPlanName.value = false
|
||||
if (activePlanId.value) {
|
||||
const currentPlan = planList.value.find(plan => plan.id === activePlanId.value)
|
||||
if (currentPlan) {
|
||||
currentPlan.name = currentPlanName.value || getDefaultPlanName(currentPlan.type)
|
||||
const newName = currentPlanName.value?.trim() || ''
|
||||
const existingNames = planList.value.map(plan => plan.name)
|
||||
|
||||
// 验证新名称
|
||||
const validation = validatePlanName(newName, existingNames, currentPlan.name)
|
||||
|
||||
if (!validation.isValid) {
|
||||
// 如果验证失败,显示错误消息并恢复原名称
|
||||
message.error(validation.message || '计划表名称无效')
|
||||
currentPlanName.value = currentPlan.name
|
||||
} else {
|
||||
// 如果验证成功,更新名称
|
||||
currentPlan.name = newName
|
||||
currentPlanName.value = newName
|
||||
// 触发保存操作,确保名称被保存到后端
|
||||
handleSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
isEditingPlanName.value = false
|
||||
}
|
||||
|
||||
const onModeChange = () => {
|
||||
@@ -263,20 +295,41 @@ const handleTableDataUpdate = async (newData: Record<string, any>) => {
|
||||
|
||||
const loadPlanData = async (planId: string) => {
|
||||
try {
|
||||
const response = await getPlans(planId)
|
||||
currentPlanData.value = response.data
|
||||
if (response.data && response.data[planId]) {
|
||||
const planData = response.data[planId] as PlanData
|
||||
// 优化:先检查缓存数据,避免不必要的API调用
|
||||
let planData: PlanData | null = null
|
||||
|
||||
if (currentPlanData.value && currentPlanData.value[planId]) {
|
||||
planData = currentPlanData.value[planId] as PlanData
|
||||
console.log(`[计划表] 使用缓存数据 (${planId})`)
|
||||
} else {
|
||||
const response = await getPlans(planId)
|
||||
currentPlanData.value = response.data
|
||||
planData = response.data[planId] as PlanData
|
||||
console.log(`[计划表] 加载新数据 (${planId})`)
|
||||
}
|
||||
|
||||
if (planData) {
|
||||
if (planData.Info) {
|
||||
const apiName = planData.Info.Name || ''
|
||||
if (!apiName && !currentPlanName.value) {
|
||||
const currentPlan = planList.value.find(plan => plan.id === planId)
|
||||
if (currentPlan) currentPlanName.value = currentPlan.name
|
||||
const currentPlan = planList.value.find(plan => plan.id === planId)
|
||||
|
||||
// 优先使用planList中的名称
|
||||
if (currentPlan && currentPlan.name) {
|
||||
currentPlanName.value = currentPlan.name
|
||||
|
||||
if (apiName !== currentPlan.name) {
|
||||
console.log(`[计划表] 同步名称: API="${apiName}" -> planList="${currentPlan.name}"`)
|
||||
}
|
||||
} else if (apiName) {
|
||||
currentPlanName.value = apiName
|
||||
if (currentPlan) {
|
||||
currentPlan.name = apiName
|
||||
}
|
||||
}
|
||||
|
||||
currentMode.value = planData.Info.Mode || 'ALL'
|
||||
}
|
||||
|
||||
tableData.value = planData
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -288,17 +341,47 @@ const initPlans = async () => {
|
||||
try {
|
||||
const response = await getPlans()
|
||||
if (response.index && response.index.length > 0) {
|
||||
// 优化:预先收集所有名称,避免O(n²)复杂度
|
||||
const allPlanNames: string[] = []
|
||||
|
||||
planList.value = response.index.map((item: any) => {
|
||||
const planId = item.uid
|
||||
const planData = response.data[planId]
|
||||
const planType = item.type
|
||||
const planName = planData?.Info?.Name || getDefaultPlanName(planType)
|
||||
let planName = planData?.Info?.Name || ''
|
||||
|
||||
// 如果API中没有名称,或者名称是默认的模板名称,则生成唯一名称
|
||||
if (!planName || planName === '新 MAA 计划表' || planName === '新通用计划表' || planName === '新自定义计划表') {
|
||||
planName = generateUniquePlanName(planType, allPlanNames)
|
||||
}
|
||||
|
||||
allPlanNames.push(planName)
|
||||
return { id: planId, name: planName, type: planType }
|
||||
})
|
||||
|
||||
const queryPlanId = (route.query.planId as string) || ''
|
||||
const target = queryPlanId ? planList.value.find(p => p.id === queryPlanId) : null
|
||||
activePlanId.value = target ? target.id : planList.value[0].id
|
||||
await loadPlanData(activePlanId.value)
|
||||
const selectedPlanId = target ? target.id : planList.value[0].id
|
||||
|
||||
// 优化:直接使用已获取的数据,避免重复API调用
|
||||
activePlanId.value = selectedPlanId
|
||||
const planData = response.data[selectedPlanId]
|
||||
if (planData) {
|
||||
currentPlanData.value = response.data
|
||||
|
||||
// 直接设置数据,避免loadPlanData的重复调用
|
||||
const selectedPlan = planList.value.find(plan => plan.id === selectedPlanId)
|
||||
if (selectedPlan) {
|
||||
currentPlanName.value = selectedPlan.name
|
||||
}
|
||||
|
||||
if (planData.Info) {
|
||||
currentMode.value = planData.Info.Mode || 'ALL'
|
||||
}
|
||||
|
||||
console.log(`[计划表] 初始加载数据 (${selectedPlanId}):`, planData)
|
||||
tableData.value = planData
|
||||
}
|
||||
} else {
|
||||
currentPlanData.value = null
|
||||
}
|
||||
@@ -310,22 +393,12 @@ const initPlans = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getDefaultPlanName = (planType: string) =>
|
||||
(
|
||||
({
|
||||
MaaPlanConfig: '新 MAA 计划表',
|
||||
GeneralPlan: '新通用计划表',
|
||||
CustomPlan: '新自定义计划表',
|
||||
}) as Record<string, string>
|
||||
)[planType] || '新计划表'
|
||||
const getPlanTypeLabel = (planType: string) =>
|
||||
(
|
||||
({
|
||||
MaaPlanConfig: 'MAA计划表',
|
||||
GeneralPlan: '通用计划表',
|
||||
CustomPlan: '自定义计划表',
|
||||
}) as Record<string, string>
|
||||
)[planType] || '计划表'
|
||||
const getDefaultPlanName = (planType: string) => {
|
||||
// 保持原来的逻辑,但添加重名检测
|
||||
const existingNames = planList.value.map(plan => plan.name)
|
||||
return generateUniquePlanName(planType, existingNames)
|
||||
}
|
||||
// getPlanTypeLabel 现在从 @/utils/planNameUtils 导入,删除本地定义
|
||||
|
||||
watch(
|
||||
() => [currentPlanName.value, currentMode.value],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,254 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket强制连接测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.force-btn {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.force-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>WebSocket强制连接测试工具</h1>
|
||||
|
||||
<div class="status" id="status">状态:未知</div>
|
||||
|
||||
<div>
|
||||
<button onclick="testNormalConnect()">测试正常连接</button>
|
||||
<button onclick="testForceConnect()" class="force-btn">测试强制连接</button>
|
||||
<button onclick="testDirectConnect()">测试直接WebSocket连接</button>
|
||||
<button onclick="checkStatus()">检查连接状态</button>
|
||||
<button onclick="clearLog()">清空日志</button>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const logDiv = document.getElementById('log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const className = type === 'error' ? 'color: red;' :
|
||||
type === 'success' ? 'color: green;' :
|
||||
type === 'warning' ? 'color: orange;' : '';
|
||||
logDiv.innerHTML += `<div style="${className}">[${timestamp}] ${message}</div>`;
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(message, type = 'info') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.textContent = `状态:${message}`;
|
||||
statusDiv.className = `status ${type}`;
|
||||
}
|
||||
|
||||
// 测试正常连接(通过前端框架)
|
||||
async function testNormalConnect() {
|
||||
log('🔍 测试正常连接模式...');
|
||||
updateStatus('测试正常连接中...', 'warning');
|
||||
|
||||
try {
|
||||
// 这里需要调用前端框架的连接方法
|
||||
if (window.wsDebug && window.wsDebug.normalConnect) {
|
||||
const result = await window.wsDebug.normalConnect();
|
||||
if (result) {
|
||||
log('✅ 正常连接成功', 'success');
|
||||
updateStatus('正常连接成功', 'success');
|
||||
} else {
|
||||
log('❌ 正常连接失败', 'error');
|
||||
updateStatus('正常连接失败', 'error');
|
||||
}
|
||||
} else {
|
||||
log('⚠️ wsDebug.normalConnect 不可用', 'warning');
|
||||
updateStatus('调试功能不可用', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ 正常连接异常: ${error}`, 'error');
|
||||
updateStatus('正常连接异常', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 测试强制连接(通过前端框架)
|
||||
async function testForceConnect() {
|
||||
log('🚀 测试强制连接模式...');
|
||||
updateStatus('测试强制连接中...', 'warning');
|
||||
|
||||
try {
|
||||
if (window.wsDebug && window.wsDebug.forceConnect) {
|
||||
const result = await window.wsDebug.forceConnect();
|
||||
if (result) {
|
||||
log('✅ 强制连接成功', 'success');
|
||||
updateStatus('强制连接成功', 'success');
|
||||
} else {
|
||||
log('❌ 强制连接失败', 'error');
|
||||
updateStatus('强制连接失败', 'error');
|
||||
}
|
||||
} else {
|
||||
log('⚠️ wsDebug.forceConnect 不可用', 'warning');
|
||||
updateStatus('调试功能不可用', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ 强制连接异常: ${error}`, 'error');
|
||||
updateStatus('强制连接异常', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 测试直接WebSocket连接
|
||||
function testDirectConnect() {
|
||||
log('🔗 测试直接WebSocket连接...');
|
||||
updateStatus('测试直接连接中...', 'warning');
|
||||
|
||||
try {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
log('⚠️ WebSocket已经连接,先关闭', 'warning');
|
||||
ws.close();
|
||||
}
|
||||
|
||||
ws = new WebSocket('ws://localhost:36163/api/core/ws');
|
||||
|
||||
ws.onopen = () => {
|
||||
log('✅ 直接WebSocket连接成功', 'success');
|
||||
updateStatus('直接连接成功', 'success');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
log(`📨 收到消息: ${JSON.stringify(data)}`, 'info');
|
||||
} catch (e) {
|
||||
log(`📨 收到消息: ${event.data}`, 'info');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
log(`❌ WebSocket错误: ${error}`, 'error');
|
||||
updateStatus('直接连接错误', 'error');
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
log(`🔒 WebSocket关闭: code=${event.code}, reason="${event.reason}"`, 'warning');
|
||||
updateStatus('连接已关闭', 'warning');
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 直接连接异常: ${error}`, 'error');
|
||||
updateStatus('直接连接异常', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查连接状态
|
||||
function checkStatus() {
|
||||
log('🔍 检查连接状态...');
|
||||
|
||||
// 检查直接WebSocket
|
||||
if (ws) {
|
||||
const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
||||
log(`📡 直接WebSocket状态: ${states[ws.readyState]} (${ws.readyState})`);
|
||||
} else {
|
||||
log('📡 直接WebSocket: 未创建');
|
||||
}
|
||||
|
||||
// 检查框架WebSocket
|
||||
if (window.wsDebug) {
|
||||
try {
|
||||
const info = window.wsDebug.getConnectionInfo ? window.wsDebug.getConnectionInfo() : {};
|
||||
log(`🔧 框架WebSocket状态: ${JSON.stringify(info, null, 2)}`);
|
||||
|
||||
const storage = window.wsDebug.getGlobalStorage ? window.wsDebug.getGlobalStorage() : {};
|
||||
log(`💾 全局存储状态: allowNewConnection=${storage.allowNewConnection}, connectionReason="${storage.connectionReason}"`);
|
||||
|
||||
if (window.wsDebug.allowedReasons) {
|
||||
log(`✅ 允许的连接原因: ${JSON.stringify(window.wsDebug.allowedReasons)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ 检查框架状态异常: ${error}`, 'error');
|
||||
}
|
||||
} else {
|
||||
log('🔧 框架WebSocket调试功能不可用');
|
||||
}
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log').innerHTML = '';
|
||||
}
|
||||
|
||||
// 页面加载时检查状态
|
||||
window.addEventListener('load', () => {
|
||||
log('📋 WebSocket强制连接测试工具已加载');
|
||||
checkStatus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,225 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket消息测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.log {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>WebSocket消息测试工具</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>连接状态</h3>
|
||||
<button onclick="connectWebSocket()">连接WebSocket</button>
|
||||
<button onclick="disconnectWebSocket()">断开连接</button>
|
||||
<div id="status" class="status disconnected">未连接</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>测试消息</h3>
|
||||
<button onclick="sendQuestionMessage()">发送Question消息</button>
|
||||
<button onclick="sendObjectMessage()">发送普通对象消息</button>
|
||||
<button onclick="sendStringMessage()">发送字符串消息</button>
|
||||
<button onclick="sendMalformedMessage()">发送格式错误消息</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>消息日志</h3>
|
||||
<button onclick="clearLog()">清空日志</button>
|
||||
<div id="log" class="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
|
||||
function log(message) {
|
||||
const logDiv = document.getElementById('log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logDiv.innerHTML += `[${timestamp}] ${message}\n`;
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(connected) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
if (connected) {
|
||||
statusDiv.textContent = 'WebSocket已连接';
|
||||
statusDiv.className = 'status connected';
|
||||
} else {
|
||||
statusDiv.textContent = 'WebSocket未连接';
|
||||
statusDiv.className = 'status disconnected';
|
||||
}
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
log('WebSocket已经连接');
|
||||
return;
|
||||
}
|
||||
|
||||
ws = new WebSocket('ws://localhost:36163/api/core/ws');
|
||||
|
||||
ws.onopen = function() {
|
||||
log('WebSocket连接已建立');
|
||||
updateStatus(true);
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
log('收到消息: ' + event.data);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
log('解析后的消息: ' + JSON.stringify(data, null, 2));
|
||||
} catch (e) {
|
||||
log('消息解析失败: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
log('WebSocket连接已关闭');
|
||||
updateStatus(false);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
log('WebSocket错误: ' + error);
|
||||
updateStatus(false);
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectWebSocket() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
log('主动断开WebSocket连接');
|
||||
updateStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
log('错误: WebSocket未连接');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageStr = JSON.stringify(message);
|
||||
ws.send(messageStr);
|
||||
log('发送消息: ' + messageStr);
|
||||
}
|
||||
|
||||
function sendQuestionMessage() {
|
||||
const message = {
|
||||
id: "test_id_" + Date.now(),
|
||||
type: "message",
|
||||
data: {
|
||||
type: "Question",
|
||||
message_id: "q_" + Date.now(),
|
||||
title: "测试问题",
|
||||
message: "这是一个测试问题,请选择是否继续?"
|
||||
}
|
||||
};
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
function sendObjectMessage() {
|
||||
const message = {
|
||||
id: "test_obj_" + Date.now(),
|
||||
type: "message",
|
||||
data: {
|
||||
action: "test_action",
|
||||
status: "running",
|
||||
content: "这是一个普通的对象消息"
|
||||
}
|
||||
};
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
function sendStringMessage() {
|
||||
const message = {
|
||||
id: "test_str_" + Date.now(),
|
||||
type: "message",
|
||||
data: "这是一个字符串消息"
|
||||
};
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
function sendMalformedMessage() {
|
||||
const message = {
|
||||
id: "test_malformed_" + Date.now(),
|
||||
type: "message",
|
||||
data: {
|
||||
type: "Question",
|
||||
// 缺少 message_id
|
||||
title: "格式错误的问题",
|
||||
message: "这个消息缺少message_id字段"
|
||||
}
|
||||
};
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log').innerHTML = '';
|
||||
}
|
||||
|
||||
// 页面加载时自动连接
|
||||
window.onload = function() {
|
||||
log('页面已加载,准备测试WebSocket连接');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user