From e286fc8d550e7871e6d922b6e9079eae469fce32 Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Wed, 1 Oct 2025 11:05:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=87=AA=E5=AE=9A=E4=B9=89webhook=E9=80=82?= =?UTF-8?q?=E9=85=8D=EF=BC=9B=E9=87=8D=E6=9E=84=E9=85=8D=E7=BD=AE=E9=A1=B9?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/setting.py | 168 ++--- app/core/config.py | 922 +++++++----------------- app/core/task_manager.py | 37 +- app/core/timer.py | 4 +- app/models/ConfigBase.py | 101 ++- app/models/__init__.py | 3 +- app/models/config.py | 569 +++++++++++++++ app/models/schema.py | 82 ++- app/services/notification.py | 263 +++---- app/task/MAA.py | 1308 +++++++++++++++++----------------- app/task/general.py | 696 +++++++++--------- app/utils/constants.py | 4 + 12 files changed, 2181 insertions(+), 1976 deletions(-) create mode 100644 app/models/config.py diff --git a/app/api/setting.py b/app/api/setting.py index 8b177ce..e0014bf 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -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( diff --git a/app/core/config.py b/app/core/config.py index b36de0c..1379ca4 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -35,7 +35,7 @@ from collections import defaultdict from datetime import datetime, timedelta, date from typing import Literal, Optional -from app.models.ConfigBase import * +from app.models.config import * from app.utils.constants import * from app.utils import get_logger @@ -52,565 +52,6 @@ except ImportError: Repo = None -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 = ConfigItem("Notify", "CustomWebhooks", []) - - 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 = ConfigItem("Notify", "CustomWebhooks", []) - - def get_plan_info(self) -> Dict[str, Union[str, int]]: - """获取当前的计划下信息""" - - if self.get("Info", "StageMode") == "Fixed": - return { - "MedicineNumb": self.get("Info", "MedicineNumb"), - "SeriesNumb": self.get("Info", "SeriesNumb"), - "Stage": self.get("Info", "Stage"), - "Stage_1": self.get("Info", "Stage_1"), - "Stage_2": self.get("Info", "Stage_2"), - "Stage_3": self.get("Info", "Stage_3"), - "Stage_Remain": self.get("Info", "Stage_Remain"), - } - else: - plan = Config.PlanConfig[uuid.UUID(self.get("Info", "StageMode"))] - if isinstance(plan, MaaPlanConfig): - return { - "MedicineNumb": plan.get_current_info("MedicineNumb").getValue(), - "SeriesNumb": plan.get_current_info("SeriesNumb").getValue(), - "Stage": plan.get_current_info("Stage").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(), - } - else: - raise ValueError("不存在的计划表配置") - - -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 = ConfigItem("Notify", "CustomWebhooks", []) - - -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} -TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"} - - class AppConfig(GlobalConfig): VERSION = [5, 0, 0, 1] @@ -995,7 +436,7 @@ class AppConfig(GlobalConfig): async def add_script( self, script: Literal["MAA", "General"] - ) -> tuple[uuid.UUID, ConfigBase]: + ) -> tuple[uuid.UUID, Union[MaaConfig, GeneralConfig]]: """添加脚本配置""" logger.info(f"添加脚本配置: {script}") @@ -1008,12 +449,13 @@ class AppConfig(GlobalConfig): logger.info(f"获取脚本配置: {script_id}") if script_id is None: + # 获取所有脚本配置 data = await self.ScriptConfig.toDict() else: + # 获取指定脚本配置 data = await self.ScriptConfig.get(uuid.UUID(script_id)) index = data.pop("instances", []) - return list(index), data async def update_script( @@ -1045,11 +487,11 @@ class AppConfig(GlobalConfig): if uid in self.task_dict: raise RuntimeError(f"脚本 {script_id} 正在运行, 无法删除") + # 删除脚本相关的队列项 for queue in self.QueueConfig.values(): - if isinstance(queue, QueueConfig): - for key, value in queue.QueueItem.items(): - if value.get("Info", "ScriptId") == str(uid): - await queue.QueueItem.remove(key) + for key, value in queue.QueueItem.items(): + if value.get("Info", "ScriptId") == str(uid): + await queue.QueueItem.remove(key) await self.ScriptConfig.remove(uid) if (Path.cwd() / f"data/{uid}").exists(): @@ -1220,28 +662,27 @@ class AppConfig(GlobalConfig): logger.info(f"获取用户配置: {script_id} - {user_id}") uid = uuid.UUID(script_id) - sc = self.ScriptConfig[uid] - if isinstance(sc, (MaaConfig | GeneralConfig)): - if user_id is None: - data = await sc.UserData.toDict() - else: - data = await sc.UserData.get(uuid.UUID(user_id)) + if user_id is None: + # 获取全部用户配置 + data = await self.ScriptConfig[uid].UserData.toDict() else: - logger.error(f"不支持的脚本配置类型: {type(sc)}") - raise TypeError(f"不支持的脚本配置类型: {type(sc)}") + # 获取指定用户配置 + data = await self.ScriptConfig[uid].UserData.get(uuid.UUID(user_id)) index = data.pop("instances", []) - return list(index), data - async def add_user(self, script_id: str) -> tuple[uuid.UUID, ConfigBase]: + async def add_user( + self, script_id: str + ) -> tuple[uuid.UUID, Union[MaaUserConfig, GeneralUserConfig]]: """添加用户配置""" logger.info(f"{script_id} 添加用户配置") script_config = self.ScriptConfig[uuid.UUID(script_id)] + # 根据脚本类型选择添加对应用户配置 if isinstance(script_config, MaaConfig): uid, config = await script_config.UserData.add(MaaUserConfig) elif isinstance(script_config, GeneralConfig): @@ -1259,14 +700,15 @@ class AppConfig(GlobalConfig): logger.info(f"{script_id} 更新用户配置: {user_id}") - script_config = self.ScriptConfig[uuid.UUID(script_id)] - uid = uuid.UUID(user_id) + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) for group, items in data.items(): for name, value in items.items(): logger.debug(f"更新脚本配置: {script_id} - {group}.{name} = {value}") - if isinstance(script_config, (MaaConfig | GeneralConfig)): - await script_config.UserData[uid].set(group, name, value) + await self.ScriptConfig[script_uid].UserData[user_uid].set( + group, name, value + ) await self.ScriptConfig.save() @@ -1275,25 +717,25 @@ class AppConfig(GlobalConfig): logger.info(f"{script_id} 删除用户配置: {user_id}") - script_config = self.ScriptConfig[uuid.UUID(script_id)] - uid = uuid.UUID(user_id) + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) - if isinstance(script_config, (MaaConfig | GeneralConfig)): - await script_config.UserData.remove(uid) - await self.ScriptConfig.save() - if (Path.cwd() / f"data/{script_id}/{user_id}").exists(): - shutil.rmtree(Path.cwd() / f"data/{script_id}/{user_id}") + await self.ScriptConfig[script_uid].UserData.remove(user_uid) + await self.ScriptConfig.save() + if (Path.cwd() / f"data/{script_id}/{user_id}").exists(): + shutil.rmtree(Path.cwd() / f"data/{script_id}/{user_id}") async def reorder_user(self, script_id: str, index_list: list[str]) -> None: """重新排序用户""" logger.info(f"{script_id} 重新排序用户: {index_list}") - script_config = self.ScriptConfig[uuid.UUID(script_id)] + script_uid = uuid.UUID(script_id) - if isinstance(script_config, (MaaConfig | GeneralConfig)): - await script_config.UserData.setOrder([uuid.UUID(_) for _ in index_list]) - await self.ScriptConfig.save() + await self.ScriptConfig[script_uid].UserData.setOrder( + list(map(uuid.UUID, index_list)) + ) + await self.ScriptConfig.save() async def set_infrastructure( self, script_id: str, user_id: str, jsonFile: str @@ -1301,13 +743,16 @@ class AppConfig(GlobalConfig): logger.info(f"{script_id} - {user_id} 设置基建配置: {jsonFile}") - script_config = self.ScriptConfig[uuid.UUID(script_id)] - uid = uuid.UUID(user_id) + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) json_path = Path(jsonFile) if not json_path.exists(): raise FileNotFoundError(f"文件未找到: {json_path}") + if not isinstance(self.ScriptConfig[script_uid], MaaConfig): + raise TypeError(f"脚本 {script_id} 不是 MAA 脚本, 无法设置基建配置") + (Path.cwd() / f"data/{script_id}/{user_id}/Infrastructure").mkdir( parents=True, exist_ok=True ) @@ -1316,13 +761,13 @@ class AppConfig(GlobalConfig): Path.cwd() / f"data/{script_id}/{user_id}/Infrastructure/infrastructure.json", ) - - if isinstance(script_config, MaaConfig): - await script_config.UserData[uid].set("Info", "InfrastPath", str(json_path)) + await self.ScriptConfig[script_uid].UserData[user_uid].set( + "Info", "InfrastPath", str(json_path) + ) async def add_plan( self, script: Literal["MaaPlan"] - ) -> tuple[uuid.UUID, ConfigBase]: + ) -> tuple[uuid.UUID, MaaPlanConfig]: """添加计划表""" logger.info(f"添加计划表: {script}") @@ -1340,7 +785,6 @@ class AppConfig(GlobalConfig): data = await self.PlanConfig.get(uuid.UUID(plan_id)) index = data.pop("instances", []) - return list(index), data async def update_plan(self, plan_id: str, data: Dict[str, Dict[str, Any]]) -> None: @@ -1348,12 +792,12 @@ class AppConfig(GlobalConfig): logger.info(f"更新计划表配置: {plan_id}") - uid = uuid.UUID(plan_id) + plan_uid = uuid.UUID(plan_id) for group, items in data.items(): for name, value in items.items(): logger.debug(f"更新计划表配置: {plan_id} - {group}.{name} = {value}") - await self.PlanConfig[uid].set(group, name, value) + await self.PlanConfig[plan_uid].set(group, name, value) await self.PlanConfig.save() @@ -1362,14 +806,14 @@ class AppConfig(GlobalConfig): logger.info(f"删除计划表配置: {plan_id}") - uid = uuid.UUID(plan_id) + plan_uid = uuid.UUID(plan_id) user_list = [] for script in self.ScriptConfig.values(): if isinstance(script, MaaConfig): for user in script.UserData.values(): - if user.get("Info", "StageMode") == str(uid): + if user.get("Info", "StageMode") == str(plan_uid): if user.is_locked: raise RuntimeError( f"用户 {user.get('Info','Name')} 正在使用此计划表且被锁定, 无法完成删除" @@ -1379,16 +823,16 @@ class AppConfig(GlobalConfig): for user in user_list: await user.set("Info", "StageMode", "Fixed") - await self.PlanConfig.remove(uid) + await self.PlanConfig.remove(plan_uid) async def reorder_plan(self, index_list: list[str]) -> None: """重新排序计划表""" logger.info(f"重新排序计划表: {index_list}") - await self.PlanConfig.setOrder([uuid.UUID(_) for _ in index_list]) + await self.PlanConfig.setOrder(list(map(uuid.UUID, index_list))) - async def add_queue(self) -> tuple[uuid.UUID, ConfigBase]: + async def add_queue(self) -> tuple[uuid.UUID, QueueConfig]: """添加调度队列""" logger.info("添加调度队列") @@ -1406,7 +850,6 @@ class AppConfig(GlobalConfig): data = await self.QueueConfig.get(uuid.UUID(queue_id)) index = data.pop("instances", []) - return list(index), data async def update_queue( @@ -1416,12 +859,12 @@ class AppConfig(GlobalConfig): logger.info(f"更新调度队列配置: {queue_id}") - uid = uuid.UUID(queue_id) + queue_uid = uuid.UUID(queue_id) for group, items in data.items(): for name, value in items.items(): logger.debug(f"更新调度队列配置: {queue_id} - {group}.{name} = {value}") - await self.QueueConfig[uid].set(group, name, value) + await self.QueueConfig[queue_uid].set(group, name, value) await self.QueueConfig.save() @@ -1437,42 +880,32 @@ class AppConfig(GlobalConfig): logger.info(f"重新排序调度队列: {index_list}") - await self.QueueConfig.setOrder([uuid.UUID(_) for _ in index_list]) + await self.QueueConfig.setOrder(list(map(uuid.UUID, index_list))) async def get_time_set( self, queue_id: str, time_set_id: Optional[str] ) -> tuple[list, dict]: """获取时间设置配置""" - logger.info(f"Get time set of queue: {queue_id} - {time_set_id}") + logger.info(f"获取队列的时间配置: {queue_id} - {time_set_id}") - uid = uuid.UUID(queue_id) - qc = self.QueueConfig[uid] + queue_uid = uuid.UUID(queue_id) - if isinstance(qc, QueueConfig): - if time_set_id is None: - data = await qc.TimeSet.toDict() - else: - data = await qc.TimeSet.get(uuid.UUID(time_set_id)) + if time_set_id is None: + data = await self.QueueConfig[queue_uid].TimeSet.toDict() else: - logger.error(f"不支持的队列配置类型: {type(qc)}") - raise TypeError(f"不支持的队列配置类型: {type(qc)}") + data = await self.QueueConfig[queue_uid].TimeSet.get(uuid.UUID(time_set_id)) index = data.pop("instances", []) - return list(index), data - async def add_time_set(self, queue_id: str) -> tuple[uuid.UUID, ConfigBase]: + async def add_time_set(self, queue_id: str) -> tuple[uuid.UUID, TimeSet]: """添加时间设置配置""" logger.info(f"{queue_id} 添加时间设置配置") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] - - if isinstance(queue_config, QueueConfig): - uid, config = await queue_config.TimeSet.add(TimeSet) - else: - raise TypeError(f"不支持的队列配置类型: {type(queue_config)}") + queue_uid = uuid.UUID(queue_id) + uid, config = await self.QueueConfig[queue_uid].TimeSet.add(TimeSet) await self.QueueConfig.save() return uid, config @@ -1484,14 +917,15 @@ class AppConfig(GlobalConfig): logger.info(f"{queue_id} 更新时间设置配置: {time_set_id}") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] - uid = uuid.UUID(time_set_id) + queue_uid = uuid.UUID(queue_id) + time_set_uid = uuid.UUID(time_set_id) for group, items in data.items(): for name, value in items.items(): logger.debug(f"更新时间设置配置: {queue_id} - {group}.{name} = {value}") - if isinstance(queue_config, QueueConfig): - await queue_config.TimeSet[uid].set(group, name, value) + await self.QueueConfig[queue_uid].TimeSet[time_set_uid].set( + group, name, value + ) await self.QueueConfig.save() @@ -1500,60 +934,51 @@ class AppConfig(GlobalConfig): logger.info(f"{queue_id} 删除时间设置配置: {time_set_id}") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] - uid = uuid.UUID(time_set_id) + queue_uid = uuid.UUID(queue_id) + time_set_uid = uuid.UUID(time_set_id) - if isinstance(queue_config, QueueConfig): - await queue_config.TimeSet.remove(uid) - await self.QueueConfig.save() + await self.QueueConfig[queue_uid].TimeSet.remove(time_set_uid) + await self.QueueConfig.save() async def reorder_time_set(self, queue_id: str, index_list: list[str]) -> None: """重新排序时间设置""" logger.info(f"{queue_id} 重新排序时间设置: {index_list}") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] + queue_uid = uuid.UUID(queue_id) - if isinstance(queue_config, QueueConfig): - await queue_config.TimeSet.setOrder([uuid.UUID(_) for _ in index_list]) - await self.QueueConfig.save() + await self.QueueConfig[queue_uid].TimeSet.setOrder( + list(map(uuid.UUID, index_list)) + ) + await self.QueueConfig.save() async def get_queue_item( self, queue_id: str, queue_item_id: Optional[str] ) -> tuple[list, dict]: """获取队列项配置""" - logger.info(f"Get queue item of queue: {queue_id} - {queue_item_id}") + logger.info(f"获取队列的队列项配置: {queue_id} - {queue_item_id}") - uid = uuid.UUID(queue_id) - qc = self.QueueConfig[uid] + queue_uid = uuid.UUID(queue_id) - if isinstance(qc, QueueConfig): - if queue_item_id is None: - data = await qc.QueueItem.toDict() - else: - data = await qc.QueueItem.get(uuid.UUID(queue_item_id)) + if queue_item_id is None: + data = await self.QueueConfig[queue_uid].QueueItem.toDict() else: - logger.error(f"不支持的队列配置类型: {type(qc)}") - raise TypeError(f"不支持的队列配置类型: {type(qc)}") + data = await self.QueueConfig[queue_uid].QueueItem.get( + uuid.UUID(queue_item_id) + ) index = data.pop("instances", []) - return list(index), data - async def add_queue_item(self, queue_id: str) -> tuple[uuid.UUID, ConfigBase]: + async def add_queue_item(self, queue_id: str) -> tuple[uuid.UUID, QueueItem]: """添加队列项配置""" logger.info(f"{queue_id} 添加队列项配置") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] - - if isinstance(queue_config, QueueConfig): - uid, config = await queue_config.QueueItem.add(QueueItem) - else: - logger.warning(f"不支持的队列配置类型: {type(queue_config)}") - raise TypeError(f"不支持的队列配置类型: {type(queue_config)}") + queue_uid = uuid.UUID(queue_id) + uid, config = await self.QueueConfig[queue_uid].QueueItem.add(QueueItem) await self.QueueConfig.save() return uid, config @@ -1564,14 +989,15 @@ class AppConfig(GlobalConfig): logger.info(f"{queue_id} 更新队列项配置: {queue_item_id}") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] - uid = uuid.UUID(queue_item_id) + queue_uid = uuid.UUID(queue_id) + queue_item_uid = uuid.UUID(queue_item_id) for group, items in data.items(): for name, value in items.items(): logger.debug(f"更新队列项配置: {queue_id} - {group}.{name} = {value}") - if isinstance(queue_config, QueueConfig): - await queue_config.QueueItem[uid].set(group, name, value) + await self.QueueConfig[queue_uid].QueueItem[queue_item_uid].set( + group, name, value + ) await self.QueueConfig.save() @@ -1580,23 +1006,23 @@ class AppConfig(GlobalConfig): logger.info(f"{queue_id} 删除队列项配置: {queue_item_id}") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] - uid = uuid.UUID(queue_item_id) + queue_uid = uuid.UUID(queue_id) + queue_item_uid = uuid.UUID(queue_item_id) - if isinstance(queue_config, QueueConfig): - await queue_config.QueueItem.remove(uid) - await self.QueueConfig.save() + await self.QueueConfig[queue_uid].QueueItem.remove(queue_item_uid) + await self.QueueConfig.save() async def reorder_queue_item(self, queue_id: str, index_list: list[str]) -> None: """重新排序队列项""" logger.info(f"{queue_id} 重新排序队列项: {index_list}") - queue_config = self.QueueConfig[uuid.UUID(queue_id)] + queue_uid = uuid.UUID(queue_id) - if isinstance(queue_config, QueueConfig): - await queue_config.QueueItem.setOrder([uuid.UUID(_) for _ in index_list]) - await self.QueueConfig.save() + await self.QueueConfig[queue_uid].QueueItem.setOrder( + list(map(uuid.UUID, index_list)) + ) + await self.QueueConfig.save() async def get_setting(self) -> Dict[str, Any]: """获取全局设置""" @@ -1617,6 +1043,158 @@ class AppConfig(GlobalConfig): logger.success("全局设置更新成功") + async def get_webhook( + self, + script_id: Optional[str], + user_id: Optional[str], + webhook_id: Optional[str], + ) -> tuple[list, dict]: + """获取webhook配置""" + + if script_id is None and user_id is None: + logger.info(f"获取全局webhook设置: {webhook_id}") + + if webhook_id is None: + data = await self.Notify_CustomWebhooks.toDict() + else: + data = await self.Notify_CustomWebhooks.get(uuid.UUID(webhook_id)) + + else: + logger.info(f"获取webhook设置: {script_id} - {user_id} - {webhook_id}") + + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) + + if webhook_id is None: + data = ( + await self.ScriptConfig[script_uid] + .UserData[user_uid] + .Notify_CustomWebhooks.toDict() + ) + else: + data = ( + await self.ScriptConfig[script_uid] + .UserData[user_uid] + .Notify_CustomWebhooks.get(uuid.UUID(webhook_id)) + ) + + index = data.pop("instances", []) + return list(index), data + + async def add_webhook( + self, script_id: Optional[str], user_id: Optional[str] + ) -> tuple[uuid.UUID, Webhook]: + """添加webhook配置""" + + if script_id is None and user_id is None: + logger.info("添加全局webhook配置") + + uid, config = await self.Notify_CustomWebhooks.add(Webhook) + await self.save() + return uid, config + + else: + logger.info(f"添加webhook配置: {script_id} - {user_id}") + + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) + + uid, config = ( + await self.ScriptConfig[script_uid] + .UserData[user_uid] + .Notify_CustomWebhooks.add(Webhook) + ) + await self.ScriptConfig.save() + return uid, config + + async def update_webhook( + self, + script_id: Optional[str], + user_id: Optional[str], + webhook_id: str, + data: Dict[str, Dict[str, Any]], + ) -> None: + """更新 webhook 配置""" + + webhook_uid = uuid.UUID(webhook_id) + + if script_id is None and user_id is None: + logger.info(f"更新 webhook 全局配置: {webhook_id}") + + for group, items in data.items(): + for name, value in items.items(): + logger.debug( + f"更新全局 webhook:{webhook_id} - {group}.{name} = {value}" + ) + await self.Notify_CustomWebhooks[webhook_uid].set( + group, name, value + ) + + await self.save() + + else: + logger.info(f"更新 webhook 配置: {script_id} - {user_id} - {webhook_id}") + + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) + + for group, items in data.items(): + for name, value in items.items(): + logger.debug( + f"更新用户 webhook: {script_id} - {user_id} - {webhook_id} - {group}.{name} = {value}" + ) + await self.ScriptConfig[script_uid].UserData[ + user_uid + ].Notify_CustomWebhooks[webhook_uid].set(group, name, value) + + await self.ScriptConfig.save() + + async def del_webhook( + self, script_id: Optional[str], user_id: Optional[str], webhook_id: str + ) -> None: + """删除 webhook 配置""" + + webhook_uid = uuid.UUID(webhook_id) + + if script_id is None and user_id is None: + logger.info(f"删除全局 webhook 配置: {webhook_id}") + + await self.Notify_CustomWebhooks.remove(webhook_uid) + await self.save() + + else: + logger.info(f"删除 webhook 配置: {script_id} - {user_id} - {webhook_id}") + + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) + + await self.ScriptConfig[script_uid].UserData[ + user_uid + ].Notify_CustomWebhooks.remove(webhook_uid) + await self.ScriptConfig.save() + + async def reorder_webhook( + self, script_id: Optional[str], user_id: Optional[str], index_list: list[str] + ) -> None: + """重新排序 webhook""" + + if script_id is None and user_id is None: + logger.info(f"重新排序全局 webhook: {index_list}") + + await self.Notify_CustomWebhooks.setOrder(list(map(uuid.UUID, index_list))) + await self.save() + + else: + logger.info(f"重新排序 webhook: {script_id} - {user_id} - {index_list}") + + script_uid = uuid.UUID(script_id) + user_uid = uuid.UUID(user_id) + + await self.ScriptConfig[script_uid].UserData[ + user_uid + ].Notify_CustomWebhooks.setOrder(list(map(uuid.UUID, index_list))) + await self.ScriptConfig.save() + def server_date(self) -> date: """ 获取当前的服务器日期 diff --git a/app/core/task_manager.py b/app/core/task_manager.py index 9b52d88..6a106fc 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -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 ], diff --git a/app/core/timer.py b/app/core/timer.py index bf72127..5100cb5 100644 --- a/app/core/timer.py +++ b/app/core/timer.py @@ -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 # 避免重复调起任务 diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py index 4470154..1b3014d 100644 --- a/app/models/ConfigBase.py +++ b/app/models/ConfigBase.py @@ -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]: """ 添加一个新的配置项 diff --git a/app/models/__init__.py b/app/models/__init__.py index 6bec6ea..131725f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -25,6 +25,7 @@ __author__ = "DLmaster361 " __license__ = "GPL-3.0 license" from .ConfigBase import * +from .config import * from .schema import * -__all__ = ["ConfigBase", "schema"] +__all__ = ["ConfigBase", "config", "schema"] diff --git a/app/models/config.py b/app/models/config.py new file mode 100644 index 0000000..bafa38f --- /dev/null +++ b/app/models/config.py @@ -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 . + +# 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} +"""配置类映射表""" diff --git a/app/models/schema.py b/app/models/schema.py index 195d4cd..0a04d5c 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -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"] diff --git a/app/services/notification.py b/app/services/notification.py index c1e0d6f..8264ea7 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -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("测试通知发送完成") diff --git a/app/task/MAA.py b/app/task/MAA.py index e82eb52..8e38529 100644 --- a/app/task/MAA.py +++ b/app/task/MAA.py @@ -74,9 +74,8 @@ class MaaManager: await Config.ScriptConfig[self.script_id].lock() self.script_config = Config.ScriptConfig[self.script_id] - if isinstance(self.script_config, MaaConfig): - self.user_config = MultipleConfig([MaaUserConfig]) - await self.user_config.load(await self.script_config.UserData.toDict()) + self.user_config = MultipleConfig([MaaUserConfig]) + await self.user_config.load(await self.script_config.UserData.toDict()) self.maa_root_path = Path(self.script_config.get("Info", "Path")) self.maa_set_path = self.maa_root_path / "config/gui.json" @@ -96,6 +95,8 @@ class MaaManager: def check_config(self) -> str: """检查配置是否可用""" + if not isinstance(Config.ScriptConfig[self.script_id], MaaConfig): + return "脚本配置类型错误, 不是MAA脚本类型" if not self.maa_exe_path.exists(): return "MAA.exe文件不存在, 请检查MAA路径设置!" if not self.maa_set_path.exists(): @@ -176,537 +177,583 @@ class MaaManager: # 开始代理 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 - - logger.info(f"开始代理用户: {user['user_id']}") - - # 详细模式用户首次代理需打开模拟器 - if self.cur_user_data.get("Info", "Mode") == "详细": - self.if_open_emulator = True - - # 初始化代理情况记录和模式替换表 - self.run_book = { - "Annihilation": bool( - self.cur_user_data.get("Info", "Annihilation") == "Close" - ), - "Routine": self.cur_user_data.get("Info", "Mode") == "复杂" - and not self.cur_user_data.get("Info", "Routine"), - } - - self.user_logs_list = [] - self.user_start_time = datetime.now() - - if self.cur_user_data.get( - "Info", "IfSkland" - ) and self.cur_user_data.get("Info", "SklandToken"): - - if self.cur_user_data.get( - "Data", "LastSklandDate" - ) != datetime.now().strftime("%Y-%m-%d"): + self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])] + if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or ( + self.cur_user_data.get("Data", "ProxyTimes") + < self.script_config.get("Run", "ProxyTimesLimit") + ): + user["status"] = "运行" await Config.send_json( WebSocketMessage( id=self.ws_id, type="Update", - data={"log": "正在执行森空岛签到中\n请稍候~"}, + data={"user_list": self.user_list}, ).model_dump() ) - - skland_result = await skland_sign_in( - self.cur_user_data.get("Info", "SklandToken") + else: + user["status"] = "跳过" + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Update", + data={"user_list": self.user_list}, + ).model_dump() ) + continue - for type, user_list in skland_result.items(): + logger.info(f"开始代理用户: {user['user_id']}") - if type != "总计" and len(user_list) > 0: - logger.info( - f"用户: {user['user_id']} - 森空岛签到{type}: {'、'.join(user_list)}" - ) + # 详细模式用户首次代理需打开模拟器 + if self.cur_user_data.get("Info", "Mode") == "详细": + self.if_open_emulator = True + + # 初始化代理情况记录和模式替换表 + self.run_book = { + "Annihilation": bool( + self.cur_user_data.get("Info", "Annihilation") == "Close" + ), + "Routine": self.cur_user_data.get("Info", "Mode") == "详细" + and not self.cur_user_data.get("Info", "Routine"), + } + + self.user_logs_list = [] + self.user_start_time = datetime.now() + + if self.cur_user_data.get( + "Info", "IfSkland" + ) and self.cur_user_data.get("Info", "SklandToken"): + + if self.cur_user_data.get( + "Data", "LastSklandDate" + ) != datetime.now().strftime("%Y-%m-%d"): + + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Update", + data={"log": "正在执行森空岛签到中\n请稍候~"}, + ).model_dump() + ) + + skland_result = await skland_sign_in( + self.cur_user_data.get("Info", "SklandToken") + ) + + for type, user_list in skland_result.items(): + + if type != "总计" and len(user_list) > 0: + logger.info( + f"用户: {user['user_id']} - 森空岛签到{type}: {'、'.join(user_list)}" + ) + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Info", + data={ + ( + "Info" + if type != "失败" + else "Error" + ): f"用户 {user['name']} 森空岛签到{type}: {'、'.join(user_list)}" + }, + ).model_dump() + ) + if skland_result["总计"] == 0: + logger.info(f"用户: {user['user_id']} - 森空岛签到失败") await Config.send_json( WebSocketMessage( id=self.ws_id, type="Info", data={ - ( - "Info" if type != "失败" else "Error" - ): f"用户 {user['name']} 森空岛签到{type}: {'、'.join(user_list)}" + "Error": f"用户 {user['name']} 森空岛签到失败", }, ).model_dump() ) - if skland_result["总计"] == 0: - logger.info(f"用户: {user['user_id']} - 森空岛签到失败") - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Info", - data={ - "Error": f"用户 {user['name']} 森空岛签到失败", - }, - ).model_dump() - ) - if ( - skland_result["总计"] > 0 - and len(skland_result["失败"]) == 0 - ): - await self.cur_user_data.set( - "Data", - "LastSklandDate", - datetime.now().strftime("%Y-%m-%d"), - ) + if ( + skland_result["总计"] > 0 + and len(skland_result["失败"]) == 0 + ): + await self.cur_user_data.set( + "Data", + "LastSklandDate", + datetime.now().strftime("%Y-%m-%d"), + ) - elif self.cur_user_data.get("Info", "IfSkland"): - logger.warning( - f"用户: {user['user_id']} - 未配置森空岛签到Token, 跳过森空岛签到" - ) - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Info", - data={ - "Warning": f"用户 {user['name']} 未配置森空岛签到Token, 跳过森空岛签到" - }, - ).model_dump() - ) - - # 剿灭-日常模式循环 - for mode in ["Annihilation", "Routine"]: - - if self.run_book[mode]: - continue - - # 剿灭模式;满足条件跳过剿灭 - if ( - mode == "Annihilation" - and self.script_config.get("Run", "AnnihilationWeeklyLimit") - and datetime.strptime( - self.cur_user_data.get("Data", "LastAnnihilationDate"), - "%Y-%m-%d", - ).isocalendar()[:2] - == datetime.strptime(self.curdate, "%Y-%m-%d").isocalendar()[:2] - ): - logger.info( - f"用户: {user['user_id']} - 本周剿灭模式已达上限, 跳过执行剿灭任务" - ) - self.run_book[mode] = True - continue - else: - self.weekly_annihilation_limit_reached = False - - if ( - self.cur_user_data.get("Info", "Mode") == "详细" - and not ( - Path.cwd() - / f"data/{self.script_id}/{user['user_id']}/ConfigFile/gui.json" - ).exists() - ): - logger.error( - f"用户: {user['user_id']} - 未找到日常详细配置文件" + elif self.cur_user_data.get("Info", "IfSkland"): + logger.warning( + f"用户: {user['user_id']} - 未配置森空岛签到Token, 跳过森空岛签到" ) await Config.send_json( WebSocketMessage( id=self.ws_id, type="Info", - data={"Error": f"未找到 {user['name']} 的详细配置文件"}, + data={ + "Warning": f"用户 {user['name']} 未配置森空岛签到Token, 跳过森空岛签到" + }, ).model_dump() ) - self.run_book[mode] = False - break - # 更新当前模式到界面 + # 剿灭-日常模式循环 + for mode in ["Annihilation", "Routine"]: + + if self.run_book[mode]: + continue + + # 剿灭模式;满足条件跳过剿灭 + if ( + mode == "Annihilation" + and self.script_config.get("Run", "AnnihilationWeeklyLimit") + and datetime.strptime( + self.cur_user_data.get("Data", "LastAnnihilationDate"), + "%Y-%m-%d", + ).isocalendar()[:2] + == datetime.strptime( + self.curdate, "%Y-%m-%d" + ).isocalendar()[:2] + ): + logger.info( + f"用户: {user['user_id']} - 本周剿灭模式已达上限, 跳过执行剿灭任务" + ) + self.run_book[mode] = True + continue + else: + self.weekly_annihilation_limit_reached = False + + if ( + self.cur_user_data.get("Info", "Mode") == "详细" + and not ( + Path.cwd() + / f"data/{self.script_id}/{user['user_id']}/ConfigFile/gui.json" + ).exists() + ): + logger.error( + f"用户: {user['user_id']} - 未找到日常详细配置文件" + ) + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Info", + data={ + "Error": f"未找到 {user['name']} 的详细配置文件" + }, + ).model_dump() + ) + self.run_book[mode] = False + break + + # 更新当前模式到界面 + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Update", + data={ + "user_status": { + "user_id": user["user_id"], + "type": mode, + } + }, + ).model_dump() + ) + + # 解析任务构成 + if mode == "Routine": + + self.task_dict = { + "WakeUp": str( + self.cur_user_data.get("Task", "IfWakeUp") + ), + "Recruiting": str( + self.cur_user_data.get("Task", "IfRecruiting") + ), + "Base": str(self.cur_user_data.get("Task", "IfBase")), + "Combat": str( + self.cur_user_data.get("Task", "IfCombat") + ), + "Mission": str( + self.cur_user_data.get("Task", "IfMission") + ), + "Mall": str(self.cur_user_data.get("Task", "IfMall")), + "AutoRoguelike": str( + self.cur_user_data.get("Task", "IfAutoRoguelike") + ), + "Reclamation": str( + self.cur_user_data.get("Task", "IfReclamation") + ), + } + + elif mode == "Annihilation": + + self.task_dict = { + "WakeUp": "True", + "Recruiting": "False", + "Base": "False", + "Combat": "True", + "Mission": "False", + "Mall": "False", + "AutoRoguelike": "False", + "Reclamation": "False", + } + + logger.info( + f"用户 {user['name']} - 模式: {mode} - 任务列表: {self.task_dict.values()}" + ) + + # 尝试次数循环 + for i in range(self.script_config.get("Run", "RunTimesLimit")): + + if self.run_book[mode]: + break + + logger.info( + f"用户 {user['name']} - 模式: {mode} - 尝试次数: {i + 1}/{self.script_config.get('Run', 'RunTimesLimit')}" + ) + + # 配置MAA + set = await self.set_maa(mode) + # 记录当前时间 + self.log_start_time = datetime.now() + + # 记录模拟器与ADB路径 + self.emulator_path = Path( + set["Configurations"]["Default"]["Start.EmulatorPath"] + ) + self.emulator_arguments = set["Configurations"]["Default"][ + "Start.EmulatorAddCommand" + ].split() + # 如果是快捷方式, 进行解析 + if ( + self.emulator_path.suffix == ".lnk" + and self.emulator_path.exists() + ): + try: + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortcut( + str(self.emulator_path) + ) + self.emulator_path = Path(shortcut.TargetPath) + self.emulator_arguments = shortcut.Arguments.split() + 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.if_open_emulator = True + break + elif not self.emulator_path.exists(): + logger.error( + f"模拟器快捷方式不存在: {self.emulator_path}" + ) + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Info", + data={ + "Error": f"模拟器快捷方式 {self.emulator_path} 不存在", + }, + ).model_dump() + ) + self.if_open_emulator = True + break + + self.wait_time = int( + set["Configurations"]["Default"][ + "Start.EmulatorWaitSeconds" + ] + ) + + self.ADB_path = Path( + set["Configurations"]["Default"]["Connect.AdbPath"] + ) + self.ADB_path = ( + self.ADB_path + if self.ADB_path.is_absolute() + else self.maa_root_path / self.ADB_path + ) + self.ADB_address = set["Configurations"]["Default"][ + "Connect.Address" + ] + self.if_kill_emulator = bool( + set["Configurations"]["Default"][ + "MainFunction.PostActions" + ] + == "12" + ) + self.if_open_emulator_process = bool( + set["Configurations"]["Default"][ + "Start.OpenEmulatorAfterLaunch" + ] + == "True" + ) + + # 任务开始前释放ADB + try: + logger.info(f"释放ADB: {self.ADB_address}") + subprocess.run( + [self.ADB_path, "disconnect", self.ADB_address], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + except subprocess.CalledProcessError as e: + # 忽略错误,因为可能本来就没有连接 + logger.warning(f"释放ADB时出现异常: {e}") + except Exception as e: + logger.exception(f"释放ADB时出现异常: {e}") + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Info", + data={"Warning": f"释放ADB时出现异常: {e}"}, + ).model_dump() + ) + + if self.if_open_emulator_process: + try: + logger.info( + f"启动模拟器: {self.emulator_path}, 参数: {self.emulator_arguments}" + ) + await self.emulator_process_manager.open_process( + self.emulator_path, self.emulator_arguments, 0 + ) + except Exception as e: + logger.exception(f"启动模拟器时出现异常: {e}") + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Info", + data={ + "Error": "启动模拟器时出现异常, 请检查MAA中模拟器路径设置" + }, + ).model_dump() + ) + self.if_open_emulator = True + break + + # 更新静默进程标记有效时间 + logger.info( + f"更新静默进程标记: {self.emulator_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.wait_time + 10)}" + ) + Config.silence_dict[self.emulator_path] = ( + datetime.now() + timedelta(seconds=self.wait_time + 10) + ) + + await self.search_ADB_address() + + # 创建MAA任务 + logger.info(f"启动MAA进程: {self.maa_exe_path}") + await self.maa_process_manager.open_process( + self.maa_exe_path, [], 0 + ) + + # 监测MAA运行状态 + self.log_check_mode = mode + await self.maa_log_monitor.start( + self.maa_log_path, self.log_start_time + ) + + self.wait_event.clear() + await self.wait_event.wait() + + await self.maa_log_monitor.stop() + + # 处理MAA结果 + if self.maa_result == "Success!": + + # 标记任务完成 + self.run_book[mode] = True + + logger.info( + f"用户: {user['user_id']} - MAA进程完成代理任务" + ) + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Update", + data={ + "log": "检测到MAA进程完成代理任务\n正在等待相关程序结束\n请等待10s" + }, + ).model_dump() + ) + + else: + logger.error( + f"用户: {user['user_id']} - 代理任务异常: {self.maa_result}" + ) + # 打印中止信息 + # 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查 + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Update", + data={ + "log": f"{self.maa_result}\n正在中止相关程序\n请等待10s" + }, + ).model_dump() + ) + # 无命令行中止MAA与其子程序 + logger.info(f"中止MAA进程: {self.maa_exe_path}") + await self.maa_process_manager.kill(if_force=True) + await System.kill_process(self.maa_exe_path) + + # 中止模拟器进程 + logger.info( + f"中止模拟器进程: {list(self.emulator_process_manager.tracked_pids)}" + ) + await self.emulator_process_manager.kill() + + self.if_open_emulator = True + + # 推送异常通知 + await Notify.push_plyer( + "用户自动代理出现异常!", + f"用户 {user['name']} 的{MOOD_BOOK[mode]}部分出现一次异常", + f"{user['name']}的{MOOD_BOOK[mode]}出现异常", + 3, + ) + + await asyncio.sleep(10) + + # 任务结束后释放ADB + try: + logger.info(f"释放ADB: {self.ADB_address}") + subprocess.run( + [self.ADB_path, "disconnect", self.ADB_address], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + except subprocess.CalledProcessError as e: + # 忽略错误,因为可能本来就没有连接 + logger.warning(f"释放ADB时出现异常: {e}") + except Exception as e: + logger.exception(f"释放ADB时出现异常: {e}") + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Info", + data={"Error": f"释放ADB时出现异常: {e}"}, + ).model_dump() + ) + # 任务结束后再次手动中止模拟器进程, 防止退出不彻底 + if self.if_kill_emulator: + logger.info( + f"任务结束后再次中止模拟器进程: {list(self.emulator_process_manager.tracked_pids)}" + ) + await self.emulator_process_manager.kill() + self.if_open_emulator = True + + # 从配置文件中解析所需信息 + with self.maa_set_path.open( + mode="r", encoding="utf-8" + ) as f: + data = json.load(f) + + # 记录自定义基建索引 + await self.cur_user_data.set( + "Data", + "CustomInfrastPlanIndex", + data["Configurations"]["Default"][ + "Infrast.CustomInfrastPlanIndex" + ], + ) + + # 记录更新包路径 + if ( + data["Global"]["VersionUpdate.package"] + and ( + self.maa_root_path + / data["Global"]["VersionUpdate.package"] + ).exists() + ): + self.maa_update_package = data["Global"][ + "VersionUpdate.package" + ] + + # 记录剿灭情况 + if ( + mode == "Annihilation" + and self.weekly_annihilation_limit_reached + ): + await self.cur_user_data.set( + "Data", "LastAnnihilationDate", self.curdate + ) + # 保存运行日志以及统计信息 + if_six_star = await Config.save_maa_log( + Path.cwd() + / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log", + self.maa_logs, + self.maa_result, + ) + self.user_logs_list.append( + Path.cwd() + / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json" + ) + if if_six_star: + try: + await self.push_notification( + "公招六星", + f"喜报: 用户 {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解压更新动作 + if self.maa_update_package: + logger.info(f"检测到MAA更新, 正在执行更新动作") + + await Config.send_json( + WebSocketMessage( + id=self.ws_id, + type="Update", + data={ + "log": "检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s" + }, + ).model_dump() + ) + await self.set_maa("Update") + subprocess.Popen( + [self.maa_exe_path], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + await asyncio.sleep(10) + await System.kill_process(self.maa_exe_path) + + self.maa_update_package = "" + + logger.info(f"更新动作结束") + + 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="Update", - data={ - "user_status": { - "user_id": user["user_id"], - "type": mode, - } - }, + type="Info", + data={"Error": f"代理用户 {user['name']} 时出现异常: {e}"}, ).model_dump() ) - # 解析任务构成 - if mode == "Routine": - - self.task_dict = { - "WakeUp": str(self.cur_user_data.get("Task", "IfWakeUp")), - "Recruiting": str( - self.cur_user_data.get("Task", "IfRecruiting") - ), - "Base": str(self.cur_user_data.get("Task", "IfBase")), - "Combat": str(self.cur_user_data.get("Task", "IfCombat")), - "Mission": str(self.cur_user_data.get("Task", "IfMission")), - "Mall": str(self.cur_user_data.get("Task", "IfMall")), - "AutoRoguelike": str( - self.cur_user_data.get("Task", "IfAutoRoguelike") - ), - "Reclamation": str( - self.cur_user_data.get("Task", "IfReclamation") - ), - } - - elif mode == "Annihilation": - - self.task_dict = { - "WakeUp": "True", - "Recruiting": "False", - "Base": "False", - "Combat": "True", - "Mission": "False", - "Mall": "False", - "AutoRoguelike": "False", - "Reclamation": "False", - } - - logger.info( - f"用户 {user['name']} - 模式: {mode} - 任务列表: {self.task_dict.values()}" - ) - - # 尝试次数循环 - for i in range(self.script_config.get("Run", "RunTimesLimit")): - - if self.run_book[mode]: - break - - logger.info( - f"用户 {user['name']} - 模式: {mode} - 尝试次数: {i + 1}/{self.script_config.get('Run', 'RunTimesLimit')}" - ) - - # 配置MAA - set = await self.set_maa(mode) - # 记录当前时间 - self.log_start_time = datetime.now() - - # 记录模拟器与ADB路径 - self.emulator_path = Path( - set["Configurations"]["Default"]["Start.EmulatorPath"] - ) - self.emulator_arguments = set["Configurations"]["Default"][ - "Start.EmulatorAddCommand" - ].split() - # 如果是快捷方式, 进行解析 - if ( - self.emulator_path.suffix == ".lnk" - and self.emulator_path.exists() - ): - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(str(self.emulator_path)) - self.emulator_path = Path(shortcut.TargetPath) - self.emulator_arguments = shortcut.Arguments.split() - 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.if_open_emulator = True - break - elif not self.emulator_path.exists(): - logger.error(f"模拟器快捷方式不存在: {self.emulator_path}") - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Info", - data={ - "Error": f"模拟器快捷方式 {self.emulator_path} 不存在", - }, - ).model_dump() - ) - self.if_open_emulator = True - break - - self.wait_time = int( - set["Configurations"]["Default"][ - "Start.EmulatorWaitSeconds" - ] - ) - - self.ADB_path = Path( - set["Configurations"]["Default"]["Connect.AdbPath"] - ) - self.ADB_path = ( - self.ADB_path - if self.ADB_path.is_absolute() - else self.maa_root_path / self.ADB_path - ) - self.ADB_address = set["Configurations"]["Default"][ - "Connect.Address" - ] - self.if_kill_emulator = bool( - set["Configurations"]["Default"]["MainFunction.PostActions"] - == "12" - ) - self.if_open_emulator_process = bool( - set["Configurations"]["Default"][ - "Start.OpenEmulatorAfterLaunch" - ] - == "True" - ) - - # 任务开始前释放ADB - try: - logger.info(f"释放ADB: {self.ADB_address}") - subprocess.run( - [self.ADB_path, "disconnect", self.ADB_address], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - except subprocess.CalledProcessError as e: - # 忽略错误,因为可能本来就没有连接 - logger.warning(f"释放ADB时出现异常: {e}") - except Exception as e: - logger.exception(f"释放ADB时出现异常: {e}") - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Info", - data={"Warning": f"释放ADB时出现异常: {e}"}, - ).model_dump() - ) - - if self.if_open_emulator_process: - try: - logger.info( - f"启动模拟器: {self.emulator_path}, 参数: {self.emulator_arguments}" - ) - await self.emulator_process_manager.open_process( - self.emulator_path, self.emulator_arguments, 0 - ) - except Exception as e: - logger.exception(f"启动模拟器时出现异常: {e}") - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Info", - data={ - "Error": "启动模拟器时出现异常, 请检查MAA中模拟器路径设置" - }, - ).model_dump() - ) - self.if_open_emulator = True - break - - # 更新静默进程标记有效时间 - logger.info( - f"更新静默进程标记: {self.emulator_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.wait_time + 10)}" - ) - Config.silence_dict[self.emulator_path] = ( - datetime.now() + timedelta(seconds=self.wait_time + 10) - ) - - await self.search_ADB_address() - - # 创建MAA任务 - logger.info(f"启动MAA进程: {self.maa_exe_path}") - await self.maa_process_manager.open_process( - self.maa_exe_path, [], 0 - ) - - # 监测MAA运行状态 - self.log_check_mode = mode - await self.maa_log_monitor.start( - self.maa_log_path, self.log_start_time - ) - - self.wait_event.clear() - await self.wait_event.wait() - - await self.maa_log_monitor.stop() - - # 处理MAA结果 - if self.maa_result == "Success!": - - # 标记任务完成 - self.run_book[mode] = True - - logger.info( - f"用户: {user['user_id']} - MAA进程完成代理任务" - ) - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Update", - data={ - "log": "检测到MAA进程完成代理任务\n正在等待相关程序结束\n请等待10s" - }, - ).model_dump() - ) - - else: - logger.error( - f"用户: {user['user_id']} - 代理任务异常: {self.maa_result}" - ) - # 打印中止信息 - # 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查 - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Update", - data={ - "log": f"{self.maa_result}\n正在中止相关程序\n请等待10s" - }, - ).model_dump() - ) - # 无命令行中止MAA与其子程序 - logger.info(f"中止MAA进程: {self.maa_exe_path}") - await self.maa_process_manager.kill(if_force=True) - await System.kill_process(self.maa_exe_path) - - # 中止模拟器进程 - logger.info( - f"中止模拟器进程: {list(self.emulator_process_manager.tracked_pids)}" - ) - await self.emulator_process_manager.kill() - - self.if_open_emulator = True - - # 推送异常通知 - await Notify.push_plyer( - "用户自动代理出现异常!", - f"用户 {user['name']} 的{MOOD_BOOK[mode]}部分出现一次异常", - f"{user['name']}的{MOOD_BOOK[mode]}出现异常", - 3, - ) - - await asyncio.sleep(10) - - # 任务结束后释放ADB - try: - logger.info(f"释放ADB: {self.ADB_address}") - subprocess.run( - [self.ADB_path, "disconnect", self.ADB_address], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - except subprocess.CalledProcessError as e: - # 忽略错误,因为可能本来就没有连接 - logger.warning(f"释放ADB时出现异常: {e}") - except Exception as e: - logger.exception(f"释放ADB时出现异常: {e}") - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Info", - data={"Error": f"释放ADB时出现异常: {e}"}, - ).model_dump() - ) - # 任务结束后再次手动中止模拟器进程, 防止退出不彻底 - if self.if_kill_emulator: - logger.info( - f"任务结束后再次中止模拟器进程: {list(self.emulator_process_manager.tracked_pids)}" - ) - await self.emulator_process_manager.kill() - self.if_open_emulator = True - - # 从配置文件中解析所需信息 - with self.maa_set_path.open(mode="r", encoding="utf-8") as f: - data = json.load(f) - - # 记录自定义基建索引 - await self.cur_user_data.set( - "Data", - "CustomInfrastPlanIndex", - data["Configurations"]["Default"][ - "Infrast.CustomInfrastPlanIndex" - ], - ) - - # 记录更新包路径 - if ( - data["Global"]["VersionUpdate.package"] - and ( - self.maa_root_path - / data["Global"]["VersionUpdate.package"] - ).exists() - ): - self.maa_update_package = data["Global"][ - "VersionUpdate.package" - ] - - # 记录剿灭情况 - if ( - mode == "Annihilation" - and self.weekly_annihilation_limit_reached - ): - await self.cur_user_data.set( - "Data", "LastAnnihilationDate", self.curdate - ) - # 保存运行日志以及统计信息 - if_six_star = await Config.save_maa_log( - Path.cwd() - / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log", - self.maa_logs, - self.maa_result, - ) - self.user_logs_list.append( - Path.cwd() - / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json" - ) - if if_six_star: - await self.push_notification( - "公招六星", - f"喜报: 用户 {user['name']} 公招出六星啦!", - { - "user_name": user["name"], - }, - ) - - # 执行MAA解压更新动作 - if self.maa_update_package: - logger.info(f"检测到MAA更新, 正在执行更新动作") - - await Config.send_json( - WebSocketMessage( - id=self.ws_id, - type="Update", - data={ - "log": "检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s" - }, - ).model_dump() - ) - await self.set_maa("Update") - subprocess.Popen( - [self.maa_exe_path], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - await asyncio.sleep(10) - await System.kill_process(self.maa_exe_path) - - self.maa_update_package = "" - - logger.info(f"更新动作结束") - - await self.result_record() - # 人工排查模式 elif self.mode == "人工排查": @@ -879,11 +926,21 @@ class MaaManager: if (self.run_book["Annihilation"] and self.run_book["Routine"]) else "代理任务未全部完成" ) - 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["Annihilation"] and self.run_book["Routine"]: # 成功完成代理的用户修改相关参数 @@ -989,13 +1046,23 @@ class MaaManager: / f"history/{self.curdate}/{self.user_list[self.index]['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json" ) if if_six_star: - await self.push_notification( - "公招六星", - f"喜报: 用户 {self.user_list[self.index]['name']} 公招出六星啦!", - { - "user_name": self.user_list[self.index]["name"], - }, - ) + try: + await self.push_notification( + "公招六星", + f"喜报: 用户 {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() @@ -1011,10 +1078,10 @@ class MaaManager: if self.mode in ["自动代理", "人工排查"]: # 更新用户数据 - sc = Config.ScriptConfig[self.script_id] - if isinstance(sc, MaaConfig): - 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"] == "完成"] @@ -1060,7 +1127,17 @@ class MaaManager: 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 == "设置脚本": ( @@ -1472,16 +1549,29 @@ class MaaManager: data["Configurations"]["Default"]["TaskQueue.Order.AutoRoguelike"] = "6" data["Configurations"]["Default"]["TaskQueue.Order.Reclamation"] = "7" - if isinstance(self.cur_user_data, MaaUserConfig): - try: - plan_data = self.cur_user_data.get_plan_info() - except Exception as e: - logger.error( - f"获取用户 {self.user_list[self.index]['user_id']} 的代理计划信息失败: {e}" - ) - plan_data = {} + if self.cur_user_data.get("Info", "StageMode") == "Fixed": + plan_data = { + "MedicineNumb": self.cur_user_data.get("Info", "MedicineNumb"), + "SeriesNumb": self.cur_user_data.get("Info", "SeriesNumb"), + "Stage": self.cur_user_data.get("Info", "Stage"), + "Stage_1": self.cur_user_data.get("Info", "Stage_1"), + "Stage_2": self.cur_user_data.get("Info", "Stage_2"), + "Stage_3": self.cur_user_data.get("Info", "Stage_3"), + "Stage_Remain": self.cur_user_data.get("Info", "Stage_Remain"), + } else: - plan_data = {} + plan = Config.PlanConfig[ + 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"] = ( "False" if plan_data.get("MedicineNumb", 0) == 0 else "True" @@ -1899,21 +1989,10 @@ class MaaManager: ) # 发送自定义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 == "统计信息": @@ -1965,21 +2044,10 @@ class MaaManager: ) # 发送自定义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( @@ -1988,51 +2056,25 @@ class MaaManager: # 发送邮件通知 if self.cur_user_data.get("Notify", "IfSendMail"): - if self.cur_user_data.get("Notify", "ToAddress"): - await Notify.send_mail( - "网页", - title, - message_html, - self.cur_user_data.get("Notify", "ToAddress"), - ) - else: - logger.error(f"用户邮箱地址为空, 无法发送用户单独的邮件通知") + await Notify.send_mail( + "网页", + title, + message_html, + self.cur_user_data.get("Notify", "ToAddress"), + ) # 发送ServerChan通知 if self.cur_user_data.get("Notify", "IfServerChan"): - if self.cur_user_data.get("Notify", "ServerChanKey"): - await Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO-MAS 敬上", - self.cur_user_data.get("Notify", "ServerChanKey"), - ) - else: - logger.error( - "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知" - ) + await Notify.ServerChanPush( + title, + f"{serverchan_message}\n\nAUTO-MAS 敬上", + self.cur_user_data.get("Notify", "ServerChanKey"), + ) - # 推送CompanyWebHookBot通知 - # 发送用户自定义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, 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通知" + # 推送CompanyWebHook通知 + for webhook in self.cur_user_data.Notify_CustomWebhooks.values(): + await Notify.WebhookPush( + title, f"{message_text}\n\nAUTO-MAS 敬上", webhook ) elif mode == "公招六星": @@ -2058,21 +2100,8 @@ class MaaManager: ) # 发送自定义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, "好羡慕~\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, "好羡慕~\n\nAUTO-MAS 敬上", webhook) # 发送用户单独通知 if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get( @@ -2081,52 +2110,21 @@ class MaaManager: # 发送邮件通知 if self.cur_user_data.get("Notify", "IfSendMail"): - if self.cur_user_data.get("Notify", "ToAddress"): - await Notify.send_mail( - "网页", - title, - message_html, - self.cur_user_data.get("Notify", "ToAddress"), - ) - else: - logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知") + await Notify.send_mail( + "网页", + title, + message_html, + self.cur_user_data.get("Notify", "ToAddress"), + ) # 发送ServerChan通知 if self.cur_user_data.get("Notify", "IfServerChan"): - - if self.cur_user_data.get("Notify", "ServerChanKey"): - await Notify.ServerChanPush( - title, - "好羡慕~\n\nAUTO-MAS 敬上", - self.cur_user_data.get("Notify", "ServerChanKey"), - ) - else: - logger.error( - "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知" - ) - - # 推送CompanyWebHookBot通知 - # 发送用户自定义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通知" + await Notify.ServerChanPush( + title, + "好羡慕~\n\nAUTO-MAS 敬上", + self.cur_user_data.get("Notify", "ServerChanKey"), ) - return None + # 推送CompanyWebHookBot通知 + for webhook in self.cur_user_data.Notify_CustomWebhooks.values(): + await Notify.WebhookPush(title, "好羡慕~\n\nAUTO-MAS 敬上", webhook) diff --git a/app/task/general.py b/app/task/general.py index 7351807..4c728d0 100644 --- a/app/task/general.py +++ b/app/task/general.py @@ -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 + ) diff --git a/app/utils/constants.py b/app/utils/constants.py index 9c153d7..14595be 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -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]},