feat: 初步完成后端自定义webhook适配;重构配置项管理体系

This commit is contained in:
DLmaster361
2025-10-01 11:05:50 +08:00
parent 68b1ed4238
commit e286fc8d55
12 changed files with 2181 additions and 1976 deletions

View File

@@ -100,141 +100,83 @@ async def test_notify() -> OutBase:
@router.post(
"/webhook/create",
summary="创建自定义Webhook",
response_model=OutBase,
"/webhook/get",
summary="查询 webhook 配置",
response_model=WebhookGetOut,
status_code=200,
)
async def create_webhook(webhook_data: dict = Body(...)) -> OutBase:
"""创建自定义Webhook"""
async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut:
try:
# 生成唯一ID
webhook_id = str(uuid.uuid4())
# 创建webhook配置
webhook_config = {
"id": webhook_id,
"name": webhook_data.get("name", ""),
"url": webhook_data.get("url", ""),
"template": webhook_data.get("template", ""),
"enabled": webhook_data.get("enabled", True),
"headers": webhook_data.get("headers", {}),
"method": webhook_data.get("method", "POST"),
}
# 获取当前配置
current_config = await Config.get_setting()
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
# 添加新webhook
custom_webhooks.append(webhook_config)
# 更新配置
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
await Config.update_setting(update_data)
return OutBase(message=f"Webhook '{webhook_config['name']}' 创建成功")
index, data = await Config.get_webhook(None, None, webhook.webhookId)
index = [WebhookIndexItem(**_) for _ in index]
data = {uid: Webhook(**cfg) for uid, cfg in data.items()}
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
return WebhookGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return WebhookGetOut(index=index, data=data)
@router.post(
"/webhook/update",
summary="更新自定义Webhook",
response_model=OutBase,
"/webhook/add",
summary="添加定时项",
response_model=WebhookCreateOut,
status_code=200,
)
async def update_webhook(webhook_data: dict = Body(...)) -> OutBase:
"""更新自定义Webhook"""
async def add_webhook() -> WebhookCreateOut:
try:
webhook_id = webhook_data.get("id")
if not webhook_id:
return OutBase(code=400, status="error", message="缺少Webhook ID")
# 获取当前配置
current_config = await Config.get_setting()
custom_webhooks = current_config.get("Notify", {}).get("CustomWebhooks", [])
# 查找并更新webhook
updated = False
for i, webhook in enumerate(custom_webhooks):
if webhook.get("id") == webhook_id:
custom_webhooks[i].update(
{
"name": webhook_data.get("name", webhook.get("name", "")),
"url": webhook_data.get("url", webhook.get("url", "")),
"template": webhook_data.get(
"template", webhook.get("template", "")
),
"enabled": webhook_data.get(
"enabled", webhook.get("enabled", True)
),
"headers": webhook_data.get(
"headers", webhook.get("headers", {})
),
"method": webhook_data.get(
"method", webhook.get("method", "POST")
),
}
)
updated = True
break
if not updated:
return OutBase(code=404, status="error", message="Webhook不存在")
# 更新配置
update_data = {"Notify": {"CustomWebhooks": custom_webhooks}}
await Config.update_setting(update_data)
return OutBase(message="Webhook更新成功")
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
uid, config = await Config.add_webhook(None, None)
data = Webhook(**(await config.toDict()))
return WebhookCreateOut(webhookId=str(uid), data=data)
@router.post(
"/webhook/delete",
summary="删除自定义Webhook",
response_model=OutBase,
status_code=200,
"/webhook/update", summary="更新定时项", response_model=OutBase, status_code=200
)
async def delete_webhook(webhook_data: dict = Body(...)) -> OutBase:
"""删除自定义Webhook"""
async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase:
try:
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删除成功")
await Config.update_webhook(
None, None, webhook.webhookId, webhook.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/webhook/delete", summary="删除定时项", response_model=OutBase, status_code=200
)
async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_webhook(None, None, webhook.webhookId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/webhook/order", summary="重新排序定时项", response_model=OutBase, status_code=200
)
async def reorder_webhook(webhook: WebhookReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_webhook(None, None, webhook.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(

File diff suppressed because it is too large Load Diff

View File

@@ -62,10 +62,7 @@ class _TaskManager:
actual_id = None
else:
for script_id, script in Config.ScriptConfig.items():
if (
isinstance(script, (MaaConfig | GeneralConfig))
and actual_id in script.UserData
):
if actual_id in script.UserData:
task_id = script_id
break
else:
@@ -142,31 +139,26 @@ class _TaskManager:
# 初始化任务列表
if task_id in Config.QueueConfig:
queue = Config.QueueConfig[task_id]
if not isinstance(queue, QueueConfig):
return
task_list = []
for queue_item in queue.QueueItem.values():
for queue_item in Config.QueueConfig[task_id].QueueItem.values():
if queue_item.get("Info", "ScriptId") == "-":
continue
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
script_uid = uuid.UUID(queue_item.get("Info", "ScriptId"))
task_list.append(
{
"script_id": str(script_id),
"script_id": str(script_uid),
"status": "等待",
"name": script.get("Info", "Name"),
"name": Config.ScriptConfig[script_uid].get("Info", "Name"),
"user_list": [
{
"user_id": str(user_id),
"status": "等待",
"name": config.get("Info", "Name"),
}
for user_id, config in script.UserData.items()
for user_id, config in Config.ScriptConfig[
script_uid
].UserData.items()
if config.get("Info", "Status")
and config.get("Info", "RemainedDay") != 0
],
@@ -175,23 +167,20 @@ class _TaskManager:
elif actual_id is not None and actual_id in Config.ScriptConfig:
script = Config.ScriptConfig[actual_id]
if not isinstance(script, (MaaConfig | GeneralConfig)):
logger.error(f"不支持的脚本类型: {type(script).__name__}")
return
task_list = [
{
"script_id": str(actual_id),
"status": "等待",
"name": script.get("Info", "Name"),
"name": Config.ScriptConfig[actual_id].get("Info", "Name"),
"user_list": [
{
"user_id": str(user_id),
"status": "等待",
"name": config.get("Info", "Name"),
}
for user_id, config in script.UserData.items()
for user_id, config in Config.ScriptConfig[
actual_id
].UserData.items()
if config.get("Info", "Status")
and config.get("Info", "RemainedDay") != 0
],

View File

@@ -81,9 +81,7 @@ class _MainTimer:
for uid, queue in Config.QueueConfig.items():
if not isinstance(queue, QueueConfig) or not queue.get(
"Info", "TimeEnabled"
):
if not queue.get("Info", "TimeEnabled"):
continue
# 避免重复调起任务

View File

@@ -25,9 +25,10 @@ import json
import uuid
import win32com.client
from copy import deepcopy
from urllib.parse import urlparse
from datetime import datetime
from pathlib import Path
from typing import List, Any, Dict, Union, Optional
from typing import List, Any, Dict, Union, Optional, TypeVar, Generic, Type
from app.utils import dpapi_encrypt, dpapi_decrypt
@@ -127,17 +128,25 @@ class DateTimeValidator(ConfigValidator):
class JSONValidator(ConfigValidator):
def __init__(self, tpye: type[dict] | type[list] = dict) -> None:
self.type = tpye
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
json.loads(value)
return True
data = json.loads(value)
if isinstance(data, self.type):
return True
else:
return False
except json.JSONDecodeError:
return False
def correct(self, value: Any) -> str:
return value if self.validate(value) else "{ }"
return (
value if self.validate(value) else ("{ }" if self.type == dict else "[ ]")
)
class EncryptValidator(ConfigValidator):
@@ -246,6 +255,67 @@ class UserNameValidator(ConfigValidator):
return value
class URLValidator(ConfigValidator):
"""URL格式验证器"""
def __init__(
self,
schemes: list[str] | None = None,
require_netloc: bool = True,
default: str = "",
):
"""
:param schemes: 允许的协议列表, 若为 None 则允许任意协议
:param require_netloc: 是否要求必须包含网络位置, 如域名或IP
"""
self.schemes = [s.lower() for s in schemes] if schemes else None
self.require_netloc = require_netloc
self.default = default
def validate(self, value: Any) -> bool:
if value == self.default:
return True
if not isinstance(value, str):
return False
try:
parsed = urlparse(value)
except Exception:
return False
# 检查协议
if self.schemes is not None:
if not parsed.scheme or parsed.scheme.lower() not in self.schemes:
return False
else:
# 不限制协议仍要求有 scheme
if not parsed.scheme:
return False
# 检查是否包含网络位置
if self.require_netloc and not parsed.netloc:
return False
return True
def correct(self, value: Any) -> str:
if self.validate(value):
return value
if isinstance(value, str):
# 简单尝试:若看起来像域名,加上 https://
stripped = value.strip()
if stripped and not stripped.startswith(("http://", "https://")):
candidate = f"https://{stripped}"
if self.validate(candidate):
return candidate
return self.default
class ConfigItem:
"""配置项"""
@@ -537,7 +607,10 @@ class ConfigBase:
await item.unlock()
class MultipleConfig:
T = TypeVar("T", bound="ConfigBase")
class MultipleConfig(Generic[T]):
"""
多配置项管理类
@@ -550,7 +623,7 @@ class MultipleConfig:
子配置项的类型列表, 必须是 ConfigBase 的子类
"""
def __init__(self, sub_config_type: List[type]):
def __init__(self, sub_config_type: List[Type[T]]):
if not sub_config_type:
raise ValueError("子配置项类型列表不能为空")
@@ -561,13 +634,13 @@ class MultipleConfig:
f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
)
self.sub_config_type = sub_config_type
self.file: None | Path = None
self.sub_config_type: List[Type[T]] = sub_config_type
self.file: Path | None = None
self.order: List[uuid.UUID] = []
self.data: Dict[uuid.UUID, ConfigBase] = {}
self.data: Dict[uuid.UUID, T] = {}
self.is_locked = False
def __getitem__(self, key: uuid.UUID) -> ConfigBase:
def __getitem__(self, key: uuid.UUID) -> T:
"""允许通过 config[uuid] 访问配置项"""
if key not in self.data:
raise KeyError(f"配置项 '{key}' 不存在")
@@ -665,7 +738,9 @@ class MultipleConfig:
if self.file:
await self.save()
async def toDict(self) -> Dict[str, Union[list, dict]]:
async def toDict(
self, ignore_multi_config: bool = False, if_decrypt: bool = True
) -> Dict[str, Union[list, dict]]:
"""
将配置项转换为字典
@@ -678,7 +753,7 @@ class MultipleConfig:
]
}
for uid, config in self.items():
data[str(uid)] = await config.toDict()
data[str(uid)] = await config.toDict(ignore_multi_config, if_decrypt)
return data
async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
@@ -721,7 +796,7 @@ class MultipleConfig:
encoding="utf-8",
)
async def add(self, config_type: type) -> tuple[uuid.UUID, ConfigBase]:
async def add(self, config_type: Type[T]) -> tuple[uuid.UUID, T]:
"""
添加一个新的配置项

View File

@@ -25,6 +25,7 @@ __author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .ConfigBase import *
from .config import *
from .schema import *
__all__ = ["ConfigBase", "schema"]
__all__ = ["ConfigBase", "config", "schema"]

569
app/models/config.py Normal file
View File

@@ -0,0 +1,569 @@
# 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}
"""配置类映射表"""

View File

@@ -75,6 +75,30 @@ class GetStageIn(BaseModel):
)
class WebhookIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["Webhook"] = Field(..., description="配置类型")
class Webhook_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="Webhook名称")
Enabled: Optional[bool] = Field(default=None, description="是否启用")
class Webhook_Data(BaseModel):
url: Optional[str] = Field(default=None, description="Webhook URL")
template: Optional[str] = Field(default=None, description="消息模板")
headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
method: Optional[Literal["POST", "GET"]] = Field(
default=None, description="请求方法"
)
class Webhook(BaseModel):
Info: Optional[Webhook_Info] = Field(default=None, description="Webhook基础信息")
Data: Optional[Webhook_Data] = Field(default=None, description="Webhook配置数据")
class GlobalConfig_Function(BaseModel):
HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
None, description="历史记录保留时间, 0表示永久保存"
@@ -111,18 +135,6 @@ class GlobalConfig_UI(BaseModel):
IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘")
class CustomWebhook(BaseModel):
id: str = Field(..., description="Webhook唯一标识")
name: str = Field(..., description="Webhook名称")
url: str = Field(..., description="Webhook URL")
template: str = Field(..., description="消息模板")
enabled: bool = Field(default=True, description="是否启用")
headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
method: Optional[Literal["POST", "GET"]] = Field(
default="POST", description="请求方法"
)
class GlobalConfig_Notify(BaseModel):
SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
default=None, description="任务结果推送时机"
@@ -143,9 +155,6 @@ class GlobalConfig_Notify(BaseModel):
default=None, description="是否使用ServerChan推送"
)
ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥")
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
default=None, description="自定义Webhook列表"
)
class GlobalConfig_Update(BaseModel):
@@ -313,9 +322,6 @@ class MaaUserConfig_Notify(BaseModel):
default=None, description="是否使用Server酱推送"
)
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
CustomWebhooks: Optional[List[CustomWebhook]] = Field(
default=None, description="用户自定义Webhook列表"
)
class GeneralUserConfig_Notify(BaseModel):
@@ -618,6 +624,46 @@ class UserSetIn(UserInBase):
jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件")
class WebhookInBase(BaseModel):
scriptId: Optional[str] = Field(
default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带"
)
userId: Optional[str] = Field(
default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带"
)
class WebhookGetIn(WebhookInBase):
webhookId: Optional[str] = Field(
default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据"
)
class WebhookGetOut(OutBase):
index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表")
data: Dict[str, Webhook] = Field(
..., description="Webhook数据字典, key来自于index列表的uid"
)
class WebhookCreateOut(OutBase):
webhookId: str = Field(..., description="新创建的Webhook ID")
data: Webhook = Field(..., description="Webhook配置数据")
class WebhookUpdateIn(WebhookInBase):
webhookId: str = Field(..., description="Webhook ID")
data: Webhook = Field(..., description="Webhook更新数据")
class WebhookDeleteIn(WebhookInBase):
webhookId: str = Field(..., description="Webhook ID")
class WebhookReorderIn(WebhookInBase):
indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列")
class PlanCreateIn(BaseModel):
type: Literal["MaaPlan"]

View File

@@ -21,17 +21,20 @@
import re
import json
import smtplib
import requests
from datetime import datetime
from plyer import notification
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from pathlib import Path
from plyer import notification
from typing import Literal
from app.core import Config
from app.models.config import Webhook
from app.utils import get_logger, ImageUtils
logger = get_logger("通知服务")
@@ -39,71 +42,76 @@ logger = get_logger("通知服务")
class Notification:
def __init__(self):
super().__init__()
async def push_plyer(self, title, message, ticker, t) -> bool:
async def push_plyer(self, title: str, message: str, ticker: str, t: int) -> None:
"""
推送系统通知
:param title: 通知标题
:param message: 通知内容
:param ticker: 通知横幅
:param t: 通知持续时间
:return: bool
Parameters
----------
title: str
通知标题
message: str
通知内容
ticker: str
通知横幅
t: int
通知持续时间
"""
if Config.get("Notify", "IfPushPlyer"):
if not Config.get("Notify", "IfPushPlyer"):
return
logger.info(f"推送系统通知: {title}")
logger.info(f"推送系统通知: {title}")
if notification.notify is not None:
notification.notify(
title=title,
message=message,
app_name="AUTO-MAS",
app_icon=(Path.cwd() / "res/icons/AUTO-MAS.ico").as_posix(),
timeout=t,
ticker=ticker,
toast=True,
)
else:
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
if notification.notify is not None:
notification.notify(
title=title,
message=message,
app_name="AUTO-MAS",
app_icon=(Path.cwd() / "res/icons/AUTO-MAS.ico").as_posix(),
timeout=t,
ticker=ticker,
toast=True,
)
else:
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
return True
async def send_mail(self, mode, title, content, to_address) -> None:
async def send_mail(
self, mode: Literal["文本", "网页"], title: str, content: str, to_address: str
) -> None:
"""
推送邮件通知
:param mode: 邮件内容模式, 支持 "文本""网页"
:param title: 邮件标题
:param content: 邮件内容
:param to_address: 收件人地址
Parameters
----------
mode: Literal["文本", "网页"]
邮件内容模式, 支持 "文本""网页"
title: str
邮件标题
content: str
邮件内容
to_address: str
收件人地址
"""
if (
Config.get("Notify", "SMTPServerAddress") == ""
or Config.get("Notify", "AuthorizationCode") == ""
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.get("Notify", "FromAddress"),
)
)
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
to_address,
)
if Config.get("Notify", "SMTPServerAddress") == "":
raise ValueError("邮件通知的SMTP服务器地址不能为空")
if Config.get("Notify", "AuthorizationCode") == "":
raise ValueError("邮件通知的授权码不能为空")
if not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.get("Notify", "FromAddress"),
)
):
logger.error(
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
)
raise ValueError(
"邮件通知的SMTP服务器地址、授权码、发件人地址或收件人地址未正确配置"
raise ValueError("邮件通知的发送邮箱格式错误或为空")
if not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
to_address,
)
):
raise ValueError("邮件通知的接收邮箱格式错误或为空")
# 定义邮件正文
if mode == "文本":
@@ -135,16 +143,21 @@ class Notification:
smtpObj.quit()
logger.success(f"邮件发送成功: {title}")
async def ServerChanPush(self, title, content, send_key) -> None:
async def ServerChanPush(self, title: str, content: str, send_key: str) -> None:
"""
使用Server酱推送通知
:param title: 通知标题
:param content: 通知内容
:param send_key: Server酱的SendKey
Parameters
----------
title: str
通知标题
content: str
通知内容
send_key: str
Server酱的SendKey
"""
if not send_key:
if send_key == "":
raise ValueError("ServerChan SendKey 不能为空")
# 构造 URL
@@ -171,33 +184,33 @@ class Notification:
else:
raise Exception(f"ServerChan 推送通知失败: {response.text}")
async def CustomWebhookPush(self, title, content, webhook_config) -> None:
async def WebhookPush(self, title: str, content: str, webhook: Webhook) -> None:
"""
自定义 Webhook 推送通知
Webhook 推送通知
:param title: 通知标题
:param content: 通知内容
:param webhook_config: Webhook配置对象
Parameters
----------
title: str
通知标题
content: str
通知内容
webhook: Webhook
Webhook配置对象
"""
if not webhook_config.get("url"):
raise ValueError("Webhook URL 不能为空")
if not webhook_config.get("enabled", True):
logger.info(
f"Webhook {webhook_config.get('name', 'Unknown')} 已禁用,跳过推送"
)
if not webhook.get("Info", "Enabled"):
return
if webhook.get("Data", "Url") == "":
raise ValueError("Webhook URL 不能为空")
# 解析模板
template = webhook_config.get(
"template", '{"title": "{title}", "content": "{content}"}'
template = (
webhook.get("Data", "Template")
or '{"title": "{title}", "content": "{content}"}'
)
# 替换模板变量
try:
import json
from datetime import datetime
# 准备模板变量
template_vars = {
@@ -264,56 +277,55 @@ class Notification:
# 准备请求头
headers = {"Content-Type": "application/json"}
if webhook_config.get("headers"):
headers.update(webhook_config["headers"])
headers.update(json.loads(webhook.get("Data", "Headers")))
# 发送请求
method = webhook_config.get("method", "POST").upper()
try:
if method == "POST":
if isinstance(data, dict):
response = requests.post(
url=webhook_config["url"],
json=data,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
else:
response = requests.post(
url=webhook_config["url"],
data=data,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
else: # GET
params = data if isinstance(data, dict) else {"message": data}
response = requests.get(
url=webhook_config["url"],
params=params,
if webhook.get("Data", "Method") == "POST":
if isinstance(data, dict):
response = requests.post(
url=webhook.get("Data", "Url"),
json=data,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
# 检查响应
if response.status_code == 200:
logger.success(
f"自定义Webhook推送成功: {webhook_config.get('name', 'Unknown')} - {title}"
elif isinstance(data, str):
response = requests.post(
url=webhook.get("Data", "Url"),
data=data,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
elif webhook.get("Data", "Method") == "GET":
if isinstance(data, dict):
# Flatten params to ensure all values are str or list of str
params = {}
for k, v in data.items():
if isinstance(v, (dict, list)):
params[k] = json.dumps(v, ensure_ascii=False)
else:
params[k] = str(v)
else:
raise Exception(f"HTTP {response.status_code}: {response.text}")
except Exception as e:
raise Exception(
f"自定义Webhook推送失败 ({webhook_config.get('name', 'Unknown')}): {str(e)}"
params = {"message": str(data)}
response = requests.get(
url=webhook.get("Data", "Url"),
params=params,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
async def WebHookPush(self, title, content, webhook_url) -> None:
# 检查响应
if response.status_code == 200:
logger.success(
f"自定义Webhook推送成功: {webhook.get('Info', 'Name')} - {title}"
)
else:
raise Exception(f"HTTP {response.status_code}: {response.text}")
async def _WebHookPush(self, title, content, webhook_url) -> None:
"""
WebHook 推送通知 (兼容旧版企业微信格式)
WebHook 推送通知 (即将弃用)
:param title: 通知标题
:param content: 通知内容
@@ -340,7 +352,7 @@ class Notification:
self, image_path: Path, webhook_url: str
) -> None:
"""
使用企业微信群机器人推送图片通知
使用企业微信群机器人推送图片通知(等待重新适配)
:param image_path: 图片文件路径
:param webhook_url: 企业微信群机器人的WebHook地址
@@ -406,23 +418,12 @@ class Notification:
)
# 发送自定义Webhook通知
try:
custom_webhooks = Config.get("Notify", "CustomWebhooks")
except AttributeError:
custom_webhooks = []
if custom_webhooks:
for webhook in custom_webhooks:
if webhook.get("enabled", True):
try:
await self.CustomWebhookPush(
"AUTO-MAS测试通知",
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
webhook,
)
except Exception as e:
logger.error(
f"自定义Webhook测试失败 ({webhook.get('name', 'Unknown')}): {e}"
)
for webhook in Config.Notify_CustomWebhooks.values():
await self.WebhookPush(
"AUTO-MAS测试通知",
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
webhook,
)
logger.success("测试通知发送完成")

File diff suppressed because it is too large Load Diff

View File

@@ -27,10 +27,9 @@ import shutil
import asyncio
import subprocess
from pathlib import Path
from fastapi import WebSocket
from datetime import datetime, timedelta
from jinja2 import Environment, FileSystemLoader
from typing import Union, List, Dict, Optional
from typing import List, Dict, Optional
from app.core import Config, GeneralConfig, GeneralUserConfig
@@ -44,7 +43,7 @@ logger = get_logger("通用调度器")
class GeneralManager:
"""通用脚本通用控制"""
"""通用脚本调度"""
def __init__(
self, mode: str, script_id: uuid.UUID, user_id: Optional[uuid.UUID], ws_id: str
@@ -69,9 +68,8 @@ class GeneralManager:
await Config.ScriptConfig[self.script_id].lock()
self.script_config = Config.ScriptConfig[self.script_id]
if isinstance(self.script_config, GeneralConfig):
self.user_config = MultipleConfig([GeneralUserConfig])
await self.user_config.load(await self.script_config.UserData.toDict())
self.user_config = MultipleConfig([GeneralUserConfig])
await self.user_config.load(await self.script_config.UserData.toDict())
self.script_root_path = Path(self.script_config.get("Info", "RootPath"))
self.script_path = Path(self.script_config.get("Script", "ScriptPath"))
@@ -143,6 +141,9 @@ class GeneralManager:
def check_config(self) -> str:
"""检查配置是否可用"""
if not isinstance(Config.ScriptConfig[self.script_id], GeneralConfig):
return "脚本配置类型错误, 不是通用脚本类型"
if self.mode == "人工排查":
return "通用脚本不支持人工排查模式"
if self.mode == "设置脚本" and self.user_id is None:
@@ -221,298 +222,319 @@ class GeneralManager:
# 开始代理
for self.index, user in enumerate(self.user_list):
self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
try:
if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
self.cur_user_data.get("Data", "ProxyTimes")
< self.script_config.get("Run", "ProxyTimesLimit")
):
user["status"] = "运行"
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={"user_list": self.user_list},
).model_dump()
)
else:
user["status"] = "跳过"
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={"user_list": self.user_list},
).model_dump()
)
continue
self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
logger.info(f"开始代理用户: {user['user_id']}")
if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
self.cur_user_data.get("Data", "ProxyTimes")
< self.script_config.get("Run", "ProxyTimesLimit")
):
user["status"] = "运行"
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={"user_list": self.user_list},
).model_dump()
)
else:
user["status"] = "跳过"
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={"user_list": self.user_list},
).model_dump()
)
continue
self.user_start_time = datetime.now()
logger.info(f"开始代理用户: {user['user_id']}")
self.run_book = False
self.user_start_time = datetime.now()
if not (
Path.cwd() / f"data/{self.script_id}/{user['user_id']}/ConfigFile"
).exists():
self.run_book = False
logger.error(f"用户: {user['user_id']} - 未找到配置文件")
if not (
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
).exists():
logger.error(f"用户: {user['user_id']} - 未找到配置文件")
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Info",
data={"Error": f"未找到 {user['user_id']} 的配置文件"},
).model_dump()
)
self.run_book = False
continue
# 尝试次数循环
for i in range(self.script_config.get("Run", "RunTimesLimit")):
if self.run_book:
break
logger.info(
f"用户 {user['user_id']} - 尝试次数: {i + 1}/{self.script_config.get('Run','RunTimesLimit')}"
)
# 配置脚本
await self.set_general()
# 记录当前时间
self.log_start_time = datetime.now()
# 执行任务前脚本
if (
self.cur_user_data.get("Info", "IfScriptBeforeTask")
and Path(
self.cur_user_data.get("Info", "ScriptBeforeTask")
).exists()
):
await self.execute_script_task(
Path(
self.cur_user_data.get("Info", "ScriptBeforeTask")
),
"脚本前任务",
)
# 启动游戏/模拟器
if self.script_config.get("Game", "Enabled"):
try:
logger.info(
f"启动游戏/模拟器: {self.game_path}, 参数: {self.script_config.get('Game','Arguments')}"
)
await self.game_process_manager.open_process(
self.game_path,
str(
self.script_config.get("Game", "Arguments")
).split(" "),
0,
)
except Exception as e:
logger.exception(f"启动游戏/模拟器时出现异常: {e}")
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Info",
data={
"Error": f"启动游戏/模拟器时出现异常: {e}"
},
).model_dump()
)
self.general_result = "游戏/模拟器启动失败"
break
# 更新静默进程标记有效时间
if self.script_config.get("Game", "Type") == "Emulator":
logger.info(
f"更新静默进程标记: {self.game_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.script_config.get('Game', 'WaitTime') + 10)}"
)
Config.silence_dict[
self.game_path
] = datetime.now() + timedelta(
seconds=self.script_config.get("Game", "WaitTime")
+ 10
)
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={
"log": f"正在等待游戏/模拟器完成启动\n请等待{self.script_config.get('Game', 'WaitTime')}s"
},
).model_dump()
)
await asyncio.sleep(
self.script_config.get("Game", "WaitTime")
)
# 运行脚本任务
logger.info(
f"运行脚本任务: {self.script_exe_path}, 参数: {self.script_arguments}"
)
await self.general_process_manager.open_process(
self.script_exe_path,
self.script_arguments,
tracking_time=(
60
if self.script_config.get("Script", "IfTrackProcess")
else 0
),
)
# 监测运行状态
await self.general_log_monitor.start(
self.script_log_path, self.log_start_time
)
self.wait_event.clear()
await self.wait_event.wait()
await self.general_log_monitor.stop()
# 处理通用脚本结果
if self.general_result == "Success!":
# 标记任务完成
self.run_book = True
logger.info(
f"用户: {user['user_id']} - 通用脚本进程完成代理任务"
)
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={
"log": "检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
},
).model_dump()
)
# 中止相关程序
logger.info(f"中止相关程序: {self.script_exe_path}")
await self.general_process_manager.kill()
await System.kill_process(self.script_exe_path)
if self.script_config.get("Game", "Enabled"):
logger.info(
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
)
await self.game_process_manager.kill()
if self.script_config.get("Game", "IfForceClose"):
await System.kill_process(self.game_path)
await asyncio.sleep(10)
# 更新脚本配置文件
if self.script_config.get("Script", "UpdateConfigMode") in [
"Success",
"Always",
]:
if (
self.script_config.get("Script", "ConfigPathMode")
== "Folder"
):
shutil.copytree(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
dirs_exist_ok=True,
)
elif (
self.script_config.get("Script", "ConfigPathMode")
== "File"
):
shutil.copy(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
/ self.script_config_path.name,
)
logger.success("通用脚本配置文件已更新")
else:
logger.error(
f"配置: {user['user_id']} - 代理任务异常: {self.general_result}"
)
# 打印中止信息
# 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={
"log": f"{self.general_result}\n正在中止相关程序\n请等待10s"
},
).model_dump()
)
# 中止相关程序
logger.info(f"中止相关程序: {self.script_exe_path}")
await self.general_process_manager.kill()
await System.kill_process(self.script_exe_path)
if self.script_config.get("Game", "Enabled"):
logger.info(
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
)
await self.game_process_manager.kill()
if self.script_config.get("Game", "IfForceClose"):
await System.kill_process(self.game_path)
# 推送异常通知
await Notify.push_plyer(
"用户自动代理出现异常!",
f"用户 {user['name']} 的自动代理出现一次异常",
f"{user['name']} 的自动代理出现异常",
3,
)
await asyncio.sleep(10)
# 更新脚本配置文件
if self.script_config.get("Script", "UpdateConfigMode") in [
"Failure",
"Always",
]:
if (
self.script_config.get("Script", "ConfigPathMode")
== "Folder"
):
shutil.copytree(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
dirs_exist_ok=True,
)
elif (
self.script_config.get("Script", "ConfigPathMode")
== "File"
):
shutil.copy(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
/ self.script_config_path.name,
)
logger.success("通用脚本配置文件已更新")
# 执行任务后脚本
if (
self.cur_user_data.get("Info", "IfScriptAfterTask")
and Path(
self.cur_user_data.get("Info", "ScriptAfterTask")
).exists()
):
await self.execute_script_task(
Path(self.cur_user_data.get("Info", "ScriptAfterTask")),
"脚本后任务",
)
# 保存运行日志以及统计信息
await Config.save_general_log(
Path.cwd()
/ f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
self.general_logs,
self.general_result,
)
await self.result_record()
except Exception as e:
logger.exception(f"用户 {user['user_id']} 代理时出现异常: {e}")
user["status"] = "异常"
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Info",
data={"Error": f"未找到 {user['user_id']} 的配置文件"},
data={"Error": f"代理用户 {user['name']} 时出现异常: {e}"},
).model_dump()
)
self.run_book = False
continue
# 尝试次数循环
for i in range(self.script_config.get("Run", "RunTimesLimit")):
if self.run_book:
break
logger.info(
f"用户 {user['user_id']} - 尝试次数: {i + 1}/{self.script_config.get('Run','RunTimesLimit')}"
)
# 配置脚本
await self.set_general()
# 记录当前时间
self.log_start_time = datetime.now()
# 执行任务前脚本
if (
self.cur_user_data.get("Info", "IfScriptBeforeTask")
and Path(
self.cur_user_data.get("Info", "ScriptBeforeTask")
).exists()
):
await self.execute_script_task(
Path(self.cur_user_data.get("Info", "ScriptBeforeTask")),
"脚本前任务",
)
# 启动游戏/模拟器
if self.script_config.get("Game", "Enabled"):
try:
logger.info(
f"启动游戏/模拟器: {self.game_path}, 参数: {self.script_config.get('Game','Arguments')}"
)
await self.game_process_manager.open_process(
self.game_path,
str(self.script_config.get("Game", "Arguments")).split(
" "
),
0,
)
except Exception as e:
logger.exception(f"启动游戏/模拟器时出现异常: {e}")
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Info",
data={"Error": f"启动游戏/模拟器时出现异常: {e}"},
).model_dump()
)
self.general_result = "游戏/模拟器启动失败"
break
# 更新静默进程标记有效时间
if self.script_config.get("Game", "Type") == "Emulator":
logger.info(
f"更新静默进程标记: {self.game_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.script_config.get('Game', 'WaitTime') + 10)}"
)
Config.silence_dict[
self.game_path
] = datetime.now() + timedelta(
seconds=self.script_config.get("Game", "WaitTime") + 10
)
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={
"log": f"正在等待游戏/模拟器完成启动\n请等待{self.script_config.get('Game', 'WaitTime')}s"
},
).model_dump()
)
await asyncio.sleep(self.script_config.get("Game", "WaitTime"))
# 运行脚本任务
logger.info(
f"运行脚本任务: {self.script_exe_path}, 参数: {self.script_arguments}"
)
await self.general_process_manager.open_process(
self.script_exe_path,
self.script_arguments,
tracking_time=(
60
if self.script_config.get("Script", "IfTrackProcess")
else 0
),
)
# 监测运行状态
await self.general_log_monitor.start(
self.script_log_path, self.log_start_time
)
self.wait_event.clear()
await self.wait_event.wait()
await self.general_log_monitor.stop()
# 处理通用脚本结果
if self.general_result == "Success!":
# 标记任务完成
self.run_book = True
logger.info(
f"用户: {user['user_id']} - 通用脚本进程完成代理任务"
)
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={
"log": "检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
},
).model_dump()
)
# 中止相关程序
logger.info(f"中止相关程序: {self.script_exe_path}")
await self.general_process_manager.kill()
await System.kill_process(self.script_exe_path)
if self.script_config.get("Game", "Enabled"):
logger.info(
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
)
await self.game_process_manager.kill()
if self.script_config.get("Game", "IfForceClose"):
await System.kill_process(self.game_path)
await asyncio.sleep(10)
# 更新脚本配置文件
if self.script_config.get("Script", "UpdateConfigMode") in [
"Success",
"Always",
]:
if (
self.script_config.get("Script", "ConfigPathMode")
== "Folder"
):
shutil.copytree(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
dirs_exist_ok=True,
)
elif (
self.script_config.get("Script", "ConfigPathMode")
== "File"
):
shutil.copy(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
/ self.script_config_path.name,
)
logger.success("通用脚本配置文件已更新")
else:
logger.error(
f"配置: {user['user_id']} - 代理任务异常: {self.general_result}"
)
# 打印中止信息
# 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Update",
data={
"log": f"{self.general_result}\n正在中止相关程序\n请等待10s"
},
).model_dump()
)
# 中止相关程序
logger.info(f"中止相关程序: {self.script_exe_path}")
await self.general_process_manager.kill()
await System.kill_process(self.script_exe_path)
if self.script_config.get("Game", "Enabled"):
logger.info(
f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
)
await self.game_process_manager.kill()
if self.script_config.get("Game", "IfForceClose"):
await System.kill_process(self.game_path)
# 推送异常通知
await Notify.push_plyer(
"用户自动代理出现异常!",
f"用户 {user['name']} 的自动代理出现一次异常",
f"{user['name']} 的自动代理出现异常",
3,
)
await asyncio.sleep(10)
# 更新脚本配置文件
if self.script_config.get("Script", "UpdateConfigMode") in [
"Failure",
"Always",
]:
if (
self.script_config.get("Script", "ConfigPathMode")
== "Folder"
):
shutil.copytree(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile",
dirs_exist_ok=True,
)
elif (
self.script_config.get("Script", "ConfigPathMode")
== "File"
):
shutil.copy(
self.script_config_path,
Path.cwd()
/ f"data/{self.script_id}/{user['user_id']}/ConfigFile"
/ self.script_config_path.name,
)
logger.success("通用脚本配置文件已更新")
# 执行任务后脚本
if (
self.cur_user_data.get("Info", "IfScriptAfterTask")
and Path(
self.cur_user_data.get("Info", "ScriptAfterTask")
).exists()
):
await self.execute_script_task(
Path(self.cur_user_data.get("Info", "ScriptAfterTask")),
"脚本后任务",
)
# 保存运行日志以及统计信息
await Config.save_general_log(
Path.cwd()
/ f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
self.general_logs,
self.general_result,
)
await self.result_record()
# 设置通用脚本模式
elif self.mode == "设置脚本":
@@ -545,11 +567,21 @@ class GeneralManager:
"end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"user_result": "代理成功" if self.run_book else self.general_result,
}
await self.push_notification(
"统计信息",
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
statistics,
)
try:
await self.push_notification(
"统计信息",
f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
statistics,
)
except Exception as e:
logger.exception(f"推送统计信息失败: {e}")
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Info",
data={"Error": f"推送统计信息失败: {e}"},
).model_dump()
)
if self.run_book:
# 成功完成代理的用户修改相关参数
@@ -659,10 +691,10 @@ class GeneralManager:
if self.mode == "自动代理":
# 更新用户数据
sc = Config.ScriptConfig[self.script_id]
if isinstance(sc, GeneralConfig):
await sc.UserData.load(await self.user_config.toDict())
await Config.ScriptConfig.save()
await Config.ScriptConfig[self.script_id].UserData.load(
await self.user_config.toDict()
)
await Config.ScriptConfig.save()
error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
over_user = [_["name"] for _ in self.user_list if _["status"] == "完成"]
@@ -708,7 +740,17 @@ class GeneralManager:
f"已完成配置数: {len(over_user)}, 未完成配置数: {len(error_user) + len(wait_user)}",
10,
)
await self.push_notification("代理结果", title, result)
try:
await self.push_notification("代理结果", title, result)
except Exception as e:
logger.exception(f"推送代理结果失败: {e}")
await Config.send_json(
WebSocketMessage(
id=self.ws_id,
type="Info",
data={"Error": f"推送代理结果失败: {e}"},
).model_dump()
)
elif self.mode == "设置脚本":
@@ -970,7 +1012,6 @@ class GeneralManager:
serverchan_message = message_text.replace("\n", "\n\n")
# 发送全局通知
if Config.get("Notify", "IfSendMail"):
await Notify.send_mail(
"网页", title, message_html, Config.get("Notify", "ToAddress")
@@ -984,21 +1025,10 @@ class GeneralManager:
)
# 发送自定义Webhook通知
try:
custom_webhooks = Config.get("Notify", "CustomWebhooks")
except AttributeError:
custom_webhooks = []
if custom_webhooks:
for webhook in custom_webhooks:
if webhook.get("enabled", True):
try:
await Notify.CustomWebhookPush(
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
)
except Exception as e:
logger.error(
f"自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
)
for webhook in Config.Notify_CustomWebhooks.values():
await Notify.WebhookPush(
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
)
elif mode == "统计信息":
@@ -1031,21 +1061,10 @@ class GeneralManager:
)
# 发送自定义Webhook通知
try:
custom_webhooks = Config.get("Notify", "CustomWebhooks")
except AttributeError:
custom_webhooks = []
if custom_webhooks:
for webhook in custom_webhooks:
if webhook.get("enabled", True):
try:
await Notify.CustomWebhookPush(
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
)
except Exception as e:
logger.error(
f"自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
)
for webhook in Config.Notify_CustomWebhooks.values():
await Notify.WebhookPush(
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
)
# 发送用户单独通知
if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
@@ -1078,22 +1097,7 @@ class GeneralManager:
)
# 推送CompanyWebHookBot通知
# 发送用户自定义Webhook通知
user_webhooks = self.cur_user_data.get("Notify", "CustomWebhooks")
if user_webhooks:
for webhook in user_webhooks:
if webhook.get("enabled", True):
try:
await Notify.CustomWebhookPush(
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
)
except Exception as e:
logger.error(
f"用户自定义Webhook推送失败 ({webhook.get('name', 'Unknown')}): {e}"
)
else:
logger.error(
"用户CompanyWebHookBot密钥为空, 无法发送用户单独的CompanyWebHookBot通知"
)
return None
for webhook in self.cur_user_data.Notify_CustomWebhooks.values():
await Notify.WebhookPush(
title, f"{message_text}\n\nAUTO-MAS 敬上", webhook
)

View File

@@ -23,6 +23,10 @@
from datetime import datetime
TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
"""配置类型映射表"""
RESOURCE_STAGE_INFO = [
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},