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.core import Config
|
||||||
from app.services import System, Notify
|
from app.services import System, Notify
|
||||||
from app.models.schema import *
|
from app.models.schema import *
|
||||||
from app.models.config import Webhook as WebhookConfig
|
import uuid
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/setting", tags=["全局设置"])
|
router = APIRouter(prefix="/api/setting", tags=["全局设置"])
|
||||||
|
|
||||||
@@ -100,99 +100,169 @@ async def test_notify() -> OutBase:
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/webhook/get",
|
"/webhook/create",
|
||||||
summary="查询 webhook 配置",
|
summary="创建自定义Webhook",
|
||||||
response_model=WebhookGetOut,
|
response_model=OutBase,
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut:
|
async def create_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||||
|
"""创建自定义Webhook"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
index, data = await Config.get_webhook(None, None, webhook.webhookId)
|
# 生成唯一ID
|
||||||
index = [WebhookIndexItem(**_) for _ in index]
|
webhook_id = str(uuid.uuid4())
|
||||||
data = {uid: Webhook(**cfg) for uid, cfg in data.items()}
|
|
||||||
|
# 创建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:
|
except Exception as e:
|
||||||
return WebhookGetOut(
|
return OutBase(
|
||||||
code=500,
|
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||||
status="error",
|
|
||||||
message=f"{type(e).__name__}: {str(e)}",
|
|
||||||
index=[],
|
|
||||||
data={},
|
|
||||||
)
|
)
|
||||||
return WebhookGetOut(index=index, data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/webhook/add",
|
"/webhook/update",
|
||||||
summary="添加定时项",
|
summary="更新自定义Webhook",
|
||||||
response_model=WebhookCreateOut,
|
response_model=OutBase,
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
async def add_webhook() -> WebhookCreateOut:
|
async def update_webhook(webhook_data: dict = Body(...)) -> OutBase:
|
||||||
|
"""更新自定义Webhook"""
|
||||||
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:
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await Config.update_webhook(
|
webhook_id = webhook_data.get("id")
|
||||||
None, None, webhook.webhookId, webhook.data.model_dump(exclude_unset=True)
|
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:
|
except Exception as e:
|
||||||
return OutBase(
|
return OutBase(
|
||||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||||
)
|
)
|
||||||
return OutBase()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@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:
|
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:
|
except Exception as e:
|
||||||
return OutBase(
|
return OutBase(
|
||||||
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
|
||||||
)
|
)
|
||||||
return OutBase()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@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:
|
async def test_webhook(webhook_data: dict = 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:
|
|
||||||
"""测试自定义Webhook"""
|
"""测试自定义Webhook"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
webhook_config = WebhookConfig()
|
webhook_config = {
|
||||||
await webhook_config.load(webhook.data.model_dump())
|
"name": webhook_data.get("name", "测试Webhook"),
|
||||||
await Notify.WebhookPush(
|
"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测试",
|
"AUTO-MAS Webhook测试",
|
||||||
"这是一条测试消息,如果您收到此消息,说明Webhook配置正确!",
|
"这是一条测试消息,如果您收到此消息,说明Webhook配置正确!",
|
||||||
webhook_config,
|
webhook_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return OutBase(message="Webhook测试成功")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return OutBase(code=500, status="error", message=f"Webhook测试失败: {str(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):
|
async def put(self, item):
|
||||||
"""向所有订阅者广播消息"""
|
"""向所有订阅者广播消息"""
|
||||||
|
logger.debug(f"向所有订阅者广播消息: {item}")
|
||||||
for subscriber in self.__subscribers:
|
for subscriber in self.__subscribers:
|
||||||
await subscriber.put(deepcopy(item))
|
await subscriber.put(deepcopy(item))
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,10 @@ class _TaskManager:
|
|||||||
actual_id = None
|
actual_id = None
|
||||||
else:
|
else:
|
||||||
for script_id, script in Config.ScriptConfig.items():
|
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
|
task_id = script_id
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -139,26 +142,31 @@ class _TaskManager:
|
|||||||
# 初始化任务列表
|
# 初始化任务列表
|
||||||
if task_id in Config.QueueConfig:
|
if task_id in Config.QueueConfig:
|
||||||
|
|
||||||
|
queue = Config.QueueConfig[task_id]
|
||||||
|
if not isinstance(queue, QueueConfig):
|
||||||
|
return
|
||||||
|
|
||||||
task_list = []
|
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") == "-":
|
if queue_item.get("Info", "ScriptId") == "-":
|
||||||
continue
|
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(
|
task_list.append(
|
||||||
{
|
{
|
||||||
"script_id": str(script_uid),
|
"script_id": str(script_id),
|
||||||
"status": "等待",
|
"status": "等待",
|
||||||
"name": Config.ScriptConfig[script_uid].get("Info", "Name"),
|
"name": script.get("Info", "Name"),
|
||||||
"user_list": [
|
"user_list": [
|
||||||
{
|
{
|
||||||
"user_id": str(user_id),
|
"user_id": str(user_id),
|
||||||
"status": "等待",
|
"status": "等待",
|
||||||
"name": config.get("Info", "Name"),
|
"name": config.get("Info", "Name"),
|
||||||
}
|
}
|
||||||
for user_id, config in Config.ScriptConfig[
|
for user_id, config in script.UserData.items()
|
||||||
script_uid
|
|
||||||
].UserData.items()
|
|
||||||
if config.get("Info", "Status")
|
if config.get("Info", "Status")
|
||||||
and config.get("Info", "RemainedDay") != 0
|
and config.get("Info", "RemainedDay") != 0
|
||||||
],
|
],
|
||||||
@@ -167,20 +175,23 @@ class _TaskManager:
|
|||||||
|
|
||||||
elif actual_id is not None and actual_id in Config.ScriptConfig:
|
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 = [
|
task_list = [
|
||||||
{
|
{
|
||||||
"script_id": str(actual_id),
|
"script_id": str(actual_id),
|
||||||
"status": "等待",
|
"status": "等待",
|
||||||
"name": Config.ScriptConfig[actual_id].get("Info", "Name"),
|
"name": script.get("Info", "Name"),
|
||||||
"user_list": [
|
"user_list": [
|
||||||
{
|
{
|
||||||
"user_id": str(user_id),
|
"user_id": str(user_id),
|
||||||
"status": "等待",
|
"status": "等待",
|
||||||
"name": config.get("Info", "Name"),
|
"name": config.get("Info", "Name"),
|
||||||
}
|
}
|
||||||
for user_id, config in Config.ScriptConfig[
|
for user_id, config in script.UserData.items()
|
||||||
actual_id
|
|
||||||
].UserData.items()
|
|
||||||
if config.get("Info", "Status")
|
if config.get("Info", "Status")
|
||||||
and config.get("Info", "RemainedDay") != 0
|
and config.get("Info", "RemainedDay") != 0
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ class _MainTimer:
|
|||||||
|
|
||||||
for uid, queue in Config.QueueConfig.items():
|
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
|
continue
|
||||||
|
|
||||||
# 避免重复调起任务
|
# 避免重复调起任务
|
||||||
|
|||||||
@@ -25,10 +25,9 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
import win32com.client
|
import win32com.client
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from urllib.parse import urlparse
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
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
|
from app.utils import dpapi_encrypt, dpapi_decrypt
|
||||||
@@ -128,25 +127,17 @@ class DateTimeValidator(ConfigValidator):
|
|||||||
|
|
||||||
class JSONValidator(ConfigValidator):
|
class JSONValidator(ConfigValidator):
|
||||||
|
|
||||||
def __init__(self, tpye: type[dict] | type[list] = dict) -> None:
|
|
||||||
self.type = tpye
|
|
||||||
|
|
||||||
def validate(self, value: Any) -> bool:
|
def validate(self, value: Any) -> bool:
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
data = json.loads(value)
|
json.loads(value)
|
||||||
if isinstance(data, self.type):
|
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
return False
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def correct(self, value: Any) -> str:
|
def correct(self, value: Any) -> str:
|
||||||
return (
|
return value if self.validate(value) else "{ }"
|
||||||
value if self.validate(value) else ("{ }" if self.type == dict else "[ ]")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EncryptValidator(ConfigValidator):
|
class EncryptValidator(ConfigValidator):
|
||||||
@@ -255,67 +246,6 @@ class UserNameValidator(ConfigValidator):
|
|||||||
return value
|
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:
|
class ConfigItem:
|
||||||
"""配置项"""
|
"""配置项"""
|
||||||
|
|
||||||
@@ -607,10 +537,7 @@ class ConfigBase:
|
|||||||
await item.unlock()
|
await item.unlock()
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="ConfigBase")
|
class MultipleConfig:
|
||||||
|
|
||||||
|
|
||||||
class MultipleConfig(Generic[T]):
|
|
||||||
"""
|
"""
|
||||||
多配置项管理类
|
多配置项管理类
|
||||||
|
|
||||||
@@ -623,7 +550,7 @@ class MultipleConfig(Generic[T]):
|
|||||||
子配置项的类型列表, 必须是 ConfigBase 的子类
|
子配置项的类型列表, 必须是 ConfigBase 的子类
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, sub_config_type: List[Type[T]]):
|
def __init__(self, sub_config_type: List[type]):
|
||||||
|
|
||||||
if not sub_config_type:
|
if not sub_config_type:
|
||||||
raise ValueError("子配置项类型列表不能为空")
|
raise ValueError("子配置项类型列表不能为空")
|
||||||
@@ -634,13 +561,13 @@ class MultipleConfig(Generic[T]):
|
|||||||
f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
|
f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.sub_config_type: List[Type[T]] = sub_config_type
|
self.sub_config_type = sub_config_type
|
||||||
self.file: Path | None = None
|
self.file: None | Path = None
|
||||||
self.order: List[uuid.UUID] = []
|
self.order: List[uuid.UUID] = []
|
||||||
self.data: Dict[uuid.UUID, T] = {}
|
self.data: Dict[uuid.UUID, ConfigBase] = {}
|
||||||
self.is_locked = False
|
self.is_locked = False
|
||||||
|
|
||||||
def __getitem__(self, key: uuid.UUID) -> T:
|
def __getitem__(self, key: uuid.UUID) -> ConfigBase:
|
||||||
"""允许通过 config[uuid] 访问配置项"""
|
"""允许通过 config[uuid] 访问配置项"""
|
||||||
if key not in self.data:
|
if key not in self.data:
|
||||||
raise KeyError(f"配置项 '{key}' 不存在")
|
raise KeyError(f"配置项 '{key}' 不存在")
|
||||||
@@ -738,9 +665,7 @@ class MultipleConfig(Generic[T]):
|
|||||||
if self.file:
|
if self.file:
|
||||||
await self.save()
|
await self.save()
|
||||||
|
|
||||||
async def toDict(
|
async def toDict(self) -> Dict[str, Union[list, dict]]:
|
||||||
self, ignore_multi_config: bool = False, if_decrypt: bool = True
|
|
||||||
) -> Dict[str, Union[list, dict]]:
|
|
||||||
"""
|
"""
|
||||||
将配置项转换为字典
|
将配置项转换为字典
|
||||||
|
|
||||||
@@ -753,7 +678,7 @@ class MultipleConfig(Generic[T]):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
for uid, config in self.items():
|
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
|
return data
|
||||||
|
|
||||||
async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
|
async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
|
||||||
@@ -796,7 +721,7 @@ class MultipleConfig(Generic[T]):
|
|||||||
encoding="utf-8",
|
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"
|
__license__ = "GPL-3.0 license"
|
||||||
|
|
||||||
from .ConfigBase import *
|
from .ConfigBase import *
|
||||||
from .config import *
|
|
||||||
from .schema 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):
|
class GlobalConfig_Function(BaseModel):
|
||||||
HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
|
HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
|
||||||
None, description="历史记录保留时间, 0表示永久保存"
|
None, description="历史记录保留时间, 0表示永久保存"
|
||||||
@@ -135,6 +111,18 @@ class GlobalConfig_UI(BaseModel):
|
|||||||
IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘")
|
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):
|
class GlobalConfig_Notify(BaseModel):
|
||||||
SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
|
SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
|
||||||
default=None, description="任务结果推送时机"
|
default=None, description="任务结果推送时机"
|
||||||
@@ -155,6 +143,9 @@ class GlobalConfig_Notify(BaseModel):
|
|||||||
default=None, description="是否使用ServerChan推送"
|
default=None, description="是否使用ServerChan推送"
|
||||||
)
|
)
|
||||||
ServerChanKey: Optional[str] = Field(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):
|
class GlobalConfig_Update(BaseModel):
|
||||||
@@ -322,6 +313,9 @@ class MaaUserConfig_Notify(BaseModel):
|
|||||||
default=None, description="是否使用Server酱推送"
|
default=None, description="是否使用Server酱推送"
|
||||||
)
|
)
|
||||||
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
|
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
|
||||||
|
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
|
||||||
|
default=None, description="用户自定义Webhook列表"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GeneralUserConfig_Notify(BaseModel):
|
class GeneralUserConfig_Notify(BaseModel):
|
||||||
@@ -624,50 +618,6 @@ class UserSetIn(UserInBase):
|
|||||||
jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件")
|
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):
|
class PlanCreateIn(BaseModel):
|
||||||
type: Literal["MaaPlan"]
|
type: Literal["MaaPlan"]
|
||||||
|
|
||||||
|
|||||||
@@ -21,20 +21,17 @@
|
|||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
import smtplib
|
import smtplib
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
|
||||||
from plyer import notification
|
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
|
||||||
|
from plyer import notification
|
||||||
|
|
||||||
from app.core import Config
|
from app.core import Config
|
||||||
from app.models.config import Webhook
|
|
||||||
from app.utils import get_logger, ImageUtils
|
from app.utils import get_logger, ImageUtils
|
||||||
|
|
||||||
logger = get_logger("通知服务")
|
logger = get_logger("通知服务")
|
||||||
@@ -42,24 +39,21 @@ logger = get_logger("通知服务")
|
|||||||
|
|
||||||
class Notification:
|
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
|
:param title: 通知标题
|
||||||
----------
|
:param message: 通知内容
|
||||||
title: str
|
:param ticker: 通知横幅
|
||||||
通知标题
|
:param t: 通知持续时间
|
||||||
message: str
|
:return: bool
|
||||||
通知内容
|
|
||||||
ticker: str
|
|
||||||
通知横幅
|
|
||||||
t: int
|
|
||||||
通知持续时间
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not Config.get("Notify", "IfPushPlyer"):
|
if Config.get("Notify", "IfPushPlyer"):
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"推送系统通知: {title}")
|
logger.info(f"推送系统通知: {title}")
|
||||||
|
|
||||||
@@ -76,42 +70,40 @@ class Notification:
|
|||||||
else:
|
else:
|
||||||
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
|
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
|
||||||
|
|
||||||
async def send_mail(
|
return True
|
||||||
self, mode: Literal["文本", "网页"], title: str, content: str, to_address: str
|
|
||||||
) -> None:
|
async def send_mail(self, mode, title, content, to_address) -> None:
|
||||||
"""
|
"""
|
||||||
推送邮件通知
|
推送邮件通知
|
||||||
|
|
||||||
Parameters
|
:param mode: 邮件内容模式, 支持 "文本" 和 "网页"
|
||||||
----------
|
:param title: 邮件标题
|
||||||
mode: Literal["文本", "网页"]
|
:param content: 邮件内容
|
||||||
邮件内容模式, 支持 "文本" 和 "网页"
|
:param to_address: 收件人地址
|
||||||
title: str
|
|
||||||
邮件标题
|
|
||||||
content: str
|
|
||||||
邮件内容
|
|
||||||
to_address: str
|
|
||||||
收件人地址
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if Config.get("Notify", "SMTPServerAddress") == "":
|
if (
|
||||||
raise ValueError("邮件通知的SMTP服务器地址不能为空")
|
Config.get("Notify", "SMTPServerAddress") == ""
|
||||||
if Config.get("Notify", "AuthorizationCode") == "":
|
or Config.get("Notify", "AuthorizationCode") == ""
|
||||||
raise ValueError("邮件通知的授权码不能为空")
|
or not bool(
|
||||||
if not bool(
|
|
||||||
re.match(
|
re.match(
|
||||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||||
Config.get("Notify", "FromAddress"),
|
Config.get("Notify", "FromAddress"),
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
raise ValueError("邮件通知的发送邮箱格式错误或为空")
|
or not bool(
|
||||||
if not bool(
|
|
||||||
re.match(
|
re.match(
|
||||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||||
to_address,
|
to_address,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
raise ValueError("邮件通知的接收邮箱格式错误或为空")
|
logger.error(
|
||||||
|
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
"邮件通知的SMTP服务器地址、授权码、发件人地址或收件人地址未正确配置"
|
||||||
|
)
|
||||||
|
|
||||||
# 定义邮件正文
|
# 定义邮件正文
|
||||||
if mode == "文本":
|
if mode == "文本":
|
||||||
@@ -143,21 +135,16 @@ class Notification:
|
|||||||
smtpObj.quit()
|
smtpObj.quit()
|
||||||
logger.success(f"邮件发送成功: {title}")
|
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酱推送通知
|
使用Server酱推送通知
|
||||||
|
|
||||||
Parameters
|
:param title: 通知标题
|
||||||
----------
|
:param content: 通知内容
|
||||||
title: str
|
:param send_key: Server酱的SendKey
|
||||||
通知标题
|
|
||||||
content: str
|
|
||||||
通知内容
|
|
||||||
send_key: str
|
|
||||||
Server酱的SendKey
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if send_key == "":
|
if not send_key:
|
||||||
raise ValueError("ServerChan SendKey 不能为空")
|
raise ValueError("ServerChan SendKey 不能为空")
|
||||||
|
|
||||||
# 构造 URL
|
# 构造 URL
|
||||||
@@ -184,33 +171,33 @@ class Notification:
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"ServerChan 推送通知失败: {response.text}")
|
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
|
:param title: 通知标题
|
||||||
----------
|
:param content: 通知内容
|
||||||
title: str
|
:param webhook_config: Webhook配置对象
|
||||||
通知标题
|
|
||||||
content: str
|
|
||||||
通知内容
|
|
||||||
webhook: Webhook
|
|
||||||
Webhook配置对象
|
|
||||||
"""
|
"""
|
||||||
if not webhook.get("Info", "Enabled"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if webhook.get("Data", "Url") == "":
|
if not webhook_config.get("url"):
|
||||||
raise ValueError("Webhook URL 不能为空")
|
raise ValueError("Webhook URL 不能为空")
|
||||||
|
|
||||||
|
if not webhook_config.get("enabled", True):
|
||||||
|
logger.info(
|
||||||
|
f"Webhook {webhook_config.get('name', 'Unknown')} 已禁用,跳过推送"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# 解析模板
|
# 解析模板
|
||||||
template = (
|
template = webhook_config.get(
|
||||||
webhook.get("Data", "Template")
|
"template", '{"title": "{title}", "content": "{content}"}'
|
||||||
or '{"title": "{title}", "content": "{content}"}'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 替换模板变量
|
# 替换模板变量
|
||||||
try:
|
try:
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# 准备模板变量
|
# 准备模板变量
|
||||||
template_vars = {
|
template_vars = {
|
||||||
@@ -277,38 +264,34 @@ class Notification:
|
|||||||
|
|
||||||
# 准备请求头
|
# 准备请求头
|
||||||
headers = {"Content-Type": "application/json"}
|
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":
|
# 发送请求
|
||||||
|
method = webhook_config.get("method", "POST").upper()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if method == "POST":
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=webhook.get("Data", "Url"),
|
url=webhook_config["url"],
|
||||||
json=data,
|
json=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
proxies=Config.get_proxies(),
|
proxies=Config.get_proxies(),
|
||||||
)
|
)
|
||||||
elif isinstance(data, str):
|
else:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=webhook.get("Data", "Url"),
|
url=webhook_config["url"],
|
||||||
data=data,
|
data=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
proxies=Config.get_proxies(),
|
proxies=Config.get_proxies(),
|
||||||
)
|
)
|
||||||
elif webhook.get("Data", "Method") == "GET":
|
else: # GET
|
||||||
if isinstance(data, dict):
|
params = data if isinstance(data, dict) else {"message": data}
|
||||||
# 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(
|
response = requests.get(
|
||||||
url=webhook.get("Data", "Url"),
|
url=webhook_config["url"],
|
||||||
params=params,
|
params=params,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
@@ -318,14 +301,19 @@ class Notification:
|
|||||||
# 检查响应
|
# 检查响应
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
logger.success(
|
logger.success(
|
||||||
f"自定义Webhook推送成功: {webhook.get('Info', 'Name')} - {title}"
|
f"自定义Webhook推送成功: {webhook_config.get('name', 'Unknown')} - {title}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
||||||
|
|
||||||
async def _WebHookPush(self, title, content, webhook_url) -> None:
|
except Exception as e:
|
||||||
|
raise Exception(
|
||||||
|
f"自定义Webhook推送失败 ({webhook_config.get('name', 'Unknown')}): {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def WebHookPush(self, title, content, webhook_url) -> None:
|
||||||
"""
|
"""
|
||||||
WebHook 推送通知 (即将弃用)
|
WebHook 推送通知 (兼容旧版企业微信格式)
|
||||||
|
|
||||||
:param title: 通知标题
|
:param title: 通知标题
|
||||||
:param content: 通知内容
|
:param content: 通知内容
|
||||||
@@ -352,7 +340,7 @@ class Notification:
|
|||||||
self, image_path: Path, webhook_url: str
|
self, image_path: Path, webhook_url: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
使用企业微信群机器人推送图片通知(等待重新适配)
|
使用企业微信群机器人推送图片通知
|
||||||
|
|
||||||
:param image_path: 图片文件路径
|
:param image_path: 图片文件路径
|
||||||
:param webhook_url: 企业微信群机器人的WebHook地址
|
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||||
@@ -418,12 +406,23 @@ class Notification:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 发送自定义Webhook通知
|
# 发送自定义Webhook通知
|
||||||
for webhook in Config.Notify_CustomWebhooks.values():
|
try:
|
||||||
await self.WebhookPush(
|
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 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
|
||||||
webhook,
|
webhook,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"自定义Webhook测试失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
logger.success("测试通知发送完成")
|
logger.success("测试通知发送完成")
|
||||||
|
|
||||||
|
|||||||
308
app/task/MAA.py
308
app/task/MAA.py
@@ -74,6 +74,7 @@ class MaaManager:
|
|||||||
await Config.ScriptConfig[self.script_id].lock()
|
await Config.ScriptConfig[self.script_id].lock()
|
||||||
|
|
||||||
self.script_config = Config.ScriptConfig[self.script_id]
|
self.script_config = Config.ScriptConfig[self.script_id]
|
||||||
|
if isinstance(self.script_config, MaaConfig):
|
||||||
self.user_config = MultipleConfig([MaaUserConfig])
|
self.user_config = MultipleConfig([MaaUserConfig])
|
||||||
await self.user_config.load(await self.script_config.UserData.toDict())
|
await self.user_config.load(await self.script_config.UserData.toDict())
|
||||||
|
|
||||||
@@ -95,8 +96,6 @@ class MaaManager:
|
|||||||
def check_config(self) -> str:
|
def check_config(self) -> str:
|
||||||
"""检查配置是否可用"""
|
"""检查配置是否可用"""
|
||||||
|
|
||||||
if not isinstance(Config.ScriptConfig[self.script_id], MaaConfig):
|
|
||||||
return "脚本配置类型错误, 不是MAA脚本类型"
|
|
||||||
if not self.maa_exe_path.exists():
|
if not self.maa_exe_path.exists():
|
||||||
return "MAA.exe文件不存在, 请检查MAA路径设置!"
|
return "MAA.exe文件不存在, 请检查MAA路径设置!"
|
||||||
if not self.maa_set_path.exists():
|
if not self.maa_set_path.exists():
|
||||||
@@ -176,7 +175,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 开始代理
|
# 开始代理
|
||||||
for self.index, user in enumerate(self.user_list):
|
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 (
|
if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
|
||||||
@@ -213,7 +212,7 @@ class MaaManager:
|
|||||||
"Annihilation": bool(
|
"Annihilation": bool(
|
||||||
self.cur_user_data.get("Info", "Annihilation") == "Close"
|
self.cur_user_data.get("Info", "Annihilation") == "Close"
|
||||||
),
|
),
|
||||||
"Routine": self.cur_user_data.get("Info", "Mode") == "详细"
|
"Routine": self.cur_user_data.get("Info", "Mode") == "复杂"
|
||||||
and not self.cur_user_data.get("Info", "Routine"),
|
and not self.cur_user_data.get("Info", "Routine"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,9 +222,11 @@ class MaaManager:
|
|||||||
if self.cur_user_data.get(
|
if self.cur_user_data.get(
|
||||||
"Info", "IfSkland"
|
"Info", "IfSkland"
|
||||||
) and self.cur_user_data.get("Info", "SklandToken"):
|
) and self.cur_user_data.get("Info", "SklandToken"):
|
||||||
|
|
||||||
if self.cur_user_data.get(
|
if self.cur_user_data.get(
|
||||||
"Data", "LastSklandDate"
|
"Data", "LastSklandDate"
|
||||||
) != datetime.now().strftime("%Y-%m-%d"):
|
) != datetime.now().strftime("%Y-%m-%d"):
|
||||||
|
|
||||||
await Config.send_json(
|
await Config.send_json(
|
||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
id=self.ws_id,
|
id=self.ws_id,
|
||||||
@@ -239,6 +240,7 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for type, user_list in skland_result.items():
|
for type, user_list in skland_result.items():
|
||||||
|
|
||||||
if type != "总计" and len(user_list) > 0:
|
if type != "总计" and len(user_list) > 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"用户: {user['user_id']} - 森空岛签到{type}: {'、'.join(user_list)}"
|
f"用户: {user['user_id']} - 森空岛签到{type}: {'、'.join(user_list)}"
|
||||||
@@ -249,9 +251,7 @@ class MaaManager:
|
|||||||
type="Info",
|
type="Info",
|
||||||
data={
|
data={
|
||||||
(
|
(
|
||||||
"Info"
|
"Info" if type != "失败" else "Error"
|
||||||
if type != "失败"
|
|
||||||
else "Error"
|
|
||||||
): f"用户 {user['name']} 森空岛签到{type}: {'、'.join(user_list)}"
|
): f"用户 {user['name']} 森空岛签到{type}: {'、'.join(user_list)}"
|
||||||
},
|
},
|
||||||
).model_dump()
|
).model_dump()
|
||||||
@@ -294,6 +294,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 剿灭-日常模式循环
|
# 剿灭-日常模式循环
|
||||||
for mode in ["Annihilation", "Routine"]:
|
for mode in ["Annihilation", "Routine"]:
|
||||||
|
|
||||||
if self.run_book[mode]:
|
if self.run_book[mode]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -305,9 +306,7 @@ class MaaManager:
|
|||||||
self.cur_user_data.get("Data", "LastAnnihilationDate"),
|
self.cur_user_data.get("Data", "LastAnnihilationDate"),
|
||||||
"%Y-%m-%d",
|
"%Y-%m-%d",
|
||||||
).isocalendar()[:2]
|
).isocalendar()[:2]
|
||||||
== datetime.strptime(
|
== datetime.strptime(self.curdate, "%Y-%m-%d").isocalendar()[:2]
|
||||||
self.curdate, "%Y-%m-%d"
|
|
||||||
).isocalendar()[:2]
|
|
||||||
):
|
):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"用户: {user['user_id']} - 本周剿灭模式已达上限, 跳过执行剿灭任务"
|
f"用户: {user['user_id']} - 本周剿灭模式已达上限, 跳过执行剿灭任务"
|
||||||
@@ -331,9 +330,7 @@ class MaaManager:
|
|||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
id=self.ws_id,
|
id=self.ws_id,
|
||||||
type="Info",
|
type="Info",
|
||||||
data={
|
data={"Error": f"未找到 {user['name']} 的详细配置文件"},
|
||||||
"Error": f"未找到 {user['name']} 的详细配置文件"
|
|
||||||
},
|
|
||||||
).model_dump()
|
).model_dump()
|
||||||
)
|
)
|
||||||
self.run_book[mode] = False
|
self.run_book[mode] = False
|
||||||
@@ -355,20 +352,15 @@ class MaaManager:
|
|||||||
|
|
||||||
# 解析任务构成
|
# 解析任务构成
|
||||||
if mode == "Routine":
|
if mode == "Routine":
|
||||||
|
|
||||||
self.task_dict = {
|
self.task_dict = {
|
||||||
"WakeUp": str(
|
"WakeUp": str(self.cur_user_data.get("Task", "IfWakeUp")),
|
||||||
self.cur_user_data.get("Task", "IfWakeUp")
|
|
||||||
),
|
|
||||||
"Recruiting": str(
|
"Recruiting": str(
|
||||||
self.cur_user_data.get("Task", "IfRecruiting")
|
self.cur_user_data.get("Task", "IfRecruiting")
|
||||||
),
|
),
|
||||||
"Base": str(self.cur_user_data.get("Task", "IfBase")),
|
"Base": str(self.cur_user_data.get("Task", "IfBase")),
|
||||||
"Combat": str(
|
"Combat": str(self.cur_user_data.get("Task", "IfCombat")),
|
||||||
self.cur_user_data.get("Task", "IfCombat")
|
"Mission": str(self.cur_user_data.get("Task", "IfMission")),
|
||||||
),
|
|
||||||
"Mission": str(
|
|
||||||
self.cur_user_data.get("Task", "IfMission")
|
|
||||||
),
|
|
||||||
"Mall": str(self.cur_user_data.get("Task", "IfMall")),
|
"Mall": str(self.cur_user_data.get("Task", "IfMall")),
|
||||||
"AutoRoguelike": str(
|
"AutoRoguelike": str(
|
||||||
self.cur_user_data.get("Task", "IfAutoRoguelike")
|
self.cur_user_data.get("Task", "IfAutoRoguelike")
|
||||||
@@ -379,6 +371,7 @@ class MaaManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
elif mode == "Annihilation":
|
elif mode == "Annihilation":
|
||||||
|
|
||||||
self.task_dict = {
|
self.task_dict = {
|
||||||
"WakeUp": "True",
|
"WakeUp": "True",
|
||||||
"Recruiting": "False",
|
"Recruiting": "False",
|
||||||
@@ -396,6 +389,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 尝试次数循环
|
# 尝试次数循环
|
||||||
for i in range(self.script_config.get("Run", "RunTimesLimit")):
|
for i in range(self.script_config.get("Run", "RunTimesLimit")):
|
||||||
|
|
||||||
if self.run_book[mode]:
|
if self.run_book[mode]:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -422,9 +416,7 @@ class MaaManager:
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
shell = win32com.client.Dispatch("WScript.Shell")
|
shell = win32com.client.Dispatch("WScript.Shell")
|
||||||
shortcut = shell.CreateShortcut(
|
shortcut = shell.CreateShortcut(str(self.emulator_path))
|
||||||
str(self.emulator_path)
|
|
||||||
)
|
|
||||||
self.emulator_path = Path(shortcut.TargetPath)
|
self.emulator_path = Path(shortcut.TargetPath)
|
||||||
self.emulator_arguments = shortcut.Arguments.split()
|
self.emulator_arguments = shortcut.Arguments.split()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -441,9 +433,7 @@ class MaaManager:
|
|||||||
self.if_open_emulator = True
|
self.if_open_emulator = True
|
||||||
break
|
break
|
||||||
elif not self.emulator_path.exists():
|
elif not self.emulator_path.exists():
|
||||||
logger.error(
|
logger.error(f"模拟器快捷方式不存在: {self.emulator_path}")
|
||||||
f"模拟器快捷方式不存在: {self.emulator_path}"
|
|
||||||
)
|
|
||||||
await Config.send_json(
|
await Config.send_json(
|
||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
id=self.ws_id,
|
id=self.ws_id,
|
||||||
@@ -474,9 +464,7 @@ class MaaManager:
|
|||||||
"Connect.Address"
|
"Connect.Address"
|
||||||
]
|
]
|
||||||
self.if_kill_emulator = bool(
|
self.if_kill_emulator = bool(
|
||||||
set["Configurations"]["Default"][
|
set["Configurations"]["Default"]["MainFunction.PostActions"]
|
||||||
"MainFunction.PostActions"
|
|
||||||
]
|
|
||||||
== "12"
|
== "12"
|
||||||
)
|
)
|
||||||
self.if_open_emulator_process = bool(
|
self.if_open_emulator_process = bool(
|
||||||
@@ -557,6 +545,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 处理MAA结果
|
# 处理MAA结果
|
||||||
if self.maa_result == "Success!":
|
if self.maa_result == "Success!":
|
||||||
|
|
||||||
# 标记任务完成
|
# 标记任务完成
|
||||||
self.run_book[mode] = True
|
self.run_book[mode] = True
|
||||||
|
|
||||||
@@ -639,9 +628,7 @@ class MaaManager:
|
|||||||
self.if_open_emulator = True
|
self.if_open_emulator = True
|
||||||
|
|
||||||
# 从配置文件中解析所需信息
|
# 从配置文件中解析所需信息
|
||||||
with self.maa_set_path.open(
|
with self.maa_set_path.open(mode="r", encoding="utf-8") as f:
|
||||||
mode="r", encoding="utf-8"
|
|
||||||
) as f:
|
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# 记录自定义基建索引
|
# 记录自定义基建索引
|
||||||
@@ -685,7 +672,6 @@ class MaaManager:
|
|||||||
/ f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json"
|
/ f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json"
|
||||||
)
|
)
|
||||||
if if_six_star:
|
if if_six_star:
|
||||||
try:
|
|
||||||
await self.push_notification(
|
await self.push_notification(
|
||||||
"公招六星",
|
"公招六星",
|
||||||
f"喜报: 用户 {user['name']} 公招出六星啦!",
|
f"喜报: 用户 {user['name']} 公招出六星啦!",
|
||||||
@@ -693,17 +679,6 @@ class MaaManager:
|
|||||||
"user_name": user["name"],
|
"user_name": user["name"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 执行MAA解压更新动作
|
# 执行MAA解压更新动作
|
||||||
if self.maa_update_package:
|
if self.maa_update_package:
|
||||||
@@ -732,19 +707,9 @@ class MaaManager:
|
|||||||
|
|
||||||
await self.result_record()
|
await self.result_record()
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"代理用户 {user['user_id']} 时出现异常: {e}")
|
|
||||||
user["status"] = "异常"
|
|
||||||
await Config.send_json(
|
|
||||||
WebSocketMessage(
|
|
||||||
id=self.ws_id,
|
|
||||||
type="Info",
|
|
||||||
data={"Error": f"代理用户 {user['name']} 时出现异常: {e}"},
|
|
||||||
).model_dump()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 人工排查模式
|
# 人工排查模式
|
||||||
elif self.mode == "人工排查":
|
elif self.mode == "人工排查":
|
||||||
|
|
||||||
# 人工排查时, 屏蔽静默操作
|
# 人工排查时, 屏蔽静默操作
|
||||||
logger.info("人工排查任务开始, 屏蔽静默操作")
|
logger.info("人工排查任务开始, 屏蔽静默操作")
|
||||||
Config.if_ignore_silence.append(self.script_id)
|
Config.if_ignore_silence.append(self.script_id)
|
||||||
@@ -754,6 +719,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 开始排查
|
# 开始排查
|
||||||
for self.index, user in enumerate(self.user_list):
|
for self.index, user in enumerate(self.user_list):
|
||||||
|
|
||||||
self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
|
self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
|
||||||
|
|
||||||
logger.info(f"开始排查用户: {user['user_id']}")
|
logger.info(f"开始排查用户: {user['user_id']}")
|
||||||
@@ -774,6 +740,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 启动重试循环
|
# 启动重试循环
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
# 配置MAA
|
# 配置MAA
|
||||||
await self.set_maa("人工排查")
|
await self.set_maa("人工排查")
|
||||||
|
|
||||||
@@ -835,6 +802,7 @@ class MaaManager:
|
|||||||
break
|
break
|
||||||
# 登录失败, 询问是否结束循环
|
# 登录失败, 询问是否结束循环
|
||||||
else:
|
else:
|
||||||
|
|
||||||
uid = str(uuid.uuid4())
|
uid = str(uuid.uuid4())
|
||||||
await Config.send_json(
|
await Config.send_json(
|
||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
@@ -845,16 +813,16 @@ class MaaManager:
|
|||||||
"type": "Question",
|
"type": "Question",
|
||||||
"title": "操作提示",
|
"title": "操作提示",
|
||||||
"message": "MAA未能正确登录到PRTS, 是否重试?",
|
"message": "MAA未能正确登录到PRTS, 是否重试?",
|
||||||
"options": ["是", "否"],
|
|
||||||
},
|
},
|
||||||
).model_dump()
|
).model_dump()
|
||||||
)
|
)
|
||||||
result = await self.get_message(uid, "Response")
|
result = await self.get_message(uid)
|
||||||
if result.get("data", {}).get("choice", False):
|
if result.get("choice", False):
|
||||||
break
|
break
|
||||||
|
|
||||||
# 登录成功, 录入人工排查情况
|
# 登录成功, 录入人工排查情况
|
||||||
if self.run_book["SignIn"]:
|
if self.run_book["SignIn"]:
|
||||||
|
|
||||||
uid = str(uuid.uuid4())
|
uid = str(uuid.uuid4())
|
||||||
await Config.send_json(
|
await Config.send_json(
|
||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
@@ -865,12 +833,11 @@ class MaaManager:
|
|||||||
"type": "Question",
|
"type": "Question",
|
||||||
"title": "操作提示",
|
"title": "操作提示",
|
||||||
"message": "请检查用户代理情况, 该用户是否正确完成代理任务?",
|
"message": "请检查用户代理情况, 该用户是否正确完成代理任务?",
|
||||||
"options": ["是", "否"],
|
|
||||||
},
|
},
|
||||||
).model_dump()
|
).model_dump()
|
||||||
)
|
)
|
||||||
result = await self.get_message(uid, "Response")
|
result = await self.get_message(uid)
|
||||||
if result.get("data", {}).get("choice", False):
|
if result.get("choice", False):
|
||||||
self.run_book["PassCheck"] = True
|
self.run_book["PassCheck"] = True
|
||||||
|
|
||||||
await self.result_record()
|
await self.result_record()
|
||||||
@@ -885,6 +852,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 设置MAA模式
|
# 设置MAA模式
|
||||||
elif self.mode == "设置脚本":
|
elif self.mode == "设置脚本":
|
||||||
|
|
||||||
# 配置MAA
|
# 配置MAA
|
||||||
await self.set_maa(self.mode)
|
await self.set_maa(self.mode)
|
||||||
# 创建MAA任务
|
# 创建MAA任务
|
||||||
@@ -911,21 +879,11 @@ class MaaManager:
|
|||||||
if (self.run_book["Annihilation"] and self.run_book["Routine"])
|
if (self.run_book["Annihilation"] and self.run_book["Routine"])
|
||||||
else "代理任务未全部完成"
|
else "代理任务未全部完成"
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
await self.push_notification(
|
await self.push_notification(
|
||||||
"统计信息",
|
"统计信息",
|
||||||
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
|
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
|
||||||
statistics,
|
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.run_book["Annihilation"] and self.run_book["Routine"]:
|
if self.run_book["Annihilation"] and self.run_book["Routine"]:
|
||||||
# 成功完成代理的用户修改相关参数
|
# 成功完成代理的用户修改相关参数
|
||||||
@@ -967,6 +925,7 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif self.mode == "人工排查":
|
elif self.mode == "人工排查":
|
||||||
|
|
||||||
if self.run_book["SignIn"] and self.run_book["PassCheck"]:
|
if self.run_book["SignIn"] and self.run_book["PassCheck"]:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"用户 {self.user_list[self.index]['user_id']} 通过人工排查"
|
f"用户 {self.user_list[self.index]['user_id']} 通过人工排查"
|
||||||
@@ -1002,6 +961,7 @@ class MaaManager:
|
|||||||
return self.check_result
|
return self.check_result
|
||||||
|
|
||||||
if self.mode == "人工排查":
|
if self.mode == "人工排查":
|
||||||
|
|
||||||
# 解除静默操作屏蔽
|
# 解除静默操作屏蔽
|
||||||
logger.info("人工排查任务结束, 解除静默操作屏蔽")
|
logger.info("人工排查任务结束, 解除静默操作屏蔽")
|
||||||
if self.script_id in Config.if_ignore_silence:
|
if self.script_id in Config.if_ignore_silence:
|
||||||
@@ -1012,7 +972,9 @@ class MaaManager:
|
|||||||
and hasattr(self, "index")
|
and hasattr(self, "index")
|
||||||
and self.user_list[self.index]["status"] == "运行"
|
and self.user_list[self.index]["status"] == "运行"
|
||||||
):
|
):
|
||||||
|
|
||||||
if not self.maa_update_package:
|
if not self.maa_update_package:
|
||||||
|
|
||||||
self.maa_result = "用户手动中止任务"
|
self.maa_result = "用户手动中止任务"
|
||||||
|
|
||||||
# 保存运行日志以及统计信息
|
# 保存运行日志以及统计信息
|
||||||
@@ -1027,7 +989,6 @@ class MaaManager:
|
|||||||
/ f"history/{self.curdate}/{self.user_list[self.index]['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json"
|
/ f"history/{self.curdate}/{self.user_list[self.index]['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json"
|
||||||
)
|
)
|
||||||
if if_six_star:
|
if if_six_star:
|
||||||
try:
|
|
||||||
await self.push_notification(
|
await self.push_notification(
|
||||||
"公招六星",
|
"公招六星",
|
||||||
f"喜报: 用户 {self.user_list[self.index]['name']} 公招出六星啦!",
|
f"喜报: 用户 {self.user_list[self.index]['name']} 公招出六星啦!",
|
||||||
@@ -1035,15 +996,6 @@ class MaaManager:
|
|||||||
"user_name": self.user_list[self.index]["name"],
|
"user_name": self.user_list[self.index]["name"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
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.result_record()
|
await self.result_record()
|
||||||
|
|
||||||
@@ -1052,14 +1004,16 @@ class MaaManager:
|
|||||||
and hasattr(self, "index")
|
and hasattr(self, "index")
|
||||||
and self.user_list[self.index]["status"] == "运行"
|
and self.user_list[self.index]["status"] == "运行"
|
||||||
):
|
):
|
||||||
|
|
||||||
await self.result_record()
|
await self.result_record()
|
||||||
|
|
||||||
# 导出结果
|
# 导出结果
|
||||||
if self.mode in ["自动代理", "人工排查"]:
|
if self.mode in ["自动代理", "人工排查"]:
|
||||||
|
|
||||||
# 更新用户数据
|
# 更新用户数据
|
||||||
await Config.ScriptConfig[self.script_id].UserData.load(
|
sc = Config.ScriptConfig[self.script_id]
|
||||||
await self.user_config.toDict()
|
if isinstance(sc, MaaConfig):
|
||||||
)
|
await sc.UserData.load(await self.user_config.toDict())
|
||||||
await Config.ScriptConfig.save()
|
await Config.ScriptConfig.save()
|
||||||
|
|
||||||
error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
|
error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
|
||||||
@@ -1106,17 +1060,7 @@ class MaaManager:
|
|||||||
f"已完成用户数: {len(over_user)}, 未完成用户数: {len(error_user) + len(wait_user)}",
|
f"已完成用户数: {len(over_user)}, 未完成用户数: {len(error_user) + len(wait_user)}",
|
||||||
10,
|
10,
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
await self.push_notification("代理结果", title, result)
|
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.mode == "设置脚本":
|
elif self.mode == "设置脚本":
|
||||||
(
|
(
|
||||||
@@ -1144,14 +1088,14 @@ class MaaManager:
|
|||||||
self.agree_bilibili(False)
|
self.agree_bilibili(False)
|
||||||
return result_text
|
return result_text
|
||||||
|
|
||||||
async def get_message(self, message_id: str, message_type: str):
|
async def get_message(self, message_id: str):
|
||||||
"""获取客户端回应消息"""
|
"""获取客户端回应消息"""
|
||||||
|
|
||||||
logger.info(f"等待客户端回应消息: {message_id}")
|
logger.info(f"等待客户端回应消息: {message_id}")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
message = await self.message_queue.get()
|
message = await self.message_queue.get()
|
||||||
if message.get("id") == message_id and message.get("type") == message_type:
|
if message.get("message_id") == message_id:
|
||||||
self.message_queue.task_done()
|
self.message_queue.task_done()
|
||||||
logger.success(f"收到客户端回应消息: {message_id}")
|
logger.success(f"收到客户端回应消息: {message_id}")
|
||||||
return message
|
return message
|
||||||
@@ -1186,6 +1130,7 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for port in self.port_range:
|
for port in self.port_range:
|
||||||
|
|
||||||
ADB_address = f"{ADB_ip}{ADB_port + port}"
|
ADB_address = f"{ADB_ip}{ADB_port + port}"
|
||||||
|
|
||||||
# 尝试通过ADB连接到指定地址
|
# 尝试通过ADB连接到指定地址
|
||||||
@@ -1199,6 +1144,7 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "connected" in connect_result.stdout:
|
if "connected" in connect_result.stdout:
|
||||||
|
|
||||||
# 检查连接状态
|
# 检查连接状态
|
||||||
devices_result = subprocess.run(
|
devices_result = subprocess.run(
|
||||||
[self.ADB_path, "devices"],
|
[self.ADB_path, "devices"],
|
||||||
@@ -1209,6 +1155,7 @@ class MaaManager:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
if ADB_address in devices_result.stdout:
|
if ADB_address in devices_result.stdout:
|
||||||
|
|
||||||
logger.info(f"ADB实际地址: {ADB_address}")
|
logger.info(f"ADB实际地址: {ADB_address}")
|
||||||
|
|
||||||
# 断开连接
|
# 断开连接
|
||||||
@@ -1255,6 +1202,7 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.mode == "自动代理":
|
if self.mode == "自动代理":
|
||||||
|
|
||||||
# 获取最近一条日志的时间
|
# 获取最近一条日志的时间
|
||||||
latest_time = self.log_start_time
|
latest_time = self.log_start_time
|
||||||
for _ in self.maa_logs[::-1]:
|
for _ in self.maa_logs[::-1]:
|
||||||
@@ -1277,6 +1225,7 @@ class MaaManager:
|
|||||||
self.maa_result = "MAA未能正确登录PRTS"
|
self.maa_result = "MAA未能正确登录PRTS"
|
||||||
|
|
||||||
elif "任务已全部完成!" in log:
|
elif "任务已全部完成!" in log:
|
||||||
|
|
||||||
if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log:
|
if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log:
|
||||||
self.task_dict["WakeUp"] = "False"
|
self.task_dict["WakeUp"] = "False"
|
||||||
if "完成任务: Recruit" in log or "完成任务: 自动公招" in log:
|
if "完成任务: Recruit" in log or "完成任务: 自动公招" in log:
|
||||||
@@ -1357,6 +1306,7 @@ class MaaManager:
|
|||||||
logger.info(f"开始配置MAA运行参数: {mode}")
|
logger.info(f"开始配置MAA运行参数: {mode}")
|
||||||
|
|
||||||
if self.mode != "设置脚本" and mode != "Update":
|
if self.mode != "设置脚本" and mode != "Update":
|
||||||
|
|
||||||
if self.cur_user_data.get("Info", "Server") == "Bilibili":
|
if self.cur_user_data.get("Info", "Server") == "Bilibili":
|
||||||
self.agree_bilibili(True)
|
self.agree_bilibili(True)
|
||||||
else:
|
else:
|
||||||
@@ -1427,6 +1377,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 自动代理配置
|
# 自动代理配置
|
||||||
if self.mode == "自动代理" and mode in ["Annihilation", "Routine"]:
|
if self.mode == "自动代理" and mode in ["Annihilation", "Routine"]:
|
||||||
|
|
||||||
if (self.index == len(self.user_list) - 1) or (
|
if (self.index == len(self.user_list) - 1) or (
|
||||||
self.user_config[
|
self.user_config[
|
||||||
uuid.UUID(self.user_list[self.index + 1]["user_id"])
|
uuid.UUID(self.user_list[self.index + 1]["user_id"])
|
||||||
@@ -1437,6 +1388,7 @@ class MaaManager:
|
|||||||
"MainFunction.PostActions"
|
"MainFunction.PostActions"
|
||||||
] = "12" # 完成后退出MAA和模拟器
|
] = "12" # 完成后退出MAA和模拟器
|
||||||
else:
|
else:
|
||||||
|
|
||||||
data["Configurations"]["Default"]["MainFunction.PostActions"] = (
|
data["Configurations"]["Default"]["MainFunction.PostActions"] = (
|
||||||
METHOD_BOOK[self.script_config.get("Run", "TaskTransitionMethod")]
|
METHOD_BOOK[self.script_config.get("Run", "TaskTransitionMethod")]
|
||||||
) # 完成后行为
|
) # 完成后行为
|
||||||
@@ -1520,29 +1472,16 @@ class MaaManager:
|
|||||||
data["Configurations"]["Default"]["TaskQueue.Order.AutoRoguelike"] = "6"
|
data["Configurations"]["Default"]["TaskQueue.Order.AutoRoguelike"] = "6"
|
||||||
data["Configurations"]["Default"]["TaskQueue.Order.Reclamation"] = "7"
|
data["Configurations"]["Default"]["TaskQueue.Order.Reclamation"] = "7"
|
||||||
|
|
||||||
if self.cur_user_data.get("Info", "StageMode") == "Fixed":
|
if isinstance(self.cur_user_data, MaaUserConfig):
|
||||||
plan_data = {
|
try:
|
||||||
"MedicineNumb": self.cur_user_data.get("Info", "MedicineNumb"),
|
plan_data = self.cur_user_data.get_plan_info()
|
||||||
"SeriesNumb": self.cur_user_data.get("Info", "SeriesNumb"),
|
except Exception as e:
|
||||||
"Stage": self.cur_user_data.get("Info", "Stage"),
|
logger.error(
|
||||||
"Stage_1": self.cur_user_data.get("Info", "Stage_1"),
|
f"获取用户 {self.user_list[self.index]['user_id']} 的代理计划信息失败: {e}"
|
||||||
"Stage_2": self.cur_user_data.get("Info", "Stage_2"),
|
)
|
||||||
"Stage_3": self.cur_user_data.get("Info", "Stage_3"),
|
plan_data = {}
|
||||||
"Stage_Remain": self.cur_user_data.get("Info", "Stage_Remain"),
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
plan = Config.PlanConfig[
|
plan_data = {}
|
||||||
uuid.UUID(self.cur_user_data.get("Info", "StageMode"))
|
|
||||||
]
|
|
||||||
plan_data = {
|
|
||||||
"MedicineNumb": plan.get_current_info("MedicineNumb").getValue(),
|
|
||||||
"SeriesNumb": plan.get_current_info("SeriesNumb").getValue(),
|
|
||||||
"Stage": plan.get_current_info("Ssstage").getValue(),
|
|
||||||
"Stage_1": plan.get_current_info("Stage_1").getValue(),
|
|
||||||
"Stage_2": plan.get_current_info("Stage_2").getValue(),
|
|
||||||
"Stage_3": plan.get_current_info("Stage_3").getValue(),
|
|
||||||
"Stage_Remain": plan.get_current_info("Stage_Remain").getValue(),
|
|
||||||
}
|
|
||||||
|
|
||||||
data["Configurations"]["Default"]["MainFunction.UseMedicine"] = (
|
data["Configurations"]["Default"]["MainFunction.UseMedicine"] = (
|
||||||
"False" if plan_data.get("MedicineNumb", 0) == 0 else "True"
|
"False" if plan_data.get("MedicineNumb", 0) == 0 else "True"
|
||||||
@@ -1555,6 +1494,7 @@ class MaaManager:
|
|||||||
) # 连战次数
|
) # 连战次数
|
||||||
|
|
||||||
if mode == "Annihilation":
|
if mode == "Annihilation":
|
||||||
|
|
||||||
data["Configurations"]["Default"][
|
data["Configurations"]["Default"][
|
||||||
"MainFunction.Stage1"
|
"MainFunction.Stage1"
|
||||||
] = "Annihilation" # 主关卡
|
] = "Annihilation" # 主关卡
|
||||||
@@ -1596,6 +1536,7 @@ class MaaManager:
|
|||||||
] = "False" # 隐藏连战次数
|
] = "False" # 隐藏连战次数
|
||||||
|
|
||||||
elif mode == "Routine":
|
elif mode == "Routine":
|
||||||
|
|
||||||
data["Configurations"]["Default"]["MainFunction.Stage1"] = (
|
data["Configurations"]["Default"]["MainFunction.Stage1"] = (
|
||||||
plan_data.get("Stage") if plan_data.get("Stage", "-") != "-" else ""
|
plan_data.get("Stage") if plan_data.get("Stage", "-") != "-" else ""
|
||||||
) # 主关卡
|
) # 主关卡
|
||||||
@@ -1627,6 +1568,7 @@ class MaaManager:
|
|||||||
) # 使用剩余理智
|
) # 使用剩余理智
|
||||||
|
|
||||||
if self.cur_user_data.get("Info", "Mode") == "简洁":
|
if self.cur_user_data.get("Info", "Mode") == "简洁":
|
||||||
|
|
||||||
data["Configurations"]["Default"][
|
data["Configurations"]["Default"][
|
||||||
"Penguin.IsDrGrandet"
|
"Penguin.IsDrGrandet"
|
||||||
] = "False" # 博朗台模式
|
] = "False" # 博朗台模式
|
||||||
@@ -1638,10 +1580,12 @@ class MaaManager:
|
|||||||
] = "True" # 无限吃48小时内过期的理智药
|
] = "True" # 无限吃48小时内过期的理智药
|
||||||
# 自定义基建配置
|
# 自定义基建配置
|
||||||
if self.cur_user_data.get("Info", "InfrastMode") == "Custom":
|
if self.cur_user_data.get("Info", "InfrastMode") == "Custom":
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Path.cwd()
|
Path.cwd()
|
||||||
/ f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/Infrastructure/infrastructure.json"
|
/ f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/Infrastructure/infrastructure.json"
|
||||||
).exists():
|
).exists():
|
||||||
|
|
||||||
data["Configurations"]["Default"][
|
data["Configurations"]["Default"][
|
||||||
"Infrast.InfrastMode"
|
"Infrast.InfrastMode"
|
||||||
] = "Custom" # 基建模式
|
] = "Custom" # 基建模式
|
||||||
@@ -1684,6 +1628,7 @@ class MaaManager:
|
|||||||
) # 基建模式
|
) # 基建模式
|
||||||
|
|
||||||
elif self.cur_user_data.get("Info", "Mode") == "详细":
|
elif self.cur_user_data.get("Info", "Mode") == "详细":
|
||||||
|
|
||||||
# 基建模式
|
# 基建模式
|
||||||
if (
|
if (
|
||||||
data["Configurations"]["Default"]["Infrast.InfrastMode"]
|
data["Configurations"]["Default"]["Infrast.InfrastMode"]
|
||||||
@@ -1697,6 +1642,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 人工排查配置
|
# 人工排查配置
|
||||||
elif self.mode == "人工排查" and self.cur_user_data is not None:
|
elif self.mode == "人工排查" and self.cur_user_data is not None:
|
||||||
|
|
||||||
data["Configurations"]["Default"][
|
data["Configurations"]["Default"][
|
||||||
"MainFunction.PostActions"
|
"MainFunction.PostActions"
|
||||||
] = "8" # 完成后退出MAA
|
] = "8" # 完成后退出MAA
|
||||||
@@ -1763,6 +1709,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 设置脚本配置
|
# 设置脚本配置
|
||||||
elif self.mode == "设置脚本":
|
elif self.mode == "设置脚本":
|
||||||
|
|
||||||
data["Configurations"]["Default"][
|
data["Configurations"]["Default"][
|
||||||
"MainFunction.PostActions"
|
"MainFunction.PostActions"
|
||||||
] = "0" # 完成后无动作
|
] = "0" # 完成后无动作
|
||||||
@@ -1813,6 +1760,7 @@ class MaaManager:
|
|||||||
] = "False" # 生息演算
|
] = "False" # 生息演算
|
||||||
|
|
||||||
elif mode == "Update":
|
elif mode == "Update":
|
||||||
|
|
||||||
data["Configurations"]["Default"][
|
data["Configurations"]["Default"][
|
||||||
"MainFunction.PostActions"
|
"MainFunction.PostActions"
|
||||||
] = "0" # 完成后无动作
|
] = "0" # 完成后无动作
|
||||||
@@ -1951,12 +1899,24 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 发送自定义Webhook通知
|
# 发送自定义Webhook通知
|
||||||
for webhook in Config.Notify_CustomWebhooks.values():
|
try:
|
||||||
await Notify.WebhookPush(
|
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
|
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
elif mode == "统计信息":
|
elif mode == "统计信息":
|
||||||
|
|
||||||
# 生成文本通知内容
|
# 生成文本通知内容
|
||||||
formatted = []
|
formatted = []
|
||||||
if "drop_statistics" in message:
|
if "drop_statistics" in message:
|
||||||
@@ -1991,6 +1951,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 发送全局通知
|
# 发送全局通知
|
||||||
if Config.get("Notify", "IfSendStatistic"):
|
if Config.get("Notify", "IfSendStatistic"):
|
||||||
|
|
||||||
if Config.get("Notify", "IfSendMail"):
|
if Config.get("Notify", "IfSendMail"):
|
||||||
await Notify.send_mail(
|
await Notify.send_mail(
|
||||||
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
||||||
@@ -2004,39 +1965,78 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 发送自定义Webhook通知
|
# 发送自定义Webhook通知
|
||||||
for webhook in Config.Notify_CustomWebhooks.values():
|
try:
|
||||||
await Notify.WebhookPush(
|
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
|
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(
|
if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
|
||||||
"Notify", "IfSendStatistic"
|
"Notify", "IfSendStatistic"
|
||||||
):
|
):
|
||||||
|
|
||||||
# 发送邮件通知
|
# 发送邮件通知
|
||||||
if self.cur_user_data.get("Notify", "IfSendMail"):
|
if self.cur_user_data.get("Notify", "IfSendMail"):
|
||||||
|
if self.cur_user_data.get("Notify", "ToAddress"):
|
||||||
await Notify.send_mail(
|
await Notify.send_mail(
|
||||||
"网页",
|
"网页",
|
||||||
title,
|
title,
|
||||||
message_html,
|
message_html,
|
||||||
self.cur_user_data.get("Notify", "ToAddress"),
|
self.cur_user_data.get("Notify", "ToAddress"),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"用户邮箱地址为空, 无法发送用户单独的邮件通知")
|
||||||
|
|
||||||
# 发送ServerChan通知
|
# 发送ServerChan通知
|
||||||
if self.cur_user_data.get("Notify", "IfServerChan"):
|
if self.cur_user_data.get("Notify", "IfServerChan"):
|
||||||
|
if self.cur_user_data.get("Notify", "ServerChanKey"):
|
||||||
await Notify.ServerChanPush(
|
await Notify.ServerChanPush(
|
||||||
title,
|
title,
|
||||||
f"{serverchan_message}\n\nAUTO-MAS 敬上",
|
f"{serverchan_message}\n\nAUTO-MAS 敬上",
|
||||||
self.cur_user_data.get("Notify", "ServerChanKey"),
|
self.cur_user_data.get("Notify", "ServerChanKey"),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知"
|
||||||
|
)
|
||||||
|
|
||||||
# 推送CompanyWebHook通知
|
# 推送CompanyWebHookBot通知
|
||||||
for webhook in self.cur_user_data.Notify_CustomWebhooks.values():
|
# 发送用户自定义Webhook通知
|
||||||
await Notify.WebhookPush(
|
try:
|
||||||
|
user_webhooks = self.cur_user_data.get("Notify", "CustomWebhooks")
|
||||||
|
except AttributeError:
|
||||||
|
user_webhooks = []
|
||||||
|
if not user_webhooks:
|
||||||
|
user_webhooks = []
|
||||||
|
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
|
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通知"
|
||||||
|
)
|
||||||
|
|
||||||
elif mode == "公招六星":
|
elif mode == "公招六星":
|
||||||
|
|
||||||
# 生成HTML通知内容
|
# 生成HTML通知内容
|
||||||
template = env.get_template("MAA_six_star.html")
|
template = env.get_template("MAA_six_star.html")
|
||||||
|
|
||||||
@@ -2044,6 +2044,7 @@ class MaaManager:
|
|||||||
|
|
||||||
# 发送全局通知
|
# 发送全局通知
|
||||||
if Config.get("Notify", "IfSendSixStar"):
|
if Config.get("Notify", "IfSendSixStar"):
|
||||||
|
|
||||||
if Config.get("Notify", "IfSendMail"):
|
if Config.get("Notify", "IfSendMail"):
|
||||||
await Notify.send_mail(
|
await Notify.send_mail(
|
||||||
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
||||||
@@ -2057,30 +2058,75 @@ class MaaManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 发送自定义Webhook通知(六星喜报)
|
# 发送自定义Webhook通知(六星喜报)
|
||||||
for webhook in Config.Notify_CustomWebhooks.values():
|
try:
|
||||||
await Notify.WebhookPush(title, "好羡慕~\n\nAUTO-MAS 敬上", webhook)
|
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, "好羡慕~\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(
|
if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
|
||||||
"Notify", "IfSendSixStar"
|
"Notify", "IfSendSixStar"
|
||||||
):
|
):
|
||||||
|
|
||||||
# 发送邮件通知
|
# 发送邮件通知
|
||||||
if self.cur_user_data.get("Notify", "IfSendMail"):
|
if self.cur_user_data.get("Notify", "IfSendMail"):
|
||||||
|
if self.cur_user_data.get("Notify", "ToAddress"):
|
||||||
await Notify.send_mail(
|
await Notify.send_mail(
|
||||||
"网页",
|
"网页",
|
||||||
title,
|
title,
|
||||||
message_html,
|
message_html,
|
||||||
self.cur_user_data.get("Notify", "ToAddress"),
|
self.cur_user_data.get("Notify", "ToAddress"),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知")
|
||||||
|
|
||||||
# 发送ServerChan通知
|
# 发送ServerChan通知
|
||||||
if self.cur_user_data.get("Notify", "IfServerChan"):
|
if self.cur_user_data.get("Notify", "IfServerChan"):
|
||||||
|
|
||||||
|
if self.cur_user_data.get("Notify", "ServerChanKey"):
|
||||||
await Notify.ServerChanPush(
|
await Notify.ServerChanPush(
|
||||||
title,
|
title,
|
||||||
"好羡慕~\n\nAUTO-MAS 敬上",
|
"好羡慕~\n\nAUTO-MAS 敬上",
|
||||||
self.cur_user_data.get("Notify", "ServerChanKey"),
|
self.cur_user_data.get("Notify", "ServerChanKey"),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知"
|
||||||
|
)
|
||||||
|
|
||||||
# 推送CompanyWebHookBot通知
|
# 推送CompanyWebHookBot通知
|
||||||
for webhook in self.cur_user_data.Notify_CustomWebhooks.values():
|
# 发送用户自定义Webhook通知(六星喜报)
|
||||||
await Notify.WebhookPush(title, "好羡慕~\n\nAUTO-MAS 敬上", webhook)
|
try:
|
||||||
|
user_webhooks = self.cur_user_data.get("Notify", "CustomWebhooks")
|
||||||
|
except AttributeError:
|
||||||
|
user_webhooks = []
|
||||||
|
if not user_webhooks:
|
||||||
|
user_webhooks = []
|
||||||
|
if user_webhooks:
|
||||||
|
for webhook in user_webhooks:
|
||||||
|
if webhook.get("enabled", True):
|
||||||
|
try:
|
||||||
|
await Notify.CustomWebhookPush(
|
||||||
|
title, "好羡慕~\n\nAUTO-MAS 敬上", webhook
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"用户自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"用户CompanyWebHookBot密钥为空, 无法发送用户单独的CompanyWebHookBot通知"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ import shutil
|
|||||||
import asyncio
|
import asyncio
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from fastapi import WebSocket
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from jinja2 import Environment, FileSystemLoader
|
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
|
from app.core import Config, GeneralConfig, GeneralUserConfig
|
||||||
@@ -43,7 +44,7 @@ logger = get_logger("通用调度器")
|
|||||||
|
|
||||||
|
|
||||||
class GeneralManager:
|
class GeneralManager:
|
||||||
"""通用脚本调度器"""
|
"""通用脚本通用控制器"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, mode: str, script_id: uuid.UUID, user_id: Optional[uuid.UUID], ws_id: str
|
self, mode: str, script_id: uuid.UUID, user_id: Optional[uuid.UUID], ws_id: str
|
||||||
@@ -68,6 +69,7 @@ class GeneralManager:
|
|||||||
await Config.ScriptConfig[self.script_id].lock()
|
await Config.ScriptConfig[self.script_id].lock()
|
||||||
|
|
||||||
self.script_config = Config.ScriptConfig[self.script_id]
|
self.script_config = Config.ScriptConfig[self.script_id]
|
||||||
|
if isinstance(self.script_config, GeneralConfig):
|
||||||
self.user_config = MultipleConfig([GeneralUserConfig])
|
self.user_config = MultipleConfig([GeneralUserConfig])
|
||||||
await self.user_config.load(await self.script_config.UserData.toDict())
|
await self.user_config.load(await self.script_config.UserData.toDict())
|
||||||
|
|
||||||
@@ -141,9 +143,6 @@ class GeneralManager:
|
|||||||
def check_config(self) -> str:
|
def check_config(self) -> str:
|
||||||
"""检查配置是否可用"""
|
"""检查配置是否可用"""
|
||||||
|
|
||||||
if not isinstance(Config.ScriptConfig[self.script_id], GeneralConfig):
|
|
||||||
return "脚本配置类型错误, 不是通用脚本类型"
|
|
||||||
|
|
||||||
if self.mode == "人工排查":
|
if self.mode == "人工排查":
|
||||||
return "通用脚本不支持人工排查模式"
|
return "通用脚本不支持人工排查模式"
|
||||||
if self.mode == "设置脚本" and self.user_id is None:
|
if self.mode == "设置脚本" and self.user_id is None:
|
||||||
@@ -222,8 +221,6 @@ class GeneralManager:
|
|||||||
# 开始代理
|
# 开始代理
|
||||||
for self.index, user in enumerate(self.user_list):
|
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 (
|
if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
|
||||||
@@ -256,8 +253,7 @@ class GeneralManager:
|
|||||||
self.run_book = False
|
self.run_book = False
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
Path.cwd()
|
Path.cwd() / f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
||||||
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
|
|
||||||
).exists():
|
).exists():
|
||||||
|
|
||||||
logger.error(f"用户: {user['user_id']} - 未找到配置文件")
|
logger.error(f"用户: {user['user_id']} - 未找到配置文件")
|
||||||
@@ -294,9 +290,7 @@ class GeneralManager:
|
|||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
await self.execute_script_task(
|
await self.execute_script_task(
|
||||||
Path(
|
Path(self.cur_user_data.get("Info", "ScriptBeforeTask")),
|
||||||
self.cur_user_data.get("Info", "ScriptBeforeTask")
|
|
||||||
),
|
|
||||||
"脚本前任务",
|
"脚本前任务",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -309,9 +303,9 @@ class GeneralManager:
|
|||||||
)
|
)
|
||||||
await self.game_process_manager.open_process(
|
await self.game_process_manager.open_process(
|
||||||
self.game_path,
|
self.game_path,
|
||||||
str(
|
str(self.script_config.get("Game", "Arguments")).split(
|
||||||
self.script_config.get("Game", "Arguments")
|
" "
|
||||||
).split(" "),
|
),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -320,9 +314,7 @@ class GeneralManager:
|
|||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
id=self.ws_id,
|
id=self.ws_id,
|
||||||
type="Info",
|
type="Info",
|
||||||
data={
|
data={"Error": f"启动游戏/模拟器时出现异常: {e}"},
|
||||||
"Error": f"启动游戏/模拟器时出现异常: {e}"
|
|
||||||
},
|
|
||||||
).model_dump()
|
).model_dump()
|
||||||
)
|
)
|
||||||
self.general_result = "游戏/模拟器启动失败"
|
self.general_result = "游戏/模拟器启动失败"
|
||||||
@@ -336,8 +328,7 @@ class GeneralManager:
|
|||||||
Config.silence_dict[
|
Config.silence_dict[
|
||||||
self.game_path
|
self.game_path
|
||||||
] = datetime.now() + timedelta(
|
] = datetime.now() + timedelta(
|
||||||
seconds=self.script_config.get("Game", "WaitTime")
|
seconds=self.script_config.get("Game", "WaitTime") + 10
|
||||||
+ 10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await Config.send_json(
|
await Config.send_json(
|
||||||
@@ -349,9 +340,7 @@ class GeneralManager:
|
|||||||
},
|
},
|
||||||
).model_dump()
|
).model_dump()
|
||||||
)
|
)
|
||||||
await asyncio.sleep(
|
await asyncio.sleep(self.script_config.get("Game", "WaitTime"))
|
||||||
self.script_config.get("Game", "WaitTime")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 运行脚本任务
|
# 运行脚本任务
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -525,17 +514,6 @@ class GeneralManager:
|
|||||||
|
|
||||||
await self.result_record()
|
await self.result_record()
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"用户 {user['user_id']} 代理时出现异常: {e}")
|
|
||||||
user["status"] = "异常"
|
|
||||||
await Config.send_json(
|
|
||||||
WebSocketMessage(
|
|
||||||
id=self.ws_id,
|
|
||||||
type="Info",
|
|
||||||
data={"Error": f"代理用户 {user['name']} 时出现异常: {e}"},
|
|
||||||
).model_dump()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置通用脚本模式
|
# 设置通用脚本模式
|
||||||
elif self.mode == "设置脚本":
|
elif self.mode == "设置脚本":
|
||||||
|
|
||||||
@@ -567,21 +545,11 @@ class GeneralManager:
|
|||||||
"end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
"end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"user_result": "代理成功" if self.run_book else self.general_result,
|
"user_result": "代理成功" if self.run_book else self.general_result,
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
await self.push_notification(
|
await self.push_notification(
|
||||||
"统计信息",
|
"统计信息",
|
||||||
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
|
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
|
||||||
statistics,
|
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.run_book:
|
if self.run_book:
|
||||||
# 成功完成代理的用户修改相关参数
|
# 成功完成代理的用户修改相关参数
|
||||||
@@ -691,9 +659,9 @@ class GeneralManager:
|
|||||||
if self.mode == "自动代理":
|
if self.mode == "自动代理":
|
||||||
|
|
||||||
# 更新用户数据
|
# 更新用户数据
|
||||||
await Config.ScriptConfig[self.script_id].UserData.load(
|
sc = Config.ScriptConfig[self.script_id]
|
||||||
await self.user_config.toDict()
|
if isinstance(sc, GeneralConfig):
|
||||||
)
|
await sc.UserData.load(await self.user_config.toDict())
|
||||||
await Config.ScriptConfig.save()
|
await Config.ScriptConfig.save()
|
||||||
|
|
||||||
error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
|
error_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)}",
|
f"已完成配置数: {len(over_user)}, 未完成配置数: {len(error_user) + len(wait_user)}",
|
||||||
10,
|
10,
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
await self.push_notification("代理结果", title, result)
|
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.mode == "设置脚本":
|
elif self.mode == "设置脚本":
|
||||||
|
|
||||||
@@ -1012,6 +970,7 @@ class GeneralManager:
|
|||||||
serverchan_message = message_text.replace("\n", "\n\n")
|
serverchan_message = message_text.replace("\n", "\n\n")
|
||||||
|
|
||||||
# 发送全局通知
|
# 发送全局通知
|
||||||
|
|
||||||
if Config.get("Notify", "IfSendMail"):
|
if Config.get("Notify", "IfSendMail"):
|
||||||
await Notify.send_mail(
|
await Notify.send_mail(
|
||||||
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
"网页", title, message_html, Config.get("Notify", "ToAddress")
|
||||||
@@ -1025,10 +984,21 @@ class GeneralManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 发送自定义Webhook通知
|
# 发送自定义Webhook通知
|
||||||
for webhook in Config.Notify_CustomWebhooks.values():
|
try:
|
||||||
await Notify.WebhookPush(
|
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
|
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
elif mode == "统计信息":
|
elif mode == "统计信息":
|
||||||
|
|
||||||
@@ -1061,10 +1031,21 @@ class GeneralManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 发送自定义Webhook通知
|
# 发送自定义Webhook通知
|
||||||
for webhook in Config.Notify_CustomWebhooks.values():
|
try:
|
||||||
await Notify.WebhookPush(
|
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
|
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(
|
if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
|
||||||
@@ -1097,7 +1078,22 @@ class GeneralManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 推送CompanyWebHookBot通知
|
# 推送CompanyWebHookBot通知
|
||||||
for webhook in self.cur_user_data.Notify_CustomWebhooks.values():
|
# 发送用户自定义Webhook通知
|
||||||
await Notify.WebhookPush(
|
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
|
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
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
|
|
||||||
"""配置类型映射表"""
|
|
||||||
|
|
||||||
RESOURCE_STAGE_INFO = [
|
RESOURCE_STAGE_INFO = [
|
||||||
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
|
{"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": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},
|
||||||
|
|||||||
@@ -1,29 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
dialog,
|
|
||||||
ipcMain,
|
ipcMain,
|
||||||
Menu,
|
dialog,
|
||||||
nativeImage,
|
|
||||||
nativeTheme,
|
|
||||||
screen,
|
|
||||||
shell,
|
shell,
|
||||||
Tray,
|
Tray,
|
||||||
|
Menu,
|
||||||
|
nativeImage,
|
||||||
|
screen,
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { exec, spawn } from 'child_process'
|
import { spawn, exec } from 'child_process'
|
||||||
import { checkEnvironment, getAppRoot } from './services/environmentService'
|
import { getAppRoot, checkEnvironment } 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'
|
|
||||||
|
|
||||||
// 强制清理相关进程的函数
|
// 强制清理相关进程的函数
|
||||||
async function forceKillRelatedProcesses(): Promise<void> {
|
async function forceKillRelatedProcesses(): Promise<void> {
|
||||||
@@ -39,9 +28,9 @@ async function forceKillRelatedProcesses(): Promise<void> {
|
|||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
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) {
|
if (error) {
|
||||||
log.warn('备用清理方法失败:', error.message)
|
log.warn('备用清理方法失败:', error.message)
|
||||||
} else {
|
} 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 {
|
function isRunningAsAdmin(): boolean {
|
||||||
@@ -114,7 +113,6 @@ interface AppConfig {
|
|||||||
IfMinimizeDirectly: boolean
|
IfMinimizeDirectly: boolean
|
||||||
IfSelfStart: boolean
|
IfSelfStart: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,9 +304,7 @@ function updateTrayVisibility(config: AppConfig) {
|
|||||||
log.info('托盘图标已销毁')
|
log.info('托盘图标已销毁')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: Electron.BrowserWindow | null = null
|
let mainWindow: Electron.BrowserWindow | null = null
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
log.info('开始创建主窗口')
|
log.info('开始创建主窗口')
|
||||||
|
|
||||||
@@ -750,232 +746,6 @@ ipcMain.handle('stop-backend', async () => {
|
|||||||
return stopBackend()
|
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相关
|
// Git相关
|
||||||
ipcMain.handle('download-git', async () => {
|
ipcMain.handle('download-git', async () => {
|
||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
@@ -1048,6 +818,7 @@ ipcMain.handle('check-git-update', async () => {
|
|||||||
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
||||||
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
||||||
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('检查Git更新失败:', error)
|
log.error('检查Git更新失败:', error)
|
||||||
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
||||||
@@ -1321,13 +1092,13 @@ app.on('before-quit', async event => {
|
|||||||
forceKillRelatedProcesses(),
|
forceKillRelatedProcesses(),
|
||||||
|
|
||||||
// 方法2: 直接使用 taskkill 命令
|
// 方法2: 直接使用 taskkill 命令
|
||||||
new Promise<void>(resolve => {
|
new Promise<void>((resolve) => {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
const commands = [
|
const commands = [
|
||||||
`taskkill /f /im python.exe`,
|
`taskkill /f /im python.exe`,
|
||||||
`wmic process where "CommandLine like '%main.py%'" delete`,
|
`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
|
let completed = 0
|
||||||
@@ -1345,7 +1116,7 @@ app.on('before-quit', async event => {
|
|||||||
} else {
|
} else {
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
// 最多等待3秒
|
// 最多等待3秒
|
||||||
|
|||||||
@@ -63,16 +63,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
||||||
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', 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) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { BrowserWindow, app } from 'electron'
|
import { BrowserWindow } from 'electron'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { downloadFile } from './downloadService'
|
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'
|
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) {
|
function copyDirSync(src: string, dest: string) {
|
||||||
if (!fs.existsSync(dest)) {
|
if (!fs.existsSync(dest)) {
|
||||||
@@ -302,226 +192,69 @@ export async function cloneBackend(
|
|||||||
success: boolean
|
success: boolean
|
||||||
error?: string
|
error?: string
|
||||||
}> {
|
}> {
|
||||||
console.log('=== 开始克隆/更新后端代码 ===')
|
|
||||||
console.log(`应用根目录: ${appRoot}`)
|
|
||||||
console.log(`仓库URL: ${repoUrl}`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backendPath = appRoot
|
const backendPath = appRoot
|
||||||
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
|
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
|
||||||
|
if (!fs.existsSync(gitPath)) throw new Error(`Git可执行文件不存在: ${gitPath}`)
|
||||||
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可执行文件存在')
|
|
||||||
const gitEnv = getGitEnvironment(appRoot)
|
const gitEnv = getGitEnvironment(appRoot)
|
||||||
console.log('✅ Git环境变量配置完成')
|
|
||||||
|
|
||||||
// 检查 git 是否可用
|
// 检查 git 是否可用
|
||||||
console.log('=== 检查Git是否可用 ===')
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const proc = spawn(gitPath, ['--version'], { env: gitEnv })
|
const proc = spawn(gitPath, ['--version'], { env: gitEnv })
|
||||||
|
proc.on('close', code => (code === 0 ? resolve() : reject(new Error('git 无法正常运行'))))
|
||||||
proc.stdout?.on('data', data => {
|
proc.on('error', reject)
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取版本号并确定目标分支
|
|
||||||
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) {
|
if (isGitRepository(backendPath)) {
|
||||||
console.log('=== 更新现有Git仓库 ===')
|
|
||||||
|
|
||||||
// 已是 git 仓库,先更新远程URL为镜像站,然后 pull
|
// 已是 git 仓库,先更新远程URL为镜像站,然后 pull
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('download-progress', {
|
mainWindow.webContents.send('download-progress', {
|
||||||
type: 'backend',
|
type: 'backend',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: 'downloading',
|
status: 'downloading',
|
||||||
message: `正在更新后端代码(分支: ${targetBranch})...`,
|
message: '正在更新后端代码...',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新远程URL为镜像站URL,避免直接访问GitHub
|
// 更新远程URL为镜像站URL,避免直接访问GitHub
|
||||||
console.log(`📡 更新远程URL为镜像站: ${repoUrl}`)
|
console.log(`更新远程URL为镜像站: ${repoUrl}`)
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
|
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
env: gitEnv,
|
env: gitEnv,
|
||||||
cwd: backendPath,
|
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取目标分支信息(显式 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)
|
|
||||||
})
|
})
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 执行pull操作
|
// 执行pull操作
|
||||||
console.log('🔄 强制同步到远程分支最新提交...')
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const proc = spawn(gitPath, ['reset', '--hard', `origin/${targetBranch}`], {
|
const proc = spawn(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath })
|
||||||
stdio: 'pipe',
|
proc.stdout?.on('data', d => console.log('git pull:', d.toString()))
|
||||||
env: gitEnv,
|
proc.stderr?.on('data', d => console.log('git pull err:', d.toString()))
|
||||||
cwd: backendPath,
|
proc.on('close', code =>
|
||||||
|
code === 0 ? resolve() : reject(new Error(`git pull失败,退出码: ${code}`))
|
||||||
|
)
|
||||||
|
proc.on('error', reject)
|
||||||
})
|
})
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('download-progress', {
|
mainWindow.webContents.send('download-progress', {
|
||||||
type: 'backend',
|
type: 'backend',
|
||||||
progress: 100,
|
progress: 100,
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
message: `后端代码更新完成(分支: ${targetBranch})`,
|
message: '后端代码更新完成',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ 后端代码更新完成(分支: ${targetBranch})`)
|
|
||||||
} else {
|
} else {
|
||||||
console.log('=== 克隆新的Git仓库 ===')
|
|
||||||
|
|
||||||
// 不是 git 仓库,clone 到 tmp,再拷贝出来
|
// 不是 git 仓库,clone 到 tmp,再拷贝出来
|
||||||
const tmpDir = path.join(appRoot, 'git_tmp')
|
const tmpDir = path.join(appRoot, 'git_tmp')
|
||||||
console.log(`临时目录: ${tmpDir}`)
|
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
|
|
||||||
if (fs.existsSync(tmpDir)) {
|
|
||||||
console.log('🗑️ 清理现有临时目录...')
|
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📁 创建临时目录...')
|
|
||||||
fs.mkdirSync(tmpDir, { recursive: true })
|
fs.mkdirSync(tmpDir, { recursive: true })
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
@@ -529,13 +262,9 @@ export async function cloneBackend(
|
|||||||
type: 'backend',
|
type: 'backend',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: 'downloading',
|
status: 'downloading',
|
||||||
message: `正在克隆后端代码(分支: ${targetBranch})...`,
|
message: '正在克隆后端代码...',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📥 开始克隆代码到临时目录...`)
|
|
||||||
console.log(`克隆参数: --single-branch --depth 1 --branch ${targetBranch}`)
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const proc = spawn(
|
const proc = spawn(
|
||||||
gitPath,
|
gitPath,
|
||||||
@@ -547,7 +276,7 @@ export async function cloneBackend(
|
|||||||
'--depth',
|
'--depth',
|
||||||
'1',
|
'1',
|
||||||
'--branch',
|
'--branch',
|
||||||
targetBranch,
|
'feature/refactor',
|
||||||
repoUrl,
|
repoUrl,
|
||||||
tmpDir,
|
tmpDir,
|
||||||
],
|
],
|
||||||
@@ -557,51 +286,26 @@ export async function cloneBackend(
|
|||||||
cwd: appRoot,
|
cwd: appRoot,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
proc.stdout?.on('data', d => console.log('git clone stdout:', d.toString().trim()))
|
proc.stdout?.on('data', d => console.log('git clone:', d.toString()))
|
||||||
proc.stderr?.on('data', d => console.log('git clone stderr:', d.toString().trim()))
|
proc.stderr?.on('data', d => console.log('git clone err:', d.toString()))
|
||||||
proc.on('close', code => {
|
proc.on('close', code =>
|
||||||
console.log(`git clone 退出码: ${code}`)
|
code === 0 ? resolve() : reject(new Error(`git clone失败,退出码: ${code}`))
|
||||||
if (code === 0) {
|
)
|
||||||
console.log('✅ 代码克隆成功')
|
proc.on('error', reject)
|
||||||
resolve()
|
|
||||||
} else {
|
|
||||||
console.error('❌ 代码克隆失败')
|
|
||||||
reject(new Error(`git clone失败,退出码: ${code}`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
proc.on('error', error => {
|
|
||||||
console.error('❌ git clone 进程错误:', error)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 复制所有文件到 backendPath(appRoot),包含 .git
|
// 复制所有文件到 backendPath(appRoot),包含 .git
|
||||||
console.log('📋 复制文件到目标目录...')
|
|
||||||
const tmpFiles = fs.readdirSync(tmpDir)
|
const tmpFiles = fs.readdirSync(tmpDir)
|
||||||
console.log(`临时目录中的文件: ${tmpFiles.join(', ')}`)
|
|
||||||
|
|
||||||
for (const file of tmpFiles) {
|
for (const file of tmpFiles) {
|
||||||
const src = path.join(tmpDir, file)
|
const src = path.join(tmpDir, file)
|
||||||
const dst = path.join(backendPath, file)
|
const dst = path.join(backendPath, file)
|
||||||
|
|
||||||
console.log(`复制: ${file}`)
|
|
||||||
|
|
||||||
if (fs.existsSync(dst)) {
|
if (fs.existsSync(dst)) {
|
||||||
console.log(` - 删除现有文件/目录: ${dst}`)
|
|
||||||
if (fs.statSync(dst).isDirectory()) fs.rmSync(dst, { recursive: true, force: true })
|
if (fs.statSync(dst).isDirectory()) fs.rmSync(dst, { recursive: true, force: true })
|
||||||
else fs.unlinkSync(dst)
|
else fs.unlinkSync(dst)
|
||||||
}
|
}
|
||||||
|
if (fs.statSync(src).isDirectory()) copyDirSync(src, dst)
|
||||||
if (fs.statSync(src).isDirectory()) {
|
else fs.copyFileSync(src, dst)
|
||||||
console.log(` - 复制目录: ${src} -> ${dst}`)
|
|
||||||
copyDirSync(src, dst)
|
|
||||||
} else {
|
|
||||||
console.log(` - 复制文件: ${src} -> ${dst}`)
|
|
||||||
fs.copyFileSync(src, dst)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🗑️ 清理临时目录...')
|
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
@@ -609,20 +313,14 @@ export async function cloneBackend(
|
|||||||
type: 'backend',
|
type: 'backend',
|
||||||
progress: 100,
|
progress: 100,
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
message: `后端代码克隆完成(分支: ${targetBranch})`,
|
message: '后端代码克隆完成',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ 后端代码克隆完成(分支: ${targetBranch})`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('=== 后端代码获取操作完成 ===')
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
console.error('❌ 获取后端代码失败:', errorMessage)
|
console.error('获取后端代码失败:', errorMessage)
|
||||||
console.error('错误堆栈:', error instanceof Error ? error.stack : 'N/A')
|
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('download-progress', {
|
mainWindow.webContents.send('download-progress', {
|
||||||
type: 'backend',
|
type: 'backend',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "v5.0.0-alpha.3",
|
"version": "5.0.0-alpha.2",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
||||||
|
|||||||
@@ -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,11 +171,46 @@
|
|||||||
基建: {{ user.Info.InfrastMode === 'Normal' ? '普通' : '自定义' }}
|
基建: {{ user.Info.InfrastMode === 'Normal' ? '普通' : '自定义' }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
|
||||||
<!-- 关卡信息 - Stage固定展示 -->
|
<!-- 关卡信息 - 根据是否使用计划表配置显示不同内容 -->
|
||||||
<a-tag v-if="user.Info.Stage" class="info-tag" color="blue">
|
<template v-if="user.Info.Stage === '1-7' && props.currentPlanData">
|
||||||
关卡: {{ user.Info.Stage === '-' ? '未选择' : user.Info.Stage }}
|
<!-- 计划表模式信息 -->
|
||||||
|
<a-tag
|
||||||
|
v-if="props.currentPlanData.Info?.Mode"
|
||||||
|
class="info-tag"
|
||||||
|
color="purple"
|
||||||
|
>
|
||||||
|
模式:
|
||||||
|
{{ props.currentPlanData.Info.Mode === 'ALL' ? '全局' : '周计划' }}
|
||||||
</a-tag>
|
</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
|
<a-tag
|
||||||
v-if="
|
v-if="
|
||||||
@@ -327,6 +362,7 @@ import { message } from 'ant-design-vue'
|
|||||||
interface Props {
|
interface Props {
|
||||||
scripts: Script[]
|
scripts: Script[]
|
||||||
activeConnections: Map<string, { subscriptionId: string; websocketId: string }>
|
activeConnections: Map<string, { subscriptionId: string; websocketId: string }>
|
||||||
|
currentPlanData?: Record<string, any> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -419,6 +455,12 @@ const getRemainingDayColor = (remainedDay: number): string => {
|
|||||||
return 'green'
|
return 'green'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取关卡标签颜色
|
||||||
|
const getStageTagColor = (stage: string): string => {
|
||||||
|
if (stage === '1-7') return 'green' // 使用计划表配置用绿色
|
||||||
|
return 'blue' // 自定义关卡用蓝色
|
||||||
|
}
|
||||||
|
|
||||||
// 获取剩余天数的显示文本
|
// 获取剩余天数的显示文本
|
||||||
const getRemainingDayText = (remainedDay: number): string => {
|
const getRemainingDayText = (remainedDay: number): string => {
|
||||||
if (remainedDay === -1) return '剩余天数: 长期有效'
|
if (remainedDay === -1) return '剩余天数: 长期有效'
|
||||||
@@ -426,6 +468,102 @@ const getRemainingDayText = (remainedDay: number): string => {
|
|||||||
return `剩余天数: ${remainedDay}天`
|
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 () => {
|
const onScriptDragEnd = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,69 +1,97 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="display: none">
|
<div style="display: none">
|
||||||
<!-- 这是一个隐藏的监听组件,不需要UI -->
|
<!-- 这是一个隐藏的监听组件,不需要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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
|
||||||
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||||
import { logger } from '@/utils/logger'
|
import { logger } from '@/utils/logger'
|
||||||
|
|
||||||
// WebSocket hook
|
// WebSocket hook
|
||||||
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const dialogData = ref({
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
options: ['确定', '取消'],
|
||||||
|
messageId: ''
|
||||||
|
})
|
||||||
|
|
||||||
// 存储订阅ID用于取消订阅
|
// 存储订阅ID用于取消订阅
|
||||||
let subscriptionId: string
|
let subscriptionId: string
|
||||||
|
|
||||||
// 检查是否在 Electron 环境中
|
|
||||||
const isElectron = () => {
|
|
||||||
return typeof window !== 'undefined' && (window as any).electronAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送用户选择结果到后端
|
// 发送用户选择结果到后端
|
||||||
const sendResponse = (messageId: string, choice: boolean) => {
|
const sendResponse = (messageId: string, choice: boolean) => {
|
||||||
const response = {"choice": choice}
|
const response = {
|
||||||
|
message_id: messageId,
|
||||||
|
choice: choice,
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('[WebSocket消息监听器] 发送用户选择结果:', response)
|
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 title = questionData.title || '操作提示'
|
||||||
const message = questionData.message || ''
|
const message = questionData.message || ''
|
||||||
const options = questionData.options || ['确定', '取消']
|
const options = questionData.options || ['确定', '取消']
|
||||||
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
||||||
|
|
||||||
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
|
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
|
||||||
|
|
||||||
if (!isElectron()) {
|
// 设置对话框数据
|
||||||
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
|
dialogData.value = {
|
||||||
// 在非 Electron 环境中,使用默认响应
|
|
||||||
sendResponse(messageId, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 调用 Electron API 显示系统级对话框
|
|
||||||
const result = await (window as any).electronAPI.showQuestionDialog({
|
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
options,
|
options,
|
||||||
messageId
|
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类型消息')
|
logger.info('[WebSocket消息监听器] 发现Question类型消息')
|
||||||
|
|
||||||
if (data.message_id) {
|
if (data.message_id) {
|
||||||
logger.info('[WebSocket消息监听器] message_id存在,显示系统级对话框')
|
logger.info('[WebSocket消息监听器] message_id存在,显示选择弹窗')
|
||||||
showQuestion(data)
|
showQuestion(data)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
||||||
// 即使缺少message_id,也尝试显示对话框,使用当前时间戳作为ID
|
// 即使缺少message_id,也尝试显示弹窗,使用当前时间戳作为ID
|
||||||
const fallbackId = 'fallback_' + Date.now()
|
const fallbackId = 'fallback_' + Date.now()
|
||||||
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
|
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
|
||||||
showQuestion({
|
showQuestion({
|
||||||
...data,
|
...data,
|
||||||
message_id: fallbackId
|
message_id: fallbackId
|
||||||
@@ -182,4 +210,150 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</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>
|
.log-entry.error .log-message {
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ import { onMounted, onUnmounted, ref } from 'vue'
|
|||||||
import { useWebSocket } from '@/composables/useWebSocket'
|
import { useWebSocket } from '@/composables/useWebSocket'
|
||||||
import { logger } from '@/utils/logger'
|
import { logger } from '@/utils/logger'
|
||||||
|
|
||||||
const { subscribe, unsubscribe, getConnectionInfo } = useWebSocket()
|
const { subscribe, unsubscribe, sendRaw, getConnectionInfo } = useWebSocket()
|
||||||
|
|
||||||
// 测试状态
|
// 测试状态
|
||||||
const isTesting = ref(false)
|
const isTesting = ref(false)
|
||||||
@@ -165,39 +165,27 @@ const directTriggerModal = () => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接调用弹窗API测试功能
|
// 发送WebSocket消息来模拟接收消息
|
||||||
const simulateMessage = (messageData: any) => {
|
const simulateMessage = (messageData: any) => {
|
||||||
logger.info('[调试工具] 直接测试弹窗功能:', messageData)
|
logger.info('[调试工具] 发送模拟消息:', messageData)
|
||||||
|
|
||||||
try {
|
// 检查连接状态
|
||||||
// 检查是否在Electron环境
|
const connInfo = getConnectionInfo()
|
||||||
if (typeof window !== 'undefined' && (window as any).electronAPI?.showQuestionDialog) {
|
if (connInfo.status !== '已连接') {
|
||||||
// 直接调用Electron的弹窗API进行测试
|
logger.warn('[调试工具] WebSocket未连接,无法发送消息')
|
||||||
(window as any).electronAPI.showQuestionDialog({
|
lastResponse.value = '发送失败: WebSocket未连接'
|
||||||
title: messageData.title || '测试标题',
|
return
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用sendRaw直接发送Message类型的消息
|
||||||
|
sendRaw('Message', messageData)
|
||||||
|
|
||||||
|
logger.info('[调试工具] 消息已发送到WebSocket')
|
||||||
|
lastResponse.value = '消息已发送,等待弹窗显示...'
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('[调试工具] 测试弹窗失败:', error)
|
logger.error('[调试工具] 发送消息失败:', error)
|
||||||
lastResponse.value = '测试失败: ' + (error?.message || '未知错误')
|
lastResponse.value = '发送失败: ' + (error?.message || '未知错误')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,28 +44,22 @@ import RouteInfoPage from './RouteInfoPage.vue'
|
|||||||
import EnvironmentPage from './EnvironmentPage.vue'
|
import EnvironmentPage from './EnvironmentPage.vue'
|
||||||
import QuickNavPage from './QuickNavPage.vue'
|
import QuickNavPage from './QuickNavPage.vue'
|
||||||
import MessageTestPage from './MessageTestPage.vue'
|
import MessageTestPage from './MessageTestPage.vue'
|
||||||
import BackendLaunchPage from './BackendLaunchPage.vue'
|
|
||||||
|
|
||||||
// 调试页面配置
|
// 调试页面配置
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'route', title: '路由', icon: '🛣️', component: RouteInfoPage },
|
{ key: 'route', title: '路由', icon: '🛣️', component: RouteInfoPage },
|
||||||
{ key: 'env', title: '环境', icon: '⚙️', component: EnvironmentPage },
|
{ 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 },
|
{ key: 'message', title: '消息', icon: '💬', component: MessageTestPage },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 开发环境检测
|
// 开发环境检测
|
||||||
const isDev = ref(
|
const isDev = ref(process.env.NODE_ENV === 'development' || import.meta.env?.DEV === true)
|
||||||
process.env.NODE_ENV === 'development' ||
|
|
||||||
(import.meta as any).env?.DEV === true ||
|
|
||||||
window.location.hostname === 'localhost'
|
|
||||||
)
|
|
||||||
|
|
||||||
// 面板状态
|
// 面板状态
|
||||||
const isCollapsed = ref(false)
|
const isCollapsed = ref(false)
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const activeTab = ref('backend') // 默认显示后端页面
|
const activeTab = ref('route')
|
||||||
|
|
||||||
// 面板位置
|
// 面板位置
|
||||||
const panelPosition = ref({
|
const panelPosition = ref({
|
||||||
|
|||||||
@@ -71,9 +71,9 @@
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { getConfig } from '@/utils/config'
|
import { getConfig } from '@/utils/config'
|
||||||
import { mirrorManager } from '@/utils/mirrorManager'
|
import { mirrorManager } from '@/utils/mirrorManager'
|
||||||
|
import router from '@/router'
|
||||||
import { useUpdateChecker } from '@/composables/useUpdateChecker'
|
import { useUpdateChecker } from '@/composables/useUpdateChecker'
|
||||||
import { connectAfterBackendStart } from '@/composables/useWebSocket'
|
import { connectAfterBackendStart } from '@/composables/useWebSocket'
|
||||||
import { forceEnterApp } from '@/utils/appEntry'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
@@ -154,12 +154,11 @@ function handleForceEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确认弹窗中的"我知道我在做什么"按钮,直接进入应用
|
// 确认弹窗中的"我知道我在做什么"按钮,直接进入应用
|
||||||
async function handleForceEnterConfirm() {
|
function handleForceEnterConfirm() {
|
||||||
clearTimers()
|
clearTimers()
|
||||||
aborted.value = true
|
aborted.value = true
|
||||||
forceEnterVisible.value = false
|
forceEnterVisible.value = false
|
||||||
|
router.push('/home')
|
||||||
await forceEnterApp('自动模式-强行进入确认')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理 - 增强重新配置环境按钮功能
|
// 事件处理 - 增强重新配置环境按钮功能
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { forceEnterApp } from '@/utils/appEntry'
|
import router from '@/router'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -79,8 +79,8 @@ function handleReconfigure() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强行进入应用
|
// 强行进入应用
|
||||||
async function handleForceEnter() {
|
function handleForceEnter() {
|
||||||
await forceEnterApp('环境不完整-强行进入')
|
router.push('/home')
|
||||||
}
|
}
|
||||||
</script>
|
</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 BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
|
||||||
const HEARTBEAT_INTERVAL = 30000 // 30秒心跳间隔,与后端保持一致
|
const HEARTBEAT_INTERVAL = 15000
|
||||||
const HEARTBEAT_TIMEOUT = 45000 // 45秒超时,给网络延迟留够时间
|
const HEARTBEAT_TIMEOUT = 5000
|
||||||
const BACKEND_CHECK_INTERVAL = 6000 // 6秒检查间隔
|
const BACKEND_CHECK_INTERVAL = 3000
|
||||||
const MAX_RESTART_ATTEMPTS = 3
|
const MAX_RESTART_ATTEMPTS = 3
|
||||||
const RESTART_DELAY = 2000
|
const RESTART_DELAY = 2000
|
||||||
const MAX_QUEUE_SIZE = 50 // 每个 ID 或全局 type 队列最大条数
|
const MAX_QUEUE_SIZE = 50 // 每个 ID 或全局 type 队列最大条数
|
||||||
@@ -111,7 +111,7 @@ const initGlobalStorage = (): GlobalWSStorage => ({
|
|||||||
|
|
||||||
const getGlobalStorage = (): GlobalWSStorage => {
|
const getGlobalStorage = (): GlobalWSStorage => {
|
||||||
if (!(window as any)[WS_STORAGE_KEY]) {
|
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]
|
return (window as any)[WS_STORAGE_KEY]
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ const handleBackendFailure = async () => {
|
|||||||
okText: '重启应用',
|
okText: '重启应用',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
if ((window.electronAPI as any)?.windowClose) {
|
if ((window.electronAPI as any)?.windowClose) {
|
||||||
; (window.electronAPI as any).windowClose()
|
;(window.electronAPI as any).windowClose()
|
||||||
} else {
|
} else {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
@@ -178,26 +178,13 @@ const handleBackendFailure = async () => {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const success = await restartBackend()
|
const success = await restartBackend()
|
||||||
if (success) {
|
if (success) {
|
||||||
// 统一在一个地方管理连接权限
|
|
||||||
setConnectionPermission(true, '后端重启后重连')
|
setConnectionPermission(true, '后端重启后重连')
|
||||||
|
|
||||||
// 等待后端完全启动
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const connected = await connectGlobalWebSocket('后端重启后重连')
|
|
||||||
if (connected) {
|
|
||||||
// 连接成功后再禁用权限
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
connectGlobalWebSocket('后端重启后重连').then(() => {
|
||||||
setConnectionPermission(false, '正常运行中')
|
setConnectionPermission(false, '正常运行中')
|
||||||
}, 1000)
|
})
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
warn('重启后重连失败:', e)
|
|
||||||
setConnectionPermission(false, '连接失败')
|
|
||||||
}
|
|
||||||
}, RESTART_DELAY)
|
}, RESTART_DELAY)
|
||||||
} else {
|
} else {
|
||||||
// 重启失败,继续尝试
|
|
||||||
setTimeout(handleBackendFailure, RESTART_DELAY)
|
setTimeout(handleBackendFailure, RESTART_DELAY)
|
||||||
}
|
}
|
||||||
}, RESTART_DELAY)
|
}, RESTART_DELAY)
|
||||||
@@ -221,15 +208,12 @@ const startBackendMonitoring = () => {
|
|||||||
setBackendStatus('stopped')
|
setBackendStatus('stopped')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查心跳超时:如果超过心跳超时时间且连接仍然打开,说明后端可能有问题
|
if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT * 2) {
|
||||||
if (global.lastPingTime > 0 && now - global.lastPingTime > HEARTBEAT_TIMEOUT) {
|
|
||||||
if (global.wsRef?.readyState === WebSocket.OPEN) {
|
if (global.wsRef?.readyState === WebSocket.OPEN) {
|
||||||
setBackendStatus('error')
|
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 },
|
data: { Ping: pingTime, connectionId: global.connectionId },
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
}, HEARTBEAT_INTERVAL)
|
}, HEARTBEAT_INTERVAL)
|
||||||
}
|
}
|
||||||
@@ -377,8 +361,8 @@ const handleMessage = (raw: WebSocketBaseMessage) => {
|
|||||||
log(`消息已缓存: type=${raw.type}, id=${raw.id}`)
|
log(`消息已缓存: type=${raw.type}, id=${raw.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定期清理过期消息(每处理50条消息触发一次,避免频繁且更可预测)
|
// 定期清理过期消息(每 10 条触发一次,避免频繁)
|
||||||
if (global.cachedMessages.value.length > 0 && global.cachedMessages.value.length % 50 === 0) {
|
if (Math.random() < 0.1) {
|
||||||
cleanupExpiredMessages(now)
|
cleanupExpiredMessages(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +455,7 @@ const releaseConnectionLock = () => {
|
|||||||
isGlobalConnectingLock = false
|
isGlobalConnectingLock = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连', '系统初始化', '手动重连', '强制连接']
|
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连']
|
||||||
const isValidConnectionReason = (reason: string) => allowedConnectionReasons.includes(reason)
|
const isValidConnectionReason = (reason: string) => allowedConnectionReasons.includes(reason)
|
||||||
const checkConnectionPermission = () => getGlobalStorage().allowNewConnection
|
const checkConnectionPermission = () => getGlobalStorage().allowNewConnection
|
||||||
const setConnectionPermission = (allow: boolean, reason: string) => {
|
const setConnectionPermission = (allow: boolean, reason: string) => {
|
||||||
@@ -482,19 +466,9 @@ const setConnectionPermission = (allow: boolean, reason: string) => {
|
|||||||
|
|
||||||
const createGlobalWebSocket = (): WebSocket => {
|
const createGlobalWebSocket = (): WebSocket => {
|
||||||
const global = getGlobalStorage()
|
const global = getGlobalStorage()
|
||||||
|
|
||||||
// 清理旧连接
|
|
||||||
if (global.wsRef) {
|
if (global.wsRef) {
|
||||||
if (global.wsRef.readyState === WebSocket.OPEN) {
|
if (global.wsRef.readyState === WebSocket.OPEN) return global.wsRef
|
||||||
log('警告:尝试创建新连接但当前连接仍有效')
|
if (global.wsRef.readyState === WebSocket.CONNECTING) return global.wsRef
|
||||||
return global.wsRef
|
|
||||||
}
|
|
||||||
if (global.wsRef.readyState === WebSocket.CONNECTING) {
|
|
||||||
log('警告:尝试创建新连接但当前连接正在建立中')
|
|
||||||
return global.wsRef
|
|
||||||
}
|
|
||||||
// 清理已关闭或错误状态的连接
|
|
||||||
global.wsRef = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(BASE_WS_URL)
|
const ws = new WebSocket(BASE_WS_URL)
|
||||||
@@ -506,11 +480,7 @@ const createGlobalWebSocket = (): WebSocket => {
|
|||||||
global.reconnectAttempts = 0
|
global.reconnectAttempts = 0
|
||||||
setGlobalStatus('已连接')
|
setGlobalStatus('已连接')
|
||||||
startGlobalHeartbeat(ws)
|
startGlobalHeartbeat(ws)
|
||||||
|
|
||||||
// 只有在特殊连接原因下才设置为正常运行
|
|
||||||
if (global.connectionReason !== '系统初始化') {
|
|
||||||
setConnectionPermission(false, '正常运行中')
|
setConnectionPermission(false, '正常运行中')
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -525,52 +495,35 @@ const createGlobalWebSocket = (): WebSocket => {
|
|||||||
data: { Pong: Date.now(), connectionId: global.connectionId },
|
data: { Pong: Date.now(), connectionId: global.connectionId },
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch {}
|
||||||
warn('发送初始信号失败:', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeGlobalSubscriptions()
|
initializeGlobalSubscriptions()
|
||||||
log('WebSocket连接已建立并初始化完成')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onmessage = ev => {
|
ws.onmessage = ev => {
|
||||||
try {
|
try {
|
||||||
const raw = JSON.parse(ev.data) as WebSocketBaseMessage
|
const raw = JSON.parse(ev.data) as WebSocketBaseMessage
|
||||||
handleMessage(raw)
|
handleMessage(raw)
|
||||||
} catch (e) {
|
} catch {}
|
||||||
warn('解析WebSocket消息失败:', e, '原始数据:', ev.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
setGlobalStatus('连接错误')
|
|
||||||
warn('WebSocket错误:', error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ws.onerror = () => setGlobalStatus('连接错误')
|
||||||
ws.onclose = event => {
|
ws.onclose = event => {
|
||||||
setGlobalStatus('已断开')
|
setGlobalStatus('已断开')
|
||||||
stopGlobalHeartbeat()
|
stopGlobalHeartbeat()
|
||||||
global.isConnecting = false
|
global.isConnecting = false
|
||||||
|
|
||||||
log(`WebSocket连接关闭: code=${event.code}, reason="${event.reason}"`)
|
|
||||||
|
|
||||||
// 根据关闭原因决定是否需要处理后端故障
|
|
||||||
if (event.code === 1000 && event.reason === 'Ping超时') {
|
if (event.code === 1000 && event.reason === 'Ping超时') {
|
||||||
handleBackendFailure().catch(e => warn('handleBackendFailure error:', e))
|
handleBackendFailure().catch(e => warn('handleBackendFailure error:', e))
|
||||||
} else if (event.code === 1000 && event.reason === '心跳超时') {
|
|
||||||
handleBackendFailure().catch(e => warn('handleBackendFailure error:', e))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectGlobalWebSocket = async (reason: string = '手动重连'): Promise<boolean> => {
|
const connectGlobalWebSocket = async (reason: string = '未指定原因'): Promise<boolean> => {
|
||||||
const global = getGlobalStorage()
|
const global = getGlobalStorage()
|
||||||
if (!checkConnectionPermission() || !isValidConnectionReason(reason)) {
|
if (!checkConnectionPermission() || !isValidConnectionReason(reason)) return false
|
||||||
warn(`连接被拒绝: 权限=${checkConnectionPermission()}, 原因="${reason}"是否有效=${isValidConnectionReason(reason)}`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!acquireConnectionLock()) return false
|
if (!acquireConnectionLock()) return false
|
||||||
|
|
||||||
try {
|
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 _defaultHandlersLoaded = true
|
||||||
let _defaultTaskManagerHandler = schedulerHandlers.handleTaskManagerMessage
|
let _defaultTaskManagerHandler = schedulerHandlers.handleTaskManagerMessage
|
||||||
@@ -740,7 +627,7 @@ const initializeGlobalSubscriptions = () => {
|
|||||||
data: { Pong: msg.data.Ping, connectionId: global.connectionId },
|
data: { Pong: msg.data.Ping, connectionId: global.connectionId },
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -761,22 +648,8 @@ export function useWebSocket() {
|
|||||||
const ws = global.wsRef
|
const ws = global.wsRef
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
const message = { id, type, data }
|
ws.send(JSON.stringify({ id, type, data }))
|
||||||
ws.send(JSON.stringify(message))
|
} catch {}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,21 +679,6 @@ export function useWebSocket() {
|
|||||||
lastCheck: global.lastBackendCheck,
|
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 {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
unsubscribe,
|
unsubscribe,
|
||||||
@@ -830,7 +688,6 @@ export function useWebSocket() {
|
|||||||
backendStatus: global.backendStatus,
|
backendStatus: global.backendStatus,
|
||||||
restartBackend: restartBackendManually,
|
restartBackend: restartBackendManually,
|
||||||
getBackendStatus,
|
getBackendStatus,
|
||||||
debug: DEBUG ? debug : undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
frontend/src/types/electron.d.ts
vendored
26
frontend/src/types/electron.d.ts
vendored
@@ -1,5 +1,4 @@
|
|||||||
declare global {
|
export interface ElectronAPI {
|
||||||
interface ElectronAPI {
|
|
||||||
openDevTools: () => Promise<void>
|
openDevTools: () => Promise<void>
|
||||||
selectFolder: () => Promise<string | null>
|
selectFolder: () => Promise<string | null>
|
||||||
selectFile: (filters?: any[]) => Promise<string[]>
|
selectFile: (filters?: any[]) => Promise<string[]>
|
||||||
@@ -54,31 +53,12 @@ declare global {
|
|||||||
openFile: (filePath: string) => Promise<void>
|
openFile: (filePath: string) => Promise<void>
|
||||||
showItemInFolder: (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
|
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||||
removeDownloadProgressListener: () => void
|
removeDownloadProgressListener: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: ElectronAPI
|
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 = {
|
interface ElectronAPI {
|
||||||
DEBUG: 'DEBUG',
|
getLogPath: () => Promise<string>
|
||||||
INFO: 'INFO',
|
getLogFiles: () => Promise<string[]>
|
||||||
WARN: 'WARN',
|
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||||
ERROR: 'ERROR'
|
clearLogs: (fileName?: string) => Promise<void>
|
||||||
} as const
|
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
|
declare global {
|
||||||
export { LogLevel }
|
interface Window {
|
||||||
|
electronAPI: ElectronAPI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 'DEBUG',
|
||||||
|
INFO = 'INFO',
|
||||||
|
WARN = 'WARN',
|
||||||
|
ERROR = 'ERROR'
|
||||||
|
}
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
// 直接使用原生console,主进程会自动处理日志记录
|
// 直接使用原生console,主进程会自动处理日志记录
|
||||||
@@ -29,32 +40,32 @@ class Logger {
|
|||||||
|
|
||||||
// 获取日志文件路径
|
// 获取日志文件路径
|
||||||
async getLogPath(): Promise<string> {
|
async getLogPath(): Promise<string> {
|
||||||
if ((window as any).electronAPI) {
|
if (window.electronAPI) {
|
||||||
return await (window as any).electronAPI.getLogPath()
|
return await window.electronAPI.getLogPath()
|
||||||
}
|
}
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取日志文件列表
|
// 获取日志文件列表
|
||||||
async getLogFiles(): Promise<string[]> {
|
async getLogFiles(): Promise<string[]> {
|
||||||
if ((window as any).electronAPI) {
|
if (window.electronAPI) {
|
||||||
return await (window as any).electronAPI.getLogFiles()
|
return await window.electronAPI.getLogFiles()
|
||||||
}
|
}
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取日志内容
|
// 获取日志内容
|
||||||
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
||||||
if ((window as any).electronAPI) {
|
if (window.electronAPI) {
|
||||||
return await (window as any).electronAPI.getLogs(lines, fileName)
|
return await window.electronAPI.getLogs(lines, fileName)
|
||||||
}
|
}
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空日志
|
// 清空日志
|
||||||
async clearLogs(fileName?: string): Promise<void> {
|
async clearLogs(fileName?: string): Promise<void> {
|
||||||
if ((window as any).electronAPI) {
|
if (window.electronAPI) {
|
||||||
await (window as any).electronAPI.clearLogs(fileName)
|
await window.electronAPI.clearLogs(fileName)
|
||||||
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
@@ -63,8 +74,8 @@ class Logger {
|
|||||||
|
|
||||||
// 清理旧日志
|
// 清理旧日志
|
||||||
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
||||||
if ((window as any).electronAPI) {
|
if (window.electronAPI) {
|
||||||
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
|
await window.electronAPI.cleanOldLogs(daysToKeep)
|
||||||
console.info(`已清理${daysToKeep}天前的旧日志`)
|
console.info(`已清理${daysToKeep}天前的旧日志`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Electron API not available')
|
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>
|
</a-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-space size="middle">
|
// 如果已有连接,先断开
|
||||||
|
if (generalSubscriptionId.value) {
|
||||||
|
unsubscribe(generalSubscriptionId.value)
|
||||||
|
generalSubscriptionId.value = null
|
||||||
|
generalWebsocketId.value = nulla-space size="middle">
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
ghost
|
ghost
|
||||||
@@ -371,15 +375,6 @@ import { Service } from '@/api'
|
|||||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||||
import WebhookManager from '@/components/WebhookManager.vue'
|
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 router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi()
|
const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi()
|
||||||
@@ -586,9 +581,8 @@ const handleGeneralConfig = async () => {
|
|||||||
showGeneralConfigMask.value = true
|
showGeneralConfigMask.value = true
|
||||||
|
|
||||||
// 如果已有连接,先断开并清理
|
// 如果已有连接,先断开并清理
|
||||||
if (generalSubscriptionId.value) {
|
if (generalWebsocketId.value) {
|
||||||
unsubscribe(generalSubscriptionId.value)
|
unsubscribe(generalWebsocketId.value)
|
||||||
generalSubscriptionId.value = null
|
|
||||||
generalWebsocketId.value = null
|
generalWebsocketId.value = null
|
||||||
showGeneralConfigMask.value = false
|
showGeneralConfigMask.value = false
|
||||||
if (generalConfigTimeout) {
|
if (generalConfigTimeout) {
|
||||||
@@ -696,7 +690,7 @@ const handleSaveGeneralConfig = async () => {
|
|||||||
// 文件选择方法
|
// 文件选择方法
|
||||||
const selectScriptBeforeTask = async () => {
|
const selectScriptBeforeTask = async () => {
|
||||||
try {
|
try {
|
||||||
const path = await window.electronAPI?.selectFile([
|
const path = await (window.electronAPI as any)?.selectFile([
|
||||||
{ name: '可执行文件', extensions: ['exe', 'bat', 'cmd', 'ps1'] },
|
{ name: '可执行文件', extensions: ['exe', 'bat', 'cmd', 'ps1'] },
|
||||||
{ name: '脚本文件', extensions: ['py', 'js', 'sh'] },
|
{ name: '脚本文件', extensions: ['py', 'js', 'sh'] },
|
||||||
{ name: '所有文件', extensions: ['*'] },
|
{ name: '所有文件', extensions: ['*'] },
|
||||||
@@ -714,7 +708,7 @@ const selectScriptBeforeTask = async () => {
|
|||||||
|
|
||||||
const selectScriptAfterTask = async () => {
|
const selectScriptAfterTask = async () => {
|
||||||
try {
|
try {
|
||||||
const path = await window.electronAPI?.selectFile([
|
const path = await (window.electronAPI as any)?.selectFile([
|
||||||
{ name: '可执行文件', extensions: ['exe', 'bat', 'cmd', 'ps1'] },
|
{ name: '可执行文件', extensions: ['exe', 'bat', 'cmd', 'ps1'] },
|
||||||
{ name: '脚本文件', extensions: ['py', 'js', 'sh'] },
|
{ name: '脚本文件', extensions: ['py', 'js', 'sh'] },
|
||||||
{ name: '所有文件', extensions: ['*'] },
|
{ name: '所有文件', extensions: ['*'] },
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import ManualMode from '@/components/initialization/ManualMode.vue'
|
|||||||
import EnvironmentIncomplete from '@/components/initialization/EnvironmentIncomplete.vue'
|
import EnvironmentIncomplete from '@/components/initialization/EnvironmentIncomplete.vue'
|
||||||
import type { DownloadProgress } from '@/types/initialization'
|
import type { DownloadProgress } from '@/types/initialization'
|
||||||
import { mirrorManager } from '@/utils/mirrorManager'
|
import { mirrorManager } from '@/utils/mirrorManager'
|
||||||
import { forceEnterApp } from '@/utils/appEntry'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -70,8 +69,8 @@ const mirrorConfigStatus = ref({
|
|||||||
const manualModeRef = ref()
|
const manualModeRef = ref()
|
||||||
|
|
||||||
// 基础功能函数
|
// 基础功能函数
|
||||||
async function skipToHome() {
|
function skipToHome() {
|
||||||
await forceEnterApp('跳过初始化直接进入')
|
router.push('/home')
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToManualMode() {
|
function switchToManualMode() {
|
||||||
@@ -85,14 +84,10 @@ async function enterApp() {
|
|||||||
try {
|
try {
|
||||||
// 设置初始化完成标记
|
// 设置初始化完成标记
|
||||||
await setInitialized(true)
|
await setInitialized(true)
|
||||||
console.log('设置初始化完成标记,准备进入应用...')
|
console.log('设置初始化完成标记,跳转到首页')
|
||||||
|
router.push('/home')
|
||||||
// 使用统一的进入应用函数
|
|
||||||
await forceEnterApp('初始化完成后进入')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('进入应用失败:', error)
|
console.error('进入应用失败:', error)
|
||||||
// 即使出错也强制进入
|
|
||||||
await forceEnterApp('初始化失败后强制进入')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
<ScriptTable
|
<ScriptTable
|
||||||
:scripts="scripts"
|
:scripts="scripts"
|
||||||
:active-connections="activeConnections"
|
:active-connections="activeConnections"
|
||||||
|
:current-plan-data="currentPlanData"
|
||||||
@edit="handleEditScript"
|
@edit="handleEditScript"
|
||||||
@delete="handleDeleteScript"
|
@delete="handleDeleteScript"
|
||||||
@add-user="handleAddUser"
|
@add-user="handleAddUser"
|
||||||
@@ -253,6 +254,7 @@ import { useScriptApi } from '@/composables/useScriptApi'
|
|||||||
import { useUserApi } from '@/composables/useUserApi'
|
import { useUserApi } from '@/composables/useUserApi'
|
||||||
import { useWebSocket } from '@/composables/useWebSocket'
|
import { useWebSocket } from '@/composables/useWebSocket'
|
||||||
import { useTemplateApi, type WebConfigTemplate } from '@/composables/useTemplateApi'
|
import { useTemplateApi, type WebConfigTemplate } from '@/composables/useTemplateApi'
|
||||||
|
import { usePlanApi } from '@/composables/usePlanApi'
|
||||||
import { Service } from '@/api/services/Service'
|
import { Service } from '@/api/services/Service'
|
||||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
@@ -262,6 +264,7 @@ const { addScript, deleteScript, getScriptsWithUsers } = useScriptApi()
|
|||||||
const { updateUser, deleteUser } = useUserApi()
|
const { updateUser, deleteUser } = useUserApi()
|
||||||
const { subscribe, unsubscribe } = useWebSocket()
|
const { subscribe, unsubscribe } = useWebSocket()
|
||||||
const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi()
|
const { getWebConfigTemplates, importScriptFromWeb } = useTemplateApi()
|
||||||
|
const { getPlans } = usePlanApi()
|
||||||
|
|
||||||
// 初始化markdown解析器
|
// 初始化markdown解析器
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
@@ -273,6 +276,8 @@ const md = new MarkdownIt({
|
|||||||
const scripts = ref<Script[]>([])
|
const scripts = ref<Script[]>([])
|
||||||
// 增加:标记是否已经完成过一次脚本列表加载(成功或失败都算一次)
|
// 增加:标记是否已经完成过一次脚本列表加载(成功或失败都算一次)
|
||||||
const loadedOnce = ref(false)
|
const loadedOnce = ref(false)
|
||||||
|
// 当前计划表数据
|
||||||
|
const currentPlanData = ref<Record<string, any> | null>(null)
|
||||||
const typeSelectVisible = ref(false)
|
const typeSelectVisible = ref(false)
|
||||||
const generalModeSelectVisible = ref(false)
|
const generalModeSelectVisible = ref(false)
|
||||||
const templateSelectVisible = ref(false)
|
const templateSelectVisible = ref(false)
|
||||||
@@ -312,6 +317,7 @@ const filteredTemplates = computed(() => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadScripts()
|
loadScripts()
|
||||||
|
loadCurrentPlan()
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadScripts = async () => {
|
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 = () => {
|
const handleAddScript = () => {
|
||||||
selectedType.value = 'MAA'
|
selectedType.value = 'MAA'
|
||||||
typeSelectVisible.value = true
|
typeSelectVisible.value = true
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
:current-mode="currentMode"
|
:current-mode="currentMode"
|
||||||
:view-mode="viewMode"
|
:view-mode="viewMode"
|
||||||
:options-loaded="!loading"
|
:options-loaded="!loading"
|
||||||
|
:plan-id="activePlanId"
|
||||||
@update-table-data="handleTableDataUpdate"
|
@update-table-data="handleTableDataUpdate"
|
||||||
/>
|
/>
|
||||||
</PlanConfig>
|
</PlanConfig>
|
||||||
@@ -70,6 +71,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { usePlanApi } from '@/composables/usePlanApi'
|
import { usePlanApi } from '@/composables/usePlanApi'
|
||||||
|
import { generateUniquePlanName, validatePlanName, getPlanTypeLabel } from '@/utils/planNameUtils'
|
||||||
import PlanHeader from './components/PlanHeader.vue'
|
import PlanHeader from './components/PlanHeader.vue'
|
||||||
import PlanSelector from './components/PlanSelector.vue'
|
import PlanSelector from './components/PlanSelector.vue'
|
||||||
import PlanConfig from './components/PlanConfig.vue'
|
import PlanConfig from './components/PlanConfig.vue'
|
||||||
@@ -100,6 +102,7 @@ const viewMode = ref<'config' | 'simple'>('config')
|
|||||||
|
|
||||||
const isEditingPlanName = ref<boolean>(false)
|
const isEditingPlanName = ref<boolean>(false)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
const switching = ref(false) // 添加切换状态
|
||||||
|
|
||||||
// Use a record to match child component expectations
|
// Use a record to match child component expectations
|
||||||
const tableData = ref<Record<string, any>>({})
|
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') => {
|
const handleAddPlan = async (planType: string = 'MaaPlanConfig') => {
|
||||||
try {
|
try {
|
||||||
const response = await createPlan(planType)
|
const response = await createPlan(planType)
|
||||||
const defaultName = getDefaultPlanName(planType)
|
const uniqueName = getDefaultPlanName(planType)
|
||||||
const newPlan = { id: response.planId, name: defaultName, type: planType }
|
const newPlan = { id: response.planId, name: uniqueName, type: planType }
|
||||||
planList.value.push(newPlan)
|
planList.value.push(newPlan)
|
||||||
activePlanId.value = newPlan.id
|
activePlanId.value = newPlan.id
|
||||||
currentPlanName.value = defaultName
|
currentPlanName.value = uniqueName
|
||||||
await loadPlanData(newPlan.id)
|
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) {
|
} catch (error) {
|
||||||
console.error('添加计划失败:', error)
|
console.error('添加计划失败:', error)
|
||||||
}
|
}
|
||||||
@@ -186,6 +194,7 @@ const saveInBackground = async (planId: string) => {
|
|||||||
const planData: Record<string, any> = { ...(tableData.value || {}) }
|
const planData: Record<string, any> = { ...(tableData.value || {}) }
|
||||||
planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType }
|
planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType }
|
||||||
|
|
||||||
|
console.log(`[计划表] 保存数据 (${planId}):`, planData)
|
||||||
await updatePlan(planId, planData)
|
await updatePlan(planId, planData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('后台保存计划数据失败:', error)
|
console.error('后台保存计划数据失败:', error)
|
||||||
@@ -214,20 +223,27 @@ const handleSave = async () => {
|
|||||||
await debouncedSave()
|
await debouncedSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优化计划切换逻辑
|
// 优化计划切换逻辑 - 异步保存,立即切换
|
||||||
const onPlanChange = async (planId: string) => {
|
const onPlanChange = async (planId: string) => {
|
||||||
if (planId === activePlanId.value) return
|
if (planId === activePlanId.value) return
|
||||||
|
|
||||||
// 触发当前计划的异步保存,但不等待完成
|
switching.value = true
|
||||||
|
try {
|
||||||
|
// 异步保存当前计划,不等待完成
|
||||||
if (activePlanId.value) {
|
if (activePlanId.value) {
|
||||||
saveInBackground(activePlanId.value).catch(error => {
|
saveInBackground(activePlanId.value).catch(error => {
|
||||||
console.warn('切换时保存当前计划失败:', error)
|
console.error('切换时保存当前计划失败:', error)
|
||||||
|
message.warning('保存当前计划时出现问题,请检查数据是否完整')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即切换到新计划
|
// 立即切换到新计划,提升响应速度
|
||||||
|
console.log(`[计划表] 切换到新计划: ${planId}`)
|
||||||
activePlanId.value = planId
|
activePlanId.value = planId
|
||||||
await loadPlanData(planId)
|
await loadPlanData(planId)
|
||||||
|
} finally {
|
||||||
|
switching.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startEditPlanName = () => {
|
const startEditPlanName = () => {
|
||||||
@@ -242,13 +258,29 @@ const startEditPlanName = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const finishEditPlanName = () => {
|
const finishEditPlanName = () => {
|
||||||
isEditingPlanName.value = false
|
|
||||||
if (activePlanId.value) {
|
if (activePlanId.value) {
|
||||||
const currentPlan = planList.value.find(plan => plan.id === activePlanId.value)
|
const currentPlan = planList.value.find(plan => plan.id === activePlanId.value)
|
||||||
if (currentPlan) {
|
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 = () => {
|
const onModeChange = () => {
|
||||||
@@ -263,20 +295,41 @@ const handleTableDataUpdate = async (newData: Record<string, any>) => {
|
|||||||
|
|
||||||
const loadPlanData = async (planId: string) => {
|
const loadPlanData = async (planId: string) => {
|
||||||
try {
|
try {
|
||||||
|
// 优化:先检查缓存数据,避免不必要的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)
|
const response = await getPlans(planId)
|
||||||
currentPlanData.value = response.data
|
currentPlanData.value = response.data
|
||||||
if (response.data && response.data[planId]) {
|
planData = response.data[planId] as PlanData
|
||||||
const planData = response.data[planId] as PlanData
|
console.log(`[计划表] 加载新数据 (${planId})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planData) {
|
||||||
if (planData.Info) {
|
if (planData.Info) {
|
||||||
const apiName = planData.Info.Name || ''
|
const apiName = planData.Info.Name || ''
|
||||||
if (!apiName && !currentPlanName.value) {
|
|
||||||
const currentPlan = planList.value.find(plan => plan.id === planId)
|
const currentPlan = planList.value.find(plan => plan.id === planId)
|
||||||
if (currentPlan) currentPlanName.value = currentPlan.name
|
|
||||||
|
// 优先使用planList中的名称
|
||||||
|
if (currentPlan && currentPlan.name) {
|
||||||
|
currentPlanName.value = currentPlan.name
|
||||||
|
|
||||||
|
if (apiName !== currentPlan.name) {
|
||||||
|
console.log(`[计划表] 同步名称: API="${apiName}" -> planList="${currentPlan.name}"`)
|
||||||
|
}
|
||||||
} else if (apiName) {
|
} else if (apiName) {
|
||||||
currentPlanName.value = apiName
|
currentPlanName.value = apiName
|
||||||
|
if (currentPlan) {
|
||||||
|
currentPlan.name = apiName
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentMode.value = planData.Info.Mode || 'ALL'
|
currentMode.value = planData.Info.Mode || 'ALL'
|
||||||
}
|
}
|
||||||
|
|
||||||
tableData.value = planData
|
tableData.value = planData
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -288,17 +341,47 @@ const initPlans = async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await getPlans()
|
const response = await getPlans()
|
||||||
if (response.index && response.index.length > 0) {
|
if (response.index && response.index.length > 0) {
|
||||||
|
// 优化:预先收集所有名称,避免O(n²)复杂度
|
||||||
|
const allPlanNames: string[] = []
|
||||||
|
|
||||||
planList.value = response.index.map((item: any) => {
|
planList.value = response.index.map((item: any) => {
|
||||||
const planId = item.uid
|
const planId = item.uid
|
||||||
const planData = response.data[planId]
|
const planData = response.data[planId]
|
||||||
const planType = item.type
|
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 }
|
return { id: planId, name: planName, type: planType }
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryPlanId = (route.query.planId as string) || ''
|
const queryPlanId = (route.query.planId as string) || ''
|
||||||
const target = queryPlanId ? planList.value.find(p => p.id === queryPlanId) : null
|
const target = queryPlanId ? planList.value.find(p => p.id === queryPlanId) : null
|
||||||
activePlanId.value = target ? target.id : planList.value[0].id
|
const selectedPlanId = target ? target.id : planList.value[0].id
|
||||||
await loadPlanData(activePlanId.value)
|
|
||||||
|
// 优化:直接使用已获取的数据,避免重复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 {
|
} else {
|
||||||
currentPlanData.value = null
|
currentPlanData.value = null
|
||||||
}
|
}
|
||||||
@@ -310,22 +393,12 @@ const initPlans = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultPlanName = (planType: string) =>
|
const getDefaultPlanName = (planType: string) => {
|
||||||
(
|
// 保持原来的逻辑,但添加重名检测
|
||||||
({
|
const existingNames = planList.value.map(plan => plan.name)
|
||||||
MaaPlanConfig: '新 MAA 计划表',
|
return generateUniquePlanName(planType, existingNames)
|
||||||
GeneralPlan: '新通用计划表',
|
}
|
||||||
CustomPlan: '新自定义计划表',
|
// getPlanTypeLabel 现在从 @/utils/planNameUtils 导入,删除本地定义
|
||||||
}) as Record<string, string>
|
|
||||||
)[planType] || '新计划表'
|
|
||||||
const getPlanTypeLabel = (planType: string) =>
|
|
||||||
(
|
|
||||||
({
|
|
||||||
MaaPlanConfig: 'MAA计划表',
|
|
||||||
GeneralPlan: '通用计划表',
|
|
||||||
CustomPlan: '自定义计划表',
|
|
||||||
}) as Record<string, string>
|
|
||||||
)[planType] || '计划表'
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [currentPlanName.value, currentMode.value],
|
() => [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