From 2627114ad805328e3b442ee9d359ed594240bb47 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 29 Mar 2026 21:53:45 +0800 Subject: [PATCH 01/29] =?UTF-8?q?refactor(config):=20=E6=8B=86=E5=88=86con?= =?UTF-8?q?fig=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/config.py | 1975 ------------------------------------------ 1 file changed, 1975 deletions(-) delete mode 100644 app/models/config.py diff --git a/app/models/config.py b/app/models/config.py deleted file mode 100644 index 081e3f19..00000000 --- a/app/models/config.py +++ /dev/null @@ -1,1975 +0,0 @@ -# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software -# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with AUTO-MAS. If not, see . - -# Contact: DLmaster_361@163.com - - -import uuid -import json -import calendar -from pathlib import Path -from datetime import datetime -from typing import Callable - -from app.utils.constants import ( - UTC4, - UTC8, - MATERIALS_MAP, - RESOURCE_STAGE_INFO, - MAA_STAGE_KEY, - MAAEND_STAGE_BOOK, - MAAEND_STAGE_WITH_AB, - STARRAIL_STAGE_BOOK, -) -from .ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - MultipleUIDValidator, - BoolValidator, - OptionsValidator, - MultipleOptionsValidator, - RangeValidator, - VirtualConfigValidator, - FileValidator, - FolderValidator, - EmulatorPathValidator, - EncryptValidator, - UUIDValidator, - DateTimeValidator, - JSONValidator, - URLValidator, - UserNameValidator, - KeyValidator, - ArgumentValidator, - AdvancedArgumentValidator, -) -from .schema import TagItem - - -class EmulatorConfig(ConfigBase): - """模拟器配置""" - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 模拟器名称 - self.Info_Name = ConfigItem("Info", "Name", "新模拟器") - ## 模拟器类型 - self.Info_Type = ConfigItem( - "Info", - "Type", - "general", - OptionsValidator( - [ - "general", - "mumu", - "ldplayer", - # "nox", # 以下都是骗你的, 根本没有写~~ - # "memu", - # "blueStacks", - ] - ), - legacy_group="Data", - ) - ## 模拟器路径 - self.Info_Path = ConfigItem( - "Info", "Path", "", EmulatorPathValidator(self.Info_Type) - ) - ## 老板键快捷键配置 - self.Info_BossKey = ConfigItem( - "Info", "BossKey", "[ ]", JSONValidator(list), legacy_group="Data" - ) - ## 最大等待时间(秒) - self.Info_MaxWaitTime = ConfigItem( - "Info", "MaxWaitTime", 60, RangeValidator(1, 9999), legacy_group="Data" - ) - - super().__init__() - - -class Webhook(ConfigBase): - """Webhook 配置""" - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## Webhook 名称 - self.Info_Name = ConfigItem("Info", "Name", "新自定义 Webhook 通知") - ## 是否启用 - self.Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator()) - - ## Data ------------------------------------------------------------ - ## Webhook URL 地址 - self.Data_Url = ConfigItem("Data", "Url", "", URLValidator()) - ## 消息模板 - self.Data_Template = ConfigItem("Data", "Template", "") - ## 请求头 - self.Data_Headers = ConfigItem("Data", "Headers", "{ }", JSONValidator()) - ## 请求方法 - self.Data_Method = ConfigItem( - "Data", "Method", "POST", OptionsValidator(["POST", "GET"]) - ) - - super().__init__() - - -class QueueItem(ConfigBase): - """队列项配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 脚本 ID - self.Info_ScriptId = ConfigItem( - "Info", - "ScriptId", - "-", - MultipleUIDValidator("-", self.related_config, "ScriptConfig"), - ) - - super().__init__() - - -class TimeSet(ConfigBase): - """时间设置配置""" - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 是否启用 - self.Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator()) - ## 执行周期 - self.Info_Days = ConfigItem( - "Info", - "Days", - list(calendar.day_name), - MultipleOptionsValidator(list(calendar.day_name)), - ) - ## 执行时间 - self.Info_Time = ConfigItem("Info", "Time", "00:00", DateTimeValidator("%H:%M")) - - super().__init__() - - -class QueueConfig(ConfigBase): - """队列配置""" - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 队列名称 - 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", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] - ), - ) - - ## Data ------------------------------------------------------------ - ## 上次定时启动时间 - 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]) - - super().__init__() - - -class MaaUserConfig(ConfigBase): - """MAA用户配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 用户名称 - self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) - ## 用户 ID - self.Info_Id = ConfigItem("Info", "Id", "") - ## 密码 - self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) - ## 脚本模式 - 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_InfrastMode = ConfigItem( - "Info", - "InfrastMode", - "Normal", - OptionsValidator(["Normal", "Rotation", "Custom"]), - ) - ## 基建配置名称 - self.Info_InfrastName = ConfigItem( - "Info", "InfrastName", "-", VirtualConfigValidator(self.getInfrastName) - ) - ## 基建配置索引 - self.Info_InfrastIndex = ConfigItem( - "Info", "InfrastIndex", "-", VirtualConfigValidator(self.getInfrastIndex) - ) - ## 备注 - 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", "-") - ## 关卡 1 - self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-") - ## 关卡 2 - self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-") - ## 关卡 3 - self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-") - ## 备用关卡 - self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-") - ## 是否启用森空岛签到 - self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) - ## 森空岛 Token - self.Info_SklandToken = ConfigItem( - "Info", "SklandToken", "", EncryptValidator() - ) - ## 用户标签信息(虚拟字段,供前端显示) - self.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "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_CustomInfrast = ConfigItem( - "Data", "CustomInfrast", "{ }", JSONValidator() - ) - ## 基建配置索引数据 - self.Data_InfrastIndex = ConfigItem( - "Data", "InfrastIndex", "0", legacy_group="Info" - ) - - ## Task ------------------------------------------------------------ - ## 是否自动唤醒 - self.Task_IfStartUp = ConfigItem("Task", "IfStartUp", True, BoolValidator()) - ## 是否理智作战 - self.Task_IfFight = ConfigItem("Task", "IfFight", True, BoolValidator()) - ## 是否基建换班 - self.Task_IfInfrast = ConfigItem("Task", "IfInfrast", True, BoolValidator()) - ## 是否公开招募 - self.Task_IfRecruit = ConfigItem("Task", "IfRecruit", True, BoolValidator()) - ## 是否信用收支 - self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator()) - ## 是否领取奖励 - self.Task_IfAward = ConfigItem("Task", "IfAward", True, BoolValidator()) - ## 是否自动肉鸽 - self.Task_IfRoguelike = ConfigItem( - "Task", "IfRoguelike", False, BoolValidator() - ) - ## 是否生息演算 - self.Task_IfReclamation = ConfigItem( - "Task", "IfReclamation", False, BoolValidator() - ) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getInfrastName(self) -> str: - - if self.get("Info", "InfrastMode") != "Custom": - return "未使用自定义基建模式" - - infrast_data = json.loads(self.get("Data", "CustomInfrast")) - if ( - infrast_data.get("title", "文件标题") != "文件标题" - and infrast_data.get("description", "文件描述") != "文件描述" - ): - return f"{infrast_data['title']} - {infrast_data['description']}" - elif infrast_data.get("title", "文件标题") != "文件标题": - return str(infrast_data["title"]) - elif infrast_data.get("id", None): - return str(infrast_data["id"]) - else: - return "未命名自定义基建" - - def getInfrastIndex(self) -> str: - - if self.get("Info", "InfrastMode") != "Custom": - return "-1" - - infrast_data = json.loads(self.get("Data", "CustomInfrast")) - - if len(infrast_data.get("plans", [])) == 0: - return "-1" - - for i, plan in enumerate(infrast_data.get("plans", [])): - - for t in plan.get("period", []): - if ( - datetime.strptime(t[0], "%H:%M").time() - <= datetime.now().time() - <= datetime.strptime(t[1], "%H:%M").time() - ): - return str(i) - - else: - return self.get("Data", "InfrastIndex") or "0" - - def getTags(self) -> str: - """生成用户标签列表,返回JSON字符串格式的TagItem列表""" - tags = [] - - # 人工排查状态标签 - if not self.get("Data", "IfPassCheck"): - tags.append({"text": "人工排查未通过", "color": "red"}) - - # 日常代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "日常:未代理", "color": "orange"}) - - # 森空岛签到标签(使用东8区时间) - if self.get("Info", "IfSkland"): - if ( - datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC8).date() - ): - tags.append({"text": "森空岛:已签到", "color": "green"}) - else: - tags.append({"text": "森空岛:未签到", "color": "orange"}) - else: - tags.append({"text": "森空岛:禁用", "color": "red"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 基建模式标签 - infrast_mode = self.get("Info", "InfrastMode") - if self.get("Task", "IfInfrast"): - if infrast_mode == "Normal": - infrast_text = "基建:常规" - elif infrast_mode == "Rotation": - infrast_text = "基建:轮换" - elif infrast_mode == "Custom": - infrast_text = f"基建:{self.getInfrastName() if len(self.getInfrastName()) < 10 else self.getInfrastName()[:10] + '...'}" - else: - infrast_text = "基建:开启" - tags.append({"text": infrast_text, "color": "purple"}) - else: - tags.append({"text": "基建:关闭", "color": "red"}) - - # 关卡信息标签 - if self.get("Info", "StageMode") == "Fixed": - plan_data = { - stage_key: self.get_stage_zh(self.get("Info", stage_key)) - for stage_key in MAA_STAGE_KEY[2:] - } - tag_color = "blue" - else: - plan = self.related_config["PlanConfig"][ - uuid.UUID(self.get("Info", "StageMode")) - ] - if isinstance(plan, MaaPlanConfig): - plan_data = { - stage_key: self.get_stage_zh( - plan.get_current_info(stage_key).getValue() - ) - for stage_key in MAA_STAGE_KEY[2:] - } - tag_color = "green" - # 主关卡 - tags.append({"text": f"主关卡:{plan_data['Stage']}", "color": tag_color}) - # 备选关卡(合并显示) - backup_stages = [ - plan_data[f"Stage_{i}"] - for i in range(1, 4) - if plan_data[f"Stage_{i}"] != "禁用" - ] - if backup_stages: - tags.append( - {"text": f"备选:{', '.join(backup_stages)}", "color": tag_color} - ) - # 剩余关卡 - if plan_data["Stage_Remain"] != "禁用": - tags.append( - {"text": f"剩余:{plan_data['Stage_Remain']}", "color": tag_color} - ) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - @staticmethod - def get_stage_zh(stage: str) -> str: - - for stage_info in RESOURCE_STAGE_INFO: - if stage_info.get("value") == stage: - return ( - stage_info.get("text", stage) - .replace("经验-6/5", "经验") - .replace("龙门币-6/5", "龙门币") - .replace("红票-5", "红票") - .replace("技能-5", "技能") - .replace("碳-5", "碳") - ) - else: - return stage - - -class MaaConfig(ConfigBase): - """MAA配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## MAA 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本") - ## MAA 路径 - self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) - - ## Emulator -------------------------------------------------------- - ## 模拟器 ID - self.Emulator_Id = ConfigItem( - "Emulator", - "Id", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Emulator_Index = ConfigItem("Emulator", "Index", "-") - - ## Run ------------------------------------------------------------- - ## 任务切换方式 - self.Run_TaskTransitionMethod = ConfigItem( - "Run", - "TaskTransitionMethod", - "ExitEmulator", - OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]), - ) - ## 代理次数限制 - self.Run_ProxyTimesLimit = ConfigItem( - "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) - ) - ## 运行次数限制 - 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_AnnihilationAvoidWaste = ConfigItem( - "Run", "AnnihilationAvoidWaste", False, BoolValidator() - ) - - self.UserData = MultipleConfig([MaaUserConfig]) - - super().__init__() - - -class MaaEndUserConfig(ConfigBase): - """MaaEnd用户配置""" - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 用户名称 - self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) - ## 是否启用 - self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) - ## 用户ID - self.Info_Id = ConfigItem("Info", "Id", "") - ## 密码 - self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) - ## 配置模式 - self.Info_Mode = ConfigItem( - "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) - ) - ## 资源名称 - self.Info_Resource = ConfigItem( - "Info", "Resource", "官服", OptionsValidator(["官服"]) - ) - ## 剩余天数 - self.Info_RemainedDay = ConfigItem( - "Info", "RemainedDay", -1, RangeValidator(-1, 9999) - ) - ## 备注 - self.Info_Notes = ConfigItem("Info", "Notes", "无") - ## 是否启用森空岛签到 - self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) - ## 森空岛 Token - self.Info_SklandToken = ConfigItem( - "Info", "SklandToken", "", EncryptValidator() - ) - ## 用户标签信息 - self.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## Task ------------------------------------------------------------ - ## 协议空间选项 - self.Task_ProtocolSpaceTab = ConfigItem( - "Task", - "ProtocolSpaceTab", - "OperatorProgression", - OptionsValidator( - ["OperatorProgression", "WeaponProgression", "CrisisDrills"] - ), - ) - self.Task_OperatorProgression = ConfigItem( - "Task", - "OperatorProgression", - "OperatorEXP", - OptionsValidator(["OperatorEXP", "Promotions", "T-Creds", "SkillUp"]), - ) - self.Task_WeaponProgression = ConfigItem( - "Task", - "WeaponProgression", - "WeaponEXP", - OptionsValidator(["WeaponEXP", "WeaponTune"]), - ) - self.Task_CrisisDrills = ConfigItem( - "Task", - "CrisisDrills", - "AdvancedProgression1", - OptionsValidator( - [ - "AdvancedProgression1", - "AdvancedProgression2", - "AdvancedProgression3", - "AdvancedProgression4", - "AdvancedProgression5", - ] - ), - ) - self.Task_RewardsSetOption = ConfigItem( - "Task", - "RewardsSetOption", - "RewardsSetA", - OptionsValidator(["RewardsSetA", "RewardsSetB"]), - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") - ) - ## 代理次数 - self.Data_ProxyTimes = ConfigItem( - "Data", "ProxyTimes", 0, RangeValidator(0, 9999) - ) - ## 上次代理状态 - self.Data_LastProxyStatus = ConfigItem( - "Data", - "LastProxyStatus", - "未知", - OptionsValidator(["未知", "成功", "失败"]), - ) - ## 上次森空岛签到日期 - self.Data_LastSklandDate = ConfigItem( - "Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") - ) - ## 是否通过检查 - self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator()) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getTags(self) -> str: - """生成用户标签列表,返回JSON字符串格式的TagItem列表""" - tags = [] - # 人工排查状态标签 - if not self.get("Data", "IfPassCheck"): - tags.append({"text": "人工排查未通过", "color": "red"}) - - # 上次代理标签 - tags.append( - { - "text": f"上次:{self.get('Data', 'LastProxyStatus')}", - "color": ( - "red" if self.get("Data", "LastProxyStatus") == "失败" else "green" - ), - } - ) - - # 日常代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "日常:未代理", "color": "orange"}) - - # 森空岛签到标签(使用东8区时间) - if self.get("Info", "IfSkland"): - if ( - datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC8).date() - ): - tags.append({"text": "森空岛:已签到", "color": "green"}) - else: - tags.append({"text": "森空岛:未签到", "color": "orange"}) - else: - tags.append({"text": "森空岛:禁用", "color": "red"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 关卡信息标签 - stage = self.get("Task", self.get("Task", "ProtocolSpaceTab")) - stage_ab = ( - f" - {self.get("Task", "RewardsSetOption")[-1]}" - if stage in MAAEND_STAGE_WITH_AB - else "" - ) - tags.append({"text": MAAEND_STAGE_BOOK[stage] + stage_ab, "color": "blue"}) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - -class MaaEndConfig(ConfigBase): - """MaaEnd配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## MaaEnd 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新 MaaEnd 脚本") - ## MaaEnd 路径 - self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) - - ## Run ------------------------------------------------------------- - ## 运行超时阈值 - self.Run_RunTimeLimit = ConfigItem( - "Run", "RunTimeLimit", 10, RangeValidator(1, 9999) - ) - ## 每日代理次数限制 - self.Run_ProxyTimesLimit = ConfigItem( - "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) - ) - ## 运行次数限制 - self.Run_RunTimesLimit = ConfigItem( - "Run", "RunTimesLimit", 3, RangeValidator(1, 9999) - ) - - ## Game ------------------------------------------------------------ - ## 控制器类型 - self.Game_ControllerType = ConfigItem( - "Game", - "ControllerType", - "Win32-Window", - OptionsValidator( - [ - "Win32-Window", - "Win32-Front", - "Win32-Window-Background", - "ADB", - ] - ), - ) - ## 终末地游戏路径 - self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator()) - ## 终末地游戏启动参数 - self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator()) - ## 等待时间(秒) - self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999)) - ## 模拟器 ID - self.Game_EmulatorId = ConfigItem( - "Game", - "EmulatorId", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-") - ## 结束后是否关闭游戏 - self.Game_CloseOnFinish = ConfigItem( - "Game", "CloseOnFinish", True, BoolValidator() - ) - - self.UserData = MultipleConfig([MaaEndUserConfig]) - - super().__init__() - - -class SrcUserConfig(ConfigBase): - """SRC用户配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 用户名称 - self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) - ## 是否启用 - self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) - ## 用户 ID - self.Info_Id = ConfigItem("Info", "Id", "") - ## 密码 - self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) - ## 脚本模式 - self.Info_Mode = ConfigItem( - "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) - ) - ## 游戏服务器 - self.Info_Server = ConfigItem( - "Info", - "Server", - "CN-Official", - OptionsValidator( - [ - "CN-Official", - "CN-Bilibili", - "VN-Official", - "OVERSEA-America", - "OVERSEA-Asia", - "OVERSEA-Europe", - "OVERSEA-TWHKMO", - ] - ), - ) - ## 剩余天数 - self.Info_RemainedDay = ConfigItem( - "Info", "RemainedDay", -1, RangeValidator(-1, 9999) - ) - ## 备注 - self.Info_Notes = ConfigItem("Info", "Notes", "无") - ## 用户标签信息 - self.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## 关卡配置---------------------------------------------------------- - ## 关卡通道 - self.Stage_Channel = ConfigItem( - "Stage", - "Channel", - "Relic", - OptionsValidator(["Relic", "Materials", "Ornament"]), - ) - ## 遗器关卡 - self.Stage_Relic = ConfigItem( - "Stage", - "Relic", - "-", - OptionsValidator( - [ - "-", - "Cavern_of_Corrosion_Path_of_Possession", - "Cavern_of_Corrosion_Path_of_Hidden_Salvation", - "Cavern_of_Corrosion_Path_of_Thundersurge", - "Cavern_of_Corrosion_Path_of_Aria", - "Cavern_of_Corrosion_Path_of_Uncertainty", - "Cavern_of_Corrosion_Path_of_Cavalier", - "Cavern_of_Corrosion_Path_of_Dreamdive" - "Cavern_of_Corrosion_Path_of_Darkness", - "Cavern_of_Corrosion_Path_of_Elixir_Seekers", - "Cavern_of_Corrosion_Path_of_Conflagration", - "Cavern_of_Corrosion_Path_of_Holy_Hymn", - "Cavern_of_Corrosion_Path_of_Providence", - "Cavern_of_Corrosion_Path_of_Drifting", - "Cavern_of_Corrosion_Path_of_Jabbing_Punch", - "Cavern_of_Corrosion_Path_of_Gelid_Wind", - ] - ), - ) - ## 材料关卡 - self.Stage_Materials = ConfigItem( - "Stage", - "Materials", - "-", - OptionsValidator( - [ - "-", - "Calyx_Golden_Memories_Planarcadia", - "Calyx_Golden_Aether_Planarcadia", - "Calyx_Golden_Treasures_Planarcadia", - "Calyx_Golden_Memories_Amphoreus", - "Calyx_Golden_Aether_Amphoreus", - "Calyx_Golden_Treasures_Amphoreus", - "Calyx_Golden_Memories_Penacony", - "Calyx_Golden_Aether_Penacony", - "Calyx_Golden_Treasures_Penacony", - "Calyx_Golden_Memories_The_Xianzhou_Luofu", - "Calyx_Golden_Aether_The_Xianzhou_Luofu", - "Calyx_Golden_Treasures_The_Xianzhou_Luofu", - "Calyx_Golden_Memories_Jarilo_VI", - "Calyx_Golden_Aether_Jarilo_VI", - "Calyx_Golden_Treasures_Jarilo_VI", - "Calyx_Crimson_Destruction_Herta_StorageZone", - "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", - "Calyx_Crimson_Preservation_Herta_SupplyZone", - "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", - "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", - "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", - "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", - "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", - "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", - "Calyx_Crimson_Erudition_Jarilo_RivetTown", - "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", - "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", - "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", - "Calyx_Crimson_Nihility_Jarilo_GreatMine", - "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", - "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", - "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", - "Stagnant_Shadow_Quanta", - "Stagnant_Shadow_Gust", - "Stagnant_Shadow_Fulmination", - "Stagnant_Shadow_Blaze", - "Stagnant_Shadow_Spike", - "Stagnant_Shadow_Rime", - "Stagnant_Shadow_Mirage", - "Stagnant_Shadow_Icicle", - "Stagnant_Shadow_Doom", - "Stagnant_Shadow_Puppetry", - "Stagnant_Shadow_Abomination", - "Stagnant_Shadow_Scorch", - "Stagnant_Shadow_Celestial", - "Stagnant_Shadow_Perdition", - "Stagnant_Shadow_Nectar", - "Stagnant_Shadow_Roast", - "Stagnant_Shadow_Ire", - "Stagnant_Shadow_Duty", - "Stagnant_Shadow_Timbre", - "Stagnant_Shadow_Mechwolf", - "Stagnant_Shadow_Gloam", - "Stagnant_Shadow_Sloggyre", - "Stagnant_Shadow_Gelidmoon", - "Stagnant_Shadow_Deepsheaf", - "Stagnant_Shadow_Cinders", - "Stagnant_Shadow_Sirens", - "Stagnant_Shadow_Ashes", - "Stagnant_Shadow_Soundburst", - ] - ), - ) - ## 饰品关卡 - self.Stage_Ornament = ConfigItem( - "Stage", - "Ornament", - "-", - OptionsValidator( - [ - "-", - "Divergent_Universe_Within_the_West_Wind", - "Divergent_Universe_Moonlit_Blood", - "Divergent_Universe_Unceasing_Strife", - "Divergent_Universe_Famished_Worker", - "Divergent_Universe_Eternal_Comedy", - "Divergent_Universe_To_Sweet_Dreams", - "Divergent_Universe_Pouring_Blades", - "Divergent_Universe_Fruit_of_Evil", - "Divergent_Universe_Permafrost", - "Divergent_Universe_Gentle_Words", - "Divergent_Universe_Smelted_Heart", - "Divergent_Universe_Untoppled_Walls", - ] - ), - ) - ## 使用储备开拓力 - self.Stage_ExtractReservedTrailblazePower = ConfigItem( - "Stage", "ExtractReservedTrailblazePower", False, BoolValidator() - ) - ## 使用燃料 - self.Stage_UseFuel = ConfigItem("Stage", "UseFuel", False, BoolValidator()) - ## 保留的燃料数量 - self.Stage_FuelReserve = ConfigItem( - "Stage", "FuelReserve", 5, RangeValidator(0, 9999) - ) - ## 历战余响关卡 - self.Stage_EchoOfWar = ConfigItem( - "Stage", - "EchoOfWar", - "-", - OptionsValidator( - [ - "-", - "Echo_of_War_Rusted_Crypt_of_the_Iron_Carcass", - "Echo_of_War_Glance_of_Twilight", - "Echo_of_War_Inner_Beast_Battlefield", - "Echo_of_War_Salutations_of_Ashen_Dreams", - "Echo_of_War_Borehole_Planet_Past_Nightmares", - "Echo_of_War_Divine_Seed", - "Echo_of_War_End_of_the_Eternal_Freeze", - "Echo_of_War_Destruction_Beginning", - ] - ), - ) - ## 模拟宇宙关卡 - self.Stage_SimulatedUniverseWorld = ConfigItem( - "Stage", - "SimulatedUniverseWorld", - "-", - OptionsValidator( - [ - "-", - "Simulated_Universe_World_3", - "Simulated_Universe_World_4", - "Simulated_Universe_World_5", - "Simulated_Universe_World_6", - "Simulated_Universe_World_8", - ] - ), - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "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()) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getTags(self) -> str: - """生成用户标签列表,返回JSON字符串格式的TagItem列表""" - tags = [] - - # 人工排查状态标签 - if not self.get("Data", "IfPassCheck"): - tags.append({"text": "人工排查未通过", "color": "red"}) - - # 日常代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "日常:未代理", "color": "orange"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 关卡信息标签 - tags.append( - { - "text": f"关卡:{STARRAIL_STAGE_BOOK.get(self.get('Stage', self.get('Stage', 'Channel')), '未知关卡')}", - "color": "blue", - } - ) - tags.append( - { - "text": f"周本:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'EchoOfWar'), '未知关卡')}", - "color": "blue", - } - ) - tags.append( - { - "text": f"模拟宇宙:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'SimulatedUniverseWorld'), '未知关卡')}", - "color": "blue", - } - ) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - -class SrcConfig(ConfigBase): - """SRC配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## SRC 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新 SRC 脚本") - ## SRC 路径 - self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) - - ## Emulator -------------------------------------------------------- - ## 模拟器 ID - self.Emulator_Id = ConfigItem( - "Emulator", - "Id", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Emulator_Index = ConfigItem("Emulator", "Index", "-") - - ## Run ------------------------------------------------------------- - ## 任务切换方式 - self.Run_TaskTransitionMethod = ConfigItem( - "Run", - "TaskTransitionMethod", - "ExitGame", - OptionsValidator(["ExitGame", "ExitEmulator"]), - ) - ## 代理次数限制 - 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([SrcUserConfig]) - - super().__init__() - - -class MaaPlanConfig(ConfigBase): - """MAA计划表配置""" - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 计划表名称 - 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", *calendar.day_name]: - 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"]), - ) - - ## 理智关卡 - for name in MAA_STAGE_KEY[2:]: - # Stage、Stage_1、Stage_2、Stage_3、Stage_Remain - self.config_item_dict[group][name] = ConfigItem(group, name, "-") - - for name in MAA_STAGE_KEY: - setattr(self, f"{group}_{name}", self.config_item_dict[group][name]) - - super().__init__() - - 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": - - today = datetime.now(tz=UTC4).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: - - ## Info ------------------------------------------------------------ - ## 用户名称 - 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.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") - ) - ## 代理次数 - self.Data_ProxyTimes = ConfigItem( - "Data", "ProxyTimes", 0, RangeValidator(0, 9999) - ) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getTags(self) -> str: - """生成通用用户标签列表""" - tags = [] - - # 任务代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"任务:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "任务:未代理", "color": "orange"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - -class GeneralConfig(ConfigBase): - """通用配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - - ## Info ------------------------------------------------------------ - ## 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新通用脚本") - ## 根目录路径 - self.Info_RootPath = ConfigItem( - "Info", "RootPath", str(Path.cwd()), FileValidator() - ) - - ## Script ---------------------------------------------------------- - ## 脚本路径 - self.Script_ScriptPath = ConfigItem( - "Script", "ScriptPath", str(Path.cwd()), FileValidator() - ) - ## 脚本参数 - self.Script_Arguments = ConfigItem( - "Script", "Arguments", "", AdvancedArgumentValidator() - ) - ## 是否追踪进程 - self.Script_IfTrackProcess = ConfigItem( - "Script", "IfTrackProcess", False, BoolValidator() - ) - ## 追踪进程的名称 - self.Script_TrackProcessName = ConfigItem("Script", "TrackProcessName", "") - ## 追踪进程的文件路径 - self.Script_TrackProcessExe = ConfigItem("Script", "TrackProcessExe", "") - ## 追踪进程的启动命令行参数 - self.Script_TrackProcessCmdline = ConfigItem( - "Script", "TrackProcessCmdline", "", ArgumentValidator() - ) - 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", "") - - ## Game ------------------------------------------------------------ - ## 是否启用游戏 - self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator()) - ## 游戏类型 - self.Game_Type = ConfigItem( - "Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client", "URL"]) - ) - ## 游戏路径 - self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator()) - ## 自定义协议URL - self.Game_URL = ConfigItem("Game", "URL", "") - ## 游戏进程名称 - self.Game_ProcessName = ConfigItem("Game", "ProcessName", "") - ## 游戏启动参数 - self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator()) - ## 等待时间(秒) - self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999)) - ## 是否强制关闭 - self.Game_IfForceClose = ConfigItem( - "Game", "IfForceClose", False, BoolValidator() - ) - ## 模拟器 ID - self.Game_EmulatorId = ConfigItem( - "Game", - "EmulatorId", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-") - - ## Run ------------------------------------------------------------- - ## 代理次数限制 - 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]) - - super().__init__() - - -class ToolsConfig(ConfigBase): - """工具配置""" - - def __init__(self) -> None: - - self.ArknightsPC_Enabled = ConfigItem( - "ArknightsPC", "Enabled", False, BoolValidator() - ) - self.ArknightsPC_PauseKey = ConfigItem( - "ArknightsPC", "PauseKey", "f10", KeyValidator("f10") - ) - self.ArknightsPC_SelectDeployedKey = ConfigItem( - "ArknightsPC", "SelectDeployedKey", "w", KeyValidator("w") - ) - self.ArknightsPC_UseSkillKey = ConfigItem( - "ArknightsPC", "UseSkillKey", "r", KeyValidator("r") - ) - self.ArknightsPC_RetreatKey = ConfigItem( - "ArknightsPC", "RetreatKey", "t", KeyValidator("t") - ) - self.ArknightsPC_NextFrameKey = ConfigItem( - "ArknightsPC", "NextFrameKey", "f", KeyValidator("f") - ) - self.ArknightsPC_AnotherQuitKey = ConfigItem( - "ArknightsPC", "AnotherQuitKey", "space", KeyValidator("space") - ) - self.ArknightsPC_Status = ConfigItem( - "ArknightsPC", - "Status", - "-", - VirtualConfigValidator(self.arknights_pc_status), - ) - - self.arknights_pc_running = False - self.arknights_pc_get_connected: Callable[[], bool] = lambda: False - - super().__init__() - - @property - def arknights_pc_connected(self) -> bool: - - return self.arknights_pc_get_connected() - - def arknights_pc_status(self) -> str: - - if not self.get("ArknightsPC", "Enabled"): - return TagItem(text="未启用", color="gray").model_dump_json() - else: - if self.arknights_pc_running: - if self.arknights_pc_connected: - return TagItem(text="运行中", color="green").model_dump_json() - else: - return TagItem(text="未连接", color="red").model_dump_json() - else: - return TagItem(text="已暂停", color="yellow").model_dump_json() - - @property - def arknights_pc_keys(self) -> list[str]: - """获取明日方舟 PC 按键配置""" - - return [ - self.get("ArknightsPC", _) - for _ in ( - "SelectDeployedKey", - "UseSkillKey", - "RetreatKey", - "NextFrameKey", - "AnotherQuitKey", - ) - ] - - -class GlobalConfig(ConfigBase): - """全局配置""" - - def __init__(self): - - ## Function --------------------------------------------------------- - ## 历史记录保留时间(天) - self.Function_HistoryRetentionTime = ConfigItem( - "Function", - "HistoryRetentionTime", - 0, - OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]), - ) - ## 是否允许睡眠 - self.Function_IfAllowSleep = ConfigItem( - "Function", "IfAllowSleep", False, BoolValidator() - ) - ## 是否启用静默模式 - self.Function_IfSilence = ConfigItem( - "Function", "IfSilence", False, BoolValidator() - ) - ## 是否同意 Bilibili 协议 - self.Function_IfAgreeBilibili = ConfigItem( - "Function", "IfAgreeBilibili", False, BoolValidator() - ) - ## 是否屏蔽模拟器广告 - self.Function_IfBlockAd = ConfigItem( - "Function", "IfBlockAd", False, BoolValidator() - ) - - ## Voice ------------------------------------------------------------ - ## 是否启用语音 - self.Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator()) - ## 语音类型 - self.Voice_Type = ConfigItem( - "Voice", "Type", "simple", OptionsValidator(["simple", "noisy"]) - ) - - ## Start ------------------------------------------------------------ - ## 是否自动启动 - self.Start_IfSelfStart = ConfigItem( - "Start", "IfSelfStart", False, BoolValidator() - ) - ## 是否启动时直接最小化 - self.Start_IfMinimizeDirectly = ConfigItem( - "Start", "IfMinimizeDirectly", False, BoolValidator() - ) - - ## UI --------------------------------------------------------------- - ## 是否显示托盘图标 - self.UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator()) - ## 是否关闭到托盘 - self.UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator()) - - ## Notify ----------------------------------------------------------- - ## 任务结果推送时间 - self.Notify_SendTaskResultTime = ConfigItem( - "Notify", - "SendTaskResultTime", - "不推送", - OptionsValidator(["不推送", "任何时刻", "仅失败时"]), - ) - ## 是否发送统计信息 - self.Notify_IfSendStatistic = ConfigItem( - "Notify", "IfSendStatistic", False, BoolValidator() - ) - ## 是否发送六星通知 - self.Notify_IfSendSixStar = ConfigItem( - "Notify", "IfSendSixStar", False, BoolValidator() - ) - ## 是否推送系统通知 - self.Notify_IfPushPlyer = ConfigItem( - "Notify", "IfPushPlyer", False, BoolValidator() - ) - ## 是否发送邮件 - self.Notify_IfSendMail = ConfigItem( - "Notify", "IfSendMail", False, BoolValidator() - ) - ## 是否发送Koishi通知 - self.Notify_IfKoishiSupport = ConfigItem( - "Notify", "IfKoishiSupport", False, BoolValidator() - ) - ## Koishi WebSocket 服务器地址 - self.Notify_KoishiServerAddress = ConfigItem( - "Notify", - "KoishiServerAddress", - "ws://localhost:5140/AUTO_MAS", - URLValidator(), - ) - ## Koishi Token - self.Notify_KoishiToken = ConfigItem("Notify", "KoishiToken", "") - ## SMTP 服务器地址 - self.Notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "") - ## 邮箱授权码 - self.Notify_AuthorizationCode = ConfigItem( - "Notify", "AuthorizationCode", "", EncryptValidator() - ) - ## 发件地址 - self.Notify_FromAddress = ConfigItem("Notify", "FromAddress", "") - ## 收件地址 - self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - ## Update ----------------------------------------------------------- - ## 是否自动更新 - self.Update_IfAutoUpdate = ConfigItem( - "Update", "IfAutoUpdate", False, BoolValidator() - ) - ## 更新源 - self.Update_Source = ConfigItem( - "Update", - "Source", - "GitHub", - OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]), - ) - ## 更新频道 - self.Update_Channel = ConfigItem( - "Update", "Channel", "stable", OptionsValidator(["stable", "beta"]) - ) - ## 代理地址 - self.Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "") - ## 镜像站 CDK - self.Update_MirrorChyanCDK = ConfigItem( - "Update", "MirrorChyanCDK", "", EncryptValidator() - ) - - ## Data ------------------------------------------------------------- - ## 唯一标识符 - self.Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) - ## 上次统计上传时间 - self.Data_LastStatisticsUpload = ConfigItem( - "Data", - "LastStatisticsUpload", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## 上次关卡更新时间 - self.Data_LastStageUpdated = ConfigItem( - "Data", - "LastStageUpdated", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## 关卡数据的版本标识符 - self.Data_StageETag = ConfigItem("Data", "StageETag", "") - ## 关卡信息数据 - self.Data_StageData = ConfigItem( - "Data", "StageData", "{ }", JSONValidator(), legacy_name="Stage" - ) - ## 关卡信息 - self.Data_Stage = ConfigItem( - "Data", "Stage", "-", VirtualConfigValidator(self.getStage) - ) - ## 上次公告更新时间 - self.Data_LastNoticeUpdated = ConfigItem( - "Data", - "LastNoticeUpdated", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## 公告的版本标识符 - self.Data_NoticeETag = ConfigItem("Data", "NoticeETag", "") - ## 是否显示公告 - self.Data_IfShowNotice = ConfigItem( - "Data", "IfShowNotice", True, BoolValidator() - ) - ## 公告内容 - self.Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator()) - ## 上次 Web 配置更新时间 - self.Data_LastWebConfigUpdated = ConfigItem( - "Data", - "LastWebConfigUpdated", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## Web 配置 - self.Data_WebConfig = ConfigItem( - "Data", "WebConfig", "[ ]", JSONValidator(list) - ) - super().__init__() - - ## 模拟器配置列表 - self.EmulatorConfig = MultipleConfig([EmulatorConfig]) - ## 计划表配置列表 - self.PlanConfig = MultipleConfig([MaaPlanConfig]) - ## 脚本配置列表 - self.ScriptConfig = MultipleConfig( - [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] - ) - ## 队列配置列表 - self.QueueConfig = MultipleConfig([QueueConfig]) - ## 工具箱配置 - self.ToolsConfig = ToolsConfig() - - MaaConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - MaaEndConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - SrcConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - GeneralConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - MaaUserConfig.related_config["PlanConfig"] = self.PlanConfig - QueueItem.related_config["ScriptConfig"] = self.ScriptConfig - - def getStage(self) -> str: - """获取关卡信息""" - - try: - raw_stage_data = json.loads(self.get("Data", "StageData")) - - activity_stage_drop_info = [] - activity_stage_combox = [] - - for side_story in raw_stage_data.values(): - if ( - datetime.strptime( - side_story["Activity"]["UtcStartTime"], "%Y/%m/%d %H:%M:%S" - ).replace(tzinfo=UTC8) - < datetime.now(tz=UTC8) - < datetime.strptime( - side_story["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S" - ).replace(tzinfo=UTC8) - ): - for stage in side_story["Stages"]: - activity_stage_combox.append( - {"label": stage["Display"], "value": stage["Value"]} - ) - - if "SSReopen" not in stage["Display"]: - - if stage["Drop"] in MATERIALS_MAP: - drop_id = stage["Drop"] - elif "玉" in stage["Drop"]: - drop_id = "30012" - else: - drop_id = "NotFound" - - activity_stage_drop_info.append( - { - "Display": stage["Display"], - "Value": stage["Value"], - "Drop": drop_id, - "DropName": MATERIALS_MAP.get( - stage["Drop"], stage["Drop"] - ), - "Activity": side_story["Activity"], - } - ) - except: - return "{ }" - - stage_data = {"Info": activity_stage_drop_info} - - for day in range(0, 8): - res_stage = [] - - for stage in RESOURCE_STAGE_INFO: - if day in stage["days"] or day == 0: - res_stage.append({"label": stage["text"], "value": stage["value"]}) - - stage_data[calendar.day_name[day - 1] if day > 0 else "ALL"] = ( - res_stage[0:1] + activity_stage_combox + res_stage[1:] - ) - - return json.dumps(stage_data, ensure_ascii=False) - - -CLASS_BOOK = { - "MAA": MaaConfig, - "MaaPlan": MaaPlanConfig, - "SRC": SrcConfig, - "MaaEnd": MaaEndConfig, - "General": GeneralConfig, -} -"""配置类映射表""" From 83747cbc7744ab3a631fffeca261dc471490addc Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 29 Mar 2026 21:54:11 +0800 Subject: [PATCH 02/29] =?UTF-8?q?refactor(config):=20=E6=8B=86=E5=88=86con?= =?UTF-8?q?fig=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/config/__init__.py | 26 ++ app/models/config/common.py | 170 ++++++++++ app/models/config/general.py | 273 ++++++++++++++++ app/models/config/global_config.py | 378 ++++++++++++++++++++++ app/models/config/maa.py | 494 +++++++++++++++++++++++++++++ app/models/config/maaend.py | 310 ++++++++++++++++++ app/models/config/src.py | 410 ++++++++++++++++++++++++ 7 files changed, 2061 insertions(+) create mode 100644 app/models/config/__init__.py create mode 100644 app/models/config/common.py create mode 100644 app/models/config/general.py create mode 100644 app/models/config/global_config.py create mode 100644 app/models/config/maa.py create mode 100644 app/models/config/maaend.py create mode 100644 app/models/config/src.py diff --git a/app/models/config/__init__.py b/app/models/config/__init__.py new file mode 100644 index 00000000..6d02c65d --- /dev/null +++ b/app/models/config/__init__.py @@ -0,0 +1,26 @@ +from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook +from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from .maaend import MaaEndConfig, MaaEndUserConfig +from .src import SrcConfig, SrcUserConfig +from .general import GeneralConfig, GeneralUserConfig +from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig + +__all__ = [ + "EmulatorConfig", + "Webhook", + "QueueItem", + "TimeSet", + "QueueConfig", + "MaaPlanConfig", + "MaaUserConfig", + "MaaConfig", + "MaaEndUserConfig", + "MaaEndConfig", + "SrcUserConfig", + "SrcConfig", + "GeneralUserConfig", + "GeneralConfig", + "ToolsConfig", + "GlobalConfig", + "CLASS_BOOK", +] diff --git a/app/models/config/common.py b/app/models/config/common.py new file mode 100644 index 00000000..1f024019 --- /dev/null +++ b/app/models/config/common.py @@ -0,0 +1,170 @@ +import calendar + +from ..ConfigBase import ( + ConfigBase, + MultipleConfig, + ConfigItem, + MultipleUIDValidator, + BoolValidator, + OptionsValidator, + MultipleOptionsValidator, + RangeValidator, + DateTimeValidator, + JSONValidator, + URLValidator, + EmulatorPathValidator, +) + + +class EmulatorConfig(ConfigBase): + """模拟器配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 模拟器名称 + self.Info_Name = ConfigItem("Info", "Name", "新模拟器") + ## 模拟器类型 + self.Info_Type = ConfigItem( + "Info", + "Type", + "general", + OptionsValidator( + [ + "general", + "mumu", + "ldplayer", + # "nox", # 以下都是骗你的, 根本没有写~~ + # "memu", + # "blueStacks", + ] + ), + legacy_group="Data", + ) + ## 模拟器路径 + self.Info_Path = ConfigItem( + "Info", "Path", "", EmulatorPathValidator(self.Info_Type) + ) + ## 老板键快捷键配置 + self.Info_BossKey = ConfigItem( + "Info", "BossKey", "[ ]", JSONValidator(list), legacy_group="Data" + ) + ## 最大等待时间(秒) + self.Info_MaxWaitTime = ConfigItem( + "Info", "MaxWaitTime", 60, RangeValidator(1, 9999), legacy_group="Data" + ) + + super().__init__() + + +class Webhook(ConfigBase): + """Webhook 配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## Webhook 名称 + self.Info_Name = ConfigItem("Info", "Name", "新自定义 Webhook 通知") + ## 是否启用 + self.Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator()) + + ## Data ------------------------------------------------------------ + ## Webhook URL 地址 + self.Data_Url = ConfigItem("Data", "Url", "", URLValidator()) + ## 消息模板 + self.Data_Template = ConfigItem("Data", "Template", "") + ## 请求头 + self.Data_Headers = ConfigItem("Data", "Headers", "{ }", JSONValidator()) + ## 请求方法 + self.Data_Method = ConfigItem( + "Data", "Method", "POST", OptionsValidator(["POST", "GET"]) + ) + + super().__init__() + + +class QueueItem(ConfigBase): + """队列项配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 脚本 ID + self.Info_ScriptId = ConfigItem( + "Info", + "ScriptId", + "-", + MultipleUIDValidator("-", self.related_config, "ScriptConfig"), + ) + + super().__init__() + + +class TimeSet(ConfigBase): + """时间设置配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 是否启用 + self.Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator()) + ## 执行周期 + self.Info_Days = ConfigItem( + "Info", + "Days", + list(calendar.day_name), + MultipleOptionsValidator(list(calendar.day_name)), + ) + ## 执行时间 + self.Info_Time = ConfigItem("Info", "Time", "00:00", DateTimeValidator("%H:%M")) + + super().__init__() + + +class QueueConfig(ConfigBase): + """队列配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 队列名称 + 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", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] + ), + ) + + ## Data ------------------------------------------------------------ + ## 上次定时启动时间 + 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]) + + super().__init__() + + +__all__ = ["EmulatorConfig", "Webhook", "QueueItem", "TimeSet", "QueueConfig"] diff --git a/app/models/config/general.py b/app/models/config/general.py new file mode 100644 index 00000000..fb94dc65 --- /dev/null +++ b/app/models/config/general.py @@ -0,0 +1,273 @@ +import json +from pathlib import Path +from datetime import datetime + +from app.utils.constants import UTC4 +from ..ConfigBase import ( + ConfigBase, + MultipleConfig, + ConfigItem, + MultipleUIDValidator, + BoolValidator, + OptionsValidator, + RangeValidator, + VirtualConfigValidator, + FileValidator, + DateTimeValidator, + UserNameValidator, + ArgumentValidator, + AdvancedArgumentValidator, +) +from .common import Webhook + + +class GeneralUserConfig(ConfigBase): + """通用脚本用户配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 用户名称 + 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.Info_Tag = ConfigItem( + "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) + ) + + ## Data ------------------------------------------------------------ + ## 上次代理日期 + self.Data_LastProxyDate = ConfigItem( + "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") + ) + ## 代理次数 + self.Data_ProxyTimes = ConfigItem( + "Data", "ProxyTimes", 0, RangeValidator(0, 9999) + ) + + ## Notify ---------------------------------------------------------- + ## 是否启用通知 + 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", "") + ## 是否启用 Server 酱 + self.Notify_IfServerChan = ConfigItem( + "Notify", "IfServerChan", False, BoolValidator() + ) + ## Server 酱密钥 + self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") + ## 自定义 Webhook 列表 + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + super().__init__() + + def getTags(self) -> str: + """生成通用用户标签列表""" + tags = [] + + # 任务代理标签(使用东4区时间) + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"任务:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "任务:未代理", "color": "orange"}) + + # 剩余天数标签 + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": ( + f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限" + ), + "color": tag_color, + } + ) + + # 备注标签 + notes = self.get("Info", "Notes") + tags.append( + { + "text": ( + f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." + ), + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + +class GeneralConfig(ConfigBase): + """通用配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 脚本名称 + self.Info_Name = ConfigItem("Info", "Name", "新通用脚本") + ## 根目录路径 + self.Info_RootPath = ConfigItem( + "Info", "RootPath", str(Path.cwd()), FileValidator() + ) + + ## Script ---------------------------------------------------------- + ## 脚本路径 + self.Script_ScriptPath = ConfigItem( + "Script", "ScriptPath", str(Path.cwd()), FileValidator() + ) + ## 脚本参数 + self.Script_Arguments = ConfigItem( + "Script", "Arguments", "", AdvancedArgumentValidator() + ) + ## 是否追踪进程 + self.Script_IfTrackProcess = ConfigItem( + "Script", "IfTrackProcess", False, BoolValidator() + ) + ## 追踪进程的名称 + self.Script_TrackProcessName = ConfigItem("Script", "TrackProcessName", "") + ## 追踪进程的文件路径 + self.Script_TrackProcessExe = ConfigItem("Script", "TrackProcessExe", "") + ## 追踪进程的启动命令行参数 + self.Script_TrackProcessCmdline = ConfigItem( + "Script", "TrackProcessCmdline", "", ArgumentValidator() + ) + 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", "") + + ## Game ------------------------------------------------------------ + ## 是否启用游戏 + self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator()) + ## 游戏类型 + self.Game_Type = ConfigItem( + "Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client", "URL"]) + ) + ## 游戏路径 + self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator()) + ## 自定义协议URL + self.Game_URL = ConfigItem("Game", "URL", "") + ## 游戏进程名称 + self.Game_ProcessName = ConfigItem("Game", "ProcessName", "") + ## 游戏启动参数 + self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator()) + ## 等待时间(秒) + self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999)) + ## 是否强制关闭 + self.Game_IfForceClose = ConfigItem( + "Game", "IfForceClose", False, BoolValidator() + ) + ## 模拟器 ID + self.Game_EmulatorId = ConfigItem( + "Game", + "EmulatorId", + "-", + MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), + ) + ## 模拟器索引 + self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-") + + ## Run ------------------------------------------------------------- + ## 代理次数限制 + 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]) + + super().__init__() + + +__all__ = ["GeneralUserConfig", "GeneralConfig"] diff --git a/app/models/config/global_config.py b/app/models/config/global_config.py new file mode 100644 index 00000000..e3a60731 --- /dev/null +++ b/app/models/config/global_config.py @@ -0,0 +1,378 @@ +import uuid +import json +import calendar +from datetime import datetime +from typing import Callable + +from app.utils.constants import UTC8, MATERIALS_MAP, RESOURCE_STAGE_INFO +from ..ConfigBase import ( + ConfigBase, + MultipleConfig, + ConfigItem, + BoolValidator, + OptionsValidator, + VirtualConfigValidator, + EncryptValidator, + UUIDValidator, + DateTimeValidator, + JSONValidator, + URLValidator, + KeyValidator, +) +from ..schema import TagItem +from .common import EmulatorConfig, QueueConfig, QueueItem, Webhook +from .general import GeneralConfig +from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from .maaend import MaaEndConfig +from .src import SrcConfig + + +class ToolsConfig(ConfigBase): + """工具配置""" + + def __init__(self) -> None: + self.ArknightsPC_Enabled = ConfigItem( + "ArknightsPC", "Enabled", False, BoolValidator() + ) + self.ArknightsPC_PauseKey = ConfigItem( + "ArknightsPC", "PauseKey", "f10", KeyValidator("f10") + ) + self.ArknightsPC_SelectDeployedKey = ConfigItem( + "ArknightsPC", "SelectDeployedKey", "w", KeyValidator("w") + ) + self.ArknightsPC_UseSkillKey = ConfigItem( + "ArknightsPC", "UseSkillKey", "r", KeyValidator("r") + ) + self.ArknightsPC_RetreatKey = ConfigItem( + "ArknightsPC", "RetreatKey", "t", KeyValidator("t") + ) + self.ArknightsPC_NextFrameKey = ConfigItem( + "ArknightsPC", "NextFrameKey", "f", KeyValidator("f") + ) + self.ArknightsPC_AnotherQuitKey = ConfigItem( + "ArknightsPC", "AnotherQuitKey", "space", KeyValidator("space") + ) + self.ArknightsPC_Status = ConfigItem( + "ArknightsPC", + "Status", + "-", + VirtualConfigValidator(self.arknights_pc_status), + ) + + self.arknights_pc_running = False + self.arknights_pc_get_connected: Callable[[], bool] = lambda: False + + super().__init__() + + @property + def arknights_pc_connected(self) -> bool: + return self.arknights_pc_get_connected() + + def arknights_pc_status(self) -> str: + if not self.get("ArknightsPC", "Enabled"): + return TagItem(text="未启用", color="gray").model_dump_json() + else: + if self.arknights_pc_running: + if self.arknights_pc_connected: + return TagItem(text="运行中", color="green").model_dump_json() + else: + return TagItem(text="未连接", color="red").model_dump_json() + else: + return TagItem(text="已暂停", color="yellow").model_dump_json() + + @property + def arknights_pc_keys(self) -> list[str]: + """获取明日方舟 PC 按键配置""" + + return [ + self.get("ArknightsPC", _) + for _ in ( + "SelectDeployedKey", + "UseSkillKey", + "RetreatKey", + "NextFrameKey", + "AnotherQuitKey", + ) + ] + + +class GlobalConfig(ConfigBase): + """全局配置""" + + def __init__(self): + ## Function --------------------------------------------------------- + ## 历史记录保留时间(天) + self.Function_HistoryRetentionTime = ConfigItem( + "Function", + "HistoryRetentionTime", + 0, + OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]), + ) + ## 是否允许睡眠 + self.Function_IfAllowSleep = ConfigItem( + "Function", "IfAllowSleep", False, BoolValidator() + ) + ## 是否启用静默模式 + self.Function_IfSilence = ConfigItem( + "Function", "IfSilence", False, BoolValidator() + ) + ## 是否同意 Bilibili 协议 + self.Function_IfAgreeBilibili = ConfigItem( + "Function", "IfAgreeBilibili", False, BoolValidator() + ) + ## 是否屏蔽模拟器广告 + self.Function_IfBlockAd = ConfigItem( + "Function", "IfBlockAd", False, BoolValidator() + ) + + ## Voice ------------------------------------------------------------ + ## 是否启用语音 + self.Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator()) + ## 语音类型 + self.Voice_Type = ConfigItem( + "Voice", "Type", "simple", OptionsValidator(["simple", "noisy"]) + ) + + ## Start ------------------------------------------------------------ + ## 是否自动启动 + self.Start_IfSelfStart = ConfigItem( + "Start", "IfSelfStart", False, BoolValidator() + ) + ## 是否启动时直接最小化 + self.Start_IfMinimizeDirectly = ConfigItem( + "Start", "IfMinimizeDirectly", False, BoolValidator() + ) + + ## UI --------------------------------------------------------------- + ## 是否显示托盘图标 + self.UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator()) + ## 是否关闭到托盘 + self.UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator()) + + ## Notify ----------------------------------------------------------- + ## 任务结果推送时间 + self.Notify_SendTaskResultTime = ConfigItem( + "Notify", + "SendTaskResultTime", + "不推送", + OptionsValidator(["不推送", "任何时刻", "仅失败时"]), + ) + ## 是否发送统计信息 + self.Notify_IfSendStatistic = ConfigItem( + "Notify", "IfSendStatistic", False, BoolValidator() + ) + ## 是否发送六星通知 + self.Notify_IfSendSixStar = ConfigItem( + "Notify", "IfSendSixStar", False, BoolValidator() + ) + ## 是否推送系统通知 + self.Notify_IfPushPlyer = ConfigItem( + "Notify", "IfPushPlyer", False, BoolValidator() + ) + ## 是否发送邮件 + self.Notify_IfSendMail = ConfigItem( + "Notify", "IfSendMail", False, BoolValidator() + ) + ## 是否发送Koishi通知 + self.Notify_IfKoishiSupport = ConfigItem( + "Notify", "IfKoishiSupport", False, BoolValidator() + ) + ## Koishi WebSocket 服务器地址 + self.Notify_KoishiServerAddress = ConfigItem( + "Notify", + "KoishiServerAddress", + "ws://localhost:5140/AUTO_MAS", + URLValidator(), + ) + ## Koishi Token + self.Notify_KoishiToken = ConfigItem("Notify", "KoishiToken", "") + ## SMTP 服务器地址 + self.Notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "") + ## 邮箱授权码 + self.Notify_AuthorizationCode = ConfigItem( + "Notify", "AuthorizationCode", "", EncryptValidator() + ) + ## 发件地址 + self.Notify_FromAddress = ConfigItem("Notify", "FromAddress", "") + ## 收件地址 + self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "") + ## 是否启用 Server 酱 + self.Notify_IfServerChan = ConfigItem( + "Notify", "IfServerChan", False, BoolValidator() + ) + ## Server 酱密钥 + self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") + ## 自定义 Webhook 列表 + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + ## Update ----------------------------------------------------------- + ## 是否自动更新 + self.Update_IfAutoUpdate = ConfigItem( + "Update", "IfAutoUpdate", False, BoolValidator() + ) + ## 更新源 + self.Update_Source = ConfigItem( + "Update", + "Source", + "GitHub", + OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]), + ) + ## 更新频道 + self.Update_Channel = ConfigItem( + "Update", "Channel", "stable", OptionsValidator(["stable", "beta"]) + ) + ## 代理地址 + self.Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "") + ## 镜像站 CDK + self.Update_MirrorChyanCDK = ConfigItem( + "Update", "MirrorChyanCDK", "", EncryptValidator() + ) + + ## Data ------------------------------------------------------------- + ## 唯一标识符 + self.Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) + ## 上次统计上传时间 + self.Data_LastStatisticsUpload = ConfigItem( + "Data", + "LastStatisticsUpload", + "2000-01-01 00:00:00", + DateTimeValidator("%Y-%m-%d %H:%M:%S"), + ) + ## 上次关卡更新时间 + self.Data_LastStageUpdated = ConfigItem( + "Data", + "LastStageUpdated", + "2000-01-01 00:00:00", + DateTimeValidator("%Y-%m-%d %H:%M:%S"), + ) + ## 关卡数据的版本标识符 + self.Data_StageETag = ConfigItem("Data", "StageETag", "") + ## 关卡信息数据 + self.Data_StageData = ConfigItem( + "Data", "StageData", "{ }", JSONValidator(), legacy_name="Stage" + ) + ## 关卡信息 + self.Data_Stage = ConfigItem( + "Data", "Stage", "-", VirtualConfigValidator(self.getStage) + ) + ## 上次公告更新时间 + self.Data_LastNoticeUpdated = ConfigItem( + "Data", + "LastNoticeUpdated", + "2000-01-01 00:00:00", + DateTimeValidator("%Y-%m-%d %H:%M:%S"), + ) + ## 公告的版本标识符 + self.Data_NoticeETag = ConfigItem("Data", "NoticeETag", "") + ## 是否显示公告 + self.Data_IfShowNotice = ConfigItem( + "Data", "IfShowNotice", True, BoolValidator() + ) + ## 公告内容 + self.Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator()) + ## 上次 Web 配置更新时间 + self.Data_LastWebConfigUpdated = ConfigItem( + "Data", + "LastWebConfigUpdated", + "2000-01-01 00:00:00", + DateTimeValidator("%Y-%m-%d %H:%M:%S"), + ) + ## Web 配置 + self.Data_WebConfig = ConfigItem( + "Data", "WebConfig", "[ ]", JSONValidator(list) + ) + super().__init__() + + ## 模拟器配置列表 + self.EmulatorConfig = MultipleConfig([EmulatorConfig]) + ## 计划表配置列表 + self.PlanConfig = MultipleConfig([MaaPlanConfig]) + ## 脚本配置列表 + self.ScriptConfig = MultipleConfig( + [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] + ) + ## 队列配置列表 + self.QueueConfig = MultipleConfig([QueueConfig]) + ## 工具箱配置 + self.ToolsConfig = ToolsConfig() + + MaaConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + MaaEndConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + SrcConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + GeneralConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + MaaUserConfig.related_config["PlanConfig"] = self.PlanConfig + QueueItem.related_config["ScriptConfig"] = self.ScriptConfig + + def getStage(self) -> str: + """获取关卡信息""" + + try: + raw_stage_data = json.loads(self.get("Data", "StageData")) + + activity_stage_drop_info = [] + activity_stage_combox = [] + + for side_story in raw_stage_data.values(): + if ( + datetime.strptime( + side_story["Activity"]["UtcStartTime"], "%Y/%m/%d %H:%M:%S" + ).replace(tzinfo=UTC8) + < datetime.now(tz=UTC8) + < datetime.strptime( + side_story["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S" + ).replace(tzinfo=UTC8) + ): + for stage in side_story["Stages"]: + activity_stage_combox.append( + {"label": stage["Display"], "value": stage["Value"]} + ) + + if "SSReopen" not in stage["Display"]: + if stage["Drop"] in MATERIALS_MAP: + drop_id = stage["Drop"] + elif "玉" in stage["Drop"]: + drop_id = "30012" + else: + drop_id = "NotFound" + + activity_stage_drop_info.append( + { + "Display": stage["Display"], + "Value": stage["Value"], + "Drop": drop_id, + "DropName": MATERIALS_MAP.get( + stage["Drop"], stage["Drop"] + ), + "Activity": side_story["Activity"], + } + ) + except Exception: + return "{ }" + + stage_data = {"Info": activity_stage_drop_info} + + for day in range(0, 8): + res_stage = [] + + for stage in RESOURCE_STAGE_INFO: + if day in stage["days"] or day == 0: + res_stage.append({"label": stage["text"], "value": stage["value"]}) + + stage_data[calendar.day_name[day - 1] if day > 0 else "ALL"] = ( + res_stage[0:1] + activity_stage_combox + res_stage[1:] + ) + + return json.dumps(stage_data, ensure_ascii=False) + + +CLASS_BOOK = { + "MAA": MaaConfig, + "MaaPlan": MaaPlanConfig, + "SRC": SrcConfig, + "MaaEnd": MaaEndConfig, + "General": GeneralConfig, +} +"""配置类映射表""" + + +__all__ = ["ToolsConfig", "GlobalConfig", "CLASS_BOOK"] diff --git a/app/models/config/maa.py b/app/models/config/maa.py new file mode 100644 index 00000000..c33649f7 --- /dev/null +++ b/app/models/config/maa.py @@ -0,0 +1,494 @@ +import uuid +import json +import calendar +from pathlib import Path +from datetime import datetime + +from app.utils.constants import UTC4, UTC8, RESOURCE_STAGE_INFO, MAA_STAGE_KEY +from ..ConfigBase import ( + ConfigBase, + MultipleConfig, + ConfigItem, + MultipleUIDValidator, + BoolValidator, + OptionsValidator, + RangeValidator, + VirtualConfigValidator, + FolderValidator, + EncryptValidator, + DateTimeValidator, + JSONValidator, + UserNameValidator, +) +from .common import Webhook + + +class MaaUserConfig(ConfigBase): + """MAA用户配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 用户名称 + self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) + ## 用户 ID + self.Info_Id = ConfigItem("Info", "Id", "") + ## 密码 + self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) + ## 脚本模式 + 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_InfrastMode = ConfigItem( + "Info", + "InfrastMode", + "Normal", + OptionsValidator(["Normal", "Rotation", "Custom"]), + ) + ## 基建配置名称 + self.Info_InfrastName = ConfigItem( + "Info", "InfrastName", "-", VirtualConfigValidator(self.getInfrastName) + ) + ## 基建配置索引 + self.Info_InfrastIndex = ConfigItem( + "Info", "InfrastIndex", "-", VirtualConfigValidator(self.getInfrastIndex) + ) + ## 备注 + 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", "-") + ## 关卡 1 + self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-") + ## 关卡 2 + self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-") + ## 关卡 3 + self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-") + ## 备用关卡 + self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-") + ## 是否启用森空岛签到 + self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) + ## 森空岛 Token + self.Info_SklandToken = ConfigItem( + "Info", "SklandToken", "", EncryptValidator() + ) + ## 用户标签信息(虚拟字段,供前端显示) + self.Info_Tag = ConfigItem( + "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) + ) + + ## Data ------------------------------------------------------------ + ## 上次代理日期 + self.Data_LastProxyDate = ConfigItem( + "Data", "LastProxyDate", "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_CustomInfrast = ConfigItem( + "Data", "CustomInfrast", "{ }", JSONValidator() + ) + ## 基建配置索引数据 + self.Data_InfrastIndex = ConfigItem( + "Data", "InfrastIndex", "0", legacy_group="Info" + ) + + ## Task ------------------------------------------------------------ + ## 是否自动唤醒 + self.Task_IfStartUp = ConfigItem("Task", "IfStartUp", True, BoolValidator()) + ## 是否理智作战 + self.Task_IfFight = ConfigItem("Task", "IfFight", True, BoolValidator()) + ## 是否基建换班 + self.Task_IfInfrast = ConfigItem("Task", "IfInfrast", True, BoolValidator()) + ## 是否公开招募 + self.Task_IfRecruit = ConfigItem("Task", "IfRecruit", True, BoolValidator()) + ## 是否信用收支 + self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator()) + ## 是否领取奖励 + self.Task_IfAward = ConfigItem("Task", "IfAward", True, BoolValidator()) + ## 是否自动肉鸽 + self.Task_IfRoguelike = ConfigItem( + "Task", "IfRoguelike", False, BoolValidator() + ) + ## 是否生息演算 + self.Task_IfReclamation = ConfigItem( + "Task", "IfReclamation", False, BoolValidator() + ) + + ## Notify ---------------------------------------------------------- + ## 是否启用通知 + 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", "") + ## 是否启用 Server 酱 + self.Notify_IfServerChan = ConfigItem( + "Notify", "IfServerChan", False, BoolValidator() + ) + ## Server 酱密钥 + self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") + ## 自定义 Webhook 列表 + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + super().__init__() + + def getInfrastName(self) -> str: + if self.get("Info", "InfrastMode") != "Custom": + return "未使用自定义基建模式" + + infrast_data = json.loads(self.get("Data", "CustomInfrast")) + if ( + infrast_data.get("title", "文件标题") != "文件标题" + and infrast_data.get("description", "文件描述") != "文件描述" + ): + return f"{infrast_data['title']} - {infrast_data['description']}" + elif infrast_data.get("title", "文件标题") != "文件标题": + return str(infrast_data["title"]) + elif infrast_data.get("id", None): + return str(infrast_data["id"]) + else: + return "未命名自定义基建" + + def getInfrastIndex(self) -> str: + if self.get("Info", "InfrastMode") != "Custom": + return "-1" + + infrast_data = json.loads(self.get("Data", "CustomInfrast")) + + if len(infrast_data.get("plans", [])) == 0: + return "-1" + + for i, plan in enumerate(infrast_data.get("plans", [])): + for t in plan.get("period", []): + if ( + datetime.strptime(t[0], "%H:%M").time() + <= datetime.now().time() + <= datetime.strptime(t[1], "%H:%M").time() + ): + return str(i) + + else: + return self.get("Data", "InfrastIndex") or "0" + + def getTags(self) -> str: + """生成用户标签列表,返回JSON字符串格式的TagItem列表""" + tags = [] + + # 人工排查状态标签 + if not self.get("Data", "IfPassCheck"): + tags.append({"text": "人工排查未通过", "color": "red"}) + + # 日常代理标签(使用东4区时间) + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "日常:未代理", "color": "orange"}) + + # 森空岛签到标签(使用东8区时间) + if self.get("Info", "IfSkland"): + if ( + datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC8).date() + ): + tags.append({"text": "森空岛:已签到", "color": "green"}) + else: + tags.append({"text": "森空岛:未签到", "color": "orange"}) + else: + tags.append({"text": "森空岛:禁用", "color": "red"}) + + # 剩余天数标签 + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": ( + f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限" + ), + "color": tag_color, + } + ) + + # 基建模式标签 + infrast_mode = self.get("Info", "InfrastMode") + if self.get("Task", "IfInfrast"): + if infrast_mode == "Normal": + infrast_text = "基建:常规" + elif infrast_mode == "Rotation": + infrast_text = "基建:轮换" + elif infrast_mode == "Custom": + infrast_text = f"基建:{self.getInfrastName() if len(self.getInfrastName()) < 10 else self.getInfrastName()[:10] + '...'}" + else: + infrast_text = "基建:开启" + tags.append({"text": infrast_text, "color": "purple"}) + else: + tags.append({"text": "基建:关闭", "color": "red"}) + + # 关卡信息标签 + plan_data = { + stage_key: self.get_stage_zh(self.get("Info", stage_key)) + for stage_key in MAA_STAGE_KEY[2:] + } + tag_color = "blue" + if self.get("Info", "StageMode") != "Fixed": + plan = self.related_config["PlanConfig"][ + uuid.UUID(self.get("Info", "StageMode")) + ] + if isinstance(plan, MaaPlanConfig): + plan_data = { + stage_key: self.get_stage_zh( + plan.get_current_info(stage_key).getValue() + ) + for stage_key in MAA_STAGE_KEY[2:] + } + tag_color = "green" + # 主关卡 + tags.append({"text": f"主关卡:{plan_data['Stage']}", "color": tag_color}) + # 备选关卡(合并显示) + backup_stages = [ + plan_data[f"Stage_{i}"] + for i in range(1, 4) + if plan_data[f"Stage_{i}"] != "禁用" + ] + if backup_stages: + tags.append( + {"text": f"备选:{', '.join(backup_stages)}", "color": tag_color} + ) + # 剩余关卡 + if plan_data["Stage_Remain"] != "禁用": + tags.append( + {"text": f"剩余:{plan_data['Stage_Remain']}", "color": tag_color} + ) + + # 备注标签 + notes = self.get("Info", "Notes") + tags.append( + { + "text": ( + f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." + ), + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + @staticmethod + def get_stage_zh(stage: str) -> str: + for stage_info in RESOURCE_STAGE_INFO: + if stage_info.get("value") == stage: + return ( + stage_info.get("text", stage) + .replace("经验-6/5", "经验") + .replace("龙门币-6/5", "龙门币") + .replace("红票-5", "红票") + .replace("技能-5", "技能") + .replace("碳-5", "碳") + ) + else: + return stage + + +class MaaConfig(ConfigBase): + """MAA配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## MAA 脚本名称 + self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本") + ## MAA 路径 + self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) + + ## Emulator -------------------------------------------------------- + ## 模拟器 ID + self.Emulator_Id = ConfigItem( + "Emulator", + "Id", + "-", + MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), + ) + ## 模拟器索引 + self.Emulator_Index = ConfigItem("Emulator", "Index", "-") + + ## Run ------------------------------------------------------------- + ## 任务切换方式 + self.Run_TaskTransitionMethod = ConfigItem( + "Run", + "TaskTransitionMethod", + "ExitEmulator", + OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]), + ) + ## 代理次数限制 + self.Run_ProxyTimesLimit = ConfigItem( + "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) + ) + ## 运行次数限制 + 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_AnnihilationAvoidWaste = ConfigItem( + "Run", "AnnihilationAvoidWaste", False, BoolValidator() + ) + + self.UserData = MultipleConfig([MaaUserConfig]) + + super().__init__() + + +class MaaPlanConfig(ConfigBase): + """MAA计划表配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 计划表名称 + 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", *calendar.day_name]: + 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"]), + ) + + ## 理智关卡 + for name in MAA_STAGE_KEY[2:]: + # Stage、Stage_1、Stage_2、Stage_3、Stage_Remain + self.config_item_dict[group][name] = ConfigItem(group, name, "-") + + for name in MAA_STAGE_KEY: + setattr(self, f"{group}_{name}", self.config_item_dict[group][name]) + + super().__init__() + + 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": + today = datetime.now(tz=UTC4).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("非法的计划表模式") + + +__all__ = ["MaaPlanConfig", "MaaUserConfig", "MaaConfig"] diff --git a/app/models/config/maaend.py b/app/models/config/maaend.py new file mode 100644 index 00000000..344c62b3 --- /dev/null +++ b/app/models/config/maaend.py @@ -0,0 +1,310 @@ +import json +from pathlib import Path +from datetime import datetime + +from app.utils.constants import UTC4, UTC8, MAAEND_STAGE_BOOK, MAAEND_STAGE_WITH_AB +from ..ConfigBase import ( + ConfigBase, + MultipleConfig, + ConfigItem, + MultipleUIDValidator, + BoolValidator, + OptionsValidator, + RangeValidator, + VirtualConfigValidator, + FileValidator, + FolderValidator, + EncryptValidator, + DateTimeValidator, + UserNameValidator, + ArgumentValidator, +) +from .common import Webhook + + +class MaaEndUserConfig(ConfigBase): + """MaaEnd用户配置""" + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 用户名称 + self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) + ## 是否启用 + self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) + ## 用户ID + self.Info_Id = ConfigItem("Info", "Id", "") + ## 密码 + self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) + ## 配置模式 + self.Info_Mode = ConfigItem( + "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) + ) + ## 资源名称 + self.Info_Resource = ConfigItem( + "Info", "Resource", "官服", OptionsValidator(["官服"]) + ) + ## 剩余天数 + self.Info_RemainedDay = ConfigItem( + "Info", "RemainedDay", -1, RangeValidator(-1, 9999) + ) + ## 备注 + self.Info_Notes = ConfigItem("Info", "Notes", "无") + ## 是否启用森空岛签到 + self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) + ## 森空岛 Token + self.Info_SklandToken = ConfigItem( + "Info", "SklandToken", "", EncryptValidator() + ) + ## 用户标签信息 + self.Info_Tag = ConfigItem( + "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) + ) + + ## Task ------------------------------------------------------------ + ## 协议空间选项 + self.Task_ProtocolSpaceTab = ConfigItem( + "Task", + "ProtocolSpaceTab", + "OperatorProgression", + OptionsValidator( + ["OperatorProgression", "WeaponProgression", "CrisisDrills"] + ), + ) + self.Task_OperatorProgression = ConfigItem( + "Task", + "OperatorProgression", + "OperatorEXP", + OptionsValidator(["OperatorEXP", "Promotions", "T-Creds", "SkillUp"]), + ) + self.Task_WeaponProgression = ConfigItem( + "Task", + "WeaponProgression", + "WeaponEXP", + OptionsValidator(["WeaponEXP", "WeaponTune"]), + ) + self.Task_CrisisDrills = ConfigItem( + "Task", + "CrisisDrills", + "AdvancedProgression1", + OptionsValidator( + [ + "AdvancedProgression1", + "AdvancedProgression2", + "AdvancedProgression3", + "AdvancedProgression4", + "AdvancedProgression5", + ] + ), + ) + self.Task_RewardsSetOption = ConfigItem( + "Task", + "RewardsSetOption", + "RewardsSetA", + OptionsValidator(["RewardsSetA", "RewardsSetB"]), + ) + + ## Data ------------------------------------------------------------ + ## 上次代理日期 + self.Data_LastProxyDate = ConfigItem( + "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") + ) + ## 代理次数 + self.Data_ProxyTimes = ConfigItem( + "Data", "ProxyTimes", 0, RangeValidator(0, 9999) + ) + ## 上次代理状态 + self.Data_LastProxyStatus = ConfigItem( + "Data", + "LastProxyStatus", + "未知", + OptionsValidator(["未知", "成功", "失败"]), + ) + ## 上次森空岛签到日期 + self.Data_LastSklandDate = ConfigItem( + "Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") + ) + ## 是否通过检查 + self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator()) + + ## Notify ---------------------------------------------------------- + ## 是否启用通知 + 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", "") + ## 是否启用 Server 酱 + self.Notify_IfServerChan = ConfigItem( + "Notify", "IfServerChan", False, BoolValidator() + ) + ## Server 酱密钥 + self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") + ## 自定义 Webhook 列表 + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + super().__init__() + + def getTags(self) -> str: + """生成用户标签列表,返回JSON字符串格式的TagItem列表""" + tags = [] + # 人工排查状态标签 + if not self.get("Data", "IfPassCheck"): + tags.append({"text": "人工排查未通过", "color": "red"}) + + # 上次代理标签 + tags.append( + { + "text": f"上次:{self.get('Data', 'LastProxyStatus')}", + "color": ( + "red" if self.get("Data", "LastProxyStatus") == "失败" else "green" + ), + } + ) + + # 日常代理标签(使用东4区时间) + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "日常:未代理", "color": "orange"}) + + # 森空岛签到标签(使用东8区时间) + if self.get("Info", "IfSkland"): + if ( + datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC8).date() + ): + tags.append({"text": "森空岛:已签到", "color": "green"}) + else: + tags.append({"text": "森空岛:未签到", "color": "orange"}) + else: + tags.append({"text": "森空岛:禁用", "color": "red"}) + + # 剩余天数标签 + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": ( + f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限" + ), + "color": tag_color, + } + ) + + # 关卡信息标签 + stage = self.get("Task", self.get("Task", "ProtocolSpaceTab")) + stage_ab = ( + f" - {self.get('Task', 'RewardsSetOption')[-1]}" + if stage in MAAEND_STAGE_WITH_AB + else "" + ) + tags.append({"text": MAAEND_STAGE_BOOK[stage] + stage_ab, "color": "blue"}) + + # 备注标签 + notes = self.get("Info", "Notes") + tags.append( + { + "text": ( + f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." + ), + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + +class MaaEndConfig(ConfigBase): + """MaaEnd配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## MaaEnd 脚本名称 + self.Info_Name = ConfigItem("Info", "Name", "新 MaaEnd 脚本") + ## MaaEnd 路径 + self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) + + ## Run ------------------------------------------------------------- + ## 运行超时阈值 + self.Run_RunTimeLimit = ConfigItem( + "Run", "RunTimeLimit", 10, RangeValidator(1, 9999) + ) + ## 每日代理次数限制 + self.Run_ProxyTimesLimit = ConfigItem( + "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) + ) + ## 运行次数限制 + self.Run_RunTimesLimit = ConfigItem( + "Run", "RunTimesLimit", 3, RangeValidator(1, 9999) + ) + + ## Game ------------------------------------------------------------ + ## 控制器类型 + self.Game_ControllerType = ConfigItem( + "Game", + "ControllerType", + "Win32-Window", + OptionsValidator( + [ + "Win32-Window", + "Win32-Front", + "Win32-Window-Background", + "ADB", + ] + ), + ) + ## 终末地游戏路径 + self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator()) + ## 终末地游戏启动参数 + self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator()) + ## 等待时间(秒) + self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999)) + ## 模拟器 ID + self.Game_EmulatorId = ConfigItem( + "Game", + "EmulatorId", + "-", + MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), + ) + ## 模拟器索引 + self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-") + ## 结束后是否关闭游戏 + self.Game_CloseOnFinish = ConfigItem( + "Game", "CloseOnFinish", True, BoolValidator() + ) + + self.UserData = MultipleConfig([MaaEndUserConfig]) + + super().__init__() + + +__all__ = ["MaaEndUserConfig", "MaaEndConfig"] diff --git a/app/models/config/src.py b/app/models/config/src.py new file mode 100644 index 00000000..0dd728ed --- /dev/null +++ b/app/models/config/src.py @@ -0,0 +1,410 @@ +import json +from pathlib import Path +from datetime import datetime + +from app.utils.constants import UTC4, STARRAIL_STAGE_BOOK +from ..ConfigBase import ( + ConfigBase, + MultipleConfig, + ConfigItem, + MultipleUIDValidator, + BoolValidator, + OptionsValidator, + RangeValidator, + VirtualConfigValidator, + FolderValidator, + EncryptValidator, + DateTimeValidator, + UserNameValidator, +) +from .common import Webhook + + +class SrcUserConfig(ConfigBase): + """SRC用户配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## 用户名称 + self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) + ## 是否启用 + self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) + ## 用户 ID + self.Info_Id = ConfigItem("Info", "Id", "") + ## 密码 + self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) + ## 脚本模式 + self.Info_Mode = ConfigItem( + "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) + ) + ## 游戏服务器 + self.Info_Server = ConfigItem( + "Info", + "Server", + "CN-Official", + OptionsValidator( + [ + "CN-Official", + "CN-Bilibili", + "VN-Official", + "OVERSEA-America", + "OVERSEA-Asia", + "OVERSEA-Europe", + "OVERSEA-TWHKMO", + ] + ), + ) + ## 剩余天数 + self.Info_RemainedDay = ConfigItem( + "Info", "RemainedDay", -1, RangeValidator(-1, 9999) + ) + ## 备注 + self.Info_Notes = ConfigItem("Info", "Notes", "无") + ## 用户标签信息 + self.Info_Tag = ConfigItem( + "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) + ) + + ## 关卡配置---------------------------------------------------------- + ## 关卡通道 + self.Stage_Channel = ConfigItem( + "Stage", + "Channel", + "Relic", + OptionsValidator(["Relic", "Materials", "Ornament"]), + ) + ## 遗器关卡 + self.Stage_Relic = ConfigItem( + "Stage", + "Relic", + "-", + OptionsValidator( + [ + "-", + "Cavern_of_Corrosion_Path_of_Possession", + "Cavern_of_Corrosion_Path_of_Hidden_Salvation", + "Cavern_of_Corrosion_Path_of_Thundersurge", + "Cavern_of_Corrosion_Path_of_Aria", + "Cavern_of_Corrosion_Path_of_Uncertainty", + "Cavern_of_Corrosion_Path_of_Cavalier", + "Cavern_of_Corrosion_Path_of_Dreamdive" + "Cavern_of_Corrosion_Path_of_Darkness", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers", + "Cavern_of_Corrosion_Path_of_Conflagration", + "Cavern_of_Corrosion_Path_of_Holy_Hymn", + "Cavern_of_Corrosion_Path_of_Providence", + "Cavern_of_Corrosion_Path_of_Drifting", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch", + "Cavern_of_Corrosion_Path_of_Gelid_Wind", + ] + ), + ) + ## 材料关卡 + self.Stage_Materials = ConfigItem( + "Stage", + "Materials", + "-", + OptionsValidator( + [ + "-", + "Calyx_Golden_Memories_Planarcadia", + "Calyx_Golden_Aether_Planarcadia", + "Calyx_Golden_Treasures_Planarcadia", + "Calyx_Golden_Memories_Amphoreus", + "Calyx_Golden_Aether_Amphoreus", + "Calyx_Golden_Treasures_Amphoreus", + "Calyx_Golden_Memories_Penacony", + "Calyx_Golden_Aether_Penacony", + "Calyx_Golden_Treasures_Penacony", + "Calyx_Golden_Memories_The_Xianzhou_Luofu", + "Calyx_Golden_Aether_The_Xianzhou_Luofu", + "Calyx_Golden_Treasures_The_Xianzhou_Luofu", + "Calyx_Golden_Memories_Jarilo_VI", + "Calyx_Golden_Aether_Jarilo_VI", + "Calyx_Golden_Treasures_Jarilo_VI", + "Calyx_Crimson_Destruction_Herta_StorageZone", + "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", + "Calyx_Crimson_Preservation_Herta_SupplyZone", + "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", + "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", + "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", + "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", + "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", + "Calyx_Crimson_Erudition_Jarilo_RivetTown", + "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", + "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", + "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", + "Calyx_Crimson_Nihility_Jarilo_GreatMine", + "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", + "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", + "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", + "Stagnant_Shadow_Quanta", + "Stagnant_Shadow_Gust", + "Stagnant_Shadow_Fulmination", + "Stagnant_Shadow_Blaze", + "Stagnant_Shadow_Spike", + "Stagnant_Shadow_Rime", + "Stagnant_Shadow_Mirage", + "Stagnant_Shadow_Icicle", + "Stagnant_Shadow_Doom", + "Stagnant_Shadow_Puppetry", + "Stagnant_Shadow_Abomination", + "Stagnant_Shadow_Scorch", + "Stagnant_Shadow_Celestial", + "Stagnant_Shadow_Perdition", + "Stagnant_Shadow_Nectar", + "Stagnant_Shadow_Roast", + "Stagnant_Shadow_Ire", + "Stagnant_Shadow_Duty", + "Stagnant_Shadow_Timbre", + "Stagnant_Shadow_Mechwolf", + "Stagnant_Shadow_Gloam", + "Stagnant_Shadow_Sloggyre", + "Stagnant_Shadow_Gelidmoon", + "Stagnant_Shadow_Deepsheaf", + "Stagnant_Shadow_Cinders", + "Stagnant_Shadow_Sirens", + "Stagnant_Shadow_Ashes", + "Stagnant_Shadow_Soundburst", + ] + ), + ) + ## 饰品关卡 + self.Stage_Ornament = ConfigItem( + "Stage", + "Ornament", + "-", + OptionsValidator( + [ + "-", + "Divergent_Universe_Within_the_West_Wind", + "Divergent_Universe_Moonlit_Blood", + "Divergent_Universe_Unceasing_Strife", + "Divergent_Universe_Famished_Worker", + "Divergent_Universe_Eternal_Comedy", + "Divergent_Universe_To_Sweet_Dreams", + "Divergent_Universe_Pouring_Blades", + "Divergent_Universe_Fruit_of_Evil", + "Divergent_Universe_Permafrost", + "Divergent_Universe_Gentle_Words", + "Divergent_Universe_Smelted_Heart", + "Divergent_Universe_Untoppled_Walls", + ] + ), + ) + ## 使用储备开拓力 + self.Stage_ExtractReservedTrailblazePower = ConfigItem( + "Stage", "ExtractReservedTrailblazePower", False, BoolValidator() + ) + ## 使用燃料 + self.Stage_UseFuel = ConfigItem("Stage", "UseFuel", False, BoolValidator()) + ## 保留的燃料数量 + self.Stage_FuelReserve = ConfigItem( + "Stage", "FuelReserve", 5, RangeValidator(0, 9999) + ) + ## 历战余响关卡 + self.Stage_EchoOfWar = ConfigItem( + "Stage", + "EchoOfWar", + "-", + OptionsValidator( + [ + "-", + "Echo_of_War_Rusted_Crypt_of_the_Iron_Carcass", + "Echo_of_War_Glance_of_Twilight", + "Echo_of_War_Inner_Beast_Battlefield", + "Echo_of_War_Salutations_of_Ashen_Dreams", + "Echo_of_War_Borehole_Planet_Past_Nightmares", + "Echo_of_War_Divine_Seed", + "Echo_of_War_End_of_the_Eternal_Freeze", + "Echo_of_War_Destruction_Beginning", + ] + ), + ) + ## 模拟宇宙关卡 + self.Stage_SimulatedUniverseWorld = ConfigItem( + "Stage", + "SimulatedUniverseWorld", + "-", + OptionsValidator( + [ + "-", + "Simulated_Universe_World_3", + "Simulated_Universe_World_4", + "Simulated_Universe_World_5", + "Simulated_Universe_World_6", + "Simulated_Universe_World_8", + ] + ), + ) + + ## Data ------------------------------------------------------------ + ## 上次代理日期 + self.Data_LastProxyDate = ConfigItem( + "Data", "LastProxyDate", "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()) + + ## Notify ---------------------------------------------------------- + ## 是否启用通知 + 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", "") + ## 是否启用 Server 酱 + self.Notify_IfServerChan = ConfigItem( + "Notify", "IfServerChan", False, BoolValidator() + ) + ## Server 酱密钥 + self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") + ## 自定义 Webhook 列表 + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + super().__init__() + + def getTags(self) -> str: + """生成用户标签列表,返回JSON字符串格式的TagItem列表""" + tags = [] + + # 人工排查状态标签 + if not self.get("Data", "IfPassCheck"): + tags.append({"text": "人工排查未通过", "color": "red"}) + + # 日常代理标签(使用东4区时间) + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "日常:未代理", "color": "orange"}) + + # 剩余天数标签 + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": ( + f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限" + ), + "color": tag_color, + } + ) + + # 关卡信息标签 + tags.append( + { + "text": f"关卡:{STARRAIL_STAGE_BOOK.get(self.get('Stage', self.get('Stage', 'Channel')), '未知关卡')}", + "color": "blue", + } + ) + tags.append( + { + "text": f"周本:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'EchoOfWar'), '未知关卡')}", + "color": "blue", + } + ) + tags.append( + { + "text": f"模拟宇宙:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'SimulatedUniverseWorld'), '未知关卡')}", + "color": "blue", + } + ) + + # 备注标签 + notes = self.get("Info", "Notes") + tags.append( + { + "text": ( + f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." + ), + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + +class SrcConfig(ConfigBase): + """SRC配置""" + + related_config: dict[str, MultipleConfig] = {} + + def __init__(self) -> None: + ## Info ------------------------------------------------------------ + ## SRC 脚本名称 + self.Info_Name = ConfigItem("Info", "Name", "新 SRC 脚本") + ## SRC 路径 + self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) + + ## Emulator -------------------------------------------------------- + ## 模拟器 ID + self.Emulator_Id = ConfigItem( + "Emulator", + "Id", + "-", + MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), + ) + ## 模拟器索引 + self.Emulator_Index = ConfigItem("Emulator", "Index", "-") + + ## Run ------------------------------------------------------------- + ## 任务切换方式 + self.Run_TaskTransitionMethod = ConfigItem( + "Run", + "TaskTransitionMethod", + "ExitGame", + OptionsValidator(["ExitGame", "ExitEmulator"]), + ) + ## 代理次数限制 + 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([SrcUserConfig]) + + super().__init__() + + +__all__ = ["SrcUserConfig", "SrcConfig"] From 33733597e0f579137805dc18cc1d8e1655947bff Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 29 Mar 2026 23:56:20 +0800 Subject: [PATCH 03/29] =?UTF-8?q?feat(config):=20=E4=BD=BF=E7=94=A8pydanti?= =?UTF-8?q?c=E9=AA=8C=E8=AF=81=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/core.py | 14 +- app/api/dispatch.py | 14 +- app/api/emulator.py | 2 +- app/api/history.py | 11 +- app/api/info.py | 73 +- app/api/ocr.py | 7 +- app/api/plan.py | 18 +- app/api/queue.py | 44 +- app/api/scripts.py | 65 +- app/api/setting.py | 4 +- app/api/tools.py | 2 +- app/api/update.py | 5 +- app/api/ws_debug.py | 2 +- app/core/broadcast.py | 11 +- app/core/config.py | 200 ++-- app/core/emulator_manager.py | 9 +- app/models/ConfigBase.py | 255 +++-- app/models/__init__.py | 64 +- app/models/common.py | 143 +++ app/models/config/__init__.py | 36 +- app/models/config/common.py | 169 +--- app/models/config/general.py | 272 +---- app/models/config/global_config.py | 377 +------ app/models/config/maa.py | 493 +-------- app/models/config/maaend.py | 309 +----- app/models/config/src.py | 409 +------- app/models/config_base.py | 1510 ++++++++++++++++++++++++++++ app/models/dto.py | 1462 +++++++++++++++++++++++++++ app/models/general.py | 209 ++++ app/models/global_config.py | 298 ++++++ app/models/maa.py | 420 ++++++++ app/models/maaend.py | 243 +++++ app/models/pydantic_base.py | 247 +++++ app/models/schema.py | 3 +- app/models/src.py | 379 +++++++ app/models/type.py | 133 +++ app/services/notification.py | 4 +- app/task/MAA/AutoProxy.py | 14 +- app/task/MAA/ManualReview.py | 10 +- app/task/MAA/ScriptConfig.py | 6 +- app/task/MAA/manager.py | 6 +- app/task/MAA/tools/notify.py | 2 +- app/task/MaaEnd/AutoProxy.py | 14 +- app/task/MaaEnd/ManualReview.py | 9 +- app/task/MaaEnd/ScriptConfig.py | 6 +- app/task/MaaEnd/manager.py | 7 +- app/task/MaaEnd/tools/notify.py | 6 +- app/task/SRC/AutoProxy.py | 13 +- app/task/SRC/ManualReview.py | 9 +- app/task/SRC/ScriptConfig.py | 6 +- app/task/SRC/manager.py | 6 +- app/task/SRC/tools/notify.py | 2 +- app/task/general/AutoProxy.py | 15 +- app/task/general/ScriptConfig.py | 6 +- app/task/general/manager.py | 6 +- app/task/general/tools/notify.py | 2 +- app/utils/emulator/general.py | 10 +- app/utils/emulator/ldplayer.py | 2 +- app/utils/emulator/mumu.py | 2 +- pyproject.toml | 12 + pyrightconfig.json | 24 + requirements.txt | 4 +- 62 files changed, 5704 insertions(+), 2411 deletions(-) create mode 100644 app/models/common.py create mode 100644 app/models/config_base.py create mode 100644 app/models/dto.py create mode 100644 app/models/general.py create mode 100644 app/models/global_config.py create mode 100644 app/models/maa.py create mode 100644 app/models/maaend.py create mode 100644 app/models/pydantic_base.py create mode 100644 app/models/src.py create mode 100644 app/models/type.py create mode 100644 pyproject.toml create mode 100644 pyrightconfig.json diff --git a/app/api/core.py b/app/api/core.py index f6ffb574..54803320 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -24,11 +24,12 @@ import os import time import asyncio +from typing import Any, cast from fastapi import APIRouter, WebSocket, WebSocketDisconnect from app.core import Config, Broadcast, TaskManager from app.services import System -from app.models.schema import * +from app.models.dto import OutBase, WebSocketMessage from app.api.ws_command import ws_command from app.utils import get_logger @@ -45,7 +46,6 @@ def is_backend_dev_mode() -> bool: @router.websocket("/ws") async def connect_websocket(websocket: WebSocket): - if Config.websocket is not None: await websocket.close(code=1000, reason="已有连接") return @@ -54,15 +54,16 @@ async def connect_websocket(websocket: WebSocket): Config.websocket = websocket last_pong = time.monotonic() last_ping = time.monotonic() - data = {} + data: dict[str, Any] = {} asyncio.create_task(TaskManager.start_startup_queue()) while True: - try: - - data = await asyncio.wait_for(websocket.receive_json(), timeout=15.0) + payload = await asyncio.wait_for(websocket.receive_json(), timeout=15.0) + if not isinstance(payload, dict): + continue + data = cast(dict[str, Any], payload) if data.get("type") == "Signal" and "Pong" in data.get("data", {}): last_pong = time.monotonic() elif data.get("type") == "Signal" and "Ping" in data.get("data", {}): @@ -75,7 +76,6 @@ async def connect_websocket(websocket: WebSocket): await Broadcast.put(data) except asyncio.TimeoutError: - if last_pong < last_ping: await websocket.close(code=1000, reason="Ping超时") break diff --git a/app/api/dispatch.py b/app/api/dispatch.py index e8c220ab..87e48349 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -25,7 +25,14 @@ from app.core import Config, TaskManager from app.services import System -from app.models.schema import * +from app.models.dto import ( + DispatchIn, + OutBase, + PowerIn, + PowerOut, + TaskCreateIn, + TaskCreateOut, +) router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) @@ -38,7 +45,6 @@ status_code=200, ) async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: - try: task_id = await TaskManager.add_task(task.mode, task.taskId) except Exception as e: @@ -56,7 +62,6 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: status_code=200, ) async def stop_task(task: DispatchIn = Body(...)) -> OutBase: - try: await TaskManager.stop_task(task.taskId) except Exception as e: @@ -74,7 +79,6 @@ async def stop_task(task: DispatchIn = Body(...)) -> OutBase: status_code=200, ) async def get_power() -> PowerOut: - try: signal = Config.power_sign except Exception as e: @@ -95,7 +99,6 @@ async def get_power() -> PowerOut: status_code=200, ) async def set_power(task: PowerIn = Body(...)) -> OutBase: - try: Config.power_sign = task.signal except Exception as e: @@ -113,7 +116,6 @@ async def set_power(task: PowerIn = Body(...)) -> OutBase: status_code=200, ) async def cancel_power_task() -> OutBase: - try: await System.cancel_power_task() except Exception as e: diff --git a/app/api/emulator.py b/app/api/emulator.py index b70ed51e..821903b2 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -23,7 +23,7 @@ from fastapi import APIRouter, Body from app.core import Config, EmulatorManager -from app.models.schema import ( +from app.models.dto import ( OutBase, EmulatorConfig, EmulatorGetIn, diff --git a/app/api/history.py b/app/api/history.py index ea70a8db..7eafc953 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -26,7 +26,14 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.schema import * +from app.models.dto import ( + HistoryData, + HistoryDataGetIn, + HistoryDataGetOut, + HistoryIndexItem, + HistorySearchIn, + HistorySearchOut, +) router = APIRouter(prefix="/api/history", tags=["历史记录"]) @@ -39,7 +46,6 @@ status_code=200, ) async def search_history(history: HistorySearchIn) -> HistorySearchOut: - try: data = await Config.search_history( history.mode, @@ -73,7 +79,6 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: status_code=200, ) async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut: - try: path = Path(history.jsonPath) data = await Config.merge_statistic_info([path]) diff --git a/app/api/info.py b/app/api/info.py index 60684002..9e5db38f 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -21,14 +21,48 @@ # Contact: DLmaster_361@163.com +from typing import Any, cast + from fastapi import APIRouter, Body from app.core import Config -from app.models.schema import * +from app.models.dto import ( + ComboBoxItem, + ComboBoxOut, + EmulatorDeleteIn, + GetStageIn, + InfoOut, + NoticeOut, + OutBase, + VersionOut, +) router = APIRouter(prefix="/api/info", tags=["信息获取"]) +def _to_combobox_items(raw_data: object) -> list[ComboBoxItem]: + if not isinstance(raw_data, list): + return [] + + items: list[ComboBoxItem] = [] + for item_any in cast(list[object], raw_data): + if not isinstance(item_any, dict): + continue + item = cast(dict[str, Any], item_any) + label = item.get("label") + if not isinstance(label, str): + continue + value_raw = item.get("value") + value: str | None + if isinstance(value_raw, str) or value_raw is None: + value = value_raw + else: + value = str(value_raw) + items.append(ComboBoxItem(label=label, value=value)) + + return items + + @router.post( "/version", tags=["Get"], @@ -37,7 +71,6 @@ status_code=200, ) async def get_git_version() -> VersionOut: - try: is_latest, commit_hash, commit_time = await Config.get_git_version() except Exception as e: @@ -64,16 +97,11 @@ async def get_git_version() -> VersionOut: status_code=200, ) async def get_stage_combox( - stage: GetStageIn = Body(..., description="关卡号类型") + stage: GetStageIn = Body(..., description="关卡号类型"), ) -> ComboBoxOut: - try: - raw_data = await Config.get_stage_info(stage.type) - data = ( - [ComboBoxItem(**item) for item in raw_data if isinstance(item, dict)] - if raw_data - else [] - ) + raw_data = cast(object, await Config.get_stage_info(stage.type)) + data = _to_combobox_items(raw_data) except Exception as e: return ComboBoxOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] @@ -89,10 +117,9 @@ async def get_stage_combox( status_code=200, ) async def get_script_combox() -> ComboBoxOut: - try: raw_data = await Config.get_script_combox() - data = [ComboBoxItem(**item) for item in raw_data] if raw_data else [] + data = _to_combobox_items(raw_data) except Exception as e: return ComboBoxOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] @@ -108,10 +135,9 @@ async def get_script_combox() -> ComboBoxOut: status_code=200, ) async def get_task_combox() -> ComboBoxOut: - try: raw_data = await Config.get_task_combox() - data = [ComboBoxItem(**item) for item in raw_data] if raw_data else [] + data = _to_combobox_items(raw_data) except Exception as e: return ComboBoxOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] @@ -127,10 +153,9 @@ async def get_task_combox() -> ComboBoxOut: status_code=200, ) async def get_plan_combox() -> ComboBoxOut: - try: raw_data = await Config.get_plan_combox() - data = [ComboBoxItem(**item) for item in raw_data] if raw_data else [] + data = _to_combobox_items(raw_data) except Exception as e: return ComboBoxOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] @@ -146,10 +171,9 @@ async def get_plan_combox() -> ComboBoxOut: status_code=200, ) async def get_emulator_combox() -> ComboBoxOut: - try: raw_data = await Config.get_emulator_combox() - data = [ComboBoxItem(**item) for item in raw_data] if raw_data else [] + data = _to_combobox_items(raw_data) except Exception as e: return ComboBoxOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] @@ -168,8 +192,10 @@ async def get_emulator_devices_combox( emulator: EmulatorDeleteIn = Body(...), ) -> ComboBoxOut: try: - raw_data = await Config.get_emulator_devices_combox(emulator.emulatorId) - data = [ComboBoxItem(**item) for item in raw_data] if raw_data else [] + raw_data = cast( + object, await Config.get_emulator_devices_combox(emulator.emulatorId) + ) + data = _to_combobox_items(raw_data) except Exception as e: return ComboBoxOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] @@ -185,7 +211,6 @@ async def get_emulator_devices_combox( status_code=200, ) async def get_notice_info() -> NoticeOut: - try: if_need_show, data = await Config.get_notice() except Exception as e: @@ -207,7 +232,6 @@ async def get_notice_info() -> NoticeOut: status_code=200, ) async def confirm_notice() -> OutBase: - try: await Config.set("Data", "IfShowNotice", False) except Exception as e: @@ -239,7 +263,6 @@ async def confirm_notice() -> OutBase: status_code=200, ) async def get_web_config() -> InfoOut: - try: data = await Config.get_web_config() except Exception as e: @@ -258,7 +281,9 @@ async def get_web_config() -> InfoOut: ) async def get_overview() -> InfoOut: try: - stage = await Config.get_stage_info("Info") + raw_stage = cast(object, await Config.get_stage_info("Info")) + stage = cast(dict[str, Any], raw_stage if isinstance(raw_stage, dict) else {}) + proxy = await Config.get_proxy_overview() except Exception as e: return InfoOut( diff --git a/app/api/ocr.py b/app/api/ocr.py index 3a6066d9..0b59fea7 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -29,7 +29,7 @@ from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger -from app.models.schema import OutBase +from app.models.dto import OutBase logger = get_logger("OCR API") @@ -159,11 +159,6 @@ async def get_screenshot(params: OCRScreenshotIn = Body(...)) -> OCRScreenshotOu OCRScreenshotOut: 包含Base64编码的截图和区域信息 """ try: - # 初始化OCRTool - ocr_tool = OCRTool( - width=params.aspect_ratio_width, height=params.aspect_ratio_height - ) - # 获取截图区域(如果没有提供自定义区域) if params.region is None: region = OCRTool.get_screenshot_region( diff --git a/app/api/plan.py b/app/api/plan.py index dd2571ec..b6c2ae64 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -24,7 +24,18 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.schema import * +from app.models.dto import ( + MaaPlanConfig, + OutBase, + PlanCreateIn, + PlanCreateOut, + PlanDeleteIn, + PlanGetIn, + PlanGetOut, + PlanIndexItem, + PlanReorderIn, + PlanUpdateIn, +) router = APIRouter(prefix="/api/plan", tags=["计划管理"]) @@ -37,7 +48,6 @@ status_code=200, ) async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: - try: uid, config = await Config.add_plan(plan.type) data = MaaPlanConfig(**(await config.toDict())) @@ -60,7 +70,6 @@ async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: status_code=200, ) async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut: - try: index, data = await Config.get_plan(plan.planId) index = [PlanIndexItem(**_) for _ in index] @@ -84,7 +93,6 @@ async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut: status_code=200, ) async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase: - try: await Config.update_plan(plan.planId, plan.data.model_dump(exclude_unset=True)) except Exception as e: @@ -102,7 +110,6 @@ async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase: - try: await Config.del_plan(plan.planId) except Exception as e: @@ -120,7 +127,6 @@ async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_plan(plan.indexList) except Exception as e: diff --git a/app/api/queue.py b/app/api/queue.py index 51848ff0..7d57cf54 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -24,7 +24,34 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.schema import * +from app.models.dto import ( + OutBase, + QueueConfig, + QueueCreateOut, + QueueDeleteIn, + QueueGetIn, + QueueGetOut, + QueueIndexItem, + QueueItem, + QueueItemCreateOut, + QueueItemDeleteIn, + QueueItemGetIn, + QueueItemGetOut, + QueueItemIndexItem, + QueueItemReorderIn, + QueueItemUpdateIn, + QueueReorderIn, + QueueSetInBase, + QueueUpdateIn, + TimeSet, + TimeSetCreateOut, + TimeSetDeleteIn, + TimeSetGetIn, + TimeSetGetOut, + TimeSetIndexItem, + TimeSetReorderIn, + TimeSetUpdateIn, +) from app.api.ws_command import ws_command router = APIRouter(prefix="/api/queue", tags=["调度队列管理"]) @@ -39,7 +66,6 @@ status_code=200, ) async def add_queue() -> QueueCreateOut: - try: uid, config = await Config.add_queue() data = QueueConfig(**(await config.toDict())) @@ -63,7 +89,6 @@ async def add_queue() -> QueueCreateOut: status_code=200, ) async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut: - try: index, config = await Config.get_queue(queue.queueId) index = [QueueIndexItem(**_) for _ in index] @@ -87,7 +112,6 @@ async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut: status_code=200, ) async def update_queue(queue: QueueUpdateIn = Body(...)) -> OutBase: - try: await Config.update_queue( queue.queueId, queue.data.model_dump(exclude_unset=True) @@ -107,7 +131,6 @@ async def update_queue(queue: QueueUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_queue(queue: QueueDeleteIn = Body(...)) -> OutBase: - try: await Config.del_queue(queue.queueId) except Exception as e: @@ -125,7 +148,6 @@ async def delete_queue(queue: QueueDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_queue(script.indexList) except Exception as e: @@ -143,7 +165,6 @@ async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase: status_code=200, ) async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut: - try: index, data = await Config.get_time_set(time.queueId, time.timeSetId) index = [TimeSetIndexItem(**_) for _ in index] @@ -167,7 +188,6 @@ async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut: status_code=200, ) async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut: - uid, config = await Config.add_time_set(time.queueId) data = TimeSet(**(await config.toDict())) return TimeSetCreateOut(timeSetId=str(uid), data=data) @@ -181,7 +201,6 @@ async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut: status_code=200, ) async def update_time_set(time: TimeSetUpdateIn = Body(...)) -> OutBase: - try: await Config.update_time_set( time.queueId, time.timeSetId, time.data.model_dump(exclude_unset=True) @@ -201,7 +220,6 @@ async def update_time_set(time: TimeSetUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_time_set(time: TimeSetDeleteIn = Body(...)) -> OutBase: - try: await Config.del_time_set(time.queueId, time.timeSetId) except Exception as e: @@ -219,7 +237,6 @@ async def delete_time_set(time: TimeSetDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_time_set(time.queueId, time.indexList) except Exception as e: @@ -237,7 +254,6 @@ async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase: status_code=200, ) async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut: - try: index, data = await Config.get_queue_item(item.queueId, item.queueItemId) index = [QueueItemIndexItem(**_) for _ in index] @@ -261,7 +277,6 @@ async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut: status_code=200, ) async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut: - uid, config = await Config.add_queue_item(item.queueId) data = QueueItem(**(await config.toDict())) return QueueItemCreateOut(queueItemId=str(uid), data=data) @@ -275,7 +290,6 @@ async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut: status_code=200, ) async def update_item(item: QueueItemUpdateIn = Body(...)) -> OutBase: - try: await Config.update_queue_item( item.queueId, item.queueItemId, item.data.model_dump(exclude_unset=True) @@ -295,7 +309,6 @@ async def update_item(item: QueueItemUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_item(item: QueueItemDeleteIn = Body(...)) -> OutBase: - try: await Config.del_queue_item(item.queueId, item.queueItemId) except Exception as e: @@ -313,7 +326,6 @@ async def delete_item(item: QueueItemDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_item(item: QueueItemReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_queue_item(item.queueId, item.indexList) except Exception as e: diff --git a/app/api/scripts.py b/app/api/scripts.py index 76b82232..f2e03dc4 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -25,7 +25,49 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.schema import * +from app.models.dto import ( + ComboBoxItem, + ComboBoxOut, + GeneralConfig, + GeneralUserConfig, + MaaConfig, + MaaEndConfig, + MaaEndUserConfig, + MaaUserConfig, + OutBase, + ScriptCreateIn, + ScriptCreateOut, + ScriptDeleteIn, + ScriptFileIn, + ScriptGetIn, + ScriptGetOut, + ScriptIndexItem, + ScriptReorderIn, + ScriptUpdateIn, + ScriptUploadIn, + ScriptUrlIn, + SrcConfig, + SrcUserConfig, + UserCreateOut, + UserDeleteIn, + UserGetIn, + UserGetOut, + UserInBase, + UserIndexItem, + UserReorderIn, + UserSetIn, + UserUpdateIn, + Webhook, + WebhookCreateOut, + WebhookDeleteIn, + WebhookGetIn, + WebhookGetOut, + WebhookInBase, + WebhookIndexItem, + WebhookReorderIn, + WebhookTestIn, + WebhookUpdateIn, +) router = APIRouter(prefix="/api/scripts", tags=["脚本管理"]) @@ -52,7 +94,6 @@ status_code=200, ) async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: - try: uid, config = await Config.add_script(script.type, script.scriptId) data = SCRIPT_BOOK[type(config).__name__](**(await config.toDict())) @@ -75,7 +116,6 @@ async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: status_code=200, ) async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut: - try: index, data = await Config.get_script(script.scriptId) index = [ScriptIndexItem(**_) for _ in index] @@ -104,7 +144,6 @@ async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut: status_code=200, ) async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase: - try: await Config.update_script( script.scriptId, script.data.model_dump(exclude_unset=True) @@ -124,7 +163,6 @@ async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_script(script: ScriptDeleteIn = Body(...)) -> OutBase: - try: await Config.del_script(script.scriptId) except Exception as e: @@ -142,7 +180,6 @@ async def delete_script(script: ScriptDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_script(script: ScriptReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_script(script.indexList) except Exception as e: @@ -160,7 +197,6 @@ async def reorder_script(script: ScriptReorderIn = Body(...)) -> OutBase: status_code=200, ) async def import_script_from_file(script: ScriptFileIn = Body(...)) -> OutBase: - try: await Config.import_script_from_file(script.scriptId, script.jsonFile) except Exception as e: @@ -178,7 +214,6 @@ async def import_script_from_file(script: ScriptFileIn = Body(...)) -> OutBase: status_code=200, ) async def export_script_to_file(script: ScriptFileIn = Body(...)) -> OutBase: - try: await Config.export_script_to_file(script.scriptId, script.jsonFile) except Exception as e: @@ -196,7 +231,6 @@ async def export_script_to_file(script: ScriptFileIn = Body(...)) -> OutBase: status_code=200, ) async def import_script_from_web(script: ScriptUrlIn = Body(...)) -> OutBase: - try: await Config.import_script_from_web(script.scriptId, script.url) except Exception as e: @@ -214,7 +248,6 @@ async def import_script_from_web(script: ScriptUrlIn = Body(...)) -> OutBase: status_code=200, ) async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase: - try: await Config.upload_script_to_web( script.scriptId, script.config_name, script.author, script.description @@ -234,7 +267,6 @@ async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase: status_code=200, ) async def get_user(user: UserGetIn = Body(...)) -> UserGetOut: - try: index, data = await Config.get_user(user.scriptId, user.userId) index = [UserIndexItem(**_) for _ in index] @@ -263,7 +295,6 @@ async def get_user(user: UserGetIn = Body(...)) -> UserGetOut: status_code=200, ) async def add_user(user: UserInBase = Body(...)) -> UserCreateOut: - try: uid, config = await Config.add_user(user.scriptId) data = USER_BOOK[type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__]( @@ -288,7 +319,6 @@ async def add_user(user: UserInBase = Body(...)) -> UserCreateOut: status_code=200, ) async def update_user(user: UserUpdateIn = Body(...)) -> OutBase: - try: await Config.update_user( user.scriptId, user.userId, user.data.model_dump(exclude_unset=True) @@ -308,7 +338,6 @@ async def update_user(user: UserUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_user(user: UserDeleteIn = Body(...)) -> OutBase: - try: await Config.del_user(user.scriptId, user.userId) except Exception as e: @@ -326,7 +355,6 @@ async def delete_user(user: UserDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_user(user: UserReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_user(user.scriptId, user.indexList) except Exception as e: @@ -344,7 +372,6 @@ async def reorder_user(user: UserReorderIn = Body(...)) -> OutBase: status_code=200, ) async def import_infrastructure(user: UserSetIn = Body(...)) -> OutBase: - try: await Config.set_infrastructure(user.scriptId, user.userId, user.jsonFile) except Exception as e: @@ -362,7 +389,6 @@ async def import_infrastructure(user: UserSetIn = Body(...)) -> OutBase: status_code=200, ) async def get_user_combox_infrastructure(user: UserDeleteIn = Body(...)) -> ComboBoxOut: - try: raw_data = await Config.get_user_combox_infrastructure( user.scriptId, user.userId @@ -383,7 +409,6 @@ async def get_user_combox_infrastructure(user: UserDeleteIn = Body(...)) -> Comb status_code=200, ) async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: - try: index, data = await Config.get_webhook( webhook.scriptId, webhook.userId, webhook.webhookId @@ -409,7 +434,6 @@ async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: status_code=200, ) async def add_webhook(webhook: WebhookInBase = Body(...)) -> WebhookCreateOut: - try: uid, config = await Config.add_webhook(webhook.scriptId, webhook.userId) data = Webhook(**(await config.toDict())) @@ -432,7 +456,6 @@ async def add_webhook(webhook: WebhookInBase = Body(...)) -> WebhookCreateOut: status_code=200, ) async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase: - try: await Config.update_webhook( webhook.scriptId, @@ -455,7 +478,6 @@ async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase: status_code=200, ) async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase: - try: await Config.del_webhook(webhook.scriptId, webhook.userId, webhook.webhookId) except Exception as e: @@ -473,7 +495,6 @@ async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase: status_code=200, ) async def reorder_webhook(webhook: WebhookReorderIn = Body(...)) -> OutBase: - try: await Config.reorder_webhook( webhook.scriptId, webhook.userId, webhook.indexList diff --git a/app/api/setting.py b/app/api/setting.py index 36a864b7..6c5ba00f 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -24,7 +24,7 @@ from fastapi import APIRouter, Body from app.core import Config from app.services import Notify -from app.models.schema import ( +from app.models.dto import ( SettingGetOut, GlobalConfig, OutBase, @@ -39,7 +39,7 @@ WebhookReorderIn, WebhookTestIn, ) -from app.models.config import Webhook as WebhookConfig +from app.models import Webhook as WebhookConfig router = APIRouter(prefix="/api/setting", tags=["全局设置"]) diff --git a/app/api/tools.py b/app/api/tools.py index 6cc1f178..aeb42008 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -23,7 +23,7 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.schema import ToolsGetOut, ToolsConfig, OutBase, ToolsUpdateIn +from app.models.dto import ToolsGetOut, ToolsConfig, OutBase, ToolsUpdateIn router = APIRouter(prefix="/api/tools", tags=["工具设置"]) diff --git a/app/api/update.py b/app/api/update.py index 9d007705..bae4aea0 100644 --- a/app/api/update.py +++ b/app/api/update.py @@ -26,7 +26,7 @@ from app.core import Config from app.services import Updater -from app.models.schema import * +from app.models.dto import OutBase, UpdateCheckIn, UpdateCheckOut router = APIRouter(prefix="/api/update", tags=["软件更新"]) @@ -39,7 +39,6 @@ status_code=200, ) async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut: - try: if_need, latest_version, update_info = await Updater.check_update( current_version=version.current_version, if_force=version.if_force @@ -66,7 +65,6 @@ async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut: status_code=200, ) async def download_update() -> OutBase: - try: task = asyncio.create_task(Updater.download_update()) Config.temp_task.append(task) @@ -86,7 +84,6 @@ async def download_update() -> OutBase: status_code=200, ) async def install_update() -> OutBase: - try: task = asyncio.create_task(Updater.install_update()) Config.temp_task.append(task) diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index ea9fac0f..b96af949 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -34,7 +34,7 @@ from app.utils.websocket import ws_client_manager from app.api.ws_command import list_ws_commands from app.utils.logger import get_logger -from app.models.schema import ( +from app.models.dto import ( WSClientCreateIn, WSClientCreateOut, WSClientConnectIn, diff --git a/app/core/broadcast.py b/app/core/broadcast.py index 62e53f8b..0d60c3e0 100644 --- a/app/core/broadcast.py +++ b/app/core/broadcast.py @@ -22,7 +22,7 @@ import asyncio from copy import deepcopy -from typing import Set +from typing import Any, Set from app.utils import get_logger @@ -31,19 +31,18 @@ class _Broadcast: - def __init__(self): - self.__subscribers: Set[asyncio.Queue] = set() + self.__subscribers: Set[asyncio.Queue[Any]] = set() - async def subscribe(self, queue: asyncio.Queue): + async def subscribe(self, queue: asyncio.Queue[Any]) -> None: """订阅者注册""" self.__subscribers.add(queue) - async def unsubscribe(self, queue: asyncio.Queue): + async def unsubscribe(self, queue: asyncio.Queue[Any]) -> None: """取消订阅""" self.__subscribers.remove(queue) - async def put(self, item): + async def put(self, item: Any) -> None: """向所有订阅者广播消息""" logger.debug(f"向所有订阅者广播消息: {item}") for subscriber in self.__subscribers: diff --git a/app/core/config.py b/app/core/config.py index 873ce995..ff987b84 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -19,6 +19,7 @@ # along with AUTO-MAS. If not, see . # Contact: DLmaster_361@163.com +# pyright: reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingParameterType=false, reportInvalidTypeForm=false, reportGeneralTypeIssues=false import os import re @@ -28,17 +29,18 @@ import asyncio import uvicorn import sqlite3 +import tomllib import truststore from pathlib import Path from fastapi import WebSocket from collections import defaultdict from jinja2 import Environment, FileSystemLoader from datetime import datetime, timedelta, date -from typing import Literal, Optional, Union, Dict, Any, List +from typing import Literal, Optional, Dict, Any, List import uuid import json -from app.models.config import ( +from app.models import ( GeneralConfig, MaaConfig, SrcConfig, @@ -51,12 +53,12 @@ MaaEndUserConfig, GeneralUserConfig, GlobalConfig, - CLASS_BOOK, Webhook, TimeSet, EmulatorConfig, + dump_toml, ) -from app.models.schema import WebSocketMessage +from app.models.dto import WebSocketMessage from app.utils.constants import ( UTC4, UTC8, @@ -69,6 +71,11 @@ logger = get_logger("配置管理") +ScriptConfigClass = ( + type[MaaConfig] | type[SrcConfig] | type[GeneralConfig] | type[MaaEndConfig] +) +ScriptConfigData = MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig + if (Path.cwd() / "environment/git/bin/git.exe").exists(): os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = str( Path.cwd() / "environment/git/bin/git.exe" @@ -128,21 +135,49 @@ def __init__(self) -> None: "Sleep", "KillSelf", ] = "NoAction" - self.temp_task: List[asyncio.Task] = [] + self.temp_task: List[asyncio.Task[Any]] = [] truststore.inject_into_ssl() + def _resolve_config_path(self, stem: str) -> Path: + """优先返回 TOML 配置路径;若仅存在 JSON,则返回 JSON。""" + + toml_path = self.config_path / f"{stem}.toml" + json_path = self.config_path / f"{stem}.json" + if toml_path.exists() or not json_path.exists(): + return toml_path + return json_path + + async def _connect_runtime_configs(self) -> None: + """连接运行期主配置文件。""" + + await self.connect(self._resolve_config_path("Config")) + await self.EmulatorConfig.connect(self._resolve_config_path("EmulatorConfig")) + await self.PlanConfig.connect(self._resolve_config_path("PlanConfig")) + await self.ScriptConfig.connect(self._resolve_config_path("ScriptConfig")) + await self.QueueConfig.connect(self._resolve_config_path("QueueConfig")) + await self.ToolsConfig.connect(self._resolve_config_path("ToolsConfig")) + + def _read_mapping_config(self, path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + if path.suffix == ".toml": + return tomllib.loads(text) if text.strip() else {} + return json.loads(text) + + def _write_mapping_config(self, path: Path, data: dict[str, Any]) -> None: + if path.suffix == ".toml": + path.write_text(dump_toml(data), encoding="utf-8") + else: + path.write_text( + json.dumps(data, ensure_ascii=False, indent=4), encoding="utf-8" + ) + async def init_config(self) -> None: """初始化配置管理""" await self.check_data() - await self.connect(self.config_path / "Config.json") - await self.EmulatorConfig.connect(self.config_path / "EmulatorConfig.json") - await self.PlanConfig.connect(self.config_path / "PlanConfig.json") - await self.ScriptConfig.connect(self.config_path / "ScriptConfig.json") - await self.QueueConfig.connect(self.config_path / "QueueConfig.json") - await self.ToolsConfig.connect(self.config_path / "ToolsConfig.json") + await self._connect_runtime_configs() from app.services import System @@ -221,15 +256,17 @@ async def check_data(self) -> None: ) if_streaming = True - await self.ScriptConfig.connect(self.config_path / "ScriptConfig.json") - await self.PlanConfig.connect(self.config_path / "PlanConfig.json") - await self.QueueConfig.connect(self.config_path / "QueueConfig.json") + await self.ScriptConfig.connect( + self._resolve_config_path("ScriptConfig") + ) + await self.PlanConfig.connect(self._resolve_config_path("PlanConfig")) + await self.QueueConfig.connect(self._resolve_config_path("QueueConfig")) if (Path.cwd() / "config/config.json").exists(): (Path.cwd() / "config/config.json").rename( Path.cwd() / "config/Config.json" ) - await self.connect(self.config_path / "Config.json") + await self.connect(self._resolve_config_path("Config")) plan_dict = {"固定": "Fixed"} @@ -376,8 +413,8 @@ async def check_data(self) -> None: await qc.load(queue_config) for i in range(10): - item_uid, item = await self.add_queue_item(str(uid)) - time_uid, time = await self.add_time_set(str(uid)) + _, item = await self.add_queue_item(str(uid)) + _, time = await self.add_time_set(str(uid)) await time.load( { @@ -420,18 +457,15 @@ async def check_data(self) -> None: ) if_streaming = True - if (Path.cwd() / "config/Config.json").exists(): - data = json.loads( - (Path.cwd() / "config/Config.json").read_text(encoding="utf-8") - ) + global_config_path = self._resolve_config_path("Config") + if global_config_path.exists(): + data = self._read_mapping_config(global_config_path) data["Data"]["LastStageUpdated"] = "" data["Data"]["Stage"] = "{ }" data["Function"]["IfBlockAd"] = data["Function"].get( "IfSkipMumuSplashAds", False ) - (Path.cwd() / "config/Config.json").write_text( - json.dumps(data, ensure_ascii=False, indent=4), encoding="utf-8" - ) + self._write_mapping_config(global_config_path, data) cur.execute("DELETE FROM version WHERE v = ?", ("v1.9",)) cur.execute("INSERT INTO version VALUES(?)", ("v1.10",)) @@ -443,19 +477,16 @@ async def check_data(self) -> None: ) if_streaming = True - if (Path.cwd() / "config/ScriptConfig.json").exists(): - data = (Path.cwd() / "config/ScriptConfig.json").read_text( - encoding="utf-8" - ) - data.replace("IfWakeUp", "IfStartUp") - data.replace("IfAutoRoguelike", "IfRoguelike") - data.replace("IfBase", "IfInfrast") - data.replace("IfCombat", "IfFight") - data.replace("IfMission", "IfAward") - data.replace("IfRecruiting", "IfRecruit") - (Path.cwd() / "config/ScriptConfig.json").write_text( - data, encoding="utf-8" - ) + script_config_path = self._resolve_config_path("ScriptConfig") + if script_config_path.exists(): + data = script_config_path.read_text(encoding="utf-8") + data = data.replace("IfWakeUp", "IfStartUp") + data = data.replace("IfAutoRoguelike", "IfRoguelike") + data = data.replace("IfBase", "IfInfrast") + data = data.replace("IfCombat", "IfFight") + data = data.replace("IfMission", "IfAward") + data = data.replace("IfRecruiting", "IfRecruit") + script_config_path.write_text(data, encoding="utf-8") cur.execute("DELETE FROM version WHERE v = ?", ("v1.10",)) cur.execute("INSERT INTO version VALUES(?)", ("v1.11",)) @@ -465,7 +496,7 @@ async def check_data(self) -> None: db.close() logger.success("数据文件版本更新完成") - async def send_json(self, data: dict) -> None: + async def send_json(self, data: dict[str, Any]) -> None: """通过WebSocket发送JSON数据""" if Config.websocket is None: logger.warning("WebSocket 未连接") @@ -490,7 +521,6 @@ async def get_git_version(self) -> tuple[bool, str, str]: """获取Git版本信息,如果Git不可用则返回默认值""" def _get_git_info(): - if self.repo is None: logger.warning("Git仓库不可用,返回默认版本信息") return False, "unknown", "unknown" @@ -532,15 +562,25 @@ async def add_script( logger.info(f"添加脚本配置: {script}, 从 {script_id} 复制") + script_class_map: dict[ + Literal["MAA", "SRC", "General", "MaaEnd"], ScriptConfigClass + ] = { + "MAA": MaaConfig, + "SRC": SrcConfig, + "General": GeneralConfig, + "MaaEnd": MaaEndConfig, + } + script_class = script_class_map[script] + if script_id is None: - return await self.ScriptConfig.add(CLASS_BOOK[script]) + return await self.ScriptConfig.add(script_class) else: script_uid = uuid.UUID(script_id) - if not isinstance(self.ScriptConfig[script_uid], CLASS_BOOK[script]): + if type(self.ScriptConfig[script_uid]) is not script_class: raise TypeError(f"脚本配置类型不匹配: {script_id} {script}") - new_uid, new_config = await self.ScriptConfig.add(CLASS_BOOK[script]) + new_uid, new_config = await self.ScriptConfig.add(script_class) await new_config.load( await self.ScriptConfig[script_uid].toDict(regenerate_uuids=True) @@ -564,7 +604,9 @@ async def add_script( return new_uid, new_config - async def get_script(self, script_id: str | None) -> tuple[list, dict]: + async def get_script( + self, script_id: str | None + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取脚本配置""" logger.info(f"获取脚本配置: {script_id}") @@ -761,7 +803,9 @@ async def upload_script_to_web( logger.error(f"无法上传配置到 AUTO-MAS 服务器: {e}") raise ConnectionError(f"无法上传配置到 AUTO-MAS 服务器: {e}") - async def remove_privacy_info(self, confg: dict, name: str) -> dict: + async def remove_privacy_info( + self, confg: dict[str, Any], name: str + ) -> dict[str, Any]: """移除配置中可能存在的隐私信息""" confg["Info"]["Name"] = name @@ -778,16 +822,16 @@ async def remove_privacy_info(self, confg: dict, name: str) -> dict: if sys.platform == "win32" and Path(confg["Script"][path]).is_relative_to( Path(os.environ["APPDATA"]) ): - confg["Script"][ - path - ] = f"%APPDATA%/{Path(confg["Script"][path]).relative_to(Path(os.environ["APPDATA"]))}" + confg["Script"][path] = ( + f"%APPDATA%/{Path(confg["Script"][path]).relative_to(Path(os.environ["APPDATA"]))}" + ) confg["Info"]["RootPath"] = str(Path(r"C:/脚本根目录")) return confg async def get_user( self, script_id: str, user_id: Optional[str] - ) -> tuple[list, dict]: + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取用户配置""" logger.info(f"获取用户配置: {script_id} - {user_id}") @@ -822,10 +866,8 @@ async def add_user( uid, config = await script_config.UserData.add(SrcUserConfig) elif isinstance(script_config, GeneralConfig): uid, config = await script_config.UserData.add(GeneralUserConfig) - elif isinstance(script_config, MaaEndConfig): - uid, config = await script_config.UserData.add(MaaEndUserConfig) else: - raise TypeError(f"不支持的脚本配置类型: {type(script_config)}") + uid, config = await script_config.UserData.add(MaaEndUserConfig) return uid, config @@ -894,13 +936,15 @@ async def set_infrastructure( if infrast_data.get("title", "文件标题") == "文件标题": infrast_data["title"] = json_path.stem - await self.ScriptConfig[script_uid].UserData[user_uid].set( - "Data", "CustomInfrast", json.dumps(infrast_data, ensure_ascii=False) + await ( + self.ScriptConfig[script_uid] + .UserData[user_uid] + .set("Data", "CustomInfrast", json.dumps(infrast_data, ensure_ascii=False)) ) async def get_user_combox_infrastructure( self, script_id: str, user_id: str - ) -> list[dict]: + ) -> list[dict[str, str]]: logger.info(f"获取用户自定义基建排班下拉框信息: {script_id} - {user_id}") script_uid = uuid.UUID(script_id) @@ -933,9 +977,11 @@ async def add_plan( logger.info(f"添加计划表: {script}") - return await self.PlanConfig.add(CLASS_BOOK[script]) + return await self.PlanConfig.add(MaaPlanConfig) - async def get_plan(self, plan_id: Optional[str]) -> tuple[list, dict]: + async def get_plan( + self, plan_id: Optional[str] + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取计划表配置""" logger.info(f"获取计划表配置: {plan_id}") @@ -990,7 +1036,9 @@ async def reorder_plan(self, index_list: list[str]) -> None: await self.PlanConfig.setOrder(list(map(uuid.UUID, index_list))) - async def get_emulator(self, emulator_id: Optional[str]) -> tuple[list, dict]: + async def get_emulator( + self, emulator_id: Optional[str] + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取模拟器配置""" logger.info(f"获取全局模拟器设置: {emulator_id}") @@ -1071,7 +1119,9 @@ async def add_queue(self) -> tuple[uuid.UUID, QueueConfig]: return await self.QueueConfig.add(QueueConfig) - async def get_queue(self, queue_id: Optional[str]) -> tuple[list, dict]: + async def get_queue( + self, queue_id: Optional[str] + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取调度队列配置""" logger.info(f"获取调度队列配置: {queue_id}") @@ -1113,7 +1163,7 @@ async def reorder_queue(self, index_list: list[str]) -> None: async def get_time_set( self, queue_id: str, time_set_id: Optional[str] - ) -> tuple[list, dict]: + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取时间设置配置""" logger.info(f"获取队列的时间配置: {queue_id} - {time_set_id}") @@ -1179,7 +1229,7 @@ async def reorder_time_set(self, queue_id: str, index_list: list[str]) -> None: async def get_queue_item( self, queue_id: str, queue_item_id: Optional[str] - ) -> tuple[list, dict]: + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取队列项配置""" logger.info(f"获取队列的队列项配置: {queue_id} - {queue_item_id}") @@ -1287,7 +1337,7 @@ async def get_webhook( script_id: Optional[str], user_id: Optional[str], webhook_id: Optional[str], - ) -> tuple[list, dict]: + ) -> tuple[list[dict[str, str]], dict[str, Any]]: """获取webhook配置""" if script_id is None and user_id is None: @@ -1471,8 +1521,10 @@ async def get_stage_info( today = datetime.now(tz=UTC4).isoweekday() res_stage_info = [] for stage in RESOURCE_STAGE_INFO: + days = stage.get("days") if ( - today in stage["days"] + isinstance(days, list) + and today in days and stage["value"] in RESOURCE_STAGE_DROP_INFO ): res_stage_info.append(RESOURCE_STAGE_DROP_INFO[stage["value"]]) @@ -1772,7 +1824,9 @@ async def get_web_config(self): return remote_web_config - async def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool: + async def save_maa_log( + self, log_path: Path, logs: list[str], maa_result: str + ) -> bool: """ 保存MAA日志并生成对应统计数据 @@ -1786,7 +1840,7 @@ async def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> boo logger.info(f"开始处理 MAA 日志, 日志长度: {len(logs)}, 日志标记: {maa_result}") - data = { + data: dict[str, Any] = { "recruit_statistics": defaultdict(int), "drop_statistics": defaultdict(dict), "sanity": 0, @@ -1841,7 +1895,7 @@ async def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> boo # 掉落统计 # 存储所有关卡的掉落统计 - all_stage_drops = {} + all_stage_drops: dict[str, dict[str, int]] = {} # 查找所有Fight任务的开始和结束位置 fight_tasks = [] @@ -1957,7 +2011,9 @@ async def save_maaend_log( logger.success(f"MaaEnd日志统计完成, 日志路径: {log_path.with_suffix('.log')}") - async def save_src_log(self, log_path: Path, logs: list, src_result: str) -> None: + async def save_src_log( + self, log_path: Path, logs: list[str], src_result: str + ) -> None: """ 保存SRC日志并生成对应统计数据 @@ -1981,7 +2037,7 @@ async def save_src_log(self, log_path: Path, logs: list, src_result: str) -> Non logger.success(f"SRC日志统计完成, 日志路径: {log_path.with_suffix('.log')}") async def save_general_log( - self, log_path: Path, logs: list, general_result: str + self, log_path: Path, logs: list[str], general_result: str ) -> None: """ 保存通用日志并生成对应统计数据 @@ -2006,7 +2062,9 @@ async def save_general_log( logger.success(f"通用日志统计完成, 日志路径: {log_path.with_suffix('.log')}") - async def merge_statistic_info(self, statistic_path_list: List[Path]) -> dict: + async def merge_statistic_info( + self, statistic_path_list: List[Path] + ) -> dict[str, Any]: """ 合并指定数据统计信息文件 @@ -2099,7 +2157,7 @@ async def search_history( mode: Literal["DAILY", "WEEKLY", "MONTHLY"], start_date: date, end_date: date, - ) -> dict: + ) -> dict[str, dict[str, list[Path]]]: """ 搜索指定时间范围内的历史记录 @@ -2113,7 +2171,7 @@ async def search_history( f"开始搜索历史记录, 合并模式: {mode}, 日期范围: {start_date} 至 {end_date}" ) - history_dict = {} + history_dict: dict[str, dict[str, list[Path]]] = {} for date_folder in self.history_path.iterdir(): if not date_folder.is_dir(): @@ -2157,7 +2215,7 @@ async def search_history( return { k: v - for k, v in sorted(history_dict.items(), key=lambda x: x[0], reverse=True) + for k, v in sorted(history_dict.items(), key=lambda kv: kv[0], reverse=True) } async def clean_old_history(self): diff --git a/app/core/emulator_manager.py b/app/core/emulator_manager.py index 95dc8987..5f702df2 100644 --- a/app/core/emulator_manager.py +++ b/app/core/emulator_manager.py @@ -28,9 +28,9 @@ from typing import Dict, Literal from .config import Config -from app.models.config import EmulatorConfig +from app.models import EmulatorConfig from app.models.emulator import DeviceBase -from app.models.schema import DeviceInfo as SchemaDeviceInfo +from app.models.dto import DeviceInfo as SchemaDeviceInfo from app.utils import ProcessRunner, EMULATOR_TYPE_BOOK from app.utils.constants import EMULATOR_SPLASH_ADS_PATH_BOOK @@ -44,14 +44,12 @@ class _EmulatorManager: """模拟器实例管理器""" async def get_emulator_instance(self, emulator_id: str) -> DeviceBase: - emulator_uid = uuid.UUID(emulator_id) config = EmulatorConfig() await config.load(await Config.EmulatorConfig[emulator_uid].toDict()) if config.get("Info", "Type") in EMULATOR_TYPE_BOOK: - # 设置模拟器广告 with suppress(Exception): if config.get("Info", "Type") in EMULATOR_SPLASH_ADS_PATH_BOOK: @@ -80,13 +78,11 @@ async def get_emulator_instance(self, emulator_id: str) -> DeviceBase: async def operate_emulator( self, operate: Literal["open", "close", "show"], emulator_id: str, index: str ): - asyncio.create_task(self.operate_emulator_task(operate, emulator_id, index)) async def operate_emulator_task( self, operate: Literal["open", "close", "show"], emulator_id: str, index: str ): - try: temp_emulator = await self.get_emulator_instance(emulator_id) if temp_emulator is None: @@ -108,7 +104,6 @@ async def operate_emulator_task( async def get_status( self, emulator_id: str | None = None ) -> Dict[str, Dict[str, SchemaDeviceInfo]]: - if emulator_id is None: emulator_range = list(map(str, Config.EmulatorConfig.keys())) else: diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py index 0084d3e4..d4736718 100644 --- a/app/models/ConfigBase.py +++ b/app/models/ConfigBase.py @@ -22,8 +22,11 @@ from __future__ import annotations + +# pyright: reportMissingParameterType=false, reportUnknownParameterType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownLambdaType=false, reportDeprecated=false, reportInvalidTypeForm=false, reportGeneralTypeIssues=false import os import json +import tomllib import uuid import shlex import inspect @@ -36,7 +39,10 @@ from contextlib import suppress from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Type, TypeVar, Generic, Callable, Coroutine +from collections.abc import Callable, Coroutine +from typing import Any, Protocol, TypeVar, Generic +from pydantic import TypeAdapter +from pydantic_settings import BaseSettings, SettingsConfigDict from app.utils import get_logger, dpapi_encrypt, dpapi_decrypt from app.utils.constants import ( @@ -49,6 +55,128 @@ logger = get_logger("配置基类") +def _load_toml_with_pydantic_settings(path: Path) -> dict[str, Any]: + """使用 pydantic-settings 校验 TOML 内容并返回 dict。""" + + raw_text = path.read_text(encoding="utf-8") + if raw_text.strip() == "": + return {} + + raw_data = tomllib.loads(raw_text) + + dynamic_settings_cls = type( + "DynamicTomlSettings", + (BaseSettings,), + { + "model_config": SettingsConfigDict(extra="allow"), + }, + ) + + loaded = dynamic_settings_cls.model_validate(raw_data) + data = loaded.model_dump(mode="python") + extra = getattr(loaded, "__pydantic_extra__", None) + if isinstance(extra, dict): + data.update(extra) + + return TypeAdapter(dict[str, Any]).validate_python(data) + + +def _toml_scalar(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if value is None: + return '""' + return json.dumps(str(value), ensure_ascii=False) + + +def _toml_inline(value: Any) -> str: + if isinstance(value, list): + return "[" + ", ".join(_toml_inline(item) for item in value) + "]" + return _toml_scalar(value) + + +def _dump_toml(data: dict[str, Any]) -> str: + lines: list[str] = [] + + def emit_table(prefix: str, obj: dict[str, Any]) -> None: + scalars: list[tuple[str, Any]] = [] + children: list[tuple[str, dict[str, Any]]] = [] + + for key, value in obj.items(): + if isinstance(value, dict): + children.append((key, value)) + else: + scalars.append((key, value)) + + if prefix: + lines.append(f"[{prefix}]") + + for key, value in scalars: + lines.append(f"{key} = {_toml_inline(value)}") + + if scalars and children: + lines.append("") + + for index, (key, child) in enumerate(children): + child_prefix = f"{prefix}.{key}" if prefix else key + emit_table(child_prefix, child) + if index != len(children) - 1: + lines.append("") + + emit_table("", data) + content = "\n".join(lines).strip() + return f"{content}\n" if content else "" + + +def dump_toml(data: dict[str, Any]) -> str: + """公共 TOML 序列化入口。""" + + return _dump_toml(data) + + +def _load_config_with_legacy_migration( + path: Path, +) -> tuple[dict[str, Any], Path | None]: + legacy_json_file = path.with_suffix(".json") + + if legacy_json_file.exists() and (not path.exists() or path.stat().st_size == 0): + try: + return json.loads( + legacy_json_file.read_text(encoding="utf-8") + ), legacy_json_file + except json.JSONDecodeError: + return {}, legacy_json_file + + if not path.exists(): + return {}, legacy_json_file if legacy_json_file.exists() else None + + try: + return _load_toml_with_pydantic_settings( + path + ), legacy_json_file if legacy_json_file.exists() else None + except Exception: + with suppress(Exception): + return tomllib.loads( + path.read_text(encoding="utf-8") + ), legacy_json_file if legacy_json_file.exists() else None + return {}, legacy_json_file if legacy_json_file.exists() else None + + +def _backup_legacy_json_if_needed( + current_file: Path, legacy_json_file: Path | None +) -> None: + if legacy_json_file is None or not legacy_json_file.exists(): + return + if not current_file.exists() or current_file.stat().st_size == 0: + return + + legacy_backup = legacy_json_file.with_suffix(".json.bak") + if not legacy_backup.exists(): + legacy_json_file.replace(legacy_backup) + + class ValidatorBase(ABC): """基础配置验证器""" @@ -98,7 +226,7 @@ def correct(self, value): class OptionsValidator(ValidatorBase): """选项验证器""" - def __init__(self, options: list): + def __init__(self, options: list[Any]): if not options: raise ValueError("可选项不能为空") @@ -114,7 +242,7 @@ def correct(self, value): class MultipleOptionsValidator(ValidatorBase): """多选选项验证器""" - def __init__(self, options: list): + def __init__(self, options: list[Any]): if not options: raise ValueError("可选项不能为空") @@ -148,7 +276,10 @@ class MultipleUIDValidator(ValidatorBase): """多配置管理类UID验证器""" def __init__( - self, default: Any, related_config: dict[str, MultipleConfig], config_name: str + self, + default: Any, + related_config: dict[str, "MultipleConfig[Any]"], + config_name: str, ): self.default = default self.related_config = related_config @@ -201,7 +332,7 @@ def correct(self, value): class JSONValidator(ValidatorBase): - def __init__(self, tpye: type[dict] | type[list] = dict) -> None: + def __init__(self, tpye: type[dict[str, Any]] | type[list[Any]] = dict) -> None: self.type = tpye def validate(self, value): @@ -218,7 +349,7 @@ def validate(self, value): def correct(self, value): return ( - value if self.validate(value) else ("{ }" if self.type == dict else "[ ]") + value if self.validate(value) else ("{ }" if self.type is dict else "[ ]") ) @@ -231,7 +362,7 @@ def validate(self, value): try: dpapi_decrypt(value) return True - except: + except Exception: return False def correct(self, value: Any) -> Any: @@ -300,7 +431,7 @@ def correct(self, value): shell = win32com.client.Dispatch("WScript.Shell") shortcut = shell.CreateShortcut(value) value = shortcut.TargetPath - except: + except Exception: pass return Path(value).resolve().as_posix() @@ -361,7 +492,6 @@ def validate(self, value): return True def correct(self, value): - if not isinstance(value, str): value = str(Path.cwd()) # 空字符串直接返回 @@ -376,7 +506,7 @@ def correct(self, value): shell = win32com.client.Dispatch("WScript.Shell") shortcut = shell.CreateShortcut(value) value = shortcut.TargetPath - except: + except Exception: pass # 不支持矫正的模拟器类型直接返回路径字符串 @@ -569,7 +699,6 @@ def correct(self, value): class ArgumentValidator(ValidatorBase): - def validate(self, value): if not isinstance(value, str): return False @@ -580,12 +709,10 @@ def validate(self, value): return False def correct(self, value): - return value if self.validate(value) else "" class AdvancedArgumentValidator(ValidatorBase): - def validate(self, value): if not isinstance(value, str): return False @@ -601,7 +728,6 @@ def validate(self, value): return False def correct(self, value): - return value if self.validate(value) else "" @@ -672,7 +798,7 @@ def setValue(self, value: Any): # deepcopy new value try: self.value = deepcopy(value) - except: + except Exception: self.value = value if isinstance(self.validator, EncryptValidator): @@ -712,7 +838,7 @@ def bind(self, slot: Callable[[Any], Any]): 槽函数,接收新值作为参数,支持同步和异步函数 """ if not callable(slot): - raise TypeError(f"槽函数必须是可调用对象") + raise TypeError("槽函数必须是可调用对象") if slot not in self._slots: self._slots.append(slot) @@ -763,7 +889,7 @@ def unlock(self): self.is_locked = False -class ConfigBase(ABC): +class ConfigBase: """ 配置基类 @@ -783,7 +909,7 @@ def __init__(self): # 配置项索引 self._config_item_index: dict[str, dict[str, ConfigItem]] = {} - self._multiple_config_index: dict[str, MultipleConfig] = {} + self._multiple_config_index: dict[str, "MultipleConfig[Any]"] = {} for name in dir(self): item = getattr(self, name) @@ -805,8 +931,8 @@ async def connect(self, path: Path): 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 """ - if path.suffix != ".json": - raise ValueError("配置文件必须是扩展名为 '.json' 的 JSON 文件") + if path.suffix != ".toml": + raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") if self.is_locked: raise ValueError("配置已锁定, 无法修改") @@ -816,16 +942,14 @@ async def connect(self, path: Path): if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.touch() - - try: - data = json.loads(self.file.read_text(encoding="utf-8")) - except json.JSONDecodeError: - data = {} + data, legacy_json_file = _load_config_with_legacy_migration(self.file) await self.load(data) await self.add_save_method(self.save) + _backup_legacy_json_if_needed(self.file, legacy_json_file) + async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] ): @@ -844,7 +968,7 @@ async def add_save_method( for sub_config in self._multiple_config_index.values(): await sub_config.add_save_method(save_method) - async def load(self, data: dict): + async def load(self, data: dict[str, Any]): """ 从字典加载配置数据 @@ -873,7 +997,7 @@ async def load(self, data: dict): for name, item in info.items(): try: item.setValue(data[group][name]) - except: + except Exception: if item.legacy_group_name is not None: with suppress(Exception): item.setValue( @@ -994,9 +1118,7 @@ async def save(self) -> None: self.file.parent.mkdir(parents=True, exist_ok=True) self.file.write_text( - json.dumps( - await self.toDict(if_decrypt=False), ensure_ascii=False, indent=4 - ), + _dump_toml(await self.toDict(if_decrypt=False)), encoding="utf-8", ) @@ -1027,7 +1149,26 @@ async def unlock(self): await config.unlock() -T = TypeVar("T", bound="ConfigBase") +class _ConfigLike(Protocol): + @property + def is_locked(self) -> bool: ... + + async def add_save_method( + self, save_method: Callable[[], Coroutine[Any, Any, None]] + ) -> None: ... + + async def load(self, data: dict[str, Any]) -> None: ... + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: ... + + async def lock(self) -> None: ... + + async def unlock(self) -> None: ... + + +T = TypeVar("T", bound=_ConfigLike) class MultipleConfig(Generic[T]): @@ -1043,17 +1184,11 @@ class MultipleConfig(Generic[T]): 子配置项的类型列表, 必须是 ConfigBase 的子类 """ - def __init__(self, sub_config_type: list[Type[T]]): + def __init__(self, sub_config_type: list[type[T]]): if not sub_config_type: raise ValueError("子配置项类型列表不能为空") - for config_type in sub_config_type: - if not issubclass(config_type, ConfigBase): - raise TypeError( - f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类" - ) - - self.sub_config_type: dict[str, Type[T]] = { + self.sub_config_type: dict[str, type[T]] = { _.__name__: _ for _ in sub_config_type } self.file: Path | None = None @@ -1094,8 +1229,8 @@ async def connect(self, path: Path): 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 """ - if path.suffix != ".json": - raise ValueError("配置文件必须是带有 '.json' 扩展名的 JSON 文件。") + if path.suffix != ".toml": + raise ValueError("配置文件必须是带有 '.toml' 扩展名的 TOML 文件。") if self.is_locked: raise ValueError("配置已锁定, 无法修改") @@ -1105,16 +1240,14 @@ async def connect(self, path: Path): if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.touch() - - try: - data = json.loads(self.file.read_text(encoding="utf-8")) - except json.JSONDecodeError: - data = {} + data, legacy_json_file = _load_config_with_legacy_migration(self.file) await self.load(data) await self.add_save_method(self.save) + _backup_legacy_json_if_needed(self.file, legacy_json_file) + async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] ): @@ -1133,7 +1266,7 @@ async def add_save_method( for sub_config in self.data.values(): await sub_config.add_save_method(save_method) - async def load(self, data: dict): + async def load(self, data: dict[str, Any]): """ 从字典加载配置数据 @@ -1157,15 +1290,23 @@ async def load(self, data: dict): return for instance in data["instances"]: - if not isinstance(instance, dict) or not data.get(instance.get("uid")): + if not isinstance(instance, dict): + continue + + uid_str = instance.get("uid") + if not isinstance(uid_str, str): + continue + + instance_data = data.get(uid_str) + if not isinstance(instance_data, dict): continue type_name = instance.get("type") if type_name in self.sub_config_type: - self.order.append(uuid.UUID(instance["uid"])) + self.order.append(uuid.UUID(uid_str)) self.data[self.order[-1]] = self.sub_config_type[type_name]() - await self.data[self.order[-1]].load(data[instance["uid"]]) + await self.data[self.order[-1]].load(instance_data) if self.file: await self.save() @@ -1174,7 +1315,7 @@ async def load(self, data: dict): async def toDict( self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, list | dict]: + ) -> dict[str, Any]: """ 将配置项转换为字典 @@ -1195,7 +1336,7 @@ async def toDict( _: uuid.uuid4() if regenerate_uuids else _ for _ in self.order } - data: dict[str, list | dict] = { + data: dict[str, Any] = { "instances": [ {"uid": str(uuid_book[_]), "type": type(self.data[_]).__name__} for _ in self.order @@ -1208,7 +1349,7 @@ async def toDict( return data - async def get(self, uid: uuid.UUID) -> dict[str, list | dict]: + async def get(self, uid: uuid.UUID) -> dict[str, Any]: """ 获取指定 UID 的配置项 @@ -1225,7 +1366,7 @@ async def get(self, uid: uuid.UUID) -> dict[str, list | dict]: if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在。") - data: dict[str, list | dict] = { + data: dict[str, Any] = { "instances": [ {"uid": str(_), "type": type(self.data[_]).__name__} for _ in self.order @@ -1244,13 +1385,11 @@ async def save(self): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.write_text( - json.dumps( - await self.toDict(if_decrypt=False), ensure_ascii=False, indent=4 - ), + _dump_toml(await self.toDict(if_decrypt=False)), encoding="utf-8", ) - async def add(self, config_type: Type[T]) -> tuple[uuid.UUID, T]: + 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 5bb4192a..d8f34b22 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -21,10 +21,62 @@ # Contact: DLmaster_361@163.com -from .ConfigBase import * -from .config import * -from .schema import * -from .emulator import * -from .task import * +from . import dto, emulator, task +from .config_base import ConfigBase, ConfigItem, MultipleConfig, dump_toml +from .pydantic_base import PydanticConfigBase +from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook +from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from .maaend import MaaEndConfig, MaaEndUserConfig +from .src import SrcConfig, SrcUserConfig +from .general import GeneralConfig, GeneralUserConfig +from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig +from .type import ( + EncryptedString, + HHMMString, + JsonDictString, + JsonListString, + KeyboardKeyString, + UrlString, + YmdHmString, + YmdHmsString, + YmdString, + decrypt_encrypted_string, +) -__all__ = ["ConfigBase", "config", "schema", "emulator", "task"] +__all__ = [ + "ConfigBase", + "MultipleConfig", + "ConfigItem", + "dump_toml", + "PydanticConfigBase", + "EmulatorConfig", + "Webhook", + "QueueItem", + "TimeSet", + "QueueConfig", + "MaaPlanConfig", + "MaaUserConfig", + "MaaConfig", + "MaaEndUserConfig", + "MaaEndConfig", + "SrcUserConfig", + "SrcConfig", + "GeneralUserConfig", + "GeneralConfig", + "ToolsConfig", + "GlobalConfig", + "CLASS_BOOK", + "JsonDictString", + "JsonListString", + "HHMMString", + "YmdHmString", + "YmdString", + "YmdHmsString", + "UrlString", + "KeyboardKeyString", + "EncryptedString", + "decrypt_encrypted_string", + "dto", + "emulator", + "task", +] diff --git a/app/models/common.py b/app/models/common.py new file mode 100644 index 00000000..84d9ecb7 --- /dev/null +++ b/app/models/common.py @@ -0,0 +1,143 @@ +from __future__ import annotations +# pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportGeneralTypeIssues=false + +import calendar +import uuid +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, Field, field_validator + +from .config_base import MultipleConfig +from .pydantic_base import PydanticConfigBase +from .type import HHMMString, JsonDictString, JsonListString, UrlString, YmdHmString + + +DAY_NAMES = tuple(calendar.day_name) +EMULATOR_TYPES = Literal["general", "mumu", "ldplayer"] +AFTER_ACCOMPLISH_OPTIONS = Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", +] +HTTP_METHOD = Literal["POST", "GET"] + + +class EmulatorConfig(PydanticConfigBase): + """模拟器配置""" + + class InfoModel(BaseModel): + Name: str = "新模拟器" + Type: EMULATOR_TYPES = "general" + Path: str = "" + BossKey: JsonListString = "[ ]" + MaxWaitTime: int = Field(default=60, ge=1, le=9999) + + Info: InfoModel = Field(default_factory=InfoModel) + + LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = { + ("Info", "Type"): ("Data", "Type"), + ("Info", "BossKey"): ("Data", "BossKey"), + ("Info", "MaxWaitTime"): ("Data", "MaxWaitTime"), + } + + +class Webhook(PydanticConfigBase): + """Webhook 配置""" + + class InfoModel(BaseModel): + Name: str = "新自定义 Webhook 通知" + Enabled: bool = True + + class DataModel(BaseModel): + Url: UrlString = "" + Template: str = "" + Headers: JsonDictString = "{ }" + Method: HTTP_METHOD = "POST" + + Info: InfoModel = Field(default_factory=InfoModel) + Data: DataModel = Field(default_factory=DataModel) + + +class QueueItem(PydanticConfigBase): + """队列项配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + ScriptId: str = "-" + + Info: InfoModel = Field(default_factory=InfoModel) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + + if (group, name) != ("Info", "ScriptId"): + return value + + if value == "-": + return "-" + if not isinstance(value, str): + return "-" + + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return "-" + + script_config = self.related_config.get("ScriptConfig") + if script_config is None or uid not in script_config: + return "-" + + return str(uid) + + +class TimeSet(PydanticConfigBase): + """时间设置配置""" + + class InfoModel(BaseModel): + Enabled: bool = True + Days: list[str] = Field(default_factory=lambda: list(DAY_NAMES)) + Time: HHMMString = "00:00" + + @field_validator("Days", mode="before") + @classmethod + def _validate_days(cls, value: Any) -> list[str]: + if not isinstance(value, list): + return [] + days: list[str] = [item for item in value if isinstance(item, str)] + return ( + days + if len(days) == len(value) and all(item in DAY_NAMES for item in days) + else [] + ) + + Info: InfoModel = Field(default_factory=InfoModel) + + +class QueueConfig(PydanticConfigBase): + """队列配置""" + + class InfoModel(BaseModel): + Name: str = "新队列" + TimeEnabled: bool = False + StartUpEnabled: bool = False + AfterAccomplish: AFTER_ACCOMPLISH_OPTIONS = "NoAction" + + class DataModel(BaseModel): + LastTimedStart: YmdHmString = "2000-01-01 00:00" + + Info: InfoModel = Field(default_factory=InfoModel) + Data: DataModel = Field(default_factory=DataModel) + + def __init__(self, **data: Any): + super().__init__(**data) + # MultipleConfig 目前约束 T 继承 ConfigBase;这里保持运行时兼容。 + self.TimeSet = MultipleConfig([TimeSet]) # pyright: ignore[reportArgumentType] + self.QueueItem = MultipleConfig([QueueItem]) # pyright: ignore[reportArgumentType] + + +__all__ = ["EmulatorConfig", "Webhook", "QueueItem", "TimeSet", "QueueConfig"] diff --git a/app/models/config/__init__.py b/app/models/config/__init__.py index 6d02c65d..42668897 100644 --- a/app/models/config/__init__.py +++ b/app/models/config/__init__.py @@ -1,9 +1,22 @@ -from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook -from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig -from .maaend import MaaEndConfig, MaaEndUserConfig -from .src import SrcConfig, SrcUserConfig -from .general import GeneralConfig, GeneralUserConfig -from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig +from ..common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook +from ..maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from ..maaend import MaaEndConfig, MaaEndUserConfig +from ..src import SrcConfig, SrcUserConfig +from ..general import GeneralConfig, GeneralUserConfig +from ..global_config import CLASS_BOOK, GlobalConfig, ToolsConfig +from ..pydantic_base import PydanticConfigBase +from ..type import ( + EncryptedString, + HHMMString, + JsonDictString, + JsonListString, + KeyboardKeyString, + UrlString, + YmdHmsString, + YmdHmString, + YmdString, + decrypt_encrypted_string, +) __all__ = [ "EmulatorConfig", @@ -23,4 +36,15 @@ "ToolsConfig", "GlobalConfig", "CLASS_BOOK", + "PydanticConfigBase", + "JsonDictString", + "JsonListString", + "HHMMString", + "YmdHmString", + "YmdString", + "YmdHmsString", + "UrlString", + "KeyboardKeyString", + "EncryptedString", + "decrypt_encrypted_string", ] diff --git a/app/models/config/common.py b/app/models/config/common.py index 1f024019..310c0a33 100644 --- a/app/models/config/common.py +++ b/app/models/config/common.py @@ -1,170 +1,3 @@ -import calendar - -from ..ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - MultipleUIDValidator, - BoolValidator, - OptionsValidator, - MultipleOptionsValidator, - RangeValidator, - DateTimeValidator, - JSONValidator, - URLValidator, - EmulatorPathValidator, -) - - -class EmulatorConfig(ConfigBase): - """模拟器配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 模拟器名称 - self.Info_Name = ConfigItem("Info", "Name", "新模拟器") - ## 模拟器类型 - self.Info_Type = ConfigItem( - "Info", - "Type", - "general", - OptionsValidator( - [ - "general", - "mumu", - "ldplayer", - # "nox", # 以下都是骗你的, 根本没有写~~ - # "memu", - # "blueStacks", - ] - ), - legacy_group="Data", - ) - ## 模拟器路径 - self.Info_Path = ConfigItem( - "Info", "Path", "", EmulatorPathValidator(self.Info_Type) - ) - ## 老板键快捷键配置 - self.Info_BossKey = ConfigItem( - "Info", "BossKey", "[ ]", JSONValidator(list), legacy_group="Data" - ) - ## 最大等待时间(秒) - self.Info_MaxWaitTime = ConfigItem( - "Info", "MaxWaitTime", 60, RangeValidator(1, 9999), legacy_group="Data" - ) - - super().__init__() - - -class Webhook(ConfigBase): - """Webhook 配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## Webhook 名称 - self.Info_Name = ConfigItem("Info", "Name", "新自定义 Webhook 通知") - ## 是否启用 - self.Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator()) - - ## Data ------------------------------------------------------------ - ## Webhook URL 地址 - self.Data_Url = ConfigItem("Data", "Url", "", URLValidator()) - ## 消息模板 - self.Data_Template = ConfigItem("Data", "Template", "") - ## 请求头 - self.Data_Headers = ConfigItem("Data", "Headers", "{ }", JSONValidator()) - ## 请求方法 - self.Data_Method = ConfigItem( - "Data", "Method", "POST", OptionsValidator(["POST", "GET"]) - ) - - super().__init__() - - -class QueueItem(ConfigBase): - """队列项配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 脚本 ID - self.Info_ScriptId = ConfigItem( - "Info", - "ScriptId", - "-", - MultipleUIDValidator("-", self.related_config, "ScriptConfig"), - ) - - super().__init__() - - -class TimeSet(ConfigBase): - """时间设置配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 是否启用 - self.Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator()) - ## 执行周期 - self.Info_Days = ConfigItem( - "Info", - "Days", - list(calendar.day_name), - MultipleOptionsValidator(list(calendar.day_name)), - ) - ## 执行时间 - self.Info_Time = ConfigItem("Info", "Time", "00:00", DateTimeValidator("%H:%M")) - - super().__init__() - - -class QueueConfig(ConfigBase): - """队列配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 队列名称 - 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", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] - ), - ) - - ## Data ------------------------------------------------------------ - ## 上次定时启动时间 - 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]) - - super().__init__() - +from ..common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook __all__ = ["EmulatorConfig", "Webhook", "QueueItem", "TimeSet", "QueueConfig"] diff --git a/app/models/config/general.py b/app/models/config/general.py index fb94dc65..2f82c895 100644 --- a/app/models/config/general.py +++ b/app/models/config/general.py @@ -1,273 +1,3 @@ -import json -from pathlib import Path -from datetime import datetime - -from app.utils.constants import UTC4 -from ..ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - MultipleUIDValidator, - BoolValidator, - OptionsValidator, - RangeValidator, - VirtualConfigValidator, - FileValidator, - DateTimeValidator, - UserNameValidator, - ArgumentValidator, - AdvancedArgumentValidator, -) -from .common import Webhook - - -class GeneralUserConfig(ConfigBase): - """通用脚本用户配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 用户名称 - 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.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") - ) - ## 代理次数 - self.Data_ProxyTimes = ConfigItem( - "Data", "ProxyTimes", 0, RangeValidator(0, 9999) - ) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getTags(self) -> str: - """生成通用用户标签列表""" - tags = [] - - # 任务代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"任务:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "任务:未代理", "color": "orange"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - -class GeneralConfig(ConfigBase): - """通用配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新通用脚本") - ## 根目录路径 - self.Info_RootPath = ConfigItem( - "Info", "RootPath", str(Path.cwd()), FileValidator() - ) - - ## Script ---------------------------------------------------------- - ## 脚本路径 - self.Script_ScriptPath = ConfigItem( - "Script", "ScriptPath", str(Path.cwd()), FileValidator() - ) - ## 脚本参数 - self.Script_Arguments = ConfigItem( - "Script", "Arguments", "", AdvancedArgumentValidator() - ) - ## 是否追踪进程 - self.Script_IfTrackProcess = ConfigItem( - "Script", "IfTrackProcess", False, BoolValidator() - ) - ## 追踪进程的名称 - self.Script_TrackProcessName = ConfigItem("Script", "TrackProcessName", "") - ## 追踪进程的文件路径 - self.Script_TrackProcessExe = ConfigItem("Script", "TrackProcessExe", "") - ## 追踪进程的启动命令行参数 - self.Script_TrackProcessCmdline = ConfigItem( - "Script", "TrackProcessCmdline", "", ArgumentValidator() - ) - 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", "") - - ## Game ------------------------------------------------------------ - ## 是否启用游戏 - self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator()) - ## 游戏类型 - self.Game_Type = ConfigItem( - "Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client", "URL"]) - ) - ## 游戏路径 - self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator()) - ## 自定义协议URL - self.Game_URL = ConfigItem("Game", "URL", "") - ## 游戏进程名称 - self.Game_ProcessName = ConfigItem("Game", "ProcessName", "") - ## 游戏启动参数 - self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator()) - ## 等待时间(秒) - self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999)) - ## 是否强制关闭 - self.Game_IfForceClose = ConfigItem( - "Game", "IfForceClose", False, BoolValidator() - ) - ## 模拟器 ID - self.Game_EmulatorId = ConfigItem( - "Game", - "EmulatorId", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-") - - ## Run ------------------------------------------------------------- - ## 代理次数限制 - 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]) - - super().__init__() - +from ..general import GeneralConfig, GeneralUserConfig __all__ = ["GeneralUserConfig", "GeneralConfig"] diff --git a/app/models/config/global_config.py b/app/models/config/global_config.py index e3a60731..5b59767b 100644 --- a/app/models/config/global_config.py +++ b/app/models/config/global_config.py @@ -1,378 +1,3 @@ -import uuid -import json -import calendar -from datetime import datetime -from typing import Callable - -from app.utils.constants import UTC8, MATERIALS_MAP, RESOURCE_STAGE_INFO -from ..ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - BoolValidator, - OptionsValidator, - VirtualConfigValidator, - EncryptValidator, - UUIDValidator, - DateTimeValidator, - JSONValidator, - URLValidator, - KeyValidator, -) -from ..schema import TagItem -from .common import EmulatorConfig, QueueConfig, QueueItem, Webhook -from .general import GeneralConfig -from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig -from .maaend import MaaEndConfig -from .src import SrcConfig - - -class ToolsConfig(ConfigBase): - """工具配置""" - - def __init__(self) -> None: - self.ArknightsPC_Enabled = ConfigItem( - "ArknightsPC", "Enabled", False, BoolValidator() - ) - self.ArknightsPC_PauseKey = ConfigItem( - "ArknightsPC", "PauseKey", "f10", KeyValidator("f10") - ) - self.ArknightsPC_SelectDeployedKey = ConfigItem( - "ArknightsPC", "SelectDeployedKey", "w", KeyValidator("w") - ) - self.ArknightsPC_UseSkillKey = ConfigItem( - "ArknightsPC", "UseSkillKey", "r", KeyValidator("r") - ) - self.ArknightsPC_RetreatKey = ConfigItem( - "ArknightsPC", "RetreatKey", "t", KeyValidator("t") - ) - self.ArknightsPC_NextFrameKey = ConfigItem( - "ArknightsPC", "NextFrameKey", "f", KeyValidator("f") - ) - self.ArknightsPC_AnotherQuitKey = ConfigItem( - "ArknightsPC", "AnotherQuitKey", "space", KeyValidator("space") - ) - self.ArknightsPC_Status = ConfigItem( - "ArknightsPC", - "Status", - "-", - VirtualConfigValidator(self.arknights_pc_status), - ) - - self.arknights_pc_running = False - self.arknights_pc_get_connected: Callable[[], bool] = lambda: False - - super().__init__() - - @property - def arknights_pc_connected(self) -> bool: - return self.arknights_pc_get_connected() - - def arknights_pc_status(self) -> str: - if not self.get("ArknightsPC", "Enabled"): - return TagItem(text="未启用", color="gray").model_dump_json() - else: - if self.arknights_pc_running: - if self.arknights_pc_connected: - return TagItem(text="运行中", color="green").model_dump_json() - else: - return TagItem(text="未连接", color="red").model_dump_json() - else: - return TagItem(text="已暂停", color="yellow").model_dump_json() - - @property - def arknights_pc_keys(self) -> list[str]: - """获取明日方舟 PC 按键配置""" - - return [ - self.get("ArknightsPC", _) - for _ in ( - "SelectDeployedKey", - "UseSkillKey", - "RetreatKey", - "NextFrameKey", - "AnotherQuitKey", - ) - ] - - -class GlobalConfig(ConfigBase): - """全局配置""" - - def __init__(self): - ## Function --------------------------------------------------------- - ## 历史记录保留时间(天) - self.Function_HistoryRetentionTime = ConfigItem( - "Function", - "HistoryRetentionTime", - 0, - OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]), - ) - ## 是否允许睡眠 - self.Function_IfAllowSleep = ConfigItem( - "Function", "IfAllowSleep", False, BoolValidator() - ) - ## 是否启用静默模式 - self.Function_IfSilence = ConfigItem( - "Function", "IfSilence", False, BoolValidator() - ) - ## 是否同意 Bilibili 协议 - self.Function_IfAgreeBilibili = ConfigItem( - "Function", "IfAgreeBilibili", False, BoolValidator() - ) - ## 是否屏蔽模拟器广告 - self.Function_IfBlockAd = ConfigItem( - "Function", "IfBlockAd", False, BoolValidator() - ) - - ## Voice ------------------------------------------------------------ - ## 是否启用语音 - self.Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator()) - ## 语音类型 - self.Voice_Type = ConfigItem( - "Voice", "Type", "simple", OptionsValidator(["simple", "noisy"]) - ) - - ## Start ------------------------------------------------------------ - ## 是否自动启动 - self.Start_IfSelfStart = ConfigItem( - "Start", "IfSelfStart", False, BoolValidator() - ) - ## 是否启动时直接最小化 - self.Start_IfMinimizeDirectly = ConfigItem( - "Start", "IfMinimizeDirectly", False, BoolValidator() - ) - - ## UI --------------------------------------------------------------- - ## 是否显示托盘图标 - self.UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator()) - ## 是否关闭到托盘 - self.UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator()) - - ## Notify ----------------------------------------------------------- - ## 任务结果推送时间 - self.Notify_SendTaskResultTime = ConfigItem( - "Notify", - "SendTaskResultTime", - "不推送", - OptionsValidator(["不推送", "任何时刻", "仅失败时"]), - ) - ## 是否发送统计信息 - self.Notify_IfSendStatistic = ConfigItem( - "Notify", "IfSendStatistic", False, BoolValidator() - ) - ## 是否发送六星通知 - self.Notify_IfSendSixStar = ConfigItem( - "Notify", "IfSendSixStar", False, BoolValidator() - ) - ## 是否推送系统通知 - self.Notify_IfPushPlyer = ConfigItem( - "Notify", "IfPushPlyer", False, BoolValidator() - ) - ## 是否发送邮件 - self.Notify_IfSendMail = ConfigItem( - "Notify", "IfSendMail", False, BoolValidator() - ) - ## 是否发送Koishi通知 - self.Notify_IfKoishiSupport = ConfigItem( - "Notify", "IfKoishiSupport", False, BoolValidator() - ) - ## Koishi WebSocket 服务器地址 - self.Notify_KoishiServerAddress = ConfigItem( - "Notify", - "KoishiServerAddress", - "ws://localhost:5140/AUTO_MAS", - URLValidator(), - ) - ## Koishi Token - self.Notify_KoishiToken = ConfigItem("Notify", "KoishiToken", "") - ## SMTP 服务器地址 - self.Notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "") - ## 邮箱授权码 - self.Notify_AuthorizationCode = ConfigItem( - "Notify", "AuthorizationCode", "", EncryptValidator() - ) - ## 发件地址 - self.Notify_FromAddress = ConfigItem("Notify", "FromAddress", "") - ## 收件地址 - self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - ## Update ----------------------------------------------------------- - ## 是否自动更新 - self.Update_IfAutoUpdate = ConfigItem( - "Update", "IfAutoUpdate", False, BoolValidator() - ) - ## 更新源 - self.Update_Source = ConfigItem( - "Update", - "Source", - "GitHub", - OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]), - ) - ## 更新频道 - self.Update_Channel = ConfigItem( - "Update", "Channel", "stable", OptionsValidator(["stable", "beta"]) - ) - ## 代理地址 - self.Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "") - ## 镜像站 CDK - self.Update_MirrorChyanCDK = ConfigItem( - "Update", "MirrorChyanCDK", "", EncryptValidator() - ) - - ## Data ------------------------------------------------------------- - ## 唯一标识符 - self.Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) - ## 上次统计上传时间 - self.Data_LastStatisticsUpload = ConfigItem( - "Data", - "LastStatisticsUpload", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## 上次关卡更新时间 - self.Data_LastStageUpdated = ConfigItem( - "Data", - "LastStageUpdated", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## 关卡数据的版本标识符 - self.Data_StageETag = ConfigItem("Data", "StageETag", "") - ## 关卡信息数据 - self.Data_StageData = ConfigItem( - "Data", "StageData", "{ }", JSONValidator(), legacy_name="Stage" - ) - ## 关卡信息 - self.Data_Stage = ConfigItem( - "Data", "Stage", "-", VirtualConfigValidator(self.getStage) - ) - ## 上次公告更新时间 - self.Data_LastNoticeUpdated = ConfigItem( - "Data", - "LastNoticeUpdated", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## 公告的版本标识符 - self.Data_NoticeETag = ConfigItem("Data", "NoticeETag", "") - ## 是否显示公告 - self.Data_IfShowNotice = ConfigItem( - "Data", "IfShowNotice", True, BoolValidator() - ) - ## 公告内容 - self.Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator()) - ## 上次 Web 配置更新时间 - self.Data_LastWebConfigUpdated = ConfigItem( - "Data", - "LastWebConfigUpdated", - "2000-01-01 00:00:00", - DateTimeValidator("%Y-%m-%d %H:%M:%S"), - ) - ## Web 配置 - self.Data_WebConfig = ConfigItem( - "Data", "WebConfig", "[ ]", JSONValidator(list) - ) - super().__init__() - - ## 模拟器配置列表 - self.EmulatorConfig = MultipleConfig([EmulatorConfig]) - ## 计划表配置列表 - self.PlanConfig = MultipleConfig([MaaPlanConfig]) - ## 脚本配置列表 - self.ScriptConfig = MultipleConfig( - [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] - ) - ## 队列配置列表 - self.QueueConfig = MultipleConfig([QueueConfig]) - ## 工具箱配置 - self.ToolsConfig = ToolsConfig() - - MaaConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - MaaEndConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - SrcConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - GeneralConfig.related_config["EmulatorConfig"] = self.EmulatorConfig - MaaUserConfig.related_config["PlanConfig"] = self.PlanConfig - QueueItem.related_config["ScriptConfig"] = self.ScriptConfig - - def getStage(self) -> str: - """获取关卡信息""" - - try: - raw_stage_data = json.loads(self.get("Data", "StageData")) - - activity_stage_drop_info = [] - activity_stage_combox = [] - - for side_story in raw_stage_data.values(): - if ( - datetime.strptime( - side_story["Activity"]["UtcStartTime"], "%Y/%m/%d %H:%M:%S" - ).replace(tzinfo=UTC8) - < datetime.now(tz=UTC8) - < datetime.strptime( - side_story["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S" - ).replace(tzinfo=UTC8) - ): - for stage in side_story["Stages"]: - activity_stage_combox.append( - {"label": stage["Display"], "value": stage["Value"]} - ) - - if "SSReopen" not in stage["Display"]: - if stage["Drop"] in MATERIALS_MAP: - drop_id = stage["Drop"] - elif "玉" in stage["Drop"]: - drop_id = "30012" - else: - drop_id = "NotFound" - - activity_stage_drop_info.append( - { - "Display": stage["Display"], - "Value": stage["Value"], - "Drop": drop_id, - "DropName": MATERIALS_MAP.get( - stage["Drop"], stage["Drop"] - ), - "Activity": side_story["Activity"], - } - ) - except Exception: - return "{ }" - - stage_data = {"Info": activity_stage_drop_info} - - for day in range(0, 8): - res_stage = [] - - for stage in RESOURCE_STAGE_INFO: - if day in stage["days"] or day == 0: - res_stage.append({"label": stage["text"], "value": stage["value"]}) - - stage_data[calendar.day_name[day - 1] if day > 0 else "ALL"] = ( - res_stage[0:1] + activity_stage_combox + res_stage[1:] - ) - - return json.dumps(stage_data, ensure_ascii=False) - - -CLASS_BOOK = { - "MAA": MaaConfig, - "MaaPlan": MaaPlanConfig, - "SRC": SrcConfig, - "MaaEnd": MaaEndConfig, - "General": GeneralConfig, -} -"""配置类映射表""" - +from ..global_config import CLASS_BOOK, GlobalConfig, ToolsConfig __all__ = ["ToolsConfig", "GlobalConfig", "CLASS_BOOK"] diff --git a/app/models/config/maa.py b/app/models/config/maa.py index c33649f7..f240210b 100644 --- a/app/models/config/maa.py +++ b/app/models/config/maa.py @@ -1,494 +1,3 @@ -import uuid -import json -import calendar -from pathlib import Path -from datetime import datetime - -from app.utils.constants import UTC4, UTC8, RESOURCE_STAGE_INFO, MAA_STAGE_KEY -from ..ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - MultipleUIDValidator, - BoolValidator, - OptionsValidator, - RangeValidator, - VirtualConfigValidator, - FolderValidator, - EncryptValidator, - DateTimeValidator, - JSONValidator, - UserNameValidator, -) -from .common import Webhook - - -class MaaUserConfig(ConfigBase): - """MAA用户配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 用户名称 - self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) - ## 用户 ID - self.Info_Id = ConfigItem("Info", "Id", "") - ## 密码 - self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) - ## 脚本模式 - 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_InfrastMode = ConfigItem( - "Info", - "InfrastMode", - "Normal", - OptionsValidator(["Normal", "Rotation", "Custom"]), - ) - ## 基建配置名称 - self.Info_InfrastName = ConfigItem( - "Info", "InfrastName", "-", VirtualConfigValidator(self.getInfrastName) - ) - ## 基建配置索引 - self.Info_InfrastIndex = ConfigItem( - "Info", "InfrastIndex", "-", VirtualConfigValidator(self.getInfrastIndex) - ) - ## 备注 - 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", "-") - ## 关卡 1 - self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-") - ## 关卡 2 - self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-") - ## 关卡 3 - self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-") - ## 备用关卡 - self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-") - ## 是否启用森空岛签到 - self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) - ## 森空岛 Token - self.Info_SklandToken = ConfigItem( - "Info", "SklandToken", "", EncryptValidator() - ) - ## 用户标签信息(虚拟字段,供前端显示) - self.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "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_CustomInfrast = ConfigItem( - "Data", "CustomInfrast", "{ }", JSONValidator() - ) - ## 基建配置索引数据 - self.Data_InfrastIndex = ConfigItem( - "Data", "InfrastIndex", "0", legacy_group="Info" - ) - - ## Task ------------------------------------------------------------ - ## 是否自动唤醒 - self.Task_IfStartUp = ConfigItem("Task", "IfStartUp", True, BoolValidator()) - ## 是否理智作战 - self.Task_IfFight = ConfigItem("Task", "IfFight", True, BoolValidator()) - ## 是否基建换班 - self.Task_IfInfrast = ConfigItem("Task", "IfInfrast", True, BoolValidator()) - ## 是否公开招募 - self.Task_IfRecruit = ConfigItem("Task", "IfRecruit", True, BoolValidator()) - ## 是否信用收支 - self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator()) - ## 是否领取奖励 - self.Task_IfAward = ConfigItem("Task", "IfAward", True, BoolValidator()) - ## 是否自动肉鸽 - self.Task_IfRoguelike = ConfigItem( - "Task", "IfRoguelike", False, BoolValidator() - ) - ## 是否生息演算 - self.Task_IfReclamation = ConfigItem( - "Task", "IfReclamation", False, BoolValidator() - ) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getInfrastName(self) -> str: - if self.get("Info", "InfrastMode") != "Custom": - return "未使用自定义基建模式" - - infrast_data = json.loads(self.get("Data", "CustomInfrast")) - if ( - infrast_data.get("title", "文件标题") != "文件标题" - and infrast_data.get("description", "文件描述") != "文件描述" - ): - return f"{infrast_data['title']} - {infrast_data['description']}" - elif infrast_data.get("title", "文件标题") != "文件标题": - return str(infrast_data["title"]) - elif infrast_data.get("id", None): - return str(infrast_data["id"]) - else: - return "未命名自定义基建" - - def getInfrastIndex(self) -> str: - if self.get("Info", "InfrastMode") != "Custom": - return "-1" - - infrast_data = json.loads(self.get("Data", "CustomInfrast")) - - if len(infrast_data.get("plans", [])) == 0: - return "-1" - - for i, plan in enumerate(infrast_data.get("plans", [])): - for t in plan.get("period", []): - if ( - datetime.strptime(t[0], "%H:%M").time() - <= datetime.now().time() - <= datetime.strptime(t[1], "%H:%M").time() - ): - return str(i) - - else: - return self.get("Data", "InfrastIndex") or "0" - - def getTags(self) -> str: - """生成用户标签列表,返回JSON字符串格式的TagItem列表""" - tags = [] - - # 人工排查状态标签 - if not self.get("Data", "IfPassCheck"): - tags.append({"text": "人工排查未通过", "color": "red"}) - - # 日常代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "日常:未代理", "color": "orange"}) - - # 森空岛签到标签(使用东8区时间) - if self.get("Info", "IfSkland"): - if ( - datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC8).date() - ): - tags.append({"text": "森空岛:已签到", "color": "green"}) - else: - tags.append({"text": "森空岛:未签到", "color": "orange"}) - else: - tags.append({"text": "森空岛:禁用", "color": "red"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 基建模式标签 - infrast_mode = self.get("Info", "InfrastMode") - if self.get("Task", "IfInfrast"): - if infrast_mode == "Normal": - infrast_text = "基建:常规" - elif infrast_mode == "Rotation": - infrast_text = "基建:轮换" - elif infrast_mode == "Custom": - infrast_text = f"基建:{self.getInfrastName() if len(self.getInfrastName()) < 10 else self.getInfrastName()[:10] + '...'}" - else: - infrast_text = "基建:开启" - tags.append({"text": infrast_text, "color": "purple"}) - else: - tags.append({"text": "基建:关闭", "color": "red"}) - - # 关卡信息标签 - plan_data = { - stage_key: self.get_stage_zh(self.get("Info", stage_key)) - for stage_key in MAA_STAGE_KEY[2:] - } - tag_color = "blue" - if self.get("Info", "StageMode") != "Fixed": - plan = self.related_config["PlanConfig"][ - uuid.UUID(self.get("Info", "StageMode")) - ] - if isinstance(plan, MaaPlanConfig): - plan_data = { - stage_key: self.get_stage_zh( - plan.get_current_info(stage_key).getValue() - ) - for stage_key in MAA_STAGE_KEY[2:] - } - tag_color = "green" - # 主关卡 - tags.append({"text": f"主关卡:{plan_data['Stage']}", "color": tag_color}) - # 备选关卡(合并显示) - backup_stages = [ - plan_data[f"Stage_{i}"] - for i in range(1, 4) - if plan_data[f"Stage_{i}"] != "禁用" - ] - if backup_stages: - tags.append( - {"text": f"备选:{', '.join(backup_stages)}", "color": tag_color} - ) - # 剩余关卡 - if plan_data["Stage_Remain"] != "禁用": - tags.append( - {"text": f"剩余:{plan_data['Stage_Remain']}", "color": tag_color} - ) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - @staticmethod - def get_stage_zh(stage: str) -> str: - for stage_info in RESOURCE_STAGE_INFO: - if stage_info.get("value") == stage: - return ( - stage_info.get("text", stage) - .replace("经验-6/5", "经验") - .replace("龙门币-6/5", "龙门币") - .replace("红票-5", "红票") - .replace("技能-5", "技能") - .replace("碳-5", "碳") - ) - else: - return stage - - -class MaaConfig(ConfigBase): - """MAA配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## MAA 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本") - ## MAA 路径 - self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) - - ## Emulator -------------------------------------------------------- - ## 模拟器 ID - self.Emulator_Id = ConfigItem( - "Emulator", - "Id", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Emulator_Index = ConfigItem("Emulator", "Index", "-") - - ## Run ------------------------------------------------------------- - ## 任务切换方式 - self.Run_TaskTransitionMethod = ConfigItem( - "Run", - "TaskTransitionMethod", - "ExitEmulator", - OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]), - ) - ## 代理次数限制 - self.Run_ProxyTimesLimit = ConfigItem( - "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) - ) - ## 运行次数限制 - 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_AnnihilationAvoidWaste = ConfigItem( - "Run", "AnnihilationAvoidWaste", False, BoolValidator() - ) - - self.UserData = MultipleConfig([MaaUserConfig]) - - super().__init__() - - -class MaaPlanConfig(ConfigBase): - """MAA计划表配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 计划表名称 - 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", *calendar.day_name]: - 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"]), - ) - - ## 理智关卡 - for name in MAA_STAGE_KEY[2:]: - # Stage、Stage_1、Stage_2、Stage_3、Stage_Remain - self.config_item_dict[group][name] = ConfigItem(group, name, "-") - - for name in MAA_STAGE_KEY: - setattr(self, f"{group}_{name}", self.config_item_dict[group][name]) - - super().__init__() - - 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": - today = datetime.now(tz=UTC4).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("非法的计划表模式") - +from ..maa import MaaConfig, MaaPlanConfig, MaaUserConfig __all__ = ["MaaPlanConfig", "MaaUserConfig", "MaaConfig"] diff --git a/app/models/config/maaend.py b/app/models/config/maaend.py index 344c62b3..bb8f56ac 100644 --- a/app/models/config/maaend.py +++ b/app/models/config/maaend.py @@ -1,310 +1,3 @@ -import json -from pathlib import Path -from datetime import datetime - -from app.utils.constants import UTC4, UTC8, MAAEND_STAGE_BOOK, MAAEND_STAGE_WITH_AB -from ..ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - MultipleUIDValidator, - BoolValidator, - OptionsValidator, - RangeValidator, - VirtualConfigValidator, - FileValidator, - FolderValidator, - EncryptValidator, - DateTimeValidator, - UserNameValidator, - ArgumentValidator, -) -from .common import Webhook - - -class MaaEndUserConfig(ConfigBase): - """MaaEnd用户配置""" - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 用户名称 - self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) - ## 是否启用 - self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) - ## 用户ID - self.Info_Id = ConfigItem("Info", "Id", "") - ## 密码 - self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) - ## 配置模式 - self.Info_Mode = ConfigItem( - "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) - ) - ## 资源名称 - self.Info_Resource = ConfigItem( - "Info", "Resource", "官服", OptionsValidator(["官服"]) - ) - ## 剩余天数 - self.Info_RemainedDay = ConfigItem( - "Info", "RemainedDay", -1, RangeValidator(-1, 9999) - ) - ## 备注 - self.Info_Notes = ConfigItem("Info", "Notes", "无") - ## 是否启用森空岛签到 - self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) - ## 森空岛 Token - self.Info_SklandToken = ConfigItem( - "Info", "SklandToken", "", EncryptValidator() - ) - ## 用户标签信息 - self.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## Task ------------------------------------------------------------ - ## 协议空间选项 - self.Task_ProtocolSpaceTab = ConfigItem( - "Task", - "ProtocolSpaceTab", - "OperatorProgression", - OptionsValidator( - ["OperatorProgression", "WeaponProgression", "CrisisDrills"] - ), - ) - self.Task_OperatorProgression = ConfigItem( - "Task", - "OperatorProgression", - "OperatorEXP", - OptionsValidator(["OperatorEXP", "Promotions", "T-Creds", "SkillUp"]), - ) - self.Task_WeaponProgression = ConfigItem( - "Task", - "WeaponProgression", - "WeaponEXP", - OptionsValidator(["WeaponEXP", "WeaponTune"]), - ) - self.Task_CrisisDrills = ConfigItem( - "Task", - "CrisisDrills", - "AdvancedProgression1", - OptionsValidator( - [ - "AdvancedProgression1", - "AdvancedProgression2", - "AdvancedProgression3", - "AdvancedProgression4", - "AdvancedProgression5", - ] - ), - ) - self.Task_RewardsSetOption = ConfigItem( - "Task", - "RewardsSetOption", - "RewardsSetA", - OptionsValidator(["RewardsSetA", "RewardsSetB"]), - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") - ) - ## 代理次数 - self.Data_ProxyTimes = ConfigItem( - "Data", "ProxyTimes", 0, RangeValidator(0, 9999) - ) - ## 上次代理状态 - self.Data_LastProxyStatus = ConfigItem( - "Data", - "LastProxyStatus", - "未知", - OptionsValidator(["未知", "成功", "失败"]), - ) - ## 上次森空岛签到日期 - self.Data_LastSklandDate = ConfigItem( - "Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d") - ) - ## 是否通过检查 - self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator()) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getTags(self) -> str: - """生成用户标签列表,返回JSON字符串格式的TagItem列表""" - tags = [] - # 人工排查状态标签 - if not self.get("Data", "IfPassCheck"): - tags.append({"text": "人工排查未通过", "color": "red"}) - - # 上次代理标签 - tags.append( - { - "text": f"上次:{self.get('Data', 'LastProxyStatus')}", - "color": ( - "red" if self.get("Data", "LastProxyStatus") == "失败" else "green" - ), - } - ) - - # 日常代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "日常:未代理", "color": "orange"}) - - # 森空岛签到标签(使用东8区时间) - if self.get("Info", "IfSkland"): - if ( - datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC8).date() - ): - tags.append({"text": "森空岛:已签到", "color": "green"}) - else: - tags.append({"text": "森空岛:未签到", "color": "orange"}) - else: - tags.append({"text": "森空岛:禁用", "color": "red"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 关卡信息标签 - stage = self.get("Task", self.get("Task", "ProtocolSpaceTab")) - stage_ab = ( - f" - {self.get('Task', 'RewardsSetOption')[-1]}" - if stage in MAAEND_STAGE_WITH_AB - else "" - ) - tags.append({"text": MAAEND_STAGE_BOOK[stage] + stage_ab, "color": "blue"}) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - -class MaaEndConfig(ConfigBase): - """MaaEnd配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## MaaEnd 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新 MaaEnd 脚本") - ## MaaEnd 路径 - self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) - - ## Run ------------------------------------------------------------- - ## 运行超时阈值 - self.Run_RunTimeLimit = ConfigItem( - "Run", "RunTimeLimit", 10, RangeValidator(1, 9999) - ) - ## 每日代理次数限制 - self.Run_ProxyTimesLimit = ConfigItem( - "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) - ) - ## 运行次数限制 - self.Run_RunTimesLimit = ConfigItem( - "Run", "RunTimesLimit", 3, RangeValidator(1, 9999) - ) - - ## Game ------------------------------------------------------------ - ## 控制器类型 - self.Game_ControllerType = ConfigItem( - "Game", - "ControllerType", - "Win32-Window", - OptionsValidator( - [ - "Win32-Window", - "Win32-Front", - "Win32-Window-Background", - "ADB", - ] - ), - ) - ## 终末地游戏路径 - self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator()) - ## 终末地游戏启动参数 - self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator()) - ## 等待时间(秒) - self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999)) - ## 模拟器 ID - self.Game_EmulatorId = ConfigItem( - "Game", - "EmulatorId", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-") - ## 结束后是否关闭游戏 - self.Game_CloseOnFinish = ConfigItem( - "Game", "CloseOnFinish", True, BoolValidator() - ) - - self.UserData = MultipleConfig([MaaEndUserConfig]) - - super().__init__() - +from ..maaend import MaaEndConfig, MaaEndUserConfig __all__ = ["MaaEndUserConfig", "MaaEndConfig"] diff --git a/app/models/config/src.py b/app/models/config/src.py index 0dd728ed..30fefe73 100644 --- a/app/models/config/src.py +++ b/app/models/config/src.py @@ -1,410 +1,3 @@ -import json -from pathlib import Path -from datetime import datetime - -from app.utils.constants import UTC4, STARRAIL_STAGE_BOOK -from ..ConfigBase import ( - ConfigBase, - MultipleConfig, - ConfigItem, - MultipleUIDValidator, - BoolValidator, - OptionsValidator, - RangeValidator, - VirtualConfigValidator, - FolderValidator, - EncryptValidator, - DateTimeValidator, - UserNameValidator, -) -from .common import Webhook - - -class SrcUserConfig(ConfigBase): - """SRC用户配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## 用户名称 - self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) - ## 是否启用 - self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) - ## 用户 ID - self.Info_Id = ConfigItem("Info", "Id", "") - ## 密码 - self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) - ## 脚本模式 - self.Info_Mode = ConfigItem( - "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) - ) - ## 游戏服务器 - self.Info_Server = ConfigItem( - "Info", - "Server", - "CN-Official", - OptionsValidator( - [ - "CN-Official", - "CN-Bilibili", - "VN-Official", - "OVERSEA-America", - "OVERSEA-Asia", - "OVERSEA-Europe", - "OVERSEA-TWHKMO", - ] - ), - ) - ## 剩余天数 - self.Info_RemainedDay = ConfigItem( - "Info", "RemainedDay", -1, RangeValidator(-1, 9999) - ) - ## 备注 - self.Info_Notes = ConfigItem("Info", "Notes", "无") - ## 用户标签信息 - self.Info_Tag = ConfigItem( - "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) - ) - - ## 关卡配置---------------------------------------------------------- - ## 关卡通道 - self.Stage_Channel = ConfigItem( - "Stage", - "Channel", - "Relic", - OptionsValidator(["Relic", "Materials", "Ornament"]), - ) - ## 遗器关卡 - self.Stage_Relic = ConfigItem( - "Stage", - "Relic", - "-", - OptionsValidator( - [ - "-", - "Cavern_of_Corrosion_Path_of_Possession", - "Cavern_of_Corrosion_Path_of_Hidden_Salvation", - "Cavern_of_Corrosion_Path_of_Thundersurge", - "Cavern_of_Corrosion_Path_of_Aria", - "Cavern_of_Corrosion_Path_of_Uncertainty", - "Cavern_of_Corrosion_Path_of_Cavalier", - "Cavern_of_Corrosion_Path_of_Dreamdive" - "Cavern_of_Corrosion_Path_of_Darkness", - "Cavern_of_Corrosion_Path_of_Elixir_Seekers", - "Cavern_of_Corrosion_Path_of_Conflagration", - "Cavern_of_Corrosion_Path_of_Holy_Hymn", - "Cavern_of_Corrosion_Path_of_Providence", - "Cavern_of_Corrosion_Path_of_Drifting", - "Cavern_of_Corrosion_Path_of_Jabbing_Punch", - "Cavern_of_Corrosion_Path_of_Gelid_Wind", - ] - ), - ) - ## 材料关卡 - self.Stage_Materials = ConfigItem( - "Stage", - "Materials", - "-", - OptionsValidator( - [ - "-", - "Calyx_Golden_Memories_Planarcadia", - "Calyx_Golden_Aether_Planarcadia", - "Calyx_Golden_Treasures_Planarcadia", - "Calyx_Golden_Memories_Amphoreus", - "Calyx_Golden_Aether_Amphoreus", - "Calyx_Golden_Treasures_Amphoreus", - "Calyx_Golden_Memories_Penacony", - "Calyx_Golden_Aether_Penacony", - "Calyx_Golden_Treasures_Penacony", - "Calyx_Golden_Memories_The_Xianzhou_Luofu", - "Calyx_Golden_Aether_The_Xianzhou_Luofu", - "Calyx_Golden_Treasures_The_Xianzhou_Luofu", - "Calyx_Golden_Memories_Jarilo_VI", - "Calyx_Golden_Aether_Jarilo_VI", - "Calyx_Golden_Treasures_Jarilo_VI", - "Calyx_Crimson_Destruction_Herta_StorageZone", - "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", - "Calyx_Crimson_Preservation_Herta_SupplyZone", - "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", - "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", - "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", - "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", - "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", - "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", - "Calyx_Crimson_Erudition_Jarilo_RivetTown", - "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", - "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", - "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", - "Calyx_Crimson_Nihility_Jarilo_GreatMine", - "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", - "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", - "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", - "Stagnant_Shadow_Quanta", - "Stagnant_Shadow_Gust", - "Stagnant_Shadow_Fulmination", - "Stagnant_Shadow_Blaze", - "Stagnant_Shadow_Spike", - "Stagnant_Shadow_Rime", - "Stagnant_Shadow_Mirage", - "Stagnant_Shadow_Icicle", - "Stagnant_Shadow_Doom", - "Stagnant_Shadow_Puppetry", - "Stagnant_Shadow_Abomination", - "Stagnant_Shadow_Scorch", - "Stagnant_Shadow_Celestial", - "Stagnant_Shadow_Perdition", - "Stagnant_Shadow_Nectar", - "Stagnant_Shadow_Roast", - "Stagnant_Shadow_Ire", - "Stagnant_Shadow_Duty", - "Stagnant_Shadow_Timbre", - "Stagnant_Shadow_Mechwolf", - "Stagnant_Shadow_Gloam", - "Stagnant_Shadow_Sloggyre", - "Stagnant_Shadow_Gelidmoon", - "Stagnant_Shadow_Deepsheaf", - "Stagnant_Shadow_Cinders", - "Stagnant_Shadow_Sirens", - "Stagnant_Shadow_Ashes", - "Stagnant_Shadow_Soundburst", - ] - ), - ) - ## 饰品关卡 - self.Stage_Ornament = ConfigItem( - "Stage", - "Ornament", - "-", - OptionsValidator( - [ - "-", - "Divergent_Universe_Within_the_West_Wind", - "Divergent_Universe_Moonlit_Blood", - "Divergent_Universe_Unceasing_Strife", - "Divergent_Universe_Famished_Worker", - "Divergent_Universe_Eternal_Comedy", - "Divergent_Universe_To_Sweet_Dreams", - "Divergent_Universe_Pouring_Blades", - "Divergent_Universe_Fruit_of_Evil", - "Divergent_Universe_Permafrost", - "Divergent_Universe_Gentle_Words", - "Divergent_Universe_Smelted_Heart", - "Divergent_Universe_Untoppled_Walls", - ] - ), - ) - ## 使用储备开拓力 - self.Stage_ExtractReservedTrailblazePower = ConfigItem( - "Stage", "ExtractReservedTrailblazePower", False, BoolValidator() - ) - ## 使用燃料 - self.Stage_UseFuel = ConfigItem("Stage", "UseFuel", False, BoolValidator()) - ## 保留的燃料数量 - self.Stage_FuelReserve = ConfigItem( - "Stage", "FuelReserve", 5, RangeValidator(0, 9999) - ) - ## 历战余响关卡 - self.Stage_EchoOfWar = ConfigItem( - "Stage", - "EchoOfWar", - "-", - OptionsValidator( - [ - "-", - "Echo_of_War_Rusted_Crypt_of_the_Iron_Carcass", - "Echo_of_War_Glance_of_Twilight", - "Echo_of_War_Inner_Beast_Battlefield", - "Echo_of_War_Salutations_of_Ashen_Dreams", - "Echo_of_War_Borehole_Planet_Past_Nightmares", - "Echo_of_War_Divine_Seed", - "Echo_of_War_End_of_the_Eternal_Freeze", - "Echo_of_War_Destruction_Beginning", - ] - ), - ) - ## 模拟宇宙关卡 - self.Stage_SimulatedUniverseWorld = ConfigItem( - "Stage", - "SimulatedUniverseWorld", - "-", - OptionsValidator( - [ - "-", - "Simulated_Universe_World_3", - "Simulated_Universe_World_4", - "Simulated_Universe_World_5", - "Simulated_Universe_World_6", - "Simulated_Universe_World_8", - ] - ), - ) - - ## Data ------------------------------------------------------------ - ## 上次代理日期 - self.Data_LastProxyDate = ConfigItem( - "Data", "LastProxyDate", "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()) - - ## Notify ---------------------------------------------------------- - ## 是否启用通知 - 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", "") - ## 是否启用 Server 酱 - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - ## Server 酱密钥 - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - ## 自定义 Webhook 列表 - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - - super().__init__() - - def getTags(self) -> str: - """生成用户标签列表,返回JSON字符串格式的TagItem列表""" - tags = [] - - # 人工排查状态标签 - if not self.get("Data", "IfPassCheck"): - tags.append({"text": "人工排查未通过", "color": "red"}) - - # 日常代理标签(使用东4区时间) - if ( - datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() - == datetime.now(tz=UTC4).date() - ): - tags.append( - { - "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", - "color": "green", - } - ) - else: - tags.append({"text": "日常:未代理", "color": "orange"}) - - # 剩余天数标签 - remained_day = self.get("Info", "RemainedDay") - if remained_day == -1: - tag_color = "gold" - elif remained_day == 0: - tag_color = "red" - elif remained_day <= 3: - tag_color = "orange" - elif remained_day <= 7: - tag_color = "yellow" - elif remained_day <= 30: - tag_color = "blue" - else: - tag_color = "green" - tags.append( - { - "text": ( - f"剩余天数:{remained_day}天" - if remained_day >= 0 - else "剩余天数:无期限" - ), - "color": tag_color, - } - ) - - # 关卡信息标签 - tags.append( - { - "text": f"关卡:{STARRAIL_STAGE_BOOK.get(self.get('Stage', self.get('Stage', 'Channel')), '未知关卡')}", - "color": "blue", - } - ) - tags.append( - { - "text": f"周本:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'EchoOfWar'), '未知关卡')}", - "color": "blue", - } - ) - tags.append( - { - "text": f"模拟宇宙:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'SimulatedUniverseWorld'), '未知关卡')}", - "color": "blue", - } - ) - - # 备注标签 - notes = self.get("Info", "Notes") - tags.append( - { - "text": ( - f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}..." - ), - "color": "pink", - } - ) - - return json.dumps(tags, ensure_ascii=False) - - -class SrcConfig(ConfigBase): - """SRC配置""" - - related_config: dict[str, MultipleConfig] = {} - - def __init__(self) -> None: - ## Info ------------------------------------------------------------ - ## SRC 脚本名称 - self.Info_Name = ConfigItem("Info", "Name", "新 SRC 脚本") - ## SRC 路径 - self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator()) - - ## Emulator -------------------------------------------------------- - ## 模拟器 ID - self.Emulator_Id = ConfigItem( - "Emulator", - "Id", - "-", - MultipleUIDValidator("-", self.related_config, "EmulatorConfig"), - ) - ## 模拟器索引 - self.Emulator_Index = ConfigItem("Emulator", "Index", "-") - - ## Run ------------------------------------------------------------- - ## 任务切换方式 - self.Run_TaskTransitionMethod = ConfigItem( - "Run", - "TaskTransitionMethod", - "ExitGame", - OptionsValidator(["ExitGame", "ExitEmulator"]), - ) - ## 代理次数限制 - 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([SrcUserConfig]) - - super().__init__() - +from ..src import SrcConfig, SrcUserConfig __all__ = ["SrcUserConfig", "SrcConfig"] diff --git a/app/models/config_base.py b/app/models/config_base.py new file mode 100644 index 00000000..f5d8247f --- /dev/null +++ b/app/models/config_base.py @@ -0,0 +1,1510 @@ +# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software +# Copyright © 2024-2025 DLmaster361 +# Copyright © 2025 MoeSnowyFox +# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with AUTO-MAS. If not, see . + +# Contact: DLmaster_361@163.com + + +from __future__ import annotations +# pyright: reportMissingParameterType=false, reportUnknownParameterType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownLambdaType=false, reportDeprecated=false, reportInvalidTypeForm=false, reportGeneralTypeIssues=false +import os +import json +import tomllib +import uuid +import shlex +import inspect +import asyncio +import pyautogui +import win32com.client +from copy import deepcopy +from urllib.parse import urlparse +from datetime import datetime +from contextlib import suppress +from abc import ABC, abstractmethod +from pathlib import Path +from collections.abc import Callable, Coroutine +from typing import Any, Protocol, TypeVar, Generic +from pydantic import TypeAdapter +from pydantic_settings import BaseSettings, SettingsConfigDict + +from app.utils import get_logger, dpapi_encrypt, dpapi_decrypt +from app.utils.constants import ( + RESERVED_NAMES, + ILLEGAL_CHARS, + DEFAULT_DATETIME, + EMULATOR_PATH_BOOK, +) + +logger = get_logger("配置基类") + + +def _load_toml_with_pydantic_settings(path: Path) -> dict[str, Any]: + """使用 pydantic-settings 校验 TOML 内容并返回 dict。""" + + raw_text = path.read_text(encoding="utf-8") + if raw_text.strip() == "": + return {} + + raw_data = tomllib.loads(raw_text) + + dynamic_settings_cls = type( + "DynamicTomlSettings", + (BaseSettings,), + { + "model_config": SettingsConfigDict(extra="allow"), + }, + ) + + loaded = dynamic_settings_cls.model_validate(raw_data) + data = loaded.model_dump(mode="python") + extra = getattr(loaded, "__pydantic_extra__", None) + if isinstance(extra, dict): + data.update(extra) + + return TypeAdapter(dict[str, Any]).validate_python(data) + + +def _toml_scalar(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if value is None: + return '""' + return json.dumps(str(value), ensure_ascii=False) + + +def _toml_inline(value: Any) -> str: + if isinstance(value, list): + return "[" + ", ".join(_toml_inline(item) for item in value) + "]" + return _toml_scalar(value) + + +def _dump_toml(data: dict[str, Any]) -> str: + lines: list[str] = [] + + def emit_table(prefix: str, obj: dict[str, Any]) -> None: + scalars: list[tuple[str, Any]] = [] + children: list[tuple[str, dict[str, Any]]] = [] + + for key, value in obj.items(): + if isinstance(value, dict): + children.append((key, value)) + else: + scalars.append((key, value)) + + if prefix: + lines.append(f"[{prefix}]") + + for key, value in scalars: + lines.append(f"{key} = {_toml_inline(value)}") + + if scalars and children: + lines.append("") + + for index, (key, child) in enumerate(children): + child_prefix = f"{prefix}.{key}" if prefix else key + emit_table(child_prefix, child) + if index != len(children) - 1: + lines.append("") + + emit_table("", data) + content = "\n".join(lines).strip() + return f"{content}\n" if content else "" + + +def dump_toml(data: dict[str, Any]) -> str: + """公共 TOML 序列化入口。""" + + return _dump_toml(data) + + +def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path | None]: + legacy_json_file = path.with_suffix(".json") + + if legacy_json_file.exists() and (not path.exists() or path.stat().st_size == 0): + try: + return json.loads(legacy_json_file.read_text(encoding="utf-8")), legacy_json_file + except json.JSONDecodeError: + return {}, legacy_json_file + + if not path.exists(): + return {}, legacy_json_file if legacy_json_file.exists() else None + + try: + return _load_toml_with_pydantic_settings(path), legacy_json_file if legacy_json_file.exists() else None + except Exception: + with suppress(Exception): + return tomllib.loads(path.read_text(encoding="utf-8")), legacy_json_file if legacy_json_file.exists() else None + return {}, legacy_json_file if legacy_json_file.exists() else None + + +def _backup_legacy_json_if_needed(current_file: Path, legacy_json_file: Path | None) -> None: + if legacy_json_file is None or not legacy_json_file.exists(): + return + if not current_file.exists() or current_file.stat().st_size == 0: + return + + legacy_backup = legacy_json_file.with_suffix(".json.bak") + if not legacy_backup.exists(): + legacy_json_file.replace(legacy_backup) + + +class ValidatorBase(ABC): + """基础配置验证器""" + + @abstractmethod + def validate(self, value: Any) -> bool: + """验证值是否合法""" + pass + + @abstractmethod + def correct(self, value: Any) -> Any: + """修正非法值""" + pass + + +class StringValidator(ValidatorBase): + """字符串验证器""" + + def validate(self, value): + return isinstance(value, str) + + def correct(self, value): + return value if self.validate(value) else "" + + +class RangeValidator(ValidatorBase): + """范围验证器""" + + def __init__(self, min: int | float, max: int | float): + self.min = min + self.max = max + self.range = (min, max) + + def validate(self, value): + if not isinstance(value, (int, float)): + return False + return self.min <= value <= self.max + + def correct(self, value): + if not isinstance(value, (int, float)): + try: + value = float(value) + except TypeError: + return self.min + return min(max(self.min, value), self.max) + + +class OptionsValidator(ValidatorBase): + """选项验证器""" + + def __init__(self, options: list[Any]): + if not options: + raise ValueError("可选项不能为空") + + self.options = options + + def validate(self, value): + return value in self.options + + def correct(self, value): + return value if self.validate(value) else self.options[0] + + +class MultipleOptionsValidator(ValidatorBase): + """多选选项验证器""" + + def __init__(self, options: list[Any]): + if not options: + raise ValueError("可选项不能为空") + + self.options = options + + def validate(self, value): + if not isinstance(value, list): + return False + + return all(item in self.options for item in value) + + def correct(self, value): + return value if self.validate(value) else [] + + +class UUIDValidator(ValidatorBase): + """UUID验证器""" + + def validate(self, value): + try: + uuid.UUID(value) + return True + except (TypeError, ValueError): + return False + + def correct(self, value): + return value if self.validate(value) else str(uuid.uuid4()) + + +class MultipleUIDValidator(ValidatorBase): + """多配置管理类UID验证器""" + + def __init__( + self, + default: Any, + related_config: dict[str, "MultipleConfig[Any]"], + config_name: str, + ): + self.default = default + self.related_config = related_config + self.config_name = config_name + + def validate(self, value): + if value == self.default: + return True + if not isinstance(value, str): + return False + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return False + if uid in self.related_config.get(self.config_name, {}): + return True + return False + + def correct(self, value): + if self.validate(value): + return value + return self.default + + +class DateTimeValidator(ValidatorBase): + """日期时间验证器""" + + def __init__(self, date_format: str) -> None: + if not date_format: + raise ValueError("日期时间格式不能为空") + self.date_format = date_format + + def validate(self, value): + if not isinstance(value, str): + return False + try: + datetime.strptime(value, self.date_format) + return True + except ValueError: + return False + + def correct(self, value): + if not isinstance(value, str): + return DEFAULT_DATETIME.strftime(self.date_format) + try: + datetime.strptime(value, self.date_format) + return value + except ValueError: + return DEFAULT_DATETIME.strftime(self.date_format) + + +class JSONValidator(ValidatorBase): + def __init__( + self, tpye: type[dict[str, Any]] | type[list[Any]] = dict + ) -> None: + self.type = tpye + + def validate(self, value): + if not isinstance(value, str): + return False + try: + data = json.loads(value) + if isinstance(data, self.type): + return True + else: + return False + except json.JSONDecodeError: + return False + + def correct(self, value): + return ( + value if self.validate(value) else ("{ }" if self.type is dict else "[ ]") + ) + + +class EncryptValidator(ValidatorBase): + """加密数据验证器""" + + def validate(self, value): + if not isinstance(value, str): + return False + try: + dpapi_decrypt(value) + return True + except Exception: + return False + + def correct(self, value: Any) -> Any: + return value if self.validate(value) else dpapi_encrypt("数据损坏, 请重新设置") + + +class VirtualConfigValidator(ValidatorBase): + """虚拟配置验证器""" + + def __init__(self, function: Callable[[], str]): + self.function = function + self.if_init = False + + def validate(self, value): + if not self.if_init: + self.if_init = True + return True + return False + + def correct(self, value): + try: + return self.function() + except Exception as e: + return str(e) + + +class BoolValidator(ValidatorBase): + """布尔值验证器""" + + def validate(self, value): + if not isinstance(value, bool): + return False + return True + + def correct(self, value): + return value if self.validate(value) else False + + +class FileValidator(ValidatorBase): + """文件路径验证器""" + + def validate(self, value): + if not isinstance(value, str): + return False + # 允许空字符串(表示未设置路径) + if value == "": + return True + if not Path(value).is_absolute(): + return False + if Path(value).suffix == ".lnk": + return False + return True + + def correct(self, value): + if not isinstance(value, str): + value = str(Path.cwd()) + # 空字符串直接返回 + if value == "": + return "" + if "%APPDATA%" in value: + value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") + if not Path(value).is_absolute(): + value = Path(value).resolve().as_posix() + if Path(value).suffix == ".lnk": + try: + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortcut(value) + value = shortcut.TargetPath + except Exception: + pass + return Path(value).resolve().as_posix() + + +class FolderValidator(ValidatorBase): + """文件夹路径验证器""" + + def validate(self, value): + if not isinstance(value, str): + return False + if not Path(value).is_absolute(): + return False + if not Path(value).is_dir(): + return False + return True + + def correct(self, value): + if not isinstance(value, str): + value = str(Path.cwd()) + if "%APPDATA%" in value: + value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") + if not Path(value).is_dir(): + value = Path(value).with_suffix("") + return Path(value).resolve().as_posix() + + +class EmulatorPathValidator(FileValidator): + """模拟器管理器路径验证器""" + + def __init__(self, emulator_type: ConfigItem) -> None: + super().__init__() + + self.emulator_type = emulator_type + + def validate(self, value): + if not isinstance(value, str): + return False + # 允许空字符串(表示未设置路径) + if value == "": + return True + if not Path(value).is_absolute(): + return False + if Path(value).suffix == ".lnk": + return False + + path = Path(value) + + if not path.is_file() or not os.access(path, os.X_OK): + return False + + if ( + self.emulator_type.getValue() in EMULATOR_PATH_BOOK + and path.name + != EMULATOR_PATH_BOOK[self.emulator_type.getValue()]["executables"][0] + ): + return False + + return True + + def correct(self, value): + + if not isinstance(value, str): + value = str(Path.cwd()) + # 空字符串直接返回 + if value == "": + return "" + if "%APPDATA%" in value: + value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") + if not Path(value).is_absolute(): + value = Path(value).resolve().as_posix() + if Path(value).suffix == ".lnk": + try: + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortcut(value) + value = shortcut.TargetPath + except Exception: + pass + + # 不支持矫正的模拟器类型直接返回路径字符串 + if self.emulator_type.getValue() not in EMULATOR_PATH_BOOK: + return Path(value).resolve().as_posix() + + try: + # 从给定路径向上或向下搜索模拟器主管理器程序的完整路径 + path = Path(value) + + # 获取模拟器配置信息 + config = EMULATOR_PATH_BOOK[self.emulator_type.getValue()] + executables = config["executables"] + + # 第一个可执行文件是主管理器程序(优先级最高) + primary_exe = executables[0] + + # 如果输入的是文件,先获取其父目录 + if path.is_file(): + path = path.parent + + # 1. 首先检查当前目录是否直接包含主管理器程序 + # 如果用户给的就是正确的主程序路径,直接返回 + primary_exe_path = path / primary_exe + if primary_exe_path.exists(): + result = str(primary_exe_path) + return result + + # 2. 向上搜索父目录,找到直接包含主管理器程序的目录(最多3层) + candidates = [] + current = path + for level in range(3): + parent = current.parent + if parent == current: # 已到达根目录 + break + + # 只接受直接包含主管理器程序的目录 + parent_exe_path = parent / primary_exe + if parent_exe_path.exists(): + candidates.append( + { + "path": parent, + "exe_path": parent_exe_path, + "depth": len(parent.parts), + "level": level + 1, + } + ) + + current = parent + + # 如果找到了候选目录,选择最优的(深度最小的,即最接近根目录的) + if candidates: + # 排序策略:深度越小越好(越靠近根目录) + candidates.sort(key=lambda x: x["depth"]) + + best_candidate = candidates[0] + result = str(best_candidate["exe_path"]) + + return result + + # 3. 如果向上没找到,尝试向下搜索子目录(仅1层,且必须直接包含主管理器程序) + try: + for subdir in path.iterdir(): + if subdir.is_dir(): + subdir_exe_path = subdir / primary_exe + # 只接受直接包含主管理器程序的子目录 + if subdir_exe_path.exists(): + result = str(subdir_exe_path) + return result + except PermissionError: + pass + + # 4. 检查兄弟目录(必须直接包含主管理器程序) + if path.parent != path: + try: + for sibling in path.parent.iterdir(): + if sibling.is_dir() and sibling != path: + sibling_exe_path = sibling / primary_exe + # 只接受直接包含主管理器程序的兄弟目录 + if sibling_exe_path.exists(): + result = str(sibling_exe_path) + return result + except PermissionError: + pass + + return Path(value).resolve().as_posix() + except Exception: + return Path(value).resolve().as_posix() + + +class UserNameValidator(ValidatorBase): + """用户名验证器""" + + def validate(self, value): + if not isinstance(value, str): + return False + + if not value or not value.strip(): + return False + + if value != value.strip() or value != value.strip("."): + return False + + if any(char in ILLEGAL_CHARS for char in value): + return False + + if value.upper() in RESERVED_NAMES: + return False + if len(value) > 255: + return False + + return True + + def correct(self, value): + if not isinstance(value, str): + value = "默认用户名" + + value = value.strip().strip(".") + + value = "".join(char for char in value if char not in ILLEGAL_CHARS) + + if value.upper() in RESERVED_NAMES or not value: + value = "默认用户名" + + if len(value) > 255: + value = value[:255] + + return value + + +class KeyValidator(ValidatorBase): + """键盘按键格式验证器""" + + def __init__(self, default: str = ""): + self.default = default + + def validate(self, value: Any) -> bool: + return value in pyautogui.KEYBOARD_KEYS + + def correct(self, value: Any) -> Any: + return value if self.validate(value) else self.default + + +class URLValidator(ValidatorBase): + """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): + 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): + return value if self.validate(value) else self.default + + +class ArgumentValidator(ValidatorBase): + + def validate(self, value): + if not isinstance(value, str): + return False + try: + shlex.split(value.strip()) + return True + except ValueError: + return False + + def correct(self, value): + + return value if self.validate(value) else "" + + +class AdvancedArgumentValidator(ValidatorBase): + + def validate(self, value): + if not isinstance(value, str): + return False + try: + for segment in value.split("|"): + segment = segment.strip() + if not segment: + continue + param_str = segment.split("%", 1)[-1].strip() + shlex.split(param_str) + return True + except ValueError: + return False + + def correct(self, value): + + return value if self.validate(value) else "" + + +class ConfigItem: + """配置项""" + + def __init__( + self, + group: str, + name: str, + default: Any, + validator: ValidatorBase = StringValidator(), + legacy_group: str | None = None, + legacy_name: str | None = None, + ): + """ + Parameters + ---------- + group: str + 配置项分组名称 + + name: str + 配置项字段名称 + + default: Any + 配置项默认值 + + validator: ValidatorBase + 配置项验证器, 默认为 None, 表示不进行验证 + """ + self.group = group + self.name = name + self.value: Any = default + self.validator = validator + self.legacy_group_name = ( + (legacy_group or group, legacy_name or name) + if legacy_group or legacy_name + else None + ) + self.is_locked = False + self._slots: list[Callable[[Any], Any]] = [] + + if not self.validator.validate(self.value): + raise ValueError( + f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法" + ) + + def setValue(self, value: Any): + """ + 设置配置项值, 将自动进行验证和修正 + + Parameters + ---------- + value: Any + 要设置的值, 可以是任何合法类型 + """ + + if ( + dpapi_decrypt(self.value) + if isinstance(self.validator, EncryptValidator) + else self.value + ) == value: + return + + if self.is_locked: + raise ValueError(f"配置项 '{self.group}.{self.name}' 已锁定, 无法修改") + + # deepcopy new value + try: + self.value = deepcopy(value) + except Exception: + self.value = value + + if isinstance(self.validator, EncryptValidator): + if self.validator.validate(self.value): + self.value = self.value + else: + self.value = dpapi_encrypt(self.value) + + if not self.validator.validate(self.value): + self.value = self.validator.correct(self.value) + + if len(self._slots) > 0: + asyncio.create_task(self._emit_signal(self.value)) + + def getValue(self, if_decrypt: bool = True) -> Any: + """ + 获取配置项值 + """ + + v = ( + self.value + if self.validator.validate(self.value) + else self.validator.correct(self.value) + ) + + if isinstance(self.validator, EncryptValidator) and if_decrypt: + return dpapi_decrypt(v) + return v + + def bind(self, slot: Callable[[Any], Any]): + """ + 连接槽函数到配置项修改信号 + + Parameters + ---------- + slot: Callable[[Any], Any] + 槽函数,接收新值作为参数,支持同步和异步函数 + """ + if not callable(slot): + raise TypeError("槽函数必须是可调用对象") + + if slot not in self._slots: + self._slots.append(slot) + + def unbind(self, slot: Callable[[Any], Any]): + """ + 断开槽函数连接 + + Parameters + ---------- + slot: Callable[[Any], Any] + 要断开的槽函数 + """ + if slot in self._slots: + self._slots.remove(slot) + + def unbind_all(self): + """断开所有槽函数连接""" + self._slots.clear() + + @logger.catch + async def _emit_signal(self, value: Any) -> None: + """ + 执行所有连接的槽函数, 将新值作为参数传递 + + Parameters + ---------- + value: Any + 新值, 已经过验证和修正 + """ + + for slot in self._slots: + if inspect.iscoroutinefunction(slot): + await slot(value) + else: + slot(value) + + def lock(self): + """ + 锁定配置项, 锁定后无法修改配置项值 + """ + self.is_locked = True + + def unlock(self): + """ + 解锁配置项, 解锁后可以修改配置项值 + """ + self.is_locked = False + + +class ConfigBase: + """ + 配置基类 + + 这个类提供了基本的配置项管理功能, 包括连接配置文件、加载配置数据、获取和设置配置项值等。 + + 此类不支持直接实例化, 必须通过子类来实现具体的配置项, + 请继承此类并在子类中定义具体的配置项, 并在定义完成后调用父类的 `__init__` 方法。 + 若将配置项设为类属性, 则所有实例都会共享同一份配置项数据。 + 若将配置项设为实例属性, 则每个实例都会有独立的配置项数据。 + 子配置项可以是 `MultipleConfig` 的实例。 + """ + + def __init__(self): + self.file: Path | None = None + self.is_locked = False + self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] + + # 配置项索引 + self._config_item_index: dict[str, dict[str, ConfigItem]] = {} + self._multiple_config_index: dict[str, "MultipleConfig[Any]"] = {} + for name in dir(self): + item = getattr(self, name) + + if isinstance(item, ConfigItem): + if not self._config_item_index.get(item.group): + self._config_item_index[item.group] = {} + self._config_item_index[item.group][item.name] = item + + elif isinstance(item, MultipleConfig): + self._multiple_config_index[name] = item + + async def connect(self, path: Path): + """ + 将配置数据绑定到指定配置文件 + + Parameters + ---------- + path: Path + 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 + """ + + if path.suffix != ".toml": + raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self.file = path + + if not self.file.exists(): + self.file.parent.mkdir(parents=True, exist_ok=True) + self.file.touch() + data, legacy_json_file = _load_config_with_legacy_migration(self.file) + + await self.load(data) + + await self.add_save_method(self.save) + + _backup_legacy_json_if_needed(self.file, legacy_json_file) + + async def add_save_method( + self, save_method: Callable[[], Coroutine[Any, Any, None]] + ): + """ + 添加父配置项的保存方法 + + Parameters + ---------- + save_method: Callable[[], Coroutine[Any, Any, None]] + 保存方法 + """ + + if save_method != self.save: + self._save_methods.append(save_method) + + for sub_config in self._multiple_config_index.values(): + await sub_config.add_save_method(save_method) + + async def load(self, data: dict[str, Any]): + """ + 从字典加载配置数据 + + 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigItem 实例中。 + 如果字典中包含 "SubConfigsInfo" 键, 则会加载子配置项, 这些子配置项应该是 MultipleConfig 的实例。 + + Parameters + ---------- + data: dict + 配置数据字典 + """ + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + # 加载多配置项类型数据 + sub_configs = data.pop("SubConfigsInfo", {}) + if not isinstance(sub_configs, dict): + sub_configs = {} + for name, sub_config in self._multiple_config_index.items(): + data_for_sub_config = sub_configs.get(name) + if isinstance(data_for_sub_config, dict): + await sub_config.load(data_for_sub_config) + + for group, info in self._config_item_index.items(): + for name, item in info.items(): + try: + item.setValue(data[group][name]) + except Exception: + if item.legacy_group_name is not None: + with suppress(Exception): + item.setValue( + data[item.legacy_group_name[0]][ + item.legacy_group_name[1] + ] + ) + + if self.file: + await self.save() + + await asyncio.gather(*(_() for _ in self._save_methods)) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + """将配置项转换为字典""" + + data = {} + + for group, info in self._config_item_index.items(): + for name, item in info.items(): + data.setdefault(group, {})[name] = item.getValue(if_decrypt) + + for name, item in self._multiple_config_index.items(): + if not data.get("SubConfigsInfo"): + data["SubConfigsInfo"] = {} + data["SubConfigsInfo"][name] = await item.toDict( + if_decrypt, regenerate_uuids + ) + + return data + + def get(self, group: str, name: str) -> Any: + """获取配置项的值""" + + if not self._config_item_index.get(group, {}).get(name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + return self._config_item_index[group][name].getValue() + + async def set(self, group: str, name: str, value: Any): + """ + 设置配置项的值 + + Parameters + ---------- + group: str + 配置项分组名称 + name: str + 配置项名称 + value: Any + 配置项新值 + """ + + if not self._config_item_index.get(group, {}).get(name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self._config_item_index[group][name].setValue(value) + + if self.file: + await self.save() + + await asyncio.gather(*(_() for _ in self._save_methods)) + + def bind(self, group: str, name: str, slot: Callable[[Any], Any]): + """ + 连接槽函数到配置项修改信号 + + Parameters + ---------- + group: str + 配置项分组名称 + name: str + 配置项名称 + slot: Callable[[Any], Any] + 槽函数,接收新值作为参数,支持同步和异步函数 + """ + + if not self._config_item_index.get(group, {}).get(name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self._config_item_index[group][name].bind(slot) + + def unbind(self, group: str, name: str, slot: Callable[[Any], Any]): + """ + 断开槽函数连接 + + Parameters + ---------- + group: str + 配置项分组名称 + name: str + 配置项名称 + slot: Callable[[Any], Any] + 要断开的槽函数 + """ + + if not self._config_item_index.get(group, {}).get(name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self._config_item_index[group][name].unbind(slot) + + async def save(self) -> None: + """保存配置""" + + if not self.file: + raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + + self.file.parent.mkdir(parents=True, exist_ok=True) + self.file.write_text( + _dump_toml(await self.toDict(if_decrypt=False)), + encoding="utf-8", + ) + + async def lock(self): + """ + 锁定配置项, 锁定后无法修改配置项值 + """ + + self.is_locked = True + + for group in self._config_item_index.values(): + for item in group.values(): + item.lock() + for config in self._multiple_config_index.values(): + await config.lock() + + async def unlock(self): + """ + 解锁配置项, 解锁后可以修改配置项值 + """ + + self.is_locked = False + + for group in self._config_item_index.values(): + for item in group.values(): + item.unlock() + for config in self._multiple_config_index.values(): + await config.unlock() + + +class _ConfigLike(Protocol): + @property + def is_locked(self) -> bool: ... + + async def add_save_method( + self, save_method: Callable[[], Coroutine[Any, Any, None]] + ) -> None: ... + + async def load(self, data: dict[str, Any]) -> None: ... + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: ... + + async def lock(self) -> None: ... + + async def unlock(self) -> None: ... + + +T = TypeVar("T", bound=_ConfigLike) + + +class MultipleConfig(Generic[T]): + """ + 多配置项管理类 + + 这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。 + 允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。 + + Parameters + ---------- + sub_config_type: List[type] + 子配置项的类型列表, 必须是 ConfigBase 的子类 + """ + + def __init__(self, sub_config_type: list[type[T]]): + if not sub_config_type: + raise ValueError("子配置项类型列表不能为空") + + self.sub_config_type: dict[str, type[T]] = { + _.__name__: _ for _ in sub_config_type + } + self.file: Path | None = None + self.order: list[uuid.UUID] = [] + self.data: dict[uuid.UUID, T] = {} + self.is_locked = False + self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] + + def __getitem__(self, key: uuid.UUID) -> T: + """允许通过 config[uuid] 访问配置项""" + if key not in self.data: + raise KeyError(f"配置项 '{key}' 不存在") + return self.data[key] + + def __contains__(self, key: uuid.UUID) -> bool: + """允许使用 uuid in config 检查是否存在""" + return key in self.data + + def __len__(self) -> int: + """允许使用 len(config) 获取配置项数量""" + return len(self.data) + + def __repr__(self) -> str: + """更好的字符串表示""" + return f"MultipleConfig(items={len(self.data)}, types={list(self.sub_config_type.keys())})" + + def __str__(self) -> str: + """用户友好的字符串表示""" + return f"MultipleConfig with {len(self.data)} items" + + async def connect(self, path: Path): + """ + 将配置文件连接到指定配置文件 + + Parameters + ---------- + path: Path + 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 + """ + + if path.suffix != ".toml": + raise ValueError("配置文件必须是带有 '.toml' 扩展名的 TOML 文件。") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self.file = path + + if not self.file.exists(): + self.file.parent.mkdir(parents=True, exist_ok=True) + self.file.touch() + data, legacy_json_file = _load_config_with_legacy_migration(self.file) + + await self.load(data) + + await self.add_save_method(self.save) + + _backup_legacy_json_if_needed(self.file, legacy_json_file) + + async def add_save_method( + self, save_method: Callable[[], Coroutine[Any, Any, None]] + ): + """ + 添加父配置项的保存方法 + + Parameters + ---------- + save_method: Callable[[], Coroutine[Any, Any, None]] + 保存方法, 必须是一个协程函数, 无参数, 无返回值 + """ + + if save_method != self.save: + self._save_methods.append(save_method) + + for sub_config in self.data.values(): + await sub_config.add_save_method(save_method) + + async def load(self, data: dict[str, Any]): + """ + 从字典加载配置数据 + + 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigBase 实例中。 + 如果字典中包含 "instances" 键, 则会加载子配置项, 这些子配置项应该是 ConfigBase 子类的实例。 + 如果字典中没有 "instances" 键, 则清空当前配置项。 + + Parameters + ---------- + data: dict + 配置数据字典 + """ + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self.order = [] + self.data = {} + + if not data.get("instances"): + return + + for instance in data["instances"]: + if not isinstance(instance, dict): + continue + + uid_str = instance.get("uid") + if not isinstance(uid_str, str): + continue + + instance_data = data.get(uid_str) + if not isinstance(instance_data, dict): + continue + + type_name = instance.get("type") + + if type_name in self.sub_config_type: + self.order.append(uuid.UUID(uid_str)) + self.data[self.order[-1]] = self.sub_config_type[type_name]() + await self.data[self.order[-1]].load(instance_data) + + if self.file: + await self.save() + + await asyncio.gather(*(_() for _ in self._save_methods)) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + """ + 将配置项转换为字典 + + Arguments + ---------- + if_decrypt: bool + 是否解密数据, 默认为 True + regenerate_uuids: bool + 是否重新生成 UUID, 默认为 False + + Returns + ------- + Dict[str, Union[list, dict]] + 配置项数据字典 + """ + + uuid_book: dict[uuid.UUID, uuid.UUID] = { + _: uuid.uuid4() if regenerate_uuids else _ for _ in self.order + } + + data: dict[str, Any] = { + "instances": [ + {"uid": str(uuid_book[_]), "type": type(self.data[_]).__name__} + for _ in self.order + ] + } + for uid, config in self.items(): + data[str(uuid_book[uid])] = await config.toDict( + if_decrypt, regenerate_uuids + ) + + return data + + async def get(self, uid: uuid.UUID) -> dict[str, Any]: + """ + 获取指定 UID 的配置项 + + Parameters + ---------- + uid: uuid.UUID + 要获取的配置项的唯一标识符 + Returns + ------- + Dict[str, Union[list, dict]] + 对应的配置项数据字典 + """ + + if uid not in self.data: + raise ValueError(f"配置项 '{uid}' 不存在。") + + data: dict[str, Any] = { + "instances": [ + {"uid": str(_), "type": type(self.data[_]).__name__} + for _ in self.order + if _ == uid + ] + } + data[str(uid)] = await self.data[uid].toDict() + + return data + + async def save(self): + """保存配置""" + + if not self.file: + raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + + self.file.parent.mkdir(parents=True, exist_ok=True) + self.file.write_text( + _dump_toml(await self.toDict(if_decrypt=False)), + encoding="utf-8", + ) + + async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: + """ + 添加一个新的配置项 + + Parameters + ---------- + config_type: type + 配置项的类型, 必须是初始化时已声明的 ConfigBase 子类 + + Returns + ------- + tuple[uuid.UUID, ConfigBase] + 新创建的配置项的唯一标识符和实例 + """ + + if config_type not in self.sub_config_type.values(): + raise ValueError(f"配置类型 {config_type.__name__} 不被允许") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + uid = uuid.uuid4() + self.order.append(uid) + self.data[uid] = config_type() + + for save_method in self._save_methods: + await self.data[uid].add_save_method(save_method) + + if self.file: + await self.data[uid].add_save_method(self.save) + await self.save() + + await asyncio.gather(*(_() for _ in self._save_methods)) + + return uid, self.data[uid] + + async def remove(self, uid: uuid.UUID): + """ + 移除配置项 + + Parameters + ---------- + uid: uuid.UUID + 要移除的配置项的唯一标识符 + """ + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + if uid not in self.data: + raise ValueError(f"配置项 '{uid}' 不存在") + + if self.data[uid].is_locked: + raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除") + + self.data.pop(uid) + self.order.remove(uid) + + if self.file: + await self.save() + + await asyncio.gather(*(_() for _ in self._save_methods)) + + async def setOrder(self, order: list[uuid.UUID]): + """ + 设置配置项的顺序 + + Parameters + ---------- + order: List[uuid.UUID] + 新的配置项顺序 + """ + + if set(order) != set(self.data.keys()): + raise ValueError("顺序与当前配置项不匹配") + + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self.order = order + + if self.file: + await self.save() + + await asyncio.gather(*(_() for _ in self._save_methods)) + + async def lock(self): + """ + 锁定配置项, 锁定后无法修改配置项值 + """ + + self.is_locked = True + + for item in self.values(): + await item.lock() + + async def unlock(self): + """ + 解锁配置项, 解锁后可以修改配置项值 + """ + + self.is_locked = False + + for item in self.values(): + await item.unlock() + + def keys(self): + """返回配置项的所有唯一标识符""" + + return iter(self.order) + + def values(self): + """返回配置项的所有实例""" + + if not self.data: + return iter(()) + + return (self.data[_] for _ in self.order) + + def items(self): + """返回配置项的所有唯一标识符和实例的元组""" + + return zip(self.keys(), self.values()) diff --git a/app/models/dto.py b/app/models/dto.py new file mode 100644 index 00000000..6875eff8 --- /dev/null +++ b/app/models/dto.py @@ -0,0 +1,1462 @@ +# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software +# Copyright © 2024-2025 DLmaster361 +# Copyright © 2025 MoeSnowyFox +# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with AUTO-MAS. If not, see . + +# Contact: DLmaster_361@163.com + + +# pyright: reportUnknownVariableType=false, reportGeneralTypeIssues=false +from pydantic import BaseModel, Field +from typing import Any, Dict, List, Union, Optional, Literal + + +class OutBase(BaseModel): + code: int = Field(default=200, description="状态码") + status: str = Field(default="success", description="操作状态") + message: str = Field(default="操作成功", description="操作消息") + + +class InfoOut(OutBase): + data: Dict[str, Any] = Field(..., description="收到的服务器数据") + + +class VersionOut(OutBase): + if_need_update: bool = Field(..., description="后端代码是否需要更新") + current_time: str = Field(..., description="后端代码当前时间戳") + current_hash: str = Field(..., description="后端代码当前哈希值") + + +class NoticeOut(OutBase): + if_need_show: bool = Field(..., description="是否需要显示公告") + data: Dict[str, str] = Field( + ..., description="公告信息, key为公告标题, value为公告内容" + ) + + +class TagItem(BaseModel): + text: str = Field(..., description="标签文本") + color: Literal[ + "red", + "blue", + "green", + "yellow", + "orange", + "purple", + "pink", + "brown", + "black", + "white", + "gray", + "silver", + "gold", + ] = Field(..., description="标签颜色") + + +class ComboBoxItem(BaseModel): + label: str = Field(..., description="展示值") + value: Optional[str] = Field(..., description="实际值") + + +class ComboBoxOut(OutBase): + data: List[ComboBoxItem] = Field(..., description="下拉框选项") + + +class GetStageIn(BaseModel): + type: Literal[ + "User", + "Today", + "ALL", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] = Field( + ..., + description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项", + ) + + +class EmulatorConfigIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["EmulatorConfig"] = Field(..., description="配置类型") + + +class EmulatorConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="模拟器名称") + Type: Optional[Literal["general", "mumu", "ldplayer"]] = Field( + default=None, description="模拟器类型" + ) + Path: Optional[str] = Field(default=None, description="模拟器路径") + BossKey: Optional[str] = Field(default=None, description="老板键快捷键配置") + MaxWaitTime: Optional[int] = Field(default=None, description="最大等待时间(秒)") + + +class EmulatorConfig(BaseModel): + Info: Optional[EmulatorConfig_Info] = Field( + default=None, description="模拟器基础信息" + ) + + +class ToolsConfig_ArknightsPC(BaseModel): + Enabled: bool | None = Field(default=None, description="是否启用 ArknightsPC 工具") + PauseKey: str | None = Field(default=None, description="暂停键位") + SelectDeployedKey: str | None = Field( + default=None, description="选中已部署干员键位" + ) + UseSkillKey: str | None = Field(default=None, description="释放技能键位") + RetreatKey: str | None = Field(default=None, description="撤退键位") + NextFrameKey: str | None = Field(default=None, description="下一帧键位") + AnotherQuitKey: str | None = Field(default=None, description="自定义退出、暂停键位") + Status: str | None = Field(default=None, description="工具状态 Tag") + + +class ToolsConfig(BaseModel): + ArknightsPC: ToolsConfig_ArknightsPC | None = Field( + default=None, description="明日方舟PC工具配置" + ) + + +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[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表示永久保存" + ) + IfAllowSleep: Optional[bool] = Field(default=None, description="允许休眠") + IfSilence: Optional[bool] = Field(default=None, description="静默模式") + IfAgreeBilibili: Optional[bool] = Field( + default=None, description="同意哔哩哔哩用户协议" + ) + IfBlockAd: Optional[bool] = Field(default=None, description="屏蔽模拟器广告") + + +class GlobalConfig_Voice(BaseModel): + Enabled: Optional[bool] = Field(default=None, description="语音功能是否启用") + Type: Optional[Literal["simple", "noisy"]] = Field( + default=None, description="语音类型, simple为简洁, noisy为聒噪" + ) + + +class GlobalConfig_Start(BaseModel): + IfSelfStart: Optional[bool] = Field( + default=None, description="是否在系统启动时自动运行" + ) + IfMinimizeDirectly: Optional[bool] = Field( + default=None, description="启动时是否直接最小化到托盘而不显示主窗口" + ) + + +class GlobalConfig_UI(BaseModel): + IfShowTray: Optional[bool] = Field(default=None, description="是否常态显示托盘图标") + IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘") + + +class GlobalConfig_Notify(BaseModel): + SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field( + default=None, description="任务结果推送时机" + ) + IfSendStatistic: Optional[bool] = Field( + default=None, description="是否发送统计信息" + ) + IfSendSixStar: Optional[bool] = Field( + default=None, description="是否发送公招六星通知" + ) + IfPushPlyer: Optional[bool] = Field(default=None, description="是否推送系统通知") + IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") + IfKoishiSupport: Optional[bool] = Field( + default=None, description="是否启用Koishi支持" + ) + KoishiServerAddress: Optional[str] = Field( + default=None, description="Koishi服务器地址" + ) + KoishiToken: Optional[str] = Field(default=None, description="Koishi Token") + SMTPServerAddress: Optional[str] = Field(default=None, description="SMTP服务器地址") + AuthorizationCode: Optional[str] = Field(default=None, description="SMTP授权码") + FromAddress: Optional[str] = Field(default=None, description="邮件发送地址") + ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") + IfServerChan: Optional[bool] = Field( + default=None, description="是否使用ServerChan推送" + ) + ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥") + + +class GlobalConfig_Update(BaseModel): + IfAutoUpdate: Optional[bool] = Field(default=None, description="是否自动更新") + Source: Optional[Literal["GitHub", "MirrorChyan", "AutoSite"]] = Field( + default=None, description="更新源: GitHub源, Mirror酱源, 自建源" + ) + Channel: Optional[Literal["stable", "beta"]] = Field( + default=None, description="更新渠道: 稳定版, 测试版" + ) + ProxyAddress: Optional[str] = Field(default=None, description="网络代理地址") + MirrorChyanCDK: Optional[str] = Field(default=None, description="Mirror酱CDK") + + +class GlobalConfig(BaseModel): + Function: Optional[GlobalConfig_Function] = Field( + default=None, description="功能相关配置" + ) + Voice: Optional[GlobalConfig_Voice] = Field( + default=None, description="语音相关配置" + ) + Start: Optional[GlobalConfig_Start] = Field( + default=None, description="启动相关配置" + ) + UI: Optional[GlobalConfig_UI] = Field(default=None, description="界面相关配置") + Notify: Optional[GlobalConfig_Notify] = Field( + default=None, description="通知相关配置" + ) + Update: Optional[GlobalConfig_Update] = Field( + default=None, description="更新相关配置" + ) + + +class QueueIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["QueueConfig"] = Field(..., description="配置类型") + + +class QueueItemIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["QueueItem"] = Field(..., description="配置类型") + + +class TimeSetIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["TimeSet"] = Field(..., description="配置类型") + + +class QueueItem_Info(BaseModel): + ScriptId: Optional[str] = Field( + default=None, description="任务所对应的脚本ID, 为None时表示未选择" + ) + + +class QueueItem(BaseModel): + Info: Optional[QueueItem_Info] = Field(default=None, description="队列项") + + +class TimeSet_Info(BaseModel): + Enabled: Optional[bool] = Field(default=None, description="是否启用") + Days: Optional[ + List[ + Literal[ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] + ] + ] = Field(default=None, description="执行周期, 可多选") + Time: Optional[str] = Field(default=None, description="时间设置, 格式为HH:MM") + + +class TimeSet(BaseModel): + Info: Optional[TimeSet_Info] = Field(default=None, description="时间项") + + +class QueueConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="队列名称") + TimeEnabled: Optional[bool] = Field(default=None, description="是否启用定时") + StartUpEnabled: Optional[bool] = Field(default=None, description="是否启动时运行") + AfterAccomplish: Optional[ + Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] + ] = Field(default=None, description="完成后操作") + + +class QueueConfig(BaseModel): + Info: Optional[QueueConfig_Info] = Field(default=None, description="队列信息") + + +class ScriptIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["MaaConfig", "GeneralConfig", "SrcConfig", "MaaEndConfig"] = Field( + ..., description="配置类型" + ) + + +class UserIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal[ + "MaaUserConfig", "GeneralUserConfig", "SrcUserConfig", "MaaEndUserConfig" + ] = Field(..., description="配置类型") + + +class MaaUserConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="用户名") + Id: Optional[str] = Field(default=None, description="用户ID") + Mode: Optional[Literal["简洁", "详细"]] = Field( + default=None, description="用户配置模式" + ) + StageMode: Optional[str] = Field(default=None, description="关卡配置模式") + Server: Optional[ + Literal["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"] + ] = Field(default=None, description="服务器") + Status: Optional[bool] = Field(default=None, description="用户状态") + RemainedDay: Optional[int] = Field(default=None, description="剩余天数") + Annihilation: Optional[ + Literal[ + "Close", + "Annihilation", + "Chernobog@Annihilation", + "LungmenOutskirts@Annihilation", + "LungmenDowntown@Annihilation", + ] + ] = Field(default=None, description="剿灭模式") + InfrastMode: Optional[Literal["Normal", "Rotation", "Custom"]] = Field( + default=None, description="基建模式" + ) + InfrastName: Optional[str] = Field(default=None, description="基建方案名称") + InfrastIndex: Optional[str] = Field(default=None, description="基建方案索引") + Password: Optional[str] = Field(default=None, description="密码") + Notes: Optional[str] = Field(default=None, description="备注") + MedicineNumb: Optional[int] = Field(default=None, description="吃理智药数量") + SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field( + default=None, description="连战次数" + ) + Stage: Optional[str] = Field(default=None, description="关卡选择") + Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1") + Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2") + Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3") + Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡") + IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到") + SklandToken: Optional[str] = Field(default=None, description="SklandToken") + Tag: Optional[str] = Field(default=None, description="状态标签列表") + + +class MaaUserConfig_Data(BaseModel): + IfPassCheck: Optional[bool] = Field(default=None, description="是否通过人工排查") + + +class MaaUserConfig_Task(BaseModel): + IfStartUp: Optional[bool] = Field(default=None, description="开始唤醒") + IfRecruit: Optional[bool] = Field(default=None, description="自动公招") + IfInfrast: Optional[bool] = Field(default=None, description="基建换班") + IfFight: Optional[bool] = Field(default=None, description="理智作战") + IfMall: Optional[bool] = Field(default=None, description="信用收支") + IfAward: Optional[bool] = Field(default=None, description="领取奖励") + IfRoguelike: Optional[bool] = Field(default=None, description="自动肉鸽") + IfReclamation: Optional[bool] = Field(default=None, description="生息演算") + + +class MaaUserConfig_Notify(BaseModel): + Enabled: Optional[bool] = Field(default=None, description="是否启用通知") + IfSendStatistic: Optional[bool] = Field( + default=None, description="是否发送统计信息" + ) + IfSendSixStar: Optional[bool] = Field(default=None, description="是否发送高资喜报") + IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") + ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") + IfServerChan: Optional[bool] = Field( + default=None, description="是否使用Server酱推送" + ) + ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey") + + +class GeneralUserConfig_Notify(BaseModel): + Enabled: Optional[bool] = Field(default=None, description="是否启用通知") + IfSendStatistic: Optional[bool] = Field( + default=None, description="是否发送统计信息" + ) + IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") + ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") + IfServerChan: Optional[bool] = Field( + default=None, description="是否使用Server酱推送" + ) + ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey") + + +class MaaUserConfig(BaseModel): + Info: Optional[MaaUserConfig_Info] = Field(default=None, description="基础信息") + Data: Optional[MaaUserConfig_Data] = Field(default=None, description="用户数据") + Task: Optional[MaaUserConfig_Task] = Field(default=None, description="任务列表") + Notify: Optional[MaaUserConfig_Notify] = Field(default=None, description="单独通知") + + +class MaaConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="脚本名称") + Path: Optional[str] = Field(default=None, description="脚本路径") + + +class MaaConfig_Emulator(BaseModel): + Id: Optional[str] = Field(default=None, description="模拟器ID") + Index: Optional[str] = Field(default=None, description="模拟器多开实例索引") + + +class MaaConfig_Run(BaseModel): + TaskTransitionMethod: Optional[Literal["NoAction", "ExitGame", "ExitEmulator"]] = ( + Field(default=None, description="简洁任务间切换方式") + ) + ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") + RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") + AnnihilationTimeLimit: Optional[int] = Field( + default=None, description="剿灭超时限制" + ) + RoutineTimeLimit: Optional[int] = Field(default=None, description="日常超时限制") + AnnihilationAvoidWaste: Optional[bool] = Field( + default=None, description="剿灭避免无代理卡浪费理智" + ) + + +class MaaConfig(BaseModel): + Info: Optional[MaaConfig_Info] = Field(default=None, description="脚本基础信息") + Emulator: Optional[MaaConfig_Emulator] = Field( + default=None, description="模拟器配置" + ) + Run: Optional[MaaConfig_Run] = Field(default=None, description="脚本运行配置") + + +class GeneralUserConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="用户名") + Status: Optional[bool] = Field(default=None, description="用户状态") + RemainedDay: Optional[int] = Field(default=None, description="剩余天数") + IfScriptBeforeTask: Optional[bool] = Field( + default=None, description="是否在任务前执行脚本" + ) + ScriptBeforeTask: Optional[str] = Field(default=None, description="任务前脚本路径") + IfScriptAfterTask: Optional[bool] = Field( + default=None, description="是否在任务后执行脚本" + ) + ScriptAfterTask: Optional[str] = Field(default=None, description="任务后脚本路径") + Notes: Optional[str] = Field(default=None, description="备注") + Tag: Optional[str] = Field( + default=None, description="用户标签列表(JSON字符串,TagItem的dict列表)" + ) + + +class GeneralUserConfig_Data(BaseModel): + LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期") + ProxyTimes: Optional[int] = Field(default=None, description="代理次数") + + +class GeneralUserConfig(BaseModel): + Info: Optional[GeneralUserConfig_Info] = Field(default=None, description="用户信息") + Data: Optional[GeneralUserConfig_Data] = Field(default=None, description="用户数据") + Notify: Optional[GeneralUserConfig_Notify] = Field( + default=None, description="单独通知" + ) + + +class GeneralConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="脚本名称") + RootPath: Optional[str] = Field(default=None, description="脚本根目录") + + +class GeneralConfig_Script(BaseModel): + ScriptPath: Optional[str] = Field(default=None, description="脚本可执行文件路径") + Arguments: Optional[str] = Field(default=None, description="脚本启动附加命令参数") + IfTrackProcess: Optional[bool] = Field( + default=None, description="是否追踪脚本子进程" + ) + TrackProcessName: Optional[str] = Field(default=None, description="追踪进程名称") + TrackProcessExe: Optional[str] = Field(default=None, description="追踪进程文件路径") + TrackProcessCmdline: Optional[str] = Field( + default=None, description="追踪进程启动命令行参数" + ) + ConfigPath: Optional[str] = Field(default=None, description="配置文件路径") + ConfigPathMode: Optional[Literal["File", "Folder"]] = Field( + default=None, description="配置文件类型: 单个文件, 文件夹" + ) + UpdateConfigMode: Optional[Literal["Never", "Success", "Failure", "Always"]] = ( + Field( + default=None, + description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时", + ) + ) + LogPath: Optional[str] = Field(default=None, description="日志文件路径") + LogPathFormat: Optional[str] = Field(default=None, description="日志文件名格式") + LogTimeStart: Optional[int] = Field(default=None, description="日志时间戳开始位置") + LogTimeEnd: Optional[int] = Field(default=None, description="日志时间戳结束位置") + LogTimeFormat: Optional[str] = Field(default=None, description="日志时间戳格式") + SuccessLog: Optional[str] = Field(default=None, description="成功时日志") + ErrorLog: Optional[str] = Field(default=None, description="错误时日志") + + +class GeneralConfig_Game(BaseModel): + Enabled: Optional[bool] = Field( + default=None, description="游戏/模拟器相关功能是否启用" + ) + Type: Optional[Literal["Emulator", "Client", "URL"]] = Field( + default=None, description="类型: 模拟器, PC端, URL协议" + ) + Path: Optional[str] = Field(default=None, description="游戏/模拟器程序路径") + URL: Optional[str] = Field(default=None, description="自定义协议URL") + ProcessName: Optional[str] = Field(default=None, description="游戏进程名称") + Arguments: Optional[str] = Field(default=None, description="游戏/模拟器启动参数") + WaitTime: Optional[int] = Field(default=None, description="游戏/模拟器等待启动时间") + IfForceClose: Optional[bool] = Field( + default=None, description="是否强制关闭游戏/模拟器进程" + ) + EmulatorId: Optional[str] = Field(default=None, description="模拟器ID") + EmulatorIndex: Optional[str] = Field(default=None, description="模拟器多开实例索引") + + +class GeneralConfig_Run(BaseModel): + ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") + RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") + RunTimeLimit: Optional[int] = Field(default=None, description="日志超时限制") + + +class GeneralConfig(BaseModel): + Info: Optional[GeneralConfig_Info] = Field(default=None, description="脚本基础信息") + Script: Optional[GeneralConfig_Script] = Field(default=None, description="脚本配置") + Game: Optional[GeneralConfig_Game] = Field(default=None, description="游戏配置") + Run: Optional[GeneralConfig_Run] = Field(default=None, description="运行配置") + + +class MaaEndUserConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="用户名") + Status: Optional[bool] = Field(default=None, description="用户状态") + Id: Optional[str] = Field(default=None, description="用户ID") + Password: Optional[str] = Field(default=None, description="密码") + Mode: Optional[Literal["简洁", "详细"]] = Field( + default=None, description="配置模式" + ) + Resource: Optional[Literal["官服"]] = Field(default=None, description="资源名称") + RemainedDay: Optional[int] = Field(default=None, description="剩余天数") + Notes: Optional[str] = Field(default=None, description="备注") + IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到") + SklandToken: Optional[str] = Field(default=None, description="SklandToken") + Tag: Optional[str] = Field(default=None, description="用户标签信息") + + +class MaaEndUserConfig_Task(BaseModel): + ProtocolSpaceTab: Optional[ + Literal["OperatorProgression", "WeaponProgression", "CrisisDrills"] + ] = Field(default=None, description="协议空间选项卡") + OperatorProgression: Optional[ + Literal["OperatorEXP", "Promotions", "T-Creds", "SkillUp"] + ] = Field(default=None, description="干员养成任务") + WeaponProgression: Optional[Literal["WeaponEXP", "WeaponTune"]] = Field( + default=None, description="武器养成任务" + ) + CrisisDrills: Optional[ + Literal[ + "AdvancedProgression1", + "AdvancedProgression2", + "AdvancedProgression3", + "AdvancedProgression4", + "AdvancedProgression5", + ] + ] = Field(default=None, description="危境预演任务") + RewardsSetOption: Optional[Literal["RewardsSetA", "RewardsSetB"]] = Field( + default=None, description="奖励套组选项" + ) + + +class MaaEndUserConfig_Notify(BaseModel): + Enabled: Optional[bool] = Field(default=None, description="是否启用通知") + IfSendStatistic: Optional[bool] = Field( + default=None, description="是否发送统计信息" + ) + IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件") + ToAddress: Optional[str] = Field(default=None, description="收件地址") + IfServerChan: Optional[bool] = Field(default=None, description="是否启用Server酱") + ServerChanKey: Optional[str] = Field(default=None, description="Server酱密钥") + + +class MaaEndUserConfig(BaseModel): + Info: Optional[MaaEndUserConfig_Info] = Field(default=None, description="用户信息") + Task: Optional[MaaEndUserConfig_Task] = Field(default=None, description="任务配置") + Notify: Optional[MaaEndUserConfig_Notify] = Field( + default=None, description="通知配置" + ) + + +class MaaEndConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="脚本名称") + Path: Optional[str] = Field(default=None, description="脚本路径") + + +class MaaEndConfig_Run(BaseModel): + RunTimeLimit: Optional[int] = Field( + default=None, description="运行时间限制(分钟)" + ) + ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") + RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") + + +class MaaEndConfig_Game(BaseModel): + ControllerType: Optional[ + Literal["Win32-Window", "Win32-Window-Background", "Win32-Front", "ADB"] + ] = Field(default=None, description="控制器类型") + Path: Optional[str] = Field(default=None, description="终末地客户端路径") + Arguments: Optional[str] = Field(default=None, description="游戏启动参数") + WaitTime: Optional[int] = Field(default=None, description="游戏等待时间") + EmulatorId: Optional[str] = Field(default=None, description="模拟器ID") + EmulatorIndex: Optional[str] = Field(default=None, description="模拟器索引") + CloseOnFinish: Optional[bool] = Field(default=None, description="结束后关闭游戏") + + +class MaaEndConfig(BaseModel): + Info: Optional[MaaEndConfig_Info] = Field(default=None, description="脚本信息") + Run: Optional[MaaEndConfig_Run] = Field(default=None, description="运行配置") + Game: Optional[MaaEndConfig_Game] = Field(default=None, description="游戏配置") + + +class SrcUserConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="用户名称") + Status: Optional[bool] = Field(default=None, description="是否启用") + Id: Optional[str] = Field(default=None, description="用户ID") + Password: Optional[str] = Field(default=None, description="密码") + Mode: Optional[Literal["简洁", "详细"]] = Field( + default=None, description="脚本模式" + ) + Server: Optional[ + Literal[ + "CN-Official", + "CN-Bilibili", + "VN-Official", + "OVERSEA-America", + "OVERSEA-Asia", + "OVERSEA-Europe", + "OVERSEA-TWHKMO", + ] + ] = Field(default=None, description="游戏服务器") + RemainedDay: Optional[int] = Field(default=None, description="剩余天数") + Notes: Optional[str] = Field(default=None, description="备注") + Tag: Optional[str] = Field(default=None, description="用户标签信息") + + +class SrcUserConfig_Stage(BaseModel): + Channel: Literal["Relic", "Materials", "Ornament"] | None = Field( + default=None, description="关卡通道" + ) + Relic: ( + Literal[ + "-", + "Cavern_of_Corrosion_Path_of_Possession", + "Cavern_of_Corrosion_Path_of_Hidden_Salvation", + "Cavern_of_Corrosion_Path_of_Thundersurge", + "Cavern_of_Corrosion_Path_of_Aria", + "Cavern_of_Corrosion_Path_of_Uncertainty", + "Cavern_of_Corrosion_Path_of_Cavalier", + "Cavern_of_Corrosion_Path_of_Dreamdive", + "Cavern_of_Corrosion_Path_of_Darkness", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers", + "Cavern_of_Corrosion_Path_of_Conflagration", + "Cavern_of_Corrosion_Path_of_Holy_Hymn", + "Cavern_of_Corrosion_Path_of_Providence", + "Cavern_of_Corrosion_Path_of_Drifting", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch", + "Cavern_of_Corrosion_Path_of_Gelid_Wind", + ] + | None + ) = Field(default=None, description="遗器关卡") + Materials: ( + Literal[ + "-", + "Calyx_Golden_Memories_Planarcadia", + "Calyx_Golden_Aether_Planarcadia", + "Calyx_Golden_Treasures_Planarcadia", + "Calyx_Golden_Memories_Amphoreus", + "Calyx_Golden_Aether_Amphoreus", + "Calyx_Golden_Treasures_Amphoreus", + "Calyx_Golden_Memories_Penacony", + "Calyx_Golden_Aether_Penacony", + "Calyx_Golden_Treasures_Penacony", + "Calyx_Golden_Memories_The_Xianzhou_Luofu", + "Calyx_Golden_Aether_The_Xianzhou_Luofu", + "Calyx_Golden_Treasures_The_Xianzhou_Luofu", + "Calyx_Golden_Memories_Jarilo_VI", + "Calyx_Golden_Aether_Jarilo_VI", + "Calyx_Golden_Treasures_Jarilo_VI", + "Calyx_Crimson_Destruction_Herta_StorageZone", + "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", + "Calyx_Crimson_Preservation_Herta_SupplyZone", + "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", + "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", + "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", + "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", + "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", + "Calyx_Crimson_Erudition_Jarilo_RivetTown", + "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", + "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", + "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", + "Calyx_Crimson_Nihility_Jarilo_GreatMine", + "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", + "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", + "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", + "Stagnant_Shadow_Quanta", + "Stagnant_Shadow_Gust", + "Stagnant_Shadow_Fulmination", + "Stagnant_Shadow_Blaze", + "Stagnant_Shadow_Spike", + "Stagnant_Shadow_Rime", + "Stagnant_Shadow_Mirage", + "Stagnant_Shadow_Icicle", + "Stagnant_Shadow_Doom", + "Stagnant_Shadow_Puppetry", + "Stagnant_Shadow_Abomination", + "Stagnant_Shadow_Scorch", + "Stagnant_Shadow_Celestial", + "Stagnant_Shadow_Perdition", + "Stagnant_Shadow_Nectar", + "Stagnant_Shadow_Roast", + "Stagnant_Shadow_Ire", + "Stagnant_Shadow_Duty", + "Stagnant_Shadow_Timbre", + "Stagnant_Shadow_Mechwolf", + "Stagnant_Shadow_Gloam", + "Stagnant_Shadow_Sloggyre", + "Stagnant_Shadow_Gelidmoon", + "Stagnant_Shadow_Deepsheaf", + "Stagnant_Shadow_Cinders", + "Stagnant_Shadow_Sirens", + "Stagnant_Shadow_Ashes", + "Stagnant_Shadow_Soundburst", + ] + | None + ) = Field(default=None, description="材料关卡") + Ornament: ( + Literal[ + "-", + "Divergent_Universe_Within_the_West_Wind", + "Divergent_Universe_Moonlit_Blood", + "Divergent_Universe_Unceasing_Strife", + "Divergent_Universe_Famished_Worker", + "Divergent_Universe_Eternal_Comedy", + "Divergent_Universe_To_Sweet_Dreams", + "Divergent_Universe_Pouring_Blades", + "Divergent_Universe_Fruit_of_Evil", + "Divergent_Universe_Permafrost", + "Divergent_Universe_Gentle_Words", + "Divergent_Universe_Smelted_Heart", + "Divergent_Universe_Untoppled_Walls", + ] + | None + ) = Field(default=None, description="饰品关卡") + ExtractReservedTrailblazePower: Optional[bool] = Field( + default=None, description="使用储备开拓力" + ) + UseFuel: Optional[bool] = Field(default=None, description="使用燃料") + FuelReserve: Optional[int] = Field(default=None, description="保留的燃料数量") + EchoOfWar: Optional[str] = Field(default=None, description="历战余响关卡") + SimulatedUniverseWorld: Optional[str] = Field( + default=None, description="模拟宇宙关卡" + ) + + +class SrcUserConfig_Data(BaseModel): + LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期") + ProxyTimes: Optional[int] = Field(default=None, description="代理次数") + IfPassCheck: Optional[bool] = Field(default=None, description="是否通过检查") + + +class SrcUserConfig_Notify(BaseModel): + Enabled: Optional[bool] = Field(default=None, description="是否启用通知") + IfSendStatistic: Optional[bool] = Field( + default=None, description="是否发送统计信息" + ) + IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件") + ToAddress: Optional[str] = Field(default=None, description="收件地址") + IfServerChan: Optional[bool] = Field(default=None, description="是否启用Server酱") + ServerChanKey: Optional[str] = Field(default=None, description="Server酱密钥") + + +class SrcUserConfig(BaseModel): + Info: Optional[SrcUserConfig_Info] = Field(default=None, description="基础信息") + Stage: Optional[SrcUserConfig_Stage] = Field(default=None, description="关卡配置") + Data: Optional[SrcUserConfig_Data] = Field(default=None, description="用户数据") + Notify: Optional[SrcUserConfig_Notify] = Field(default=None, description="单独通知") + + +class SrcConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="SRC脚本名称") + Path: Optional[str] = Field(default=None, description="SRC路径") + + +class SrcConfig_Emulator(BaseModel): + Id: Optional[str] = Field(default=None, description="模拟器ID") + Index: Optional[str] = Field(default=None, description="模拟器索引") + + +class SrcConfig_Run(BaseModel): + TaskTransitionMethod: Optional[Literal["ExitGame", "ExitEmulator"]] = Field( + default=None, description="任务切换方式" + ) + ProxyTimesLimit: Optional[int] = Field(default=None, description="代理次数限制") + RunTimesLimit: Optional[int] = Field(default=None, description="运行次数限制") + RunTimeLimit: Optional[int] = Field( + default=None, description="运行时间限制(分钟)" + ) + + +class SrcConfig(BaseModel): + Info: Optional[SrcConfig_Info] = Field(default=None, description="脚本基础信息") + Emulator: Optional[SrcConfig_Emulator] = Field( + default=None, description="模拟器配置" + ) + Run: Optional[SrcConfig_Run] = Field(default=None, description="脚本运行配置") + + +class PlanIndexItem(BaseModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["MaaPlanConfig"] = Field(..., description="配置类型") + + +class MaaPlanConfig_Info(BaseModel): + Name: Optional[str] = Field(default=None, description="计划表名称") + Mode: Optional[Literal["ALL", "Weekly"]] = Field( + default=None, description="计划表模式" + ) + + +class MaaPlanConfig_Item(BaseModel): + MedicineNumb: Optional[int] = Field(default=None, description="吃理智药") + SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field( + None, description="连战次数" + ) + Stage: Optional[str] = Field(default=None, description="关卡选择") + Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1") + Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2") + Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3") + Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡") + + +class MaaPlanConfig(BaseModel): + Info: Optional[MaaPlanConfig_Info] = Field(default=None, description="基础信息") + ALL: Optional[MaaPlanConfig_Item] = Field(default=None, description="全局") + Monday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周一") + Tuesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周二") + Wednesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周三") + Thursday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周四") + Friday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周五") + Saturday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周六") + Sunday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周日") + + +class HistoryIndexItem(BaseModel): + date: str = Field(..., description="日期") + status: Literal["DONE", "ERROR"] = Field(..., description="状态") + jsonFile: str = Field(..., description="对应JSON文件") + + +class HistoryData(BaseModel): + index: Optional[List[HistoryIndexItem]] = Field( + default=None, description="历史记录索引列表" + ) + recruit_statistics: Optional[Dict[str, int]] = Field( + default=None, description="公招统计数据, key为星级, value为对应的公招数量" + ) + drop_statistics: Optional[Dict[str, Dict[str, int]]] = Field( + default=None, + description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }", + ) + error_info: Optional[Dict[str, str]] = Field( + default=None, description="报错信息, key为时间戳, value为错误描述" + ) + log_content: Optional[str] = Field( + default=None, description="日志内容, 仅在提取单条历史记录数据时返回" + ) + + +class ScriptCreateIn(BaseModel): + type: Literal["MAA", "SRC", "General", "MaaEnd"] = Field( + ..., description="脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本" + ) + scriptId: str | None = Field( + default=None, description="直接从该脚本ID复制创建, 仅在复制创建时使用" + ) + + +class ScriptCreateOut(OutBase): + scriptId: str = Field(..., description="新创建的脚本ID") + data: Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig] = Field( + ..., description="脚本配置数据" + ) + + +class ScriptGetIn(BaseModel): + scriptId: Optional[str] = Field( + default=None, description="脚本ID, 未携带时表示获取所有脚本数据" + ) + + +class ScriptGetOut(OutBase): + index: List[ScriptIndexItem] = Field(..., description="脚本索引列表") + data: Dict[str, Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig]] = Field( + ..., description="脚本数据字典, key来自于index列表的uid" + ) + + +class ScriptUpdateIn(BaseModel): + scriptId: str = Field(..., description="脚本ID") + data: Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig] = Field( + ..., description="脚本更新数据" + ) + + +class ScriptDeleteIn(BaseModel): + scriptId: str = Field(..., description="脚本ID") + + +class ScriptReorderIn(BaseModel): + indexList: List[str] = Field(..., description="脚本ID列表, 按新顺序排列") + + +class ScriptFileIn(BaseModel): + scriptId: str = Field(..., description="脚本ID") + jsonFile: str = Field(..., description="配置文件路径") + + +class ScriptUrlIn(BaseModel): + scriptId: str = Field(..., description="脚本ID") + url: str = Field(..., description="配置文件URL") + + +class ScriptUploadIn(BaseModel): + scriptId: str = Field(..., description="脚本ID") + config_name: str = Field(..., description="配置名称") + author: str = Field(..., description="作者") + description: str = Field(..., description="描述") + + +class UserInBase(BaseModel): + scriptId: str = Field(..., description="所属脚本ID") + + +class UserGetIn(UserInBase): + userId: Optional[str] = Field( + default=None, description="用户ID, 未携带时表示获取所有用户数据" + ) + + +class UserGetOut(OutBase): + index: List[UserIndexItem] = Field(..., description="用户索引列表") + data: Dict[ + str, Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] + ] = Field(..., description="用户数据字典, key来自于index列表的uid") + + +class UserCreateOut(OutBase): + userId: str = Field(..., description="新创建的用户ID") + data: Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] = ( + Field(..., description="用户配置数据") + ) + + +class UserUpdateIn(UserInBase): + userId: str = Field(..., description="用户ID") + data: Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] = ( + Field(..., description="用户更新数据") + ) + + +class UserDeleteIn(UserInBase): + userId: str = Field(..., description="用户ID") + + +class UserReorderIn(UserInBase): + indexList: List[str] = Field(..., description="用户ID列表, 按新顺序排列") + + +class UserSetIn(UserInBase): + userId: str = Field(..., description="用户ID") + jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件") + + +class EmulatorGetIn(BaseModel): + emulatorId: Optional[str] = Field( + default=None, description="模拟器ID, 未携带时表示获取所有模拟器数据" + ) + + +class EmulatorGetOut(OutBase): + index: List[EmulatorConfigIndexItem] = Field(..., description="模拟器索引列表") + data: Dict[str, EmulatorConfig] = Field( + ..., description="模拟器数据字典, key来自于index列表的uid" + ) + + +class EmulatorCreateOut(OutBase): + emulatorId: str = Field(..., description="新创建的模拟器 ID") + data: EmulatorConfig = Field(..., description="模拟器配置数据") + + +class EmulatorUpdateIn(BaseModel): + emulatorId: str = Field(..., description="模拟器 ID") + data: EmulatorConfig = Field(..., description="模拟器更新数据") + + +class EmulatorDeleteIn(BaseModel): + emulatorId: str = Field(..., description="模拟器 ID") + + +class EmulatorReorderIn(BaseModel): + indexList: List[str] = Field(..., description="模拟器 ID列表, 按新顺序排列") + + +class EmulatorOperateIn(BaseModel): + emulatorId: str = Field(..., description="模拟器 ID") + operate: Literal["open", "close", "show"] = Field(..., description="操作类型") + index: str = Field(..., description="模拟器索引") + + +class DeviceStatus(BaseModel): + """设备状态枚举""" + + ONLINE: int = Field(default=0, description="设备在线") + OFFLINE: int = Field(default=1, description="设备离线") + STARTING: int = Field(default=2, description="设备开启中") + CLOSEING: int = Field(default=3, description="设备关闭中") + ERROR: int = Field(default=4, description="错误") + NOT_FOUND: int = Field(default=5, description="未找到设备") + UNKNOWN: int = Field(default=10, description="未知状态") + + +class DeviceInfo(BaseModel): + """设备信息""" + + title: str = Field(..., description="设备标题/名称") + status: int = Field(..., description="设备状态, 参考DeviceStatus枚举值") + adb_address: str = Field(..., description="ADB连接地址") + + +class EmulatorStatusOut(OutBase): + data: Dict[str, Dict[str, DeviceInfo]] = Field( + ..., + description="模拟器状态信息, 外层key为模拟器ID, 内层key为设备索引, value为设备信息", + ) + + +class EmulatorSearchResult(BaseModel): + type: str = Field(..., description="模拟器类型") + path: str = Field(..., description="模拟器路径") + name: str = Field(..., description="模拟器名称") + + +class EmulatorSearchOut(OutBase): + emulators: list[EmulatorSearchResult] = Field( + default_factory=list, description="搜索到的模拟器列表" + ) + + +class WebhookInBase(BaseModel): + scriptId: Optional[str] = Field( + default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带" + ) + userId: Optional[str] = Field( + default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带" + ) + + +class WebhookGetIn(WebhookInBase): + webhookId: Optional[str] = Field( + default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据" + ) + + +class WebhookGetOut(OutBase): + index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表") + data: Dict[str, Webhook] = Field( + ..., description="Webhook数据字典, key来自于index列表的uid" + ) + + +class WebhookCreateOut(OutBase): + webhookId: str = Field(..., description="新创建的Webhook ID") + data: Webhook = Field(..., description="Webhook配置数据") + + +class WebhookUpdateIn(WebhookInBase): + webhookId: str = Field(..., description="Webhook ID") + data: Webhook = Field(..., description="Webhook更新数据") + + +class WebhookDeleteIn(WebhookInBase): + webhookId: str = Field(..., description="Webhook ID") + + +class WebhookReorderIn(WebhookInBase): + indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列") + + +class WebhookTestIn(WebhookInBase): + data: Webhook = Field(..., description="Webhook配置数据") + + +class PlanCreateIn(BaseModel): + type: Literal["MaaPlan"] + + +class PlanCreateOut(OutBase): + planId: str = Field(..., description="新创建的计划ID") + data: MaaPlanConfig = Field(..., description="计划配置数据") + + +class PlanGetIn(BaseModel): + planId: Optional[str] = Field( + default=None, description="计划ID, 未携带时表示获取所有计划数据" + ) + + +class PlanGetOut(OutBase): + index: List[PlanIndexItem] = Field(..., description="计划索引列表") + data: Dict[str, MaaPlanConfig] = Field(..., description="计划列表或单个计划数据") + + +class PlanUpdateIn(BaseModel): + planId: str = Field(..., description="计划ID") + data: MaaPlanConfig = Field(..., description="计划更新数据") + + +class PlanDeleteIn(BaseModel): + planId: str = Field(..., description="计划ID") + + +class PlanReorderIn(BaseModel): + indexList: List[str] = Field(..., description="计划ID列表, 按新顺序排列") + + +class QueueCreateOut(OutBase): + queueId: str = Field(..., description="新创建的队列ID") + data: QueueConfig = Field(..., description="队列配置数据") + + +class QueueGetIn(BaseModel): + queueId: Optional[str] = Field( + default=None, description="队列ID, 未携带时表示获取所有队列数据" + ) + + +class QueueGetOut(OutBase): + index: List[QueueIndexItem] = Field(..., description="队列索引列表") + data: Dict[str, QueueConfig] = Field( + ..., description="队列数据字典, key来自于index列表的uid" + ) + + +class QueueUpdateIn(BaseModel): + queueId: str = Field(..., description="队列ID") + data: QueueConfig = Field(..., description="队列更新数据") + + +class QueueDeleteIn(BaseModel): + queueId: str = Field(..., description="队列ID") + + +class QueueReorderIn(BaseModel): + indexList: List[str] = Field(..., description="按新顺序排列的调度队列UID列表") + + +class QueueSetInBase(BaseModel): + queueId: str = Field(..., description="所属队列ID") + + +class TimeSetGetIn(QueueSetInBase): + timeSetId: Optional[str] = Field( + default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据" + ) + + +class TimeSetGetOut(OutBase): + index: List[TimeSetIndexItem] = Field(..., description="时间设置索引列表") + data: Dict[str, TimeSet] = Field( + ..., description="时间设置数据字典, key来自于index列表的uid" + ) + + +class TimeSetCreateOut(OutBase): + timeSetId: str = Field(..., description="新创建的时间设置ID") + data: TimeSet = Field(..., description="时间设置配置数据") + + +class TimeSetUpdateIn(QueueSetInBase): + timeSetId: str = Field(..., description="时间设置ID") + data: TimeSet = Field(..., description="时间设置更新数据") + + +class TimeSetDeleteIn(QueueSetInBase): + timeSetId: str = Field(..., description="时间设置ID") + + +class TimeSetReorderIn(QueueSetInBase): + indexList: List[str] = Field(..., description="时间设置ID列表, 按新顺序排列") + + +class QueueItemGetIn(QueueSetInBase): + queueItemId: Optional[str] = Field( + default=None, description="队列项ID, 未携带时表示获取所有队列项数据" + ) + + +class QueueItemGetOut(OutBase): + index: List[QueueItemIndexItem] = Field(..., description="队列项索引列表") + data: Dict[str, QueueItem] = Field( + ..., description="队列项数据字典, key来自于index列表的uid" + ) + + +class QueueItemCreateOut(OutBase): + queueItemId: str = Field(..., description="新创建的队列项ID") + data: QueueItem = Field(..., description="队列项配置数据") + + +class QueueItemUpdateIn(QueueSetInBase): + queueItemId: str = Field(..., description="队列项ID") + data: QueueItem = Field(..., description="队列项更新数据") + + +class QueueItemDeleteIn(QueueSetInBase): + queueItemId: str = Field(..., description="队列项ID") + + +class QueueItemReorderIn(QueueSetInBase): + indexList: List[str] = Field(..., description="队列项ID列表, 按新顺序排列") + + +class DispatchIn(BaseModel): + taskId: str = Field( + ..., + description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID", + ) + + +class TaskCreateIn(DispatchIn): + mode: Literal["AutoProxy", "ManualReview", "ScriptConfig"] = Field( + ..., description="任务模式" + ) + + +class TaskCreateOut(OutBase): + taskId: str = Field(..., description="新创建的任务ID") + + +class WebSocketMessage(BaseModel): + id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程") + type: Literal["Update", "Message", "Info", "Signal"] = Field( + ..., + description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号", + ) + data: Dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定") + + +class PowerIn(BaseModel): + signal: Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] = Field(..., description="电源操作信号") + + +class PowerOut(OutBase): + signal: Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] = Field(..., description="电源操作信号") + + +class HistorySearchIn(BaseModel): + mode: Literal["DAILY", "WEEKLY", "MONTHLY"] = Field(..., description="合并模式") + start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD") + end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD") + + +class HistorySearchOut(OutBase): + data: Dict[str, Dict[str, HistoryData]] = Field( + ..., + description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }", + ) + + +class HistoryDataGetIn(BaseModel): + jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件") + + +class HistoryDataGetOut(OutBase): + data: HistoryData = Field(..., description="历史记录数据") + + +class ToolsGetOut(OutBase): + data: ToolsConfig = Field(..., description="工具配置数据") + + +class ToolsUpdateIn(BaseModel): + data: ToolsConfig = Field(..., description="工具配置需要更新的数据") + + +class SettingGetOut(OutBase): + data: GlobalConfig = Field(..., description="全局设置数据") + + +class SettingUpdateIn(BaseModel): + data: GlobalConfig = Field(..., description="全局设置需要更新的数据") + + +class UpdateCheckIn(BaseModel): + current_version: str = Field(..., description="当前前端版本号") + if_force: bool = Field(default=False, description="是否强制拉取更新信息") + + +class UpdateCheckOut(OutBase): + if_need_update: bool = Field(..., description="是否需要更新前端") + latest_version: str = Field(..., description="最新前端版本号") + update_info: Dict[str, List[str]] = Field(..., description="版本更新信息字典") + + +# ============== WebSocket 调试相关模型 ============== + + +class WSClientCreateIn(BaseModel): + """创建 WebSocket 客户端请求""" + + name: str = Field(..., description="客户端名称,用于标识") + url: str = Field( + ..., description="WebSocket 服务器地址,如 ws://localhost:5140/path" + ) + ping_interval: float = Field(default=15.0, description="心跳发送间隔(秒)") + ping_timeout: float = Field(default=30.0, description="心跳超时时间(秒)") + reconnect_interval: float = Field(default=5.0, description="重连间隔(秒)") + max_reconnect_attempts: int = Field( + default=-1, description="最大重连次数,-1为无限" + ) + + +class WSClientCreateOut(OutBase): + """创建客户端响应""" + + data: Optional[Dict[str, Any]] = Field(default=None, description="返回数据") + + +class WSClientConnectIn(BaseModel): + """连接请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientDisconnectIn(BaseModel): + """断开连接请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientRemoveIn(BaseModel): + """删除客户端请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientSendIn(BaseModel): + """发送消息请求""" + + name: str = Field(..., description="客户端名称") + message: Dict[str, Any] = Field(..., description="要发送的 JSON 消息") + + +class WSClientSendJsonIn(BaseModel): + """发送自定义 JSON 消息请求""" + + name: str = Field(..., description="客户端名称") + msg_id: str = Field(default="Client", description="消息 ID") + msg_type: str = Field(..., description="消息类型") + data: Dict[str, Any] = Field(default_factory=dict, description="消息数据") + + +class WSClientAuthIn(BaseModel): + """发送认证请求""" + + name: str = Field(..., description="客户端名称") + token: str = Field(..., description="认证 Token") + auth_type: str = Field(default="auth", description="认证消息类型") + extra_data: Optional[Dict[str, Any]] = Field( + default=None, description="额外认证数据" + ) + + +class WSClientStatusIn(BaseModel): + """获取客户端状态请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientStatusOut(OutBase): + """客户端状态响应""" + + data: Optional[Dict[str, Any]] = Field(default=None, description="状态数据") + + +class WSClientListOut(OutBase): + """客户端列表响应""" + + data: Optional[Dict[str, Any]] = Field(default=None, description="客户端列表") + + +class WSMessageHistoryOut(OutBase): + """消息历史响应""" + + data: Optional[Dict[str, Any]] = Field(default=None, description="消息历史") + + +class WSClearHistoryIn(BaseModel): + """清空消息历史请求""" + + name: Optional[str] = Field(default=None, description="客户端名称,为空则清空所有") + + +class WSCommandsOut(OutBase): + """可用命令列表响应""" + + data: Optional[Dict[str, Any]] = Field(default=None, description="命令列表") diff --git a/app/models/general.py b/app/models/general.py new file mode 100644 index 00000000..6360dcc8 --- /dev/null +++ b/app/models/general.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import json +import uuid +from datetime import datetime +from pathlib import Path +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, Field, field_validator + +from app.utils.constants import UTC4 +from .config_base import MultipleConfig +from .common import Webhook +from .pydantic_base import PydanticConfigBase +from .type import UrlString + + +class GeneralUserConfig(PydanticConfigBase): + """通用脚本用户配置""" + + class InfoModel(BaseModel): + Name: str = "新用户" + Status: bool = True + RemainedDay: int = Field(default=-1, ge=-1, le=9999) + IfScriptBeforeTask: bool = False + ScriptBeforeTask: str = str(Path.cwd()) + IfScriptAfterTask: bool = False + ScriptAfterTask: str = str(Path.cwd()) + Notes: str = "无" + Tag: str = "[ ]" + + class DataModel(BaseModel): + LastProxyDate: str = "2000-01-01" + ProxyTimes: int = Field(default=0, ge=0, le=9999) + + @field_validator("LastProxyDate", mode="before") + @classmethod + def _normalize_ymd(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + try: + datetime.strptime(text, "%Y-%m-%d") + return text + except ValueError: + return "2000-01-01" + + class NotifyModel(BaseModel): + Enabled: bool = False + IfSendStatistic: bool = False + IfSendMail: bool = False + ToAddress: str = "" + IfServerChan: bool = False + ServerChanKey: str = "" + + Info: InfoModel = Field(default_factory=InfoModel) + Data: DataModel = Field(default_factory=DataModel) + Notify: NotifyModel = Field(default_factory=NotifyModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + if (group, name) == ("Info", "Tag"): + return self.getTags() + return value + + def get(self, group: str, name: str) -> Any: + if (group, name) == ("Info", "Tag"): + return self.getTags() + return super().get(group, name) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data = await super().toDict(if_decrypt, regenerate_uuids) + info = data.get("Info") + if isinstance(info, dict): + info["Tag"] = self.getTags() + return data + + def getTags(self) -> str: # noqa: N802 + """生成通用用户标签列表""" + tags: list[dict[str, str]] = [] + + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"任务:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "任务:未代理", "color": "orange"}) + + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限", + "color": tag_color, + } + ) + + notes = self.get("Info", "Notes") + tags.append( + { + "text": f"备注:{notes}" + if len(notes) <= 20 + else f"备注:{notes[:20]}...", + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + +class GeneralConfig(PydanticConfigBase): + """通用配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + Name: str = "新通用脚本" + RootPath: str = str(Path.cwd()) + + class ScriptModel(BaseModel): + ScriptPath: str = str(Path.cwd()) + Arguments: str = "" + IfTrackProcess: bool = False + TrackProcessName: str = "" + TrackProcessExe: str = "" + TrackProcessCmdline: str = "" + ConfigPath: str = str(Path.cwd()) + ConfigPathMode: Literal["File", "Folder"] = "File" + UpdateConfigMode: Literal["Never", "Success", "Failure", "Always"] = "Never" + LogPath: str = str(Path.cwd()) + LogPathFormat: str = "%Y-%m-%d" + LogTimeStart: int = Field(default=1, ge=1, le=9999) + LogTimeEnd: int = Field(default=1, ge=1, le=9999) + LogTimeFormat: str = "%Y-%m-%d %H:%M:%S" + SuccessLog: str = "" + ErrorLog: str = "" + + class GameModel(BaseModel): + Enabled: bool = False + Type: Literal["Emulator", "Client", "URL"] = "Emulator" + Path: str = str(Path.cwd()) + URL: UrlString = "" + ProcessName: str = "" + Arguments: str = "" + WaitTime: int = Field(default=0, ge=0, le=9999) + IfForceClose: bool = False + EmulatorId: str = "-" + EmulatorIndex: str = "-" + + class RunModel(BaseModel): + ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) + RunTimesLimit: int = Field(default=3, ge=1, le=9999) + RunTimeLimit: int = Field(default=10, ge=1, le=9999) + + Info: InfoModel = Field(default_factory=InfoModel) + Script: ScriptModel = Field(default_factory=ScriptModel) + Game: GameModel = Field(default_factory=GameModel) + Run: RunModel = Field(default_factory=RunModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.UserData = MultipleConfig([GeneralUserConfig]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + + if (group, name) != ("Game", "EmulatorId"): + return value + + if value == "-": + return "-" + if not isinstance(value, str): + return "-" + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return "-" + + emulator_config = self.related_config.get("EmulatorConfig") + if emulator_config is None or uid not in emulator_config: + return "-" + + return str(uid) + + +__all__ = ["GeneralUserConfig", "GeneralConfig"] diff --git a/app/models/global_config.py b/app/models/global_config.py new file mode 100644 index 00000000..8ef3ece3 --- /dev/null +++ b/app/models/global_config.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import calendar +import json +import uuid +from datetime import datetime +from typing import Any, Callable, Literal + +from pydantic import BaseModel, Field, field_validator + +from app.utils.constants import MATERIALS_MAP, RESOURCE_STAGE_INFO, UTC8 +from .config_base import MultipleConfig +from .dto import TagItem +from .common import EmulatorConfig, QueueConfig, QueueItem, Webhook +from .general import GeneralConfig +from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from .maaend import MaaEndConfig +from .pydantic_base import PydanticConfigBase +from .src import SrcConfig +from .type import ( + EncryptedString, + JsonDictString, + JsonListString, + KeyboardKeyString, + UrlString, + YmdHmsString, +) + + +class ToolsConfig(PydanticConfigBase): + """工具配置""" + + class ArknightsPCModel(BaseModel): + Enabled: bool = False + PauseKey: KeyboardKeyString = "f10" + SelectDeployedKey: KeyboardKeyString = "w" + UseSkillKey: KeyboardKeyString = "r" + RetreatKey: KeyboardKeyString = "t" + NextFrameKey: KeyboardKeyString = "f" + AnotherQuitKey: KeyboardKeyString = "space" + Status: str = "-" + + ArknightsPC: ArknightsPCModel = Field(default_factory=ArknightsPCModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.arknights_pc_running = False + self.arknights_pc_get_connected: Callable[[], bool] = lambda: False + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + if (group, name) == ("ArknightsPC", "Status"): + return self.arknights_pc_status() + return value + + def get(self, group: str, name: str) -> Any: + if (group, name) == ("ArknightsPC", "Status"): + return self.arknights_pc_status() + return super().get(group, name) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data = await super().toDict(if_decrypt, regenerate_uuids) + pc = data.get("ArknightsPC") + if isinstance(pc, dict): + pc["Status"] = self.arknights_pc_status() + return data + + @property + def arknights_pc_connected(self) -> bool: + return self.arknights_pc_get_connected() + + def arknights_pc_status(self) -> str: + if not self.get("ArknightsPC", "Enabled"): + return TagItem(text="未启用", color="gray").model_dump_json() + + if self.arknights_pc_running: + if self.arknights_pc_connected: + return TagItem(text="运行中", color="green").model_dump_json() + return TagItem(text="未连接", color="red").model_dump_json() + + return TagItem(text="已暂停", color="yellow").model_dump_json() + + @property + def arknights_pc_keys(self) -> list[str]: + """获取明日方舟 PC 按键配置""" + + return [ + self.get("ArknightsPC", _) + for _ in ( + "SelectDeployedKey", + "UseSkillKey", + "RetreatKey", + "NextFrameKey", + "AnotherQuitKey", + ) + ] + + +class GlobalConfig(PydanticConfigBase): + """全局配置""" + + class FunctionModel(BaseModel): + HistoryRetentionTime: Literal[7, 15, 30, 60, 90, 180, 365, 0] = 0 + IfAllowSleep: bool = False + IfSilence: bool = False + IfAgreeBilibili: bool = False + IfBlockAd: bool = False + + class VoiceModel(BaseModel): + Enabled: bool = False + Type: Literal["simple", "noisy"] = "simple" + + class StartModel(BaseModel): + IfSelfStart: bool = False + IfMinimizeDirectly: bool = False + + class UIModel(BaseModel): + IfShowTray: bool = False + IfToTray: bool = False + + class NotifyModel(BaseModel): + SendTaskResultTime: Literal["不推送", "任何时刻", "仅失败时"] = "不推送" + IfSendStatistic: bool = False + IfSendSixStar: bool = False + IfPushPlyer: bool = False + IfSendMail: bool = False + IfKoishiSupport: bool = False + KoishiServerAddress: UrlString = "ws://localhost:5140/AUTO_MAS" + KoishiToken: str = "" + SMTPServerAddress: str = "" + AuthorizationCode: EncryptedString = "" + FromAddress: str = "" + ToAddress: str = "" + IfServerChan: bool = False + ServerChanKey: str = "" + + class UpdateModel(BaseModel): + IfAutoUpdate: bool = False + Source: Literal["GitHub", "MirrorChyan", "AutoSite"] = "GitHub" + Channel: Literal["stable", "beta"] = "stable" + ProxyAddress: str = "" + MirrorChyanCDK: EncryptedString = "" + + class DataModel(BaseModel): + UID: str = str(uuid.uuid4()) + LastStatisticsUpload: YmdHmsString = "2000-01-01 00:00:00" + LastStageUpdated: YmdHmsString = "2000-01-01 00:00:00" + StageETag: str = "" + StageData: JsonDictString = "{ }" + Stage: str = "-" + LastNoticeUpdated: YmdHmsString = "2000-01-01 00:00:00" + NoticeETag: str = "" + IfShowNotice: bool = True + Notice: JsonDictString = "{ }" + LastWebConfigUpdated: YmdHmsString = "2000-01-01 00:00:00" + WebConfig: JsonListString = "[ ]" + + @field_validator("UID", mode="before") + @classmethod + def _normalize_uid(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + try: + return str(uuid.UUID(text)) + except (TypeError, ValueError): + return str(uuid.uuid4()) + + Function: FunctionModel = Field(default_factory=FunctionModel) + Voice: VoiceModel = Field(default_factory=VoiceModel) + Start: StartModel = Field(default_factory=StartModel) + UI: UIModel = Field(default_factory=UIModel) + Notify: NotifyModel = Field(default_factory=NotifyModel) + Update: UpdateModel = Field(default_factory=UpdateModel) + Data: DataModel = Field(default_factory=DataModel) + + LEGACY_FIELD_MAP = {("Data", "StageData"): ("Data", "Stage")} + + def __init__(self, **data: Any): + super().__init__(**data) + + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + self.EmulatorConfig = MultipleConfig([EmulatorConfig]) + self.PlanConfig = MultipleConfig([MaaPlanConfig]) + self.ScriptConfig = MultipleConfig( + [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] + ) + self.QueueConfig = MultipleConfig([QueueConfig]) + self.ToolsConfig = ToolsConfig() + + MaaConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + MaaEndConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + SrcConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + GeneralConfig.related_config["EmulatorConfig"] = self.EmulatorConfig + MaaUserConfig.related_config["PlanConfig"] = self.PlanConfig + QueueItem.related_config["ScriptConfig"] = self.ScriptConfig + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + if (group, name) == ("Data", "Stage"): + return self.getStage() + return value + + def get(self, group: str, name: str) -> Any: + if (group, name) == ("Data", "Stage"): + return self.getStage() + return super().get(group, name) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data = await super().toDict(if_decrypt, regenerate_uuids) + cfg_data = data.get("Data") + if isinstance(cfg_data, dict): + cfg_data["Stage"] = self.getStage() + return data + + def getStage(self) -> str: # noqa: N802 + """获取关卡信息""" + + try: + raw_stage_data = json.loads(self.get("Data", "StageData")) + + activity_stage_drop_info: list[dict[str, Any]] = [] + activity_stage_combox: list[dict[str, str]] = [] + + for side_story in raw_stage_data.values(): + if ( + datetime.strptime( + side_story["Activity"]["UtcStartTime"], "%Y/%m/%d %H:%M:%S" + ).replace(tzinfo=UTC8) + < datetime.now(tz=UTC8) + < datetime.strptime( + side_story["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S" + ).replace(tzinfo=UTC8) + ): + for stage in side_story["Stages"]: + activity_stage_combox.append( + {"label": stage["Display"], "value": stage["Value"]} + ) + + if "SSReopen" not in stage["Display"]: + if stage["Drop"] in MATERIALS_MAP: + drop_id = stage["Drop"] + elif "玉" in stage["Drop"]: + drop_id = "30012" + else: + drop_id = "NotFound" + + activity_stage_drop_info.append( + { + "Display": stage["Display"], + "Value": stage["Value"], + "Drop": drop_id, + "DropName": MATERIALS_MAP.get( + stage["Drop"], stage["Drop"] + ), + "Activity": side_story["Activity"], + } + ) + except Exception: + return "{ }" + + stage_data: dict[str, Any] = {"Info": activity_stage_drop_info} + + for day in range(0, 8): + res_stage: list[dict[str, str]] = [] + + for stage in RESOURCE_STAGE_INFO: + stage_days = stage.get("days") + stage_text = stage.get("text") + stage_value = stage.get("value") + if ( + isinstance(stage_days, list) + and isinstance(stage_text, str) + and isinstance(stage_value, str) + and (day in stage_days or day == 0) + ): + res_stage.append({"label": stage_text, "value": stage_value}) + + stage_data[calendar.day_name[day - 1] if day > 0 else "ALL"] = ( + res_stage[0:1] + activity_stage_combox + res_stage[1:] + ) + + return json.dumps(stage_data, ensure_ascii=False) + + +CLASS_BOOK = { + "MAA": MaaConfig, + "MaaPlan": MaaPlanConfig, + "SRC": SrcConfig, + "MaaEnd": MaaEndConfig, + "General": GeneralConfig, +} +"""配置类映射表""" + + +__all__ = ["ToolsConfig", "GlobalConfig", "CLASS_BOOK"] diff --git a/app/models/maa.py b/app/models/maa.py new file mode 100644 index 00000000..e427f0d4 --- /dev/null +++ b/app/models/maa.py @@ -0,0 +1,420 @@ +from __future__ import annotations + +import json +import uuid +from datetime import datetime +from pathlib import Path +from typing import Any, ClassVar, Callable, Literal + +from pydantic import BaseModel, Field, field_validator + +from app.utils.constants import MAA_STAGE_KEY, RESOURCE_STAGE_INFO, UTC4, UTC8 +from .config_base import MultipleConfig +from .common import Webhook +from .pydantic_base import PydanticConfigBase +from .type import EncryptedString, JsonDictString + + +class _ValueProxy: + def __init__(self, value_getter: Callable[[], Any]): + self._value_getter = value_getter + + def getValue(self, if_decrypt: bool = True) -> Any: # noqa: N802 + return self._value_getter() + + +class MaaUserConfig(PydanticConfigBase): + """MAA用户配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + Name: str = "新用户" + Id: str = "" + Password: EncryptedString = "" + Mode: Literal["简洁", "详细"] = "简洁" + StageMode: str = "Fixed" + Server: Literal[ + "Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy" + ] = "Official" + Status: bool = True + RemainedDay: int = Field(default=-1, ge=-1, le=9999) + Annihilation: Literal[ + "Close", + "Annihilation", + "Chernobog@Annihilation", + "LungmenOutskirts@Annihilation", + "LungmenDowntown@Annihilation", + ] = "Annihilation" + InfrastMode: Literal["Normal", "Rotation", "Custom"] = "Normal" + InfrastName: str = "-" + InfrastIndex: str = "-" + Notes: str = "无" + MedicineNumb: int = Field(default=0, ge=0, le=9999) + SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = "0" + Stage: str = "-" + Stage_1: str = "-" + Stage_2: str = "-" + Stage_3: str = "-" + Stage_Remain: str = "-" + IfSkland: bool = False + SklandToken: EncryptedString = "" + Tag: str = "[ ]" + + class DataModel(BaseModel): + LastProxyDate: str = "2000-01-01" + LastSklandDate: str = "2000-01-01" + ProxyTimes: int = Field(default=0, ge=0, le=9999) + IfPassCheck: bool = True + CustomInfrast: JsonDictString = "{ }" + InfrastIndex: str = "0" + + @field_validator("LastProxyDate", "LastSklandDate", mode="before") + @classmethod + def _normalize_ymd(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + try: + datetime.strptime(text, "%Y-%m-%d") + return text + except ValueError: + return "2000-01-01" + + class TaskModel(BaseModel): + IfStartUp: bool = True + IfFight: bool = True + IfInfrast: bool = True + IfRecruit: bool = True + IfMall: bool = True + IfAward: bool = True + IfRoguelike: bool = False + IfReclamation: bool = False + + class NotifyModel(BaseModel): + Enabled: bool = False + IfSendStatistic: bool = False + IfSendSixStar: bool = False + IfSendMail: bool = False + ToAddress: str = "" + IfServerChan: bool = False + ServerChanKey: str = "" + + Info: InfoModel = Field(default_factory=InfoModel) + Data: DataModel = Field(default_factory=DataModel) + Task: TaskModel = Field(default_factory=TaskModel) + Notify: NotifyModel = Field(default_factory=NotifyModel) + + LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = { + ("Data", "InfrastIndex"): ("Info", "InfrastIndex") + } + + def __init__(self, **data: Any): + super().__init__(**data) + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + + if (group, name) == ("Info", "StageMode"): + if value == "Fixed": + return "Fixed" + if not isinstance(value, str): + return "Fixed" + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return "Fixed" + plan_config = self.related_config.get("PlanConfig") + if plan_config is None or uid not in plan_config: + return "Fixed" + return str(uid) + + if (group, name) == ("Info", "InfrastName"): + return self.getInfrastName() + if (group, name) == ("Info", "InfrastIndex"): + return self.getInfrastIndex() + if (group, name) == ("Info", "Tag"): + return self.getTags() + + return value + + def get(self, group: str, name: str) -> Any: + if (group, name) == ("Info", "InfrastName"): + return self.getInfrastName() + if (group, name) == ("Info", "InfrastIndex"): + return self.getInfrastIndex() + if (group, name) == ("Info", "Tag"): + return self.getTags() + return super().get(group, name) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data = await super().toDict(if_decrypt, regenerate_uuids) + info = data.get("Info") + if isinstance(info, dict): + info["InfrastName"] = self.getInfrastName() + info["InfrastIndex"] = self.getInfrastIndex() + info["Tag"] = self.getTags() + return data + + def getInfrastName(self) -> str: # noqa: N802 + if self.get("Info", "InfrastMode") != "Custom": + return "未使用自定义基建模式" + + infrast_data = json.loads(self.get("Data", "CustomInfrast")) + if ( + infrast_data.get("title", "文件标题") != "文件标题" + and infrast_data.get("description", "文件描述") != "文件描述" + ): + return f"{infrast_data['title']} - {infrast_data['description']}" + if infrast_data.get("title", "文件标题") != "文件标题": + return str(infrast_data["title"]) + if infrast_data.get("id", None): + return str(infrast_data["id"]) + return "未命名自定义基建" + + def getInfrastIndex(self) -> str: # noqa: N802 + if self.get("Info", "InfrastMode") != "Custom": + return "-1" + + infrast_data = json.loads(self.get("Data", "CustomInfrast")) + + if len(infrast_data.get("plans", [])) == 0: + return "-1" + + for i, plan in enumerate(infrast_data.get("plans", [])): + for t in plan.get("period", []): + if ( + datetime.strptime(t[0], "%H:%M").time() + <= datetime.now().time() + <= datetime.strptime(t[1], "%H:%M").time() + ): + return str(i) + + return self.get("Data", "InfrastIndex") or "0" + + def getTags(self) -> str: # noqa: N802 + """生成用户标签列表,返回JSON字符串格式的TagItem列表""" + tags: list[dict[str, str]] = [] + + if not self.get("Data", "IfPassCheck"): + tags.append({"text": "人工排查未通过", "color": "red"}) + + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "日常:未代理", "color": "orange"}) + + if self.get("Info", "IfSkland"): + if ( + datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC8).date() + ): + tags.append({"text": "森空岛:已签到", "color": "green"}) + else: + tags.append({"text": "森空岛:未签到", "color": "orange"}) + else: + tags.append({"text": "森空岛:禁用", "color": "red"}) + + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": ( + f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限" + ), + "color": tag_color, + } + ) + + infrast_mode = self.get("Info", "InfrastMode") + if self.get("Task", "IfInfrast"): + if infrast_mode == "Normal": + infrast_text = "基建:常规" + elif infrast_mode == "Rotation": + infrast_text = "基建:轮换" + elif infrast_mode == "Custom": + name = self.getInfrastName() + infrast_text = f"基建:{name if len(name) < 10 else name[:10] + '...'}" + else: + infrast_text = "基建:开启" + tags.append({"text": infrast_text, "color": "purple"}) + else: + tags.append({"text": "基建:关闭", "color": "red"}) + + plan_data: dict[str, str] = { + stage_key: self.get_stage_zh(self.get("Info", stage_key)) + for stage_key in MAA_STAGE_KEY[2:] + } + tag_color = "blue" + if self.get("Info", "StageMode") != "Fixed": + plan = self.related_config["PlanConfig"][ + uuid.UUID(self.get("Info", "StageMode")) + ] + if isinstance(plan, MaaPlanConfig): + plan_data = { + stage_key: self.get_stage_zh( + plan.get_current_info(stage_key).getValue() + ) + for stage_key in MAA_STAGE_KEY[2:] + } + tag_color = "green" + + tags.append({"text": f"主关卡:{plan_data['Stage']}", "color": tag_color}) + backup_stages = [ + plan_data[f"Stage_{i}"] + for i in range(1, 4) + if plan_data[f"Stage_{i}"] != "禁用" + ] + if backup_stages: + tags.append( + {"text": f"备选:{', '.join(backup_stages)}", "color": tag_color} + ) + if plan_data["Stage_Remain"] != "禁用": + tags.append( + {"text": f"剩余:{plan_data['Stage_Remain']}", "color": tag_color} + ) + + notes = self.get("Info", "Notes") + tags.append( + { + "text": f"备注:{notes}" + if len(notes) <= 20 + else f"备注:{notes[:20]}...", + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + @staticmethod + def get_stage_zh(stage: str) -> str: + for stage_info in RESOURCE_STAGE_INFO: + if stage_info.get("value") == stage: + text_value = stage_info.get("text", stage) + text = text_value if isinstance(text_value, str) else stage + return ( + text.replace("经验-6/5", "经验") + .replace("龙门币-6/5", "龙门币") + .replace("红票-5", "红票") + .replace("技能-5", "技能") + .replace("碳-5", "碳") + ) + return stage + + +class MaaConfig(PydanticConfigBase): + """MAA配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + Name: str = "新 MAA 脚本" + Path: str = str(Path.cwd()) + + class EmulatorModel(BaseModel): + Id: str = "-" + Index: str = "-" + + class RunModel(BaseModel): + TaskTransitionMethod: Literal["NoAction", "ExitGame", "ExitEmulator"] = ( + "ExitEmulator" + ) + ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) + RunTimesLimit: int = Field(default=3, ge=1, le=9999) + AnnihilationTimeLimit: int = Field(default=40, ge=1, le=9999) + RoutineTimeLimit: int = Field(default=10, ge=1, le=9999) + AnnihilationAvoidWaste: bool = False + + Info: InfoModel = Field(default_factory=InfoModel) + Emulator: EmulatorModel = Field(default_factory=EmulatorModel) + Run: RunModel = Field(default_factory=RunModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.UserData = MultipleConfig([MaaUserConfig]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + + if (group, name) != ("Emulator", "Id"): + return value + + if value == "-": + return "-" + if not isinstance(value, str): + return "-" + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return "-" + + emulator_config = self.related_config.get("EmulatorConfig") + if emulator_config is None or uid not in emulator_config: + return "-" + + return str(uid) + + +class MaaPlanConfig(PydanticConfigBase): + """MAA计划表配置""" + + class InfoModel(BaseModel): + Name: str = "新 MAA 计划表" + Mode: Literal["ALL", "Weekly"] = "ALL" + + class DayPlanModel(BaseModel): + MedicineNumb: int = Field(default=0, ge=0, le=9999) + SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = "0" + Stage: str = "-" + Stage_1: str = "-" + Stage_2: str = "-" + Stage_3: str = "-" + Stage_Remain: str = "-" + + Info: InfoModel = Field(default_factory=InfoModel) + ALL: DayPlanModel = Field(default_factory=DayPlanModel) + Monday: DayPlanModel = Field(default_factory=DayPlanModel) + Tuesday: DayPlanModel = Field(default_factory=DayPlanModel) + Wednesday: DayPlanModel = Field(default_factory=DayPlanModel) + Thursday: DayPlanModel = Field(default_factory=DayPlanModel) + Friday: DayPlanModel = Field(default_factory=DayPlanModel) + Saturday: DayPlanModel = Field(default_factory=DayPlanModel) + Sunday: DayPlanModel = Field(default_factory=DayPlanModel) + + def get_current_info(self, name: str) -> _ValueProxy: + """获取当前的计划表配置项""" + + if self.get("Info", "Mode") == "ALL": + return _ValueProxy(lambda: getattr(self.ALL, name, "-")) + + if self.get("Info", "Mode") == "Weekly": + today = datetime.now(tz=UTC4).strftime("%A") + plan = getattr(self, today, self.ALL) + return _ValueProxy(lambda: getattr(plan, name, "-")) + + raise ValueError("非法的计划表模式") + + +__all__ = ["MaaPlanConfig", "MaaUserConfig", "MaaConfig"] diff --git a/app/models/maaend.py b/app/models/maaend.py new file mode 100644 index 00000000..db6c990a --- /dev/null +++ b/app/models/maaend.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import json +import uuid +from datetime import datetime +from pathlib import Path +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, Field, field_validator + +from app.utils.constants import MAAEND_STAGE_BOOK, MAAEND_STAGE_WITH_AB, UTC4, UTC8 +from .config_base import MultipleConfig +from .common import Webhook +from .pydantic_base import PydanticConfigBase +from .type import EncryptedString + + +class MaaEndUserConfig(PydanticConfigBase): + """MaaEnd用户配置""" + + class InfoModel(BaseModel): + Name: str = "新用户" + Status: bool = True + Id: str = "" + Password: EncryptedString = "" + Mode: Literal["简洁", "详细"] = "简洁" + Resource: Literal["官服"] = "官服" + RemainedDay: int = Field(default=-1, ge=-1, le=9999) + Notes: str = "无" + IfSkland: bool = False + SklandToken: EncryptedString = "" + Tag: str = "[ ]" + + class TaskModel(BaseModel): + ProtocolSpaceTab: Literal[ + "OperatorProgression", "WeaponProgression", "CrisisDrills" + ] = "OperatorProgression" + OperatorProgression: Literal[ + "OperatorEXP", "Promotions", "T-Creds", "SkillUp" + ] = "OperatorEXP" + WeaponProgression: Literal["WeaponEXP", "WeaponTune"] = "WeaponEXP" + CrisisDrills: Literal[ + "AdvancedProgression1", + "AdvancedProgression2", + "AdvancedProgression3", + "AdvancedProgression4", + "AdvancedProgression5", + ] = "AdvancedProgression1" + RewardsSetOption: Literal["RewardsSetA", "RewardsSetB"] = "RewardsSetA" + + class DataModel(BaseModel): + LastProxyDate: str = "2000-01-01" + ProxyTimes: int = Field(default=0, ge=0, le=9999) + LastProxyStatus: Literal["未知", "成功", "失败"] = "未知" + LastSklandDate: str = "2000-01-01" + IfPassCheck: bool = True + + @field_validator("LastProxyDate", "LastSklandDate", mode="before") + @classmethod + def _normalize_ymd(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + try: + datetime.strptime(text, "%Y-%m-%d") + return text + except ValueError: + return "2000-01-01" + + class NotifyModel(BaseModel): + Enabled: bool = False + IfSendStatistic: bool = False + IfSendMail: bool = False + ToAddress: str = "" + IfServerChan: bool = False + ServerChanKey: str = "" + + Info: InfoModel = Field(default_factory=InfoModel) + Task: TaskModel = Field(default_factory=TaskModel) + Data: DataModel = Field(default_factory=DataModel) + Notify: NotifyModel = Field(default_factory=NotifyModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + if (group, name) == ("Info", "Tag"): + return self.getTags() + return value + + def get(self, group: str, name: str) -> Any: + if (group, name) == ("Info", "Tag"): + return self.getTags() + return super().get(group, name) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data = await super().toDict(if_decrypt, regenerate_uuids) + info = data.get("Info") + if isinstance(info, dict): + info["Tag"] = self.getTags() + return data + + def getTags(self) -> str: # noqa: N802 + """生成用户标签列表,返回JSON字符串格式的TagItem列表""" + tags: list[dict[str, str]] = [] + + if not self.get("Data", "IfPassCheck"): + tags.append({"text": "人工排查未通过", "color": "red"}) + + tags.append( + { + "text": f"上次:{self.get('Data', 'LastProxyStatus')}", + "color": "red" + if self.get("Data", "LastProxyStatus") == "失败" + else "green", + } + ) + + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "日常:未代理", "color": "orange"}) + + if self.get("Info", "IfSkland"): + if ( + datetime.strptime(self.get("Data", "LastSklandDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC8).date() + ): + tags.append({"text": "森空岛:已签到", "color": "green"}) + else: + tags.append({"text": "森空岛:未签到", "color": "orange"}) + else: + tags.append({"text": "森空岛:禁用", "color": "red"}) + + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限", + "color": tag_color, + } + ) + + stage = self.get("Task", self.get("Task", "ProtocolSpaceTab")) + stage_ab = ( + f" - {self.get('Task', 'RewardsSetOption')[-1]}" + if stage in MAAEND_STAGE_WITH_AB + else "" + ) + tags.append({"text": MAAEND_STAGE_BOOK[stage] + stage_ab, "color": "blue"}) + + notes = self.get("Info", "Notes") + tags.append( + { + "text": f"备注:{notes}" + if len(notes) <= 20 + else f"备注:{notes[:20]}...", + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + +class MaaEndConfig(PydanticConfigBase): + """MaaEnd配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + Name: str = "新 MaaEnd 脚本" + Path: str = str(Path.cwd()) + + class RunModel(BaseModel): + RunTimeLimit: int = Field(default=10, ge=1, le=9999) + ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) + RunTimesLimit: int = Field(default=3, ge=1, le=9999) + + class GameModel(BaseModel): + ControllerType: Literal[ + "Win32-Window", "Win32-Front", "Win32-Window-Background", "ADB" + ] = "Win32-Window" + Path: str = str(Path.cwd()) + Arguments: str = "" + WaitTime: int = Field(default=0, ge=0, le=9999) + EmulatorId: str = "-" + EmulatorIndex: str = "-" + CloseOnFinish: bool = True + + Info: InfoModel = Field(default_factory=InfoModel) + Run: RunModel = Field(default_factory=RunModel) + Game: GameModel = Field(default_factory=GameModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.UserData = MultipleConfig([MaaEndUserConfig]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + + if (group, name) != ("Game", "EmulatorId"): + return value + + if value == "-": + return "-" + if not isinstance(value, str): + return "-" + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return "-" + + emulator_config = self.related_config.get("EmulatorConfig") + if emulator_config is None or uid not in emulator_config: + return "-" + + return str(uid) + + +__all__ = ["MaaEndUserConfig", "MaaEndConfig"] diff --git a/app/models/pydantic_base.py b/app/models/pydantic_base.py new file mode 100644 index 00000000..b5f0ab31 --- /dev/null +++ b/app/models/pydantic_base.py @@ -0,0 +1,247 @@ +from __future__ import annotations +# pyright: reportPrivateUsage=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false + +import asyncio +import inspect +from pathlib import Path +from collections.abc import Callable, Coroutine +from typing import Any, ClassVar + +from pydantic import BaseModel, ConfigDict + +from .config_base import ( + MultipleConfig, + _backup_legacy_json_if_needed, + _load_config_with_legacy_migration, + dump_toml, +) + + +SaveMethod = Callable[[], Coroutine[Any, Any, None]] +Slot = Callable[[Any], Any] | Callable[[Any], Coroutine[Any, Any, Any]] + + +class PydanticConfigBase(BaseModel): + """基于 pydantic v2 的配置基类,兼容旧版 ConfigBase 常用接口。""" + + model_config = ConfigDict(extra="allow", validate_assignment=True) + + LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = {} + + def __init__(self, **data: Any): + super().__init__(**data) + self._file: Path | None = None + self._is_locked = False + self._save_methods: list[SaveMethod] = [] + self._bindings: dict[tuple[str, str], list[Slot]] = {} + + @property + def file(self) -> Path | None: + return self._file + + @property + def is_locked(self) -> bool: + return self._is_locked + + def _multiple_config_index(self) -> dict[str, MultipleConfig[Any]]: + result: dict[str, MultipleConfig[Any]] = {} + for name, value in self.__dict__.items(): + if isinstance(value, MultipleConfig): + result[name] = value + return result + + def _group_index(self) -> dict[str, BaseModel]: + result: dict[str, BaseModel] = {} + for name in type(self).model_fields: + value = getattr(self, name, None) + if isinstance(value, BaseModel): + result[name] = value + return result + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + return value + + async def connect(self, path: Path): + if path.suffix != ".toml": + raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") + + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") + + self._file = path + + if not self._file.exists(): + self._file.parent.mkdir(parents=True, exist_ok=True) + self._file.touch() + + data, legacy_json_file = _load_config_with_legacy_migration(self._file) + await self.load(data) + await self.add_save_method(self.save) + _backup_legacy_json_if_needed(self._file, legacy_json_file) + + async def add_save_method(self, save_method: SaveMethod): + if save_method != self.save and save_method not in self._save_methods: + self._save_methods.append(save_method) + + for sub_config in self._multiple_config_index().values(): + await sub_config.add_save_method(save_method) + + async def load(self, data: dict[str, Any]): + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") + + raw: dict[str, Any] = dict(data) + + sub_configs_any = raw.pop("SubConfigsInfo", {}) + sub_configs: dict[str, Any] + if isinstance(sub_configs_any, dict): + sub_configs = dict(sub_configs_any) + else: + sub_configs = {} + + for name, sub_config in self._multiple_config_index().items(): + data_for_sub = sub_configs.get(name) + if isinstance(data_for_sub, dict): + await sub_config.load(data_for_sub) + + for group_name, group_model in self._group_index().items(): + group_data = raw.get(group_name, {}) + if not isinstance(group_data, dict): + group_data = {} + + default_group = type(group_model)() + for field_name in list(type(group_model).model_fields.keys()): + candidate: Any = None + has_value = False + + if field_name in group_data: + candidate = group_data[field_name] + has_value = True + else: + legacy = self.LEGACY_FIELD_MAP.get((group_name, field_name)) + if legacy is not None: + legacy_group, legacy_name = legacy + legacy_data = raw.get(legacy_group, {}) + if isinstance(legacy_data, dict) and legacy_name in legacy_data: + candidate = legacy_data[legacy_name] + has_value = True + + if not has_value: + continue + + candidate = self._normalize_value(group_name, field_name, candidate) + try: + setattr(group_model, field_name, candidate) + except Exception: + setattr(group_model, field_name, getattr(default_group, field_name)) + + if self._file: + await self.save() + + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data: dict[str, Any] = {} + + for group_name, group_model in self._group_index().items(): + data[group_name] = group_model.model_dump(mode="python") + + for name, item in self._multiple_config_index().items(): + if "SubConfigsInfo" not in data: + data["SubConfigsInfo"] = {} + data["SubConfigsInfo"][name] = await item.toDict( + if_decrypt, regenerate_uuids + ) + + return data + + def get(self, group: str, name: str) -> Any: + group_model = self._group_index().get(group) + if group_model is None or not hasattr(group_model, name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + return getattr(group_model, name) + + async def set(self, group: str, name: str, value: Any): + group_model = self._group_index().get(group) + if group_model is None or not hasattr(group_model, name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") + + old_value = getattr(group_model, name) + value = self._normalize_value(group, name, value) + + default_group = type(group_model)() + try: + setattr(group_model, name, value) + except Exception: + setattr(group_model, name, getattr(default_group, name)) + + new_value = getattr(group_model, name) + if old_value != new_value: + await self._emit_bindings(group, name, new_value) + + if self._file: + await self.save() + + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) + + def bind(self, group: str, name: str, slot: Slot): + group_model = self._group_index().get(group) + if group_model is None or not hasattr(group_model, name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") + + key = (group, name) + if key not in self._bindings: + self._bindings[key] = [] + if slot not in self._bindings[key]: + self._bindings[key].append(slot) + + def unbind(self, group: str, name: str, slot: Slot): + group_model = self._group_index().get(group) + if group_model is None or not hasattr(group_model, name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") + + key = (group, name) + if key in self._bindings and slot in self._bindings[key]: + self._bindings[key].remove(slot) + + async def _emit_bindings(self, group: str, name: str, value: Any): + key = (group, name) + slots = self._bindings.get(key, []) + for slot in slots: + if inspect.iscoroutinefunction(slot): + await slot(value) + else: + slot(value) + + async def save(self): + if not self._file: + raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + + self._file.parent.mkdir(parents=True, exist_ok=True) + self._file.write_text( + dump_toml(await self.toDict(if_decrypt=False)), + encoding="utf-8", + ) + + async def lock(self): + self._is_locked = True + for config in self._multiple_config_index().values(): + await config.lock() + + async def unlock(self): + self._is_locked = False + for config in self._multiple_config_index().values(): + await config.unlock() diff --git a/app/models/schema.py b/app/models/schema.py index 016d19a2..6875eff8 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -21,6 +21,7 @@ # Contact: DLmaster_361@163.com +# pyright: reportUnknownVariableType=false, reportGeneralTypeIssues=false from pydantic import BaseModel, Field from typing import Any, Dict, List, Union, Optional, Literal @@ -1079,7 +1080,7 @@ class EmulatorSearchResult(BaseModel): class EmulatorSearchOut(OutBase): - emulators: List[EmulatorSearchResult] = Field( + emulators: list[EmulatorSearchResult] = Field( default_factory=list, description="搜索到的模拟器列表" ) diff --git a/app/models/src.py b/app/models/src.py new file mode 100644 index 00000000..b0387474 --- /dev/null +++ b/app/models/src.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import json +import uuid +from datetime import datetime +from pathlib import Path +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, Field, field_validator + +from app.utils.constants import STARRAIL_STAGE_BOOK, UTC4 +from .config_base import MultipleConfig +from .common import Webhook +from .pydantic_base import PydanticConfigBase +from .type import EncryptedString + + +RELIC_OPTIONS: tuple[str, ...] = ( + "-", + "Cavern_of_Corrosion_Path_of_Possession", + "Cavern_of_Corrosion_Path_of_Hidden_Salvation", + "Cavern_of_Corrosion_Path_of_Thundersurge", + "Cavern_of_Corrosion_Path_of_Aria", + "Cavern_of_Corrosion_Path_of_Uncertainty", + "Cavern_of_Corrosion_Path_of_Cavalier", + "Cavern_of_Corrosion_Path_of_Dreamdive", + "Cavern_of_Corrosion_Path_of_Darkness", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers", + "Cavern_of_Corrosion_Path_of_Conflagration", + "Cavern_of_Corrosion_Path_of_Holy_Hymn", + "Cavern_of_Corrosion_Path_of_Providence", + "Cavern_of_Corrosion_Path_of_Drifting", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch", + "Cavern_of_Corrosion_Path_of_Gelid_Wind", +) + +MATERIAL_OPTIONS: tuple[str, ...] = ( + "-", + "Calyx_Golden_Memories_Planarcadia", + "Calyx_Golden_Aether_Planarcadia", + "Calyx_Golden_Treasures_Planarcadia", + "Calyx_Golden_Memories_Amphoreus", + "Calyx_Golden_Aether_Amphoreus", + "Calyx_Golden_Treasures_Amphoreus", + "Calyx_Golden_Memories_Penacony", + "Calyx_Golden_Aether_Penacony", + "Calyx_Golden_Treasures_Penacony", + "Calyx_Golden_Memories_The_Xianzhou_Luofu", + "Calyx_Golden_Aether_The_Xianzhou_Luofu", + "Calyx_Golden_Treasures_The_Xianzhou_Luofu", + "Calyx_Golden_Memories_Jarilo_VI", + "Calyx_Golden_Aether_Jarilo_VI", + "Calyx_Golden_Treasures_Jarilo_VI", + "Calyx_Crimson_Destruction_Herta_StorageZone", + "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", + "Calyx_Crimson_Preservation_Herta_SupplyZone", + "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", + "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", + "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", + "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", + "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", + "Calyx_Crimson_Erudition_Jarilo_RivetTown", + "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", + "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", + "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", + "Calyx_Crimson_Nihility_Jarilo_GreatMine", + "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", + "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", + "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", + "Stagnant_Shadow_Quanta", + "Stagnant_Shadow_Gust", + "Stagnant_Shadow_Fulmination", + "Stagnant_Shadow_Blaze", + "Stagnant_Shadow_Spike", + "Stagnant_Shadow_Rime", + "Stagnant_Shadow_Mirage", + "Stagnant_Shadow_Icicle", + "Stagnant_Shadow_Doom", + "Stagnant_Shadow_Puppetry", + "Stagnant_Shadow_Abomination", + "Stagnant_Shadow_Scorch", + "Stagnant_Shadow_Celestial", + "Stagnant_Shadow_Perdition", + "Stagnant_Shadow_Nectar", + "Stagnant_Shadow_Roast", + "Stagnant_Shadow_Ire", + "Stagnant_Shadow_Duty", + "Stagnant_Shadow_Timbre", + "Stagnant_Shadow_Mechwolf", + "Stagnant_Shadow_Gloam", + "Stagnant_Shadow_Sloggyre", + "Stagnant_Shadow_Gelidmoon", + "Stagnant_Shadow_Deepsheaf", + "Stagnant_Shadow_Cinders", + "Stagnant_Shadow_Sirens", + "Stagnant_Shadow_Ashes", + "Stagnant_Shadow_Soundburst", +) + +ORNAMENT_OPTIONS: tuple[str, ...] = ( + "-", + "Divergent_Universe_Within_the_West_Wind", + "Divergent_Universe_Moonlit_Blood", + "Divergent_Universe_Unceasing_Strife", + "Divergent_Universe_Famished_Worker", + "Divergent_Universe_Eternal_Comedy", + "Divergent_Universe_To_Sweet_Dreams", + "Divergent_Universe_Pouring_Blades", + "Divergent_Universe_Fruit_of_Evil", + "Divergent_Universe_Permafrost", + "Divergent_Universe_Gentle_Words", + "Divergent_Universe_Smelted_Heart", + "Divergent_Universe_Untoppled_Walls", +) + +ECHO_OF_WAR_OPTIONS: tuple[str, ...] = ( + "-", + "Echo_of_War_Rusted_Crypt_of_the_Iron_Carcass", + "Echo_of_War_Glance_of_Twilight", + "Echo_of_War_Inner_Beast_Battlefield", + "Echo_of_War_Salutations_of_Ashen_Dreams", + "Echo_of_War_Borehole_Planet_Past_Nightmares", + "Echo_of_War_Divine_Seed", + "Echo_of_War_End_of_the_Eternal_Freeze", + "Echo_of_War_Destruction_Beginning", +) + +SIM_WORLD_OPTIONS: tuple[str, ...] = ( + "-", + "Simulated_Universe_World_3", + "Simulated_Universe_World_4", + "Simulated_Universe_World_5", + "Simulated_Universe_World_6", + "Simulated_Universe_World_8", +) + + +class SrcUserConfig(PydanticConfigBase): + """SRC用户配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + Name: str = "新用户" + Status: bool = True + Id: str = "" + Password: EncryptedString = "" + Mode: Literal["简洁", "详细"] = "简洁" + Server: Literal[ + "CN-Official", + "CN-Bilibili", + "VN-Official", + "OVERSEA-America", + "OVERSEA-Asia", + "OVERSEA-Europe", + "OVERSEA-TWHKMO", + ] = "CN-Official" + RemainedDay: int = Field(default=-1, ge=-1, le=9999) + Notes: str = "无" + Tag: str = "[ ]" + + class StageModel(BaseModel): + Channel: Literal["Relic", "Materials", "Ornament"] = "Relic" + Relic: str = "-" + Materials: str = "-" + Ornament: str = "-" + ExtractReservedTrailblazePower: bool = False + UseFuel: bool = False + FuelReserve: int = Field(default=5, ge=0, le=9999) + EchoOfWar: str = "-" + SimulatedUniverseWorld: str = "-" + + @field_validator("Relic", mode="before") + @classmethod + def _validate_relic(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + return text if text in RELIC_OPTIONS else "-" + + @field_validator("Materials", mode="before") + @classmethod + def _validate_materials(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + return text if text in MATERIAL_OPTIONS else "-" + + @field_validator("Ornament", mode="before") + @classmethod + def _validate_ornament(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + return text if text in ORNAMENT_OPTIONS else "-" + + @field_validator("EchoOfWar", mode="before") + @classmethod + def _validate_echo_of_war(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + return text if text in ECHO_OF_WAR_OPTIONS else "-" + + @field_validator("SimulatedUniverseWorld", mode="before") + @classmethod + def _validate_sim_world(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + return text if text in SIM_WORLD_OPTIONS else "-" + + class DataModel(BaseModel): + LastProxyDate: str = "2000-01-01" + ProxyTimes: int = Field(default=0, ge=0, le=9999) + IfPassCheck: bool = True + + @field_validator("LastProxyDate", mode="before") + @classmethod + def _normalize_ymd(cls, value: Any) -> str: + text = value if isinstance(value, str) else str(value) + try: + datetime.strptime(text, "%Y-%m-%d") + return text + except ValueError: + return "2000-01-01" + + class NotifyModel(BaseModel): + Enabled: bool = False + IfSendStatistic: bool = False + IfSendMail: bool = False + ToAddress: str = "" + IfServerChan: bool = False + ServerChanKey: str = "" + + Info: InfoModel = Field(default_factory=InfoModel) + Stage: StageModel = Field(default_factory=StageModel) + Data: DataModel = Field(default_factory=DataModel) + Notify: NotifyModel = Field(default_factory=NotifyModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + if (group, name) == ("Info", "Tag"): + return self.getTags() + return value + + def get(self, group: str, name: str) -> Any: + if (group, name) == ("Info", "Tag"): + return self.getTags() + return super().get(group, name) + + async def toDict( + self, if_decrypt: bool = True, regenerate_uuids: bool = False + ) -> dict[str, Any]: + data = await super().toDict(if_decrypt, regenerate_uuids) + info = data.get("Info") + if isinstance(info, dict): + info["Tag"] = self.getTags() + return data + + def getTags(self) -> str: # noqa: N802 + """生成用户标签列表,返回JSON字符串格式的TagItem列表""" + tags: list[dict[str, str]] = [] + + if not self.get("Data", "IfPassCheck"): + tags.append({"text": "人工排查未通过", "color": "red"}) + + if ( + datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date() + == datetime.now(tz=UTC4).date() + ): + tags.append( + { + "text": f"日常:已代理{self.get('Data', 'ProxyTimes')}次", + "color": "green", + } + ) + else: + tags.append({"text": "日常:未代理", "color": "orange"}) + + remained_day = self.get("Info", "RemainedDay") + if remained_day == -1: + tag_color = "gold" + elif remained_day == 0: + tag_color = "red" + elif remained_day <= 3: + tag_color = "orange" + elif remained_day <= 7: + tag_color = "yellow" + elif remained_day <= 30: + tag_color = "blue" + else: + tag_color = "green" + tags.append( + { + "text": f"剩余天数:{remained_day}天" + if remained_day >= 0 + else "剩余天数:无期限", + "color": tag_color, + } + ) + + tags.append( + { + "text": f"关卡:{STARRAIL_STAGE_BOOK.get(self.get('Stage', self.get('Stage', 'Channel')), '未知关卡')}", + "color": "blue", + } + ) + tags.append( + { + "text": f"周本:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'EchoOfWar'), '未知关卡')}", + "color": "blue", + } + ) + tags.append( + { + "text": f"模拟宇宙:{STARRAIL_STAGE_BOOK.get(self.get('Stage', 'SimulatedUniverseWorld'), '未知关卡')}", + "color": "blue", + } + ) + + notes = self.get("Info", "Notes") + tags.append( + { + "text": f"备注:{notes}" + if len(notes) <= 20 + else f"备注:{notes[:20]}...", + "color": "pink", + } + ) + + return json.dumps(tags, ensure_ascii=False) + + +class SrcConfig(PydanticConfigBase): + """SRC配置""" + + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + + class InfoModel(BaseModel): + Name: str = "新 SRC 脚本" + Path: str = str(Path.cwd()) + + class EmulatorModel(BaseModel): + Id: str = "-" + Index: str = "-" + + class RunModel(BaseModel): + TaskTransitionMethod: Literal["ExitGame", "ExitEmulator"] = "ExitGame" + ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) + RunTimesLimit: int = Field(default=3, ge=1, le=9999) + RunTimeLimit: int = Field(default=10, ge=1, le=9999) + + Info: InfoModel = Field(default_factory=InfoModel) + Emulator: EmulatorModel = Field(default_factory=EmulatorModel) + Run: RunModel = Field(default_factory=RunModel) + + def __init__(self, **data: Any): + super().__init__(**data) + self.UserData = MultipleConfig([SrcUserConfig]) + + def _normalize_value(self, group: str, name: str, value: Any) -> Any: + value = super()._normalize_value(group, name, value) + + if (group, name) != ("Emulator", "Id"): + return value + + if value == "-": + return "-" + if not isinstance(value, str): + return "-" + try: + uid = uuid.UUID(value) + except (TypeError, ValueError): + return "-" + + emulator_config = self.related_config.get("EmulatorConfig") + if emulator_config is None or uid not in emulator_config: + return "-" + + return str(uid) + + +__all__ = ["SrcUserConfig", "SrcConfig"] diff --git a/app/models/type.py b/app/models/type.py new file mode 100644 index 00000000..ccc1fac1 --- /dev/null +++ b/app/models/type.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import json +from datetime import datetime +from typing import Annotated, Any +from urllib.parse import urlparse + +import pyautogui +from pydantic import AfterValidator + +from app.utils.constants import DEFAULT_DATETIME +from app.utils.security import dpapi_decrypt, dpapi_encrypt + + +def _to_string(value: Any) -> str: + if isinstance(value, str): + return value + return str(value) + + +def _validate_json_dict_string(value: str) -> str: + text = _to_string(value) + try: + parsed = json.loads(text) + except json.JSONDecodeError: + return "{ }" + return text if isinstance(parsed, dict) else "{ }" + + +def _validate_json_list_string(value: str) -> str: + text = _to_string(value) + try: + parsed = json.loads(text) + except json.JSONDecodeError: + return "[ ]" + return text if isinstance(parsed, list) else "[ ]" + + +def _validate_hhmm_string(value: str) -> str: + text = _to_string(value) + try: + datetime.strptime(text, "%H:%M") + return text + except ValueError: + return DEFAULT_DATETIME.strftime("%H:%M") + + +def _validate_ymd_hm_string(value: str) -> str: + text = _to_string(value) + try: + datetime.strptime(text, "%Y-%m-%d %H:%M") + return text + except ValueError: + return DEFAULT_DATETIME.strftime("%Y-%m-%d %H:%M") + + +def _validate_ymd_string(value: str) -> str: + text = _to_string(value) + try: + datetime.strptime(text, "%Y-%m-%d") + return text + except ValueError: + return DEFAULT_DATETIME.strftime("%Y-%m-%d") + + +def _validate_ymd_hms_string(value: str) -> str: + text = _to_string(value) + try: + datetime.strptime(text, "%Y-%m-%d %H:%M:%S") + return text + except ValueError: + return DEFAULT_DATETIME.strftime("%Y-%m-%d %H:%M:%S") + + +def _validate_url_string(value: str) -> str: + text = _to_string(value) + if text == "": + return "" + try: + parsed = urlparse(text) + except Exception: + return "" + return text if parsed.scheme and parsed.netloc else "" + + +def _validate_keyboard_key(value: str) -> str: + text = _to_string(value).lower() + return text if text in pyautogui.KEYBOARD_KEYS else "" + + +def _normalize_encrypted_string(value: str) -> str: + text = _to_string(value) + if text == "": + return "" + try: + dpapi_decrypt(text) + return text + except Exception: + return dpapi_encrypt(text) + + +def decrypt_encrypted_string(value: str) -> str: + if value == "": + return "" + try: + return dpapi_decrypt(value) + except Exception: + return "数据损坏, 请重新设置" + + +JsonDictString = Annotated[str, AfterValidator(_validate_json_dict_string)] +JsonListString = Annotated[str, AfterValidator(_validate_json_list_string)] +HHMMString = Annotated[str, AfterValidator(_validate_hhmm_string)] +YmdHmString = Annotated[str, AfterValidator(_validate_ymd_hm_string)] +YmdString = Annotated[str, AfterValidator(_validate_ymd_string)] +YmdHmsString = Annotated[str, AfterValidator(_validate_ymd_hms_string)] +UrlString = Annotated[str, AfterValidator(_validate_url_string)] +KeyboardKeyString = Annotated[str, AfterValidator(_validate_keyboard_key)] +EncryptedString = Annotated[str, AfterValidator(_normalize_encrypted_string)] + + +__all__ = [ + "JsonDictString", + "JsonListString", + "HHMMString", + "YmdHmString", + "YmdString", + "YmdHmsString", + "UrlString", + "KeyboardKeyString", + "EncryptedString", + "decrypt_encrypted_string", +] diff --git a/app/services/notification.py b/app/services/notification.py index 1df7810b..644b1855 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -35,14 +35,13 @@ from typing import Literal from app.core import Config -from app.models.config import Webhook +from app.models import Webhook from app.utils import get_logger, ImageUtils logger = get_logger("通知服务") class Notification: - async def push_plyer(self, title: str, message: str, ticker: str, t: int) -> None: """ 推送系统通知 @@ -211,7 +210,6 @@ async def WebhookPush(self, title: str, content: str, webhook: Webhook) -> None: # 替换模板变量 try: - # 准备模板变量 template_vars = { "title": title, diff --git a/app/task/MAA/AutoProxy.py b/app/task/MAA/AutoProxy.py index def7cd53..91eb4c14 100644 --- a/app/task/MAA/AutoProxy.py +++ b/app/task/MAA/AutoProxy.py @@ -29,8 +29,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaConfig, MaaUserConfig +from app.models import MaaConfig, MaaUserConfig, MultipleConfig from app.models.emulator import DeviceInfo, DeviceBase from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, skland_sign_in @@ -77,14 +76,11 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if self.script_config.get( "Run", "ProxyTimesLimit" ) != 0 and self.cur_user_config.get( "Data", "ProxyTimes" - ) >= self.script_config.get( - "Run", "ProxyTimesLimit" - ): + ) >= self.script_config.get("Run", "ProxyTimesLimit"): self.cur_user_item.status = "跳过" return "今日代理次数已达上限, 跳过该用户" @@ -100,7 +96,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.maa_process_manager = ProcessManager() self.maa_log_monitor = LogMonitor( (1, 20), @@ -378,7 +373,6 @@ async def set_maa(self, emulator_info: DeviceInfo): task_set = {} # 每个任务类型匹配第一个配置作为配置基础 for en_task, zh_task in zip(MAA_TASKS, MAA_TASKS_ZH): - for task_item in gui_new_set["Configurations"]["Default"]["TaskQueue"]: if task_item.get("TaskType", "") == en_task: task_set[en_task] = task_item @@ -546,7 +540,6 @@ async def set_maa(self, emulator_info: DeviceInfo): self.task_dict["StartUp"] = True task_queue = gui_new_set["Configurations"]["Default"]["TaskQueue"] = [] for task_type in MAA_TASKS: - task_set[task_type]["IsEnable"] = self.task_dict[task_type] task_queue.append(task_set[task_type]) @@ -588,7 +581,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None elif "任务出错: 开始唤醒" in log: self.cur_user_log.status = "MAA 未能正确登录 PRTS" elif "任务已全部完成!" in log: - for en_task, zh_task in zip(MAA_TASKS, MAA_TASKS_ZH): if f"完成任务: {zh_task}" in log: self.task_dict[en_task] = False @@ -627,7 +619,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None self.wait_event.set() async def final_task(self): - if self.check_result != "Pass": return @@ -647,7 +638,6 @@ async def final_task(self): user_logs_list = [] if_six_star = False for t, log_item in self.cur_user_item.log_record.items(): - if log_item.status == "MAA 正常运行中": log_item.status = "任务被用户手动中止" diff --git a/app/task/MAA/ManualReview.py b/app/task/MAA/ManualReview.py index d73a0d79..7e968bae 100644 --- a/app/task/MAA/ManualReview.py +++ b/app/task/MAA/ManualReview.py @@ -29,8 +29,7 @@ from app.core import Config, Broadcast from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaConfig, MaaUserConfig +from app.models import MaaConfig, MaaUserConfig, MultipleConfig from app.models.emulator import DeviceInfo, DeviceBase from app.services import System from app.utils import get_logger, LogMonitor, ProcessManager @@ -66,7 +65,6 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if ( self.cur_user_config.get("Info", "Mode") == "详细" and not ( @@ -79,7 +77,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.maa_process_manager = ProcessManager() self.maa_log_monitor = LogMonitor((1, 20), "%Y-%m-%d %H:%M:%S", self.check_log) self.message_queue = asyncio.Queue() @@ -122,7 +119,6 @@ async def main_task(self): self.cur_user_item.status = "运行" while True: - try: self.script_info.log = "正在启动模拟器" emulator_info = await self.emulator_manager.open( @@ -130,7 +126,6 @@ async def main_task(self): ARKNIGHTS_PACKAGE_NAME[self.cur_user_config.get("Info", "Server")], ) except Exception as e: - logger.exception( f"用户: {self.cur_user_item.user_id} - 模拟器启动失败: {e}" ) @@ -182,7 +177,6 @@ async def main_task(self): self.run_book["SignIn"] = True break else: - logger.error( f"用户: {self.cur_user_item.user_id} - MAA进程异常: {self.cur_user_log.status}" ) @@ -214,7 +208,6 @@ async def main_task(self): break if self.run_book["SignIn"]: - try: await self.emulator_manager.setVisible( self.script_config.get("Emulator", "Index"), True @@ -380,7 +373,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None self.wait_event.set() async def final_task(self): - if self.check_result != "Pass": return diff --git a/app/task/MAA/ScriptConfig.py b/app/task/MAA/ScriptConfig.py index 8dbd5a93..63809b26 100644 --- a/app/task/MAA/ScriptConfig.py +++ b/app/task/MAA/ScriptConfig.py @@ -26,8 +26,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaConfig, MaaUserConfig +from app.models import MaaConfig, MaaUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager @@ -58,7 +57,6 @@ def __init__( self.cur_user_item = self.script_info.user_list[self.script_info.current_index] async def prepare(self): - self.maa_process_manager = ProcessManager() self.wait_event = asyncio.Event() @@ -67,7 +65,6 @@ async def prepare(self): self.maa_exe_path = self.maa_root_path / "MAA.exe" async def main_task(self): - await self.prepare() await self.set_maa() @@ -144,7 +141,6 @@ async def set_maa(self): logger.success(f"MAA运行参数配置完成: 设置脚本 {self.cur_user_item.user_id}") async def final_task(self): - await self.maa_process_manager.kill() await System.kill_process(self.maa_exe_path) diff --git a/app/task/MAA/manager.py b/app/task/MAA/manager.py index bdc0a3e1..c3987b94 100644 --- a/app/task/MAA/manager.py +++ b/app/task/MAA/manager.py @@ -27,8 +27,7 @@ from app.core import Config, EmulatorManager from app.models.task import TaskExecuteBase, ScriptItem, UserItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaConfig, MaaUserConfig +from app.models import MaaConfig, MaaUserConfig, MultipleConfig from app.services import Notify from app.utils import get_logger from app.utils.constants import TASK_MODE_ZH @@ -158,7 +157,6 @@ async def prepare(self): ) async def main_task(self): - self.check_result = await self.check() if self.check_result != "Pass": logger.error(f"未通过配置检查: {self.check_result}") @@ -196,7 +194,6 @@ async def final_task(self): logger.success(f"已解锁脚本配置 {self.script_info.script_id}") if self.task_info.mode in ["AutoProxy", "ManualReview"]: - await self.emulator_manager.close( self.script_config.get("Emulator", "Index") ) @@ -250,7 +247,6 @@ async def final_task(self): self.script_info.status = "完成" async def on_crash(self, e: Exception): - self.script_info.status = "异常" logger.exception(f"MAA任务出现异常: {e}") await Config.send_websocket_message( diff --git a/app/task/MAA/tools/notify.py b/app/task/MAA/tools/notify.py index d58f9cc7..4490491a 100644 --- a/app/task/MAA/tools/notify.py +++ b/app/task/MAA/tools/notify.py @@ -22,7 +22,7 @@ from app.core import Config from app.services import Notify from app.utils import get_logger -from app.models.config import MaaUserConfig +from app.models import MaaUserConfig logger = get_logger("MAA 通知工具") diff --git a/app/task/MaaEnd/AutoProxy.py b/app/task/MaaEnd/AutoProxy.py index 8066b7ac..35f48841 100644 --- a/app/task/MaaEnd/AutoProxy.py +++ b/app/task/MaaEnd/AutoProxy.py @@ -27,8 +27,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaEndConfig, MaaEndUserConfig +from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig from app.models.emulator import DeviceBase, DeviceInfo from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, skland_sign_in @@ -65,14 +64,11 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if self.script_config.get( "Run", "ProxyTimesLimit" ) != 0 and self.cur_user_config.get( "Data", "ProxyTimes" - ) >= self.script_config.get( - "Run", "ProxyTimesLimit" - ): + ) >= self.script_config.get("Run", "ProxyTimesLimit"): self.cur_user_item.status = "跳过" return "今日代理次数已达上限, 跳过该用户" @@ -91,7 +87,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.maaend_process_manager = ProcessManager() if self.emulator_manager is None: self.game_process_manager = ProcessManager() @@ -303,7 +298,6 @@ async def main_task(self): async def handle_pre_maaend_error( self, error_message: str, e: Exception | None = None ): - if e is None: logger.error(f"用户: {self.cur_user_uid} - {error_message}") await Config.send_websocket_message( @@ -399,7 +393,6 @@ async def set_maaend(self, device_info: DeviceInfo | None) -> None: "Game", "ControllerType" ) if device_info is not None: - from app.core import MaaFWManager maaend_instance["savedDevice"] = { @@ -474,7 +467,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None if "资源加载失败" in log: self.cur_user_log.status = "MaaEnd 资源加载失败" elif not await self.maaend_process_manager.is_running(): - if self.task_dict is None: self.cur_user_log.status = "MaaEnd 未加载任何任务" else: @@ -502,7 +494,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None self.wait_event.set() async def final_task(self): - if self.check_result != "Pass": return @@ -523,7 +514,6 @@ async def final_task(self): user_logs_list = [] for t, log_item in self.cur_user_item.log_record.items(): - dt = t.replace(tzinfo=datetime.now().astimezone().tzinfo).astimezone(UTC4) log_path = ( Path.cwd() diff --git a/app/task/MaaEnd/ManualReview.py b/app/task/MaaEnd/ManualReview.py index b7bfbb22..08258c11 100644 --- a/app/task/MaaEnd/ManualReview.py +++ b/app/task/MaaEnd/ManualReview.py @@ -26,8 +26,7 @@ from app.core import Broadcast, Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaEndConfig, MaaEndUserConfig +from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager @@ -63,7 +62,6 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if ( self.cur_user_config.get("Info", "Mode") == "详细" and not ( @@ -82,7 +80,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - if self.emulator_manager is None: self.game_process_manager = ProcessManager() self.message_queue = asyncio.Queue() @@ -117,7 +114,6 @@ async def main_task(self): self.cur_user_item.status = "运行" while True: - try: self.script_info.log = "正在启动游戏..." if self.emulator_manager is None: @@ -139,7 +135,6 @@ async def main_task(self): "com.hypergryph.endfield", ) except Exception as e: - logger.exception(f"用户 {self.cur_user_item.user_id} 游戏启动失败: {e}") self.script_info.log = ( f"正在启动模拟器\n模拟器启动失败: {e}\n正在中止相关程序" @@ -198,7 +193,6 @@ async def main_task(self): break if self.run_book["SignIn"]: - try: if self.emulator_manager is not None: await self.emulator_manager.setVisible( @@ -249,7 +243,6 @@ async def kill_managed_process(self) -> None: logger.exception(f"关闭游戏失败: {e}") async def final_task(self): - if self.check_result != "Pass": return diff --git a/app/task/MaaEnd/ScriptConfig.py b/app/task/MaaEnd/ScriptConfig.py index ad5e7f6e..b3946e6f 100644 --- a/app/task/MaaEnd/ScriptConfig.py +++ b/app/task/MaaEnd/ScriptConfig.py @@ -27,8 +27,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaEndConfig, MaaEndUserConfig +from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager @@ -117,7 +116,6 @@ def __init__( self.cur_user_item = self.script_info.user_list[self.script_info.current_index] async def prepare(self): - self.maaend_process_manager = ProcessManager() self.wait_event = asyncio.Event() @@ -130,7 +128,6 @@ async def prepare(self): ) async def main_task(self): - await self.prepare() await self.set_maaend() @@ -169,7 +166,6 @@ async def set_maaend(self): ) async def final_task(self): - await self.maaend_process_manager.kill() await System.kill_process(self.maaend_exe_path) diff --git a/app/task/MaaEnd/manager.py b/app/task/MaaEnd/manager.py index 920ae46e..3ca40b1b 100644 --- a/app/task/MaaEnd/manager.py +++ b/app/task/MaaEnd/manager.py @@ -25,8 +25,7 @@ from pathlib import Path from app.core import Config, EmulatorManager -from app.models.ConfigBase import MultipleConfig -from app.models.config import MaaEndConfig, MaaEndUserConfig +from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig from app.models.task import ScriptItem, TaskExecuteBase, UserItem from app.services import Notify from app.utils import get_logger @@ -91,7 +90,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - # 锁定脚本配置并加载用户配置 await Config.ScriptConfig[uuid.UUID(self.script_info.script_id)].lock() self.script_config = Config.ScriptConfig[uuid.UUID(self.script_info.script_id)] @@ -137,7 +135,6 @@ async def prepare(self): ) async def main_task(self): - self.check_result = await self.check() if self.check_result != "Pass": logger.error(f"未通过配置检查: {self.check_result}") @@ -164,7 +161,6 @@ async def main_task(self): await self.spawn(task) async def final_task(self): - if self.check_result != "Pass": self.script_info.status = "异常" return @@ -174,7 +170,6 @@ async def final_task(self): logger.success(f"已解锁脚本配置 {self.script_info.script_id}") if self.task_info.mode in ["AutoProxy", "ManualReview"]: - if self.emulator_manager is not None: await self.emulator_manager.close( self.script_config.get("Game", "EmulatorIndex") diff --git a/app/task/MaaEnd/tools/notify.py b/app/task/MaaEnd/tools/notify.py index 12f8de9d..7fa81c91 100644 --- a/app/task/MaaEnd/tools/notify.py +++ b/app/task/MaaEnd/tools/notify.py @@ -20,7 +20,7 @@ from app.core import Config -from app.models.config import MaaEndUserConfig +from app.models import MaaEndUserConfig from app.services import Notify from app.utils import get_logger @@ -124,7 +124,9 @@ async def push_notification( user_config.get("Notify", "ServerChanKey"), ) else: - logger.error("用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知") + logger.error( + "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知" + ) for webhook in user_config.Notify_CustomWebhooks.values(): await Notify.WebhookPush( diff --git a/app/task/SRC/AutoProxy.py b/app/task/SRC/AutoProxy.py index a4ceb3fd..09dd1855 100644 --- a/app/task/SRC/AutoProxy.py +++ b/app/task/SRC/AutoProxy.py @@ -30,8 +30,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models.ConfigBase import MultipleConfig -from app.models.config import SrcConfig, SrcUserConfig +from app.models import SrcConfig, SrcUserConfig, MultipleConfig from app.models.emulator import DeviceBase, DeviceInfo from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, strptime @@ -67,14 +66,11 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if self.script_config.get( "Run", "ProxyTimesLimit" ) != 0 and self.cur_user_config.get( "Data", "ProxyTimes" - ) >= self.script_config.get( - "Run", "ProxyTimesLimit" - ): + ) >= self.script_config.get("Run", "ProxyTimesLimit"): self.cur_user_item.status = "跳过" return "今日代理次数已达上限, 跳过该用户" @@ -90,7 +86,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.src_process_manager = ProcessManager() self.src_log_monitor = LogMonitor( (0, 23), "%Y-%m-%d %H:%M:%S.%f", self.check_log @@ -193,7 +188,6 @@ async def main_task(self): self.script_info.log = "正在启动模拟器...\n模拟器启动成功\n正在登录「崩坏·星穹铁道」\n「崩坏·星穹铁道」登录成功\n正在等待 SRC 日志文件生成" if_get_file = False while datetime.now() - t < timedelta(minutes=1): - for log_file in self.src_log_path.parent.iterdir(): if log_file.is_file(): with suppress(ValueError): @@ -260,7 +254,6 @@ async def main_task(self): async def handle_pre_src_error( self, error_message: str, e: Exception | None = None ): - if e is None: logger.error(f"用户: {self.cur_user_uid} - {error_message}") await Config.send_websocket_message( @@ -462,7 +455,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None self.wait_event.set() async def final_task(self): - if self.check_result != "Pass": return @@ -484,7 +476,6 @@ async def final_task(self): user_logs_list = [] for t, log_item in self.cur_user_item.log_record.items(): - dt = t.replace(tzinfo=datetime.now().astimezone().tzinfo).astimezone(UTC4) log_path = ( Path.cwd() diff --git a/app/task/SRC/ManualReview.py b/app/task/SRC/ManualReview.py index b8e013d7..78bba0e9 100644 --- a/app/task/SRC/ManualReview.py +++ b/app/task/SRC/ManualReview.py @@ -27,8 +27,7 @@ from app.core import Config, Broadcast from app.models.task import TaskExecuteBase, ScriptItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import SrcConfig, SrcUserConfig +from app.models import SrcConfig, SrcUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.utils import get_logger from app.utils.constants import STARRAIL_PACKAGE_NAME, UTC4 @@ -63,7 +62,6 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if ( self.cur_user_config.get("Info", "Mode") == "详细" and not ( @@ -76,7 +74,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.message_queue = asyncio.Queue() await Broadcast.subscribe(self.message_queue) self.wait_event = asyncio.Event() @@ -110,7 +107,6 @@ async def main_task(self): self.cur_user_item.status = "运行" while True: - try: self.script_info.log = "正在启动模拟器" emulator_info = await self.emulator_manager.open( @@ -118,7 +114,6 @@ async def main_task(self): STARRAIL_PACKAGE_NAME[self.cur_user_config.get("Info", "Server")], ) except Exception as e: - logger.exception( f"用户: {self.cur_user_item.user_id} - 模拟器启动失败: {e}" ) @@ -190,7 +185,6 @@ async def main_task(self): break if self.run_book["SignIn"]: - try: await self.emulator_manager.setVisible( self.script_config.get("Emulator", "Index"), True @@ -226,7 +220,6 @@ async def _wait_for_user_response(self, message_id: str): self.message_queue.task_done() async def final_task(self): - if self.check_result != "Pass": return diff --git a/app/task/SRC/ScriptConfig.py b/app/task/SRC/ScriptConfig.py index 3f37e68b..cd504f00 100644 --- a/app/task/SRC/ScriptConfig.py +++ b/app/task/SRC/ScriptConfig.py @@ -26,8 +26,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import SrcConfig, SrcUserConfig +from app.models import SrcConfig, SrcUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager @@ -58,7 +57,6 @@ def __init__( self.cur_user_item = self.script_info.user_list[self.script_info.current_index] async def prepare(self): - self.src_process_manager = ProcessManager() self.wait_event = asyncio.Event() @@ -67,7 +65,6 @@ async def prepare(self): self.src_exe_path = self.src_root_path / "src.exe" async def main_task(self): - await self.prepare() await self.set_src() @@ -144,7 +141,6 @@ async def set_src(self): logger.success(f"SRC运行参数配置完成: 设置脚本 {self.cur_user_item.user_id}") async def final_task(self): - await self.src_process_manager.kill() await System.kill_process(self.src_exe_path) diff --git a/app/task/SRC/manager.py b/app/task/SRC/manager.py index 8b686628..b257ae2e 100644 --- a/app/task/SRC/manager.py +++ b/app/task/SRC/manager.py @@ -27,8 +27,7 @@ from app.core import Config, EmulatorManager from app.models.task import TaskExecuteBase, ScriptItem, UserItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import SrcConfig, SrcUserConfig +from app.models import SrcConfig, SrcUserConfig, MultipleConfig from app.services import Notify from app.utils import get_logger from app.utils.constants import TASK_MODE_ZH @@ -161,7 +160,6 @@ async def prepare(self): ) async def main_task(self): - self.check_result = await self.check() if self.check_result != "Pass": logger.error(f"未通过配置检查: {self.check_result}") @@ -199,7 +197,6 @@ async def final_task(self): logger.success(f"已解锁脚本配置 {self.script_info.script_id}") if self.task_info.mode in ["AutoProxy", "ManualReview"]: - await self.emulator_manager.close( self.script_config.get("Emulator", "Index") ) @@ -253,7 +250,6 @@ async def final_task(self): self.script_info.status = "完成" async def on_crash(self, e: Exception): - self.script_info.status = "异常" logger.exception(f"SRC任务出现异常: {e}") await Config.send_websocket_message( diff --git a/app/task/SRC/tools/notify.py b/app/task/SRC/tools/notify.py index 313ee2ce..2075bcfc 100644 --- a/app/task/SRC/tools/notify.py +++ b/app/task/SRC/tools/notify.py @@ -21,7 +21,7 @@ from app.core import Config from app.services import Notify from app.utils import get_logger -from app.models.config import SrcUserConfig +from app.models import SrcUserConfig logger = get_logger("SRC通知工具") diff --git a/app/task/general/AutoProxy.py b/app/task/general/AutoProxy.py index 4f36dbe4..1fd51769 100644 --- a/app/task/general/AutoProxy.py +++ b/app/task/general/AutoProxy.py @@ -30,8 +30,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models.ConfigBase import MultipleConfig -from app.models.config import GeneralConfig, GeneralUserConfig +from app.models import GeneralConfig, GeneralUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, ProcessInfo, strptime @@ -67,14 +66,11 @@ def __init__( self.check_result = "-" async def check(self) -> str: - if self.script_config.get( "Run", "ProxyTimesLimit" ) != 0 and self.cur_user_config.get( "Data", "ProxyTimes" - ) >= self.script_config.get( - "Run", "ProxyTimesLimit" - ): + ) >= self.script_config.get("Run", "ProxyTimesLimit"): self.cur_user_item.status = "跳过" return "今日代理次数已达上限, 跳过该用户" @@ -89,7 +85,6 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.general_process_manager = ProcessManager() self.wait_event = asyncio.Event() self.user_start_time = datetime.now() @@ -222,7 +217,6 @@ async def main_task(self): if self.game_manager is not None: try: if isinstance(self.game_manager, ProcessManager): - if self.script_config.get("Game", "Type") == "URL": logger.info( f"启动游戏: {self.game_process_name}, 参数{self.game_url}" @@ -273,7 +267,6 @@ async def main_task(self): self.script_info.log = "正在等待脚本日志文件生成" if_get_file = False while datetime.now() - t < timedelta(minutes=1): - for log_file in self.script_log_path.parent.iterdir(): if log_file.is_file(): with suppress(ValueError): @@ -351,7 +344,6 @@ async def main_task(self): async def handle_pre_script_error( self, error_message: str, e: Exception | None = None ): - if e is None: logger.error(f"用户: {self.cur_user_uid} - {error_message}") await Config.send_websocket_message( @@ -379,7 +371,6 @@ async def handle_pre_script_error( ) async def update_config(self): - if self.script_config.get("Script", "ConfigPathMode") == "Folder": shutil.copytree( self.script_config_path, @@ -481,7 +472,6 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None self.wait_event.set() async def final_task(self): - if self.check_result != "Pass": return @@ -493,7 +483,6 @@ async def final_task(self): user_logs_list = [] for t, log_item in self.cur_user_item.log_record.items(): - dt = t.replace(tzinfo=datetime.now().astimezone().tzinfo).astimezone(UTC4) log_path = ( Path.cwd() diff --git a/app/task/general/ScriptConfig.py b/app/task/general/ScriptConfig.py index 1a6d5938..f8f7dfe3 100644 --- a/app/task/general/ScriptConfig.py +++ b/app/task/general/ScriptConfig.py @@ -27,8 +27,7 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import GeneralConfig, GeneralUserConfig +from app.models import GeneralConfig, GeneralUserConfig, MultipleConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager @@ -59,7 +58,6 @@ def __init__( self.cur_user_item = self.script_info.user_list[self.script_info.current_index] async def prepare(self): - self.general_process_manager = ProcessManager() self.wait_event = asyncio.Event() @@ -95,7 +93,6 @@ async def prepare(self): self.script_config_path = Path(self.script_config.get("Script", "ConfigPath")) async def main_task(self): - await self.prepare() await self.set_general() @@ -149,7 +146,6 @@ async def set_general(self) -> None: logger.success(f"MAA运行参数配置完成: 设置脚本 {self.cur_user_item.user_id}") async def final_task(self): - await self.general_process_manager.kill() await System.kill_process(self.script_set_exe_path) del self.general_process_manager diff --git a/app/task/general/manager.py b/app/task/general/manager.py index d02fa93a..621e4307 100644 --- a/app/task/general/manager.py +++ b/app/task/general/manager.py @@ -27,8 +27,7 @@ from app.core import Config, EmulatorManager from app.models.task import TaskExecuteBase, ScriptItem, UserItem -from app.models.ConfigBase import MultipleConfig -from app.models.config import GeneralConfig, GeneralUserConfig +from app.models import GeneralConfig, GeneralUserConfig, MultipleConfig from app.services import Notify from app.utils import get_logger, ProcessManager from app.utils.constants import TASK_MODE_ZH @@ -191,7 +190,6 @@ async def prepare(self): ) async def main_task(self): - self.check_result = await self.check() if self.check_result != "Pass": logger.error(f"未通过配置检查: {self.check_result}") @@ -238,7 +236,6 @@ async def final_task(self): logger.success(f"已解锁脚本配置 {self.script_info.script_id}") if self.task_info.mode == "AutoProxy": - await Config.ScriptConfig[ uuid.UUID(self.script_info.script_id) ].UserData.load(await self.user_config.toDict()) @@ -300,7 +297,6 @@ async def final_task(self): self.script_info.status = "完成" async def on_crash(self, e: Exception): - self.script_info.status = "异常" logger.exception(f"通用脚本任务出现异常: {e}") await Config.send_websocket_message( diff --git a/app/task/general/tools/notify.py b/app/task/general/tools/notify.py index 53ee4695..ffdcaf6f 100644 --- a/app/task/general/tools/notify.py +++ b/app/task/general/tools/notify.py @@ -22,7 +22,7 @@ from app.core import Config from app.services import Notify from app.utils import get_logger -from app.models.config import GeneralUserConfig +from app.models import GeneralUserConfig logger = get_logger("通用通知工具") diff --git a/app/utils/emulator/general.py b/app/utils/emulator/general.py index d78a3787..c4a71127 100644 --- a/app/utils/emulator/general.py +++ b/app/utils/emulator/general.py @@ -32,7 +32,7 @@ from app.utils.ProcessManager import ProcessManager from app.models.emulator import DeviceStatus, DeviceBase, DeviceInfo -from app.models.config import EmulatorConfig +from app.models import EmulatorConfig from app.utils import get_logger logger = get_logger("通用模拟器管理") @@ -44,7 +44,6 @@ class GeneralDeviceManager(DeviceBase): """ def __init__(self, config: EmulatorConfig) -> None: - if not Path(config.get("Info", "Path")).exists(): raise FileNotFoundError(f"模拟器文件不存在: {config.get('Info', 'Path')}") @@ -57,7 +56,6 @@ def __init__(self, config: EmulatorConfig) -> None: self.device_info: Dict[str, Dict[str, Any]] = {} async def open(self, idx: str, package_name: str = "") -> DeviceInfo: - # 检查是否已经在运行 current_status = await self.getStatus(idx) if current_status == DeviceStatus.ONLINE: @@ -79,7 +77,6 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: return (await self.getInfo(idx))[idx] async def close(self, idx: str) -> DeviceStatus: - status = await self.getStatus(idx) if status == DeviceStatus.OFFLINE: logger.warning(f"设备{idx}未在线,当前状态: {status}") @@ -101,7 +98,6 @@ async def close(self, idx: str) -> DeviceStatus: raise RuntimeError(f"关闭设备{idx}超时") async def getStatus(self, idx: str) -> DeviceStatus: - if idx not in self.process_managers: return DeviceStatus.OFFLINE @@ -111,7 +107,6 @@ async def getStatus(self, idx: str) -> DeviceStatus: return DeviceStatus.OFFLINE async def getInfo(self, idx: str | None) -> Dict[str, DeviceInfo]: - data = {} for index in self.process_managers: if idx is not None and index != idx: @@ -124,7 +119,6 @@ async def getInfo(self, idx: str | None) -> Dict[str, DeviceInfo]: return data async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: - status = await self.getStatus(idx) if status != DeviceStatus.ONLINE: logger.warning(f"设备{idx}未在线,当前状态码: {status}") @@ -134,7 +128,6 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: while datetime.now() - t < timedelta( seconds=self.config.get("Info", "MaxWaitTime") ): - # 检查窗口可见性是否符合预期 if self.process_managers[idx].main_pid is not None and ( win32gui.IsWindowVisible(self.process_managers[idx].main_pid) @@ -158,7 +151,6 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: raise RuntimeError(f"隐藏设备{idx}窗口超时") def parse_index(self, idx: str): - if "|" not in idx: raise ValueError("缺少 '|' 分隔符") diff --git a/app/utils/emulator/ldplayer.py b/app/utils/emulator/ldplayer.py index ae1b36da..7249c378 100644 --- a/app/utils/emulator/ldplayer.py +++ b/app/utils/emulator/ldplayer.py @@ -30,7 +30,7 @@ from pathlib import Path from app.models.emulator import DeviceStatus, DeviceInfo, DeviceBase -from app.models.config import EmulatorConfig +from app.models import EmulatorConfig from app.utils import ProcessRunner, get_logger logger = get_logger("雷电模拟器管理") diff --git a/app/utils/emulator/mumu.py b/app/utils/emulator/mumu.py index 03f6a1b4..80245f99 100644 --- a/app/utils/emulator/mumu.py +++ b/app/utils/emulator/mumu.py @@ -31,7 +31,7 @@ from pathlib import Path from app.models.emulator import DeviceStatus, DeviceInfo, DeviceBase -from app.models.config import EmulatorConfig +from app.models import EmulatorConfig from app.utils import ProcessRunner, get_logger diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9ce40165 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[tool.ruff] +line-length = 88 +target-version = "py311" +exclude = ["frontend", "data", "history", "debug", "__pycache__", "test"] + +[tool.ruff.lint] +select = ["E", "F"] +ignore = ["E501"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..777e8009 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/pyright/schema/pyrightconfig.schema.json", + "pythonVersion": "3.11", + "typeCheckingMode": "strict", + "include": [ + "app", + "main.py" + ], + "exclude": [ + "frontend", + "data", + "history", + "debug", + "__pycache__", + "test" + ], + "reportMissingTypeStubs": "warning", + "reportUnknownVariableType": "warning", + "reportUnknownMemberType": "warning", + "reportUnknownArgumentType": "warning", + "reportUnknownLambdaType": "warning", + "reportUnknownParameterType": "warning", + "reportUnknownReturnType": "warning" +} diff --git a/requirements.txt b/requirements.txt index be826f7b..e1aa9658 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ loguru==0.7.3 fastapi==0.116.1 pydantic==2.11.7 +pydantic-settings==2.10.1 uvicorn==0.35.0 websockets==15.0.1 aiofiles==24.1.0 @@ -24,4 +25,5 @@ numpy==2.3.4 opencv-python==4.10.0.84 maafw==5.8.1 mss==9.0.2 -fastapi-mcp==0.4.0 \ No newline at end of file +fastapi-mcp==0.4.0 +tomli-w==1.2.0 \ No newline at end of file From 1c2dac66000664bcc550b85d18e54ea9e8381b91 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 30 Mar 2026 00:01:13 +0800 Subject: [PATCH 04/29] =?UTF-8?q?fix(config):=E8=A1=A5=E5=85=85=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index ff987b84..54c4026e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -36,7 +36,7 @@ from collections import defaultdict from jinja2 import Environment, FileSystemLoader from datetime import datetime, timedelta, date -from typing import Literal, Optional, Dict, Any, List +from typing import Literal, Optional, Dict, Any, List, ClassVar import uuid import json @@ -88,7 +88,7 @@ class AppConfig(GlobalConfig): - VERSION = "v5.2.0-beta.1" + VERSION: ClassVar[str] = "v5.2.0-beta.1" def __init__(self) -> None: super().__init__() @@ -140,13 +140,9 @@ def __init__(self) -> None: truststore.inject_into_ssl() def _resolve_config_path(self, stem: str) -> Path: - """优先返回 TOML 配置路径;若仅存在 JSON,则返回 JSON。""" + """返回 TOML 配置路径(内部会自动处理同名 JSON 的迁移兼容)。""" - toml_path = self.config_path / f"{stem}.toml" - json_path = self.config_path / f"{stem}.json" - if toml_path.exists() or not json_path.exists(): - return toml_path - return json_path + return self.config_path / f"{stem}.toml" async def _connect_runtime_configs(self) -> None: """连接运行期主配置文件。""" From 884ac0540e5e3d733d49acba0476254207c5561b Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 30 Mar 2026 23:41:08 +0800 Subject: [PATCH 05/29] =?UTF-8?q?refactor(config):=20=E4=BD=BF=E7=94=A8tom?= =?UTF-8?q?l=E6=A0=BC=E5=BC=8F=E9=85=8D=E7=BD=AE,=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config/__init__.py | 36 + .../config_base.py => core/config/base.py} | 138 +- app/core/{config.py => config/manager.py} | 23 +- .../config/pydantic.py} | 22 +- app/{models/type.py => core/config/types.py} | 0 app/core/task_manager.py | 3 +- app/models/ConfigBase.py | 1514 ----------------- app/models/__init__.py | 59 +- app/models/common.py | 18 +- app/models/config/__init__.py | 50 - app/models/config/common.py | 3 - app/models/config/general.py | 3 - app/models/config/global_config.py | 3 - app/models/config/maa.py | 3 - app/models/config/maaend.py | 3 - app/models/config/src.py | 3 - app/models/general.py | 6 +- app/models/global_config.py | 34 +- app/models/maa.py | 6 +- app/models/maaend.py | 6 +- app/models/src.py | 6 +- app/task/MAA/AutoProxy.py | 3 +- app/task/MAA/ManualReview.py | 3 +- app/task/MAA/ScriptConfig.py | 3 +- app/task/MAA/manager.py | 3 +- app/task/MaaEnd/AutoProxy.py | 3 +- app/task/MaaEnd/ManualReview.py | 3 +- app/task/MaaEnd/ScriptConfig.py | 3 +- app/task/MaaEnd/manager.py | 3 +- app/task/SRC/AutoProxy.py | 3 +- app/task/SRC/ManualReview.py | 3 +- app/task/SRC/ScriptConfig.py | 3 +- app/task/SRC/manager.py | 3 +- app/task/general/AutoProxy.py | 3 +- app/task/general/ScriptConfig.py | 3 +- app/task/general/manager.py | 3 +- requirements.txt | 2 - 37 files changed, 218 insertions(+), 1768 deletions(-) create mode 100644 app/core/config/__init__.py rename app/{models/config_base.py => core/config/base.py} (92%) rename app/core/{config.py => config/manager.py} (99%) rename app/{models/pydantic_base.py => core/config/pydantic.py} (92%) rename app/{models/type.py => core/config/types.py} (100%) delete mode 100644 app/models/ConfigBase.py delete mode 100644 app/models/config/__init__.py delete mode 100644 app/models/config/common.py delete mode 100644 app/models/config/general.py delete mode 100644 app/models/config/global_config.py delete mode 100644 app/models/config/maa.py delete mode 100644 app/models/config/maaend.py delete mode 100644 app/models/config/src.py diff --git a/app/core/config/__init__.py b/app/core/config/__init__.py new file mode 100644 index 00000000..10e6c42a --- /dev/null +++ b/app/core/config/__init__.py @@ -0,0 +1,36 @@ +from .base import ConfigBase, ConfigItem, MultipleConfig, dump_json, dump_toml +from .manager import AppConfig, Config +from .pydantic import PydanticConfigBase +from .types import ( + EncryptedString, + HHMMString, + JsonDictString, + JsonListString, + KeyboardKeyString, + UrlString, + YmdHmString, + YmdHmsString, + YmdString, + decrypt_encrypted_string, +) + +__all__ = [ + "ConfigBase", + "ConfigItem", + "MultipleConfig", + "dump_json", + "dump_toml", + "PydanticConfigBase", + "JsonDictString", + "JsonListString", + "HHMMString", + "YmdHmString", + "YmdString", + "YmdHmsString", + "UrlString", + "KeyboardKeyString", + "EncryptedString", + "decrypt_encrypted_string", + "AppConfig", + "Config", +] diff --git a/app/models/config_base.py b/app/core/config/base.py similarity index 92% rename from app/models/config_base.py rename to app/core/config/base.py index f5d8247f..ed46b907 100644 --- a/app/models/config_base.py +++ b/app/core/config/base.py @@ -40,8 +40,6 @@ from pathlib import Path from collections.abc import Callable, Coroutine from typing import Any, Protocol, TypeVar, Generic -from pydantic import TypeAdapter -from pydantic_settings import BaseSettings, SettingsConfigDict from app.utils import get_logger, dpapi_encrypt, dpapi_decrypt from app.utils.constants import ( @@ -54,93 +52,114 @@ logger = get_logger("配置基类") -def _load_toml_with_pydantic_settings(path: Path) -> dict[str, Any]: - """使用 pydantic-settings 校验 TOML 内容并返回 dict。""" +PRIMARY_CONFIG_SUFFIX = ".toml" +LEGACY_CONFIG_SUFFIX = ".json" + + +def dump_json(data: dict[str, Any]) -> str: + """公共 JSON 序列化入口。""" + + return json.dumps(data, ensure_ascii=False, indent=4) - raw_text = path.read_text(encoding="utf-8") - if raw_text.strip() == "": - return {} - raw_data = tomllib.loads(raw_text) +def _toml_key(key: str) -> str: + """生成兼容 UUID 等特殊字符的 TOML 键名。""" - dynamic_settings_cls = type( - "DynamicTomlSettings", - (BaseSettings,), - { - "model_config": SettingsConfigDict(extra="allow"), - }, - ) + return json.dumps(str(key), ensure_ascii=False) - loaded = dynamic_settings_cls.model_validate(raw_data) - data = loaded.model_dump(mode="python") - extra = getattr(loaded, "__pydantic_extra__", None) - if isinstance(extra, dict): - data.update(extra) - return TypeAdapter(dict[str, Any]).validate_python(data) +def _toml_path(path: tuple[str, ...]) -> str: + """生成 TOML 表头路径。""" + + return ".".join(_toml_key(part) for part in path) def _toml_scalar(value: Any) -> str: + """序列化 TOML 标量值。""" + if isinstance(value, bool): return "true" if value else "false" - if isinstance(value, (int, float)): + if isinstance(value, int): return str(value) + if isinstance(value, float): + return repr(value) if value is None: return '""' return json.dumps(str(value), ensure_ascii=False) def _toml_inline(value: Any) -> str: + """序列化内联 TOML 值,支持数组和内联表。""" + + if isinstance(value, dict): + items = ", ".join( + f"{_toml_key(str(key))} = {_toml_inline(item)}" + for key, item in value.items() + ) + return "{ " + items + " }" if isinstance(value, list): return "[" + ", ".join(_toml_inline(item) for item in value) + "]" return _toml_scalar(value) -def _dump_toml(data: dict[str, Any]) -> str: +def dump_toml(data: dict[str, Any]) -> str: + """公共 TOML 序列化入口。""" + lines: list[str] = [] - def emit_table(prefix: str, obj: dict[str, Any]) -> None: + def emit_table(path: tuple[str, ...], obj: dict[str, Any]) -> None: scalars: list[tuple[str, Any]] = [] children: list[tuple[str, dict[str, Any]]] = [] for key, value in obj.items(): if isinstance(value, dict): - children.append((key, value)) + children.append((str(key), value)) else: - scalars.append((key, value)) + scalars.append((str(key), value)) - if prefix: - lines.append(f"[{prefix}]") + if path: + lines.append(f"[{_toml_path(path)}]") for key, value in scalars: - lines.append(f"{key} = {_toml_inline(value)}") + lines.append(f"{_toml_key(key)} = {_toml_inline(value)}") if scalars and children: lines.append("") for index, (key, child) in enumerate(children): - child_prefix = f"{prefix}.{key}" if prefix else key - emit_table(child_prefix, child) + emit_table((*path, key), child) if index != len(children) - 1: lines.append("") - emit_table("", data) + emit_table((), data) content = "\n".join(lines).strip() return f"{content}\n" if content else "" -def dump_toml(data: dict[str, Any]) -> str: - """公共 TOML 序列化入口。""" +def _load_json_config(path: Path) -> dict[str, Any]: + raw_text = path.read_text(encoding="utf-8") + if raw_text.strip() == "": + return {} + + loaded = json.loads(raw_text) + return loaded if isinstance(loaded, dict) else {} + + +def _load_toml_config(path: Path) -> dict[str, Any]: + raw_text = path.read_text(encoding="utf-8") + if raw_text.strip() == "": + return {} - return _dump_toml(data) + loaded = tomllib.loads(raw_text) + return loaded if isinstance(loaded, dict) else {} def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path | None]: - legacy_json_file = path.with_suffix(".json") + legacy_json_file = path.with_suffix(LEGACY_CONFIG_SUFFIX) if legacy_json_file.exists() and (not path.exists() or path.stat().st_size == 0): try: - return json.loads(legacy_json_file.read_text(encoding="utf-8")), legacy_json_file + return _load_json_config(legacy_json_file), legacy_json_file except json.JSONDecodeError: return {}, legacy_json_file @@ -148,22 +167,25 @@ def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path return {}, legacy_json_file if legacy_json_file.exists() else None try: - return _load_toml_with_pydantic_settings(path), legacy_json_file if legacy_json_file.exists() else None - except Exception: - with suppress(Exception): - return tomllib.loads(path.read_text(encoding="utf-8")), legacy_json_file if legacy_json_file.exists() else None + return _load_toml_config(path), legacy_json_file if legacy_json_file.exists() else None + except tomllib.TOMLDecodeError: + if legacy_json_file.exists(): + with suppress(json.JSONDecodeError): + return _load_json_config(legacy_json_file), legacy_json_file return {}, legacy_json_file if legacy_json_file.exists() else None -def _backup_legacy_json_if_needed(current_file: Path, legacy_json_file: Path | None) -> None: - if legacy_json_file is None or not legacy_json_file.exists(): +def _backup_legacy_config_if_needed( + current_file: Path, legacy_file: Path | None +) -> None: + if legacy_file is None or not legacy_file.exists(): return if not current_file.exists() or current_file.stat().st_size == 0: return - legacy_backup = legacy_json_file.with_suffix(".json.bak") + legacy_backup = legacy_file.with_suffix(f"{legacy_file.suffix}.bak") if not legacy_backup.exists(): - legacy_json_file.replace(legacy_backup) + legacy_file.replace(legacy_backup) class ValidatorBase(ABC): @@ -924,10 +946,10 @@ async def connect(self, path: Path): Parameters ---------- path: Path - 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 + 配置文件路径, 必须为 TOML 文件, 如果不存在则会创建 """ - if path.suffix != ".toml": + if path.suffix != PRIMARY_CONFIG_SUFFIX: raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") if self.is_locked: @@ -938,13 +960,13 @@ async def connect(self, path: Path): if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.touch() - data, legacy_json_file = _load_config_with_legacy_migration(self.file) + data, legacy_file = _load_config_with_legacy_migration(self.file) await self.load(data) await self.add_save_method(self.save) - _backup_legacy_json_if_needed(self.file, legacy_json_file) + _backup_legacy_config_if_needed(self.file, legacy_file) async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] @@ -958,7 +980,7 @@ async def add_save_method( 保存方法 """ - if save_method != self.save: + if save_method != self.save and save_method not in self._save_methods: self._save_methods.append(save_method) for sub_config in self._multiple_config_index.values(): @@ -1114,7 +1136,7 @@ async def save(self) -> None: self.file.parent.mkdir(parents=True, exist_ok=True) self.file.write_text( - _dump_toml(await self.toDict(if_decrypt=False)), + dump_toml(await self.toDict(if_decrypt=False)), encoding="utf-8", ) @@ -1171,7 +1193,7 @@ class MultipleConfig(Generic[T]): """ 多配置项管理类 - 这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。 + 这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 TOML 文件中。 允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。 Parameters @@ -1222,10 +1244,10 @@ async def connect(self, path: Path): Parameters ---------- path: Path - 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 + 配置文件路径, 必须为 TOML 文件, 如果不存在则会创建 """ - if path.suffix != ".toml": + if path.suffix != PRIMARY_CONFIG_SUFFIX: raise ValueError("配置文件必须是带有 '.toml' 扩展名的 TOML 文件。") if self.is_locked: @@ -1236,13 +1258,13 @@ async def connect(self, path: Path): if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.touch() - data, legacy_json_file = _load_config_with_legacy_migration(self.file) + data, legacy_file = _load_config_with_legacy_migration(self.file) await self.load(data) await self.add_save_method(self.save) - _backup_legacy_json_if_needed(self.file, legacy_json_file) + _backup_legacy_config_if_needed(self.file, legacy_file) async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] @@ -1256,7 +1278,7 @@ async def add_save_method( 保存方法, 必须是一个协程函数, 无参数, 无返回值 """ - if save_method != self.save: + if save_method != self.save and save_method not in self._save_methods: self._save_methods.append(save_method) for sub_config in self.data.values(): @@ -1381,7 +1403,7 @@ async def save(self): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.write_text( - _dump_toml(await self.toDict(if_decrypt=False)), + dump_toml(await self.toDict(if_decrypt=False)), encoding="utf-8", ) diff --git a/app/core/config.py b/app/core/config/manager.py similarity index 99% rename from app/core/config.py rename to app/core/config/manager.py index 54c4026e..6cd89a33 100644 --- a/app/core/config.py +++ b/app/core/config/manager.py @@ -140,7 +140,7 @@ def __init__(self) -> None: truststore.inject_into_ssl() def _resolve_config_path(self, stem: str) -> Path: - """返回 TOML 配置路径(内部会自动处理同名 JSON 的迁移兼容)。""" + """返回运行期 TOML 配置路径。""" return self.config_path / f"{stem}.toml" @@ -155,18 +155,25 @@ async def _connect_runtime_configs(self) -> None: await self.ToolsConfig.connect(self._resolve_config_path("ToolsConfig")) def _read_mapping_config(self, path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + text = path.read_text(encoding="utf-8") + if not text.strip(): + return {} + if path.suffix == ".toml": - return tomllib.loads(text) if text.strip() else {} - return json.loads(text) + data = tomllib.loads(text) + else: + data = json.loads(text) + return data if isinstance(data, dict) else {} def _write_mapping_config(self, path: Path, data: dict[str, Any]) -> None: if path.suffix == ".toml": path.write_text(dump_toml(data), encoding="utf-8") - else: - path.write_text( - json.dumps(data, ensure_ascii=False, indent=4), encoding="utf-8" - ) + return + + path.write_text(json.dumps(data, ensure_ascii=False, indent=4), encoding="utf-8") async def init_config(self) -> None: """初始化配置管理""" @@ -819,7 +826,7 @@ async def remove_privacy_info( Path(os.environ["APPDATA"]) ): confg["Script"][path] = ( - f"%APPDATA%/{Path(confg["Script"][path]).relative_to(Path(os.environ["APPDATA"]))}" + f"%APPDATA%/{Path(confg['Script'][path]).relative_to(Path(os.environ['APPDATA']))}" ) confg["Info"]["RootPath"] = str(Path(r"C:/脚本根目录")) diff --git a/app/models/pydantic_base.py b/app/core/config/pydantic.py similarity index 92% rename from app/models/pydantic_base.py rename to app/core/config/pydantic.py index b5f0ab31..b7f91bd6 100644 --- a/app/models/pydantic_base.py +++ b/app/core/config/pydantic.py @@ -1,5 +1,4 @@ from __future__ import annotations -# pyright: reportPrivateUsage=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false import asyncio import inspect @@ -7,11 +6,11 @@ from collections.abc import Callable, Coroutine from typing import Any, ClassVar -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, PrivateAttr -from .config_base import ( +from .base import ( MultipleConfig, - _backup_legacy_json_if_needed, + _backup_legacy_config_if_needed, _load_config_with_legacy_migration, dump_toml, ) @@ -27,13 +26,10 @@ class PydanticConfigBase(BaseModel): model_config = ConfigDict(extra="allow", validate_assignment=True) LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = {} - - def __init__(self, **data: Any): - super().__init__(**data) - self._file: Path | None = None - self._is_locked = False - self._save_methods: list[SaveMethod] = [] - self._bindings: dict[tuple[str, str], list[Slot]] = {} + _file: Path | None = PrivateAttr(default=None) + _is_locked: bool = PrivateAttr(default=False) + _save_methods: list[SaveMethod] = PrivateAttr(default_factory=list) + _bindings: dict[tuple[str, str], list[Slot]] = PrivateAttr(default_factory=dict) @property def file(self) -> Path | None: @@ -74,10 +70,10 @@ async def connect(self, path: Path): self._file.parent.mkdir(parents=True, exist_ok=True) self._file.touch() - data, legacy_json_file = _load_config_with_legacy_migration(self._file) + data, legacy_file = _load_config_with_legacy_migration(self._file) await self.load(data) await self.add_save_method(self.save) - _backup_legacy_json_if_needed(self._file, legacy_json_file) + _backup_legacy_config_if_needed(self._file, legacy_file) async def add_save_method(self, save_method: SaveMethod): if save_method != self.save and save_method not in self._save_methods: diff --git a/app/models/type.py b/app/core/config/types.py similarity index 100% rename from app/models/type.py rename to app/core/config/types.py diff --git a/app/core/task_manager.py b/app/core/task_manager.py index b6031d2e..d5a85847 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -24,7 +24,8 @@ import asyncio from typing import Dict, Literal -from .config import Config, MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig +from app.models import GeneralConfig, MaaConfig, MaaEndConfig, SrcConfig +from .config import Config from app.services import System from app.models.task import TaskItem, ScriptItem, UserItem, TaskExecuteBase from app.utils import get_logger diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py deleted file mode 100644 index d4736718..00000000 --- a/app/models/ConfigBase.py +++ /dev/null @@ -1,1514 +0,0 @@ -# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software -# Copyright © 2024-2025 DLmaster361 -# Copyright © 2025 MoeSnowyFox -# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with AUTO-MAS. If not, see . - -# Contact: DLmaster_361@163.com - - -from __future__ import annotations - -# pyright: reportMissingParameterType=false, reportUnknownParameterType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownLambdaType=false, reportDeprecated=false, reportInvalidTypeForm=false, reportGeneralTypeIssues=false -import os -import json -import tomllib -import uuid -import shlex -import inspect -import asyncio -import pyautogui -import win32com.client -from copy import deepcopy -from urllib.parse import urlparse -from datetime import datetime -from contextlib import suppress -from abc import ABC, abstractmethod -from pathlib import Path -from collections.abc import Callable, Coroutine -from typing import Any, Protocol, TypeVar, Generic -from pydantic import TypeAdapter -from pydantic_settings import BaseSettings, SettingsConfigDict - -from app.utils import get_logger, dpapi_encrypt, dpapi_decrypt -from app.utils.constants import ( - RESERVED_NAMES, - ILLEGAL_CHARS, - DEFAULT_DATETIME, - EMULATOR_PATH_BOOK, -) - -logger = get_logger("配置基类") - - -def _load_toml_with_pydantic_settings(path: Path) -> dict[str, Any]: - """使用 pydantic-settings 校验 TOML 内容并返回 dict。""" - - raw_text = path.read_text(encoding="utf-8") - if raw_text.strip() == "": - return {} - - raw_data = tomllib.loads(raw_text) - - dynamic_settings_cls = type( - "DynamicTomlSettings", - (BaseSettings,), - { - "model_config": SettingsConfigDict(extra="allow"), - }, - ) - - loaded = dynamic_settings_cls.model_validate(raw_data) - data = loaded.model_dump(mode="python") - extra = getattr(loaded, "__pydantic_extra__", None) - if isinstance(extra, dict): - data.update(extra) - - return TypeAdapter(dict[str, Any]).validate_python(data) - - -def _toml_scalar(value: Any) -> str: - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, (int, float)): - return str(value) - if value is None: - return '""' - return json.dumps(str(value), ensure_ascii=False) - - -def _toml_inline(value: Any) -> str: - if isinstance(value, list): - return "[" + ", ".join(_toml_inline(item) for item in value) + "]" - return _toml_scalar(value) - - -def _dump_toml(data: dict[str, Any]) -> str: - lines: list[str] = [] - - def emit_table(prefix: str, obj: dict[str, Any]) -> None: - scalars: list[tuple[str, Any]] = [] - children: list[tuple[str, dict[str, Any]]] = [] - - for key, value in obj.items(): - if isinstance(value, dict): - children.append((key, value)) - else: - scalars.append((key, value)) - - if prefix: - lines.append(f"[{prefix}]") - - for key, value in scalars: - lines.append(f"{key} = {_toml_inline(value)}") - - if scalars and children: - lines.append("") - - for index, (key, child) in enumerate(children): - child_prefix = f"{prefix}.{key}" if prefix else key - emit_table(child_prefix, child) - if index != len(children) - 1: - lines.append("") - - emit_table("", data) - content = "\n".join(lines).strip() - return f"{content}\n" if content else "" - - -def dump_toml(data: dict[str, Any]) -> str: - """公共 TOML 序列化入口。""" - - return _dump_toml(data) - - -def _load_config_with_legacy_migration( - path: Path, -) -> tuple[dict[str, Any], Path | None]: - legacy_json_file = path.with_suffix(".json") - - if legacy_json_file.exists() and (not path.exists() or path.stat().st_size == 0): - try: - return json.loads( - legacy_json_file.read_text(encoding="utf-8") - ), legacy_json_file - except json.JSONDecodeError: - return {}, legacy_json_file - - if not path.exists(): - return {}, legacy_json_file if legacy_json_file.exists() else None - - try: - return _load_toml_with_pydantic_settings( - path - ), legacy_json_file if legacy_json_file.exists() else None - except Exception: - with suppress(Exception): - return tomllib.loads( - path.read_text(encoding="utf-8") - ), legacy_json_file if legacy_json_file.exists() else None - return {}, legacy_json_file if legacy_json_file.exists() else None - - -def _backup_legacy_json_if_needed( - current_file: Path, legacy_json_file: Path | None -) -> None: - if legacy_json_file is None or not legacy_json_file.exists(): - return - if not current_file.exists() or current_file.stat().st_size == 0: - return - - legacy_backup = legacy_json_file.with_suffix(".json.bak") - if not legacy_backup.exists(): - legacy_json_file.replace(legacy_backup) - - -class ValidatorBase(ABC): - """基础配置验证器""" - - @abstractmethod - def validate(self, value: Any) -> bool: - """验证值是否合法""" - pass - - @abstractmethod - def correct(self, value: Any) -> Any: - """修正非法值""" - pass - - -class StringValidator(ValidatorBase): - """字符串验证器""" - - def validate(self, value): - return isinstance(value, str) - - def correct(self, value): - return value if self.validate(value) else "" - - -class RangeValidator(ValidatorBase): - """范围验证器""" - - def __init__(self, min: int | float, max: int | float): - self.min = min - self.max = max - self.range = (min, max) - - def validate(self, value): - if not isinstance(value, (int, float)): - return False - return self.min <= value <= self.max - - def correct(self, value): - if not isinstance(value, (int, float)): - try: - value = float(value) - except TypeError: - return self.min - return min(max(self.min, value), self.max) - - -class OptionsValidator(ValidatorBase): - """选项验证器""" - - def __init__(self, options: list[Any]): - if not options: - raise ValueError("可选项不能为空") - - self.options = options - - def validate(self, value): - return value in self.options - - def correct(self, value): - return value if self.validate(value) else self.options[0] - - -class MultipleOptionsValidator(ValidatorBase): - """多选选项验证器""" - - def __init__(self, options: list[Any]): - if not options: - raise ValueError("可选项不能为空") - - self.options = options - - def validate(self, value): - if not isinstance(value, list): - return False - - return all(item in self.options for item in value) - - def correct(self, value): - return value if self.validate(value) else [] - - -class UUIDValidator(ValidatorBase): - """UUID验证器""" - - def validate(self, value): - try: - uuid.UUID(value) - return True - except (TypeError, ValueError): - return False - - def correct(self, value): - return value if self.validate(value) else str(uuid.uuid4()) - - -class MultipleUIDValidator(ValidatorBase): - """多配置管理类UID验证器""" - - def __init__( - self, - default: Any, - related_config: dict[str, "MultipleConfig[Any]"], - config_name: str, - ): - self.default = default - self.related_config = related_config - self.config_name = config_name - - def validate(self, value): - if value == self.default: - return True - if not isinstance(value, str): - return False - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return False - if uid in self.related_config.get(self.config_name, {}): - return True - return False - - def correct(self, value): - if self.validate(value): - return value - return self.default - - -class DateTimeValidator(ValidatorBase): - """日期时间验证器""" - - def __init__(self, date_format: str) -> None: - if not date_format: - raise ValueError("日期时间格式不能为空") - self.date_format = date_format - - def validate(self, value): - if not isinstance(value, str): - return False - try: - datetime.strptime(value, self.date_format) - return True - except ValueError: - return False - - def correct(self, value): - if not isinstance(value, str): - return DEFAULT_DATETIME.strftime(self.date_format) - try: - datetime.strptime(value, self.date_format) - return value - except ValueError: - return DEFAULT_DATETIME.strftime(self.date_format) - - -class JSONValidator(ValidatorBase): - def __init__(self, tpye: type[dict[str, Any]] | type[list[Any]] = dict) -> None: - self.type = tpye - - def validate(self, value): - if not isinstance(value, str): - return False - try: - data = json.loads(value) - if isinstance(data, self.type): - return True - else: - return False - except json.JSONDecodeError: - return False - - def correct(self, value): - return ( - value if self.validate(value) else ("{ }" if self.type is dict else "[ ]") - ) - - -class EncryptValidator(ValidatorBase): - """加密数据验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - try: - dpapi_decrypt(value) - return True - except Exception: - return False - - def correct(self, value: Any) -> Any: - return value if self.validate(value) else dpapi_encrypt("数据损坏, 请重新设置") - - -class VirtualConfigValidator(ValidatorBase): - """虚拟配置验证器""" - - def __init__(self, function: Callable[[], str]): - self.function = function - self.if_init = False - - def validate(self, value): - if not self.if_init: - self.if_init = True - return True - return False - - def correct(self, value): - try: - return self.function() - except Exception as e: - return str(e) - - -class BoolValidator(ValidatorBase): - """布尔值验证器""" - - def validate(self, value): - if not isinstance(value, bool): - return False - return True - - def correct(self, value): - return value if self.validate(value) else False - - -class FileValidator(ValidatorBase): - """文件路径验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - # 允许空字符串(表示未设置路径) - if value == "": - return True - if not Path(value).is_absolute(): - return False - if Path(value).suffix == ".lnk": - return False - return True - - def correct(self, value): - if not isinstance(value, str): - value = str(Path.cwd()) - # 空字符串直接返回 - if value == "": - return "" - if "%APPDATA%" in value: - value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") - if not Path(value).is_absolute(): - value = Path(value).resolve().as_posix() - if Path(value).suffix == ".lnk": - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(value) - value = shortcut.TargetPath - except Exception: - pass - return Path(value).resolve().as_posix() - - -class FolderValidator(ValidatorBase): - """文件夹路径验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - if not Path(value).is_absolute(): - return False - if not Path(value).is_dir(): - return False - return True - - def correct(self, value): - if not isinstance(value, str): - value = str(Path.cwd()) - if "%APPDATA%" in value: - value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") - if not Path(value).is_dir(): - value = Path(value).with_suffix("") - return Path(value).resolve().as_posix() - - -class EmulatorPathValidator(FileValidator): - """模拟器管理器路径验证器""" - - def __init__(self, emulator_type: ConfigItem) -> None: - super().__init__() - - self.emulator_type = emulator_type - - def validate(self, value): - if not isinstance(value, str): - return False - # 允许空字符串(表示未设置路径) - if value == "": - return True - if not Path(value).is_absolute(): - return False - if Path(value).suffix == ".lnk": - return False - - path = Path(value) - - if not path.is_file() or not os.access(path, os.X_OK): - return False - - if ( - self.emulator_type.getValue() in EMULATOR_PATH_BOOK - and path.name - != EMULATOR_PATH_BOOK[self.emulator_type.getValue()]["executables"][0] - ): - return False - - return True - - def correct(self, value): - if not isinstance(value, str): - value = str(Path.cwd()) - # 空字符串直接返回 - if value == "": - return "" - if "%APPDATA%" in value: - value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") - if not Path(value).is_absolute(): - value = Path(value).resolve().as_posix() - if Path(value).suffix == ".lnk": - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(value) - value = shortcut.TargetPath - except Exception: - pass - - # 不支持矫正的模拟器类型直接返回路径字符串 - if self.emulator_type.getValue() not in EMULATOR_PATH_BOOK: - return Path(value).resolve().as_posix() - - try: - # 从给定路径向上或向下搜索模拟器主管理器程序的完整路径 - path = Path(value) - - # 获取模拟器配置信息 - config = EMULATOR_PATH_BOOK[self.emulator_type.getValue()] - executables = config["executables"] - - # 第一个可执行文件是主管理器程序(优先级最高) - primary_exe = executables[0] - - # 如果输入的是文件,先获取其父目录 - if path.is_file(): - path = path.parent - - # 1. 首先检查当前目录是否直接包含主管理器程序 - # 如果用户给的就是正确的主程序路径,直接返回 - primary_exe_path = path / primary_exe - if primary_exe_path.exists(): - result = str(primary_exe_path) - return result - - # 2. 向上搜索父目录,找到直接包含主管理器程序的目录(最多3层) - candidates = [] - current = path - for level in range(3): - parent = current.parent - if parent == current: # 已到达根目录 - break - - # 只接受直接包含主管理器程序的目录 - parent_exe_path = parent / primary_exe - if parent_exe_path.exists(): - candidates.append( - { - "path": parent, - "exe_path": parent_exe_path, - "depth": len(parent.parts), - "level": level + 1, - } - ) - - current = parent - - # 如果找到了候选目录,选择最优的(深度最小的,即最接近根目录的) - if candidates: - # 排序策略:深度越小越好(越靠近根目录) - candidates.sort(key=lambda x: x["depth"]) - - best_candidate = candidates[0] - result = str(best_candidate["exe_path"]) - - return result - - # 3. 如果向上没找到,尝试向下搜索子目录(仅1层,且必须直接包含主管理器程序) - try: - for subdir in path.iterdir(): - if subdir.is_dir(): - subdir_exe_path = subdir / primary_exe - # 只接受直接包含主管理器程序的子目录 - if subdir_exe_path.exists(): - result = str(subdir_exe_path) - return result - except PermissionError: - pass - - # 4. 检查兄弟目录(必须直接包含主管理器程序) - if path.parent != path: - try: - for sibling in path.parent.iterdir(): - if sibling.is_dir() and sibling != path: - sibling_exe_path = sibling / primary_exe - # 只接受直接包含主管理器程序的兄弟目录 - if sibling_exe_path.exists(): - result = str(sibling_exe_path) - return result - except PermissionError: - pass - - return Path(value).resolve().as_posix() - except Exception: - return Path(value).resolve().as_posix() - - -class UserNameValidator(ValidatorBase): - """用户名验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - - if not value or not value.strip(): - return False - - if value != value.strip() or value != value.strip("."): - return False - - if any(char in ILLEGAL_CHARS for char in value): - return False - - if value.upper() in RESERVED_NAMES: - return False - if len(value) > 255: - return False - - return True - - def correct(self, value): - if not isinstance(value, str): - value = "默认用户名" - - value = value.strip().strip(".") - - value = "".join(char for char in value if char not in ILLEGAL_CHARS) - - if value.upper() in RESERVED_NAMES or not value: - value = "默认用户名" - - if len(value) > 255: - value = value[:255] - - return value - - -class KeyValidator(ValidatorBase): - """键盘按键格式验证器""" - - def __init__(self, default: str = ""): - self.default = default - - def validate(self, value: Any) -> bool: - return value in pyautogui.KEYBOARD_KEYS - - def correct(self, value: Any) -> Any: - return value if self.validate(value) else self.default - - -class URLValidator(ValidatorBase): - """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): - 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): - return value if self.validate(value) else self.default - - -class ArgumentValidator(ValidatorBase): - def validate(self, value): - if not isinstance(value, str): - return False - try: - shlex.split(value.strip()) - return True - except ValueError: - return False - - def correct(self, value): - return value if self.validate(value) else "" - - -class AdvancedArgumentValidator(ValidatorBase): - def validate(self, value): - if not isinstance(value, str): - return False - try: - for segment in value.split("|"): - segment = segment.strip() - if not segment: - continue - param_str = segment.split("%", 1)[-1].strip() - shlex.split(param_str) - return True - except ValueError: - return False - - def correct(self, value): - return value if self.validate(value) else "" - - -class ConfigItem: - """配置项""" - - def __init__( - self, - group: str, - name: str, - default: Any, - validator: ValidatorBase = StringValidator(), - legacy_group: str | None = None, - legacy_name: str | None = None, - ): - """ - Parameters - ---------- - group: str - 配置项分组名称 - - name: str - 配置项字段名称 - - default: Any - 配置项默认值 - - validator: ValidatorBase - 配置项验证器, 默认为 None, 表示不进行验证 - """ - self.group = group - self.name = name - self.value: Any = default - self.validator = validator - self.legacy_group_name = ( - (legacy_group or group, legacy_name or name) - if legacy_group or legacy_name - else None - ) - self.is_locked = False - self._slots: list[Callable[[Any], Any]] = [] - - if not self.validator.validate(self.value): - raise ValueError( - f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法" - ) - - def setValue(self, value: Any): - """ - 设置配置项值, 将自动进行验证和修正 - - Parameters - ---------- - value: Any - 要设置的值, 可以是任何合法类型 - """ - - if ( - dpapi_decrypt(self.value) - if isinstance(self.validator, EncryptValidator) - else self.value - ) == value: - return - - if self.is_locked: - raise ValueError(f"配置项 '{self.group}.{self.name}' 已锁定, 无法修改") - - # deepcopy new value - try: - self.value = deepcopy(value) - except Exception: - self.value = value - - if isinstance(self.validator, EncryptValidator): - if self.validator.validate(self.value): - self.value = self.value - else: - self.value = dpapi_encrypt(self.value) - - if not self.validator.validate(self.value): - self.value = self.validator.correct(self.value) - - if len(self._slots) > 0: - asyncio.create_task(self._emit_signal(self.value)) - - def getValue(self, if_decrypt: bool = True) -> Any: - """ - 获取配置项值 - """ - - v = ( - self.value - if self.validator.validate(self.value) - else self.validator.correct(self.value) - ) - - if isinstance(self.validator, EncryptValidator) and if_decrypt: - return dpapi_decrypt(v) - return v - - def bind(self, slot: Callable[[Any], Any]): - """ - 连接槽函数到配置项修改信号 - - Parameters - ---------- - slot: Callable[[Any], Any] - 槽函数,接收新值作为参数,支持同步和异步函数 - """ - if not callable(slot): - raise TypeError("槽函数必须是可调用对象") - - if slot not in self._slots: - self._slots.append(slot) - - def unbind(self, slot: Callable[[Any], Any]): - """ - 断开槽函数连接 - - Parameters - ---------- - slot: Callable[[Any], Any] - 要断开的槽函数 - """ - if slot in self._slots: - self._slots.remove(slot) - - def unbind_all(self): - """断开所有槽函数连接""" - self._slots.clear() - - @logger.catch - async def _emit_signal(self, value: Any) -> None: - """ - 执行所有连接的槽函数, 将新值作为参数传递 - - Parameters - ---------- - value: Any - 新值, 已经过验证和修正 - """ - - for slot in self._slots: - if inspect.iscoroutinefunction(slot): - await slot(value) - else: - slot(value) - - def lock(self): - """ - 锁定配置项, 锁定后无法修改配置项值 - """ - self.is_locked = True - - def unlock(self): - """ - 解锁配置项, 解锁后可以修改配置项值 - """ - self.is_locked = False - - -class ConfigBase: - """ - 配置基类 - - 这个类提供了基本的配置项管理功能, 包括连接配置文件、加载配置数据、获取和设置配置项值等。 - - 此类不支持直接实例化, 必须通过子类来实现具体的配置项, - 请继承此类并在子类中定义具体的配置项, 并在定义完成后调用父类的 `__init__` 方法。 - 若将配置项设为类属性, 则所有实例都会共享同一份配置项数据。 - 若将配置项设为实例属性, 则每个实例都会有独立的配置项数据。 - 子配置项可以是 `MultipleConfig` 的实例。 - """ - - def __init__(self): - self.file: Path | None = None - self.is_locked = False - self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] - - # 配置项索引 - self._config_item_index: dict[str, dict[str, ConfigItem]] = {} - self._multiple_config_index: dict[str, "MultipleConfig[Any]"] = {} - for name in dir(self): - item = getattr(self, name) - - if isinstance(item, ConfigItem): - if not self._config_item_index.get(item.group): - self._config_item_index[item.group] = {} - self._config_item_index[item.group][item.name] = item - - elif isinstance(item, MultipleConfig): - self._multiple_config_index[name] = item - - async def connect(self, path: Path): - """ - 将配置数据绑定到指定配置文件 - - Parameters - ---------- - path: Path - 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 - """ - - if path.suffix != ".toml": - raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self.file = path - - if not self.file.exists(): - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.touch() - data, legacy_json_file = _load_config_with_legacy_migration(self.file) - - await self.load(data) - - await self.add_save_method(self.save) - - _backup_legacy_json_if_needed(self.file, legacy_json_file) - - async def add_save_method( - self, save_method: Callable[[], Coroutine[Any, Any, None]] - ): - """ - 添加父配置项的保存方法 - - Parameters - ---------- - save_method: Callable[[], Coroutine[Any, Any, None]] - 保存方法 - """ - - if save_method != self.save: - self._save_methods.append(save_method) - - for sub_config in self._multiple_config_index.values(): - await sub_config.add_save_method(save_method) - - async def load(self, data: dict[str, Any]): - """ - 从字典加载配置数据 - - 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigItem 实例中。 - 如果字典中包含 "SubConfigsInfo" 键, 则会加载子配置项, 这些子配置项应该是 MultipleConfig 的实例。 - - Parameters - ---------- - data: dict - 配置数据字典 - """ - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - # 加载多配置项类型数据 - sub_configs = data.pop("SubConfigsInfo", {}) - if not isinstance(sub_configs, dict): - sub_configs = {} - for name, sub_config in self._multiple_config_index.items(): - data_for_sub_config = sub_configs.get(name) - if isinstance(data_for_sub_config, dict): - await sub_config.load(data_for_sub_config) - - for group, info in self._config_item_index.items(): - for name, item in info.items(): - try: - item.setValue(data[group][name]) - except Exception: - if item.legacy_group_name is not None: - with suppress(Exception): - item.setValue( - data[item.legacy_group_name[0]][ - item.legacy_group_name[1] - ] - ) - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - """将配置项转换为字典""" - - data = {} - - for group, info in self._config_item_index.items(): - for name, item in info.items(): - data.setdefault(group, {})[name] = item.getValue(if_decrypt) - - for name, item in self._multiple_config_index.items(): - if not data.get("SubConfigsInfo"): - data["SubConfigsInfo"] = {} - data["SubConfigsInfo"][name] = await item.toDict( - if_decrypt, regenerate_uuids - ) - - return data - - def get(self, group: str, name: str) -> Any: - """获取配置项的值""" - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - return self._config_item_index[group][name].getValue() - - async def set(self, group: str, name: str, value: Any): - """ - 设置配置项的值 - - Parameters - ---------- - group: str - 配置项分组名称 - name: str - 配置项名称 - value: Any - 配置项新值 - """ - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self._config_item_index[group][name].setValue(value) - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - def bind(self, group: str, name: str, slot: Callable[[Any], Any]): - """ - 连接槽函数到配置项修改信号 - - Parameters - ---------- - group: str - 配置项分组名称 - name: str - 配置项名称 - slot: Callable[[Any], Any] - 槽函数,接收新值作为参数,支持同步和异步函数 - """ - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self._config_item_index[group][name].bind(slot) - - def unbind(self, group: str, name: str, slot: Callable[[Any], Any]): - """ - 断开槽函数连接 - - Parameters - ---------- - group: str - 配置项分组名称 - name: str - 配置项名称 - slot: Callable[[Any], Any] - 要断开的槽函数 - """ - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self._config_item_index[group][name].unbind(slot) - - async def save(self) -> None: - """保存配置""" - - if not self.file: - raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") - - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.write_text( - _dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) - - async def lock(self): - """ - 锁定配置项, 锁定后无法修改配置项值 - """ - - self.is_locked = True - - for group in self._config_item_index.values(): - for item in group.values(): - item.lock() - for config in self._multiple_config_index.values(): - await config.lock() - - async def unlock(self): - """ - 解锁配置项, 解锁后可以修改配置项值 - """ - - self.is_locked = False - - for group in self._config_item_index.values(): - for item in group.values(): - item.unlock() - for config in self._multiple_config_index.values(): - await config.unlock() - - -class _ConfigLike(Protocol): - @property - def is_locked(self) -> bool: ... - - async def add_save_method( - self, save_method: Callable[[], Coroutine[Any, Any, None]] - ) -> None: ... - - async def load(self, data: dict[str, Any]) -> None: ... - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: ... - - async def lock(self) -> None: ... - - async def unlock(self) -> None: ... - - -T = TypeVar("T", bound=_ConfigLike) - - -class MultipleConfig(Generic[T]): - """ - 多配置项管理类 - - 这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。 - 允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。 - - Parameters - ---------- - sub_config_type: List[type] - 子配置项的类型列表, 必须是 ConfigBase 的子类 - """ - - def __init__(self, sub_config_type: list[type[T]]): - if not sub_config_type: - raise ValueError("子配置项类型列表不能为空") - - self.sub_config_type: dict[str, type[T]] = { - _.__name__: _ for _ in sub_config_type - } - self.file: Path | None = None - self.order: list[uuid.UUID] = [] - self.data: dict[uuid.UUID, T] = {} - self.is_locked = False - self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] - - def __getitem__(self, key: uuid.UUID) -> T: - """允许通过 config[uuid] 访问配置项""" - if key not in self.data: - raise KeyError(f"配置项 '{key}' 不存在") - return self.data[key] - - def __contains__(self, key: uuid.UUID) -> bool: - """允许使用 uuid in config 检查是否存在""" - return key in self.data - - def __len__(self) -> int: - """允许使用 len(config) 获取配置项数量""" - return len(self.data) - - def __repr__(self) -> str: - """更好的字符串表示""" - return f"MultipleConfig(items={len(self.data)}, types={list(self.sub_config_type.keys())})" - - def __str__(self) -> str: - """用户友好的字符串表示""" - return f"MultipleConfig with {len(self.data)} items" - - async def connect(self, path: Path): - """ - 将配置文件连接到指定配置文件 - - Parameters - ---------- - path: Path - 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建 - """ - - if path.suffix != ".toml": - raise ValueError("配置文件必须是带有 '.toml' 扩展名的 TOML 文件。") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self.file = path - - if not self.file.exists(): - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.touch() - data, legacy_json_file = _load_config_with_legacy_migration(self.file) - - await self.load(data) - - await self.add_save_method(self.save) - - _backup_legacy_json_if_needed(self.file, legacy_json_file) - - async def add_save_method( - self, save_method: Callable[[], Coroutine[Any, Any, None]] - ): - """ - 添加父配置项的保存方法 - - Parameters - ---------- - save_method: Callable[[], Coroutine[Any, Any, None]] - 保存方法, 必须是一个协程函数, 无参数, 无返回值 - """ - - if save_method != self.save: - self._save_methods.append(save_method) - - for sub_config in self.data.values(): - await sub_config.add_save_method(save_method) - - async def load(self, data: dict[str, Any]): - """ - 从字典加载配置数据 - - 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigBase 实例中。 - 如果字典中包含 "instances" 键, 则会加载子配置项, 这些子配置项应该是 ConfigBase 子类的实例。 - 如果字典中没有 "instances" 键, 则清空当前配置项。 - - Parameters - ---------- - data: dict - 配置数据字典 - """ - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self.order = [] - self.data = {} - - if not data.get("instances"): - return - - for instance in data["instances"]: - if not isinstance(instance, dict): - continue - - uid_str = instance.get("uid") - if not isinstance(uid_str, str): - continue - - instance_data = data.get(uid_str) - if not isinstance(instance_data, dict): - continue - - type_name = instance.get("type") - - if type_name in self.sub_config_type: - self.order.append(uuid.UUID(uid_str)) - self.data[self.order[-1]] = self.sub_config_type[type_name]() - await self.data[self.order[-1]].load(instance_data) - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - """ - 将配置项转换为字典 - - Arguments - ---------- - if_decrypt: bool - 是否解密数据, 默认为 True - regenerate_uuids: bool - 是否重新生成 UUID, 默认为 False - - Returns - ------- - Dict[str, Union[list, dict]] - 配置项数据字典 - """ - - uuid_book: dict[uuid.UUID, uuid.UUID] = { - _: uuid.uuid4() if regenerate_uuids else _ for _ in self.order - } - - data: dict[str, Any] = { - "instances": [ - {"uid": str(uuid_book[_]), "type": type(self.data[_]).__name__} - for _ in self.order - ] - } - for uid, config in self.items(): - data[str(uuid_book[uid])] = await config.toDict( - if_decrypt, regenerate_uuids - ) - - return data - - async def get(self, uid: uuid.UUID) -> dict[str, Any]: - """ - 获取指定 UID 的配置项 - - Parameters - ---------- - uid: uuid.UUID - 要获取的配置项的唯一标识符 - Returns - ------- - Dict[str, Union[list, dict]] - 对应的配置项数据字典 - """ - - if uid not in self.data: - raise ValueError(f"配置项 '{uid}' 不存在。") - - data: dict[str, Any] = { - "instances": [ - {"uid": str(_), "type": type(self.data[_]).__name__} - for _ in self.order - if _ == uid - ] - } - data[str(uid)] = await self.data[uid].toDict() - - return data - - async def save(self): - """保存配置""" - - if not self.file: - raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") - - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.write_text( - _dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) - - async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: - """ - 添加一个新的配置项 - - Parameters - ---------- - config_type: type - 配置项的类型, 必须是初始化时已声明的 ConfigBase 子类 - - Returns - ------- - tuple[uuid.UUID, ConfigBase] - 新创建的配置项的唯一标识符和实例 - """ - - if config_type not in self.sub_config_type.values(): - raise ValueError(f"配置类型 {config_type.__name__} 不被允许") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - uid = uuid.uuid4() - self.order.append(uid) - self.data[uid] = config_type() - - for save_method in self._save_methods: - await self.data[uid].add_save_method(save_method) - - if self.file: - await self.data[uid].add_save_method(self.save) - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - return uid, self.data[uid] - - async def remove(self, uid: uuid.UUID): - """ - 移除配置项 - - Parameters - ---------- - uid: uuid.UUID - 要移除的配置项的唯一标识符 - """ - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - if uid not in self.data: - raise ValueError(f"配置项 '{uid}' 不存在") - - if self.data[uid].is_locked: - raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除") - - self.data.pop(uid) - self.order.remove(uid) - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - async def setOrder(self, order: list[uuid.UUID]): - """ - 设置配置项的顺序 - - Parameters - ---------- - order: List[uuid.UUID] - 新的配置项顺序 - """ - - if set(order) != set(self.data.keys()): - raise ValueError("顺序与当前配置项不匹配") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self.order = order - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - async def lock(self): - """ - 锁定配置项, 锁定后无法修改配置项值 - """ - - self.is_locked = True - - for item in self.values(): - await item.lock() - - async def unlock(self): - """ - 解锁配置项, 解锁后可以修改配置项值 - """ - - self.is_locked = False - - for item in self.values(): - await item.unlock() - - def keys(self): - """返回配置项的所有唯一标识符""" - - return iter(self.order) - - def values(self): - """返回配置项的所有实例""" - - if not self.data: - return iter(()) - - return (self.data[_] for _ in self.order) - - def items(self): - """返回配置项的所有唯一标识符和实例的元组""" - - return zip(self.keys(), self.values()) diff --git a/app/models/__init__.py b/app/models/__init__.py index d8f34b22..c71720ce 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,54 +1,12 @@ -# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software -# Copyright © 2024-2025 DLmaster361 -# Copyright © 2025 MoeSnowyFox -# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with AUTO-MAS. If not, see . - -# Contact: DLmaster_361@163.com - - -from . import dto, emulator, task -from .config_base import ConfigBase, ConfigItem, MultipleConfig, dump_toml -from .pydantic_base import PydanticConfigBase +from . import dto, emulator, schema, task from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook +from .general import GeneralConfig, GeneralUserConfig +from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig from .maaend import MaaEndConfig, MaaEndUserConfig from .src import SrcConfig, SrcUserConfig -from .general import GeneralConfig, GeneralUserConfig -from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig -from .type import ( - EncryptedString, - HHMMString, - JsonDictString, - JsonListString, - KeyboardKeyString, - UrlString, - YmdHmString, - YmdHmsString, - YmdString, - decrypt_encrypted_string, -) __all__ = [ - "ConfigBase", - "MultipleConfig", - "ConfigItem", - "dump_toml", - "PydanticConfigBase", "EmulatorConfig", "Webhook", "QueueItem", @@ -66,17 +24,8 @@ "ToolsConfig", "GlobalConfig", "CLASS_BOOK", - "JsonDictString", - "JsonListString", - "HHMMString", - "YmdHmString", - "YmdString", - "YmdHmsString", - "UrlString", - "KeyboardKeyString", - "EncryptedString", - "decrypt_encrypted_string", "dto", "emulator", + "schema", "task", ] diff --git a/app/models/common.py b/app/models/common.py index 84d9ecb7..dade424d 100644 --- a/app/models/common.py +++ b/app/models/common.py @@ -1,5 +1,4 @@ from __future__ import annotations -# pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportGeneralTypeIssues=false import calendar import uuid @@ -7,9 +6,15 @@ from pydantic import BaseModel, Field, field_validator -from .config_base import MultipleConfig -from .pydantic_base import PydanticConfigBase -from .type import HHMMString, JsonDictString, JsonListString, UrlString, YmdHmString +from app.core.config.base import MultipleConfig +from app.core.config.pydantic import PydanticConfigBase +from app.core.config.types import ( + HHMMString, + JsonDictString, + JsonListString, + UrlString, + YmdHmString, +) DAY_NAMES = tuple(calendar.day_name) @@ -135,9 +140,8 @@ class DataModel(BaseModel): def __init__(self, **data: Any): super().__init__(**data) - # MultipleConfig 目前约束 T 继承 ConfigBase;这里保持运行时兼容。 - self.TimeSet = MultipleConfig([TimeSet]) # pyright: ignore[reportArgumentType] - self.QueueItem = MultipleConfig([QueueItem]) # pyright: ignore[reportArgumentType] + self.TimeSet: MultipleConfig[TimeSet] = MultipleConfig([TimeSet]) + self.QueueItem: MultipleConfig[QueueItem] = MultipleConfig([QueueItem]) __all__ = ["EmulatorConfig", "Webhook", "QueueItem", "TimeSet", "QueueConfig"] diff --git a/app/models/config/__init__.py b/app/models/config/__init__.py deleted file mode 100644 index 42668897..00000000 --- a/app/models/config/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -from ..common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook -from ..maa import MaaConfig, MaaPlanConfig, MaaUserConfig -from ..maaend import MaaEndConfig, MaaEndUserConfig -from ..src import SrcConfig, SrcUserConfig -from ..general import GeneralConfig, GeneralUserConfig -from ..global_config import CLASS_BOOK, GlobalConfig, ToolsConfig -from ..pydantic_base import PydanticConfigBase -from ..type import ( - EncryptedString, - HHMMString, - JsonDictString, - JsonListString, - KeyboardKeyString, - UrlString, - YmdHmsString, - YmdHmString, - YmdString, - decrypt_encrypted_string, -) - -__all__ = [ - "EmulatorConfig", - "Webhook", - "QueueItem", - "TimeSet", - "QueueConfig", - "MaaPlanConfig", - "MaaUserConfig", - "MaaConfig", - "MaaEndUserConfig", - "MaaEndConfig", - "SrcUserConfig", - "SrcConfig", - "GeneralUserConfig", - "GeneralConfig", - "ToolsConfig", - "GlobalConfig", - "CLASS_BOOK", - "PydanticConfigBase", - "JsonDictString", - "JsonListString", - "HHMMString", - "YmdHmString", - "YmdString", - "YmdHmsString", - "UrlString", - "KeyboardKeyString", - "EncryptedString", - "decrypt_encrypted_string", -] diff --git a/app/models/config/common.py b/app/models/config/common.py deleted file mode 100644 index 310c0a33..00000000 --- a/app/models/config/common.py +++ /dev/null @@ -1,3 +0,0 @@ -from ..common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook - -__all__ = ["EmulatorConfig", "Webhook", "QueueItem", "TimeSet", "QueueConfig"] diff --git a/app/models/config/general.py b/app/models/config/general.py deleted file mode 100644 index 2f82c895..00000000 --- a/app/models/config/general.py +++ /dev/null @@ -1,3 +0,0 @@ -from ..general import GeneralConfig, GeneralUserConfig - -__all__ = ["GeneralUserConfig", "GeneralConfig"] diff --git a/app/models/config/global_config.py b/app/models/config/global_config.py deleted file mode 100644 index 5b59767b..00000000 --- a/app/models/config/global_config.py +++ /dev/null @@ -1,3 +0,0 @@ -from ..global_config import CLASS_BOOK, GlobalConfig, ToolsConfig - -__all__ = ["ToolsConfig", "GlobalConfig", "CLASS_BOOK"] diff --git a/app/models/config/maa.py b/app/models/config/maa.py deleted file mode 100644 index f240210b..00000000 --- a/app/models/config/maa.py +++ /dev/null @@ -1,3 +0,0 @@ -from ..maa import MaaConfig, MaaPlanConfig, MaaUserConfig - -__all__ = ["MaaPlanConfig", "MaaUserConfig", "MaaConfig"] diff --git a/app/models/config/maaend.py b/app/models/config/maaend.py deleted file mode 100644 index bb8f56ac..00000000 --- a/app/models/config/maaend.py +++ /dev/null @@ -1,3 +0,0 @@ -from ..maaend import MaaEndConfig, MaaEndUserConfig - -__all__ = ["MaaEndUserConfig", "MaaEndConfig"] diff --git a/app/models/config/src.py b/app/models/config/src.py deleted file mode 100644 index 30fefe73..00000000 --- a/app/models/config/src.py +++ /dev/null @@ -1,3 +0,0 @@ -from ..src import SrcConfig, SrcUserConfig - -__all__ = ["SrcUserConfig", "SrcConfig"] diff --git a/app/models/general.py b/app/models/general.py index 6360dcc8..d98cfb9b 100644 --- a/app/models/general.py +++ b/app/models/general.py @@ -9,10 +9,10 @@ from pydantic import BaseModel, Field, field_validator from app.utils.constants import UTC4 -from .config_base import MultipleConfig +from app.core.config.base import MultipleConfig +from app.core.config.pydantic import PydanticConfigBase +from app.core.config.types import UrlString from .common import Webhook -from .pydantic_base import PydanticConfigBase -from .type import UrlString class GeneralUserConfig(PydanticConfigBase): diff --git a/app/models/global_config.py b/app/models/global_config.py index 8ef3ece3..0e103fd7 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -8,16 +8,9 @@ from pydantic import BaseModel, Field, field_validator -from app.utils.constants import MATERIALS_MAP, RESOURCE_STAGE_INFO, UTC8 -from .config_base import MultipleConfig -from .dto import TagItem -from .common import EmulatorConfig, QueueConfig, QueueItem, Webhook -from .general import GeneralConfig -from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig -from .maaend import MaaEndConfig -from .pydantic_base import PydanticConfigBase -from .src import SrcConfig -from .type import ( +from app.core.config.base import MultipleConfig +from app.core.config.pydantic import PydanticConfigBase +from app.core.config.types import ( EncryptedString, JsonDictString, JsonListString, @@ -25,6 +18,13 @@ UrlString, YmdHmsString, ) +from app.utils.constants import MATERIALS_MAP, RESOURCE_STAGE_INFO, UTC8 +from app.models.dto import TagItem +from .common import EmulatorConfig, QueueConfig, QueueItem, Webhook +from .general import GeneralConfig +from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from .maaend import MaaEndConfig +from .src import SrcConfig class ToolsConfig(PydanticConfigBase): @@ -179,13 +179,17 @@ def _normalize_uid(cls, value: Any) -> str: def __init__(self, **data: Any): super().__init__(**data) - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - self.EmulatorConfig = MultipleConfig([EmulatorConfig]) - self.PlanConfig = MultipleConfig([MaaPlanConfig]) - self.ScriptConfig = MultipleConfig( + self.Notify_CustomWebhooks: MultipleConfig[Webhook] = MultipleConfig([Webhook]) + self.EmulatorConfig: MultipleConfig[EmulatorConfig] = MultipleConfig( + [EmulatorConfig] + ) + self.PlanConfig: MultipleConfig[MaaPlanConfig] = MultipleConfig([MaaPlanConfig]) + self.ScriptConfig: MultipleConfig[ + MaaConfig | MaaEndConfig | SrcConfig | GeneralConfig + ] = MultipleConfig( [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] ) - self.QueueConfig = MultipleConfig([QueueConfig]) + self.QueueConfig: MultipleConfig[QueueConfig] = MultipleConfig([QueueConfig]) self.ToolsConfig = ToolsConfig() MaaConfig.related_config["EmulatorConfig"] = self.EmulatorConfig diff --git a/app/models/maa.py b/app/models/maa.py index e427f0d4..50dd8359 100644 --- a/app/models/maa.py +++ b/app/models/maa.py @@ -8,11 +8,11 @@ from pydantic import BaseModel, Field, field_validator +from app.core.config.base import MultipleConfig +from app.core.config.pydantic import PydanticConfigBase +from app.core.config.types import EncryptedString, JsonDictString from app.utils.constants import MAA_STAGE_KEY, RESOURCE_STAGE_INFO, UTC4, UTC8 -from .config_base import MultipleConfig from .common import Webhook -from .pydantic_base import PydanticConfigBase -from .type import EncryptedString, JsonDictString class _ValueProxy: diff --git a/app/models/maaend.py b/app/models/maaend.py index db6c990a..5ca78252 100644 --- a/app/models/maaend.py +++ b/app/models/maaend.py @@ -8,11 +8,11 @@ from pydantic import BaseModel, Field, field_validator +from app.core.config.base import MultipleConfig +from app.core.config.pydantic import PydanticConfigBase +from app.core.config.types import EncryptedString from app.utils.constants import MAAEND_STAGE_BOOK, MAAEND_STAGE_WITH_AB, UTC4, UTC8 -from .config_base import MultipleConfig from .common import Webhook -from .pydantic_base import PydanticConfigBase -from .type import EncryptedString class MaaEndUserConfig(PydanticConfigBase): diff --git a/app/models/src.py b/app/models/src.py index b0387474..0af60cf1 100644 --- a/app/models/src.py +++ b/app/models/src.py @@ -8,11 +8,11 @@ from pydantic import BaseModel, Field, field_validator +from app.core.config.base import MultipleConfig +from app.core.config.pydantic import PydanticConfigBase +from app.core.config.types import EncryptedString from app.utils.constants import STARRAIL_STAGE_BOOK, UTC4 -from .config_base import MultipleConfig from .common import Webhook -from .pydantic_base import PydanticConfigBase -from .type import EncryptedString RELIC_OPTIONS: tuple[str, ...] = ( diff --git a/app/task/MAA/AutoProxy.py b/app/task/MAA/AutoProxy.py index 91eb4c14..9d5c11d2 100644 --- a/app/task/MAA/AutoProxy.py +++ b/app/task/MAA/AutoProxy.py @@ -29,7 +29,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models import MaaConfig, MaaUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaConfig, MaaUserConfig from app.models.emulator import DeviceInfo, DeviceBase from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, skland_sign_in diff --git a/app/task/MAA/ManualReview.py b/app/task/MAA/ManualReview.py index 7e968bae..b32b08ed 100644 --- a/app/task/MAA/ManualReview.py +++ b/app/task/MAA/ManualReview.py @@ -29,7 +29,8 @@ from app.core import Config, Broadcast from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models import MaaConfig, MaaUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaConfig, MaaUserConfig from app.models.emulator import DeviceInfo, DeviceBase from app.services import System from app.utils import get_logger, LogMonitor, ProcessManager diff --git a/app/task/MAA/ScriptConfig.py b/app/task/MAA/ScriptConfig.py index 63809b26..8db64064 100644 --- a/app/task/MAA/ScriptConfig.py +++ b/app/task/MAA/ScriptConfig.py @@ -26,7 +26,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models import MaaConfig, MaaUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaConfig, MaaUserConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager diff --git a/app/task/MAA/manager.py b/app/task/MAA/manager.py index c3987b94..a5681820 100644 --- a/app/task/MAA/manager.py +++ b/app/task/MAA/manager.py @@ -27,7 +27,8 @@ from app.core import Config, EmulatorManager from app.models.task import TaskExecuteBase, ScriptItem, UserItem -from app.models import MaaConfig, MaaUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaConfig, MaaUserConfig from app.services import Notify from app.utils import get_logger from app.utils.constants import TASK_MODE_ZH diff --git a/app/task/MaaEnd/AutoProxy.py b/app/task/MaaEnd/AutoProxy.py index 35f48841..884e14c0 100644 --- a/app/task/MaaEnd/AutoProxy.py +++ b/app/task/MaaEnd/AutoProxy.py @@ -27,7 +27,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaEndConfig, MaaEndUserConfig from app.models.emulator import DeviceBase, DeviceInfo from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, skland_sign_in diff --git a/app/task/MaaEnd/ManualReview.py b/app/task/MaaEnd/ManualReview.py index 08258c11..4002c4fc 100644 --- a/app/task/MaaEnd/ManualReview.py +++ b/app/task/MaaEnd/ManualReview.py @@ -26,7 +26,8 @@ from app.core import Broadcast, Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaEndConfig, MaaEndUserConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager diff --git a/app/task/MaaEnd/ScriptConfig.py b/app/task/MaaEnd/ScriptConfig.py index b3946e6f..20d7db77 100644 --- a/app/task/MaaEnd/ScriptConfig.py +++ b/app/task/MaaEnd/ScriptConfig.py @@ -27,7 +27,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaEndConfig, MaaEndUserConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager diff --git a/app/task/MaaEnd/manager.py b/app/task/MaaEnd/manager.py index 3ca40b1b..af41f66f 100644 --- a/app/task/MaaEnd/manager.py +++ b/app/task/MaaEnd/manager.py @@ -25,7 +25,8 @@ from pathlib import Path from app.core import Config, EmulatorManager -from app.models import MaaEndConfig, MaaEndUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import MaaEndConfig, MaaEndUserConfig from app.models.task import ScriptItem, TaskExecuteBase, UserItem from app.services import Notify from app.utils import get_logger diff --git a/app/task/SRC/AutoProxy.py b/app/task/SRC/AutoProxy.py index 09dd1855..38abe885 100644 --- a/app/task/SRC/AutoProxy.py +++ b/app/task/SRC/AutoProxy.py @@ -30,7 +30,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models import SrcConfig, SrcUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import SrcConfig, SrcUserConfig from app.models.emulator import DeviceBase, DeviceInfo from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, strptime diff --git a/app/task/SRC/ManualReview.py b/app/task/SRC/ManualReview.py index 78bba0e9..35d9679a 100644 --- a/app/task/SRC/ManualReview.py +++ b/app/task/SRC/ManualReview.py @@ -27,7 +27,8 @@ from app.core import Config, Broadcast from app.models.task import TaskExecuteBase, ScriptItem -from app.models import SrcConfig, SrcUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import SrcConfig, SrcUserConfig from app.models.emulator import DeviceBase from app.utils import get_logger from app.utils.constants import STARRAIL_PACKAGE_NAME, UTC4 diff --git a/app/task/SRC/ScriptConfig.py b/app/task/SRC/ScriptConfig.py index cd504f00..8303f56e 100644 --- a/app/task/SRC/ScriptConfig.py +++ b/app/task/SRC/ScriptConfig.py @@ -26,7 +26,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models import SrcConfig, SrcUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import SrcConfig, SrcUserConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager diff --git a/app/task/SRC/manager.py b/app/task/SRC/manager.py index b257ae2e..0ceb7ed6 100644 --- a/app/task/SRC/manager.py +++ b/app/task/SRC/manager.py @@ -27,7 +27,8 @@ from app.core import Config, EmulatorManager from app.models.task import TaskExecuteBase, ScriptItem, UserItem -from app.models import SrcConfig, SrcUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import SrcConfig, SrcUserConfig from app.services import Notify from app.utils import get_logger from app.utils.constants import TASK_MODE_ZH diff --git a/app/task/general/AutoProxy.py b/app/task/general/AutoProxy.py index 1fd51769..7741f7d7 100644 --- a/app/task/general/AutoProxy.py +++ b/app/task/general/AutoProxy.py @@ -30,7 +30,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem, LogRecord -from app.models import GeneralConfig, GeneralUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import GeneralConfig, GeneralUserConfig from app.models.emulator import DeviceBase from app.services import Notify, System from app.utils import get_logger, LogMonitor, ProcessManager, ProcessInfo, strptime diff --git a/app/task/general/ScriptConfig.py b/app/task/general/ScriptConfig.py index f8f7dfe3..c51bad5d 100644 --- a/app/task/general/ScriptConfig.py +++ b/app/task/general/ScriptConfig.py @@ -27,7 +27,8 @@ from app.core import Config from app.models.task import TaskExecuteBase, ScriptItem -from app.models import GeneralConfig, GeneralUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import GeneralConfig, GeneralUserConfig from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager diff --git a/app/task/general/manager.py b/app/task/general/manager.py index 621e4307..22e3a39c 100644 --- a/app/task/general/manager.py +++ b/app/task/general/manager.py @@ -27,7 +27,8 @@ from app.core import Config, EmulatorManager from app.models.task import TaskExecuteBase, ScriptItem, UserItem -from app.models import GeneralConfig, GeneralUserConfig, MultipleConfig +from app.core.config.base import MultipleConfig +from app.models import GeneralConfig, GeneralUserConfig from app.services import Notify from app.utils import get_logger, ProcessManager from app.utils.constants import TASK_MODE_ZH diff --git a/requirements.txt b/requirements.txt index e1aa9658..4f25a513 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ loguru==0.7.3 fastapi==0.116.1 pydantic==2.11.7 -pydantic-settings==2.10.1 uvicorn==0.35.0 websockets==15.0.1 aiofiles==24.1.0 @@ -26,4 +25,3 @@ opencv-python==4.10.0.84 maafw==5.8.1 mss==9.0.2 fastapi-mcp==0.4.0 -tomli-w==1.2.0 \ No newline at end of file From 95c132d149c1fc4dde88bdfaee384642551ac46c Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 30 Mar 2026 23:58:43 +0800 Subject: [PATCH 06/29] =?UTF-8?q?chore(project):=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=99=A8=E8=87=B3python312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- pyrightconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9ce40165..db1f8f2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.ruff] line-length = 88 -target-version = "py311" +target-version = "py312" exclude = ["frontend", "data", "history", "debug", "__pycache__", "test"] [tool.ruff.lint] diff --git a/pyrightconfig.json b/pyrightconfig.json index 777e8009..3340fd71 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/pyright/schema/pyrightconfig.schema.json", - "pythonVersion": "3.11", + "pythonVersion": "3.12", "typeCheckingMode": "strict", "include": [ "app", From 6551bc4c5f93e672103b8a7539eab6ecc392e22a Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Tue, 31 Mar 2026 00:13:30 +0800 Subject: [PATCH 07/29] =?UTF-8?q?fix(config):=20=E6=9B=B4=E6=AD=A3?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config/base.py | 19 +++++++- app/core/config/manager.py | 4 +- app/core/config/pydantic.py | 74 ++++++++++++++++++------------- app/models/common.py | 10 +++-- app/models/task.py | 77 ++++++++++++++++++++------------- app/task/MAA/manager.py | 2 +- app/task/MaaEnd/tools/notify.py | 17 +++++--- app/task/SRC/manager.py | 2 +- app/task/general/manager.py | 2 +- 9 files changed, 131 insertions(+), 76 deletions(-) diff --git a/app/core/config/base.py b/app/core/config/base.py index ed46b907..2d8bf08b 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -150,8 +150,7 @@ def _load_toml_config(path: Path) -> dict[str, Any]: if raw_text.strip() == "": return {} - loaded = tomllib.loads(raw_text) - return loaded if isinstance(loaded, dict) else {} + return tomllib.loads(raw_text) def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path | None]: @@ -188,6 +187,22 @@ def _backup_legacy_config_if_needed( legacy_file.replace(legacy_backup) +def load_config_with_legacy_migration( + path: Path, +) -> tuple[dict[str, Any], Path | None]: + """公开的兼容加载入口,供其他模块调用。""" + + return _load_config_with_legacy_migration(path) + + +def backup_legacy_config_if_needed( + current_file: Path, legacy_file: Path | None +) -> None: + """公开的旧版配置备份入口,供其他模块调用。""" + + _backup_legacy_config_if_needed(current_file, legacy_file) + + class ValidatorBase(ABC): """基础配置验证器""" diff --git a/app/core/config/manager.py b/app/core/config/manager.py index 6cd89a33..b8a61306 100644 --- a/app/core/config/manager.py +++ b/app/core/config/manager.py @@ -56,8 +56,8 @@ Webhook, TimeSet, EmulatorConfig, - dump_toml, ) +from .base import dump_toml from app.models.dto import WebSocketMessage from app.utils.constants import ( UTC4, @@ -1715,7 +1715,7 @@ async def get_emulator_devices_combox(self, emulator_id: str): data = [{"label": "未选择", "value": "-"}] - from .emulator_manager import EmulatorManager + from ..emulator_manager import EmulatorManager for index, device in ( await (await EmulatorManager.get_emulator_instance(emulator_id)).getInfo( diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index b7f91bd6..b4d5f152 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -4,15 +4,15 @@ import inspect from pathlib import Path from collections.abc import Callable, Coroutine -from typing import Any, ClassVar +from typing import Any, ClassVar, cast from pydantic import BaseModel, ConfigDict, PrivateAttr from .base import ( MultipleConfig, - _backup_legacy_config_if_needed, - _load_config_with_legacy_migration, + backup_legacy_config_if_needed, dump_toml, + load_config_with_legacy_migration, ) @@ -20,6 +20,23 @@ Slot = Callable[[Any], Any] | Callable[[Any], Coroutine[Any, Any, Any]] +def _default_save_methods() -> list[SaveMethod]: + return [] + + +def _default_bindings() -> dict[tuple[str, str], list[Slot]]: + return {} + + +def _normalize_mapping(value: Any) -> dict[str, Any]: + """将任意映射值规范化为 `dict[str, Any]`。""" + + if not isinstance(value, dict): + return {} + mapping = cast(dict[object, Any], value) + return {str(key): item for key, item in mapping.items()} + + class PydanticConfigBase(BaseModel): """基于 pydantic v2 的配置基类,兼容旧版 ConfigBase 常用接口。""" @@ -28,8 +45,10 @@ class PydanticConfigBase(BaseModel): LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = {} _file: Path | None = PrivateAttr(default=None) _is_locked: bool = PrivateAttr(default=False) - _save_methods: list[SaveMethod] = PrivateAttr(default_factory=list) - _bindings: dict[tuple[str, str], list[Slot]] = PrivateAttr(default_factory=dict) + _save_methods: list[SaveMethod] = PrivateAttr(default_factory=_default_save_methods) + _bindings: dict[tuple[str, str], list[Slot]] = PrivateAttr( + default_factory=_default_bindings + ) @property def file(self) -> Path | None: @@ -57,7 +76,7 @@ def _group_index(self) -> dict[str, BaseModel]: def _normalize_value(self, group: str, name: str, value: Any) -> Any: return value - async def connect(self, path: Path): + async def connect(self, path: Path) -> None: if path.suffix != ".toml": raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") @@ -70,40 +89,33 @@ async def connect(self, path: Path): self._file.parent.mkdir(parents=True, exist_ok=True) self._file.touch() - data, legacy_file = _load_config_with_legacy_migration(self._file) + data, legacy_file = load_config_with_legacy_migration(self._file) await self.load(data) await self.add_save_method(self.save) - _backup_legacy_config_if_needed(self._file, legacy_file) + backup_legacy_config_if_needed(self._file, legacy_file) - async def add_save_method(self, save_method: SaveMethod): + async def add_save_method(self, save_method: SaveMethod) -> None: if save_method != self.save and save_method not in self._save_methods: self._save_methods.append(save_method) for sub_config in self._multiple_config_index().values(): await sub_config.add_save_method(save_method) - async def load(self, data: dict[str, Any]): + async def load(self, data: dict[str, Any]) -> None: if self._is_locked: raise ValueError("配置已锁定, 无法修改") raw: dict[str, Any] = dict(data) - sub_configs_any = raw.pop("SubConfigsInfo", {}) - sub_configs: dict[str, Any] - if isinstance(sub_configs_any, dict): - sub_configs = dict(sub_configs_any) - else: - sub_configs = {} + sub_configs = _normalize_mapping(raw.pop("SubConfigsInfo", {})) for name, sub_config in self._multiple_config_index().items(): data_for_sub = sub_configs.get(name) if isinstance(data_for_sub, dict): - await sub_config.load(data_for_sub) + await sub_config.load(_normalize_mapping(data_for_sub)) for group_name, group_model in self._group_index().items(): - group_data = raw.get(group_name, {}) - if not isinstance(group_data, dict): - group_data = {} + group_data = _normalize_mapping(raw.get(group_name, {})) default_group = type(group_model)() for field_name in list(type(group_model).model_fields.keys()): @@ -117,8 +129,8 @@ async def load(self, data: dict[str, Any]): legacy = self.LEGACY_FIELD_MAP.get((group_name, field_name)) if legacy is not None: legacy_group, legacy_name = legacy - legacy_data = raw.get(legacy_group, {}) - if isinstance(legacy_data, dict) and legacy_name in legacy_data: + legacy_data = _normalize_mapping(raw.get(legacy_group, {})) + if legacy_name in legacy_data: candidate = legacy_data[legacy_name] has_value = True @@ -160,7 +172,7 @@ def get(self, group: str, name: str) -> Any: raise AttributeError(f"配置项 '{group}.{name}' 不存在") return getattr(group_model, name) - async def set(self, group: str, name: str, value: Any): + async def set(self, group: str, name: str, value: Any) -> None: group_model = self._group_index().get(group) if group_model is None or not hasattr(group_model, name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") @@ -187,7 +199,7 @@ async def set(self, group: str, name: str, value: Any): if self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) - def bind(self, group: str, name: str, slot: Slot): + def bind(self, group: str, name: str, slot: Slot) -> None: group_model = self._group_index().get(group) if group_model is None or not hasattr(group_model, name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") @@ -201,7 +213,7 @@ def bind(self, group: str, name: str, slot: Slot): if slot not in self._bindings[key]: self._bindings[key].append(slot) - def unbind(self, group: str, name: str, slot: Slot): + def unbind(self, group: str, name: str, slot: Slot) -> None: group_model = self._group_index().get(group) if group_model is None or not hasattr(group_model, name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") @@ -213,16 +225,18 @@ def unbind(self, group: str, name: str, slot: Slot): if key in self._bindings and slot in self._bindings[key]: self._bindings[key].remove(slot) - async def _emit_bindings(self, group: str, name: str, value: Any): + async def _emit_bindings(self, group: str, name: str, value: Any) -> None: key = (group, name) - slots = self._bindings.get(key, []) + slots = self._bindings.get(key) + if slots is None: + return for slot in slots: if inspect.iscoroutinefunction(slot): await slot(value) else: slot(value) - async def save(self): + async def save(self) -> None: if not self._file: raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") @@ -232,12 +246,12 @@ async def save(self): encoding="utf-8", ) - async def lock(self): + async def lock(self) -> None: self._is_locked = True for config in self._multiple_config_index().values(): await config.lock() - async def unlock(self): + async def unlock(self) -> None: self._is_locked = False for config in self._multiple_config_index().values(): await config.unlock() diff --git a/app/models/common.py b/app/models/common.py index dade424d..aa241ba9 100644 --- a/app/models/common.py +++ b/app/models/common.py @@ -2,7 +2,7 @@ import calendar import uuid -from typing import Any, ClassVar, Literal +from typing import Any, ClassVar, Literal, cast from pydantic import BaseModel, Field, field_validator @@ -17,7 +17,7 @@ ) -DAY_NAMES = tuple(calendar.day_name) +DAY_NAMES: tuple[str, ...] = tuple(calendar.day_name) EMULATOR_TYPES = Literal["general", "mumu", "ldplayer"] AFTER_ACCOMPLISH_OPTIONS = Literal[ "NoAction", @@ -113,10 +113,12 @@ class InfoModel(BaseModel): def _validate_days(cls, value: Any) -> list[str]: if not isinstance(value, list): return [] - days: list[str] = [item for item in value if isinstance(item, str)] + raw_days = cast(list[object], value) + days: list[str] = [item for item in raw_days if isinstance(item, str)] return ( days - if len(days) == len(value) and all(item in DAY_NAMES for item in days) + if len(days) == len(raw_days) + and all(item in DAY_NAMES for item in days) else [] ) diff --git a/app/models/task.py b/app/models/task.py index 9432354e..1b796663 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -25,28 +25,42 @@ from datetime import datetime from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import List, Optional, Literal +from typing import Any, Literal, Optional + + +def _default_content() -> list[str]: + return [] + + +def _default_log_record() -> dict[datetime, LogRecord]: + return {} + + +def _default_user_list() -> list[UserItem]: + return [] + + +def _default_script_list() -> list[ScriptItem]: + return [] @dataclass class LogRecord: - - content: list[str] = field(default_factory=list) + content: list[str] = field(default_factory=_default_content) status: str = "未开始监看日志" @dataclass class UserItem: - user_id: str # 用户ID name: str # 用户名称 status: str # 用户执行状态 log_record: dict[datetime, LogRecord] = field( - default_factory=dict + default_factory=_default_log_record ) # 用户本次代理的全部日志记录 _task_item_ref: Optional[weakref.ReferenceType[TaskItem]] = None - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: super().__setattr__(name, value) # 监听所有字段变化 if name in ("user_id", "name", "status") and self._task_item_ref is not None: @@ -69,16 +83,17 @@ def result(self) -> str: @dataclass class ScriptItem: - script_id: str # 脚本ID name: str # 脚本名称 status: str # 脚本执行状态 - user_list: List[UserItem] = field(default_factory=list) # 用户信息列表 + user_list: list[UserItem] = field( + default_factory=_default_user_list + ) # 用户信息列表 current_index: int = -1 # 当前执行的用户索引,-1 表示未开始 log: str = "" # 脚本执行日志 _task_item_ref: Optional[weakref.ReferenceType[TaskItem]] = None - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: super().__setattr__(name, value) # 如果 user_list 被整体替换,重新绑定 @@ -114,10 +129,12 @@ class TaskItem(ABC): queue_id: str | None # 执行的队列ID script_id: str | None # 执行的脚本ID user_id: str | None # 执行的用户ID - script_list: List[ScriptItem] = field(default_factory=list) # 脚本信息列表 + script_list: list[ScriptItem] = field( + default_factory=_default_script_list + ) # 脚本信息列表 current_index: int = -1 # 当前执行的脚本索引,-1 表示未开始 - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: super().__setattr__(name, value) # 如果 script_list 被整体替换,重新绑定 @@ -125,7 +142,7 @@ def __setattr__(self, name, value): for item in self.script_list: self._bind_task_item(item) - def _bind_task_item(self, item: ScriptItem): + def _bind_task_item(self, item: ScriptItem) -> None: """绑定 TaskItem 及其内部所有 UserItem 到当前 TaskItem""" ti_ref = weakref.ref(self) object.__setattr__(item, "_task_item_ref", ti_ref) @@ -134,12 +151,12 @@ def _bind_task_item(self, item: ScriptItem): object.__setattr__(user, "_task_item_ref", ti_ref) @abstractmethod - async def on_change(self): + async def on_change(self) -> None: """统一回调入口""" raise NotImplementedError("子类必须实现 on_change") @property - def asdict(self) -> list: + def asdict(self) -> list[dict[str, str | list[dict[str, str]]]]: """将 TaskItem 转换为字典形式""" return [ { @@ -162,30 +179,30 @@ def result(self) -> str: if not self.script_list: return "任务未加载" - return "\n\n\n".join( - [ - f"{script.name}:\n\n" - f" 已完成用户数:{sum(1 for user in script.user_list if user.status == '完成')};未完成用户数:{sum(1 for user in script.user_list if user.status != '完成')}\n\n" - f" {script.result.replace('\n', '\n ')}" - for script in self.script_list - ] - ) + formatted_result: list[str] = [] + formatted_result = [ + f"{script.name}:\n\n" + f" 已完成用户数:{sum(1 for user in script.user_list if user.status == '完成')};未完成用户数:{sum(1 for user in script.user_list if user.status != '完成')}\n\n" + f" {script.result.replace('\n', '\n ')}" + for script in self.script_list + ] + return "\n\n\n".join(formatted_result) @dataclass class TaskExecuteBase(ABC): - task: asyncio.Task | None = None + task: asyncio.Task[None] | None = None _task_group: asyncio.TaskGroup | None = None accomplish: asyncio.Event = field(default_factory=asyncio.Event) @abstractmethod - async def main_task(self): ... + async def main_task(self) -> None: ... @abstractmethod - async def final_task(self): ... + async def final_task(self) -> None: ... @abstractmethod - async def on_crash(self, e): ... + async def on_crash(self, e: Exception) -> None: ... - async def _execute_task(self, parent_tg: asyncio.TaskGroup): + async def _execute_task(self, parent_tg: asyncio.TaskGroup) -> None: self._task_group = parent_tg try: await self.main_task() @@ -200,19 +217,19 @@ async def _execute_task(self, parent_tg: asyncio.TaskGroup): finally: self.accomplish.set() - def spawn(self, child: TaskExecuteBase) -> asyncio.Task: + def spawn(self, child: TaskExecuteBase) -> asyncio.Task[None]: if self._task_group is None: raise RuntimeError("子任务必须在主任务中启动") return self._task_group.create_task(child._execute_task(self._task_group)) - def execute(self): + def execute(self) -> None: if self.task is not None and not self.task.done(): raise RuntimeError("任务已在运行") if self._task_group is not None: raise RuntimeError("execute() 仅可由顶层任务调用,子任务请使用 spawn()") - async def _root_coro(): + async def _root_coro() -> None: async with asyncio.TaskGroup() as tg: self.task = tg.create_task(self._execute_task(tg)) diff --git a/app/task/MAA/manager.py b/app/task/MAA/manager.py index a5681820..99d1963e 100644 --- a/app/task/MAA/manager.py +++ b/app/task/MAA/manager.py @@ -188,7 +188,7 @@ async def final_task(self): if self.check_result != "Pass": self.script_info.status = "异常" - return self.check_result + return logger.info("MAA 主任务已结束, 开始执行后续操作") await Config.ScriptConfig[uuid.UUID(self.script_info.script_id)].unlock() diff --git a/app/task/MaaEnd/tools/notify.py b/app/task/MaaEnd/tools/notify.py index 7fa81c91..c591b76f 100644 --- a/app/task/MaaEnd/tools/notify.py +++ b/app/task/MaaEnd/tools/notify.py @@ -18,6 +18,7 @@ # Contact: DLmaster_361@163.com +from typing import Any, cast from app.core import Config from app.models import MaaEndUserConfig @@ -28,10 +29,14 @@ async def push_notification( - mode: str, title: str, message: dict, user_config: MaaEndUserConfig | None + mode: str, + title: str, + message: dict[str, Any], + user_config: MaaEndUserConfig | None, ) -> None: """通过所有渠道推送通知。""" + config = cast(Any, Config) logger.info(f"开始推送通知, 模式: {mode}, 标题: {title}") if mode == "代理结果" and ( @@ -46,8 +51,9 @@ async def push_notification( f"已完成数: {message['completed_count']}, 未完成数: {message['uncompleted_count']}\n\n" f"{message['result']}" ) - template = Config.notify_env.get_template("general_result.html") - message_html = template.render(message) + notify_env = config.notify_env + template = notify_env.get_template("general_result.html") + message_html = str(template.render(message)) serverchan_message = message_text.replace("\n", "\n\n") if Config.get("Notify", "IfSendMail"): @@ -75,8 +81,9 @@ async def push_notification( f"MaaEnd执行结果: {message['user_result']}\n\n" ) - template = Config.notify_env.get_template("general_statistics.html") - message_html = template.render(message) + notify_env = config.notify_env + template = notify_env.get_template("general_statistics.html") + message_html = str(template.render(message)) serverchan_message = message_text.replace("\n", "\n\n") if Config.get("Notify", "IfSendStatistic"): diff --git a/app/task/SRC/manager.py b/app/task/SRC/manager.py index 0ceb7ed6..821a3f30 100644 --- a/app/task/SRC/manager.py +++ b/app/task/SRC/manager.py @@ -191,7 +191,7 @@ async def final_task(self): if self.check_result != "Pass": self.script_info.status = "异常" - return self.check_result + return logger.info("SRC 主任务已结束, 开始执行后续操作") await Config.ScriptConfig[uuid.UUID(self.script_info.script_id)].unlock() diff --git a/app/task/general/manager.py b/app/task/general/manager.py index 22e3a39c..1df1622e 100644 --- a/app/task/general/manager.py +++ b/app/task/general/manager.py @@ -230,7 +230,7 @@ async def final_task(self): if self.check_result != "Pass": self.script_info.status = "异常" - return self.check_result + return logger.info("通用脚本任务已结束, 开始执行后续操作") await Config.ScriptConfig[uuid.UUID(self.script_info.script_id)].unlock() From 6bf402ba6ab04774cb0d40d2d33387fcd46ed096 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Tue, 31 Mar 2026 00:59:57 +0800 Subject: [PATCH 08/29] =?UTF-8?q?fix(config):=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config/__init__.py | 5 +- app/core/config/base.py | 1232 ++--------------------------- app/core/config/pydantic.py | 39 +- app/core/config/types.py | 10 +- app/models/__init__.py | 3 +- app/models/schema.py | 1462 ----------------------------------- 6 files changed, 121 insertions(+), 2630 deletions(-) delete mode 100644 app/models/schema.py diff --git a/app/core/config/__init__.py b/app/core/config/__init__.py index 10e6c42a..369ec643 100644 --- a/app/core/config/__init__.py +++ b/app/core/config/__init__.py @@ -1,4 +1,4 @@ -from .base import ConfigBase, ConfigItem, MultipleConfig, dump_json, dump_toml +from .base import MultipleConfig, dump_toml from .manager import AppConfig, Config from .pydantic import PydanticConfigBase from .types import ( @@ -15,10 +15,7 @@ ) __all__ = [ - "ConfigBase", - "ConfigItem", "MultipleConfig", - "dump_json", "dump_toml", "PydanticConfigBase", "JsonDictString", diff --git a/app/core/config/base.py b/app/core/config/base.py index 2d8bf08b..eed2b46a 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -22,46 +22,21 @@ from __future__ import annotations -# pyright: reportMissingParameterType=false, reportUnknownParameterType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownLambdaType=false, reportDeprecated=false, reportInvalidTypeForm=false, reportGeneralTypeIssues=false -import os + +import asyncio import json import tomllib import uuid -import shlex -import inspect -import asyncio -import pyautogui -import win32com.client -from copy import deepcopy -from urllib.parse import urlparse -from datetime import datetime from contextlib import suppress -from abc import ABC, abstractmethod from pathlib import Path from collections.abc import Callable, Coroutine -from typing import Any, Protocol, TypeVar, Generic - -from app.utils import get_logger, dpapi_encrypt, dpapi_decrypt -from app.utils.constants import ( - RESERVED_NAMES, - ILLEGAL_CHARS, - DEFAULT_DATETIME, - EMULATOR_PATH_BOOK, -) - -logger = get_logger("配置基类") +from typing import Any, Generic, Protocol, TypeVar PRIMARY_CONFIG_SUFFIX = ".toml" LEGACY_CONFIG_SUFFIX = ".json" -def dump_json(data: dict[str, Any]) -> str: - """公共 JSON 序列化入口。""" - - return json.dumps(data, ensure_ascii=False, indent=4) - - def _toml_key(key: str) -> str: """生成兼容 UUID 等特殊字符的 TOML 键名。""" @@ -150,7 +125,8 @@ def _load_toml_config(path: Path) -> dict[str, Any]: if raw_text.strip() == "": return {} - return tomllib.loads(raw_text) + loaded = tomllib.loads(raw_text) + return loaded if isinstance(loaded, dict) else {} def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path | None]: @@ -203,985 +179,6 @@ def backup_legacy_config_if_needed( _backup_legacy_config_if_needed(current_file, legacy_file) -class ValidatorBase(ABC): - """基础配置验证器""" - - @abstractmethod - def validate(self, value: Any) -> bool: - """验证值是否合法""" - pass - - @abstractmethod - def correct(self, value: Any) -> Any: - """修正非法值""" - pass - - -class StringValidator(ValidatorBase): - """字符串验证器""" - - def validate(self, value): - return isinstance(value, str) - - def correct(self, value): - return value if self.validate(value) else "" - - -class RangeValidator(ValidatorBase): - """范围验证器""" - - def __init__(self, min: int | float, max: int | float): - self.min = min - self.max = max - self.range = (min, max) - - def validate(self, value): - if not isinstance(value, (int, float)): - return False - return self.min <= value <= self.max - - def correct(self, value): - if not isinstance(value, (int, float)): - try: - value = float(value) - except TypeError: - return self.min - return min(max(self.min, value), self.max) - - -class OptionsValidator(ValidatorBase): - """选项验证器""" - - def __init__(self, options: list[Any]): - if not options: - raise ValueError("可选项不能为空") - - self.options = options - - def validate(self, value): - return value in self.options - - def correct(self, value): - return value if self.validate(value) else self.options[0] - - -class MultipleOptionsValidator(ValidatorBase): - """多选选项验证器""" - - def __init__(self, options: list[Any]): - if not options: - raise ValueError("可选项不能为空") - - self.options = options - - def validate(self, value): - if not isinstance(value, list): - return False - - return all(item in self.options for item in value) - - def correct(self, value): - return value if self.validate(value) else [] - - -class UUIDValidator(ValidatorBase): - """UUID验证器""" - - def validate(self, value): - try: - uuid.UUID(value) - return True - except (TypeError, ValueError): - return False - - def correct(self, value): - return value if self.validate(value) else str(uuid.uuid4()) - - -class MultipleUIDValidator(ValidatorBase): - """多配置管理类UID验证器""" - - def __init__( - self, - default: Any, - related_config: dict[str, "MultipleConfig[Any]"], - config_name: str, - ): - self.default = default - self.related_config = related_config - self.config_name = config_name - - def validate(self, value): - if value == self.default: - return True - if not isinstance(value, str): - return False - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return False - if uid in self.related_config.get(self.config_name, {}): - return True - return False - - def correct(self, value): - if self.validate(value): - return value - return self.default - - -class DateTimeValidator(ValidatorBase): - """日期时间验证器""" - - def __init__(self, date_format: str) -> None: - if not date_format: - raise ValueError("日期时间格式不能为空") - self.date_format = date_format - - def validate(self, value): - if not isinstance(value, str): - return False - try: - datetime.strptime(value, self.date_format) - return True - except ValueError: - return False - - def correct(self, value): - if not isinstance(value, str): - return DEFAULT_DATETIME.strftime(self.date_format) - try: - datetime.strptime(value, self.date_format) - return value - except ValueError: - return DEFAULT_DATETIME.strftime(self.date_format) - - -class JSONValidator(ValidatorBase): - def __init__( - self, tpye: type[dict[str, Any]] | type[list[Any]] = dict - ) -> None: - self.type = tpye - - def validate(self, value): - if not isinstance(value, str): - return False - try: - data = json.loads(value) - if isinstance(data, self.type): - return True - else: - return False - except json.JSONDecodeError: - return False - - def correct(self, value): - return ( - value if self.validate(value) else ("{ }" if self.type is dict else "[ ]") - ) - - -class EncryptValidator(ValidatorBase): - """加密数据验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - try: - dpapi_decrypt(value) - return True - except Exception: - return False - - def correct(self, value: Any) -> Any: - return value if self.validate(value) else dpapi_encrypt("数据损坏, 请重新设置") - - -class VirtualConfigValidator(ValidatorBase): - """虚拟配置验证器""" - - def __init__(self, function: Callable[[], str]): - self.function = function - self.if_init = False - - def validate(self, value): - if not self.if_init: - self.if_init = True - return True - return False - - def correct(self, value): - try: - return self.function() - except Exception as e: - return str(e) - - -class BoolValidator(ValidatorBase): - """布尔值验证器""" - - def validate(self, value): - if not isinstance(value, bool): - return False - return True - - def correct(self, value): - return value if self.validate(value) else False - - -class FileValidator(ValidatorBase): - """文件路径验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - # 允许空字符串(表示未设置路径) - if value == "": - return True - if not Path(value).is_absolute(): - return False - if Path(value).suffix == ".lnk": - return False - return True - - def correct(self, value): - if not isinstance(value, str): - value = str(Path.cwd()) - # 空字符串直接返回 - if value == "": - return "" - if "%APPDATA%" in value: - value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") - if not Path(value).is_absolute(): - value = Path(value).resolve().as_posix() - if Path(value).suffix == ".lnk": - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(value) - value = shortcut.TargetPath - except Exception: - pass - return Path(value).resolve().as_posix() - - -class FolderValidator(ValidatorBase): - """文件夹路径验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - if not Path(value).is_absolute(): - return False - if not Path(value).is_dir(): - return False - return True - - def correct(self, value): - if not isinstance(value, str): - value = str(Path.cwd()) - if "%APPDATA%" in value: - value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") - if not Path(value).is_dir(): - value = Path(value).with_suffix("") - return Path(value).resolve().as_posix() - - -class EmulatorPathValidator(FileValidator): - """模拟器管理器路径验证器""" - - def __init__(self, emulator_type: ConfigItem) -> None: - super().__init__() - - self.emulator_type = emulator_type - - def validate(self, value): - if not isinstance(value, str): - return False - # 允许空字符串(表示未设置路径) - if value == "": - return True - if not Path(value).is_absolute(): - return False - if Path(value).suffix == ".lnk": - return False - - path = Path(value) - - if not path.is_file() or not os.access(path, os.X_OK): - return False - - if ( - self.emulator_type.getValue() in EMULATOR_PATH_BOOK - and path.name - != EMULATOR_PATH_BOOK[self.emulator_type.getValue()]["executables"][0] - ): - return False - - return True - - def correct(self, value): - - if not isinstance(value, str): - value = str(Path.cwd()) - # 空字符串直接返回 - if value == "": - return "" - if "%APPDATA%" in value: - value = value.replace("%APPDATA%", os.getenv("APPDATA") or "") - if not Path(value).is_absolute(): - value = Path(value).resolve().as_posix() - if Path(value).suffix == ".lnk": - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(value) - value = shortcut.TargetPath - except Exception: - pass - - # 不支持矫正的模拟器类型直接返回路径字符串 - if self.emulator_type.getValue() not in EMULATOR_PATH_BOOK: - return Path(value).resolve().as_posix() - - try: - # 从给定路径向上或向下搜索模拟器主管理器程序的完整路径 - path = Path(value) - - # 获取模拟器配置信息 - config = EMULATOR_PATH_BOOK[self.emulator_type.getValue()] - executables = config["executables"] - - # 第一个可执行文件是主管理器程序(优先级最高) - primary_exe = executables[0] - - # 如果输入的是文件,先获取其父目录 - if path.is_file(): - path = path.parent - - # 1. 首先检查当前目录是否直接包含主管理器程序 - # 如果用户给的就是正确的主程序路径,直接返回 - primary_exe_path = path / primary_exe - if primary_exe_path.exists(): - result = str(primary_exe_path) - return result - - # 2. 向上搜索父目录,找到直接包含主管理器程序的目录(最多3层) - candidates = [] - current = path - for level in range(3): - parent = current.parent - if parent == current: # 已到达根目录 - break - - # 只接受直接包含主管理器程序的目录 - parent_exe_path = parent / primary_exe - if parent_exe_path.exists(): - candidates.append( - { - "path": parent, - "exe_path": parent_exe_path, - "depth": len(parent.parts), - "level": level + 1, - } - ) - - current = parent - - # 如果找到了候选目录,选择最优的(深度最小的,即最接近根目录的) - if candidates: - # 排序策略:深度越小越好(越靠近根目录) - candidates.sort(key=lambda x: x["depth"]) - - best_candidate = candidates[0] - result = str(best_candidate["exe_path"]) - - return result - - # 3. 如果向上没找到,尝试向下搜索子目录(仅1层,且必须直接包含主管理器程序) - try: - for subdir in path.iterdir(): - if subdir.is_dir(): - subdir_exe_path = subdir / primary_exe - # 只接受直接包含主管理器程序的子目录 - if subdir_exe_path.exists(): - result = str(subdir_exe_path) - return result - except PermissionError: - pass - - # 4. 检查兄弟目录(必须直接包含主管理器程序) - if path.parent != path: - try: - for sibling in path.parent.iterdir(): - if sibling.is_dir() and sibling != path: - sibling_exe_path = sibling / primary_exe - # 只接受直接包含主管理器程序的兄弟目录 - if sibling_exe_path.exists(): - result = str(sibling_exe_path) - return result - except PermissionError: - pass - - return Path(value).resolve().as_posix() - except Exception: - return Path(value).resolve().as_posix() - - -class UserNameValidator(ValidatorBase): - """用户名验证器""" - - def validate(self, value): - if not isinstance(value, str): - return False - - if not value or not value.strip(): - return False - - if value != value.strip() or value != value.strip("."): - return False - - if any(char in ILLEGAL_CHARS for char in value): - return False - - if value.upper() in RESERVED_NAMES: - return False - if len(value) > 255: - return False - - return True - - def correct(self, value): - if not isinstance(value, str): - value = "默认用户名" - - value = value.strip().strip(".") - - value = "".join(char for char in value if char not in ILLEGAL_CHARS) - - if value.upper() in RESERVED_NAMES or not value: - value = "默认用户名" - - if len(value) > 255: - value = value[:255] - - return value - - -class KeyValidator(ValidatorBase): - """键盘按键格式验证器""" - - def __init__(self, default: str = ""): - self.default = default - - def validate(self, value: Any) -> bool: - return value in pyautogui.KEYBOARD_KEYS - - def correct(self, value: Any) -> Any: - return value if self.validate(value) else self.default - - -class URLValidator(ValidatorBase): - """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): - 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): - return value if self.validate(value) else self.default - - -class ArgumentValidator(ValidatorBase): - - def validate(self, value): - if not isinstance(value, str): - return False - try: - shlex.split(value.strip()) - return True - except ValueError: - return False - - def correct(self, value): - - return value if self.validate(value) else "" - - -class AdvancedArgumentValidator(ValidatorBase): - - def validate(self, value): - if not isinstance(value, str): - return False - try: - for segment in value.split("|"): - segment = segment.strip() - if not segment: - continue - param_str = segment.split("%", 1)[-1].strip() - shlex.split(param_str) - return True - except ValueError: - return False - - def correct(self, value): - - return value if self.validate(value) else "" - - -class ConfigItem: - """配置项""" - - def __init__( - self, - group: str, - name: str, - default: Any, - validator: ValidatorBase = StringValidator(), - legacy_group: str | None = None, - legacy_name: str | None = None, - ): - """ - Parameters - ---------- - group: str - 配置项分组名称 - - name: str - 配置项字段名称 - - default: Any - 配置项默认值 - - validator: ValidatorBase - 配置项验证器, 默认为 None, 表示不进行验证 - """ - self.group = group - self.name = name - self.value: Any = default - self.validator = validator - self.legacy_group_name = ( - (legacy_group or group, legacy_name or name) - if legacy_group or legacy_name - else None - ) - self.is_locked = False - self._slots: list[Callable[[Any], Any]] = [] - - if not self.validator.validate(self.value): - raise ValueError( - f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法" - ) - - def setValue(self, value: Any): - """ - 设置配置项值, 将自动进行验证和修正 - - Parameters - ---------- - value: Any - 要设置的值, 可以是任何合法类型 - """ - - if ( - dpapi_decrypt(self.value) - if isinstance(self.validator, EncryptValidator) - else self.value - ) == value: - return - - if self.is_locked: - raise ValueError(f"配置项 '{self.group}.{self.name}' 已锁定, 无法修改") - - # deepcopy new value - try: - self.value = deepcopy(value) - except Exception: - self.value = value - - if isinstance(self.validator, EncryptValidator): - if self.validator.validate(self.value): - self.value = self.value - else: - self.value = dpapi_encrypt(self.value) - - if not self.validator.validate(self.value): - self.value = self.validator.correct(self.value) - - if len(self._slots) > 0: - asyncio.create_task(self._emit_signal(self.value)) - - def getValue(self, if_decrypt: bool = True) -> Any: - """ - 获取配置项值 - """ - - v = ( - self.value - if self.validator.validate(self.value) - else self.validator.correct(self.value) - ) - - if isinstance(self.validator, EncryptValidator) and if_decrypt: - return dpapi_decrypt(v) - return v - - def bind(self, slot: Callable[[Any], Any]): - """ - 连接槽函数到配置项修改信号 - - Parameters - ---------- - slot: Callable[[Any], Any] - 槽函数,接收新值作为参数,支持同步和异步函数 - """ - if not callable(slot): - raise TypeError("槽函数必须是可调用对象") - - if slot not in self._slots: - self._slots.append(slot) - - def unbind(self, slot: Callable[[Any], Any]): - """ - 断开槽函数连接 - - Parameters - ---------- - slot: Callable[[Any], Any] - 要断开的槽函数 - """ - if slot in self._slots: - self._slots.remove(slot) - - def unbind_all(self): - """断开所有槽函数连接""" - self._slots.clear() - - @logger.catch - async def _emit_signal(self, value: Any) -> None: - """ - 执行所有连接的槽函数, 将新值作为参数传递 - - Parameters - ---------- - value: Any - 新值, 已经过验证和修正 - """ - - for slot in self._slots: - if inspect.iscoroutinefunction(slot): - await slot(value) - else: - slot(value) - - def lock(self): - """ - 锁定配置项, 锁定后无法修改配置项值 - """ - self.is_locked = True - - def unlock(self): - """ - 解锁配置项, 解锁后可以修改配置项值 - """ - self.is_locked = False - - -class ConfigBase: - """ - 配置基类 - - 这个类提供了基本的配置项管理功能, 包括连接配置文件、加载配置数据、获取和设置配置项值等。 - - 此类不支持直接实例化, 必须通过子类来实现具体的配置项, - 请继承此类并在子类中定义具体的配置项, 并在定义完成后调用父类的 `__init__` 方法。 - 若将配置项设为类属性, 则所有实例都会共享同一份配置项数据。 - 若将配置项设为实例属性, 则每个实例都会有独立的配置项数据。 - 子配置项可以是 `MultipleConfig` 的实例。 - """ - - def __init__(self): - self.file: Path | None = None - self.is_locked = False - self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] - - # 配置项索引 - self._config_item_index: dict[str, dict[str, ConfigItem]] = {} - self._multiple_config_index: dict[str, "MultipleConfig[Any]"] = {} - for name in dir(self): - item = getattr(self, name) - - if isinstance(item, ConfigItem): - if not self._config_item_index.get(item.group): - self._config_item_index[item.group] = {} - self._config_item_index[item.group][item.name] = item - - elif isinstance(item, MultipleConfig): - self._multiple_config_index[name] = item - - async def connect(self, path: Path): - """ - 将配置数据绑定到指定配置文件 - - Parameters - ---------- - path: Path - 配置文件路径, 必须为 TOML 文件, 如果不存在则会创建 - """ - - if path.suffix != PRIMARY_CONFIG_SUFFIX: - raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self.file = path - - if not self.file.exists(): - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.touch() - data, legacy_file = _load_config_with_legacy_migration(self.file) - - await self.load(data) - - await self.add_save_method(self.save) - - _backup_legacy_config_if_needed(self.file, legacy_file) - - async def add_save_method( - self, save_method: Callable[[], Coroutine[Any, Any, None]] - ): - """ - 添加父配置项的保存方法 - - Parameters - ---------- - save_method: Callable[[], Coroutine[Any, Any, None]] - 保存方法 - """ - - if save_method != self.save and save_method not in self._save_methods: - self._save_methods.append(save_method) - - for sub_config in self._multiple_config_index.values(): - await sub_config.add_save_method(save_method) - - async def load(self, data: dict[str, Any]): - """ - 从字典加载配置数据 - - 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigItem 实例中。 - 如果字典中包含 "SubConfigsInfo" 键, 则会加载子配置项, 这些子配置项应该是 MultipleConfig 的实例。 - - Parameters - ---------- - data: dict - 配置数据字典 - """ - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - # 加载多配置项类型数据 - sub_configs = data.pop("SubConfigsInfo", {}) - if not isinstance(sub_configs, dict): - sub_configs = {} - for name, sub_config in self._multiple_config_index.items(): - data_for_sub_config = sub_configs.get(name) - if isinstance(data_for_sub_config, dict): - await sub_config.load(data_for_sub_config) - - for group, info in self._config_item_index.items(): - for name, item in info.items(): - try: - item.setValue(data[group][name]) - except Exception: - if item.legacy_group_name is not None: - with suppress(Exception): - item.setValue( - data[item.legacy_group_name[0]][ - item.legacy_group_name[1] - ] - ) - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - """将配置项转换为字典""" - - data = {} - - for group, info in self._config_item_index.items(): - for name, item in info.items(): - data.setdefault(group, {})[name] = item.getValue(if_decrypt) - - for name, item in self._multiple_config_index.items(): - if not data.get("SubConfigsInfo"): - data["SubConfigsInfo"] = {} - data["SubConfigsInfo"][name] = await item.toDict( - if_decrypt, regenerate_uuids - ) - - return data - - def get(self, group: str, name: str) -> Any: - """获取配置项的值""" - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - return self._config_item_index[group][name].getValue() - - async def set(self, group: str, name: str, value: Any): - """ - 设置配置项的值 - - Parameters - ---------- - group: str - 配置项分组名称 - name: str - 配置项名称 - value: Any - 配置项新值 - """ - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self._config_item_index[group][name].setValue(value) - - if self.file: - await self.save() - - await asyncio.gather(*(_() for _ in self._save_methods)) - - def bind(self, group: str, name: str, slot: Callable[[Any], Any]): - """ - 连接槽函数到配置项修改信号 - - Parameters - ---------- - group: str - 配置项分组名称 - name: str - 配置项名称 - slot: Callable[[Any], Any] - 槽函数,接收新值作为参数,支持同步和异步函数 - """ - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self._config_item_index[group][name].bind(slot) - - def unbind(self, group: str, name: str, slot: Callable[[Any], Any]): - """ - 断开槽函数连接 - - Parameters - ---------- - group: str - 配置项分组名称 - name: str - 配置项名称 - slot: Callable[[Any], Any] - 要断开的槽函数 - """ - - if not self._config_item_index.get(group, {}).get(name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self._config_item_index[group][name].unbind(slot) - - async def save(self) -> None: - """保存配置""" - - if not self.file: - raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") - - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.write_text( - dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) - - async def lock(self): - """ - 锁定配置项, 锁定后无法修改配置项值 - """ - - self.is_locked = True - - for group in self._config_item_index.values(): - for item in group.values(): - item.lock() - for config in self._multiple_config_index.values(): - await config.lock() - - async def unlock(self): - """ - 解锁配置项, 解锁后可以修改配置项值 - """ - - self.is_locked = False - - for group in self._config_item_index.values(): - for item in group.values(): - item.unlock() - for config in self._multiple_config_index.values(): - await config.unlock() - - class _ConfigLike(Protocol): @property def is_locked(self) -> bool: ... @@ -1206,15 +203,9 @@ async def unlock(self) -> None: ... class MultipleConfig(Generic[T]): """ - 多配置项管理类 + 多配置项管理类。 - 这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 TOML 文件中。 - 允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。 - - Parameters - ---------- - sub_config_type: List[type] - 子配置项的类型列表, 必须是 ConfigBase 的子类 + 负责管理同一类或同一组配置实例,并统一提供加载、保存、增删改序等接口。 """ def __init__(self, sub_config_type: list[type[T]]): @@ -1222,7 +213,7 @@ def __init__(self, sub_config_type: list[type[T]]): raise ValueError("子配置项类型列表不能为空") self.sub_config_type: dict[str, type[T]] = { - _.__name__: _ for _ in sub_config_type + config_type.__name__: config_type for config_type in sub_config_type } self.file: Path | None = None self.order: list[uuid.UUID] = [] @@ -1231,36 +222,29 @@ def __init__(self, sub_config_type: list[type[T]]): self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] def __getitem__(self, key: uuid.UUID) -> T: - """允许通过 config[uuid] 访问配置项""" if key not in self.data: raise KeyError(f"配置项 '{key}' 不存在") return self.data[key] def __contains__(self, key: uuid.UUID) -> bool: - """允许使用 uuid in config 检查是否存在""" return key in self.data def __len__(self) -> int: - """允许使用 len(config) 获取配置项数量""" return len(self.data) def __repr__(self) -> str: - """更好的字符串表示""" - return f"MultipleConfig(items={len(self.data)}, types={list(self.sub_config_type.keys())})" + return ( + "MultipleConfig(" + f"items={len(self.data)}, " + f"types={list(self.sub_config_type.keys())}" + ")" + ) def __str__(self) -> str: - """用户友好的字符串表示""" return f"MultipleConfig with {len(self.data)} items" - async def connect(self, path: Path): - """ - 将配置文件连接到指定配置文件 - - Parameters - ---------- - path: Path - 配置文件路径, 必须为 TOML 文件, 如果不存在则会创建 - """ + async def connect(self, path: Path) -> None: + """将运行期配置连接到指定 TOML 文件。""" if path.suffix != PRIMARY_CONFIG_SUFFIX: raise ValueError("配置文件必须是带有 '.toml' 扩展名的 TOML 文件。") @@ -1273,25 +257,16 @@ async def connect(self, path: Path): if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.touch() - data, legacy_file = _load_config_with_legacy_migration(self.file) + data, legacy_file = _load_config_with_legacy_migration(self.file) await self.load(data) - await self.add_save_method(self.save) - _backup_legacy_config_if_needed(self.file, legacy_file) async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] - ): - """ - 添加父配置项的保存方法 - - Parameters - ---------- - save_method: Callable[[], Coroutine[Any, Any, None]] - 保存方法, 必须是一个协程函数, 无参数, 无返回值 - """ + ) -> None: + """为当前管理器及其子配置添加级联保存方法。""" if save_method != self.save and save_method not in self._save_methods: self._save_methods.append(save_method) @@ -1299,19 +274,8 @@ async def add_save_method( for sub_config in self.data.values(): await sub_config.add_save_method(save_method) - async def load(self, data: dict[str, Any]): - """ - 从字典加载配置数据 - - 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigBase 实例中。 - 如果字典中包含 "instances" 键, 则会加载子配置项, 这些子配置项应该是 ConfigBase 子类的实例。 - 如果字典中没有 "instances" 键, 则清空当前配置项。 - - Parameters - ---------- - data: dict - 配置数据字典 - """ + async def load(self, data: dict[str, Any]) -> None: + """从字典加载多实例配置数据。""" if self.is_locked: raise ValueError("配置已锁定, 无法修改") @@ -1319,62 +283,57 @@ async def load(self, data: dict[str, Any]): self.order = [] self.data = {} - if not data.get("instances"): + instances = data.get("instances") + if not isinstance(instances, list): return - for instance in data["instances"]: + for instance in instances: if not isinstance(instance, dict): continue uid_str = instance.get("uid") - if not isinstance(uid_str, str): + type_name = instance.get("type") + if not isinstance(uid_str, str) or not isinstance(type_name, str): + continue + if type_name not in self.sub_config_type: continue instance_data = data.get(uid_str) if not isinstance(instance_data, dict): continue - type_name = instance.get("type") + try: + uid = uuid.UUID(uid_str) + except (TypeError, ValueError): + continue - if type_name in self.sub_config_type: - self.order.append(uuid.UUID(uid_str)) - self.data[self.order[-1]] = self.sub_config_type[type_name]() - await self.data[self.order[-1]].load(instance_data) + config = self.sub_config_type[type_name]() + self.order.append(uid) + self.data[uid] = config + await config.load(instance_data) if self.file: await self.save() - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) async def toDict( self, if_decrypt: bool = True, regenerate_uuids: bool = False ) -> dict[str, Any]: - """ - 将配置项转换为字典 - - Arguments - ---------- - if_decrypt: bool - 是否解密数据, 默认为 True - regenerate_uuids: bool - 是否重新生成 UUID, 默认为 False - - Returns - ------- - Dict[str, Union[list, dict]] - 配置项数据字典 - """ + """将全部子配置序列化为统一字典结构。""" uuid_book: dict[uuid.UUID, uuid.UUID] = { - _: uuid.uuid4() if regenerate_uuids else _ for _ in self.order + uid: uuid.uuid4() if regenerate_uuids else uid for uid in self.order } data: dict[str, Any] = { "instances": [ - {"uid": str(uuid_book[_]), "type": type(self.data[_]).__name__} - for _ in self.order + {"uid": str(uuid_book[uid]), "type": type(self.data[uid]).__name__} + for uid in self.order ] } + for uid, config in self.items(): data[str(uuid_book[uid])] = await config.toDict( if_decrypt, regenerate_uuids @@ -1383,35 +342,23 @@ async def toDict( return data async def get(self, uid: uuid.UUID) -> dict[str, Any]: - """ - 获取指定 UID 的配置项 - - Parameters - ---------- - uid: uuid.UUID - 要获取的配置项的唯一标识符 - Returns - ------- - Dict[str, Union[list, dict]] - 对应的配置项数据字典 - """ + """获取指定 UID 的单个配置。""" if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在。") data: dict[str, Any] = { "instances": [ - {"uid": str(_), "type": type(self.data[_]).__name__} - for _ in self.order - if _ == uid + {"uid": str(current_uid), "type": type(self.data[current_uid]).__name__} + for current_uid in self.order + if current_uid == uid ] } data[str(uid)] = await self.data[uid].toDict() - return data - async def save(self): - """保存配置""" + async def save(self) -> None: + """保存当前多实例配置。""" if not self.file: raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") @@ -1423,57 +370,37 @@ async def save(self): ) async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: - """ - 添加一个新的配置项 - - Parameters - ---------- - config_type: type - 配置项的类型, 必须是初始化时已声明的 ConfigBase 子类 - - Returns - ------- - tuple[uuid.UUID, ConfigBase] - 新创建的配置项的唯一标识符和实例 - """ + """新增一个指定类型的子配置实例。""" if config_type not in self.sub_config_type.values(): raise ValueError(f"配置类型 {config_type.__name__} 不被允许") - if self.is_locked: raise ValueError("配置已锁定, 无法修改") uid = uuid.uuid4() + config = config_type() self.order.append(uid) - self.data[uid] = config_type() + self.data[uid] = config for save_method in self._save_methods: - await self.data[uid].add_save_method(save_method) + await config.add_save_method(save_method) if self.file: - await self.data[uid].add_save_method(self.save) + await config.add_save_method(self.save) await self.save() - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) - return uid, self.data[uid] + return uid, config - async def remove(self, uid: uuid.UUID): - """ - 移除配置项 - - Parameters - ---------- - uid: uuid.UUID - 要移除的配置项的唯一标识符 - """ + async def remove(self, uid: uuid.UUID) -> None: + """移除一个子配置实例。""" if self.is_locked: raise ValueError("配置已锁定, 无法修改") - if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在") - if self.data[uid].is_locked: raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除") @@ -1483,21 +410,14 @@ async def remove(self, uid: uuid.UUID): if self.file: await self.save() - await asyncio.gather(*(_() for _ in self._save_methods)) - - async def setOrder(self, order: list[uuid.UUID]): - """ - 设置配置项的顺序 + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) - Parameters - ---------- - order: List[uuid.UUID] - 新的配置项顺序 - """ + async def setOrder(self, order: list[uuid.UUID]) -> None: # noqa: N802 + """设置子配置实例顺序。""" if set(order) != set(self.data.keys()): raise ValueError("顺序与当前配置项不匹配") - if self.is_locked: raise ValueError("配置已锁定, 无法修改") @@ -1506,42 +426,36 @@ async def setOrder(self, order: list[uuid.UUID]): if self.file: await self.save() - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) - async def lock(self): - """ - 锁定配置项, 锁定后无法修改配置项值 - """ + async def lock(self) -> None: + """锁定当前管理器及全部子配置。""" self.is_locked = True - for item in self.values(): await item.lock() - async def unlock(self): - """ - 解锁配置项, 解锁后可以修改配置项值 - """ + async def unlock(self) -> None: + """解锁当前管理器及全部子配置。""" self.is_locked = False - for item in self.values(): await item.unlock() def keys(self): - """返回配置项的所有唯一标识符""" + """返回全部 UID。""" return iter(self.order) def values(self): - """返回配置项的所有实例""" + """按顺序返回全部子配置实例。""" if not self.data: return iter(()) - - return (self.data[_] for _ in self.order) + return (self.data[uid] for uid in self.order) def items(self): - """返回配置项的所有唯一标识符和实例的元组""" + """按顺序返回 `(uid, config)` 对。""" return zip(self.keys(), self.values()) diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index b4d5f152..ead3623e 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -14,6 +14,7 @@ dump_toml, load_config_with_legacy_migration, ) +from .types import _EncryptedFieldMarker, decrypt_encrypted_string SaveMethod = Callable[[], Coroutine[Any, Any, None]] @@ -37,6 +38,36 @@ def _normalize_mapping(value: Any) -> dict[str, Any]: return {str(key): item for key, item in mapping.items()} +def _is_encrypted_field(group_model: BaseModel, field_name: str) -> bool: + """判断字段是否为对外需要自动解密的字符串字段。""" + + field = type(group_model).model_fields.get(field_name) + if field is None: + return False + return any(isinstance(item, _EncryptedFieldMarker) for item in field.metadata) + + +def _export_group_model(group_model: BaseModel, if_decrypt: bool) -> dict[str, Any]: + """将分组模型导出为字典,并按需解密加密字段。""" + + data: dict[str, Any] = {} + + for field_name in type(group_model).model_fields: + value = getattr(group_model, field_name) + + if isinstance(value, BaseModel): + data[field_name] = _export_group_model(value, if_decrypt) + continue + + if if_decrypt and _is_encrypted_field(group_model, field_name): + data[field_name] = decrypt_encrypted_string(str(value)) + continue + + data[field_name] = value + + return data + + class PydanticConfigBase(BaseModel): """基于 pydantic v2 的配置基类,兼容旧版 ConfigBase 常用接口。""" @@ -155,7 +186,7 @@ async def toDict( data: dict[str, Any] = {} for group_name, group_model in self._group_index().items(): - data[group_name] = group_model.model_dump(mode="python") + data[group_name] = _export_group_model(group_model, if_decrypt) for name, item in self._multiple_config_index().items(): if "SubConfigsInfo" not in data: @@ -170,7 +201,11 @@ def get(self, group: str, name: str) -> Any: group_model = self._group_index().get(group) if group_model is None or not hasattr(group_model, name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") - return getattr(group_model, name) + + value = getattr(group_model, name) + if _is_encrypted_field(group_model, name): + return decrypt_encrypted_string(str(value)) + return value async def set(self, group: str, name: str, value: Any) -> None: group_model = self._group_index().get(group) diff --git a/app/core/config/types.py b/app/core/config/types.py index ccc1fac1..f0066f9e 100644 --- a/app/core/config/types.py +++ b/app/core/config/types.py @@ -12,6 +12,10 @@ from app.utils.security import dpapi_decrypt, dpapi_encrypt +class _EncryptedFieldMarker: + """标记需要在对外读取时自动解密的字段。""" + + def _to_string(value: Any) -> str: if isinstance(value, str): return value @@ -116,7 +120,11 @@ def decrypt_encrypted_string(value: str) -> str: YmdHmsString = Annotated[str, AfterValidator(_validate_ymd_hms_string)] UrlString = Annotated[str, AfterValidator(_validate_url_string)] KeyboardKeyString = Annotated[str, AfterValidator(_validate_keyboard_key)] -EncryptedString = Annotated[str, AfterValidator(_normalize_encrypted_string)] +EncryptedString = Annotated[ + str, + _EncryptedFieldMarker(), + AfterValidator(_normalize_encrypted_string), +] __all__ = [ diff --git a/app/models/__init__.py b/app/models/__init__.py index c71720ce..c1d072b5 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,4 @@ -from . import dto, emulator, schema, task +from . import dto, emulator, task from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook from .general import GeneralConfig, GeneralUserConfig from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig @@ -26,6 +26,5 @@ "CLASS_BOOK", "dto", "emulator", - "schema", "task", ] diff --git a/app/models/schema.py b/app/models/schema.py deleted file mode 100644 index 6875eff8..00000000 --- a/app/models/schema.py +++ /dev/null @@ -1,1462 +0,0 @@ -# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software -# Copyright © 2024-2025 DLmaster361 -# Copyright © 2025 MoeSnowyFox -# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with AUTO-MAS. If not, see . - -# Contact: DLmaster_361@163.com - - -# pyright: reportUnknownVariableType=false, reportGeneralTypeIssues=false -from pydantic import BaseModel, Field -from typing import Any, Dict, List, Union, Optional, Literal - - -class OutBase(BaseModel): - code: int = Field(default=200, description="状态码") - status: str = Field(default="success", description="操作状态") - message: str = Field(default="操作成功", description="操作消息") - - -class InfoOut(OutBase): - data: Dict[str, Any] = Field(..., description="收到的服务器数据") - - -class VersionOut(OutBase): - if_need_update: bool = Field(..., description="后端代码是否需要更新") - current_time: str = Field(..., description="后端代码当前时间戳") - current_hash: str = Field(..., description="后端代码当前哈希值") - - -class NoticeOut(OutBase): - if_need_show: bool = Field(..., description="是否需要显示公告") - data: Dict[str, str] = Field( - ..., description="公告信息, key为公告标题, value为公告内容" - ) - - -class TagItem(BaseModel): - text: str = Field(..., description="标签文本") - color: Literal[ - "red", - "blue", - "green", - "yellow", - "orange", - "purple", - "pink", - "brown", - "black", - "white", - "gray", - "silver", - "gold", - ] = Field(..., description="标签颜色") - - -class ComboBoxItem(BaseModel): - label: str = Field(..., description="展示值") - value: Optional[str] = Field(..., description="实际值") - - -class ComboBoxOut(OutBase): - data: List[ComboBoxItem] = Field(..., description="下拉框选项") - - -class GetStageIn(BaseModel): - type: Literal[ - "User", - "Today", - "ALL", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] = Field( - ..., - description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项", - ) - - -class EmulatorConfigIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["EmulatorConfig"] = Field(..., description="配置类型") - - -class EmulatorConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="模拟器名称") - Type: Optional[Literal["general", "mumu", "ldplayer"]] = Field( - default=None, description="模拟器类型" - ) - Path: Optional[str] = Field(default=None, description="模拟器路径") - BossKey: Optional[str] = Field(default=None, description="老板键快捷键配置") - MaxWaitTime: Optional[int] = Field(default=None, description="最大等待时间(秒)") - - -class EmulatorConfig(BaseModel): - Info: Optional[EmulatorConfig_Info] = Field( - default=None, description="模拟器基础信息" - ) - - -class ToolsConfig_ArknightsPC(BaseModel): - Enabled: bool | None = Field(default=None, description="是否启用 ArknightsPC 工具") - PauseKey: str | None = Field(default=None, description="暂停键位") - SelectDeployedKey: str | None = Field( - default=None, description="选中已部署干员键位" - ) - UseSkillKey: str | None = Field(default=None, description="释放技能键位") - RetreatKey: str | None = Field(default=None, description="撤退键位") - NextFrameKey: str | None = Field(default=None, description="下一帧键位") - AnotherQuitKey: str | None = Field(default=None, description="自定义退出、暂停键位") - Status: str | None = Field(default=None, description="工具状态 Tag") - - -class ToolsConfig(BaseModel): - ArknightsPC: ToolsConfig_ArknightsPC | None = Field( - default=None, description="明日方舟PC工具配置" - ) - - -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[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表示永久保存" - ) - IfAllowSleep: Optional[bool] = Field(default=None, description="允许休眠") - IfSilence: Optional[bool] = Field(default=None, description="静默模式") - IfAgreeBilibili: Optional[bool] = Field( - default=None, description="同意哔哩哔哩用户协议" - ) - IfBlockAd: Optional[bool] = Field(default=None, description="屏蔽模拟器广告") - - -class GlobalConfig_Voice(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="语音功能是否启用") - Type: Optional[Literal["simple", "noisy"]] = Field( - default=None, description="语音类型, simple为简洁, noisy为聒噪" - ) - - -class GlobalConfig_Start(BaseModel): - IfSelfStart: Optional[bool] = Field( - default=None, description="是否在系统启动时自动运行" - ) - IfMinimizeDirectly: Optional[bool] = Field( - default=None, description="启动时是否直接最小化到托盘而不显示主窗口" - ) - - -class GlobalConfig_UI(BaseModel): - IfShowTray: Optional[bool] = Field(default=None, description="是否常态显示托盘图标") - IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘") - - -class GlobalConfig_Notify(BaseModel): - SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field( - default=None, description="任务结果推送时机" - ) - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendSixStar: Optional[bool] = Field( - default=None, description="是否发送公招六星通知" - ) - IfPushPlyer: Optional[bool] = Field(default=None, description="是否推送系统通知") - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") - IfKoishiSupport: Optional[bool] = Field( - default=None, description="是否启用Koishi支持" - ) - KoishiServerAddress: Optional[str] = Field( - default=None, description="Koishi服务器地址" - ) - KoishiToken: Optional[str] = Field(default=None, description="Koishi Token") - SMTPServerAddress: Optional[str] = Field(default=None, description="SMTP服务器地址") - AuthorizationCode: Optional[str] = Field(default=None, description="SMTP授权码") - FromAddress: Optional[str] = Field(default=None, description="邮件发送地址") - ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") - IfServerChan: Optional[bool] = Field( - default=None, description="是否使用ServerChan推送" - ) - ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥") - - -class GlobalConfig_Update(BaseModel): - IfAutoUpdate: Optional[bool] = Field(default=None, description="是否自动更新") - Source: Optional[Literal["GitHub", "MirrorChyan", "AutoSite"]] = Field( - default=None, description="更新源: GitHub源, Mirror酱源, 自建源" - ) - Channel: Optional[Literal["stable", "beta"]] = Field( - default=None, description="更新渠道: 稳定版, 测试版" - ) - ProxyAddress: Optional[str] = Field(default=None, description="网络代理地址") - MirrorChyanCDK: Optional[str] = Field(default=None, description="Mirror酱CDK") - - -class GlobalConfig(BaseModel): - Function: Optional[GlobalConfig_Function] = Field( - default=None, description="功能相关配置" - ) - Voice: Optional[GlobalConfig_Voice] = Field( - default=None, description="语音相关配置" - ) - Start: Optional[GlobalConfig_Start] = Field( - default=None, description="启动相关配置" - ) - UI: Optional[GlobalConfig_UI] = Field(default=None, description="界面相关配置") - Notify: Optional[GlobalConfig_Notify] = Field( - default=None, description="通知相关配置" - ) - Update: Optional[GlobalConfig_Update] = Field( - default=None, description="更新相关配置" - ) - - -class QueueIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["QueueConfig"] = Field(..., description="配置类型") - - -class QueueItemIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["QueueItem"] = Field(..., description="配置类型") - - -class TimeSetIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["TimeSet"] = Field(..., description="配置类型") - - -class QueueItem_Info(BaseModel): - ScriptId: Optional[str] = Field( - default=None, description="任务所对应的脚本ID, 为None时表示未选择" - ) - - -class QueueItem(BaseModel): - Info: Optional[QueueItem_Info] = Field(default=None, description="队列项") - - -class TimeSet_Info(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用") - Days: Optional[ - List[ - Literal[ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] - ] - ] = Field(default=None, description="执行周期, 可多选") - Time: Optional[str] = Field(default=None, description="时间设置, 格式为HH:MM") - - -class TimeSet(BaseModel): - Info: Optional[TimeSet_Info] = Field(default=None, description="时间项") - - -class QueueConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="队列名称") - TimeEnabled: Optional[bool] = Field(default=None, description="是否启用定时") - StartUpEnabled: Optional[bool] = Field(default=None, description="是否启动时运行") - AfterAccomplish: Optional[ - Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] - ] = Field(default=None, description="完成后操作") - - -class QueueConfig(BaseModel): - Info: Optional[QueueConfig_Info] = Field(default=None, description="队列信息") - - -class ScriptIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["MaaConfig", "GeneralConfig", "SrcConfig", "MaaEndConfig"] = Field( - ..., description="配置类型" - ) - - -class UserIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal[ - "MaaUserConfig", "GeneralUserConfig", "SrcUserConfig", "MaaEndUserConfig" - ] = Field(..., description="配置类型") - - -class MaaUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名") - Id: Optional[str] = Field(default=None, description="用户ID") - Mode: Optional[Literal["简洁", "详细"]] = Field( - default=None, description="用户配置模式" - ) - StageMode: Optional[str] = Field(default=None, description="关卡配置模式") - Server: Optional[ - Literal["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"] - ] = Field(default=None, description="服务器") - Status: Optional[bool] = Field(default=None, description="用户状态") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - Annihilation: Optional[ - Literal[ - "Close", - "Annihilation", - "Chernobog@Annihilation", - "LungmenOutskirts@Annihilation", - "LungmenDowntown@Annihilation", - ] - ] = Field(default=None, description="剿灭模式") - InfrastMode: Optional[Literal["Normal", "Rotation", "Custom"]] = Field( - default=None, description="基建模式" - ) - InfrastName: Optional[str] = Field(default=None, description="基建方案名称") - InfrastIndex: Optional[str] = Field(default=None, description="基建方案索引") - Password: Optional[str] = Field(default=None, description="密码") - Notes: Optional[str] = Field(default=None, description="备注") - MedicineNumb: Optional[int] = Field(default=None, description="吃理智药数量") - SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field( - default=None, description="连战次数" - ) - Stage: Optional[str] = Field(default=None, description="关卡选择") - Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1") - Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2") - Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3") - Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡") - IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到") - SklandToken: Optional[str] = Field(default=None, description="SklandToken") - Tag: Optional[str] = Field(default=None, description="状态标签列表") - - -class MaaUserConfig_Data(BaseModel): - IfPassCheck: Optional[bool] = Field(default=None, description="是否通过人工排查") - - -class MaaUserConfig_Task(BaseModel): - IfStartUp: Optional[bool] = Field(default=None, description="开始唤醒") - IfRecruit: Optional[bool] = Field(default=None, description="自动公招") - IfInfrast: Optional[bool] = Field(default=None, description="基建换班") - IfFight: Optional[bool] = Field(default=None, description="理智作战") - IfMall: Optional[bool] = Field(default=None, description="信用收支") - IfAward: Optional[bool] = Field(default=None, description="领取奖励") - IfRoguelike: Optional[bool] = Field(default=None, description="自动肉鸽") - IfReclamation: Optional[bool] = Field(default=None, description="生息演算") - - -class MaaUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendSixStar: Optional[bool] = Field(default=None, description="是否发送高资喜报") - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") - ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") - IfServerChan: Optional[bool] = Field( - default=None, description="是否使用Server酱推送" - ) - ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey") - - -class GeneralUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") - ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") - IfServerChan: Optional[bool] = Field( - default=None, description="是否使用Server酱推送" - ) - ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey") - - -class MaaUserConfig(BaseModel): - Info: Optional[MaaUserConfig_Info] = Field(default=None, description="基础信息") - Data: Optional[MaaUserConfig_Data] = Field(default=None, description="用户数据") - Task: Optional[MaaUserConfig_Task] = Field(default=None, description="任务列表") - Notify: Optional[MaaUserConfig_Notify] = Field(default=None, description="单独通知") - - -class MaaConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="脚本名称") - Path: Optional[str] = Field(default=None, description="脚本路径") - - -class MaaConfig_Emulator(BaseModel): - Id: Optional[str] = Field(default=None, description="模拟器ID") - Index: Optional[str] = Field(default=None, description="模拟器多开实例索引") - - -class MaaConfig_Run(BaseModel): - TaskTransitionMethod: Optional[Literal["NoAction", "ExitGame", "ExitEmulator"]] = ( - Field(default=None, description="简洁任务间切换方式") - ) - ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") - AnnihilationTimeLimit: Optional[int] = Field( - default=None, description="剿灭超时限制" - ) - RoutineTimeLimit: Optional[int] = Field(default=None, description="日常超时限制") - AnnihilationAvoidWaste: Optional[bool] = Field( - default=None, description="剿灭避免无代理卡浪费理智" - ) - - -class MaaConfig(BaseModel): - Info: Optional[MaaConfig_Info] = Field(default=None, description="脚本基础信息") - Emulator: Optional[MaaConfig_Emulator] = Field( - default=None, description="模拟器配置" - ) - Run: Optional[MaaConfig_Run] = Field(default=None, description="脚本运行配置") - - -class GeneralUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名") - Status: Optional[bool] = Field(default=None, description="用户状态") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - IfScriptBeforeTask: Optional[bool] = Field( - default=None, description="是否在任务前执行脚本" - ) - ScriptBeforeTask: Optional[str] = Field(default=None, description="任务前脚本路径") - IfScriptAfterTask: Optional[bool] = Field( - default=None, description="是否在任务后执行脚本" - ) - ScriptAfterTask: Optional[str] = Field(default=None, description="任务后脚本路径") - Notes: Optional[str] = Field(default=None, description="备注") - Tag: Optional[str] = Field( - default=None, description="用户标签列表(JSON字符串,TagItem的dict列表)" - ) - - -class GeneralUserConfig_Data(BaseModel): - LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期") - ProxyTimes: Optional[int] = Field(default=None, description="代理次数") - - -class GeneralUserConfig(BaseModel): - Info: Optional[GeneralUserConfig_Info] = Field(default=None, description="用户信息") - Data: Optional[GeneralUserConfig_Data] = Field(default=None, description="用户数据") - Notify: Optional[GeneralUserConfig_Notify] = Field( - default=None, description="单独通知" - ) - - -class GeneralConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="脚本名称") - RootPath: Optional[str] = Field(default=None, description="脚本根目录") - - -class GeneralConfig_Script(BaseModel): - ScriptPath: Optional[str] = Field(default=None, description="脚本可执行文件路径") - Arguments: Optional[str] = Field(default=None, description="脚本启动附加命令参数") - IfTrackProcess: Optional[bool] = Field( - default=None, description="是否追踪脚本子进程" - ) - TrackProcessName: Optional[str] = Field(default=None, description="追踪进程名称") - TrackProcessExe: Optional[str] = Field(default=None, description="追踪进程文件路径") - TrackProcessCmdline: Optional[str] = Field( - default=None, description="追踪进程启动命令行参数" - ) - ConfigPath: Optional[str] = Field(default=None, description="配置文件路径") - ConfigPathMode: Optional[Literal["File", "Folder"]] = Field( - default=None, description="配置文件类型: 单个文件, 文件夹" - ) - UpdateConfigMode: Optional[Literal["Never", "Success", "Failure", "Always"]] = ( - Field( - default=None, - description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时", - ) - ) - LogPath: Optional[str] = Field(default=None, description="日志文件路径") - LogPathFormat: Optional[str] = Field(default=None, description="日志文件名格式") - LogTimeStart: Optional[int] = Field(default=None, description="日志时间戳开始位置") - LogTimeEnd: Optional[int] = Field(default=None, description="日志时间戳结束位置") - LogTimeFormat: Optional[str] = Field(default=None, description="日志时间戳格式") - SuccessLog: Optional[str] = Field(default=None, description="成功时日志") - ErrorLog: Optional[str] = Field(default=None, description="错误时日志") - - -class GeneralConfig_Game(BaseModel): - Enabled: Optional[bool] = Field( - default=None, description="游戏/模拟器相关功能是否启用" - ) - Type: Optional[Literal["Emulator", "Client", "URL"]] = Field( - default=None, description="类型: 模拟器, PC端, URL协议" - ) - Path: Optional[str] = Field(default=None, description="游戏/模拟器程序路径") - URL: Optional[str] = Field(default=None, description="自定义协议URL") - ProcessName: Optional[str] = Field(default=None, description="游戏进程名称") - Arguments: Optional[str] = Field(default=None, description="游戏/模拟器启动参数") - WaitTime: Optional[int] = Field(default=None, description="游戏/模拟器等待启动时间") - IfForceClose: Optional[bool] = Field( - default=None, description="是否强制关闭游戏/模拟器进程" - ) - EmulatorId: Optional[str] = Field(default=None, description="模拟器ID") - EmulatorIndex: Optional[str] = Field(default=None, description="模拟器多开实例索引") - - -class GeneralConfig_Run(BaseModel): - ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") - RunTimeLimit: Optional[int] = Field(default=None, description="日志超时限制") - - -class GeneralConfig(BaseModel): - Info: Optional[GeneralConfig_Info] = Field(default=None, description="脚本基础信息") - Script: Optional[GeneralConfig_Script] = Field(default=None, description="脚本配置") - Game: Optional[GeneralConfig_Game] = Field(default=None, description="游戏配置") - Run: Optional[GeneralConfig_Run] = Field(default=None, description="运行配置") - - -class MaaEndUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名") - Status: Optional[bool] = Field(default=None, description="用户状态") - Id: Optional[str] = Field(default=None, description="用户ID") - Password: Optional[str] = Field(default=None, description="密码") - Mode: Optional[Literal["简洁", "详细"]] = Field( - default=None, description="配置模式" - ) - Resource: Optional[Literal["官服"]] = Field(default=None, description="资源名称") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - Notes: Optional[str] = Field(default=None, description="备注") - IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到") - SklandToken: Optional[str] = Field(default=None, description="SklandToken") - Tag: Optional[str] = Field(default=None, description="用户标签信息") - - -class MaaEndUserConfig_Task(BaseModel): - ProtocolSpaceTab: Optional[ - Literal["OperatorProgression", "WeaponProgression", "CrisisDrills"] - ] = Field(default=None, description="协议空间选项卡") - OperatorProgression: Optional[ - Literal["OperatorEXP", "Promotions", "T-Creds", "SkillUp"] - ] = Field(default=None, description="干员养成任务") - WeaponProgression: Optional[Literal["WeaponEXP", "WeaponTune"]] = Field( - default=None, description="武器养成任务" - ) - CrisisDrills: Optional[ - Literal[ - "AdvancedProgression1", - "AdvancedProgression2", - "AdvancedProgression3", - "AdvancedProgression4", - "AdvancedProgression5", - ] - ] = Field(default=None, description="危境预演任务") - RewardsSetOption: Optional[Literal["RewardsSetA", "RewardsSetB"]] = Field( - default=None, description="奖励套组选项" - ) - - -class MaaEndUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件") - ToAddress: Optional[str] = Field(default=None, description="收件地址") - IfServerChan: Optional[bool] = Field(default=None, description="是否启用Server酱") - ServerChanKey: Optional[str] = Field(default=None, description="Server酱密钥") - - -class MaaEndUserConfig(BaseModel): - Info: Optional[MaaEndUserConfig_Info] = Field(default=None, description="用户信息") - Task: Optional[MaaEndUserConfig_Task] = Field(default=None, description="任务配置") - Notify: Optional[MaaEndUserConfig_Notify] = Field( - default=None, description="通知配置" - ) - - -class MaaEndConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="脚本名称") - Path: Optional[str] = Field(default=None, description="脚本路径") - - -class MaaEndConfig_Run(BaseModel): - RunTimeLimit: Optional[int] = Field( - default=None, description="运行时间限制(分钟)" - ) - ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") - - -class MaaEndConfig_Game(BaseModel): - ControllerType: Optional[ - Literal["Win32-Window", "Win32-Window-Background", "Win32-Front", "ADB"] - ] = Field(default=None, description="控制器类型") - Path: Optional[str] = Field(default=None, description="终末地客户端路径") - Arguments: Optional[str] = Field(default=None, description="游戏启动参数") - WaitTime: Optional[int] = Field(default=None, description="游戏等待时间") - EmulatorId: Optional[str] = Field(default=None, description="模拟器ID") - EmulatorIndex: Optional[str] = Field(default=None, description="模拟器索引") - CloseOnFinish: Optional[bool] = Field(default=None, description="结束后关闭游戏") - - -class MaaEndConfig(BaseModel): - Info: Optional[MaaEndConfig_Info] = Field(default=None, description="脚本信息") - Run: Optional[MaaEndConfig_Run] = Field(default=None, description="运行配置") - Game: Optional[MaaEndConfig_Game] = Field(default=None, description="游戏配置") - - -class SrcUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名称") - Status: Optional[bool] = Field(default=None, description="是否启用") - Id: Optional[str] = Field(default=None, description="用户ID") - Password: Optional[str] = Field(default=None, description="密码") - Mode: Optional[Literal["简洁", "详细"]] = Field( - default=None, description="脚本模式" - ) - Server: Optional[ - Literal[ - "CN-Official", - "CN-Bilibili", - "VN-Official", - "OVERSEA-America", - "OVERSEA-Asia", - "OVERSEA-Europe", - "OVERSEA-TWHKMO", - ] - ] = Field(default=None, description="游戏服务器") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - Notes: Optional[str] = Field(default=None, description="备注") - Tag: Optional[str] = Field(default=None, description="用户标签信息") - - -class SrcUserConfig_Stage(BaseModel): - Channel: Literal["Relic", "Materials", "Ornament"] | None = Field( - default=None, description="关卡通道" - ) - Relic: ( - Literal[ - "-", - "Cavern_of_Corrosion_Path_of_Possession", - "Cavern_of_Corrosion_Path_of_Hidden_Salvation", - "Cavern_of_Corrosion_Path_of_Thundersurge", - "Cavern_of_Corrosion_Path_of_Aria", - "Cavern_of_Corrosion_Path_of_Uncertainty", - "Cavern_of_Corrosion_Path_of_Cavalier", - "Cavern_of_Corrosion_Path_of_Dreamdive", - "Cavern_of_Corrosion_Path_of_Darkness", - "Cavern_of_Corrosion_Path_of_Elixir_Seekers", - "Cavern_of_Corrosion_Path_of_Conflagration", - "Cavern_of_Corrosion_Path_of_Holy_Hymn", - "Cavern_of_Corrosion_Path_of_Providence", - "Cavern_of_Corrosion_Path_of_Drifting", - "Cavern_of_Corrosion_Path_of_Jabbing_Punch", - "Cavern_of_Corrosion_Path_of_Gelid_Wind", - ] - | None - ) = Field(default=None, description="遗器关卡") - Materials: ( - Literal[ - "-", - "Calyx_Golden_Memories_Planarcadia", - "Calyx_Golden_Aether_Planarcadia", - "Calyx_Golden_Treasures_Planarcadia", - "Calyx_Golden_Memories_Amphoreus", - "Calyx_Golden_Aether_Amphoreus", - "Calyx_Golden_Treasures_Amphoreus", - "Calyx_Golden_Memories_Penacony", - "Calyx_Golden_Aether_Penacony", - "Calyx_Golden_Treasures_Penacony", - "Calyx_Golden_Memories_The_Xianzhou_Luofu", - "Calyx_Golden_Aether_The_Xianzhou_Luofu", - "Calyx_Golden_Treasures_The_Xianzhou_Luofu", - "Calyx_Golden_Memories_Jarilo_VI", - "Calyx_Golden_Aether_Jarilo_VI", - "Calyx_Golden_Treasures_Jarilo_VI", - "Calyx_Crimson_Destruction_Herta_StorageZone", - "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", - "Calyx_Crimson_Preservation_Herta_SupplyZone", - "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", - "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", - "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", - "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", - "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", - "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", - "Calyx_Crimson_Erudition_Jarilo_RivetTown", - "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", - "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", - "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", - "Calyx_Crimson_Nihility_Jarilo_GreatMine", - "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", - "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", - "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", - "Stagnant_Shadow_Quanta", - "Stagnant_Shadow_Gust", - "Stagnant_Shadow_Fulmination", - "Stagnant_Shadow_Blaze", - "Stagnant_Shadow_Spike", - "Stagnant_Shadow_Rime", - "Stagnant_Shadow_Mirage", - "Stagnant_Shadow_Icicle", - "Stagnant_Shadow_Doom", - "Stagnant_Shadow_Puppetry", - "Stagnant_Shadow_Abomination", - "Stagnant_Shadow_Scorch", - "Stagnant_Shadow_Celestial", - "Stagnant_Shadow_Perdition", - "Stagnant_Shadow_Nectar", - "Stagnant_Shadow_Roast", - "Stagnant_Shadow_Ire", - "Stagnant_Shadow_Duty", - "Stagnant_Shadow_Timbre", - "Stagnant_Shadow_Mechwolf", - "Stagnant_Shadow_Gloam", - "Stagnant_Shadow_Sloggyre", - "Stagnant_Shadow_Gelidmoon", - "Stagnant_Shadow_Deepsheaf", - "Stagnant_Shadow_Cinders", - "Stagnant_Shadow_Sirens", - "Stagnant_Shadow_Ashes", - "Stagnant_Shadow_Soundburst", - ] - | None - ) = Field(default=None, description="材料关卡") - Ornament: ( - Literal[ - "-", - "Divergent_Universe_Within_the_West_Wind", - "Divergent_Universe_Moonlit_Blood", - "Divergent_Universe_Unceasing_Strife", - "Divergent_Universe_Famished_Worker", - "Divergent_Universe_Eternal_Comedy", - "Divergent_Universe_To_Sweet_Dreams", - "Divergent_Universe_Pouring_Blades", - "Divergent_Universe_Fruit_of_Evil", - "Divergent_Universe_Permafrost", - "Divergent_Universe_Gentle_Words", - "Divergent_Universe_Smelted_Heart", - "Divergent_Universe_Untoppled_Walls", - ] - | None - ) = Field(default=None, description="饰品关卡") - ExtractReservedTrailblazePower: Optional[bool] = Field( - default=None, description="使用储备开拓力" - ) - UseFuel: Optional[bool] = Field(default=None, description="使用燃料") - FuelReserve: Optional[int] = Field(default=None, description="保留的燃料数量") - EchoOfWar: Optional[str] = Field(default=None, description="历战余响关卡") - SimulatedUniverseWorld: Optional[str] = Field( - default=None, description="模拟宇宙关卡" - ) - - -class SrcUserConfig_Data(BaseModel): - LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期") - ProxyTimes: Optional[int] = Field(default=None, description="代理次数") - IfPassCheck: Optional[bool] = Field(default=None, description="是否通过检查") - - -class SrcUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件") - ToAddress: Optional[str] = Field(default=None, description="收件地址") - IfServerChan: Optional[bool] = Field(default=None, description="是否启用Server酱") - ServerChanKey: Optional[str] = Field(default=None, description="Server酱密钥") - - -class SrcUserConfig(BaseModel): - Info: Optional[SrcUserConfig_Info] = Field(default=None, description="基础信息") - Stage: Optional[SrcUserConfig_Stage] = Field(default=None, description="关卡配置") - Data: Optional[SrcUserConfig_Data] = Field(default=None, description="用户数据") - Notify: Optional[SrcUserConfig_Notify] = Field(default=None, description="单独通知") - - -class SrcConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="SRC脚本名称") - Path: Optional[str] = Field(default=None, description="SRC路径") - - -class SrcConfig_Emulator(BaseModel): - Id: Optional[str] = Field(default=None, description="模拟器ID") - Index: Optional[str] = Field(default=None, description="模拟器索引") - - -class SrcConfig_Run(BaseModel): - TaskTransitionMethod: Optional[Literal["ExitGame", "ExitEmulator"]] = Field( - default=None, description="任务切换方式" - ) - ProxyTimesLimit: Optional[int] = Field(default=None, description="代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="运行次数限制") - RunTimeLimit: Optional[int] = Field( - default=None, description="运行时间限制(分钟)" - ) - - -class SrcConfig(BaseModel): - Info: Optional[SrcConfig_Info] = Field(default=None, description="脚本基础信息") - Emulator: Optional[SrcConfig_Emulator] = Field( - default=None, description="模拟器配置" - ) - Run: Optional[SrcConfig_Run] = Field(default=None, description="脚本运行配置") - - -class PlanIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["MaaPlanConfig"] = Field(..., description="配置类型") - - -class MaaPlanConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="计划表名称") - Mode: Optional[Literal["ALL", "Weekly"]] = Field( - default=None, description="计划表模式" - ) - - -class MaaPlanConfig_Item(BaseModel): - MedicineNumb: Optional[int] = Field(default=None, description="吃理智药") - SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field( - None, description="连战次数" - ) - Stage: Optional[str] = Field(default=None, description="关卡选择") - Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1") - Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2") - Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3") - Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡") - - -class MaaPlanConfig(BaseModel): - Info: Optional[MaaPlanConfig_Info] = Field(default=None, description="基础信息") - ALL: Optional[MaaPlanConfig_Item] = Field(default=None, description="全局") - Monday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周一") - Tuesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周二") - Wednesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周三") - Thursday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周四") - Friday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周五") - Saturday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周六") - Sunday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周日") - - -class HistoryIndexItem(BaseModel): - date: str = Field(..., description="日期") - status: Literal["DONE", "ERROR"] = Field(..., description="状态") - jsonFile: str = Field(..., description="对应JSON文件") - - -class HistoryData(BaseModel): - index: Optional[List[HistoryIndexItem]] = Field( - default=None, description="历史记录索引列表" - ) - recruit_statistics: Optional[Dict[str, int]] = Field( - default=None, description="公招统计数据, key为星级, value为对应的公招数量" - ) - drop_statistics: Optional[Dict[str, Dict[str, int]]] = Field( - default=None, - description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }", - ) - error_info: Optional[Dict[str, str]] = Field( - default=None, description="报错信息, key为时间戳, value为错误描述" - ) - log_content: Optional[str] = Field( - default=None, description="日志内容, 仅在提取单条历史记录数据时返回" - ) - - -class ScriptCreateIn(BaseModel): - type: Literal["MAA", "SRC", "General", "MaaEnd"] = Field( - ..., description="脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本" - ) - scriptId: str | None = Field( - default=None, description="直接从该脚本ID复制创建, 仅在复制创建时使用" - ) - - -class ScriptCreateOut(OutBase): - scriptId: str = Field(..., description="新创建的脚本ID") - data: Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig] = Field( - ..., description="脚本配置数据" - ) - - -class ScriptGetIn(BaseModel): - scriptId: Optional[str] = Field( - default=None, description="脚本ID, 未携带时表示获取所有脚本数据" - ) - - -class ScriptGetOut(OutBase): - index: List[ScriptIndexItem] = Field(..., description="脚本索引列表") - data: Dict[str, Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig]] = Field( - ..., description="脚本数据字典, key来自于index列表的uid" - ) - - -class ScriptUpdateIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - data: Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig] = Field( - ..., description="脚本更新数据" - ) - - -class ScriptDeleteIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - - -class ScriptReorderIn(BaseModel): - indexList: List[str] = Field(..., description="脚本ID列表, 按新顺序排列") - - -class ScriptFileIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - jsonFile: str = Field(..., description="配置文件路径") - - -class ScriptUrlIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - url: str = Field(..., description="配置文件URL") - - -class ScriptUploadIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - config_name: str = Field(..., description="配置名称") - author: str = Field(..., description="作者") - description: str = Field(..., description="描述") - - -class UserInBase(BaseModel): - scriptId: str = Field(..., description="所属脚本ID") - - -class UserGetIn(UserInBase): - userId: Optional[str] = Field( - default=None, description="用户ID, 未携带时表示获取所有用户数据" - ) - - -class UserGetOut(OutBase): - index: List[UserIndexItem] = Field(..., description="用户索引列表") - data: Dict[ - str, Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] - ] = Field(..., description="用户数据字典, key来自于index列表的uid") - - -class UserCreateOut(OutBase): - userId: str = Field(..., description="新创建的用户ID") - data: Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] = ( - Field(..., description="用户配置数据") - ) - - -class UserUpdateIn(UserInBase): - userId: str = Field(..., description="用户ID") - data: Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] = ( - Field(..., description="用户更新数据") - ) - - -class UserDeleteIn(UserInBase): - userId: str = Field(..., description="用户ID") - - -class UserReorderIn(UserInBase): - indexList: List[str] = Field(..., description="用户ID列表, 按新顺序排列") - - -class UserSetIn(UserInBase): - userId: str = Field(..., description="用户ID") - jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件") - - -class EmulatorGetIn(BaseModel): - emulatorId: Optional[str] = Field( - default=None, description="模拟器ID, 未携带时表示获取所有模拟器数据" - ) - - -class EmulatorGetOut(OutBase): - index: List[EmulatorConfigIndexItem] = Field(..., description="模拟器索引列表") - data: Dict[str, EmulatorConfig] = Field( - ..., description="模拟器数据字典, key来自于index列表的uid" - ) - - -class EmulatorCreateOut(OutBase): - emulatorId: str = Field(..., description="新创建的模拟器 ID") - data: EmulatorConfig = Field(..., description="模拟器配置数据") - - -class EmulatorUpdateIn(BaseModel): - emulatorId: str = Field(..., description="模拟器 ID") - data: EmulatorConfig = Field(..., description="模拟器更新数据") - - -class EmulatorDeleteIn(BaseModel): - emulatorId: str = Field(..., description="模拟器 ID") - - -class EmulatorReorderIn(BaseModel): - indexList: List[str] = Field(..., description="模拟器 ID列表, 按新顺序排列") - - -class EmulatorOperateIn(BaseModel): - emulatorId: str = Field(..., description="模拟器 ID") - operate: Literal["open", "close", "show"] = Field(..., description="操作类型") - index: str = Field(..., description="模拟器索引") - - -class DeviceStatus(BaseModel): - """设备状态枚举""" - - ONLINE: int = Field(default=0, description="设备在线") - OFFLINE: int = Field(default=1, description="设备离线") - STARTING: int = Field(default=2, description="设备开启中") - CLOSEING: int = Field(default=3, description="设备关闭中") - ERROR: int = Field(default=4, description="错误") - NOT_FOUND: int = Field(default=5, description="未找到设备") - UNKNOWN: int = Field(default=10, description="未知状态") - - -class DeviceInfo(BaseModel): - """设备信息""" - - title: str = Field(..., description="设备标题/名称") - status: int = Field(..., description="设备状态, 参考DeviceStatus枚举值") - adb_address: str = Field(..., description="ADB连接地址") - - -class EmulatorStatusOut(OutBase): - data: Dict[str, Dict[str, DeviceInfo]] = Field( - ..., - description="模拟器状态信息, 外层key为模拟器ID, 内层key为设备索引, value为设备信息", - ) - - -class EmulatorSearchResult(BaseModel): - type: str = Field(..., description="模拟器类型") - path: str = Field(..., description="模拟器路径") - name: str = Field(..., description="模拟器名称") - - -class EmulatorSearchOut(OutBase): - emulators: list[EmulatorSearchResult] = Field( - default_factory=list, description="搜索到的模拟器列表" - ) - - -class WebhookInBase(BaseModel): - scriptId: Optional[str] = Field( - default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带" - ) - userId: Optional[str] = Field( - default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带" - ) - - -class WebhookGetIn(WebhookInBase): - webhookId: Optional[str] = Field( - default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据" - ) - - -class WebhookGetOut(OutBase): - index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表") - data: Dict[str, Webhook] = Field( - ..., description="Webhook数据字典, key来自于index列表的uid" - ) - - -class WebhookCreateOut(OutBase): - webhookId: str = Field(..., description="新创建的Webhook ID") - data: Webhook = Field(..., description="Webhook配置数据") - - -class WebhookUpdateIn(WebhookInBase): - webhookId: str = Field(..., description="Webhook ID") - data: Webhook = Field(..., description="Webhook更新数据") - - -class WebhookDeleteIn(WebhookInBase): - webhookId: str = Field(..., description="Webhook ID") - - -class WebhookReorderIn(WebhookInBase): - indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列") - - -class WebhookTestIn(WebhookInBase): - data: Webhook = Field(..., description="Webhook配置数据") - - -class PlanCreateIn(BaseModel): - type: Literal["MaaPlan"] - - -class PlanCreateOut(OutBase): - planId: str = Field(..., description="新创建的计划ID") - data: MaaPlanConfig = Field(..., description="计划配置数据") - - -class PlanGetIn(BaseModel): - planId: Optional[str] = Field( - default=None, description="计划ID, 未携带时表示获取所有计划数据" - ) - - -class PlanGetOut(OutBase): - index: List[PlanIndexItem] = Field(..., description="计划索引列表") - data: Dict[str, MaaPlanConfig] = Field(..., description="计划列表或单个计划数据") - - -class PlanUpdateIn(BaseModel): - planId: str = Field(..., description="计划ID") - data: MaaPlanConfig = Field(..., description="计划更新数据") - - -class PlanDeleteIn(BaseModel): - planId: str = Field(..., description="计划ID") - - -class PlanReorderIn(BaseModel): - indexList: List[str] = Field(..., description="计划ID列表, 按新顺序排列") - - -class QueueCreateOut(OutBase): - queueId: str = Field(..., description="新创建的队列ID") - data: QueueConfig = Field(..., description="队列配置数据") - - -class QueueGetIn(BaseModel): - queueId: Optional[str] = Field( - default=None, description="队列ID, 未携带时表示获取所有队列数据" - ) - - -class QueueGetOut(OutBase): - index: List[QueueIndexItem] = Field(..., description="队列索引列表") - data: Dict[str, QueueConfig] = Field( - ..., description="队列数据字典, key来自于index列表的uid" - ) - - -class QueueUpdateIn(BaseModel): - queueId: str = Field(..., description="队列ID") - data: QueueConfig = Field(..., description="队列更新数据") - - -class QueueDeleteIn(BaseModel): - queueId: str = Field(..., description="队列ID") - - -class QueueReorderIn(BaseModel): - indexList: List[str] = Field(..., description="按新顺序排列的调度队列UID列表") - - -class QueueSetInBase(BaseModel): - queueId: str = Field(..., description="所属队列ID") - - -class TimeSetGetIn(QueueSetInBase): - timeSetId: Optional[str] = Field( - default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据" - ) - - -class TimeSetGetOut(OutBase): - index: List[TimeSetIndexItem] = Field(..., description="时间设置索引列表") - data: Dict[str, TimeSet] = Field( - ..., description="时间设置数据字典, key来自于index列表的uid" - ) - - -class TimeSetCreateOut(OutBase): - timeSetId: str = Field(..., description="新创建的时间设置ID") - data: TimeSet = Field(..., description="时间设置配置数据") - - -class TimeSetUpdateIn(QueueSetInBase): - timeSetId: str = Field(..., description="时间设置ID") - data: TimeSet = Field(..., description="时间设置更新数据") - - -class TimeSetDeleteIn(QueueSetInBase): - timeSetId: str = Field(..., description="时间设置ID") - - -class TimeSetReorderIn(QueueSetInBase): - indexList: List[str] = Field(..., description="时间设置ID列表, 按新顺序排列") - - -class QueueItemGetIn(QueueSetInBase): - queueItemId: Optional[str] = Field( - default=None, description="队列项ID, 未携带时表示获取所有队列项数据" - ) - - -class QueueItemGetOut(OutBase): - index: List[QueueItemIndexItem] = Field(..., description="队列项索引列表") - data: Dict[str, QueueItem] = Field( - ..., description="队列项数据字典, key来自于index列表的uid" - ) - - -class QueueItemCreateOut(OutBase): - queueItemId: str = Field(..., description="新创建的队列项ID") - data: QueueItem = Field(..., description="队列项配置数据") - - -class QueueItemUpdateIn(QueueSetInBase): - queueItemId: str = Field(..., description="队列项ID") - data: QueueItem = Field(..., description="队列项更新数据") - - -class QueueItemDeleteIn(QueueSetInBase): - queueItemId: str = Field(..., description="队列项ID") - - -class QueueItemReorderIn(QueueSetInBase): - indexList: List[str] = Field(..., description="队列项ID列表, 按新顺序排列") - - -class DispatchIn(BaseModel): - taskId: str = Field( - ..., - description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID", - ) - - -class TaskCreateIn(DispatchIn): - mode: Literal["AutoProxy", "ManualReview", "ScriptConfig"] = Field( - ..., description="任务模式" - ) - - -class TaskCreateOut(OutBase): - taskId: str = Field(..., description="新创建的任务ID") - - -class WebSocketMessage(BaseModel): - id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程") - type: Literal["Update", "Message", "Info", "Signal"] = Field( - ..., - description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号", - ) - data: Dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定") - - -class PowerIn(BaseModel): - signal: Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] = Field(..., description="电源操作信号") - - -class PowerOut(OutBase): - signal: Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] = Field(..., description="电源操作信号") - - -class HistorySearchIn(BaseModel): - mode: Literal["DAILY", "WEEKLY", "MONTHLY"] = Field(..., description="合并模式") - start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD") - end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD") - - -class HistorySearchOut(OutBase): - data: Dict[str, Dict[str, HistoryData]] = Field( - ..., - description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }", - ) - - -class HistoryDataGetIn(BaseModel): - jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件") - - -class HistoryDataGetOut(OutBase): - data: HistoryData = Field(..., description="历史记录数据") - - -class ToolsGetOut(OutBase): - data: ToolsConfig = Field(..., description="工具配置数据") - - -class ToolsUpdateIn(BaseModel): - data: ToolsConfig = Field(..., description="工具配置需要更新的数据") - - -class SettingGetOut(OutBase): - data: GlobalConfig = Field(..., description="全局设置数据") - - -class SettingUpdateIn(BaseModel): - data: GlobalConfig = Field(..., description="全局设置需要更新的数据") - - -class UpdateCheckIn(BaseModel): - current_version: str = Field(..., description="当前前端版本号") - if_force: bool = Field(default=False, description="是否强制拉取更新信息") - - -class UpdateCheckOut(OutBase): - if_need_update: bool = Field(..., description="是否需要更新前端") - latest_version: str = Field(..., description="最新前端版本号") - update_info: Dict[str, List[str]] = Field(..., description="版本更新信息字典") - - -# ============== WebSocket 调试相关模型 ============== - - -class WSClientCreateIn(BaseModel): - """创建 WebSocket 客户端请求""" - - name: str = Field(..., description="客户端名称,用于标识") - url: str = Field( - ..., description="WebSocket 服务器地址,如 ws://localhost:5140/path" - ) - ping_interval: float = Field(default=15.0, description="心跳发送间隔(秒)") - ping_timeout: float = Field(default=30.0, description="心跳超时时间(秒)") - reconnect_interval: float = Field(default=5.0, description="重连间隔(秒)") - max_reconnect_attempts: int = Field( - default=-1, description="最大重连次数,-1为无限" - ) - - -class WSClientCreateOut(OutBase): - """创建客户端响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="返回数据") - - -class WSClientConnectIn(BaseModel): - """连接请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientDisconnectIn(BaseModel): - """断开连接请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientRemoveIn(BaseModel): - """删除客户端请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientSendIn(BaseModel): - """发送消息请求""" - - name: str = Field(..., description="客户端名称") - message: Dict[str, Any] = Field(..., description="要发送的 JSON 消息") - - -class WSClientSendJsonIn(BaseModel): - """发送自定义 JSON 消息请求""" - - name: str = Field(..., description="客户端名称") - msg_id: str = Field(default="Client", description="消息 ID") - msg_type: str = Field(..., description="消息类型") - data: Dict[str, Any] = Field(default_factory=dict, description="消息数据") - - -class WSClientAuthIn(BaseModel): - """发送认证请求""" - - name: str = Field(..., description="客户端名称") - token: str = Field(..., description="认证 Token") - auth_type: str = Field(default="auth", description="认证消息类型") - extra_data: Optional[Dict[str, Any]] = Field( - default=None, description="额外认证数据" - ) - - -class WSClientStatusIn(BaseModel): - """获取客户端状态请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientStatusOut(OutBase): - """客户端状态响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="状态数据") - - -class WSClientListOut(OutBase): - """客户端列表响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="客户端列表") - - -class WSMessageHistoryOut(OutBase): - """消息历史响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="消息历史") - - -class WSClearHistoryIn(BaseModel): - """清空消息历史请求""" - - name: Optional[str] = Field(default=None, description="客户端名称,为空则清空所有") - - -class WSCommandsOut(OutBase): - """可用命令列表响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="命令列表") From 9b094ab2d850f71cb839036ac0f94fa7240dd721 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Tue, 31 Mar 2026 02:21:14 +0800 Subject: [PATCH 09/29] =?UTF-8?q?refactor(config):=20=E5=AE=8C=E5=96=84con?= =?UTF-8?q?fig=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/common.py | 21 +++ app/core/config/__init__.py | 14 +- app/core/config/base.py | 236 ++++++++++++++++++++++++- app/core/config/fields.py | 29 +++ app/core/config/manager.py | 115 ++---------- app/core/config/pydantic.py | 344 ++++++++++++++++++++++++++++++++++-- app/models/common.py | 36 ++-- app/models/general.py | 68 +++---- app/models/global_config.py | 56 ++---- app/models/maa.py | 136 +++++++------- app/models/maaend.py | 77 ++++---- app/models/shared.py | 44 +++++ app/models/src.py | 75 ++++---- 13 files changed, 853 insertions(+), 398 deletions(-) create mode 100644 app/api/common.py create mode 100644 app/core/config/fields.py create mode 100644 app/models/shared.py diff --git a/app/api/common.py b/app/api/common.py new file mode 100644 index 00000000..538b092b --- /dev/null +++ b/app/api/common.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class OutBase(BaseModel): + code: int = Field(default=200, description="状态码") + status: str = Field(default="success", description="操作状态") + message: str = Field(default="操作成功", description="操作消息") + + +class ComboBoxItem(BaseModel): + label: str = Field(..., description="展示值") + value: str | None = Field(..., description="实际值") + + +class ComboBoxOut(OutBase): + data: list[ComboBoxItem] = Field(..., description="下拉框选项") + + +__all__ = ["OutBase", "ComboBoxItem", "ComboBoxOut"] diff --git a/app/core/config/__init__.py b/app/core/config/__init__.py index 369ec643..5a0f2554 100644 --- a/app/core/config/__init__.py +++ b/app/core/config/__init__.py @@ -1,4 +1,11 @@ -from .base import MultipleConfig, dump_toml +from .base import ( + MultipleConfig, + MultipleConfigAddEvent, + MultipleConfigDeleteEvent, + MultipleConfigReorderEvent, + dump_toml, +) +from .fields import RefField, VirtualField from .manager import AppConfig, Config from .pydantic import PydanticConfigBase from .types import ( @@ -16,7 +23,12 @@ __all__ = [ "MultipleConfig", + "MultipleConfigAddEvent", + "MultipleConfigDeleteEvent", + "MultipleConfigReorderEvent", "dump_toml", + "RefField", + "VirtualField", "PydanticConfigBase", "JsonDictString", "JsonListString", diff --git a/app/core/config/base.py b/app/core/config/base.py index eed2b46a..85a51497 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -24,12 +24,15 @@ from __future__ import annotations import asyncio +import inspect import json import tomllib import uuid -from contextlib import suppress +import weakref +from contextlib import asynccontextmanager, suppress +from dataclasses import dataclass from pathlib import Path -from collections.abc import Callable, Coroutine +from collections.abc import AsyncIterator, Callable, Coroutine from typing import Any, Generic, Protocol, TypeVar @@ -201,6 +204,89 @@ async def unlock(self) -> None: ... T = TypeVar("T", bound=_ConfigLike) +CollectionEventSlot = Callable[[Any], Any] | Callable[[Any], Coroutine[Any, Any, Any]] + + +@dataclass(slots=True) +class MultipleConfigAddEvent(Generic[T]): + """新增配置项事件。""" + + collection: "MultipleConfig[T]" + uid: uuid.UUID + config: T + + +@dataclass(slots=True) +class MultipleConfigDeleteEvent(Generic[T]): + """删除配置项事件。""" + + collection: "MultipleConfig[T]" + uid: uuid.UUID + config: T + + +@dataclass(slots=True) +class MultipleConfigReorderEvent(Generic[T]): + """重排配置项事件。""" + + collection: "MultipleConfig[T]" + order: list[uuid.UUID] + + +def _callback_identity(callback: CollectionEventSlot) -> object: + """生成回调唯一标识。""" + + if inspect.ismethod(callback) and getattr(callback, "__self__", None) is not None: + return (id(callback.__self__), callback.__func__) + return callback + + +@dataclass(slots=True) +class _WeakCallbackSlot: + """容器事件弱引用回调槽。""" + + identity: object + callback: CollectionEventSlot | None = None + weak_method: weakref.WeakMethod[Any] | None = None + + @classmethod + def build(cls, callback: CollectionEventSlot) -> "_WeakCallbackSlot": + if inspect.ismethod(callback) and getattr(callback, "__self__", None) is not None: + return cls( + identity=_callback_identity(callback), + weak_method=weakref.WeakMethod(callback), + ) + return cls(identity=_callback_identity(callback), callback=callback) + + def resolve(self) -> CollectionEventSlot | None: + if self.weak_method is not None: + resolved = self.weak_method() + if resolved is None: + return None + return resolved + return self.callback + + +async def _emit_collection_slots( + slots: list[_WeakCallbackSlot], event: Any +) -> None: + """依次触发容器事件回调,并清理失效弱引用。""" + + alive_slots: list[_WeakCallbackSlot] = [] + + for slot in slots: + callback = slot.resolve() + if callback is None: + continue + + alive_slots.append(slot) + result = callback(event) + if inspect.isawaitable(result): + await result + + slots[:] = alive_slots + + class MultipleConfig(Generic[T]): """ 多配置项管理类。 @@ -220,6 +306,13 @@ def __init__(self, sub_config_type: list[type[T]]): self.data: dict[uuid.UUID, T] = {} self.is_locked = False self._save_methods: list[Callable[[], Coroutine[Any, Any, None]]] = [] + self._transaction_depth = 0 + self._pending_save = False + self._pending_sync = False + self._on_add_slots: list[_WeakCallbackSlot] = [] + self._on_before_del_slots: list[_WeakCallbackSlot] = [] + self._on_del_slots: list[_WeakCallbackSlot] = [] + self._on_reorder_slots: list[_WeakCallbackSlot] = [] def __getitem__(self, key: uuid.UUID) -> T: if key not in self.data: @@ -274,6 +367,34 @@ async def add_save_method( for sub_config in self.data.values(): await sub_config.add_save_method(save_method) + @asynccontextmanager + async def transaction(self) -> AsyncIterator["MultipleConfig[T]"]: + """开启一个延迟保存事务。""" + + self._transaction_depth += 1 + try: + yield self + finally: + self._transaction_depth -= 1 + if self._transaction_depth == 0: + await self._flush_pending_changes() + + async def _flush_pending_changes(self) -> None: + """提交事务期间累积的保存请求。""" + + if self._pending_save and self.file: + self._pending_save = False + self.file.parent.mkdir(parents=True, exist_ok=True) + self.file.write_text( + dump_toml(await self.toDict(if_decrypt=False)), + encoding="utf-8", + ) + + if self._pending_sync: + self._pending_sync = False + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) + async def load(self, data: dict[str, Any]) -> None: """从字典加载多实例配置数据。""" @@ -308,6 +429,8 @@ async def load(self, data: dict[str, Any]) -> None: continue config = self.sub_config_type[type_name]() + if hasattr(config, "_bind_owner_collection"): + config._bind_owner_collection(self, uid) self.order.append(uid) self.data[uid] = config await config.load(instance_data) @@ -363,6 +486,10 @@ async def save(self) -> None: if not self.file: raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + if self._transaction_depth > 0: + self._pending_save = True + return + self.file.parent.mkdir(parents=True, exist_ok=True) self.file.write_text( dump_toml(await self.toDict(if_decrypt=False)), @@ -379,6 +506,8 @@ async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: uid = uuid.uuid4() config = config_type() + if hasattr(config, "_bind_owner_collection"): + config._bind_owner_collection(self, uid) self.order.append(uid) self.data[uid] = config @@ -389,9 +518,15 @@ async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: await config.add_save_method(self.save) await self.save() - if self._save_methods: + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) + await _emit_collection_slots( + self._on_add_slots, MultipleConfigAddEvent(self, uid, config) + ) + return uid, config async def remove(self, uid: uuid.UUID) -> None: @@ -404,15 +539,26 @@ async def remove(self, uid: uuid.UUID) -> None: if self.data[uid].is_locked: raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除") + config = self.data[uid] + await _emit_collection_slots( + self._on_before_del_slots, MultipleConfigDeleteEvent(self, uid, config) + ) + self.data.pop(uid) self.order.remove(uid) if self.file: await self.save() - if self._save_methods: + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) + await _emit_collection_slots( + self._on_del_slots, MultipleConfigDeleteEvent(self, uid, config) + ) + async def setOrder(self, order: list[uuid.UUID]) -> None: # noqa: N802 """设置子配置实例顺序。""" @@ -426,9 +572,15 @@ async def setOrder(self, order: list[uuid.UUID]) -> None: # noqa: N802 if self.file: await self.save() - if self._save_methods: + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) + await _emit_collection_slots( + self._on_reorder_slots, MultipleConfigReorderEvent(self, list(order)) + ) + async def lock(self) -> None: """锁定当前管理器及全部子配置。""" @@ -446,16 +598,84 @@ async def unlock(self) -> None: def keys(self): """返回全部 UID。""" - return iter(self.order) + return iter(tuple(self.order)) def values(self): """按顺序返回全部子配置实例。""" if not self.data: return iter(()) - return (self.data[uid] for uid in self.order) + order_snapshot = tuple(self.order) + return iter(tuple(self.data[uid] for uid in order_snapshot if uid in self.data)) def items(self): """按顺序返回 `(uid, config)` 对。""" - return zip(self.keys(), self.values()) + order_snapshot = tuple(self.order) + return iter( + tuple((uid, self.data[uid]) for uid in order_snapshot if uid in self.data) + ) + + def bind_add(self, slot: CollectionEventSlot) -> None: + """绑定新增事件。""" + + identity = _callback_identity(slot) + if any(item.identity == identity for item in self._on_add_slots): + return + self._on_add_slots.append(_WeakCallbackSlot.build(slot)) + + def bind_before_del(self, slot: CollectionEventSlot) -> None: + """绑定删除前事件。""" + + identity = _callback_identity(slot) + if any(item.identity == identity for item in self._on_before_del_slots): + return + self._on_before_del_slots.append(_WeakCallbackSlot.build(slot)) + + def bind_del(self, slot: CollectionEventSlot) -> None: + """绑定删除后事件。""" + + identity = _callback_identity(slot) + if any(item.identity == identity for item in self._on_del_slots): + return + self._on_del_slots.append(_WeakCallbackSlot.build(slot)) + + def bind_reorder(self, slot: CollectionEventSlot) -> None: + """绑定重排事件。""" + + identity = _callback_identity(slot) + if any(item.identity == identity for item in self._on_reorder_slots): + return + self._on_reorder_slots.append(_WeakCallbackSlot.build(slot)) + + def unbind_add(self, slot: CollectionEventSlot) -> None: + """解绑新增事件。""" + + identity = _callback_identity(slot) + self._on_add_slots = [ + item for item in self._on_add_slots if item.identity != identity + ] + + def unbind_before_del(self, slot: CollectionEventSlot) -> None: + """解绑删除前事件。""" + + identity = _callback_identity(slot) + self._on_before_del_slots = [ + item for item in self._on_before_del_slots if item.identity != identity + ] + + def unbind_del(self, slot: CollectionEventSlot) -> None: + """解绑删除后事件。""" + + identity = _callback_identity(slot) + self._on_del_slots = [ + item for item in self._on_del_slots if item.identity != identity + ] + + def unbind_reorder(self, slot: CollectionEventSlot) -> None: + """解绑重排事件。""" + + identity = _callback_identity(slot) + self._on_reorder_slots = [ + item for item in self._on_reorder_slots if item.identity != identity + ] diff --git a/app/core/config/fields.py b/app/core/config/fields.py new file mode 100644 index 00000000..136a0eae --- /dev/null +++ b/app/core/config/fields.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Callable, Literal + + +RefDeleteAction = Literal["restrict", "set_default", "cascade", "custom"] +VirtualDependency = tuple[str, str] + + +@dataclass(frozen=True, slots=True) +class RefField: + """声明式引用字段元数据。""" + + target: str + default: Any + allow_values: tuple[Any, ...] = () + on_delete: RefDeleteAction = "set_default" + on_delete_callback: str | Callable[[Any, Any], Any] | None = None + + +@dataclass(frozen=True, slots=True) +class VirtualField: + """声明式虚拟字段元数据。""" + + getter: str | Callable[[Any], Any] + setter: str | Callable[[Any, Any], Any] | None = None + depends_on: tuple[VirtualDependency, ...] = field(default_factory=tuple) + diff --git a/app/core/config/manager.py b/app/core/config/manager.py index b8a61306..3354354c 100644 --- a/app/core/config/manager.py +++ b/app/core/config/manager.py @@ -636,9 +636,7 @@ async def update_script( if self.ScriptConfig[uid].is_locked: raise RuntimeError(f"脚本 {script_id} 正在运行, 无法更新配置项") - for group, items in data.items(): - for name, value in items.items(): - await self.ScriptConfig[uid].set(group, name, value) + await self.ScriptConfig[uid].set_many(data) async def del_script(self, script_id: str) -> None: """删除脚本配置""" @@ -650,12 +648,6 @@ async def del_script(self, script_id: str) -> None: if self.ScriptConfig[uid].is_locked: raise RuntimeError(f"脚本 {script_id} 正在运行, 无法删除") - # 删除脚本相关的队列项 - for queue in self.QueueConfig.values(): - 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(): shutil.rmtree(Path.cwd() / f"data/{uid}") @@ -884,13 +876,7 @@ async def update_user( script_uid = uuid.UUID(script_id) user_uid = uuid.UUID(user_id) - for group, items in data.items(): - for name, value in items.items(): - await ( - self.ScriptConfig[script_uid] - .UserData[user_uid] - .set(group, name, value) - ) + await self.ScriptConfig[script_uid].UserData[user_uid].set_many(data) async def del_user(self, script_id: str, user_id: str) -> None: """删除用户配置""" @@ -1004,9 +990,7 @@ async def update_plan(self, plan_id: str, data: Dict[str, Dict[str, Any]]) -> No plan_uid = uuid.UUID(plan_id) - for group, items in data.items(): - for name, value in items.items(): - await self.PlanConfig[plan_uid].set(group, name, value) + await self.PlanConfig[plan_uid].set_many(data) async def del_plan(self, plan_id: str) -> None: """删除计划表配置""" @@ -1015,21 +999,6 @@ async def del_plan(self, plan_id: str) -> None: 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(plan_uid): - if user.is_locked: - raise RuntimeError( - f"用户 {user.get('Info','Name')} 正在使用此计划表且被锁定, 无法完成删除" - ) - user_list.append(user) - - for user in user_list: - await user.set("Info", "StageMode", "Fixed") - await self.PlanConfig.remove(plan_uid) async def reorder_plan(self, index_list: list[str]) -> None: @@ -1069,9 +1038,7 @@ async def update_emulator( logger.info(f"更新模拟器配置: {emulator_id}") - for group, items in data.items(): - for name, value in items.items(): - await self.EmulatorConfig[emulator_uid].set(group, name, value) + await self.EmulatorConfig[emulator_uid].set_many(data) async def del_emulator(self, emulator_id: str) -> None: """删除模拟器配置""" @@ -1080,32 +1047,6 @@ async def del_emulator(self, emulator_id: str) -> None: logger.info(f"删除全局模拟器配置: {emulator_id}") - script_list = [] - - for script in self.ScriptConfig.values(): - if isinstance(script, MaaConfig): - if script.get("Emulator", "Id") == str(emulator_id): - if script.is_locked: - raise RuntimeError( - f"脚本 {script.get('Info','Name')} 正在使用此模拟器且被锁定, 无法完成删除" - ) - script_list.append(script) - elif isinstance(script, GeneralConfig): - if script.get("Game", "Type") == "Emulator" and script.get( - "Game", "EmulatorId" - ) == str(emulator_id): - if script.is_locked: - raise RuntimeError( - f"脚本 {script.get('Info','Name')} 正在使用此模拟器且被锁定, 无法完成删除" - ) - script_list.append(script) - - for script in script_list: - if isinstance(script, MaaConfig): - await script.set("Emulator", "Id", "-") - elif isinstance(script, GeneralConfig): - await script.set("Game", "EmulatorId", "-") - await self.EmulatorConfig.remove(emulator_uid) async def reorder_emulator(self, index_list: list[str]) -> None: @@ -1146,9 +1087,7 @@ async def update_queue( queue_uid = uuid.UUID(queue_id) - for group, items in data.items(): - for name, value in items.items(): - await self.QueueConfig[queue_uid].set(group, name, value) + await self.QueueConfig[queue_uid].set_many(data) async def del_queue(self, queue_id: str) -> None: """删除调度队列配置""" @@ -1201,13 +1140,7 @@ async def update_time_set( 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(): - await ( - self.QueueConfig[queue_uid] - .TimeSet[time_set_uid] - .set(group, name, value) - ) + await self.QueueConfig[queue_uid].TimeSet[time_set_uid].set_many(data) async def del_time_set(self, queue_id: str, time_set_id: str) -> None: """删除时间设置配置""" @@ -1270,13 +1203,7 @@ async def update_queue_item( 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(): - await ( - self.QueueConfig[queue_uid] - .QueueItem[queue_item_uid] - .set(group, name, value) - ) + await self.QueueConfig[queue_uid].QueueItem[queue_item_uid].set_many(data) async def del_queue_item(self, queue_id: str, queue_item_id: str) -> None: """删除队列项配置""" @@ -1311,9 +1238,7 @@ async def update_tools(self, data: Dict[str, Dict[str, Any]]) -> None: logger.info("更新工具设置") - for group, items in data.items(): - for name, value in items.items(): - await self.ToolsConfig.set(group, name, value) + await self.ToolsConfig.set_many(data) logger.success("工具设置更新成功") @@ -1329,9 +1254,7 @@ async def update_setting(self, data: Dict[str, Dict[str, Any]]) -> None: logger.info("更新全局设置") - for group, items in data.items(): - for name, value in items.items(): - await self.set(group, name, value) + await self.set_many(data) logger.success("全局设置更新成功") @@ -1411,11 +1334,7 @@ async def update_webhook( 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(): - await self.Notify_CustomWebhooks[webhook_uid].set( - group, name, value - ) + await self.Notify_CustomWebhooks[webhook_uid].set_many(data) else: logger.info(f"更新 webhook 配置: {script_id} - {user_id} - {webhook_id}") @@ -1423,14 +1342,12 @@ async def update_webhook( script_uid = uuid.UUID(script_id) user_uid = uuid.UUID(user_id) - for group, items in data.items(): - for name, value in items.items(): - await ( - self.ScriptConfig[script_uid] - .UserData[user_uid] - .Notify_CustomWebhooks[webhook_uid] - .set(group, name, value) - ) + await ( + self.ScriptConfig[script_uid] + .UserData[user_uid] + .Notify_CustomWebhooks[webhook_uid] + .set_many(data) + ) async def del_webhook( self, script_id: Optional[str], user_id: Optional[str], webhook_id: str diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index ead3623e..15651f7d 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -2,18 +2,23 @@ import asyncio import inspect +import uuid +from contextlib import asynccontextmanager from pathlib import Path from collections.abc import Callable, Coroutine -from typing import Any, ClassVar, cast +from collections.abc import AsyncIterator +from typing import Any, ClassVar, TypeVar, cast from pydantic import BaseModel, ConfigDict, PrivateAttr from .base import ( MultipleConfig, + MultipleConfigDeleteEvent, backup_legacy_config_if_needed, dump_toml, load_config_with_legacy_migration, ) +from .fields import RefField, VirtualField from .types import _EncryptedFieldMarker, decrypt_encrypted_string @@ -38,25 +43,53 @@ def _normalize_mapping(value: Any) -> dict[str, Any]: return {str(key): item for key, item in mapping.items()} -def _is_encrypted_field(group_model: BaseModel, field_name: str) -> bool: - """判断字段是否为对外需要自动解密的字符串字段。""" +FieldMarkerT = TypeVar("FieldMarkerT") + + +def _get_field_marker( + group_model: BaseModel, field_name: str, marker_type: type[FieldMarkerT] +) -> FieldMarkerT | None: + """获取字段上声明的特定元数据。""" field = type(group_model).model_fields.get(field_name) if field is None: - return False - return any(isinstance(item, _EncryptedFieldMarker) for item in field.metadata) + return None + + for marker in field.metadata: + if isinstance(marker, marker_type): + return marker + return None + + +def _is_encrypted_field(group_model: BaseModel, field_name: str) -> bool: + """判断字段是否为对外需要自动解密的字符串字段。""" -def _export_group_model(group_model: BaseModel, if_decrypt: bool) -> dict[str, Any]: + return _get_field_marker(group_model, field_name, _EncryptedFieldMarker) is not None + + +def _export_group_model( + owner: "PydanticConfigBase", + group_name: str, + group_model: BaseModel, + if_decrypt: bool, +) -> dict[str, Any]: """将分组模型导出为字典,并按需解密加密字段。""" data: dict[str, Any] = {} for field_name in type(group_model).model_fields: + virtual_field = _get_field_marker(group_model, field_name, VirtualField) + if virtual_field is not None: + data[field_name] = owner._get_virtual_value( + group_name, field_name, virtual_field + ) + continue + value = getattr(group_model, field_name) if isinstance(value, BaseModel): - data[field_name] = _export_group_model(value, if_decrypt) + data[field_name] = _export_group_model(owner, field_name, value, if_decrypt) continue if if_decrypt and _is_encrypted_field(group_model, field_name): @@ -80,6 +113,13 @@ class PydanticConfigBase(BaseModel): _bindings: dict[tuple[str, str], list[Slot]] = PrivateAttr( default_factory=_default_bindings ) + _transaction_depth: int = PrivateAttr(default=0) + _pending_save: bool = PrivateAttr(default=False) + _pending_sync: bool = PrivateAttr(default=False) + _pending_bindings: dict[tuple[str, str], Any] = PrivateAttr(default_factory=dict) + _registered_ref_targets: set[str] = PrivateAttr(default_factory=set) + _owner_collection: MultipleConfig[Any] | None = PrivateAttr(default=None) + _owner_uid: uuid.UUID | None = PrivateAttr(default=None) @property def file(self) -> Path | None: @@ -89,6 +129,9 @@ def file(self) -> Path | None: def is_locked(self) -> bool: return self._is_locked + def model_post_init(self, __context: Any) -> None: + self._register_ref_bindings() + def _multiple_config_index(self) -> dict[str, MultipleConfig[Any]]: result: dict[str, MultipleConfig[Any]] = {} for name, value in self.__dict__.items(): @@ -105,8 +148,239 @@ def _group_index(self) -> dict[str, BaseModel]: return result def _normalize_value(self, group: str, name: str, value: Any) -> Any: + ref_field = self._get_ref_field(group, name) + if ref_field is not None: + return self._normalize_ref_value(ref_field, value) return value + def _bind_owner_collection( + self, collection: MultipleConfig[Any], uid: uuid.UUID + ) -> None: + """记录当前配置项所属容器。""" + + self._owner_collection = collection + self._owner_uid = uid + + def _resolve_related_collection(self, target: str) -> MultipleConfig[Any] | None: + value = getattr(self, target, None) + if isinstance(value, MultipleConfig): + return value + + related = getattr(type(self), "related_config", None) + if isinstance(related, dict): + target_collection = related.get(target) + if isinstance(target_collection, MultipleConfig): + return target_collection + + return None + + def _get_ref_field(self, group: str, name: str) -> RefField | None: + group_model = self._group_index().get(group) + if group_model is None: + return None + return _get_field_marker(group_model, name, RefField) + + def _get_virtual_field(self, group: str, name: str) -> VirtualField | None: + group_model = self._group_index().get(group) + if group_model is None: + return None + return _get_field_marker(group_model, name, VirtualField) + + def _iter_ref_fields(self): + for group_name, group_model in self._group_index().items(): + for field_name in type(group_model).model_fields: + ref_field = _get_field_marker(group_model, field_name, RefField) + if ref_field is not None: + yield group_name, field_name, ref_field + + def _iter_virtual_dependents(self, dependency: tuple[str, str]): + for group_name, group_model in self._group_index().items(): + for field_name in type(group_model).model_fields: + virtual_field = _get_field_marker(group_model, field_name, VirtualField) + if virtual_field is None: + continue + if dependency in virtual_field.depends_on: + yield group_name, field_name, virtual_field + + def _get_virtual_value( + self, group: str, name: str, spec: VirtualField | None = None + ) -> Any: + spec = spec or self._get_virtual_field(group, name) + if spec is None: + raise AttributeError(f"配置项 '{group}.{name}' 不是虚拟字段") + + getter = spec.getter + if isinstance(getter, str): + result = getattr(self, getter)() + else: + result = getter(self) + + if inspect.isawaitable(result): + raise TypeError(f"虚拟配置项 '{group}.{name}' 的 getter 不能是异步方法") + + return result + + async def _set_virtual_value(self, group: str, name: str, value: Any) -> None: + spec = self._get_virtual_field(group, name) + if spec is None: + raise AttributeError(f"配置项 '{group}.{name}' 不是虚拟字段") + if spec.setter is None: + raise ValueError(f"虚拟配置项 '{group}.{name}' 为只读") + + setter = spec.setter + if isinstance(setter, str): + result = getattr(self, setter)(value) + else: + result = setter(self, value) + + if inspect.isawaitable(result): + await result + + def _normalize_ref_value(self, spec: RefField, value: Any) -> Any: + if value in spec.allow_values: + return value + if value == spec.default: + return spec.default + + text = value if isinstance(value, str) else str(value) + try: + uid = uuid.UUID(text) + except (TypeError, ValueError): + return spec.default + + target_collection = self._resolve_related_collection(spec.target) + if target_collection is None or uid not in target_collection: + return spec.default + + return str(uid) + + def _display_name(self) -> str: + info_model = self._group_index().get("Info") + if info_model is not None and hasattr(info_model, "Name"): + name = getattr(info_model, "Name") + if isinstance(name, str) and name: + return name + if self._owner_uid is not None: + return str(self._owner_uid) + return type(self).__name__ + + def _register_ref_bindings(self) -> None: + for _, _, ref_field in self._iter_ref_fields(): + if ref_field.target in self._registered_ref_targets: + continue + + target_collection = self._resolve_related_collection(ref_field.target) + if target_collection is None: + continue + + target_collection.bind_before_del(self._on_related_config_deleted) + self._registered_ref_targets.add(ref_field.target) + + async def _on_related_config_deleted( + self, event: MultipleConfigDeleteEvent[Any] + ) -> None: + matched_fields = [] + + for group_name, field_name, ref_field in self._iter_ref_fields(): + target_collection = self._resolve_related_collection(ref_field.target) + if target_collection is not event.collection: + continue + + group_model = self._group_index().get(group_name) + if group_model is None: + continue + + if getattr(group_model, field_name) != str(event.uid): + continue + + matched_fields.append((group_name, field_name, ref_field)) + + if not matched_fields: + return + + for group_name, field_name, ref_field in matched_fields: + action = ref_field.on_delete + + if action == "restrict": + raise RuntimeError( + f"{self._display_name()} 正在引用 {event.uid}, 无法删除" + ) + + if self.is_locked: + raise RuntimeError( + f"{self._display_name()} 正在引用 {event.uid} 且已锁定, 无法删除" + ) + + if action == "set_default": + await self.set(group_name, field_name, ref_field.default) + continue + + if action == "cascade": + if self._owner_collection is None or self._owner_uid is None: + raise RuntimeError( + f"{self._display_name()} 缺少所属容器信息, 无法级联删除" + ) + await self._owner_collection.remove(self._owner_uid) + return + + if action == "custom": + callback = ref_field.on_delete_callback + if callback is None: + raise RuntimeError( + f"{group_name}.{field_name} 未声明自定义删除回调" + ) + + if isinstance(callback, str): + result = getattr(self, callback)(event) + else: + result = callback(self, event) + + if inspect.isawaitable(result): + await result + continue + + raise ValueError(f"不支持的引用删除策略: {action}") + + async def _queue_binding(self, group: str, name: str, value: Any) -> None: + self._pending_bindings[(group, name)] = value + if self._transaction_depth == 0: + await self._flush_pending_bindings() + + async def _flush_pending_bindings(self) -> None: + while self._pending_bindings: + pending_items = list(self._pending_bindings.items()) + self._pending_bindings.clear() + for (group, name), value in pending_items: + await self._emit_bindings(group, name, value) + + @asynccontextmanager + async def transaction(self) -> AsyncIterator["PydanticConfigBase"]: + """开启一个延迟保存事务。""" + + self._transaction_depth += 1 + try: + yield self + finally: + self._transaction_depth -= 1 + if self._transaction_depth == 0: + await self._flush_pending_changes() + + async def _flush_pending_changes(self) -> None: + await self._flush_pending_bindings() + + if self._pending_save and self._file: + self._pending_save = False + self._file.parent.mkdir(parents=True, exist_ok=True) + self._file.write_text( + dump_toml(await self.toDict(if_decrypt=False)), + encoding="utf-8", + ) + + if self._pending_sync: + self._pending_sync = False + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) + async def connect(self, path: Path) -> None: if path.suffix != ".toml": raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") @@ -150,6 +424,9 @@ async def load(self, data: dict[str, Any]) -> None: default_group = type(group_model)() for field_name in list(type(group_model).model_fields.keys()): + if self._get_virtual_field(group_name, field_name) is not None: + continue + candidate: Any = None has_value = False @@ -186,7 +463,9 @@ async def toDict( data: dict[str, Any] = {} for group_name, group_model in self._group_index().items(): - data[group_name] = _export_group_model(group_model, if_decrypt) + data[group_name] = _export_group_model( + self, group_name, group_model, if_decrypt + ) for name, item in self._multiple_config_index().items(): if "SubConfigsInfo" not in data: @@ -202,6 +481,10 @@ def get(self, group: str, name: str) -> Any: if group_model is None or not hasattr(group_model, name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") + virtual_field = self._get_virtual_field(group, name) + if virtual_field is not None: + return self._get_virtual_value(group, name, virtual_field) + value = getattr(group_model, name) if _is_encrypted_field(group_model, name): return decrypt_encrypted_string(str(value)) @@ -215,7 +498,20 @@ async def set(self, group: str, name: str, value: Any) -> None: if self._is_locked: raise ValueError("配置已锁定, 无法修改") + virtual_field = self._get_virtual_field(group, name) + if virtual_field is not None: + await self._set_virtual_value(group, name, value) + return + old_value = getattr(group_model, name) + virtual_old_values = { + (virtual_group, virtual_name): self._get_virtual_value( + virtual_group, virtual_name, virtual_field + ) + for virtual_group, virtual_name, virtual_field in self._iter_virtual_dependents( + (group, name) + ) + } value = self._normalize_value(group, name, value) default_group = type(group_model)() @@ -226,14 +522,31 @@ async def set(self, group: str, name: str, value: Any) -> None: new_value = getattr(group_model, name) if old_value != new_value: - await self._emit_bindings(group, name, new_value) + await self._queue_binding(group, name, new_value) + + for (virtual_group, virtual_name), old_virtual_value in virtual_old_values.items(): + new_virtual_value = self.get(virtual_group, virtual_name) + if old_virtual_value != new_virtual_value: + await self._queue_binding( + virtual_group, virtual_name, new_virtual_value + ) if self._file: await self.save() - if self._save_methods: + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) + async def set_many(self, values: dict[str, dict[str, Any]]) -> None: + """批量更新多个配置项。""" + + async with self.transaction(): + for group, items in values.items(): + for name, value in items.items(): + await self.set(group, name, value) + def bind(self, group: str, name: str, slot: Slot) -> None: group_model = self._group_index().get(group) if group_model is None or not hasattr(group_model, name): @@ -266,15 +579,18 @@ async def _emit_bindings(self, group: str, name: str, value: Any) -> None: if slots is None: return for slot in slots: - if inspect.iscoroutinefunction(slot): - await slot(value) - else: - slot(value) + result = slot(value) + if inspect.isawaitable(result): + await result async def save(self) -> None: if not self._file: raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + if self._transaction_depth > 0: + self._pending_save = True + return + self._file.parent.mkdir(parents=True, exist_ok=True) self._file.write_text( dump_toml(await self.toDict(if_decrypt=False)), diff --git a/app/models/common.py b/app/models/common.py index aa241ba9..2ae64dcc 100644 --- a/app/models/common.py +++ b/app/models/common.py @@ -1,12 +1,12 @@ from __future__ import annotations import calendar -import uuid -from typing import Any, ClassVar, Literal, cast +from typing import Annotated, Any, ClassVar, Literal, cast from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig +from app.core.config.fields import RefField from app.core.config.pydantic import PydanticConfigBase from app.core.config.types import ( HHMMString, @@ -73,32 +73,18 @@ class QueueItem(PydanticConfigBase): related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} class InfoModel(BaseModel): - ScriptId: str = "-" + ScriptId: Annotated[ + str, + RefField( + "ScriptConfig", + default="-", + allow_values=("-",), + on_delete="cascade", + ), + ] = "-" Info: InfoModel = Field(default_factory=InfoModel) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - - if (group, name) != ("Info", "ScriptId"): - return value - - if value == "-": - return "-" - if not isinstance(value, str): - return "-" - - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return "-" - - script_config = self.related_config.get("ScriptConfig") - if script_config is None or uid not in script_config: - return "-" - - return str(uid) - class TimeSet(PydanticConfigBase): """时间设置配置""" diff --git a/app/models/general.py b/app/models/general.py index d98cfb9b..d2094e69 100644 --- a/app/models/general.py +++ b/app/models/general.py @@ -1,15 +1,15 @@ from __future__ import annotations import json -import uuid from datetime import datetime from pathlib import Path -from typing import Any, ClassVar, Literal +from typing import Annotated, Any, ClassVar, Literal from pydantic import BaseModel, Field, field_validator from app.utils.constants import UTC4 from app.core.config.base import MultipleConfig +from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase from app.core.config.types import UrlString from .common import Webhook @@ -27,7 +27,18 @@ class InfoModel(BaseModel): IfScriptAfterTask: bool = False ScriptAfterTask: str = str(Path.cwd()) Notes: str = "无" - Tag: str = "[ ]" + Tag: Annotated[ + str, + VirtualField( + "getTags", + depends_on=( + ("Data", "LastProxyDate"), + ("Data", "ProxyTimes"), + ("Info", "RemainedDay"), + ("Info", "Notes"), + ), + ), + ] = "[ ]" class DataModel(BaseModel): LastProxyDate: str = "2000-01-01" @@ -59,26 +70,6 @@ def __init__(self, **data: Any): super().__init__(**data) self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - if (group, name) == ("Info", "Tag"): - return self.getTags() - return value - - def get(self, group: str, name: str) -> Any: - if (group, name) == ("Info", "Tag"): - return self.getTags() - return super().get(group, name) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - data = await super().toDict(if_decrypt, regenerate_uuids) - info = data.get("Info") - if isinstance(info, dict): - info["Tag"] = self.getTags() - return data - def getTags(self) -> str: # noqa: N802 """生成通用用户标签列表""" tags: list[dict[str, str]] = [] @@ -167,7 +158,15 @@ class GameModel(BaseModel): Arguments: str = "" WaitTime: int = Field(default=0, ge=0, le=9999) IfForceClose: bool = False - EmulatorId: str = "-" + EmulatorId: Annotated[ + str, + RefField( + "EmulatorConfig", + default="-", + allow_values=("-",), + on_delete="set_default", + ), + ] = "-" EmulatorIndex: str = "-" class RunModel(BaseModel): @@ -184,26 +183,5 @@ def __init__(self, **data: Any): super().__init__(**data) self.UserData = MultipleConfig([GeneralUserConfig]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - - if (group, name) != ("Game", "EmulatorId"): - return value - - if value == "-": - return "-" - if not isinstance(value, str): - return "-" - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return "-" - - emulator_config = self.related_config.get("EmulatorConfig") - if emulator_config is None or uid not in emulator_config: - return "-" - - return str(uid) - __all__ = ["GeneralUserConfig", "GeneralConfig"] diff --git a/app/models/global_config.py b/app/models/global_config.py index 0e103fd7..2d7db879 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -4,11 +4,12 @@ import json import uuid from datetime import datetime -from typing import Any, Callable, Literal +from typing import Annotated, Any, Callable, Literal from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig +from app.core.config.fields import VirtualField from app.core.config.pydantic import PydanticConfigBase from app.core.config.types import ( EncryptedString, @@ -38,7 +39,13 @@ class ArknightsPCModel(BaseModel): RetreatKey: KeyboardKeyString = "t" NextFrameKey: KeyboardKeyString = "f" AnotherQuitKey: KeyboardKeyString = "space" - Status: str = "-" + Status: Annotated[ + str, + VirtualField( + "arknights_pc_status", + depends_on=(("ArknightsPC", "Enabled"),), + ), + ] = "-" ArknightsPC: ArknightsPCModel = Field(default_factory=ArknightsPCModel) @@ -47,26 +54,6 @@ def __init__(self, **data: Any): self.arknights_pc_running = False self.arknights_pc_get_connected: Callable[[], bool] = lambda: False - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - if (group, name) == ("ArknightsPC", "Status"): - return self.arknights_pc_status() - return value - - def get(self, group: str, name: str) -> Any: - if (group, name) == ("ArknightsPC", "Status"): - return self.arknights_pc_status() - return super().get(group, name) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - data = await super().toDict(if_decrypt, regenerate_uuids) - pc = data.get("ArknightsPC") - if isinstance(pc, dict): - pc["Status"] = self.arknights_pc_status() - return data - @property def arknights_pc_connected(self) -> bool: return self.arknights_pc_get_connected() @@ -149,13 +136,16 @@ class DataModel(BaseModel): LastStageUpdated: YmdHmsString = "2000-01-01 00:00:00" StageETag: str = "" StageData: JsonDictString = "{ }" - Stage: str = "-" LastNoticeUpdated: YmdHmsString = "2000-01-01 00:00:00" NoticeETag: str = "" IfShowNotice: bool = True Notice: JsonDictString = "{ }" LastWebConfigUpdated: YmdHmsString = "2000-01-01 00:00:00" WebConfig: JsonListString = "[ ]" + Stage: Annotated[ + str, + VirtualField("getStage", depends_on=(("Data", "StageData"),)), + ] = "-" @field_validator("UID", mode="before") @classmethod @@ -199,26 +189,6 @@ def __init__(self, **data: Any): MaaUserConfig.related_config["PlanConfig"] = self.PlanConfig QueueItem.related_config["ScriptConfig"] = self.ScriptConfig - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - if (group, name) == ("Data", "Stage"): - return self.getStage() - return value - - def get(self, group: str, name: str) -> Any: - if (group, name) == ("Data", "Stage"): - return self.getStage() - return super().get(group, name) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - data = await super().toDict(if_decrypt, regenerate_uuids) - cfg_data = data.get("Data") - if isinstance(cfg_data, dict): - cfg_data["Stage"] = self.getStage() - return data - def getStage(self) -> str: # noqa: N802 """获取关卡信息""" diff --git a/app/models/maa.py b/app/models/maa.py index 50dd8359..509d930e 100644 --- a/app/models/maa.py +++ b/app/models/maa.py @@ -4,11 +4,12 @@ import uuid from datetime import datetime from pathlib import Path -from typing import Any, ClassVar, Callable, Literal +from typing import Annotated, Any, ClassVar, Callable, Literal from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig +from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase from app.core.config.types import EncryptedString, JsonDictString from app.utils.constants import MAA_STAGE_KEY, RESOURCE_STAGE_INFO, UTC4, UTC8 @@ -33,7 +34,15 @@ class InfoModel(BaseModel): Id: str = "" Password: EncryptedString = "" Mode: Literal["简洁", "详细"] = "简洁" - StageMode: str = "Fixed" + StageMode: Annotated[ + str, + RefField( + "PlanConfig", + default="Fixed", + allow_values=("Fixed",), + on_delete="set_default", + ), + ] = "Fixed" Server: Literal[ "Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy" ] = "Official" @@ -47,8 +56,24 @@ class InfoModel(BaseModel): "LungmenDowntown@Annihilation", ] = "Annihilation" InfrastMode: Literal["Normal", "Rotation", "Custom"] = "Normal" - InfrastName: str = "-" - InfrastIndex: str = "-" + InfrastName: Annotated[ + str, + VirtualField( + "getInfrastName", + depends_on=(("Info", "InfrastMode"), ("Data", "CustomInfrast")), + ), + ] = "-" + InfrastIndex: Annotated[ + str, + VirtualField( + "getInfrastIndex", + depends_on=( + ("Info", "InfrastMode"), + ("Data", "CustomInfrast"), + ("Data", "InfrastIndex"), + ), + ), + ] = "-" Notes: str = "无" MedicineNumb: int = Field(default=0, ge=0, le=9999) SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = "0" @@ -59,7 +84,31 @@ class InfoModel(BaseModel): Stage_Remain: str = "-" IfSkland: bool = False SklandToken: EncryptedString = "" - Tag: str = "[ ]" + Tag: Annotated[ + str, + VirtualField( + "getTags", + depends_on=( + ("Data", "IfPassCheck"), + ("Data", "LastProxyDate"), + ("Data", "ProxyTimes"), + ("Info", "IfSkland"), + ("Data", "LastSklandDate"), + ("Info", "RemainedDay"), + ("Task", "IfInfrast"), + ("Info", "InfrastMode"), + ("Data", "CustomInfrast"), + ("Data", "InfrastIndex"), + ("Info", "StageMode"), + ("Info", "Stage"), + ("Info", "Stage_1"), + ("Info", "Stage_2"), + ("Info", "Stage_3"), + ("Info", "Stage_Remain"), + ("Info", "Notes"), + ), + ), + ] = "[ ]" class DataModel(BaseModel): LastProxyDate: str = "2000-01-01" @@ -111,52 +160,6 @@ def __init__(self, **data: Any): super().__init__(**data) self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - - if (group, name) == ("Info", "StageMode"): - if value == "Fixed": - return "Fixed" - if not isinstance(value, str): - return "Fixed" - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return "Fixed" - plan_config = self.related_config.get("PlanConfig") - if plan_config is None or uid not in plan_config: - return "Fixed" - return str(uid) - - if (group, name) == ("Info", "InfrastName"): - return self.getInfrastName() - if (group, name) == ("Info", "InfrastIndex"): - return self.getInfrastIndex() - if (group, name) == ("Info", "Tag"): - return self.getTags() - - return value - - def get(self, group: str, name: str) -> Any: - if (group, name) == ("Info", "InfrastName"): - return self.getInfrastName() - if (group, name) == ("Info", "InfrastIndex"): - return self.getInfrastIndex() - if (group, name) == ("Info", "Tag"): - return self.getTags() - return super().get(group, name) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - data = await super().toDict(if_decrypt, regenerate_uuids) - info = data.get("Info") - if isinstance(info, dict): - info["InfrastName"] = self.getInfrastName() - info["InfrastIndex"] = self.getInfrastIndex() - info["Tag"] = self.getTags() - return data - def getInfrastName(self) -> str: # noqa: N802 if self.get("Info", "InfrastMode") != "Custom": return "未使用自定义基建模式" @@ -334,7 +337,15 @@ class InfoModel(BaseModel): Path: str = str(Path.cwd()) class EmulatorModel(BaseModel): - Id: str = "-" + Id: Annotated[ + str, + RefField( + "EmulatorConfig", + default="-", + allow_values=("-",), + on_delete="set_default", + ), + ] = "-" Index: str = "-" class RunModel(BaseModel): @@ -355,27 +366,6 @@ def __init__(self, **data: Any): super().__init__(**data) self.UserData = MultipleConfig([MaaUserConfig]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - - if (group, name) != ("Emulator", "Id"): - return value - - if value == "-": - return "-" - if not isinstance(value, str): - return "-" - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return "-" - - emulator_config = self.related_config.get("EmulatorConfig") - if emulator_config is None or uid not in emulator_config: - return "-" - - return str(uid) - class MaaPlanConfig(PydanticConfigBase): """MAA计划表配置""" diff --git a/app/models/maaend.py b/app/models/maaend.py index 5ca78252..b263b5a9 100644 --- a/app/models/maaend.py +++ b/app/models/maaend.py @@ -1,14 +1,14 @@ from __future__ import annotations import json -import uuid from datetime import datetime from pathlib import Path -from typing import Any, ClassVar, Literal +from typing import Annotated, Any, ClassVar, Literal from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig +from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase from app.core.config.types import EncryptedString from app.utils.constants import MAAEND_STAGE_BOOK, MAAEND_STAGE_WITH_AB, UTC4, UTC8 @@ -29,7 +29,27 @@ class InfoModel(BaseModel): Notes: str = "无" IfSkland: bool = False SklandToken: EncryptedString = "" - Tag: str = "[ ]" + Tag: Annotated[ + str, + VirtualField( + "getTags", + depends_on=( + ("Data", "IfPassCheck"), + ("Data", "LastProxyStatus"), + ("Data", "LastProxyDate"), + ("Data", "ProxyTimes"), + ("Info", "IfSkland"), + ("Data", "LastSklandDate"), + ("Info", "RemainedDay"), + ("Task", "ProtocolSpaceTab"), + ("Task", "OperatorProgression"), + ("Task", "WeaponProgression"), + ("Task", "CrisisDrills"), + ("Task", "RewardsSetOption"), + ("Info", "Notes"), + ), + ), + ] = "[ ]" class TaskModel(BaseModel): ProtocolSpaceTab: Literal[ @@ -82,26 +102,6 @@ def __init__(self, **data: Any): super().__init__(**data) self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - if (group, name) == ("Info", "Tag"): - return self.getTags() - return value - - def get(self, group: str, name: str) -> Any: - if (group, name) == ("Info", "Tag"): - return self.getTags() - return super().get(group, name) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - data = await super().toDict(if_decrypt, regenerate_uuids) - info = data.get("Info") - if isinstance(info, dict): - info["Tag"] = self.getTags() - return data - def getTags(self) -> str: # noqa: N802 """生成用户标签列表,返回JSON字符串格式的TagItem列表""" tags: list[dict[str, str]] = [] @@ -206,7 +206,15 @@ class GameModel(BaseModel): Path: str = str(Path.cwd()) Arguments: str = "" WaitTime: int = Field(default=0, ge=0, le=9999) - EmulatorId: str = "-" + EmulatorId: Annotated[ + str, + RefField( + "EmulatorConfig", + default="-", + allow_values=("-",), + on_delete="set_default", + ), + ] = "-" EmulatorIndex: str = "-" CloseOnFinish: bool = True @@ -218,26 +226,5 @@ def __init__(self, **data: Any): super().__init__(**data) self.UserData = MultipleConfig([MaaEndUserConfig]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - - if (group, name) != ("Game", "EmulatorId"): - return value - - if value == "-": - return "-" - if not isinstance(value, str): - return "-" - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return "-" - - emulator_config = self.related_config.get("EmulatorConfig") - if emulator_config is None or uid not in emulator_config: - return "-" - - return str(uid) - __all__ = ["MaaEndUserConfig", "MaaEndConfig"] diff --git a/app/models/shared.py b/app/models/shared.py new file mode 100644 index 00000000..f5a26675 --- /dev/null +++ b/app/models/shared.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +class TagItem(BaseModel): + text: str = Field(..., description="标签文本") + color: Literal[ + "red", + "blue", + "green", + "yellow", + "orange", + "purple", + "pink", + "brown", + "black", + "white", + "gray", + "silver", + "gold", + ] = Field(..., description="标签颜色") + + +class WebSocketMessage(BaseModel): + id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程") + type: Literal["Update", "Message", "Info", "Signal"] = Field( + ..., + description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号", + ) + data: dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定") + + +class DeviceInfo(BaseModel): + """API 层使用的设备信息模型。""" + + title: str = Field(..., description="设备标题/名称") + status: int = Field(..., description="设备状态, 参考 DeviceStatus 枚举值") + adb_address: str = Field(..., description="ADB连接地址") + + +__all__ = ["TagItem", "WebSocketMessage", "DeviceInfo"] diff --git a/app/models/src.py b/app/models/src.py index 0af60cf1..a2ee853d 100644 --- a/app/models/src.py +++ b/app/models/src.py @@ -1,14 +1,14 @@ from __future__ import annotations import json -import uuid from datetime import datetime from pathlib import Path -from typing import Any, ClassVar, Literal +from typing import Annotated, Any, ClassVar, Literal from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig +from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase from app.core.config.types import EncryptedString from app.utils.constants import STARRAIL_STAGE_BOOK, UTC4 @@ -158,7 +158,25 @@ class InfoModel(BaseModel): ] = "CN-Official" RemainedDay: int = Field(default=-1, ge=-1, le=9999) Notes: str = "无" - Tag: str = "[ ]" + Tag: Annotated[ + str, + VirtualField( + "getTags", + depends_on=( + ("Data", "IfPassCheck"), + ("Data", "LastProxyDate"), + ("Data", "ProxyTimes"), + ("Info", "RemainedDay"), + ("Stage", "Channel"), + ("Stage", "Relic"), + ("Stage", "Materials"), + ("Stage", "Ornament"), + ("Stage", "EchoOfWar"), + ("Stage", "SimulatedUniverseWorld"), + ("Info", "Notes"), + ), + ), + ] = "[ ]" class StageModel(BaseModel): Channel: Literal["Relic", "Materials", "Ornament"] = "Relic" @@ -233,26 +251,6 @@ def __init__(self, **data: Any): super().__init__(**data) self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - if (group, name) == ("Info", "Tag"): - return self.getTags() - return value - - def get(self, group: str, name: str) -> Any: - if (group, name) == ("Info", "Tag"): - return self.getTags() - return super().get(group, name) - - async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False - ) -> dict[str, Any]: - data = await super().toDict(if_decrypt, regenerate_uuids) - info = data.get("Info") - if isinstance(info, dict): - info["Tag"] = self.getTags() - return data - def getTags(self) -> str: # noqa: N802 """生成用户标签列表,返回JSON字符串格式的TagItem列表""" tags: list[dict[str, str]] = [] @@ -337,7 +335,15 @@ class InfoModel(BaseModel): Path: str = str(Path.cwd()) class EmulatorModel(BaseModel): - Id: str = "-" + Id: Annotated[ + str, + RefField( + "EmulatorConfig", + default="-", + allow_values=("-",), + on_delete="set_default", + ), + ] = "-" Index: str = "-" class RunModel(BaseModel): @@ -354,26 +360,5 @@ def __init__(self, **data: Any): super().__init__(**data) self.UserData = MultipleConfig([SrcUserConfig]) - def _normalize_value(self, group: str, name: str, value: Any) -> Any: - value = super()._normalize_value(group, name, value) - - if (group, name) != ("Emulator", "Id"): - return value - - if value == "-": - return "-" - if not isinstance(value, str): - return "-" - try: - uid = uuid.UUID(value) - except (TypeError, ValueError): - return "-" - - emulator_config = self.related_config.get("EmulatorConfig") - if emulator_config is None or uid not in emulator_config: - return "-" - - return str(uid) - __all__ = ["SrcUserConfig", "SrcConfig"] From 6782e9c9135d1b243d13e029e27108866ef24b15 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sat, 4 Apr 2026 13:29:13 +0800 Subject: [PATCH 10/29] =?UTF-8?q?refactor(config):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=AF=BC=E5=85=A5=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E5=92=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/common.py | 68 +- app/api/core.py | 3 +- app/api/dispatch.py | 4 +- app/api/emulator.py | 33 +- app/api/history.py | 44 +- app/api/info.py | 9 +- app/api/ocr.py | 2 +- app/api/plan.py | 44 +- app/api/queue.py | 33 +- app/api/scripts.py | 107 +-- app/api/setting.py | 77 +- app/api/tools.py | 17 +- app/api/update.py | 3 +- app/api/ws_command.py | 35 +- app/api/ws_debug.py | 2 +- app/core/config/__init__.py | 14 +- app/core/config/base.py | 40 +- app/core/config/manager.py | 2 +- app/core/config/pydantic.py | 49 +- app/core/config/types.py | 5 +- app/core/emulator_manager.py | 8 +- app/core/task_manager.py | 2 +- app/models/__init__.py | 3 +- app/models/common_contract.py | 184 ++++ app/models/dispatch_contract.py | 51 ++ app/models/dto.py | 1462 ------------------------------- app/models/emulator_contract.py | 116 +++ app/models/general_contract.py | 131 +++ app/models/global_config.py | 2 +- app/models/history_contract.py | 63 ++ app/models/info_contract.py | 41 + app/models/maa_contract.py | 127 +++ app/models/maaend_contract.py | 109 +++ app/models/plan_contract.py | 123 +++ app/models/queue_contract.py | 262 ++++++ app/models/scripts_contract.py | 293 +++++++ app/models/setting_contract.py | 293 +++++++ app/models/src_contract.py | 204 +++++ app/models/tools_contract.py | 56 ++ app/models/update_contract.py | 19 + app/models/ws_contract.py | 127 +++ pyrightconfig.json | 7 +- requirements-dev.txt | 2 + 43 files changed, 2546 insertions(+), 1730 deletions(-) create mode 100644 app/models/common_contract.py create mode 100644 app/models/dispatch_contract.py delete mode 100644 app/models/dto.py create mode 100644 app/models/emulator_contract.py create mode 100644 app/models/general_contract.py create mode 100644 app/models/history_contract.py create mode 100644 app/models/info_contract.py create mode 100644 app/models/maa_contract.py create mode 100644 app/models/maaend_contract.py create mode 100644 app/models/plan_contract.py create mode 100644 app/models/queue_contract.py create mode 100644 app/models/scripts_contract.py create mode 100644 app/models/setting_contract.py create mode 100644 app/models/src_contract.py create mode 100644 app/models/tools_contract.py create mode 100644 app/models/update_contract.py create mode 100644 app/models/ws_contract.py create mode 100644 requirements-dev.txt diff --git a/app/api/common.py b/app/api/common.py index 538b092b..fa7649c8 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -1,21 +1,51 @@ from __future__ import annotations -from pydantic import BaseModel, Field - - -class OutBase(BaseModel): - code: int = Field(default=200, description="状态码") - status: str = Field(default="success", description="操作状态") - message: str = Field(default="操作成功", description="操作消息") - - -class ComboBoxItem(BaseModel): - label: str = Field(..., description="展示值") - value: str | None = Field(..., description="实际值") - - -class ComboBoxOut(OutBase): - data: list[ComboBoxItem] = Field(..., description="下拉框选项") - - -__all__ = ["OutBase", "ComboBoxItem", "ComboBoxOut"] +from collections.abc import Awaitable, Callable +from typing import TypeVar + +from app.models.common_contract import ComboBoxItem, ComboBoxOut, OutBase + + +OutT = TypeVar("OutT", bound=OutBase) + + +def error_out( + model_cls: type[OutT], + exc: Exception, + *, + message: str | None = None, + **kwargs: object, +) -> OutT: + return model_cls( + code=500, + status="error", + message=message or f"{type(exc).__name__}: {str(exc)}", + **kwargs, + ) + + +async def run_api( + success_factory: Callable[[], Awaitable[OutT]], + *, + model_cls: type[OutT], + message: str | None = None, + **fallback_kwargs: object, +) -> OutT: + try: + return await success_factory() + except Exception as exc: + return error_out( + model_cls, + exc, + message=message, + **fallback_kwargs, + ) + + +__all__ = [ + "OutBase", + "ComboBoxItem", + "ComboBoxOut", + "error_out", + "run_api", +] diff --git a/app/api/core.py b/app/api/core.py index 54803320..91f24dde 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -29,7 +29,8 @@ from app.core import Config, Broadcast, TaskManager from app.services import System -from app.models.dto import OutBase, WebSocketMessage +from app.models.common_contract import OutBase +from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command from app.utils import get_logger diff --git a/app/api/dispatch.py b/app/api/dispatch.py index 87e48349..2c36f07c 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -25,9 +25,9 @@ from app.core import Config, TaskManager from app.services import System -from app.models.dto import ( +from app.models.common_contract import OutBase +from app.models.dispatch_contract import ( DispatchIn, - OutBase, PowerIn, PowerOut, TaskCreateIn, diff --git a/app/api/emulator.py b/app/api/emulator.py index 821903b2..6b284d00 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -23,20 +23,25 @@ from fastapi import APIRouter, Body from app.core import Config, EmulatorManager -from app.models.dto import ( +from app.models.common_contract import ( OutBase, - EmulatorConfig, - EmulatorGetIn, - EmulatorGetOut, - EmulatorConfigIndexItem, + project_model, + project_model_list, + project_model_map, +) +from app.models.emulator_contract import ( EmulatorCreateOut, - EmulatorUpdateIn, + EmulatorConfigIndexItem, EmulatorDeleteIn, - EmulatorReorderIn, + EmulatorGetIn, + EmulatorGetOut, EmulatorOperateIn, - EmulatorStatusOut, - EmulatorSearchOut, + EmulatorRead, + EmulatorReorderIn, EmulatorSearchResult, + EmulatorSearchOut, + EmulatorStatusOut, + EmulatorUpdateIn, ) router = APIRouter(prefix="/api/emulator", tags=["模拟器管理"]) @@ -52,8 +57,8 @@ async def get_emulator(emulator: EmulatorGetIn = Body(...)) -> EmulatorGetOut: try: index, data = await Config.get_emulator(emulator.emulatorId) - index = [EmulatorConfigIndexItem(**_) for _ in index] - data = {uid: EmulatorConfig(**cfg) for uid, cfg in data.items()} + index = project_model_list(EmulatorConfigIndexItem, index) + data = project_model_map(EmulatorRead, data) except Exception as e: return EmulatorGetOut( code=500, @@ -75,14 +80,14 @@ async def get_emulator(emulator: EmulatorGetIn = Body(...)) -> EmulatorGetOut: async def add_emulator() -> EmulatorCreateOut: try: uid, config = await Config.add_emulator() - data = EmulatorConfig(**(await config.toDict())) + data = project_model(EmulatorRead, await config.toDict()) except Exception as e: return EmulatorCreateOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", emulatorId="", - data=EmulatorConfig(**{}), + data=EmulatorRead(), ) return EmulatorCreateOut(emulatorId=str(uid), data=data) @@ -189,7 +194,7 @@ async def search_emulators() -> EmulatorSearchOut: from app.utils import search_all_emulators emulators = await search_all_emulators() - results = [EmulatorSearchResult(**emulator) for emulator in emulators] + results = project_model_list(EmulatorSearchResult, emulators) except Exception as e: return EmulatorSearchOut( code=500, diff --git a/app/api/history.py b/app/api/history.py index 7eafc953..193aa9fa 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -23,10 +23,12 @@ from datetime import datetime from pathlib import Path +from typing import Any, cast from fastapi import APIRouter, Body from app.core import Config -from app.models.dto import ( +from app.models.common_contract import project_model_list +from app.models.history_contract import ( HistoryData, HistoryDataGetIn, HistoryDataGetOut, @@ -38,6 +40,22 @@ router = APIRouter(prefix="/api/history", tags=["历史记录"]) +def _build_history_data(raw: dict[str, object]) -> HistoryData: + index_data = raw.get("index", []) + raw["index"] = [] + if isinstance(index_data, list): + index_rows = [ + cast(dict[str, Any], item) + for item in cast(list[object], index_data) + if isinstance(item, dict) + ] + raw["index"] = project_model_list( + HistoryIndexItem, + index_rows, + ) + return HistoryData.model_validate(raw) + + @router.post( "/search", tags=["Get"], @@ -47,20 +65,18 @@ ) async def search_history(history: HistorySearchIn) -> HistorySearchOut: try: - data = await Config.search_history( + raw_data = await Config.search_history( history.mode, datetime.strptime(history.start_date, "%Y-%m-%d").date(), datetime.strptime(history.end_date, "%Y-%m-%d").date(), ) - for date, users in data.items(): + data: dict[str, dict[str, HistoryData]] = {} + for date, users in raw_data.items(): + current_users: dict[str, HistoryData] = {} for user, records in users.items(): record = await Config.merge_statistic_info(records) - # 安全检查:确保 index 字段存在 - if "index" not in record: - record["index"] = [] - record["index"] = [HistoryIndexItem(**_) for _ in record["index"]] - record = HistoryData(**record) - data[date][user] = record + current_users[user] = _build_history_data(record) + data[date] = current_users except Exception as e: return HistorySearchOut( code=500, @@ -81,15 +97,15 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut: try: path = Path(history.jsonPath) - data = await Config.merge_statistic_info([path]) - data.pop("index", None) - data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8") - data = HistoryData(**data) + raw_data = await Config.merge_statistic_info([path]) + raw_data.pop("index", None) + raw_data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8") + data = _build_history_data(raw_data) except Exception as e: return HistoryDataGetOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", - data=HistoryData(**{}), + data=HistoryData(), ) return HistoryDataGetOut(data=data) diff --git a/app/api/info.py b/app/api/info.py index 9e5db38f..a5fed4b8 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -26,14 +26,11 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.dto import ( - ComboBoxItem, - ComboBoxOut, - EmulatorDeleteIn, +from app.models.common_contract import ComboBoxItem, ComboBoxOut, InfoOut, OutBase +from app.models.emulator_contract import EmulatorDeleteIn +from app.models.info_contract import ( GetStageIn, - InfoOut, NoticeOut, - OutBase, VersionOut, ) diff --git a/app/api/ocr.py b/app/api/ocr.py index 0b59fea7..8c8e22a3 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -29,7 +29,7 @@ from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger -from app.models.dto import OutBase +from app.models.common_contract import OutBase logger = get_logger("OCR API") diff --git a/app/api/plan.py b/app/api/plan.py index b6c2ae64..986f8cb1 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -24,9 +24,14 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.dto import ( - MaaPlanConfig, +from app.models.common_contract import ( OutBase, + project_model, + project_model_list, + project_model_map, +) +from app.models.plan_contract import ( + MaaPlanRead, PlanCreateIn, PlanCreateOut, PlanDeleteIn, @@ -36,6 +41,7 @@ PlanReorderIn, PlanUpdateIn, ) +from app.api.common import error_out router = APIRouter(prefix="/api/plan", tags=["计划管理"]) @@ -50,15 +56,9 @@ async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: try: uid, config = await Config.add_plan(plan.type) - data = MaaPlanConfig(**(await config.toDict())) + data = project_model(MaaPlanRead, await config.toDict()) except Exception as e: - return PlanCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - planId="", - data=MaaPlanConfig(**{}), - ) + return error_out(PlanCreateOut, e, planId="", data=MaaPlanRead()) return PlanCreateOut(planId=str(uid), data=data) @@ -72,16 +72,10 @@ async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut: try: index, data = await Config.get_plan(plan.planId) - index = [PlanIndexItem(**_) for _ in index] - data = {uid: MaaPlanConfig(**cfg) for uid, cfg in data.items()} + index = project_model_list(PlanIndexItem, index) + data = project_model_map(MaaPlanRead, data) except Exception as e: - return PlanGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) + return error_out(PlanGetOut, e, index=[], data={}) return PlanGetOut(index=index, data=data) @@ -96,9 +90,7 @@ async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase: try: await Config.update_plan(plan.planId, plan.data.model_dump(exclude_unset=True)) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -113,9 +105,7 @@ async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase: try: await Config.del_plan(plan.planId) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -130,7 +120,5 @@ async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase: try: await Config.reorder_plan(plan.indexList) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() diff --git a/app/api/queue.py b/app/api/queue.py index 7d57cf54..e01a06a9 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -24,15 +24,20 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.dto import ( +from app.models.common_contract import ( OutBase, - QueueConfig, + project_model, + project_model_list, + project_model_map, +) +from app.models.queue_contract import ( + QueueRead, QueueCreateOut, QueueDeleteIn, QueueGetIn, QueueGetOut, QueueIndexItem, - QueueItem, + QueueItemRead, QueueItemCreateOut, QueueItemDeleteIn, QueueItemGetIn, @@ -43,7 +48,7 @@ QueueReorderIn, QueueSetInBase, QueueUpdateIn, - TimeSet, + TimeSetRead, TimeSetCreateOut, TimeSetDeleteIn, TimeSetGetIn, @@ -68,14 +73,14 @@ async def add_queue() -> QueueCreateOut: try: uid, config = await Config.add_queue() - data = QueueConfig(**(await config.toDict())) + data = project_model(QueueRead, await config.toDict()) except Exception as e: return QueueCreateOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", queueId="", - data=QueueConfig(**{}), + data=QueueRead(), ) return QueueCreateOut(queueId=str(uid), data=data) @@ -91,8 +96,8 @@ async def add_queue() -> QueueCreateOut: async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut: try: index, config = await Config.get_queue(queue.queueId) - index = [QueueIndexItem(**_) for _ in index] - data = {uid: QueueConfig(**cfg) for uid, cfg in config.items()} + index = project_model_list(QueueIndexItem, index) + data = project_model_map(QueueRead, config) except Exception as e: return QueueGetOut( code=500, @@ -167,8 +172,8 @@ async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase: async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut: try: index, data = await Config.get_time_set(time.queueId, time.timeSetId) - index = [TimeSetIndexItem(**_) for _ in index] - data = {uid: TimeSet(**cfg) for uid, cfg in data.items()} + index = project_model_list(TimeSetIndexItem, index) + data = project_model_map(TimeSetRead, data) except Exception as e: return TimeSetGetOut( code=500, @@ -189,7 +194,7 @@ async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut: ) async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut: uid, config = await Config.add_time_set(time.queueId) - data = TimeSet(**(await config.toDict())) + data = project_model(TimeSetRead, await config.toDict()) return TimeSetCreateOut(timeSetId=str(uid), data=data) @@ -256,8 +261,8 @@ async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase: async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut: try: index, data = await Config.get_queue_item(item.queueId, item.queueItemId) - index = [QueueItemIndexItem(**_) for _ in index] - data = {uid: QueueItem(**cfg) for uid, cfg in data.items()} + index = project_model_list(QueueItemIndexItem, index) + data = project_model_map(QueueItemRead, data) except Exception as e: return QueueItemGetOut( code=500, @@ -278,7 +283,7 @@ async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut: ) async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut: uid, config = await Config.add_queue_item(item.queueId) - data = QueueItem(**(await config.toDict())) + data = project_model(QueueItemRead, await config.toDict()) return QueueItemCreateOut(queueItemId=str(uid), data=data) diff --git a/app/api/scripts.py b/app/api/scripts.py index f2e03dc4..c0555e0f 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -25,16 +25,15 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.dto import ( +from app.models.common_contract import ( ComboBoxItem, ComboBoxOut, - GeneralConfig, - GeneralUserConfig, - MaaConfig, - MaaEndConfig, - MaaEndUserConfig, - MaaUserConfig, OutBase, + project_model, + project_model_list, + project_model_map, +) +from app.models.scripts_contract import ( ScriptCreateIn, ScriptCreateOut, ScriptDeleteIn, @@ -46,8 +45,6 @@ ScriptUpdateIn, ScriptUploadIn, ScriptUrlIn, - SrcConfig, - SrcUserConfig, UserCreateOut, UserDeleteIn, UserGetIn, @@ -57,35 +54,30 @@ UserReorderIn, UserSetIn, UserUpdateIn, - Webhook, + project_script_model, + project_script_model_map, + project_user_model, + project_user_model_map, + script_contract_type_from_create, + script_contract_type_from_runtime, + user_contract_type_from_script, + validate_script_patch_data, + validate_user_patch_data, +) +from app.models.setting_contract import ( WebhookCreateOut, WebhookDeleteIn, WebhookGetIn, WebhookGetOut, WebhookInBase, WebhookIndexItem, + WebhookRead, WebhookReorderIn, - WebhookTestIn, WebhookUpdateIn, ) - router = APIRouter(prefix="/api/scripts", tags=["脚本管理"]) -SCRIPT_BOOK = { - "MaaConfig": MaaConfig, - "SrcConfig": SrcConfig, - "MaaEndConfig": MaaEndConfig, - "GeneralConfig": GeneralConfig, -} -USER_BOOK = { - "MaaConfig": MaaUserConfig, - "SrcConfig": SrcUserConfig, - "MaaEndConfig": MaaEndUserConfig, - "GeneralConfig": GeneralUserConfig, -} - - @router.post( "/add", tags=["Add"], @@ -96,14 +88,20 @@ async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: try: uid, config = await Config.add_script(script.type, script.scriptId) - data = SCRIPT_BOOK[type(config).__name__](**(await config.toDict())) + data = project_script_model( + script_contract_type_from_runtime(type(config).__name__), + await config.toDict(), + ) except Exception as e: return ScriptCreateOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", scriptId="", - data=GeneralConfig(**{}), + data=project_script_model( + script_contract_type_from_create(script.type), + {}, + ), ) return ScriptCreateOut(scriptId=str(uid), data=data) @@ -118,13 +116,8 @@ async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut: try: index, data = await Config.get_script(script.scriptId) - index = [ScriptIndexItem(**_) for _ in index] - data = { - uid: SCRIPT_BOOK[next((_.type for _ in index if _.uid == uid), "General")]( - **cfg - ) - for uid, cfg in data.items() - } + index = project_model_list(ScriptIndexItem, index) + data = project_script_model_map(index, data) except Exception as e: return ScriptGetOut( code=500, @@ -145,8 +138,11 @@ async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut: ) async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase: try: + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(script.scriptId)]).__name__ + ) await Config.update_script( - script.scriptId, script.data.model_dump(exclude_unset=True) + script.scriptId, validate_script_patch_data(script_type, script.data) ) except Exception as e: return OutBase( @@ -269,13 +265,8 @@ async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase: async def get_user(user: UserGetIn = Body(...)) -> UserGetOut: try: index, data = await Config.get_user(user.scriptId, user.userId) - index = [UserIndexItem(**_) for _ in index] - data = { - uid: USER_BOOK[ - type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__ - ](**cfg) - for uid, cfg in data.items() - } + index = project_model_list(UserIndexItem, index) + data = project_user_model_map(index, data) except Exception as e: return UserGetOut( code=500, @@ -295,18 +286,26 @@ async def get_user(user: UserGetIn = Body(...)) -> UserGetOut: status_code=200, ) async def add_user(user: UserInBase = Body(...)) -> UserCreateOut: + script_type = None try: uid, config = await Config.add_user(user.scriptId) - data = USER_BOOK[type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__]( - **(await config.toDict()) + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__ ) + user_type = user_contract_type_from_script(script_type) + data = project_user_model(user_type, await config.toDict()) except Exception as e: + user_type = ( + user_contract_type_from_script(script_type) + if script_type is not None + else "GeneralUserConfig" + ) return UserCreateOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", userId="", - data=GeneralUserConfig(**{}), + data=project_user_model(user_type, {}), ) return UserCreateOut(userId=str(uid), data=data) @@ -320,8 +319,14 @@ async def add_user(user: UserInBase = Body(...)) -> UserCreateOut: ) async def update_user(user: UserUpdateIn = Body(...)) -> OutBase: try: + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__ + ) + user_type = user_contract_type_from_script(script_type) await Config.update_user( - user.scriptId, user.userId, user.data.model_dump(exclude_unset=True) + user.scriptId, + user.userId, + validate_user_patch_data(user_type, user.data), ) except Exception as e: return OutBase( @@ -413,8 +418,8 @@ async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: index, data = await Config.get_webhook( webhook.scriptId, webhook.userId, webhook.webhookId ) - index = [WebhookIndexItem(**_) for _ in index] - data = {uid: Webhook(**cfg) for uid, cfg in data.items()} + index = project_model_list(WebhookIndexItem, index) + data = project_model_map(WebhookRead, data) except Exception as e: return WebhookGetOut( code=500, @@ -436,14 +441,14 @@ async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: async def add_webhook(webhook: WebhookInBase = Body(...)) -> WebhookCreateOut: try: uid, config = await Config.add_webhook(webhook.scriptId, webhook.userId) - data = Webhook(**(await config.toDict())) + data = project_model(WebhookRead, await config.toDict()) except Exception as e: return WebhookCreateOut( code=500, status="error", message=f"{type(e).__name__}: {str(e)}", webhookId="", - data=Webhook(**{}), + data=WebhookRead(), ) return WebhookCreateOut(webhookId=str(uid), data=data) diff --git a/app/api/setting.py b/app/api/setting.py index 6c5ba00f..db186df4 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -24,22 +24,28 @@ from fastapi import APIRouter, Body from app.core import Config from app.services import Notify -from app.models.dto import ( - SettingGetOut, - GlobalConfig, +from app.models.common_contract import ( OutBase, + project_model, + project_model_list, + project_model_map, +) +from app.models.setting_contract import ( + GlobalConfigRead, + SettingGetOut, SettingUpdateIn, - WebhookGetOut, - WebhookIndexItem, - Webhook, - WebhookGetIn, WebhookCreateOut, - WebhookUpdateIn, WebhookDeleteIn, + WebhookGetIn, + WebhookGetOut, + WebhookIndexItem, + WebhookRead, WebhookReorderIn, WebhookTestIn, + WebhookUpdateIn, ) from app.models import Webhook as WebhookConfig +from app.api.common import error_out router = APIRouter(prefix="/api/setting", tags=["全局设置"]) @@ -57,13 +63,8 @@ async def get_scripts() -> SettingGetOut: try: data = await Config.get_setting() except Exception as e: - return SettingGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - data=GlobalConfig(**{}), - ) - return SettingGetOut(data=GlobalConfig(**data)) + return error_out(SettingGetOut, e, data=GlobalConfigRead()) + return SettingGetOut(data=project_model(GlobalConfigRead, data)) @router.post( @@ -81,9 +82,7 @@ async def update_script(script: SettingUpdateIn = Body(...)) -> OutBase: await Config.update_setting(data) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -100,9 +99,7 @@ async def test_notify() -> OutBase: try: await Notify.send_test_notification() except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -116,16 +113,10 @@ async def test_notify() -> OutBase: async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: try: index, data = await Config.get_webhook(None, None, webhook.webhookId) - index = [WebhookIndexItem(**_) for _ in index] - data = {uid: Webhook(**cfg) for uid, cfg in data.items()} + index = project_model_list(WebhookIndexItem, index) + data = project_model_map(WebhookRead, data) except Exception as e: - return WebhookGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) + return error_out(WebhookGetOut, e, index=[], data={}) return WebhookGetOut(index=index, data=data) @@ -139,15 +130,9 @@ async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: async def add_webhook() -> WebhookCreateOut: try: uid, config = await Config.add_webhook(None, None) - data = Webhook(**(await config.toDict())) + data = project_model(WebhookRead, await config.toDict()) except Exception as e: - return WebhookCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - webhookId="", - data=Webhook(**{}), - ) + return error_out(WebhookCreateOut, e, webhookId="", data=WebhookRead()) return WebhookCreateOut(webhookId=str(uid), data=data) @@ -164,9 +149,7 @@ async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase: 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 error_out(OutBase, e) return OutBase() @@ -181,9 +164,7 @@ 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 error_out(OutBase, e) return OutBase() @@ -198,9 +179,7 @@ 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 error_out(OutBase, e) return OutBase() @@ -216,12 +195,12 @@ async def test_webhook(webhook: WebhookTestIn = Body(...)) -> OutBase: try: webhook_config = WebhookConfig() - await webhook_config.load(webhook.data.model_dump()) + await webhook_config.load(webhook.data.model_dump(exclude_unset=True)) await Notify.WebhookPush( "AUTO-MAS Webhook测试", "这是一条测试消息,如果您收到此消息,说明Webhook配置正确!", webhook_config, ) except Exception as e: - return OutBase(code=500, status="error", message=f"Webhook测试失败: {str(e)}") + return error_out(OutBase, e, message=f"Webhook测试失败: {str(e)}") return OutBase() diff --git a/app/api/tools.py b/app/api/tools.py index aeb42008..183cfab8 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -23,7 +23,9 @@ from fastapi import APIRouter, Body from app.core import Config -from app.models.dto import ToolsGetOut, ToolsConfig, OutBase, ToolsUpdateIn +from app.models.common_contract import OutBase, project_model +from app.models.tools_contract import ToolsConfigRead, ToolsGetOut, ToolsUpdateIn +from app.api.common import error_out router = APIRouter(prefix="/api/tools", tags=["工具设置"]) @@ -41,13 +43,8 @@ async def get_tools() -> ToolsGetOut: try: data = await Config.get_tools() except Exception as e: - return ToolsGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - data=ToolsConfig(**{}), - ) - return ToolsGetOut(data=ToolsConfig(**data)) + return error_out(ToolsGetOut, e, data=ToolsConfigRead()) + return ToolsGetOut(data=project_model(ToolsConfigRead, data)) @router.post( @@ -65,7 +62,5 @@ async def update_tools(script: ToolsUpdateIn = Body(...)) -> OutBase: await Config.update_tools(data) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() diff --git a/app/api/update.py b/app/api/update.py index bae4aea0..a2d5b6ee 100644 --- a/app/api/update.py +++ b/app/api/update.py @@ -26,7 +26,8 @@ from app.core import Config from app.services import Updater -from app.models.dto import OutBase, UpdateCheckIn, UpdateCheckOut +from app.models.common_contract import OutBase +from app.models.update_contract import UpdateCheckIn, UpdateCheckOut router = APIRouter(prefix="/api/update", tags=["软件更新"]) diff --git a/app/api/ws_command.py b/app/api/ws_command.py index b93828bd..13d407f4 100644 --- a/app/api/ws_command.py +++ b/app/api/ws_command.py @@ -28,20 +28,26 @@ """ import inspect -from typing import Callable, Dict, Any, Optional from functools import wraps +from typing import Any, Callable, ParamSpec, TypeAlias, TypeVar, cast from pydantic import BaseModel from app.utils.logger import get_logger logger = get_logger("WS命令") +P = ParamSpec("P") +R = TypeVar("R") +RegisteredWsCommand: TypeAlias = Callable[..., Any] + # 全局命令注册表 -_ws_command_registry: Dict[str, Callable] = {} +_ws_command_registry: dict[str, RegisteredWsCommand] = {} -def ws_command(endpoint: str): +def ws_command( + endpoint: str, +) -> Callable[[Callable[P, Any]], Callable[P, Any]]: """ WebSocket 命令装饰器 @@ -68,24 +74,24 @@ async def clone_task(params: TaskCloneIn): endpoint: 命令的唯一标识符,如 "ws.clone", "core.shutdown" """ - def decorator(func: Callable): + def decorator(func: Callable[P, Any]) -> Callable[P, Any]: # 注册到全局命令表 _ws_command_registry[endpoint] = func logger.debug(f"已注册 WebSocket 命令: {endpoint}") @wraps(func) - async def wrapper(*args, **kwargs): + async def wrapper(*args: P.args, **kwargs: P.kwargs): # 保持原函数功能不变 return await func(*args, **kwargs) - return wrapper + return cast(Callable[P, Any], wrapper) return decorator async def execute_ws_command( - endpoint: str, params: Optional[Dict[str, Any]] = None -) -> Dict[str, Any]: + endpoint: str, params: dict[str, Any] | None = None +) -> dict[str, Any]: """ 执行 WebSocket 命令 @@ -120,7 +126,7 @@ async def execute_ws_command( result = await func() else: # 检查第一个参数是否是 Pydantic Model - first_param = list(parameters.values())[0] + first_param = next(iter(parameters.values())) param_type = first_param.annotation if ( @@ -156,11 +162,12 @@ async def execute_ws_command( "message": result_dict.get("message"), } elif isinstance(result, dict): + result_dict = cast(dict[str, Any], result) return { - "success": result.get("code", 200) == 200, - "data": result, - "code": result.get("code", 200), - "message": result.get("message"), + "success": result_dict.get("code", 200) == 200, + "data": result_dict, + "code": result_dict.get("code", 200), + "message": result_dict.get("message"), } else: return {"success": True, "data": result, "code": 200} @@ -176,7 +183,7 @@ async def execute_ws_command( } -def get_ws_command_registry() -> Dict[str, Callable]: +def get_ws_command_registry() -> dict[str, RegisteredWsCommand]: """获取所有已注册的 WebSocket 命令""" return _ws_command_registry.copy() diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index b96af949..7d3af4ae 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -34,7 +34,7 @@ from app.utils.websocket import ws_client_manager from app.api.ws_command import list_ws_commands from app.utils.logger import get_logger -from app.models.dto import ( +from app.models.ws_contract import ( WSClientCreateIn, WSClientCreateOut, WSClientConnectIn, diff --git a/app/core/config/__init__.py b/app/core/config/__init__.py index 5a0f2554..7adcd81f 100644 --- a/app/core/config/__init__.py +++ b/app/core/config/__init__.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from .base import ( MultipleConfig, MultipleConfigAddEvent, @@ -6,7 +8,6 @@ dump_toml, ) from .fields import RefField, VirtualField -from .manager import AppConfig, Config from .pydantic import PydanticConfigBase from .types import ( EncryptedString, @@ -21,6 +22,17 @@ decrypt_encrypted_string, ) +if TYPE_CHECKING: + from .manager import AppConfig, Config + + +def __getattr__(name: str): + if name in {"AppConfig", "Config"}: + from .manager import AppConfig, Config + + return {"AppConfig": AppConfig, "Config": Config}[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + __all__ = [ "MultipleConfig", "MultipleConfigAddEvent", diff --git a/app/core/config/base.py b/app/core/config/base.py index 85a51497..b79a8951 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -33,7 +33,7 @@ from dataclasses import dataclass from pathlib import Path from collections.abc import AsyncIterator, Callable, Coroutine -from typing import Any, Generic, Protocol, TypeVar +from typing import Any, Generic, Protocol, TypeVar, cast PRIMARY_CONFIG_SUFFIX = ".toml" @@ -70,13 +70,15 @@ def _toml_inline(value: Any) -> str: """序列化内联 TOML 值,支持数组和内联表。""" if isinstance(value, dict): + mapping = cast(dict[object, Any], value) items = ", ".join( f"{_toml_key(str(key))} = {_toml_inline(item)}" - for key, item in value.items() + for key, item in mapping.items() ) return "{ " + items + " }" if isinstance(value, list): - return "[" + ", ".join(_toml_inline(item) for item in value) + "]" + items = cast(list[Any], value) + return "[" + ", ".join(_toml_inline(item) for item in items) + "]" return _toml_scalar(value) @@ -91,7 +93,7 @@ def emit_table(path: tuple[str, ...], obj: dict[str, Any]) -> None: for key, value in obj.items(): if isinstance(value, dict): - children.append((str(key), value)) + children.append((str(key), cast(dict[str, Any], value))) else: scalars.append((str(key), value)) @@ -120,7 +122,11 @@ def _load_json_config(path: Path) -> dict[str, Any]: return {} loaded = json.loads(raw_text) - return loaded if isinstance(loaded, dict) else {} + if not isinstance(loaded, dict): + return {} + + mapping = cast(dict[object, Any], loaded) + return {str(key): item for key, item in mapping.items()} def _load_toml_config(path: Path) -> dict[str, Any]: @@ -129,7 +135,8 @@ def _load_toml_config(path: Path) -> dict[str, Any]: return {} loaded = tomllib.loads(raw_text) - return loaded if isinstance(loaded, dict) else {} + mapping = cast(dict[object, Any], loaded) + return {str(key): item for key, item in mapping.items()} def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path | None]: @@ -200,6 +207,10 @@ async def lock(self) -> None: ... async def unlock(self) -> None: ... + def bind_owner_collection( + self, collection: "MultipleConfig[Any]", uid: uuid.UUID + ) -> None: ... + T = TypeVar("T", bound=_ConfigLike) @@ -407,13 +418,15 @@ async def load(self, data: dict[str, Any]) -> None: instances = data.get("instances") if not isinstance(instances, list): return + instances_list = cast(list[object], instances) - for instance in instances: + for instance in instances_list: if not isinstance(instance, dict): continue + instance_dict = cast(dict[object, Any], instance) - uid_str = instance.get("uid") - type_name = instance.get("type") + uid_str = instance_dict.get("uid") + type_name = instance_dict.get("type") if not isinstance(uid_str, str) or not isinstance(type_name, str): continue if type_name not in self.sub_config_type: @@ -422,6 +435,7 @@ async def load(self, data: dict[str, Any]) -> None: instance_data = data.get(uid_str) if not isinstance(instance_data, dict): continue + instance_data_dict = cast(dict[str, Any], instance_data) try: uid = uuid.UUID(uid_str) @@ -429,11 +443,10 @@ async def load(self, data: dict[str, Any]) -> None: continue config = self.sub_config_type[type_name]() - if hasattr(config, "_bind_owner_collection"): - config._bind_owner_collection(self, uid) + config.bind_owner_collection(self, uid) self.order.append(uid) self.data[uid] = config - await config.load(instance_data) + await config.load(instance_data_dict) if self.file: await self.save() @@ -506,8 +519,7 @@ async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: uid = uuid.uuid4() config = config_type() - if hasattr(config, "_bind_owner_collection"): - config._bind_owner_collection(self, uid) + config.bind_owner_collection(self, uid) self.order.append(uid) self.data[uid] = config diff --git a/app/core/config/manager.py b/app/core/config/manager.py index 3354354c..c00e2d58 100644 --- a/app/core/config/manager.py +++ b/app/core/config/manager.py @@ -58,7 +58,7 @@ EmulatorConfig, ) from .base import dump_toml -from app.models.dto import WebSocketMessage +from app.models.shared import WebSocketMessage from app.utils.constants import ( UTC4, UTC8, diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index 15651f7d..3c67f836 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -19,7 +19,7 @@ load_config_with_legacy_migration, ) from .fields import RefField, VirtualField -from .types import _EncryptedFieldMarker, decrypt_encrypted_string +from .types import EncryptedFieldMarker, decrypt_encrypted_string SaveMethod = Callable[[], Coroutine[Any, Any, None]] @@ -34,6 +34,14 @@ def _default_bindings() -> dict[tuple[str, str], list[Slot]]: return {} +def _default_pending_bindings() -> dict[tuple[str, str], Any]: + return {} + + +def _default_registered_ref_targets() -> set[str]: + return set() + + def _normalize_mapping(value: Any) -> dict[str, Any]: """将任意映射值规范化为 `dict[str, Any]`。""" @@ -65,7 +73,7 @@ def _get_field_marker( def _is_encrypted_field(group_model: BaseModel, field_name: str) -> bool: """判断字段是否为对外需要自动解密的字符串字段。""" - return _get_field_marker(group_model, field_name, _EncryptedFieldMarker) is not None + return _get_field_marker(group_model, field_name, EncryptedFieldMarker) is not None def _export_group_model( @@ -81,7 +89,7 @@ def _export_group_model( for field_name in type(group_model).model_fields: virtual_field = _get_field_marker(group_model, field_name, VirtualField) if virtual_field is not None: - data[field_name] = owner._get_virtual_value( + data[field_name] = owner.get_virtual_value( group_name, field_name, virtual_field ) continue @@ -107,6 +115,7 @@ class PydanticConfigBase(BaseModel): model_config = ConfigDict(extra="allow", validate_assignment=True) LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = {} + related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} _file: Path | None = PrivateAttr(default=None) _is_locked: bool = PrivateAttr(default=False) _save_methods: list[SaveMethod] = PrivateAttr(default_factory=_default_save_methods) @@ -116,8 +125,12 @@ class PydanticConfigBase(BaseModel): _transaction_depth: int = PrivateAttr(default=0) _pending_save: bool = PrivateAttr(default=False) _pending_sync: bool = PrivateAttr(default=False) - _pending_bindings: dict[tuple[str, str], Any] = PrivateAttr(default_factory=dict) - _registered_ref_targets: set[str] = PrivateAttr(default_factory=set) + _pending_bindings: dict[tuple[str, str], Any] = PrivateAttr( + default_factory=_default_pending_bindings + ) + _registered_ref_targets: set[str] = PrivateAttr( + default_factory=_default_registered_ref_targets + ) _owner_collection: MultipleConfig[Any] | None = PrivateAttr(default=None) _owner_uid: uuid.UUID | None = PrivateAttr(default=None) @@ -161,16 +174,21 @@ def _bind_owner_collection( self._owner_collection = collection self._owner_uid = uid + def bind_owner_collection( + self, collection: MultipleConfig[Any], uid: uuid.UUID + ) -> None: + """公开所属容器绑定入口,供 `MultipleConfig` 调用。""" + + self._bind_owner_collection(collection, uid) + def _resolve_related_collection(self, target: str) -> MultipleConfig[Any] | None: value = getattr(self, target, None) if isinstance(value, MultipleConfig): - return value + return cast(MultipleConfig[Any], value) - related = getattr(type(self), "related_config", None) - if isinstance(related, dict): - target_collection = related.get(target) - if isinstance(target_collection, MultipleConfig): - return target_collection + target_collection = type(self).related_config.get(target) + if isinstance(target_collection, MultipleConfig): + return target_collection return None @@ -220,6 +238,13 @@ def _get_virtual_value( return result + def get_virtual_value( + self, group: str, name: str, spec: VirtualField | None = None + ) -> Any: + """公开只读虚拟值访问入口,便于模块内辅助函数调用。""" + + return self._get_virtual_value(group, name, spec) + async def _set_virtual_value(self, group: str, name: str, value: Any) -> None: spec = self._get_virtual_field(group, name) if spec is None: @@ -279,7 +304,7 @@ def _register_ref_bindings(self) -> None: async def _on_related_config_deleted( self, event: MultipleConfigDeleteEvent[Any] ) -> None: - matched_fields = [] + matched_fields: list[tuple[str, str, RefField]] = [] for group_name, field_name, ref_field in self._iter_ref_fields(): target_collection = self._resolve_related_collection(ref_field.target) diff --git a/app/core/config/types.py b/app/core/config/types.py index f0066f9e..37f5ea43 100644 --- a/app/core/config/types.py +++ b/app/core/config/types.py @@ -12,7 +12,7 @@ from app.utils.security import dpapi_decrypt, dpapi_encrypt -class _EncryptedFieldMarker: +class EncryptedFieldMarker: """标记需要在对外读取时自动解密的字段。""" @@ -122,7 +122,7 @@ def decrypt_encrypted_string(value: str) -> str: KeyboardKeyString = Annotated[str, AfterValidator(_validate_keyboard_key)] EncryptedString = Annotated[ str, - _EncryptedFieldMarker(), + EncryptedFieldMarker(), AfterValidator(_normalize_encrypted_string), ] @@ -137,5 +137,6 @@ def decrypt_encrypted_string(value: str) -> str: "UrlString", "KeyboardKeyString", "EncryptedString", + "EncryptedFieldMarker", "decrypt_encrypted_string", ] diff --git a/app/core/emulator_manager.py b/app/core/emulator_manager.py index 5f702df2..ac3d4645 100644 --- a/app/core/emulator_manager.py +++ b/app/core/emulator_manager.py @@ -30,7 +30,7 @@ from .config import Config from app.models import EmulatorConfig from app.models.emulator import DeviceBase -from app.models.dto import DeviceInfo as SchemaDeviceInfo +from app.models.shared import DeviceInfo as SchemaDeviceInfo from app.utils import ProcessRunner, EMULATOR_TYPE_BOOK from app.utils.constants import EMULATOR_SPLASH_ADS_PATH_BOOK @@ -85,8 +85,6 @@ async def operate_emulator_task( ): try: temp_emulator = await self.get_emulator_instance(emulator_id) - if temp_emulator is None: - raise KeyError(f"未找到UUID为 {emulator_id} 的模拟器配置") if operate == "open": await temp_emulator.open(index) @@ -109,13 +107,13 @@ async def get_status( else: emulator_range = [emulator_id] - data = {} + data: Dict[str, Dict[str, SchemaDeviceInfo]] = {} for emulator_id in emulator_range: temp_emulator = await self.get_emulator_instance(emulator_id) emulator_device_info = await temp_emulator.getInfo(None) # 转换 EmulatorDeviceInfo 到 SchemaDeviceInfo - converted_devices = {} + converted_devices: Dict[str, SchemaDeviceInfo] = {} for device_index, device_info in emulator_device_info.items(): converted_devices[device_index] = SchemaDeviceInfo( title=device_info.title, diff --git a/app/core/task_manager.py b/app/core/task_manager.py index d5a85847..9c015806 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -196,7 +196,7 @@ async def add_task( self, mode: Literal["AutoProxy", "ManualReview", "ScriptConfig"], id: str, - new_task_info: dict | None = None, + new_task_info: dict[str, str] | None = None, ) -> uuid.UUID: """ 添加任务, 根据 id 值搜索实际指向的任务配置 diff --git a/app/models/__init__.py b/app/models/__init__.py index c1d072b5..182b2893 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,4 @@ -from . import dto, emulator, task +from . import emulator, task from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook from .general import GeneralConfig, GeneralUserConfig from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig @@ -24,7 +24,6 @@ "ToolsConfig", "GlobalConfig", "CLASS_BOOK", - "dto", "emulator", "task", ] diff --git a/app/models/common_contract.py b/app/models/common_contract.py new file mode 100644 index 00000000..85a8ccd8 --- /dev/null +++ b/app/models/common_contract.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from types import UnionType +from typing import Any, TypeAlias, TypeVar, Union, cast, get_args, get_origin + +from pydantic import BaseModel, ConfigDict, Field + + +ModelT = TypeVar("ModelT", bound=BaseModel) +MapKeyT = TypeVar("MapKeyT") +RawModelSource: TypeAlias = Mapping[str, Any] | BaseModel + + +class ApiModel(BaseModel): + """API Contract 的统一基线。""" + + model_config = ConfigDict(extra="forbid", validate_assignment=True) + + +class OutBase(ApiModel): + code: int = Field(default=200, description="状态码") + status: str = Field(default="success", description="操作状态") + message: str = Field(default="操作成功", description="操作消息") + + +class InfoOut(OutBase): + data: dict[str, Any] = Field(..., description="收到的服务器数据") + + +class ComboBoxItem(ApiModel): + label: str = Field(..., description="展示值") + value: str | None = Field(..., description="实际值") + + +class ComboBoxOut(OutBase): + data: list[ComboBoxItem] = Field(..., description="下拉框选项") + + +def _normalize_source(raw: RawModelSource | None) -> dict[str, Any]: + if raw is None: + return {} + if isinstance(raw, BaseModel): + return raw.model_dump() + return dict(raw) + + +def _project_value(annotation: Any, value: Any) -> Any: + if value is None: + return None + + origin = get_origin(annotation) + + if origin is None: + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + return _project_model_data(annotation, value) + return value + + if origin in (Union, UnionType): + for candidate in (arg for arg in get_args(annotation) if arg is not type(None)): + try: + projected = _project_value(candidate, value) + if isinstance(candidate, type) and issubclass(candidate, BaseModel): + candidate.model_validate(projected) + return projected + except Exception: + continue + return value + + if origin in (list, set, frozenset): + item_annotation = get_args(annotation)[0] if get_args(annotation) else Any + if not isinstance(value, (list, tuple, set, frozenset)): + return value + + iterable_value = cast(list[Any] | tuple[Any, ...] | set[Any] | frozenset[Any], value) + items = [_project_value(item_annotation, item) for item in iterable_value] + if origin is set: + return set(items) + if origin is frozenset: + return frozenset(items) + return items + + if origin is tuple: + if not isinstance(value, (list, tuple)): + return value + + tuple_value = cast(list[Any] | tuple[Any, ...], value) + item_annotations = get_args(annotation) + if len(item_annotations) == 2 and item_annotations[1] is Ellipsis: + return tuple(_project_value(item_annotations[0], item) for item in tuple_value) + + return tuple( + _project_value(item_annotation, item) + for item_annotation, item in zip(item_annotations, tuple_value) + ) + + if origin in (dict, Mapping): + key_annotation, value_annotation = ( + get_args(annotation) if get_args(annotation) else (Any, Any) + ) + if not isinstance(value, Mapping): + return value + + mapping_value = cast(Mapping[Any, Any], value) + return { + _project_value(key_annotation, key): _project_value( + value_annotation, item + ) + for key, item in mapping_value.items() + } + + return value + + +def _project_model_data( + model_cls: type[BaseModel], + raw: RawModelSource | None, + include_keys: Iterable[str] | None = None, +) -> dict[str, Any]: + source = _normalize_source(raw) + field_names = include_keys or model_cls.model_fields.keys() + + projected: dict[str, Any] = {} + for name in field_names: + field = model_cls.model_fields.get(name) + if field is None or name not in source: + continue + projected[name] = _project_value(field.annotation, source[name]) + + return projected + + +def project_model( + model_cls: type[ModelT], + raw: RawModelSource | None, + include_keys: Iterable[str] | None = None, +) -> ModelT: + """把运行期字典投影为声明过字段的 Contract 模型。""" + + return model_cls.model_validate(_project_model_data(model_cls, raw, include_keys)) + + +def project_model_list( + model_cls: type[ModelT], + raw_list: Iterable[RawModelSource] | None, + include_keys: Iterable[str] | None = None, +) -> list[ModelT]: + if raw_list is None: + return [] + + projected_items: list[ModelT] = [] + for raw_item in raw_list: + projected_items.append( + project_model(model_cls, raw_item, include_keys=include_keys) + ) + return projected_items + + +def project_model_map( + model_cls: type[ModelT], + raw_map: Mapping[MapKeyT, RawModelSource] | None, + include_keys: Iterable[str] | None = None, +) -> dict[MapKeyT, ModelT]: + if raw_map is None: + return {} + + projected_map: dict[MapKeyT, ModelT] = {} + for key, raw_item in raw_map.items(): + projected_map[key] = project_model( + model_cls, raw_item, include_keys=include_keys + ) + return projected_map + + +__all__ = [ + "ApiModel", + "OutBase", + "InfoOut", + "ComboBoxItem", + "ComboBoxOut", + "project_model", + "project_model_list", + "project_model_map", +] diff --git a/app/models/dispatch_contract.py b/app/models/dispatch_contract.py new file mode 100644 index 00000000..d7daf40c --- /dev/null +++ b/app/models/dispatch_contract.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class DispatchIn(ApiModel): + taskId: str = Field( + ..., + description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID", + ) + + +class TaskCreateIn(DispatchIn): + mode: Literal["AutoProxy", "ManualReview", "ScriptConfig"] = Field( + ..., description="任务模式" + ) + + +class TaskCreateOut(OutBase): + taskId: str = Field(..., description="新创建的任务ID") + + +class PowerIn(ApiModel): + signal: Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] = Field(..., description="电源操作信号") + + +class PowerOut(OutBase): + signal: Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] = Field(..., description="电源操作信号") + + +__all__ = ["DispatchIn", "TaskCreateIn", "TaskCreateOut", "PowerIn", "PowerOut"] diff --git a/app/models/dto.py b/app/models/dto.py deleted file mode 100644 index 6875eff8..00000000 --- a/app/models/dto.py +++ /dev/null @@ -1,1462 +0,0 @@ -# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software -# Copyright © 2024-2025 DLmaster361 -# Copyright © 2025 MoeSnowyFox -# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with AUTO-MAS. If not, see . - -# Contact: DLmaster_361@163.com - - -# pyright: reportUnknownVariableType=false, reportGeneralTypeIssues=false -from pydantic import BaseModel, Field -from typing import Any, Dict, List, Union, Optional, Literal - - -class OutBase(BaseModel): - code: int = Field(default=200, description="状态码") - status: str = Field(default="success", description="操作状态") - message: str = Field(default="操作成功", description="操作消息") - - -class InfoOut(OutBase): - data: Dict[str, Any] = Field(..., description="收到的服务器数据") - - -class VersionOut(OutBase): - if_need_update: bool = Field(..., description="后端代码是否需要更新") - current_time: str = Field(..., description="后端代码当前时间戳") - current_hash: str = Field(..., description="后端代码当前哈希值") - - -class NoticeOut(OutBase): - if_need_show: bool = Field(..., description="是否需要显示公告") - data: Dict[str, str] = Field( - ..., description="公告信息, key为公告标题, value为公告内容" - ) - - -class TagItem(BaseModel): - text: str = Field(..., description="标签文本") - color: Literal[ - "red", - "blue", - "green", - "yellow", - "orange", - "purple", - "pink", - "brown", - "black", - "white", - "gray", - "silver", - "gold", - ] = Field(..., description="标签颜色") - - -class ComboBoxItem(BaseModel): - label: str = Field(..., description="展示值") - value: Optional[str] = Field(..., description="实际值") - - -class ComboBoxOut(OutBase): - data: List[ComboBoxItem] = Field(..., description="下拉框选项") - - -class GetStageIn(BaseModel): - type: Literal[ - "User", - "Today", - "ALL", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] = Field( - ..., - description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项", - ) - - -class EmulatorConfigIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["EmulatorConfig"] = Field(..., description="配置类型") - - -class EmulatorConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="模拟器名称") - Type: Optional[Literal["general", "mumu", "ldplayer"]] = Field( - default=None, description="模拟器类型" - ) - Path: Optional[str] = Field(default=None, description="模拟器路径") - BossKey: Optional[str] = Field(default=None, description="老板键快捷键配置") - MaxWaitTime: Optional[int] = Field(default=None, description="最大等待时间(秒)") - - -class EmulatorConfig(BaseModel): - Info: Optional[EmulatorConfig_Info] = Field( - default=None, description="模拟器基础信息" - ) - - -class ToolsConfig_ArknightsPC(BaseModel): - Enabled: bool | None = Field(default=None, description="是否启用 ArknightsPC 工具") - PauseKey: str | None = Field(default=None, description="暂停键位") - SelectDeployedKey: str | None = Field( - default=None, description="选中已部署干员键位" - ) - UseSkillKey: str | None = Field(default=None, description="释放技能键位") - RetreatKey: str | None = Field(default=None, description="撤退键位") - NextFrameKey: str | None = Field(default=None, description="下一帧键位") - AnotherQuitKey: str | None = Field(default=None, description="自定义退出、暂停键位") - Status: str | None = Field(default=None, description="工具状态 Tag") - - -class ToolsConfig(BaseModel): - ArknightsPC: ToolsConfig_ArknightsPC | None = Field( - default=None, description="明日方舟PC工具配置" - ) - - -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[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表示永久保存" - ) - IfAllowSleep: Optional[bool] = Field(default=None, description="允许休眠") - IfSilence: Optional[bool] = Field(default=None, description="静默模式") - IfAgreeBilibili: Optional[bool] = Field( - default=None, description="同意哔哩哔哩用户协议" - ) - IfBlockAd: Optional[bool] = Field(default=None, description="屏蔽模拟器广告") - - -class GlobalConfig_Voice(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="语音功能是否启用") - Type: Optional[Literal["simple", "noisy"]] = Field( - default=None, description="语音类型, simple为简洁, noisy为聒噪" - ) - - -class GlobalConfig_Start(BaseModel): - IfSelfStart: Optional[bool] = Field( - default=None, description="是否在系统启动时自动运行" - ) - IfMinimizeDirectly: Optional[bool] = Field( - default=None, description="启动时是否直接最小化到托盘而不显示主窗口" - ) - - -class GlobalConfig_UI(BaseModel): - IfShowTray: Optional[bool] = Field(default=None, description="是否常态显示托盘图标") - IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘") - - -class GlobalConfig_Notify(BaseModel): - SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field( - default=None, description="任务结果推送时机" - ) - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendSixStar: Optional[bool] = Field( - default=None, description="是否发送公招六星通知" - ) - IfPushPlyer: Optional[bool] = Field(default=None, description="是否推送系统通知") - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") - IfKoishiSupport: Optional[bool] = Field( - default=None, description="是否启用Koishi支持" - ) - KoishiServerAddress: Optional[str] = Field( - default=None, description="Koishi服务器地址" - ) - KoishiToken: Optional[str] = Field(default=None, description="Koishi Token") - SMTPServerAddress: Optional[str] = Field(default=None, description="SMTP服务器地址") - AuthorizationCode: Optional[str] = Field(default=None, description="SMTP授权码") - FromAddress: Optional[str] = Field(default=None, description="邮件发送地址") - ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") - IfServerChan: Optional[bool] = Field( - default=None, description="是否使用ServerChan推送" - ) - ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥") - - -class GlobalConfig_Update(BaseModel): - IfAutoUpdate: Optional[bool] = Field(default=None, description="是否自动更新") - Source: Optional[Literal["GitHub", "MirrorChyan", "AutoSite"]] = Field( - default=None, description="更新源: GitHub源, Mirror酱源, 自建源" - ) - Channel: Optional[Literal["stable", "beta"]] = Field( - default=None, description="更新渠道: 稳定版, 测试版" - ) - ProxyAddress: Optional[str] = Field(default=None, description="网络代理地址") - MirrorChyanCDK: Optional[str] = Field(default=None, description="Mirror酱CDK") - - -class GlobalConfig(BaseModel): - Function: Optional[GlobalConfig_Function] = Field( - default=None, description="功能相关配置" - ) - Voice: Optional[GlobalConfig_Voice] = Field( - default=None, description="语音相关配置" - ) - Start: Optional[GlobalConfig_Start] = Field( - default=None, description="启动相关配置" - ) - UI: Optional[GlobalConfig_UI] = Field(default=None, description="界面相关配置") - Notify: Optional[GlobalConfig_Notify] = Field( - default=None, description="通知相关配置" - ) - Update: Optional[GlobalConfig_Update] = Field( - default=None, description="更新相关配置" - ) - - -class QueueIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["QueueConfig"] = Field(..., description="配置类型") - - -class QueueItemIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["QueueItem"] = Field(..., description="配置类型") - - -class TimeSetIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["TimeSet"] = Field(..., description="配置类型") - - -class QueueItem_Info(BaseModel): - ScriptId: Optional[str] = Field( - default=None, description="任务所对应的脚本ID, 为None时表示未选择" - ) - - -class QueueItem(BaseModel): - Info: Optional[QueueItem_Info] = Field(default=None, description="队列项") - - -class TimeSet_Info(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用") - Days: Optional[ - List[ - Literal[ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] - ] - ] = Field(default=None, description="执行周期, 可多选") - Time: Optional[str] = Field(default=None, description="时间设置, 格式为HH:MM") - - -class TimeSet(BaseModel): - Info: Optional[TimeSet_Info] = Field(default=None, description="时间项") - - -class QueueConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="队列名称") - TimeEnabled: Optional[bool] = Field(default=None, description="是否启用定时") - StartUpEnabled: Optional[bool] = Field(default=None, description="是否启动时运行") - AfterAccomplish: Optional[ - Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] - ] = Field(default=None, description="完成后操作") - - -class QueueConfig(BaseModel): - Info: Optional[QueueConfig_Info] = Field(default=None, description="队列信息") - - -class ScriptIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["MaaConfig", "GeneralConfig", "SrcConfig", "MaaEndConfig"] = Field( - ..., description="配置类型" - ) - - -class UserIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal[ - "MaaUserConfig", "GeneralUserConfig", "SrcUserConfig", "MaaEndUserConfig" - ] = Field(..., description="配置类型") - - -class MaaUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名") - Id: Optional[str] = Field(default=None, description="用户ID") - Mode: Optional[Literal["简洁", "详细"]] = Field( - default=None, description="用户配置模式" - ) - StageMode: Optional[str] = Field(default=None, description="关卡配置模式") - Server: Optional[ - Literal["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"] - ] = Field(default=None, description="服务器") - Status: Optional[bool] = Field(default=None, description="用户状态") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - Annihilation: Optional[ - Literal[ - "Close", - "Annihilation", - "Chernobog@Annihilation", - "LungmenOutskirts@Annihilation", - "LungmenDowntown@Annihilation", - ] - ] = Field(default=None, description="剿灭模式") - InfrastMode: Optional[Literal["Normal", "Rotation", "Custom"]] = Field( - default=None, description="基建模式" - ) - InfrastName: Optional[str] = Field(default=None, description="基建方案名称") - InfrastIndex: Optional[str] = Field(default=None, description="基建方案索引") - Password: Optional[str] = Field(default=None, description="密码") - Notes: Optional[str] = Field(default=None, description="备注") - MedicineNumb: Optional[int] = Field(default=None, description="吃理智药数量") - SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field( - default=None, description="连战次数" - ) - Stage: Optional[str] = Field(default=None, description="关卡选择") - Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1") - Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2") - Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3") - Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡") - IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到") - SklandToken: Optional[str] = Field(default=None, description="SklandToken") - Tag: Optional[str] = Field(default=None, description="状态标签列表") - - -class MaaUserConfig_Data(BaseModel): - IfPassCheck: Optional[bool] = Field(default=None, description="是否通过人工排查") - - -class MaaUserConfig_Task(BaseModel): - IfStartUp: Optional[bool] = Field(default=None, description="开始唤醒") - IfRecruit: Optional[bool] = Field(default=None, description="自动公招") - IfInfrast: Optional[bool] = Field(default=None, description="基建换班") - IfFight: Optional[bool] = Field(default=None, description="理智作战") - IfMall: Optional[bool] = Field(default=None, description="信用收支") - IfAward: Optional[bool] = Field(default=None, description="领取奖励") - IfRoguelike: Optional[bool] = Field(default=None, description="自动肉鸽") - IfReclamation: Optional[bool] = Field(default=None, description="生息演算") - - -class MaaUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendSixStar: Optional[bool] = Field(default=None, description="是否发送高资喜报") - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") - ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") - IfServerChan: Optional[bool] = Field( - default=None, description="是否使用Server酱推送" - ) - ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey") - - -class GeneralUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知") - ToAddress: Optional[str] = Field(default=None, description="邮件接收地址") - IfServerChan: Optional[bool] = Field( - default=None, description="是否使用Server酱推送" - ) - ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey") - - -class MaaUserConfig(BaseModel): - Info: Optional[MaaUserConfig_Info] = Field(default=None, description="基础信息") - Data: Optional[MaaUserConfig_Data] = Field(default=None, description="用户数据") - Task: Optional[MaaUserConfig_Task] = Field(default=None, description="任务列表") - Notify: Optional[MaaUserConfig_Notify] = Field(default=None, description="单独通知") - - -class MaaConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="脚本名称") - Path: Optional[str] = Field(default=None, description="脚本路径") - - -class MaaConfig_Emulator(BaseModel): - Id: Optional[str] = Field(default=None, description="模拟器ID") - Index: Optional[str] = Field(default=None, description="模拟器多开实例索引") - - -class MaaConfig_Run(BaseModel): - TaskTransitionMethod: Optional[Literal["NoAction", "ExitGame", "ExitEmulator"]] = ( - Field(default=None, description="简洁任务间切换方式") - ) - ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") - AnnihilationTimeLimit: Optional[int] = Field( - default=None, description="剿灭超时限制" - ) - RoutineTimeLimit: Optional[int] = Field(default=None, description="日常超时限制") - AnnihilationAvoidWaste: Optional[bool] = Field( - default=None, description="剿灭避免无代理卡浪费理智" - ) - - -class MaaConfig(BaseModel): - Info: Optional[MaaConfig_Info] = Field(default=None, description="脚本基础信息") - Emulator: Optional[MaaConfig_Emulator] = Field( - default=None, description="模拟器配置" - ) - Run: Optional[MaaConfig_Run] = Field(default=None, description="脚本运行配置") - - -class GeneralUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名") - Status: Optional[bool] = Field(default=None, description="用户状态") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - IfScriptBeforeTask: Optional[bool] = Field( - default=None, description="是否在任务前执行脚本" - ) - ScriptBeforeTask: Optional[str] = Field(default=None, description="任务前脚本路径") - IfScriptAfterTask: Optional[bool] = Field( - default=None, description="是否在任务后执行脚本" - ) - ScriptAfterTask: Optional[str] = Field(default=None, description="任务后脚本路径") - Notes: Optional[str] = Field(default=None, description="备注") - Tag: Optional[str] = Field( - default=None, description="用户标签列表(JSON字符串,TagItem的dict列表)" - ) - - -class GeneralUserConfig_Data(BaseModel): - LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期") - ProxyTimes: Optional[int] = Field(default=None, description="代理次数") - - -class GeneralUserConfig(BaseModel): - Info: Optional[GeneralUserConfig_Info] = Field(default=None, description="用户信息") - Data: Optional[GeneralUserConfig_Data] = Field(default=None, description="用户数据") - Notify: Optional[GeneralUserConfig_Notify] = Field( - default=None, description="单独通知" - ) - - -class GeneralConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="脚本名称") - RootPath: Optional[str] = Field(default=None, description="脚本根目录") - - -class GeneralConfig_Script(BaseModel): - ScriptPath: Optional[str] = Field(default=None, description="脚本可执行文件路径") - Arguments: Optional[str] = Field(default=None, description="脚本启动附加命令参数") - IfTrackProcess: Optional[bool] = Field( - default=None, description="是否追踪脚本子进程" - ) - TrackProcessName: Optional[str] = Field(default=None, description="追踪进程名称") - TrackProcessExe: Optional[str] = Field(default=None, description="追踪进程文件路径") - TrackProcessCmdline: Optional[str] = Field( - default=None, description="追踪进程启动命令行参数" - ) - ConfigPath: Optional[str] = Field(default=None, description="配置文件路径") - ConfigPathMode: Optional[Literal["File", "Folder"]] = Field( - default=None, description="配置文件类型: 单个文件, 文件夹" - ) - UpdateConfigMode: Optional[Literal["Never", "Success", "Failure", "Always"]] = ( - Field( - default=None, - description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时", - ) - ) - LogPath: Optional[str] = Field(default=None, description="日志文件路径") - LogPathFormat: Optional[str] = Field(default=None, description="日志文件名格式") - LogTimeStart: Optional[int] = Field(default=None, description="日志时间戳开始位置") - LogTimeEnd: Optional[int] = Field(default=None, description="日志时间戳结束位置") - LogTimeFormat: Optional[str] = Field(default=None, description="日志时间戳格式") - SuccessLog: Optional[str] = Field(default=None, description="成功时日志") - ErrorLog: Optional[str] = Field(default=None, description="错误时日志") - - -class GeneralConfig_Game(BaseModel): - Enabled: Optional[bool] = Field( - default=None, description="游戏/模拟器相关功能是否启用" - ) - Type: Optional[Literal["Emulator", "Client", "URL"]] = Field( - default=None, description="类型: 模拟器, PC端, URL协议" - ) - Path: Optional[str] = Field(default=None, description="游戏/模拟器程序路径") - URL: Optional[str] = Field(default=None, description="自定义协议URL") - ProcessName: Optional[str] = Field(default=None, description="游戏进程名称") - Arguments: Optional[str] = Field(default=None, description="游戏/模拟器启动参数") - WaitTime: Optional[int] = Field(default=None, description="游戏/模拟器等待启动时间") - IfForceClose: Optional[bool] = Field( - default=None, description="是否强制关闭游戏/模拟器进程" - ) - EmulatorId: Optional[str] = Field(default=None, description="模拟器ID") - EmulatorIndex: Optional[str] = Field(default=None, description="模拟器多开实例索引") - - -class GeneralConfig_Run(BaseModel): - ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") - RunTimeLimit: Optional[int] = Field(default=None, description="日志超时限制") - - -class GeneralConfig(BaseModel): - Info: Optional[GeneralConfig_Info] = Field(default=None, description="脚本基础信息") - Script: Optional[GeneralConfig_Script] = Field(default=None, description="脚本配置") - Game: Optional[GeneralConfig_Game] = Field(default=None, description="游戏配置") - Run: Optional[GeneralConfig_Run] = Field(default=None, description="运行配置") - - -class MaaEndUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名") - Status: Optional[bool] = Field(default=None, description="用户状态") - Id: Optional[str] = Field(default=None, description="用户ID") - Password: Optional[str] = Field(default=None, description="密码") - Mode: Optional[Literal["简洁", "详细"]] = Field( - default=None, description="配置模式" - ) - Resource: Optional[Literal["官服"]] = Field(default=None, description="资源名称") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - Notes: Optional[str] = Field(default=None, description="备注") - IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到") - SklandToken: Optional[str] = Field(default=None, description="SklandToken") - Tag: Optional[str] = Field(default=None, description="用户标签信息") - - -class MaaEndUserConfig_Task(BaseModel): - ProtocolSpaceTab: Optional[ - Literal["OperatorProgression", "WeaponProgression", "CrisisDrills"] - ] = Field(default=None, description="协议空间选项卡") - OperatorProgression: Optional[ - Literal["OperatorEXP", "Promotions", "T-Creds", "SkillUp"] - ] = Field(default=None, description="干员养成任务") - WeaponProgression: Optional[Literal["WeaponEXP", "WeaponTune"]] = Field( - default=None, description="武器养成任务" - ) - CrisisDrills: Optional[ - Literal[ - "AdvancedProgression1", - "AdvancedProgression2", - "AdvancedProgression3", - "AdvancedProgression4", - "AdvancedProgression5", - ] - ] = Field(default=None, description="危境预演任务") - RewardsSetOption: Optional[Literal["RewardsSetA", "RewardsSetB"]] = Field( - default=None, description="奖励套组选项" - ) - - -class MaaEndUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件") - ToAddress: Optional[str] = Field(default=None, description="收件地址") - IfServerChan: Optional[bool] = Field(default=None, description="是否启用Server酱") - ServerChanKey: Optional[str] = Field(default=None, description="Server酱密钥") - - -class MaaEndUserConfig(BaseModel): - Info: Optional[MaaEndUserConfig_Info] = Field(default=None, description="用户信息") - Task: Optional[MaaEndUserConfig_Task] = Field(default=None, description="任务配置") - Notify: Optional[MaaEndUserConfig_Notify] = Field( - default=None, description="通知配置" - ) - - -class MaaEndConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="脚本名称") - Path: Optional[str] = Field(default=None, description="脚本路径") - - -class MaaEndConfig_Run(BaseModel): - RunTimeLimit: Optional[int] = Field( - default=None, description="运行时间限制(分钟)" - ) - ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制") - - -class MaaEndConfig_Game(BaseModel): - ControllerType: Optional[ - Literal["Win32-Window", "Win32-Window-Background", "Win32-Front", "ADB"] - ] = Field(default=None, description="控制器类型") - Path: Optional[str] = Field(default=None, description="终末地客户端路径") - Arguments: Optional[str] = Field(default=None, description="游戏启动参数") - WaitTime: Optional[int] = Field(default=None, description="游戏等待时间") - EmulatorId: Optional[str] = Field(default=None, description="模拟器ID") - EmulatorIndex: Optional[str] = Field(default=None, description="模拟器索引") - CloseOnFinish: Optional[bool] = Field(default=None, description="结束后关闭游戏") - - -class MaaEndConfig(BaseModel): - Info: Optional[MaaEndConfig_Info] = Field(default=None, description="脚本信息") - Run: Optional[MaaEndConfig_Run] = Field(default=None, description="运行配置") - Game: Optional[MaaEndConfig_Game] = Field(default=None, description="游戏配置") - - -class SrcUserConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="用户名称") - Status: Optional[bool] = Field(default=None, description="是否启用") - Id: Optional[str] = Field(default=None, description="用户ID") - Password: Optional[str] = Field(default=None, description="密码") - Mode: Optional[Literal["简洁", "详细"]] = Field( - default=None, description="脚本模式" - ) - Server: Optional[ - Literal[ - "CN-Official", - "CN-Bilibili", - "VN-Official", - "OVERSEA-America", - "OVERSEA-Asia", - "OVERSEA-Europe", - "OVERSEA-TWHKMO", - ] - ] = Field(default=None, description="游戏服务器") - RemainedDay: Optional[int] = Field(default=None, description="剩余天数") - Notes: Optional[str] = Field(default=None, description="备注") - Tag: Optional[str] = Field(default=None, description="用户标签信息") - - -class SrcUserConfig_Stage(BaseModel): - Channel: Literal["Relic", "Materials", "Ornament"] | None = Field( - default=None, description="关卡通道" - ) - Relic: ( - Literal[ - "-", - "Cavern_of_Corrosion_Path_of_Possession", - "Cavern_of_Corrosion_Path_of_Hidden_Salvation", - "Cavern_of_Corrosion_Path_of_Thundersurge", - "Cavern_of_Corrosion_Path_of_Aria", - "Cavern_of_Corrosion_Path_of_Uncertainty", - "Cavern_of_Corrosion_Path_of_Cavalier", - "Cavern_of_Corrosion_Path_of_Dreamdive", - "Cavern_of_Corrosion_Path_of_Darkness", - "Cavern_of_Corrosion_Path_of_Elixir_Seekers", - "Cavern_of_Corrosion_Path_of_Conflagration", - "Cavern_of_Corrosion_Path_of_Holy_Hymn", - "Cavern_of_Corrosion_Path_of_Providence", - "Cavern_of_Corrosion_Path_of_Drifting", - "Cavern_of_Corrosion_Path_of_Jabbing_Punch", - "Cavern_of_Corrosion_Path_of_Gelid_Wind", - ] - | None - ) = Field(default=None, description="遗器关卡") - Materials: ( - Literal[ - "-", - "Calyx_Golden_Memories_Planarcadia", - "Calyx_Golden_Aether_Planarcadia", - "Calyx_Golden_Treasures_Planarcadia", - "Calyx_Golden_Memories_Amphoreus", - "Calyx_Golden_Aether_Amphoreus", - "Calyx_Golden_Treasures_Amphoreus", - "Calyx_Golden_Memories_Penacony", - "Calyx_Golden_Aether_Penacony", - "Calyx_Golden_Treasures_Penacony", - "Calyx_Golden_Memories_The_Xianzhou_Luofu", - "Calyx_Golden_Aether_The_Xianzhou_Luofu", - "Calyx_Golden_Treasures_The_Xianzhou_Luofu", - "Calyx_Golden_Memories_Jarilo_VI", - "Calyx_Golden_Aether_Jarilo_VI", - "Calyx_Golden_Treasures_Jarilo_VI", - "Calyx_Crimson_Destruction_Herta_StorageZone", - "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", - "Calyx_Crimson_Preservation_Herta_SupplyZone", - "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", - "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", - "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", - "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", - "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", - "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", - "Calyx_Crimson_Erudition_Jarilo_RivetTown", - "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", - "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", - "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", - "Calyx_Crimson_Nihility_Jarilo_GreatMine", - "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", - "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", - "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", - "Stagnant_Shadow_Quanta", - "Stagnant_Shadow_Gust", - "Stagnant_Shadow_Fulmination", - "Stagnant_Shadow_Blaze", - "Stagnant_Shadow_Spike", - "Stagnant_Shadow_Rime", - "Stagnant_Shadow_Mirage", - "Stagnant_Shadow_Icicle", - "Stagnant_Shadow_Doom", - "Stagnant_Shadow_Puppetry", - "Stagnant_Shadow_Abomination", - "Stagnant_Shadow_Scorch", - "Stagnant_Shadow_Celestial", - "Stagnant_Shadow_Perdition", - "Stagnant_Shadow_Nectar", - "Stagnant_Shadow_Roast", - "Stagnant_Shadow_Ire", - "Stagnant_Shadow_Duty", - "Stagnant_Shadow_Timbre", - "Stagnant_Shadow_Mechwolf", - "Stagnant_Shadow_Gloam", - "Stagnant_Shadow_Sloggyre", - "Stagnant_Shadow_Gelidmoon", - "Stagnant_Shadow_Deepsheaf", - "Stagnant_Shadow_Cinders", - "Stagnant_Shadow_Sirens", - "Stagnant_Shadow_Ashes", - "Stagnant_Shadow_Soundburst", - ] - | None - ) = Field(default=None, description="材料关卡") - Ornament: ( - Literal[ - "-", - "Divergent_Universe_Within_the_West_Wind", - "Divergent_Universe_Moonlit_Blood", - "Divergent_Universe_Unceasing_Strife", - "Divergent_Universe_Famished_Worker", - "Divergent_Universe_Eternal_Comedy", - "Divergent_Universe_To_Sweet_Dreams", - "Divergent_Universe_Pouring_Blades", - "Divergent_Universe_Fruit_of_Evil", - "Divergent_Universe_Permafrost", - "Divergent_Universe_Gentle_Words", - "Divergent_Universe_Smelted_Heart", - "Divergent_Universe_Untoppled_Walls", - ] - | None - ) = Field(default=None, description="饰品关卡") - ExtractReservedTrailblazePower: Optional[bool] = Field( - default=None, description="使用储备开拓力" - ) - UseFuel: Optional[bool] = Field(default=None, description="使用燃料") - FuelReserve: Optional[int] = Field(default=None, description="保留的燃料数量") - EchoOfWar: Optional[str] = Field(default=None, description="历战余响关卡") - SimulatedUniverseWorld: Optional[str] = Field( - default=None, description="模拟宇宙关卡" - ) - - -class SrcUserConfig_Data(BaseModel): - LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期") - ProxyTimes: Optional[int] = Field(default=None, description="代理次数") - IfPassCheck: Optional[bool] = Field(default=None, description="是否通过检查") - - -class SrcUserConfig_Notify(BaseModel): - Enabled: Optional[bool] = Field(default=None, description="是否启用通知") - IfSendStatistic: Optional[bool] = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件") - ToAddress: Optional[str] = Field(default=None, description="收件地址") - IfServerChan: Optional[bool] = Field(default=None, description="是否启用Server酱") - ServerChanKey: Optional[str] = Field(default=None, description="Server酱密钥") - - -class SrcUserConfig(BaseModel): - Info: Optional[SrcUserConfig_Info] = Field(default=None, description="基础信息") - Stage: Optional[SrcUserConfig_Stage] = Field(default=None, description="关卡配置") - Data: Optional[SrcUserConfig_Data] = Field(default=None, description="用户数据") - Notify: Optional[SrcUserConfig_Notify] = Field(default=None, description="单独通知") - - -class SrcConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="SRC脚本名称") - Path: Optional[str] = Field(default=None, description="SRC路径") - - -class SrcConfig_Emulator(BaseModel): - Id: Optional[str] = Field(default=None, description="模拟器ID") - Index: Optional[str] = Field(default=None, description="模拟器索引") - - -class SrcConfig_Run(BaseModel): - TaskTransitionMethod: Optional[Literal["ExitGame", "ExitEmulator"]] = Field( - default=None, description="任务切换方式" - ) - ProxyTimesLimit: Optional[int] = Field(default=None, description="代理次数限制") - RunTimesLimit: Optional[int] = Field(default=None, description="运行次数限制") - RunTimeLimit: Optional[int] = Field( - default=None, description="运行时间限制(分钟)" - ) - - -class SrcConfig(BaseModel): - Info: Optional[SrcConfig_Info] = Field(default=None, description="脚本基础信息") - Emulator: Optional[SrcConfig_Emulator] = Field( - default=None, description="模拟器配置" - ) - Run: Optional[SrcConfig_Run] = Field(default=None, description="脚本运行配置") - - -class PlanIndexItem(BaseModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["MaaPlanConfig"] = Field(..., description="配置类型") - - -class MaaPlanConfig_Info(BaseModel): - Name: Optional[str] = Field(default=None, description="计划表名称") - Mode: Optional[Literal["ALL", "Weekly"]] = Field( - default=None, description="计划表模式" - ) - - -class MaaPlanConfig_Item(BaseModel): - MedicineNumb: Optional[int] = Field(default=None, description="吃理智药") - SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field( - None, description="连战次数" - ) - Stage: Optional[str] = Field(default=None, description="关卡选择") - Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1") - Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2") - Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3") - Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡") - - -class MaaPlanConfig(BaseModel): - Info: Optional[MaaPlanConfig_Info] = Field(default=None, description="基础信息") - ALL: Optional[MaaPlanConfig_Item] = Field(default=None, description="全局") - Monday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周一") - Tuesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周二") - Wednesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周三") - Thursday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周四") - Friday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周五") - Saturday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周六") - Sunday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周日") - - -class HistoryIndexItem(BaseModel): - date: str = Field(..., description="日期") - status: Literal["DONE", "ERROR"] = Field(..., description="状态") - jsonFile: str = Field(..., description="对应JSON文件") - - -class HistoryData(BaseModel): - index: Optional[List[HistoryIndexItem]] = Field( - default=None, description="历史记录索引列表" - ) - recruit_statistics: Optional[Dict[str, int]] = Field( - default=None, description="公招统计数据, key为星级, value为对应的公招数量" - ) - drop_statistics: Optional[Dict[str, Dict[str, int]]] = Field( - default=None, - description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }", - ) - error_info: Optional[Dict[str, str]] = Field( - default=None, description="报错信息, key为时间戳, value为错误描述" - ) - log_content: Optional[str] = Field( - default=None, description="日志内容, 仅在提取单条历史记录数据时返回" - ) - - -class ScriptCreateIn(BaseModel): - type: Literal["MAA", "SRC", "General", "MaaEnd"] = Field( - ..., description="脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本" - ) - scriptId: str | None = Field( - default=None, description="直接从该脚本ID复制创建, 仅在复制创建时使用" - ) - - -class ScriptCreateOut(OutBase): - scriptId: str = Field(..., description="新创建的脚本ID") - data: Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig] = Field( - ..., description="脚本配置数据" - ) - - -class ScriptGetIn(BaseModel): - scriptId: Optional[str] = Field( - default=None, description="脚本ID, 未携带时表示获取所有脚本数据" - ) - - -class ScriptGetOut(OutBase): - index: List[ScriptIndexItem] = Field(..., description="脚本索引列表") - data: Dict[str, Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig]] = Field( - ..., description="脚本数据字典, key来自于index列表的uid" - ) - - -class ScriptUpdateIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - data: Union[MaaConfig, SrcConfig, GeneralConfig, MaaEndConfig] = Field( - ..., description="脚本更新数据" - ) - - -class ScriptDeleteIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - - -class ScriptReorderIn(BaseModel): - indexList: List[str] = Field(..., description="脚本ID列表, 按新顺序排列") - - -class ScriptFileIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - jsonFile: str = Field(..., description="配置文件路径") - - -class ScriptUrlIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - url: str = Field(..., description="配置文件URL") - - -class ScriptUploadIn(BaseModel): - scriptId: str = Field(..., description="脚本ID") - config_name: str = Field(..., description="配置名称") - author: str = Field(..., description="作者") - description: str = Field(..., description="描述") - - -class UserInBase(BaseModel): - scriptId: str = Field(..., description="所属脚本ID") - - -class UserGetIn(UserInBase): - userId: Optional[str] = Field( - default=None, description="用户ID, 未携带时表示获取所有用户数据" - ) - - -class UserGetOut(OutBase): - index: List[UserIndexItem] = Field(..., description="用户索引列表") - data: Dict[ - str, Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] - ] = Field(..., description="用户数据字典, key来自于index列表的uid") - - -class UserCreateOut(OutBase): - userId: str = Field(..., description="新创建的用户ID") - data: Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] = ( - Field(..., description="用户配置数据") - ) - - -class UserUpdateIn(UserInBase): - userId: str = Field(..., description="用户ID") - data: Union[MaaUserConfig, SrcUserConfig, GeneralUserConfig, MaaEndUserConfig] = ( - Field(..., description="用户更新数据") - ) - - -class UserDeleteIn(UserInBase): - userId: str = Field(..., description="用户ID") - - -class UserReorderIn(UserInBase): - indexList: List[str] = Field(..., description="用户ID列表, 按新顺序排列") - - -class UserSetIn(UserInBase): - userId: str = Field(..., description="用户ID") - jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件") - - -class EmulatorGetIn(BaseModel): - emulatorId: Optional[str] = Field( - default=None, description="模拟器ID, 未携带时表示获取所有模拟器数据" - ) - - -class EmulatorGetOut(OutBase): - index: List[EmulatorConfigIndexItem] = Field(..., description="模拟器索引列表") - data: Dict[str, EmulatorConfig] = Field( - ..., description="模拟器数据字典, key来自于index列表的uid" - ) - - -class EmulatorCreateOut(OutBase): - emulatorId: str = Field(..., description="新创建的模拟器 ID") - data: EmulatorConfig = Field(..., description="模拟器配置数据") - - -class EmulatorUpdateIn(BaseModel): - emulatorId: str = Field(..., description="模拟器 ID") - data: EmulatorConfig = Field(..., description="模拟器更新数据") - - -class EmulatorDeleteIn(BaseModel): - emulatorId: str = Field(..., description="模拟器 ID") - - -class EmulatorReorderIn(BaseModel): - indexList: List[str] = Field(..., description="模拟器 ID列表, 按新顺序排列") - - -class EmulatorOperateIn(BaseModel): - emulatorId: str = Field(..., description="模拟器 ID") - operate: Literal["open", "close", "show"] = Field(..., description="操作类型") - index: str = Field(..., description="模拟器索引") - - -class DeviceStatus(BaseModel): - """设备状态枚举""" - - ONLINE: int = Field(default=0, description="设备在线") - OFFLINE: int = Field(default=1, description="设备离线") - STARTING: int = Field(default=2, description="设备开启中") - CLOSEING: int = Field(default=3, description="设备关闭中") - ERROR: int = Field(default=4, description="错误") - NOT_FOUND: int = Field(default=5, description="未找到设备") - UNKNOWN: int = Field(default=10, description="未知状态") - - -class DeviceInfo(BaseModel): - """设备信息""" - - title: str = Field(..., description="设备标题/名称") - status: int = Field(..., description="设备状态, 参考DeviceStatus枚举值") - adb_address: str = Field(..., description="ADB连接地址") - - -class EmulatorStatusOut(OutBase): - data: Dict[str, Dict[str, DeviceInfo]] = Field( - ..., - description="模拟器状态信息, 外层key为模拟器ID, 内层key为设备索引, value为设备信息", - ) - - -class EmulatorSearchResult(BaseModel): - type: str = Field(..., description="模拟器类型") - path: str = Field(..., description="模拟器路径") - name: str = Field(..., description="模拟器名称") - - -class EmulatorSearchOut(OutBase): - emulators: list[EmulatorSearchResult] = Field( - default_factory=list, description="搜索到的模拟器列表" - ) - - -class WebhookInBase(BaseModel): - scriptId: Optional[str] = Field( - default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带" - ) - userId: Optional[str] = Field( - default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带" - ) - - -class WebhookGetIn(WebhookInBase): - webhookId: Optional[str] = Field( - default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据" - ) - - -class WebhookGetOut(OutBase): - index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表") - data: Dict[str, Webhook] = Field( - ..., description="Webhook数据字典, key来自于index列表的uid" - ) - - -class WebhookCreateOut(OutBase): - webhookId: str = Field(..., description="新创建的Webhook ID") - data: Webhook = Field(..., description="Webhook配置数据") - - -class WebhookUpdateIn(WebhookInBase): - webhookId: str = Field(..., description="Webhook ID") - data: Webhook = Field(..., description="Webhook更新数据") - - -class WebhookDeleteIn(WebhookInBase): - webhookId: str = Field(..., description="Webhook ID") - - -class WebhookReorderIn(WebhookInBase): - indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列") - - -class WebhookTestIn(WebhookInBase): - data: Webhook = Field(..., description="Webhook配置数据") - - -class PlanCreateIn(BaseModel): - type: Literal["MaaPlan"] - - -class PlanCreateOut(OutBase): - planId: str = Field(..., description="新创建的计划ID") - data: MaaPlanConfig = Field(..., description="计划配置数据") - - -class PlanGetIn(BaseModel): - planId: Optional[str] = Field( - default=None, description="计划ID, 未携带时表示获取所有计划数据" - ) - - -class PlanGetOut(OutBase): - index: List[PlanIndexItem] = Field(..., description="计划索引列表") - data: Dict[str, MaaPlanConfig] = Field(..., description="计划列表或单个计划数据") - - -class PlanUpdateIn(BaseModel): - planId: str = Field(..., description="计划ID") - data: MaaPlanConfig = Field(..., description="计划更新数据") - - -class PlanDeleteIn(BaseModel): - planId: str = Field(..., description="计划ID") - - -class PlanReorderIn(BaseModel): - indexList: List[str] = Field(..., description="计划ID列表, 按新顺序排列") - - -class QueueCreateOut(OutBase): - queueId: str = Field(..., description="新创建的队列ID") - data: QueueConfig = Field(..., description="队列配置数据") - - -class QueueGetIn(BaseModel): - queueId: Optional[str] = Field( - default=None, description="队列ID, 未携带时表示获取所有队列数据" - ) - - -class QueueGetOut(OutBase): - index: List[QueueIndexItem] = Field(..., description="队列索引列表") - data: Dict[str, QueueConfig] = Field( - ..., description="队列数据字典, key来自于index列表的uid" - ) - - -class QueueUpdateIn(BaseModel): - queueId: str = Field(..., description="队列ID") - data: QueueConfig = Field(..., description="队列更新数据") - - -class QueueDeleteIn(BaseModel): - queueId: str = Field(..., description="队列ID") - - -class QueueReorderIn(BaseModel): - indexList: List[str] = Field(..., description="按新顺序排列的调度队列UID列表") - - -class QueueSetInBase(BaseModel): - queueId: str = Field(..., description="所属队列ID") - - -class TimeSetGetIn(QueueSetInBase): - timeSetId: Optional[str] = Field( - default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据" - ) - - -class TimeSetGetOut(OutBase): - index: List[TimeSetIndexItem] = Field(..., description="时间设置索引列表") - data: Dict[str, TimeSet] = Field( - ..., description="时间设置数据字典, key来自于index列表的uid" - ) - - -class TimeSetCreateOut(OutBase): - timeSetId: str = Field(..., description="新创建的时间设置ID") - data: TimeSet = Field(..., description="时间设置配置数据") - - -class TimeSetUpdateIn(QueueSetInBase): - timeSetId: str = Field(..., description="时间设置ID") - data: TimeSet = Field(..., description="时间设置更新数据") - - -class TimeSetDeleteIn(QueueSetInBase): - timeSetId: str = Field(..., description="时间设置ID") - - -class TimeSetReorderIn(QueueSetInBase): - indexList: List[str] = Field(..., description="时间设置ID列表, 按新顺序排列") - - -class QueueItemGetIn(QueueSetInBase): - queueItemId: Optional[str] = Field( - default=None, description="队列项ID, 未携带时表示获取所有队列项数据" - ) - - -class QueueItemGetOut(OutBase): - index: List[QueueItemIndexItem] = Field(..., description="队列项索引列表") - data: Dict[str, QueueItem] = Field( - ..., description="队列项数据字典, key来自于index列表的uid" - ) - - -class QueueItemCreateOut(OutBase): - queueItemId: str = Field(..., description="新创建的队列项ID") - data: QueueItem = Field(..., description="队列项配置数据") - - -class QueueItemUpdateIn(QueueSetInBase): - queueItemId: str = Field(..., description="队列项ID") - data: QueueItem = Field(..., description="队列项更新数据") - - -class QueueItemDeleteIn(QueueSetInBase): - queueItemId: str = Field(..., description="队列项ID") - - -class QueueItemReorderIn(QueueSetInBase): - indexList: List[str] = Field(..., description="队列项ID列表, 按新顺序排列") - - -class DispatchIn(BaseModel): - taskId: str = Field( - ..., - description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID", - ) - - -class TaskCreateIn(DispatchIn): - mode: Literal["AutoProxy", "ManualReview", "ScriptConfig"] = Field( - ..., description="任务模式" - ) - - -class TaskCreateOut(OutBase): - taskId: str = Field(..., description="新创建的任务ID") - - -class WebSocketMessage(BaseModel): - id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程") - type: Literal["Update", "Message", "Info", "Signal"] = Field( - ..., - description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号", - ) - data: Dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定") - - -class PowerIn(BaseModel): - signal: Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] = Field(..., description="电源操作信号") - - -class PowerOut(OutBase): - signal: Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] = Field(..., description="电源操作信号") - - -class HistorySearchIn(BaseModel): - mode: Literal["DAILY", "WEEKLY", "MONTHLY"] = Field(..., description="合并模式") - start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD") - end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD") - - -class HistorySearchOut(OutBase): - data: Dict[str, Dict[str, HistoryData]] = Field( - ..., - description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }", - ) - - -class HistoryDataGetIn(BaseModel): - jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件") - - -class HistoryDataGetOut(OutBase): - data: HistoryData = Field(..., description="历史记录数据") - - -class ToolsGetOut(OutBase): - data: ToolsConfig = Field(..., description="工具配置数据") - - -class ToolsUpdateIn(BaseModel): - data: ToolsConfig = Field(..., description="工具配置需要更新的数据") - - -class SettingGetOut(OutBase): - data: GlobalConfig = Field(..., description="全局设置数据") - - -class SettingUpdateIn(BaseModel): - data: GlobalConfig = Field(..., description="全局设置需要更新的数据") - - -class UpdateCheckIn(BaseModel): - current_version: str = Field(..., description="当前前端版本号") - if_force: bool = Field(default=False, description="是否强制拉取更新信息") - - -class UpdateCheckOut(OutBase): - if_need_update: bool = Field(..., description="是否需要更新前端") - latest_version: str = Field(..., description="最新前端版本号") - update_info: Dict[str, List[str]] = Field(..., description="版本更新信息字典") - - -# ============== WebSocket 调试相关模型 ============== - - -class WSClientCreateIn(BaseModel): - """创建 WebSocket 客户端请求""" - - name: str = Field(..., description="客户端名称,用于标识") - url: str = Field( - ..., description="WebSocket 服务器地址,如 ws://localhost:5140/path" - ) - ping_interval: float = Field(default=15.0, description="心跳发送间隔(秒)") - ping_timeout: float = Field(default=30.0, description="心跳超时时间(秒)") - reconnect_interval: float = Field(default=5.0, description="重连间隔(秒)") - max_reconnect_attempts: int = Field( - default=-1, description="最大重连次数,-1为无限" - ) - - -class WSClientCreateOut(OutBase): - """创建客户端响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="返回数据") - - -class WSClientConnectIn(BaseModel): - """连接请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientDisconnectIn(BaseModel): - """断开连接请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientRemoveIn(BaseModel): - """删除客户端请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientSendIn(BaseModel): - """发送消息请求""" - - name: str = Field(..., description="客户端名称") - message: Dict[str, Any] = Field(..., description="要发送的 JSON 消息") - - -class WSClientSendJsonIn(BaseModel): - """发送自定义 JSON 消息请求""" - - name: str = Field(..., description="客户端名称") - msg_id: str = Field(default="Client", description="消息 ID") - msg_type: str = Field(..., description="消息类型") - data: Dict[str, Any] = Field(default_factory=dict, description="消息数据") - - -class WSClientAuthIn(BaseModel): - """发送认证请求""" - - name: str = Field(..., description="客户端名称") - token: str = Field(..., description="认证 Token") - auth_type: str = Field(default="auth", description="认证消息类型") - extra_data: Optional[Dict[str, Any]] = Field( - default=None, description="额外认证数据" - ) - - -class WSClientStatusIn(BaseModel): - """获取客户端状态请求""" - - name: str = Field(..., description="客户端名称") - - -class WSClientStatusOut(OutBase): - """客户端状态响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="状态数据") - - -class WSClientListOut(OutBase): - """客户端列表响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="客户端列表") - - -class WSMessageHistoryOut(OutBase): - """消息历史响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="消息历史") - - -class WSClearHistoryIn(BaseModel): - """清空消息历史请求""" - - name: Optional[str] = Field(default=None, description="客户端名称,为空则清空所有") - - -class WSCommandsOut(OutBase): - """可用命令列表响应""" - - data: Optional[Dict[str, Any]] = Field(default=None, description="命令列表") diff --git a/app/models/emulator_contract.py b/app/models/emulator_contract.py new file mode 100644 index 00000000..556f6697 --- /dev/null +++ b/app/models/emulator_contract.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase +from .shared import DeviceInfo + + +class EmulatorConfigIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["EmulatorConfig"] = Field(..., description="配置类型") + + +class EmulatorInfoRead(ApiModel): + Name: str = Field(default="新模拟器", description="模拟器名称") + Type: Literal["general", "mumu", "ldplayer"] = Field( + default="general", description="模拟器类型" + ) + Path: str = Field(default="", description="模拟器路径") + BossKey: str = Field(default="[ ]", description="老板键快捷键配置") + MaxWaitTime: int = Field(default=60, description="最大等待时间(秒)") + + +class EmulatorInfoPatch(ApiModel): + Name: str | None = Field(default=None, description="模拟器名称") + Type: Literal["general", "mumu", "ldplayer"] | None = Field( + default=None, description="模拟器类型" + ) + Path: str | None = Field(default=None, description="模拟器路径") + BossKey: str | None = Field(default=None, description="老板键快捷键配置") + MaxWaitTime: int | None = Field(default=None, description="最大等待时间(秒)") + + +class EmulatorRead(ApiModel): + Info: EmulatorInfoRead = Field( + default_factory=EmulatorInfoRead, description="模拟器基础信息" + ) + + +class EmulatorPatch(ApiModel): + Info: EmulatorInfoPatch | None = Field(default=None, description="模拟器基础信息") + + +class EmulatorGetIn(ApiModel): + emulatorId: str | None = Field( + default=None, description="模拟器ID, 未携带时表示获取所有模拟器数据" + ) + + +class EmulatorGetOut(OutBase): + index: list[EmulatorConfigIndexItem] = Field(..., description="模拟器索引列表") + data: dict[str, EmulatorRead] = Field( + ..., description="模拟器数据字典, key来自于index列表的uid" + ) + + +class EmulatorCreateOut(OutBase): + emulatorId: str = Field(..., description="新创建的模拟器 ID") + data: EmulatorRead = Field(..., description="模拟器配置数据") + + +class EmulatorUpdateIn(ApiModel): + emulatorId: str = Field(..., description="模拟器 ID") + data: EmulatorPatch = Field(..., description="模拟器更新数据") + + +class EmulatorDeleteIn(ApiModel): + emulatorId: str = Field(..., description="模拟器 ID") + + +class EmulatorReorderIn(ApiModel): + indexList: list[str] = Field(..., description="模拟器 ID列表, 按新顺序排列") + + +class EmulatorOperateIn(ApiModel): + emulatorId: str = Field(..., description="模拟器 ID") + operate: Literal["open", "close", "show"] = Field(..., description="操作类型") + index: str = Field(..., description="模拟器索引") + + +class EmulatorStatusOut(OutBase): + data: dict[str, dict[str, DeviceInfo]] = Field( + ..., + description="模拟器状态信息, 外层key为模拟器ID, 内层key为设备索引, value为设备信息", + ) + + +class EmulatorSearchResult(ApiModel): + type: str = Field(..., description="模拟器类型") + path: str = Field(..., description="模拟器路径") + name: str = Field(..., description="模拟器名称") + + +class EmulatorSearchOut(OutBase): + emulators: list[EmulatorSearchResult] = Field(..., description="搜索到的模拟器列表") + + +__all__ = [ + "EmulatorConfigIndexItem", + "EmulatorInfoRead", + "EmulatorInfoPatch", + "EmulatorRead", + "EmulatorPatch", + "EmulatorGetIn", + "EmulatorGetOut", + "EmulatorCreateOut", + "EmulatorUpdateIn", + "EmulatorDeleteIn", + "EmulatorReorderIn", + "EmulatorOperateIn", + "EmulatorStatusOut", + "EmulatorSearchResult", + "EmulatorSearchOut", +] diff --git a/app/models/general_contract.py b/app/models/general_contract.py new file mode 100644 index 00000000..f0e81a2a --- /dev/null +++ b/app/models/general_contract.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel + + +class GeneralUserConfigNotify(ApiModel): + Enabled: bool | None = Field(default=None, description="是否启用通知") + IfSendStatistic: bool | None = Field( + default=None, description="是否发送统计信息" + ) + IfSendMail: bool | None = Field(default=None, description="是否发送邮件通知") + ToAddress: str | None = Field(default=None, description="邮件接收地址") + IfServerChan: bool | None = Field( + default=None, description="是否使用Server酱推送" + ) + ServerChanKey: str | None = Field(default=None, description="ServerChanKey") + + +class GeneralUserConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="用户名") + Status: bool | None = Field(default=None, description="用户状态") + RemainedDay: int | None = Field(default=None, description="剩余天数") + IfScriptBeforeTask: bool | None = Field( + default=None, description="是否在任务前执行脚本" + ) + ScriptBeforeTask: str | None = Field(default=None, description="任务前脚本路径") + IfScriptAfterTask: bool | None = Field( + default=None, description="是否在任务后执行脚本" + ) + ScriptAfterTask: str | None = Field(default=None, description="任务后脚本路径") + Notes: str | None = Field(default=None, description="备注") + Tag: str | None = Field( + default=None, description="用户标签列表(JSON字符串,TagItem的dict列表)" + ) + + +class GeneralUserConfigData(ApiModel): + LastProxyDate: str | None = Field(default=None, description="上次代理日期") + ProxyTimes: int | None = Field(default=None, description="代理次数") + + +class GeneralUserConfig(ApiModel): + type: Literal["GeneralUserConfig"] = Field( + default="GeneralUserConfig", description="配置类型" + ) + Info: GeneralUserConfigInfo | None = Field(default=None, description="用户信息") + Data: GeneralUserConfigData | None = Field(default=None, description="用户数据") + Notify: GeneralUserConfigNotify | None = Field( + default=None, description="单独通知" + ) + + +class GeneralConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="脚本名称") + RootPath: str | None = Field(default=None, description="脚本根目录") + + +class GeneralConfigScript(ApiModel): + ScriptPath: str | None = Field(default=None, description="脚本可执行文件路径") + Arguments: str | None = Field(default=None, description="脚本启动附加命令参数") + IfTrackProcess: bool | None = Field( + default=None, description="是否追踪脚本子进程" + ) + TrackProcessName: str | None = Field(default=None, description="追踪进程名称") + TrackProcessExe: str | None = Field(default=None, description="追踪进程文件路径") + TrackProcessCmdline: str | None = Field( + default=None, description="追踪进程启动命令行参数" + ) + ConfigPath: str | None = Field(default=None, description="配置文件路径") + ConfigPathMode: Literal["File", "Folder"] | None = Field( + default=None, description="配置文件类型: 单个文件, 文件夹" + ) + UpdateConfigMode: Literal["Never", "Success", "Failure", "Always"] | None = Field( + default=None, + description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时", + ) + LogPath: str | None = Field(default=None, description="日志文件路径") + LogPathFormat: str | None = Field(default=None, description="日志文件名格式") + LogTimeStart: int | None = Field(default=None, description="日志时间戳开始位置") + LogTimeEnd: int | None = Field(default=None, description="日志时间戳结束位置") + LogTimeFormat: str | None = Field(default=None, description="日志时间戳格式") + SuccessLog: str | None = Field(default=None, description="成功时日志") + ErrorLog: str | None = Field(default=None, description="错误时日志") + + +class GeneralConfigGame(ApiModel): + Enabled: bool | None = Field(default=None, description="游戏/模拟器相关功能是否启用") + Type: Literal["Emulator", "Client", "URL"] | None = Field( + default=None, description="类型: 模拟器, PC端, URL协议" + ) + Path: str | None = Field(default=None, description="游戏/模拟器程序路径") + URL: str | None = Field(default=None, description="自定义协议URL") + ProcessName: str | None = Field(default=None, description="游戏进程名称") + Arguments: str | None = Field(default=None, description="游戏/模拟器启动参数") + WaitTime: int | None = Field(default=None, description="游戏/模拟器等待启动时间") + IfForceClose: bool | None = Field( + default=None, description="是否强制关闭游戏/模拟器进程" + ) + EmulatorId: str | None = Field(default=None, description="模拟器ID") + EmulatorIndex: str | None = Field(default=None, description="模拟器多开实例索引") + + +class GeneralConfigRun(ApiModel): + ProxyTimesLimit: int | None = Field(default=None, description="每日代理次数限制") + RunTimesLimit: int | None = Field(default=None, description="重试次数限制") + RunTimeLimit: int | None = Field(default=None, description="日志超时限制") + + +class GeneralConfig(ApiModel): + type: Literal["GeneralConfig"] = Field(default="GeneralConfig", description="配置类型") + Info: GeneralConfigInfo | None = Field(default=None, description="脚本基础信息") + Script: GeneralConfigScript | None = Field(default=None, description="脚本配置") + Game: GeneralConfigGame | None = Field(default=None, description="游戏配置") + Run: GeneralConfigRun | None = Field(default=None, description="运行配置") + + +__all__ = [ + "GeneralUserConfigNotify", + "GeneralUserConfigInfo", + "GeneralUserConfigData", + "GeneralUserConfig", + "GeneralConfigInfo", + "GeneralConfigScript", + "GeneralConfigGame", + "GeneralConfigRun", + "GeneralConfig", +] diff --git a/app/models/global_config.py b/app/models/global_config.py index 2d7db879..006286d0 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -20,7 +20,7 @@ YmdHmsString, ) from app.utils.constants import MATERIALS_MAP, RESOURCE_STAGE_INFO, UTC8 -from app.models.dto import TagItem +from app.models.shared import TagItem from .common import EmulatorConfig, QueueConfig, QueueItem, Webhook from .general import GeneralConfig from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig diff --git a/app/models/history_contract.py b/app/models/history_contract.py new file mode 100644 index 00000000..3f375666 --- /dev/null +++ b/app/models/history_contract.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class HistoryIndexItem(ApiModel): + date: str = Field(..., description="日期") + status: Literal["DONE", "ERROR"] = Field(..., description="状态") + jsonFile: str = Field(..., description="对应JSON文件") + + +class HistoryData(ApiModel): + index: list[HistoryIndexItem] | None = Field( + default=None, description="历史记录索引列表" + ) + recruit_statistics: dict[str, int] | None = Field( + default=None, description="公招统计数据, key为星级, value为对应的公招数量" + ) + drop_statistics: dict[str, dict[str, int]] | None = Field( + default=None, + description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }", + ) + error_info: dict[str, str] | None = Field( + default=None, description="报错信息, key为时间戳, value为错误描述" + ) + log_content: str | None = Field( + default=None, description="日志内容, 仅在提取单条历史记录数据时返回" + ) + + +class HistorySearchIn(ApiModel): + mode: Literal["DAILY", "WEEKLY", "MONTHLY"] = Field(..., description="合并模式") + start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD") + end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD") + + +class HistorySearchOut(OutBase): + data: dict[str, dict[str, HistoryData]] = Field( + ..., + description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }", + ) + + +class HistoryDataGetIn(ApiModel): + jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件") + + +class HistoryDataGetOut(OutBase): + data: HistoryData = Field(..., description="历史记录数据") + + +__all__ = [ + "HistoryIndexItem", + "HistoryData", + "HistorySearchIn", + "HistorySearchOut", + "HistoryDataGetIn", + "HistoryDataGetOut", +] diff --git a/app/models/info_contract.py b/app/models/info_contract.py new file mode 100644 index 00000000..6db1330b --- /dev/null +++ b/app/models/info_contract.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class VersionOut(OutBase): + if_need_update: bool = Field(..., description="后端代码是否需要更新") + current_time: str = Field(..., description="后端代码当前时间戳") + current_hash: str = Field(..., description="后端代码当前哈希值") + + +class NoticeOut(OutBase): + if_need_show: bool = Field(..., description="是否需要显示公告") + data: dict[str, str] = Field( + ..., description="公告信息, key为公告标题, value为公告内容" + ) + + +class GetStageIn(ApiModel): + type: Literal[ + "User", + "Today", + "ALL", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] = Field( + ..., + description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项", + ) + + +__all__ = ["VersionOut", "NoticeOut", "GetStageIn"] diff --git a/app/models/maa_contract.py b/app/models/maa_contract.py new file mode 100644 index 00000000..6556c24b --- /dev/null +++ b/app/models/maa_contract.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel + + +class MaaUserConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="用户名") + Id: str | None = Field(default=None, description="用户ID") + Mode: Literal["简洁", "详细"] | None = Field( + default=None, description="用户配置模式" + ) + StageMode: str | None = Field(default=None, description="关卡配置模式") + Server: Literal[ + "Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy" + ] | None = Field(default=None, description="服务器") + Status: bool | None = Field(default=None, description="用户状态") + RemainedDay: int | None = Field(default=None, description="剩余天数") + Annihilation: Literal[ + "Close", + "Annihilation", + "Chernobog@Annihilation", + "LungmenOutskirts@Annihilation", + "LungmenDowntown@Annihilation", + ] | None = Field(default=None, description="剿灭模式") + InfrastMode: Literal["Normal", "Rotation", "Custom"] | None = Field( + default=None, description="基建模式" + ) + InfrastName: str | None = Field(default=None, description="基建方案名称") + InfrastIndex: str | None = Field(default=None, description="基建方案索引") + Password: str | None = Field(default=None, description="密码") + Notes: str | None = Field(default=None, description="备注") + MedicineNumb: int | None = Field(default=None, description="吃理智药数量") + SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] | None = Field( + default=None, description="连战次数" + ) + Stage: str | None = Field(default=None, description="关卡选择") + Stage_1: str | None = Field(default=None, description="备选关卡 - 1") + Stage_2: str | None = Field(default=None, description="备选关卡 - 2") + Stage_3: str | None = Field(default=None, description="备选关卡 - 3") + Stage_Remain: str | None = Field(default=None, description="剩余理智关卡") + IfSkland: bool | None = Field(default=None, description="是否启用森空岛签到") + SklandToken: str | None = Field(default=None, description="SklandToken") + Tag: str | None = Field(default=None, description="状态标签列表") + + +class MaaUserConfigData(ApiModel): + IfPassCheck: bool | None = Field(default=None, description="是否通过人工排查") + + +class MaaUserConfigTask(ApiModel): + IfStartUp: bool | None = Field(default=None, description="开始唤醒") + IfRecruit: bool | None = Field(default=None, description="自动公招") + IfInfrast: bool | None = Field(default=None, description="基建换班") + IfFight: bool | None = Field(default=None, description="理智作战") + IfMall: bool | None = Field(default=None, description="信用收支") + IfAward: bool | None = Field(default=None, description="领取奖励") + IfRoguelike: bool | None = Field(default=None, description="自动肉鸽") + IfReclamation: bool | None = Field(default=None, description="生息演算") + + +class MaaUserConfigNotify(ApiModel): + Enabled: bool | None = Field(default=None, description="是否启用通知") + IfSendStatistic: bool | None = Field( + default=None, description="是否发送统计信息" + ) + IfSendSixStar: bool | None = Field(default=None, description="是否发送高资喜报") + IfSendMail: bool | None = Field(default=None, description="是否发送邮件通知") + ToAddress: str | None = Field(default=None, description="邮件接收地址") + IfServerChan: bool | None = Field( + default=None, description="是否使用Server酱推送" + ) + ServerChanKey: str | None = Field(default=None, description="ServerChanKey") + + +class MaaUserConfig(ApiModel): + type: Literal["MaaUserConfig"] = Field(default="MaaUserConfig", description="配置类型") + Info: MaaUserConfigInfo | None = Field(default=None, description="基础信息") + Data: MaaUserConfigData | None = Field(default=None, description="用户数据") + Task: MaaUserConfigTask | None = Field(default=None, description="任务列表") + Notify: MaaUserConfigNotify | None = Field(default=None, description="单独通知") + + +class MaaConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="脚本名称") + Path: str | None = Field(default=None, description="脚本路径") + + +class MaaConfigEmulator(ApiModel): + Id: str | None = Field(default=None, description="模拟器ID") + Index: str | None = Field(default=None, description="模拟器多开实例索引") + + +class MaaConfigRun(ApiModel): + TaskTransitionMethod: Literal["NoAction", "ExitGame", "ExitEmulator"] | None = ( + Field(default=None, description="简洁任务间切换方式") + ) + ProxyTimesLimit: int | None = Field(default=None, description="每日代理次数限制") + RunTimesLimit: int | None = Field(default=None, description="重试次数限制") + AnnihilationTimeLimit: int | None = Field(default=None, description="剿灭超时限制") + RoutineTimeLimit: int | None = Field(default=None, description="日常超时限制") + AnnihilationAvoidWaste: bool | None = Field( + default=None, description="剿灭避免无代理卡浪费理智" + ) + + +class MaaConfig(ApiModel): + type: Literal["MaaConfig"] = Field(default="MaaConfig", description="配置类型") + Info: MaaConfigInfo | None = Field(default=None, description="脚本基础信息") + Emulator: MaaConfigEmulator | None = Field(default=None, description="模拟器配置") + Run: MaaConfigRun | None = Field(default=None, description="脚本运行配置") + + +__all__ = [ + "MaaUserConfigInfo", + "MaaUserConfigData", + "MaaUserConfigTask", + "MaaUserConfigNotify", + "MaaUserConfig", + "MaaConfigInfo", + "MaaConfigEmulator", + "MaaConfigRun", + "MaaConfig", +] diff --git a/app/models/maaend_contract.py b/app/models/maaend_contract.py new file mode 100644 index 00000000..d2a1f2f7 --- /dev/null +++ b/app/models/maaend_contract.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel + + +class MaaEndUserConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="用户名") + Status: bool | None = Field(default=None, description="用户状态") + Id: str | None = Field(default=None, description="用户ID") + Password: str | None = Field(default=None, description="密码") + Mode: Literal["简洁", "详细"] | None = Field( + default=None, description="配置模式" + ) + Resource: Literal["官服"] | None = Field(default=None, description="资源名称") + RemainedDay: int | None = Field(default=None, description="剩余天数") + Notes: str | None = Field(default=None, description="备注") + IfSkland: bool | None = Field(default=None, description="是否启用森空岛签到") + SklandToken: str | None = Field(default=None, description="SklandToken") + Tag: str | None = Field(default=None, description="用户标签信息") + + +class MaaEndUserConfigTask(ApiModel): + ProtocolSpaceTab: Literal[ + "OperatorProgression", "WeaponProgression", "CrisisDrills" + ] | None = Field(default=None, description="协议空间选项卡") + OperatorProgression: Literal[ + "OperatorEXP", "Promotions", "T-Creds", "SkillUp" + ] | None = Field(default=None, description="干员养成任务") + WeaponProgression: Literal["WeaponEXP", "WeaponTune"] | None = Field( + default=None, description="武器养成任务" + ) + CrisisDrills: Literal[ + "AdvancedProgression1", + "AdvancedProgression2", + "AdvancedProgression3", + "AdvancedProgression4", + "AdvancedProgression5", + ] | None = Field(default=None, description="危境预演任务") + RewardsSetOption: Literal["RewardsSetA", "RewardsSetB"] | None = Field( + default=None, description="奖励套组选项" + ) + + +class MaaEndUserConfigNotify(ApiModel): + Enabled: bool | None = Field(default=None, description="是否启用通知") + IfSendStatistic: bool | None = Field( + default=None, description="是否发送统计信息" + ) + IfSendMail: bool | None = Field(default=None, description="是否发送邮件") + ToAddress: str | None = Field(default=None, description="收件地址") + IfServerChan: bool | None = Field(default=None, description="是否启用Server酱") + ServerChanKey: str | None = Field(default=None, description="Server酱密钥") + + +class MaaEndUserConfig(ApiModel): + type: Literal["MaaEndUserConfig"] = Field( + default="MaaEndUserConfig", description="配置类型" + ) + Info: MaaEndUserConfigInfo | None = Field(default=None, description="用户信息") + Task: MaaEndUserConfigTask | None = Field(default=None, description="任务配置") + Notify: MaaEndUserConfigNotify | None = Field( + default=None, description="通知配置" + ) + + +class MaaEndConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="脚本名称") + Path: str | None = Field(default=None, description="脚本路径") + + +class MaaEndConfigRun(ApiModel): + RunTimeLimit: int | None = Field(default=None, description="运行时间限制(分钟)") + ProxyTimesLimit: int | None = Field(default=None, description="每日代理次数限制") + RunTimesLimit: int | None = Field(default=None, description="重试次数限制") + + +class MaaEndConfigGame(ApiModel): + ControllerType: Literal[ + "Win32-Window", "Win32-Window-Background", "Win32-Front", "ADB" + ] | None = Field(default=None, description="控制器类型") + Path: str | None = Field(default=None, description="终末地客户端路径") + Arguments: str | None = Field(default=None, description="游戏启动参数") + WaitTime: int | None = Field(default=None, description="游戏等待时间") + EmulatorId: str | None = Field(default=None, description="模拟器ID") + EmulatorIndex: str | None = Field(default=None, description="模拟器索引") + CloseOnFinish: bool | None = Field(default=None, description="结束后关闭游戏") + + +class MaaEndConfig(ApiModel): + type: Literal["MaaEndConfig"] = Field(default="MaaEndConfig", description="配置类型") + Info: MaaEndConfigInfo | None = Field(default=None, description="脚本信息") + Run: MaaEndConfigRun | None = Field(default=None, description="运行配置") + Game: MaaEndConfigGame | None = Field(default=None, description="游戏配置") + + +__all__ = [ + "MaaEndUserConfigInfo", + "MaaEndUserConfigTask", + "MaaEndUserConfigNotify", + "MaaEndUserConfig", + "MaaEndConfigInfo", + "MaaEndConfigRun", + "MaaEndConfigGame", + "MaaEndConfig", +] diff --git a/app/models/plan_contract.py b/app/models/plan_contract.py new file mode 100644 index 00000000..f7a6320d --- /dev/null +++ b/app/models/plan_contract.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class PlanIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["MaaPlanConfig"] = Field(..., description="配置类型") + + +class MaaPlanInfoRead(ApiModel): + Name: str = Field(default="新 MAA 计划表", description="计划表名称") + Mode: Literal["ALL", "Weekly"] = Field(default="ALL", description="计划表模式") + + +class MaaPlanInfoPatch(ApiModel): + Name: str | None = Field(default=None, description="计划表名称") + Mode: Literal["ALL", "Weekly"] | None = Field( + default=None, description="计划表模式" + ) + + +class MaaPlanDayRead(ApiModel): + MedicineNumb: int = Field(default=0, description="吃理智药") + SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = Field( + default="0", description="连战次数" + ) + Stage: str = Field(default="-", description="关卡选择") + Stage_1: str = Field(default="-", description="备选关卡 - 1") + Stage_2: str = Field(default="-", description="备选关卡 - 2") + Stage_3: str = Field(default="-", description="备选关卡 - 3") + Stage_Remain: str = Field(default="-", description="剩余理智关卡") + + +class MaaPlanDayPatch(ApiModel): + MedicineNumb: int | None = Field(default=None, description="吃理智药") + SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] | None = Field( + default=None, description="连战次数" + ) + Stage: str | None = Field(default=None, description="关卡选择") + Stage_1: str | None = Field(default=None, description="备选关卡 - 1") + Stage_2: str | None = Field(default=None, description="备选关卡 - 2") + Stage_3: str | None = Field(default=None, description="备选关卡 - 3") + Stage_Remain: str | None = Field(default=None, description="剩余理智关卡") + + +class MaaPlanRead(ApiModel): + Info: MaaPlanInfoRead = Field(default_factory=MaaPlanInfoRead, description="基础信息") + ALL: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="全局") + Monday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周一") + Tuesday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周二") + Wednesday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周三") + Thursday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周四") + Friday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周五") + Saturday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周六") + Sunday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周日") + + +class MaaPlanPatch(ApiModel): + Info: MaaPlanInfoPatch | None = Field(default=None, description="基础信息") + ALL: MaaPlanDayPatch | None = Field(default=None, description="全局") + Monday: MaaPlanDayPatch | None = Field(default=None, description="周一") + Tuesday: MaaPlanDayPatch | None = Field(default=None, description="周二") + Wednesday: MaaPlanDayPatch | None = Field(default=None, description="周三") + Thursday: MaaPlanDayPatch | None = Field(default=None, description="周四") + Friday: MaaPlanDayPatch | None = Field(default=None, description="周五") + Saturday: MaaPlanDayPatch | None = Field(default=None, description="周六") + Sunday: MaaPlanDayPatch | None = Field(default=None, description="周日") + + +class PlanCreateIn(ApiModel): + type: Literal["MaaPlan"] = Field(..., description="计划类型") + + +class PlanCreateOut(OutBase): + planId: str = Field(..., description="新创建的计划ID") + data: MaaPlanRead = Field(..., description="计划配置数据") + + +class PlanGetIn(ApiModel): + planId: str | None = Field( + default=None, description="计划ID, 未携带时表示获取所有计划数据" + ) + + +class PlanGetOut(OutBase): + index: list[PlanIndexItem] = Field(..., description="计划索引列表") + data: dict[str, MaaPlanRead] = Field(..., description="计划列表或单个计划数据") + + +class PlanUpdateIn(ApiModel): + planId: str = Field(..., description="计划ID") + data: MaaPlanPatch = Field(..., description="计划更新数据") + + +class PlanDeleteIn(ApiModel): + planId: str = Field(..., description="计划ID") + + +class PlanReorderIn(ApiModel): + indexList: list[str] = Field(..., description="计划ID列表, 按新顺序排列") + + +__all__ = [ + "PlanIndexItem", + "MaaPlanInfoRead", + "MaaPlanInfoPatch", + "MaaPlanDayRead", + "MaaPlanDayPatch", + "MaaPlanRead", + "MaaPlanPatch", + "PlanCreateIn", + "PlanCreateOut", + "PlanGetIn", + "PlanGetOut", + "PlanUpdateIn", + "PlanDeleteIn", + "PlanReorderIn", +] diff --git a/app/models/queue_contract.py b/app/models/queue_contract.py new file mode 100644 index 00000000..8c40610c --- /dev/null +++ b/app/models/queue_contract.py @@ -0,0 +1,262 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class QueueIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["QueueConfig"] = Field(..., description="配置类型") + + +class QueueItemIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["QueueItem"] = Field(..., description="配置类型") + + +class TimeSetIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["TimeSet"] = Field(..., description="配置类型") + + +class QueueItemInfoRead(ApiModel): + ScriptId: str = Field(default="-", description="任务所对应的脚本ID") + + +class QueueItemInfoPatch(ApiModel): + ScriptId: str | None = Field(default=None, description="任务所对应的脚本ID") + + +class QueueItemRead(ApiModel): + Info: QueueItemInfoRead = Field(default_factory=QueueItemInfoRead, description="队列项") + + +class QueueItemPatch(ApiModel): + Info: QueueItemInfoPatch | None = Field(default=None, description="队列项") + + +class TimeSetInfoRead(ApiModel): + Enabled: bool = Field(default=True, description="是否启用") + Days: list[ + Literal[ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] + ] = Field( + default_factory=lambda: [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ], + description="执行周期, 可多选", + ) + Time: str = Field(default="00:00", description="时间设置, 格式为HH:MM") + + +class TimeSetInfoPatch(ApiModel): + Enabled: bool | None = Field(default=None, description="是否启用") + Days: list[ + Literal[ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] + ] | None = Field(default=None, description="执行周期, 可多选") + Time: str | None = Field(default=None, description="时间设置, 格式为HH:MM") + + +class TimeSetRead(ApiModel): + Info: TimeSetInfoRead = Field(default_factory=TimeSetInfoRead, description="时间项") + + +class TimeSetPatch(ApiModel): + Info: TimeSetInfoPatch | None = Field(default=None, description="时间项") + + +class QueueInfoRead(ApiModel): + Name: str = Field(default="新队列", description="队列名称") + TimeEnabled: bool = Field(default=False, description="是否启用定时") + StartUpEnabled: bool = Field(default=False, description="是否启动时运行") + AfterAccomplish: Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] = Field(default="NoAction", description="完成后操作") + + +class QueueInfoPatch(ApiModel): + Name: str | None = Field(default=None, description="队列名称") + TimeEnabled: bool | None = Field(default=None, description="是否启用定时") + StartUpEnabled: bool | None = Field(default=None, description="是否启动时运行") + AfterAccomplish: Literal[ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf", + ] | None = Field(default=None, description="完成后操作") + + +class QueueRead(ApiModel): + Info: QueueInfoRead = Field(default_factory=QueueInfoRead, description="队列信息") + + +class QueuePatch(ApiModel): + Info: QueueInfoPatch | None = Field(default=None, description="队列信息") + + +class QueueCreateOut(OutBase): + queueId: str = Field(..., description="新创建的队列ID") + data: QueueRead = Field(..., description="队列配置数据") + + +class QueueGetIn(ApiModel): + queueId: str | None = Field( + default=None, description="队列ID, 未携带时表示获取所有队列数据" + ) + + +class QueueGetOut(OutBase): + index: list[QueueIndexItem] = Field(..., description="队列索引列表") + data: dict[str, QueueRead] = Field( + ..., description="队列数据字典, key来自于index列表的uid" + ) + + +class QueueUpdateIn(ApiModel): + queueId: str = Field(..., description="队列ID") + data: QueuePatch = Field(..., description="队列更新数据") + + +class QueueDeleteIn(ApiModel): + queueId: str = Field(..., description="队列ID") + + +class QueueReorderIn(ApiModel): + indexList: list[str] = Field(..., description="按新顺序排列的调度队列UID列表") + + +class QueueSetInBase(ApiModel): + queueId: str = Field(..., description="所属队列ID") + + +class TimeSetGetIn(QueueSetInBase): + timeSetId: str | None = Field( + default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据" + ) + + +class TimeSetGetOut(OutBase): + index: list[TimeSetIndexItem] = Field(..., description="时间设置索引列表") + data: dict[str, TimeSetRead] = Field( + ..., description="时间设置数据字典, key来自于index列表的uid" + ) + + +class TimeSetCreateOut(OutBase): + timeSetId: str = Field(..., description="新创建的时间设置ID") + data: TimeSetRead = Field(..., description="时间设置配置数据") + + +class TimeSetUpdateIn(QueueSetInBase): + timeSetId: str = Field(..., description="时间设置ID") + data: TimeSetPatch = Field(..., description="时间设置更新数据") + + +class TimeSetDeleteIn(QueueSetInBase): + timeSetId: str = Field(..., description="时间设置ID") + + +class TimeSetReorderIn(QueueSetInBase): + indexList: list[str] = Field(..., description="时间设置ID列表, 按新顺序排列") + + +class QueueItemGetIn(QueueSetInBase): + queueItemId: str | None = Field( + default=None, description="队列项ID, 未携带时表示获取所有队列项数据" + ) + + +class QueueItemGetOut(OutBase): + index: list[QueueItemIndexItem] = Field(..., description="队列项索引列表") + data: dict[str, QueueItemRead] = Field( + ..., description="队列项数据字典, key来自于index列表的uid" + ) + + +class QueueItemCreateOut(OutBase): + queueItemId: str = Field(..., description="新创建的队列项ID") + data: QueueItemRead = Field(..., description="队列项配置数据") + + +class QueueItemUpdateIn(QueueSetInBase): + queueItemId: str = Field(..., description="队列项ID") + data: QueueItemPatch = Field(..., description="队列项更新数据") + + +class QueueItemDeleteIn(QueueSetInBase): + queueItemId: str = Field(..., description="队列项ID") + + +class QueueItemReorderIn(QueueSetInBase): + indexList: list[str] = Field(..., description="队列项ID列表, 按新顺序排列") + + +__all__ = [ + "QueueIndexItem", + "QueueItemIndexItem", + "TimeSetIndexItem", + "QueueItemInfoRead", + "QueueItemInfoPatch", + "QueueItemRead", + "QueueItemPatch", + "TimeSetInfoRead", + "TimeSetInfoPatch", + "TimeSetRead", + "TimeSetPatch", + "QueueInfoRead", + "QueueInfoPatch", + "QueueRead", + "QueuePatch", + "QueueCreateOut", + "QueueGetIn", + "QueueGetOut", + "QueueUpdateIn", + "QueueDeleteIn", + "QueueReorderIn", + "QueueSetInBase", + "TimeSetGetIn", + "TimeSetGetOut", + "TimeSetCreateOut", + "TimeSetUpdateIn", + "TimeSetDeleteIn", + "TimeSetReorderIn", + "QueueItemGetIn", + "QueueItemGetOut", + "QueueItemCreateOut", + "QueueItemUpdateIn", + "QueueItemDeleteIn", + "QueueItemReorderIn", +] diff --git a/app/models/scripts_contract.py b/app/models/scripts_contract.py new file mode 100644 index 00000000..eb8651f5 --- /dev/null +++ b/app/models/scripts_contract.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Annotated, Any, Literal, TypeAlias + +from pydantic import Field, TypeAdapter + +from .common_contract import ApiModel, OutBase, project_model +from .general_contract import GeneralConfig, GeneralUserConfig +from .maa_contract import MaaConfig, MaaUserConfig +from .maaend_contract import MaaEndConfig, MaaEndUserConfig +from .src_contract import SrcConfig, SrcUserConfig + +ScriptConfigType = Literal["MaaConfig", "GeneralConfig", "SrcConfig", "MaaEndConfig"] +UserConfigType = Literal[ + "MaaUserConfig", "GeneralUserConfig", "SrcUserConfig", "MaaEndUserConfig" +] +ScriptCreateType = Literal["MAA", "SRC", "General", "MaaEnd"] +JsonScalar: TypeAlias = str | int | float | bool | None +JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"] +PatchPayload: TypeAlias = dict[str, JsonValue] + +ScriptModel = MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig +UserModel = MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig +ScriptModelClass = ( + type[MaaConfig] | type[SrcConfig] | type[GeneralConfig] | type[MaaEndConfig] +) +UserModelClass = ( + type[MaaUserConfig] + | type[SrcUserConfig] + | type[GeneralUserConfig] + | type[MaaEndUserConfig] +) + +ScriptReadData = Annotated[ + MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig, + Field(discriminator="type"), +] +UserReadData = Annotated[ + MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig, + Field(discriminator="type"), +] + +SCRIPT_CONTRACT_BY_TYPE: dict[ScriptConfigType, ScriptModelClass] = { + "MaaConfig": MaaConfig, + "GeneralConfig": GeneralConfig, + "SrcConfig": SrcConfig, + "MaaEndConfig": MaaEndConfig, +} +USER_CONTRACT_BY_TYPE: dict[UserConfigType, UserModelClass] = { + "MaaUserConfig": MaaUserConfig, + "GeneralUserConfig": GeneralUserConfig, + "SrcUserConfig": SrcUserConfig, + "MaaEndUserConfig": MaaEndUserConfig, +} +SCRIPT_CREATE_TO_CONFIG_TYPE: dict[ScriptCreateType, ScriptConfigType] = { + "MAA": "MaaConfig", + "SRC": "SrcConfig", + "General": "GeneralConfig", + "MaaEnd": "MaaEndConfig", +} +SCRIPT_CONFIG_TO_USER_TYPE: dict[ScriptConfigType, UserConfigType] = { + "MaaConfig": "MaaUserConfig", + "GeneralConfig": "GeneralUserConfig", + "SrcConfig": "SrcUserConfig", + "MaaEndConfig": "MaaEndUserConfig", +} +PATCH_PAYLOAD_ADAPTER: TypeAdapter[PatchPayload] = TypeAdapter(dict[str, JsonValue]) + + +class ScriptIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: ScriptConfigType = Field(..., description="配置类型") + + +class UserIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: UserConfigType = Field(..., description="配置类型") + + +class ScriptCreateIn(ApiModel): + type: ScriptCreateType = Field( + ..., description="脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本" + ) + scriptId: str | None = Field( + default=None, description="直接从该脚本ID复制创建, 仅在复制创建时使用" + ) + + +class ScriptCreateOut(OutBase): + scriptId: str = Field(..., description="新创建的脚本ID") + data: ScriptReadData = Field(..., description="脚本配置数据") + + +class ScriptGetIn(ApiModel): + scriptId: str | None = Field( + default=None, description="脚本ID, 未携带时表示获取所有脚本数据" + ) + + +class ScriptGetOut(OutBase): + index: list[ScriptIndexItem] = Field(..., description="脚本索引列表") + data: dict[str, ScriptReadData] = Field( + ..., description="脚本数据字典, key来自于index列表的uid" + ) + + +class ScriptUpdateIn(ApiModel): + scriptId: str = Field(..., description="脚本ID") + data: PatchPayload = Field( + ..., description="脚本更新数据, 由后端根据 scriptId 选择对应 Patch 模型校验" + ) + + +class ScriptDeleteIn(ApiModel): + scriptId: str = Field(..., description="脚本ID") + + +class ScriptReorderIn(ApiModel): + indexList: list[str] = Field(..., description="脚本ID列表, 按新顺序排列") + + +class ScriptFileIn(ApiModel): + scriptId: str = Field(..., description="脚本ID") + jsonFile: str = Field(..., description="配置文件路径") + + +class ScriptUrlIn(ApiModel): + scriptId: str = Field(..., description="脚本ID") + url: str = Field(..., description="配置文件URL") + + +class ScriptUploadIn(ApiModel): + scriptId: str = Field(..., description="脚本ID") + config_name: str = Field(..., description="配置名称") + author: str = Field(..., description="作者") + description: str = Field(..., description="描述") + + +class UserInBase(ApiModel): + scriptId: str = Field(..., description="所属脚本ID") + + +class UserGetIn(UserInBase): + userId: str | None = Field( + default=None, description="用户ID, 未携带时表示获取所有用户数据" + ) + + +class UserGetOut(OutBase): + index: list[UserIndexItem] = Field(..., description="用户索引列表") + data: dict[str, UserReadData] = Field( + ..., description="用户数据字典, key来自于index列表的uid" + ) + + +class UserCreateOut(OutBase): + userId: str = Field(..., description="新创建的用户ID") + data: UserReadData = Field(..., description="用户配置数据") + + +class UserUpdateIn(UserInBase): + userId: str = Field(..., description="用户ID") + data: PatchPayload = Field( + ..., description="用户更新数据, 由后端根据 scriptId 选择对应 Patch 模型校验" + ) + + +class UserDeleteIn(UserInBase): + userId: str = Field(..., description="用户ID") + + +class UserReorderIn(UserInBase): + indexList: list[str] = Field(..., description="用户ID列表, 按新顺序排列") + + +class UserSetIn(UserInBase): + userId: str = Field(..., description="用户ID") + jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件") + + +def script_contract_type_from_create(create_type: ScriptCreateType) -> ScriptConfigType: + return SCRIPT_CREATE_TO_CONFIG_TYPE[create_type] + + +def script_contract_type_from_runtime(type_name: str) -> ScriptConfigType: + if type_name not in SCRIPT_CONTRACT_BY_TYPE: + raise KeyError(f"未知脚本 Contract 类型: {type_name}") + return type_name + + +def user_contract_type_from_script(script_type: ScriptConfigType) -> UserConfigType: + return SCRIPT_CONFIG_TO_USER_TYPE[script_type] + + +def project_script_model( + script_type: ScriptConfigType, + raw: Mapping[str, Any] | ApiModel | None, +) -> ScriptModel: + return project_model(SCRIPT_CONTRACT_BY_TYPE[script_type], raw) + + +def project_user_model( + user_type: UserConfigType, + raw: Mapping[str, Any] | ApiModel | None, +) -> UserModel: + return project_model(USER_CONTRACT_BY_TYPE[user_type], raw) + + +def project_script_model_map( + index_list: list[ScriptIndexItem], + raw_map: Mapping[str, Mapping[str, Any] | ApiModel], +) -> dict[str, ScriptModel]: + index_map: dict[str, ScriptConfigType] = {item.uid: item.type for item in index_list} + return { + uid: project_script_model(index_map[uid], raw) + for uid, raw in raw_map.items() + if uid in index_map + } + + +def project_user_model_map( + index_list: list[UserIndexItem], + raw_map: Mapping[str, Mapping[str, Any] | ApiModel], +) -> dict[str, UserModel]: + index_map: dict[str, UserConfigType] = {item.uid: item.type for item in index_list} + return { + uid: project_user_model(index_map[uid], raw) + for uid, raw in raw_map.items() + if uid in index_map + } + + +def validate_script_patch_data( + script_type: ScriptConfigType, raw: Mapping[str, JsonValue] +) -> dict[str, Any]: + normalized = PATCH_PAYLOAD_ADAPTER.validate_python(raw) + validated = SCRIPT_CONTRACT_BY_TYPE[script_type].model_validate(normalized) + return validated.model_dump(exclude_unset=True, exclude={"type"}) + + +def validate_user_patch_data( + user_type: UserConfigType, raw: Mapping[str, JsonValue] +) -> dict[str, Any]: + normalized = PATCH_PAYLOAD_ADAPTER.validate_python(raw) + validated = USER_CONTRACT_BY_TYPE[user_type].model_validate(normalized) + return validated.model_dump(exclude_unset=True, exclude={"type"}) + + +__all__ = [ + "ScriptConfigType", + "UserConfigType", + "ScriptCreateType", + "JsonValue", + "PatchPayload", + "ScriptModel", + "UserModel", + "ScriptReadData", + "UserReadData", + "SCRIPT_CONTRACT_BY_TYPE", + "USER_CONTRACT_BY_TYPE", + "SCRIPT_CREATE_TO_CONFIG_TYPE", + "SCRIPT_CONFIG_TO_USER_TYPE", + "ScriptIndexItem", + "UserIndexItem", + "ScriptCreateIn", + "ScriptCreateOut", + "ScriptGetIn", + "ScriptGetOut", + "ScriptUpdateIn", + "ScriptDeleteIn", + "ScriptReorderIn", + "ScriptFileIn", + "ScriptUrlIn", + "ScriptUploadIn", + "UserInBase", + "UserGetIn", + "UserGetOut", + "UserCreateOut", + "UserUpdateIn", + "UserDeleteIn", + "UserReorderIn", + "UserSetIn", + "script_contract_type_from_create", + "script_contract_type_from_runtime", + "user_contract_type_from_script", + "project_script_model", + "project_user_model", + "project_script_model_map", + "project_user_model_map", + "validate_script_patch_data", + "validate_user_patch_data", +] diff --git a/app/models/setting_contract.py b/app/models/setting_contract.py new file mode 100644 index 00000000..49ea15c2 --- /dev/null +++ b/app/models/setting_contract.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class WebhookIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["Webhook"] = Field(..., description="配置类型") + + +class WebhookInfoRead(ApiModel): + Name: str = Field(default="新自定义 Webhook 通知", description="Webhook名称") + Enabled: bool = Field(default=True, description="是否启用") + + +class WebhookInfoPatch(ApiModel): + Name: str | None = Field(default=None, description="Webhook名称") + Enabled: bool | None = Field(default=None, description="是否启用") + + +class WebhookDataRead(ApiModel): + Url: str = Field(default="", description="Webhook URL") + Template: str = Field(default="", description="消息模板") + Headers: str = Field(default="{ }", description="自定义请求头") + Method: Literal["POST", "GET"] = Field(default="POST", description="请求方法") + + +class WebhookDataPatch(ApiModel): + Url: str | None = Field(default=None, description="Webhook URL") + Template: str | None = Field(default=None, description="消息模板") + Headers: str | None = Field(default=None, description="自定义请求头") + Method: Literal["POST", "GET"] | None = Field( + default=None, description="请求方法" + ) + + +class WebhookRead(ApiModel): + Info: WebhookInfoRead = Field( + default_factory=WebhookInfoRead, description="Webhook基础信息" + ) + Data: WebhookDataRead = Field( + default_factory=WebhookDataRead, description="Webhook配置数据" + ) + + +class WebhookPatch(ApiModel): + Info: WebhookInfoPatch | None = Field(default=None, description="Webhook基础信息") + Data: WebhookDataPatch | None = Field(default=None, description="Webhook配置数据") + + +class GlobalConfigFunctionRead(ApiModel): + HistoryRetentionTime: Literal[7, 15, 30, 60, 90, 180, 365, 0] = Field( + default=0, description="历史记录保留时间, 0表示永久保存" + ) + IfAllowSleep: bool = Field(default=False, description="允许休眠") + IfSilence: bool = Field(default=False, description="静默模式") + IfAgreeBilibili: bool = Field(default=False, description="同意哔哩哔哩用户协议") + IfBlockAd: bool = Field(default=False, description="屏蔽模拟器广告") + + +class GlobalConfigFunctionPatch(ApiModel): + HistoryRetentionTime: Literal[7, 15, 30, 60, 90, 180, 365, 0] | None = Field( + default=None, description="历史记录保留时间, 0表示永久保存" + ) + IfAllowSleep: bool | None = Field(default=None, description="允许休眠") + IfSilence: bool | None = Field(default=None, description="静默模式") + IfAgreeBilibili: bool | None = Field( + default=None, description="同意哔哩哔哩用户协议" + ) + IfBlockAd: bool | None = Field(default=None, description="屏蔽模拟器广告") + + +class GlobalConfigVoiceRead(ApiModel): + Enabled: bool = Field(default=False, description="语音功能是否启用") + Type: Literal["simple", "noisy"] = Field( + default="simple", description="语音类型, simple为简洁, noisy为聒噪" + ) + + +class GlobalConfigVoicePatch(ApiModel): + Enabled: bool | None = Field(default=None, description="语音功能是否启用") + Type: Literal["simple", "noisy"] | None = Field( + default=None, description="语音类型, simple为简洁, noisy为聒噪" + ) + + +class GlobalConfigStartRead(ApiModel): + IfSelfStart: bool = Field(default=False, description="是否在系统启动时自动运行") + IfMinimizeDirectly: bool = Field( + default=False, description="启动时是否直接最小化到托盘而不显示主窗口" + ) + + +class GlobalConfigStartPatch(ApiModel): + IfSelfStart: bool | None = Field(default=None, description="是否在系统启动时自动运行") + IfMinimizeDirectly: bool | None = Field( + default=None, description="启动时是否直接最小化到托盘而不显示主窗口" + ) + + +class GlobalConfigUIRead(ApiModel): + IfShowTray: bool = Field(default=False, description="是否常态显示托盘图标") + IfToTray: bool = Field(default=False, description="是否最小化到托盘") + + +class GlobalConfigUIPatch(ApiModel): + IfShowTray: bool | None = Field(default=None, description="是否常态显示托盘图标") + IfToTray: bool | None = Field(default=None, description="是否最小化到托盘") + + +class GlobalConfigNotifyRead(ApiModel): + SendTaskResultTime: Literal["不推送", "任何时刻", "仅失败时"] = Field( + default="不推送", description="任务结果推送时机" + ) + IfSendStatistic: bool = Field(default=False, description="是否发送统计信息") + IfSendSixStar: bool = Field(default=False, description="是否发送公招六星通知") + IfPushPlyer: bool = Field(default=False, description="是否推送系统通知") + IfSendMail: bool = Field(default=False, description="是否发送邮件通知") + IfKoishiSupport: bool = Field(default=False, description="是否启用Koishi支持") + KoishiServerAddress: str = Field( + default="ws://localhost:5140/AUTO_MAS", description="Koishi服务器地址" + ) + KoishiToken: str = Field(default="", description="Koishi Token") + SMTPServerAddress: str = Field(default="", description="SMTP服务器地址") + AuthorizationCode: str = Field(default="", description="SMTP授权码") + FromAddress: str = Field(default="", description="邮件发送地址") + ToAddress: str = Field(default="", description="邮件接收地址") + IfServerChan: bool = Field(default=False, description="是否使用ServerChan推送") + ServerChanKey: str = Field(default="", description="ServerChan推送密钥") + + +class GlobalConfigNotifyPatch(ApiModel): + SendTaskResultTime: Literal["不推送", "任何时刻", "仅失败时"] | None = Field( + default=None, description="任务结果推送时机" + ) + IfSendStatistic: bool | None = Field(default=None, description="是否发送统计信息") + IfSendSixStar: bool | None = Field(default=None, description="是否发送公招六星通知") + IfPushPlyer: bool | None = Field(default=None, description="是否推送系统通知") + IfSendMail: bool | None = Field(default=None, description="是否发送邮件通知") + IfKoishiSupport: bool | None = Field(default=None, description="是否启用Koishi支持") + KoishiServerAddress: str | None = Field(default=None, description="Koishi服务器地址") + KoishiToken: str | None = Field(default=None, description="Koishi Token") + SMTPServerAddress: str | None = Field(default=None, description="SMTP服务器地址") + AuthorizationCode: str | None = Field(default=None, description="SMTP授权码") + FromAddress: str | None = Field(default=None, description="邮件发送地址") + ToAddress: str | None = Field(default=None, description="邮件接收地址") + IfServerChan: bool | None = Field(default=None, description="是否使用ServerChan推送") + ServerChanKey: str | None = Field(default=None, description="ServerChan推送密钥") + + +class GlobalConfigUpdateRead(ApiModel): + IfAutoUpdate: bool = Field(default=False, description="是否自动更新") + Source: Literal["GitHub", "MirrorChyan", "AutoSite"] = Field( + default="GitHub", description="更新源: GitHub源, Mirror酱源, 自建源" + ) + Channel: Literal["stable", "beta"] = Field( + default="stable", description="更新渠道: 稳定版, 测试版" + ) + ProxyAddress: str = Field(default="", description="网络代理地址") + MirrorChyanCDK: str = Field(default="", description="Mirror酱CDK") + + +class GlobalConfigUpdatePatch(ApiModel): + IfAutoUpdate: bool | None = Field(default=None, description="是否自动更新") + Source: Literal["GitHub", "MirrorChyan", "AutoSite"] | None = Field( + default=None, description="更新源: GitHub源, Mirror酱源, 自建源" + ) + Channel: Literal["stable", "beta"] | None = Field( + default=None, description="更新渠道: 稳定版, 测试版" + ) + ProxyAddress: str | None = Field(default=None, description="网络代理地址") + MirrorChyanCDK: str | None = Field(default=None, description="Mirror酱CDK") + + +class GlobalConfigRead(ApiModel): + Function: GlobalConfigFunctionRead = Field( + default_factory=GlobalConfigFunctionRead, description="功能相关配置" + ) + Voice: GlobalConfigVoiceRead = Field( + default_factory=GlobalConfigVoiceRead, description="语音相关配置" + ) + Start: GlobalConfigStartRead = Field( + default_factory=GlobalConfigStartRead, description="启动相关配置" + ) + UI: GlobalConfigUIRead = Field(default_factory=GlobalConfigUIRead, description="界面相关配置") + Notify: GlobalConfigNotifyRead = Field( + default_factory=GlobalConfigNotifyRead, description="通知相关配置" + ) + Update: GlobalConfigUpdateRead = Field( + default_factory=GlobalConfigUpdateRead, description="更新相关配置" + ) + + +class GlobalConfigPatch(ApiModel): + Function: GlobalConfigFunctionPatch | None = Field( + default=None, description="功能相关配置" + ) + Voice: GlobalConfigVoicePatch | None = Field(default=None, description="语音相关配置") + Start: GlobalConfigStartPatch | None = Field(default=None, description="启动相关配置") + UI: GlobalConfigUIPatch | None = Field(default=None, description="界面相关配置") + Notify: GlobalConfigNotifyPatch | None = Field(default=None, description="通知相关配置") + Update: GlobalConfigUpdatePatch | None = Field(default=None, description="更新相关配置") + + +class WebhookInBase(ApiModel): + scriptId: str | None = Field( + default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带" + ) + userId: str | None = Field( + default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带" + ) + + +class WebhookGetIn(WebhookInBase): + webhookId: str | None = Field( + default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据" + ) + + +class WebhookGetOut(OutBase): + index: list[WebhookIndexItem] = Field(..., description="Webhook索引列表") + data: dict[str, WebhookRead] = Field( + ..., description="Webhook数据字典, key来自于index列表的uid" + ) + + +class WebhookCreateOut(OutBase): + webhookId: str = Field(..., description="新创建的Webhook ID") + data: WebhookRead = Field(..., description="Webhook配置数据") + + +class WebhookUpdateIn(WebhookInBase): + webhookId: str = Field(..., description="Webhook ID") + data: WebhookPatch = Field(..., description="Webhook更新数据") + + +class WebhookDeleteIn(WebhookInBase): + webhookId: str = Field(..., description="Webhook ID") + + +class WebhookReorderIn(WebhookInBase): + indexList: list[str] = Field(..., description="Webhook ID列表, 按新顺序排列") + + +class WebhookTestIn(WebhookInBase): + data: WebhookPatch = Field(..., description="Webhook配置数据") + + +class SettingGetOut(OutBase): + data: GlobalConfigRead = Field(..., description="全局设置数据") + + +class SettingUpdateIn(ApiModel): + data: GlobalConfigPatch = Field(..., description="全局设置需要更新的数据") + + +__all__ = [ + "WebhookIndexItem", + "WebhookInfoRead", + "WebhookInfoPatch", + "WebhookDataRead", + "WebhookDataPatch", + "WebhookRead", + "WebhookPatch", + "GlobalConfigFunctionRead", + "GlobalConfigFunctionPatch", + "GlobalConfigVoiceRead", + "GlobalConfigVoicePatch", + "GlobalConfigStartRead", + "GlobalConfigStartPatch", + "GlobalConfigUIRead", + "GlobalConfigUIPatch", + "GlobalConfigNotifyRead", + "GlobalConfigNotifyPatch", + "GlobalConfigUpdateRead", + "GlobalConfigUpdatePatch", + "GlobalConfigRead", + "GlobalConfigPatch", + "WebhookInBase", + "WebhookGetIn", + "WebhookGetOut", + "WebhookCreateOut", + "WebhookUpdateIn", + "WebhookDeleteIn", + "WebhookReorderIn", + "WebhookTestIn", + "SettingGetOut", + "SettingUpdateIn", +] diff --git a/app/models/src_contract.py b/app/models/src_contract.py new file mode 100644 index 00000000..670319bd --- /dev/null +++ b/app/models/src_contract.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from .common_contract import ApiModel + + +class SrcUserConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="用户名称") + Status: bool | None = Field(default=None, description="是否启用") + Id: str | None = Field(default=None, description="用户ID") + Password: str | None = Field(default=None, description="密码") + Mode: Literal["简洁", "详细"] | None = Field( + default=None, description="脚本模式" + ) + Server: Literal[ + "CN-Official", + "CN-Bilibili", + "VN-Official", + "OVERSEA-America", + "OVERSEA-Asia", + "OVERSEA-Europe", + "OVERSEA-TWHKMO", + ] | None = Field(default=None, description="游戏服务器") + RemainedDay: int | None = Field(default=None, description="剩余天数") + Notes: str | None = Field(default=None, description="备注") + Tag: str | None = Field(default=None, description="用户标签信息") + + +class SrcUserConfigStage(ApiModel): + Channel: Literal["Relic", "Materials", "Ornament"] | None = Field( + default=None, description="关卡通道" + ) + Relic: Literal[ + "-", + "Cavern_of_Corrosion_Path_of_Possession", + "Cavern_of_Corrosion_Path_of_Hidden_Salvation", + "Cavern_of_Corrosion_Path_of_Thundersurge", + "Cavern_of_Corrosion_Path_of_Aria", + "Cavern_of_Corrosion_Path_of_Uncertainty", + "Cavern_of_Corrosion_Path_of_Cavalier", + "Cavern_of_Corrosion_Path_of_Dreamdive", + "Cavern_of_Corrosion_Path_of_Darkness", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers", + "Cavern_of_Corrosion_Path_of_Conflagration", + "Cavern_of_Corrosion_Path_of_Holy_Hymn", + "Cavern_of_Corrosion_Path_of_Providence", + "Cavern_of_Corrosion_Path_of_Drifting", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch", + "Cavern_of_Corrosion_Path_of_Gelid_Wind", + ] | None = Field(default=None, description="遗器关卡") + Materials: Literal[ + "-", + "Calyx_Golden_Memories_Planarcadia", + "Calyx_Golden_Aether_Planarcadia", + "Calyx_Golden_Treasures_Planarcadia", + "Calyx_Golden_Memories_Amphoreus", + "Calyx_Golden_Aether_Amphoreus", + "Calyx_Golden_Treasures_Amphoreus", + "Calyx_Golden_Memories_Penacony", + "Calyx_Golden_Aether_Penacony", + "Calyx_Golden_Treasures_Penacony", + "Calyx_Golden_Memories_The_Xianzhou_Luofu", + "Calyx_Golden_Aether_The_Xianzhou_Luofu", + "Calyx_Golden_Treasures_The_Xianzhou_Luofu", + "Calyx_Golden_Memories_Jarilo_VI", + "Calyx_Golden_Aether_Jarilo_VI", + "Calyx_Golden_Treasures_Jarilo_VI", + "Calyx_Crimson_Destruction_Herta_StorageZone", + "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", + "Calyx_Crimson_Preservation_Herta_SupplyZone", + "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", + "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", + "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", + "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", + "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", + "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", + "Calyx_Crimson_Erudition_Jarilo_RivetTown", + "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", + "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", + "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", + "Calyx_Crimson_Nihility_Jarilo_GreatMine", + "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", + "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", + "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", + "Stagnant_Shadow_Quanta", + "Stagnant_Shadow_Gust", + "Stagnant_Shadow_Fulmination", + "Stagnant_Shadow_Blaze", + "Stagnant_Shadow_Spike", + "Stagnant_Shadow_Rime", + "Stagnant_Shadow_Mirage", + "Stagnant_Shadow_Icicle", + "Stagnant_Shadow_Doom", + "Stagnant_Shadow_Puppetry", + "Stagnant_Shadow_Abomination", + "Stagnant_Shadow_Scorch", + "Stagnant_Shadow_Celestial", + "Stagnant_Shadow_Perdition", + "Stagnant_Shadow_Nectar", + "Stagnant_Shadow_Roast", + "Stagnant_Shadow_Ire", + "Stagnant_Shadow_Duty", + "Stagnant_Shadow_Timbre", + "Stagnant_Shadow_Mechwolf", + "Stagnant_Shadow_Gloam", + "Stagnant_Shadow_Sloggyre", + "Stagnant_Shadow_Gelidmoon", + "Stagnant_Shadow_Deepsheaf", + "Stagnant_Shadow_Cinders", + "Stagnant_Shadow_Sirens", + "Stagnant_Shadow_Ashes", + "Stagnant_Shadow_Soundburst", + ] | None = Field(default=None, description="材料关卡") + Ornament: Literal[ + "-", + "Divergent_Universe_Within_the_West_Wind", + "Divergent_Universe_Moonlit_Blood", + "Divergent_Universe_Unceasing_Strife", + "Divergent_Universe_Famished_Worker", + "Divergent_Universe_Eternal_Comedy", + "Divergent_Universe_To_Sweet_Dreams", + "Divergent_Universe_Pouring_Blades", + "Divergent_Universe_Fruit_of_Evil", + "Divergent_Universe_Permafrost", + "Divergent_Universe_Gentle_Words", + "Divergent_Universe_Smelted_Heart", + "Divergent_Universe_Untoppled_Walls", + ] | None = Field(default=None, description="饰品关卡") + ExtractReservedTrailblazePower: bool | None = Field( + default=None, description="使用储备开拓力" + ) + UseFuel: bool | None = Field(default=None, description="使用燃料") + FuelReserve: int | None = Field(default=None, description="保留的燃料数量") + EchoOfWar: str | None = Field(default=None, description="历战余响关卡") + SimulatedUniverseWorld: str | None = Field( + default=None, description="模拟宇宙关卡" + ) + + +class SrcUserConfigData(ApiModel): + LastProxyDate: str | None = Field(default=None, description="上次代理日期") + ProxyTimes: int | None = Field(default=None, description="代理次数") + IfPassCheck: bool | None = Field(default=None, description="是否通过检查") + + +class SrcUserConfigNotify(ApiModel): + Enabled: bool | None = Field(default=None, description="是否启用通知") + IfSendStatistic: bool | None = Field( + default=None, description="是否发送统计信息" + ) + IfSendMail: bool | None = Field(default=None, description="是否发送邮件") + ToAddress: str | None = Field(default=None, description="收件地址") + IfServerChan: bool | None = Field(default=None, description="是否启用Server酱") + ServerChanKey: str | None = Field(default=None, description="Server酱密钥") + + +class SrcUserConfig(ApiModel): + type: Literal["SrcUserConfig"] = Field(default="SrcUserConfig", description="配置类型") + Info: SrcUserConfigInfo | None = Field(default=None, description="基础信息") + Stage: SrcUserConfigStage | None = Field(default=None, description="关卡配置") + Data: SrcUserConfigData | None = Field(default=None, description="用户数据") + Notify: SrcUserConfigNotify | None = Field(default=None, description="单独通知") + + +class SrcConfigInfo(ApiModel): + Name: str | None = Field(default=None, description="SRC脚本名称") + Path: str | None = Field(default=None, description="SRC路径") + + +class SrcConfigEmulator(ApiModel): + Id: str | None = Field(default=None, description="模拟器ID") + Index: str | None = Field(default=None, description="模拟器索引") + + +class SrcConfigRun(ApiModel): + TaskTransitionMethod: Literal["ExitGame", "ExitEmulator"] | None = Field( + default=None, description="任务切换方式" + ) + ProxyTimesLimit: int | None = Field(default=None, description="代理次数限制") + RunTimesLimit: int | None = Field(default=None, description="运行次数限制") + RunTimeLimit: int | None = Field(default=None, description="运行时间限制(分钟)") + + +class SrcConfig(ApiModel): + type: Literal["SrcConfig"] = Field(default="SrcConfig", description="配置类型") + Info: SrcConfigInfo | None = Field(default=None, description="脚本基础信息") + Emulator: SrcConfigEmulator | None = Field(default=None, description="模拟器配置") + Run: SrcConfigRun | None = Field(default=None, description="脚本运行配置") + + +__all__ = [ + "SrcUserConfigInfo", + "SrcUserConfigStage", + "SrcUserConfigData", + "SrcUserConfigNotify", + "SrcUserConfig", + "SrcConfigInfo", + "SrcConfigEmulator", + "SrcConfigRun", + "SrcConfig", +] diff --git a/app/models/tools_contract.py b/app/models/tools_contract.py new file mode 100644 index 00000000..6921fd08 --- /dev/null +++ b/app/models/tools_contract.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class ToolsConfigArknightsPCRead(ApiModel): + Enabled: bool = Field(default=False, description="是否启用 ArknightsPC 工具") + PauseKey: str = Field(default="f10", description="暂停键位") + SelectDeployedKey: str = Field(default="w", description="选中已部署干员键位") + UseSkillKey: str = Field(default="r", description="释放技能键位") + RetreatKey: str = Field(default="t", description="撤退键位") + NextFrameKey: str = Field(default="f", description="下一帧键位") + AnotherQuitKey: str = Field(default="space", description="自定义退出、暂停键位") + Status: str = Field(default="-", description="工具状态 Tag") + + +class ToolsConfigArknightsPCPatch(ApiModel): + Enabled: bool | None = Field(default=None, description="是否启用 ArknightsPC 工具") + PauseKey: str | None = Field(default=None, description="暂停键位") + SelectDeployedKey: str | None = Field(default=None, description="选中已部署干员键位") + UseSkillKey: str | None = Field(default=None, description="释放技能键位") + RetreatKey: str | None = Field(default=None, description="撤退键位") + NextFrameKey: str | None = Field(default=None, description="下一帧键位") + AnotherQuitKey: str | None = Field(default=None, description="自定义退出、暂停键位") + + +class ToolsConfigRead(ApiModel): + ArknightsPC: ToolsConfigArknightsPCRead = Field( + default_factory=ToolsConfigArknightsPCRead, description="明日方舟PC工具配置" + ) + + +class ToolsConfigPatch(ApiModel): + ArknightsPC: ToolsConfigArknightsPCPatch | None = Field( + default=None, description="明日方舟PC工具配置" + ) + + +class ToolsGetOut(OutBase): + data: ToolsConfigRead = Field(..., description="工具配置数据") + + +class ToolsUpdateIn(ApiModel): + data: ToolsConfigPatch = Field(..., description="工具配置需要更新的数据") + + +__all__ = [ + "ToolsConfigArknightsPCRead", + "ToolsConfigArknightsPCPatch", + "ToolsConfigRead", + "ToolsConfigPatch", + "ToolsGetOut", + "ToolsUpdateIn", +] diff --git a/app/models/update_contract.py b/app/models/update_contract.py new file mode 100644 index 00000000..0fe4974e --- /dev/null +++ b/app/models/update_contract.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class UpdateCheckIn(ApiModel): + current_version: str = Field(..., description="当前前端版本号") + if_force: bool = Field(default=False, description="是否强制拉取更新信息") + + +class UpdateCheckOut(OutBase): + if_need_update: bool = Field(..., description="是否需要更新前端") + latest_version: str = Field(..., description="最新前端版本号") + update_info: dict[str, list[str]] = Field(..., description="版本更新信息字典") + + +__all__ = ["UpdateCheckIn", "UpdateCheckOut"] diff --git a/app/models/ws_contract.py b/app/models/ws_contract.py new file mode 100644 index 00000000..893670eb --- /dev/null +++ b/app/models/ws_contract.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import Field + +from .common_contract import ApiModel, OutBase + + +class WSClientCreateIn(ApiModel): + """创建 WebSocket 客户端请求""" + + name: str = Field(..., description="客户端名称,用于标识") + url: str = Field( + ..., description="WebSocket 服务器地址,如 ws://localhost:5140/path" + ) + ping_interval: float = Field(default=15.0, description="心跳发送间隔(秒)") + ping_timeout: float = Field(default=30.0, description="心跳超时时间(秒)") + reconnect_interval: float = Field(default=5.0, description="重连间隔(秒)") + max_reconnect_attempts: int = Field( + default=-1, description="最大重连次数,-1为无限" + ) + + +class WSClientCreateOut(OutBase): + """创建客户端响应""" + + data: dict[str, Any] | None = Field(default=None, description="返回数据") + + +class WSClientConnectIn(ApiModel): + """连接请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientDisconnectIn(ApiModel): + """断开连接请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientRemoveIn(ApiModel): + """删除客户端请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientSendIn(ApiModel): + """发送消息请求""" + + name: str = Field(..., description="客户端名称") + message: dict[str, Any] = Field(..., description="要发送的 JSON 消息") + + +class WSClientSendJsonIn(ApiModel): + """发送自定义 JSON 消息请求""" + + name: str = Field(..., description="客户端名称") + msg_id: str = Field(default="Client", description="消息 ID") + msg_type: str = Field(..., description="消息类型") + data: dict[str, Any] = Field(default_factory=dict, description="消息数据") + + +class WSClientAuthIn(ApiModel): + """发送认证请求""" + + name: str = Field(..., description="客户端名称") + token: str = Field(..., description="认证 Token") + auth_type: str = Field(default="auth", description="认证消息类型") + extra_data: dict[str, Any] | None = Field( + default=None, description="额外认证数据" + ) + + +class WSClientStatusIn(ApiModel): + """获取客户端状态请求""" + + name: str = Field(..., description="客户端名称") + + +class WSClientStatusOut(OutBase): + """客户端状态响应""" + + data: dict[str, Any] | None = Field(default=None, description="状态数据") + + +class WSClientListOut(OutBase): + """客户端列表响应""" + + data: dict[str, Any] | None = Field(default=None, description="客户端列表") + + +class WSMessageHistoryOut(OutBase): + """消息历史响应""" + + data: dict[str, Any] | None = Field(default=None, description="消息历史") + + +class WSClearHistoryIn(ApiModel): + """清空消息历史请求""" + + name: str | None = Field(default=None, description="客户端名称,为空则清空所有") + + +class WSCommandsOut(OutBase): + """可用命令列表响应""" + + data: dict[str, Any] | None = Field(default=None, description="命令列表") + + +__all__ = [ + "WSClientCreateIn", + "WSClientCreateOut", + "WSClientConnectIn", + "WSClientDisconnectIn", + "WSClientRemoveIn", + "WSClientSendIn", + "WSClientSendJsonIn", + "WSClientAuthIn", + "WSClientStatusIn", + "WSClientStatusOut", + "WSClientListOut", + "WSMessageHistoryOut", + "WSClearHistoryIn", + "WSCommandsOut", +] diff --git a/pyrightconfig.json b/pyrightconfig.json index 3340fd71..5ddc25c7 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,6 +1,8 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/pyright/schema/pyrightconfig.schema.json", "pythonVersion": "3.12", + "pythonPlatform": "Windows", + "venvPath": ".", + "venv": ".venv", "typeCheckingMode": "strict", "include": [ "app", @@ -19,6 +21,5 @@ "reportUnknownMemberType": "warning", "reportUnknownArgumentType": "warning", "reportUnknownLambdaType": "warning", - "reportUnknownParameterType": "warning", - "reportUnknownReturnType": "warning" + "reportUnknownParameterType": "warning" } diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..0b1f5d77 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pyright==1.1.408 From d0af1c7238e68654b24ca43df5d68c180f3f6c07 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sat, 4 Apr 2026 23:02:26 +0800 Subject: [PATCH 11/29] =?UTF-8?q?refactor(api):=20=E9=87=8D=E6=9E=84api?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E6=A0=BC=E5=BC=8F,=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E9=83=A8=E5=88=86api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_CONFIG_REFACTOR.md | 270 +++++++++++ app/api/common.py | 243 ++++++++-- app/api/core.py | 5 +- app/api/dispatch.py | 24 +- app/api/emulator.py | 368 ++++++++------- app/api/history.py | 39 +- app/api/info.py | 98 ++-- app/api/ocr.py | 230 +++++----- app/api/queue.py | 666 ++++++++++++++++----------- app/api/scripts.py | 769 ++++++++++++++++++-------------- app/api/setting.py | 341 ++++++++------ app/api/tools.py | 65 +-- app/api/update.py | 130 ++++-- app/api/ws_command.py | 42 +- app/api/ws_debug.py | 100 +++-- app/core/config/pydantic.py | 85 +++- app/models/common.py | 27 +- app/models/common_contract.py | 244 +++++++++- app/models/emulator_contract.py | 108 ++--- app/models/general_contract.py | 132 ++---- app/models/global_config.py | 13 +- app/models/history_contract.py | 4 + app/models/maa.py | 13 +- app/models/maa_contract.py | 152 ++----- app/models/maaend_contract.py | 118 ++--- app/models/queue_contract.py | 288 +++--------- app/models/scripts_contract.py | 189 ++++---- app/models/setting_contract.py | 299 ++----------- app/models/src_contract.py | 207 ++------- app/models/tools_contract.py | 51 +-- 30 files changed, 2810 insertions(+), 2510 deletions(-) create mode 100644 API_CONFIG_REFACTOR.md diff --git a/API_CONFIG_REFACTOR.md b/API_CONFIG_REFACTOR.md new file mode 100644 index 00000000..8c137373 --- /dev/null +++ b/API_CONFIG_REFACTOR.md @@ -0,0 +1,270 @@ +# 配置重构 API 迁移说明 + +本文档记录 2026-04 配置重构相关 API 的破坏性变更,供前端组统一适配。 + +## 总体规则 + +1. 旧的 `/get`、`/add`、`/update`、`/delete`、`/time/*`、`/item/*` 兼容路由已移除,只保留新的 REST 风格路径。 +2. 创建接口的返回字段统一为 `id`,不再返回 `emulatorId`、`queueId`、`timeSetId`、`queueItemId`、`webhookId` 这类资源专属字段。 +3. 单资源查询统一返回: + +```json +{ + "code": 200, + "status": "success", + "message": "操作成功", + "data": {} +} +``` + +4. 集合查询统一返回: + +```json +{ + "code": 200, + "status": "success", + "message": "操作成功", + "index": [], + "data": {} +} +``` + +5. 创建接口统一返回: + +```json +{ + "code": 200, + "status": "success", + "message": "操作成功", + "id": "资源ID", + "data": {} +} +``` + +6. 更新接口的请求体直接传 `Patch` 数据,不再包一层 `{ "data": ... }`。 +7. 排序接口统一为 `PATCH .../order`,请求体统一为: + +```json +{ + "indexList": ["id1", "id2"] +} +``` + +## 模拟器 API + +旧接口: +`POST /api/emulator/get` +`POST /api/emulator/add` +`POST /api/emulator/update` +`POST /api/emulator/delete` +`POST /api/emulator/order` +`POST /api/emulator/operate` +`POST /api/emulator/status` +`POST /api/emulator/emulator/search` + +新接口: +`GET /api/emulator` +`POST /api/emulator` +`PATCH /api/emulator/order` +`GET /api/emulator/detected` +`GET /api/emulator/status` +`GET /api/emulator/{emulator_id}` +`PATCH /api/emulator/{emulator_id}` +`DELETE /api/emulator/{emulator_id}` +`GET /api/emulator/{emulator_id}/status` +`POST /api/emulator/{emulator_id}/actions/{action}` + +动作接口说明: +`action` 取值为 `open`、`close`、`show` +请求体只保留: + +```json +{ + "index": "0" +} +``` + +## 调度队列 API + +旧接口: +`POST /api/queue/add` +`POST /api/queue/get` +`POST /api/queue/update` +`POST /api/queue/delete` +`POST /api/queue/order` +`POST /api/queue/time/get` +`POST /api/queue/time/add` +`POST /api/queue/time/update` +`POST /api/queue/time/delete` +`POST /api/queue/time/order` +`POST /api/queue/item/get` +`POST /api/queue/item/add` +`POST /api/queue/item/update` +`POST /api/queue/item/delete` +`POST /api/queue/item/order` + +新接口: +`GET /api/queue` +`POST /api/queue` +`PATCH /api/queue/order` +`GET /api/queue/{queue_id}` +`PATCH /api/queue/{queue_id}` +`DELETE /api/queue/{queue_id}` +`GET /api/queue/{queue_id}/times` +`POST /api/queue/{queue_id}/times` +`PATCH /api/queue/{queue_id}/times/order` +`GET /api/queue/{queue_id}/times/{time_set_id}` +`PATCH /api/queue/{queue_id}/times/{time_set_id}` +`DELETE /api/queue/{queue_id}/times/{time_set_id}` +`GET /api/queue/{queue_id}/items` +`POST /api/queue/{queue_id}/items` +`PATCH /api/queue/{queue_id}/items/order` +`GET /api/queue/{queue_id}/items/{queue_item_id}` +`PATCH /api/queue/{queue_id}/items/{queue_item_id}` +`DELETE /api/queue/{queue_id}/items/{queue_item_id}` + +关键变化: +单个队列、单个定时项、单个队列项不再返回 `index + data map`,只返回单个 `data`。 +嵌套资源更新和删除不再在 body 中重复传 `queueId`、`timeSetId`、`queueItemId`。 +WebSocket 命令 `queue.get` 现在只对应“查询单个队列”,如果需要拉取全部队列,请改走 `GET /api/queue`。 + +## 全局设置 API + +旧接口: +`POST /api/setting/get` +`POST /api/setting/update` +`POST /api/setting/test_notify` +`POST /api/setting/webhook/get` +`POST /api/setting/webhook/add` +`POST /api/setting/webhook/update` +`POST /api/setting/webhook/delete` +`POST /api/setting/webhook/order` +`POST /api/setting/webhook/test` + +新接口: +`GET /api/setting` +`PATCH /api/setting` +`POST /api/setting/actions/test-notify` +`GET /api/setting/webhooks` +`POST /api/setting/webhooks` +`PATCH /api/setting/webhooks/order` +`POST /api/setting/webhooks/test` +`GET /api/setting/webhooks/{webhook_id}` +`PATCH /api/setting/webhooks/{webhook_id}` +`DELETE /api/setting/webhooks/{webhook_id}` + +关键变化: +`PATCH /api/setting` 请求体直接传 `GlobalConfigPatch`。 +Webhook 单项查询返回单个 `data`,不再返回 `index + data map`。 +Webhook 测试接口请求体直接传 `WebhookPatch`。 + +## 工具设置 API + +旧接口: +`POST /api/tools/get` +`POST /api/tools/update` + +新接口: +`GET /api/tools` +`PATCH /api/tools` + +关键变化: +`PATCH /api/tools` 请求体直接传 `ToolsConfigPatch`。 + +## 脚本管理 API + +旧接口: +`POST /api/scripts/add` +`POST /api/scripts/get` +`POST /api/scripts/update` +`POST /api/scripts/delete` +`POST /api/scripts/order` +`POST /api/scripts/import/file` +`POST /api/scripts/export/file` +`POST /api/scripts/import/web` +`POST /api/scripts/Upload/web` +`POST /api/scripts/user/get` +`POST /api/scripts/user/add` +`POST /api/scripts/user/update` +`POST /api/scripts/user/delete` +`POST /api/scripts/user/order` +`POST /api/scripts/user/infrastructure` +`POST /api/scripts/user/combox/infrastructure` +`POST /api/scripts/webhook/get` +`POST /api/scripts/webhook/add` +`POST /api/scripts/webhook/update` +`POST /api/scripts/webhook/delete` +`POST /api/scripts/webhook/order` + +新接口: +`GET /api/scripts` +`POST /api/scripts` +`PATCH /api/scripts/order` +`GET /api/scripts/{script_id}` +`PATCH /api/scripts/{script_id}` +`DELETE /api/scripts/{script_id}` +`POST /api/scripts/{script_id}/actions/import-file` +`POST /api/scripts/{script_id}/actions/export-file` +`POST /api/scripts/{script_id}/actions/import-web` +`POST /api/scripts/{script_id}/actions/upload-web` +`GET /api/scripts/{script_id}/users` +`POST /api/scripts/{script_id}/users` +`PATCH /api/scripts/{script_id}/users/order` +`GET /api/scripts/{script_id}/users/{user_id}` +`PATCH /api/scripts/{script_id}/users/{user_id}` +`DELETE /api/scripts/{script_id}/users/{user_id}` +`POST /api/scripts/{script_id}/users/{user_id}/actions/import-infrastructure` +`GET /api/scripts/{script_id}/users/{user_id}/infrastructure-options` +`GET /api/scripts/{script_id}/users/{user_id}/webhooks` +`POST /api/scripts/{script_id}/users/{user_id}/webhooks` +`PATCH /api/scripts/{script_id}/users/{user_id}/webhooks/order` +`GET /api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}` +`PATCH /api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}` +`DELETE /api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}` + +关键变化: +脚本创建请求中的复制来源字段从 `scriptId` 改为 `copyFromId`。 +脚本、用户、脚本用户下的 Webhook 都已经切到单资源 REST 返回,不再混用旧的 body 包装。 +脚本级和用户级 patch 校验不再手写维护,而是基于运行期配置模型直接派生。 + +## 共享 Contract 变更 + +新增通用响应模型: +`ResourceCollectionOut[Index, Data]` +`ResourceItemOut[Data]` +`ResourceCreateOut[Data]` +`IndexOrderPatch` + +这意味着后续新增配置资源时,应优先复用通用响应模型,不要再重复定义新的 `XXXGetOut`、`XXXCreateOut`、`XXXReorderIn` 壳子。 + +## Contract 生成规则 + +配置相关 `Read/Patch` contract 现在优先通过运行期 `PydanticConfigBase` 模型派生生成。 + +当前已经接入: +`EmulatorConfig` +`QueueConfig` +`TimeSet` +`QueueItem` +`Webhook` +`GlobalConfig` +`ToolsConfig` +`GeneralConfig` +`GeneralUserConfig` +`MaaConfig` +`MaaUserConfig` +`MaaPlanConfig` +`SrcConfig` +`SrcUserConfig` +`MaaEndConfig` +`MaaEndUserConfig` + +规则说明: +读模型保留运行期模型字段和虚拟字段。 +补丁模型自动把字段转为可选,并跳过虚拟字段。 +脚本类读模型额外保留静态 `type` 字段,用于前端按 discriminator 分发。 + +## 额外说明 + +脚本相关创建接口和 Webhook 创建接口的返回字段都已统一为 `id`。 +如果前端依赖旧的 `scriptId`、`userId`、`webhookId` 返回字段,需要同步调整读取逻辑。 diff --git a/app/api/common.py b/app/api/common.py index fa7649c8..a7ad966b 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -1,51 +1,230 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable -from typing import TypeVar +from collections.abc import Awaitable, Callable, Iterable +from functools import wraps +from typing import Any, ParamSpec, TypeVar, cast + +from fastapi import APIRouter from app.models.common_contract import ComboBoxItem, ComboBoxOut, OutBase OutT = TypeVar("OutT", bound=OutBase) +P = ParamSpec("P") def error_out( - model_cls: type[OutT], - exc: Exception, - *, - message: str | None = None, - **kwargs: object, + model_cls: type[OutT], + exc: Exception, + *, + message: str | None = None, + **kwargs: object, ) -> OutT: - return model_cls( - code=500, - status="error", - message=message or f"{type(exc).__name__}: {str(exc)}", - **kwargs, - ) + return model_cls( + code=500, + status="error", + message=message or f"{type(exc).__name__}: {str(exc)}", + **kwargs, + ) async def run_api( - success_factory: Callable[[], Awaitable[OutT]], - *, - model_cls: type[OutT], - message: str | None = None, - **fallback_kwargs: object, + success_factory: Callable[[], Awaitable[OutT]], + *, + model_cls: type[OutT], + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + **fallback_kwargs: object, ) -> OutT: - try: - return await success_factory() - except Exception as exc: - return error_out( - model_cls, - exc, - message=message, - **fallback_kwargs, - ) + try: + return await success_factory() + except Exception as exc: + if on_error is not None: + on_error(exc) + return error_out( + model_cls, + exc, + message=message, + **fallback_kwargs, + ) + + +def api_guard( + *, + model_cls: type[OutT], + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + **fallback_kwargs: object, +) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: + """为 FastAPI 路由提供统一异常包装的装饰器。""" + + def decorator(func: Callable[P, Awaitable[OutT]]) -> Callable[P, Awaitable[OutT]]: + @wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> OutT: + return await run_api( + lambda: func(*args, **kwargs), + model_cls=model_cls, + message=message, + on_error=on_error, + **fallback_kwargs, + ) + + return wrapper + + return decorator + + +def api_post( + router: APIRouter, + path: str, + *, + model_cls: type[OutT], + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, +) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: + """统一 POST 路由注册装饰器。""" + + return api_route( + router, + path, + methods=("POST",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + +def api_route( + router: APIRouter, + path: str, + *, + methods: Iterable[str], + model_cls: type[OutT], + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, +) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: + """统一路由注册装饰器:路由 + 守卫 + 可选 WS 命令。""" + + guard = api_guard( + model_cls=model_cls, + message=message, + on_error=on_error, + **fallback_kwargs, + ) + route = router.api_route( + path, + methods=list(methods), + **cast(dict[str, Any], route_kwargs or {}), + ) + + def decorator(func: Callable[P, Awaitable[OutT]]) -> Callable[P, Awaitable[OutT]]: + wrapped = guard(func) + if ws_endpoint is not None: + from app.api.ws_command import ws_command + + wrapped = ws_command(ws_endpoint)(wrapped) + return route(wrapped) + + return decorator + + +def api_get( + router: APIRouter, + path: str, + *, + model_cls: type[OutT], + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, +) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: + """统一 GET 路由注册装饰器。""" + + return api_route( + router, + path, + methods=("GET",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + +def api_patch( + router: APIRouter, + path: str, + *, + model_cls: type[OutT], + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, +) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: + """统一 PATCH 路由注册装饰器。""" + + return api_route( + router, + path, + methods=("PATCH",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + +def api_delete( + router: APIRouter, + path: str, + *, + model_cls: type[OutT], + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, +) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: + """统一 DELETE 路由注册装饰器。""" + + return api_route( + router, + path, + methods=("DELETE",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) __all__ = [ - "OutBase", - "ComboBoxItem", - "ComboBoxOut", - "error_out", - "run_api", + "OutBase", + "ComboBoxItem", + "ComboBoxOut", + "error_out", + "run_api", + "api_guard", + "api_route", + "api_get", + "api_post", + "api_patch", + "api_delete", ] diff --git a/app/api/core.py b/app/api/core.py index 91f24dde..4823ce48 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -32,6 +32,7 @@ from app.models.common_contract import OutBase from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command +from app.api.common import error_out from app.utils import get_logger router = APIRouter(prefix="/api/core", tags=["核心信息"]) @@ -112,7 +113,5 @@ async def close() -> OutBase: await Config.websocket.close(code=1000, reason="正常关闭") await System.set_power("KillSelf", from_frontend=True) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() diff --git a/app/api/dispatch.py b/app/api/dispatch.py index 2c36f07c..7992f929 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -33,6 +33,7 @@ TaskCreateIn, TaskCreateOut, ) +from app.api.common import error_out router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) @@ -48,9 +49,7 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: try: task_id = await TaskManager.add_task(task.mode, task.taskId) except Exception as e: - return TaskCreateOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", taskId="" - ) + return error_out(TaskCreateOut, e, taskId="") return TaskCreateOut(taskId=str(task_id)) @@ -65,9 +64,7 @@ async def stop_task(task: DispatchIn = Body(...)) -> OutBase: try: await TaskManager.stop_task(task.taskId) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -82,12 +79,7 @@ async def get_power() -> PowerOut: try: signal = Config.power_sign except Exception as e: - return PowerOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - signal="NoAction", - ) + return error_out(PowerOut, e, signal="NoAction") return PowerOut(signal=signal) @@ -102,9 +94,7 @@ async def set_power(task: PowerIn = Body(...)) -> OutBase: try: Config.power_sign = task.signal except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -119,7 +109,5 @@ async def cancel_power_task() -> OutBase: try: await System.cancel_power_task() except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() diff --git a/app/api/emulator.py b/app/api/emulator.py index 6b284d00..b756f7a0 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -21,185 +21,247 @@ # Contact: DLmaster_361@163.com -from fastapi import APIRouter, Body +from typing import Annotated, Literal + +from fastapi import APIRouter, Body, Path + +from app.api.common import api_delete, api_get, api_patch, api_post from app.core import Config, EmulatorManager -from app.models.common_contract import ( - OutBase, - project_model, - project_model_list, - project_model_map, -) +from app.models.common_contract import IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map from app.models.emulator_contract import ( - EmulatorCreateOut, + EmulatorActionBody, EmulatorConfigIndexItem, - EmulatorDeleteIn, - EmulatorGetIn, + EmulatorCreateOut, + EmulatorDetailOut, + EmulatorDeviceStatusOut, EmulatorGetOut, - EmulatorOperateIn, + EmulatorPatch, EmulatorRead, - EmulatorReorderIn, - EmulatorSearchResult, EmulatorSearchOut, + EmulatorSearchResult, EmulatorStatusOut, - EmulatorUpdateIn, ) router = APIRouter(prefix="/api/emulator", tags=["模拟器管理"]) +EmulatorIdPath = Annotated[str, Path(description="模拟器 ID")] +EmulatorActionPath = Annotated[ + Literal["open", "close", "show"], + Path(description="模拟器动作"), +] -@router.post( - "/get", - tags=["Get"], - summary="查询模拟器配置", - response_model=EmulatorGetOut, - status_code=200, -) -async def get_emulator(emulator: EmulatorGetIn = Body(...)) -> EmulatorGetOut: - try: - index, data = await Config.get_emulator(emulator.emulatorId) - index = project_model_list(EmulatorConfigIndexItem, index) - data = project_model_map(EmulatorRead, data) - except Exception as e: - return EmulatorGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) - return EmulatorGetOut(index=index, data=data) - - -@router.post( - "/add", - tags=["Add"], - summary="添加模拟器项", - response_model=EmulatorCreateOut, - status_code=200, -) -async def add_emulator() -> EmulatorCreateOut: - try: - uid, config = await Config.add_emulator() - data = project_model(EmulatorRead, await config.toDict()) - except Exception as e: - return EmulatorCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - emulatorId="", - data=EmulatorRead(), - ) - return EmulatorCreateOut(emulatorId=str(uid), data=data) - - -@router.post( - "/update", - tags=["Update"], - summary="更新模拟器项", - response_model=OutBase, - status_code=200, -) -async def update_emulator(emulator: EmulatorUpdateIn = Body(...)) -> OutBase: - try: - await Config.update_emulator( - emulator.emulatorId, emulator.data.model_dump(exclude_unset=True) - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + +async def _build_emulator_collection_out() -> EmulatorGetOut: + index, data = await Config.get_emulator(None) + return EmulatorGetOut( + index=project_model_list(EmulatorConfigIndexItem, index), + data=project_model_map(EmulatorRead, data), + ) + + +async def _build_emulator_detail_out(emulator_id: str) -> EmulatorDetailOut: + _, data = await Config.get_emulator(emulator_id) + projected = project_model_map(EmulatorRead, data) + return EmulatorDetailOut(data=projected[emulator_id]) + + +async def _build_emulator_create_out() -> EmulatorCreateOut: + uid, config = await Config.add_emulator() + return EmulatorCreateOut( + id=str(uid), + data=project_model(EmulatorRead, await config.toDict()), + ) + + +async def _update_emulator_config(emulator_id: str, data: EmulatorPatch) -> OutBase: + await Config.update_emulator(emulator_id, data.model_dump(exclude_unset=True)) return OutBase() -@router.post( - "/delete", - tags=["Delete"], - summary="删除模拟器项", - response_model=OutBase, - status_code=200, -) -async def delete_emulator(emulator: EmulatorDeleteIn = Body(...)) -> OutBase: - try: - await Config.del_emulator(emulator.emulatorId) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def _delete_emulator_config(emulator_id: str) -> OutBase: + await Config.del_emulator(emulator_id) return OutBase() -@router.post( +async def _build_emulator_status_out() -> EmulatorStatusOut: + return EmulatorStatusOut(data=await EmulatorManager.get_status(None)) + + +async def _build_emulator_device_status_out( + emulator_id: str, +) -> EmulatorDeviceStatusOut: + statuses = await EmulatorManager.get_status(emulator_id) + return EmulatorDeviceStatusOut(data=statuses.get(emulator_id, {})) + + +async def _build_emulator_search_out() -> EmulatorSearchOut: + from app.utils import search_all_emulators + + emulators = await search_all_emulators() + return EmulatorSearchOut(data=project_model_list(EmulatorSearchResult, emulators)) + + +@api_get( + router, + "", + model_cls=EmulatorGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询全部模拟器配置", + "response_model": EmulatorGetOut, + "status_code": 200, + }, +) +async def list_emulators() -> EmulatorGetOut: + return await _build_emulator_collection_out() + + +@api_post( + router, + "", + model_cls=EmulatorCreateOut, + id="", + data=EmulatorRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建模拟器配置", + "response_model": EmulatorCreateOut, + "status_code": 200, + }, +) +async def create_emulator() -> EmulatorCreateOut: + return await _build_emulator_create_out() + + +@api_patch( + router, "/order", - tags=["Update"], - summary="重新排序模拟器项", - response_model=OutBase, - status_code=200, + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序模拟器", + "response_model": OutBase, + "status_code": 200, + }, ) -async def reorder_emulator(emulator: EmulatorReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_emulator(emulator.indexList) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def reorder_emulator(body: IndexOrderPatch = Body(...)) -> OutBase: + await Config.reorder_emulator(body.indexList) return OutBase() -@router.post( - "/operate", - tags=["Action"], - summary="操作模拟器", - response_model=OutBase, - status_code=200, +@api_get( + router, + "/detected", + model_cls=EmulatorSearchOut, + data=[], + route_kwargs={ + "tags": ["Get"], + "summary": "搜索已安装的模拟器", + "response_model": EmulatorSearchOut, + "status_code": 200, + }, ) -async def operation_emulator(emulator: EmulatorOperateIn = Body(...)) -> OutBase: - try: - await EmulatorManager.operate_emulator( - emulator.operate, emulator.emulatorId, emulator.index - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) - return OutBase() +async def detect_emulators() -> EmulatorSearchOut: + return await _build_emulator_search_out() -@router.post( +@api_get( + router, "/status", - tags=["Get"], - summary="查询模拟器状态", - response_model=EmulatorStatusOut, - status_code=200, + model_cls=EmulatorStatusOut, + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询全部模拟器状态", + "response_model": EmulatorStatusOut, + "status_code": 200, + }, ) -async def get_status(emulator: EmulatorGetIn = Body(...)) -> EmulatorStatusOut: - try: - data = await EmulatorManager.get_status(emulator.emulatorId) - except Exception as e: - return EmulatorStatusOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={} - ) - return EmulatorStatusOut(data=data) - - -@router.post( - "/emulator/search", - tags=["Get"], - summary="搜索已安装的模拟器", - response_model=EmulatorSearchOut, - status_code=200, +async def get_emulator_statuses() -> EmulatorStatusOut: + return await _build_emulator_status_out() + + +@api_get( + router, + "/{emulator_id}", + model_cls=EmulatorDetailOut, + data=EmulatorRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个模拟器配置", + "response_model": EmulatorDetailOut, + "status_code": 200, + }, ) -async def search_emulators() -> EmulatorSearchOut: - """自动搜索系统中已安装的模拟器""" - try: - from app.utils import search_all_emulators - - emulators = await search_all_emulators() - results = project_model_list(EmulatorSearchResult, emulators) - except Exception as e: - return EmulatorSearchOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - emulators=[], - ) - return EmulatorSearchOut(emulators=results) +async def get_emulator(emulator_id: EmulatorIdPath) -> EmulatorDetailOut: + return await _build_emulator_detail_out(emulator_id) + + +@api_patch( + router, + "/{emulator_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新模拟器配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_emulator( + emulator_id: EmulatorIdPath, data: EmulatorPatch = Body(...) +) -> OutBase: + return await _update_emulator_config(emulator_id, data) + + +@api_delete( + router, + "/{emulator_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除模拟器配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def delete_emulator(emulator_id: EmulatorIdPath) -> OutBase: + return await _delete_emulator_config(emulator_id) + + +@api_get( + router, + "/{emulator_id}/status", + model_cls=EmulatorDeviceStatusOut, + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个模拟器状态", + "response_model": EmulatorDeviceStatusOut, + "status_code": 200, + }, +) +async def get_emulator_status(emulator_id: EmulatorIdPath) -> EmulatorDeviceStatusOut: + return await _build_emulator_device_status_out(emulator_id) + + +@api_post( + router, + "/{emulator_id}/actions/{action}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "执行模拟器动作", + "response_model": OutBase, + "status_code": 200, + }, +) +async def operate_emulator( + emulator_id: EmulatorIdPath, + action: EmulatorActionPath, + body: EmulatorActionBody = Body(...), +) -> OutBase: + await EmulatorManager.operate_emulator(action, emulator_id, body.index) + return OutBase() diff --git a/app/api/history.py b/app/api/history.py index 193aa9fa..8e98b91c 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -23,11 +23,11 @@ from datetime import datetime from pathlib import Path -from typing import Any, cast from fastapi import APIRouter, Body +from pydantic import TypeAdapter from app.core import Config -from app.models.common_contract import project_model_list +from app.api.common import error_out from app.models.history_contract import ( HistoryData, HistoryDataGetIn, @@ -39,21 +39,18 @@ router = APIRouter(prefix="/api/history", tags=["历史记录"]) +HISTORY_INDEX_ADAPTER: TypeAdapter[list[HistoryIndexItem]] = TypeAdapter( + list[HistoryIndexItem] +) + def _build_history_data(raw: dict[str, object]) -> HistoryData: - index_data = raw.get("index", []) - raw["index"] = [] + data = dict(raw) + index_data = data.get("index", []) + data["index"] = [] if isinstance(index_data, list): - index_rows = [ - cast(dict[str, Any], item) - for item in cast(list[object], index_data) - if isinstance(item, dict) - ] - raw["index"] = project_model_list( - HistoryIndexItem, - index_rows, - ) - return HistoryData.model_validate(raw) + data["index"] = HISTORY_INDEX_ADAPTER.validate_python(index_data) + return HistoryData.model_validate(data) @router.post( @@ -78,12 +75,7 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: current_users[user] = _build_history_data(record) data[date] = current_users except Exception as e: - return HistorySearchOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - data={}, - ) + return error_out(HistorySearchOut, e, data={}) return HistorySearchOut(data=data) @@ -102,10 +94,5 @@ async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryData raw_data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8") data = _build_history_data(raw_data) except Exception as e: - return HistoryDataGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - data=HistoryData(), - ) + return error_out(HistoryDataGetOut, e, data=HistoryData()) return HistoryDataGetOut(data=data) diff --git a/app/api/info.py b/app/api/info.py index a5fed4b8..aacfa2ad 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -24,10 +24,17 @@ from typing import Any, cast from fastapi import APIRouter, Body +from pydantic import Field, TypeAdapter from app.core import Config -from app.models.common_contract import ComboBoxItem, ComboBoxOut, InfoOut, OutBase -from app.models.emulator_contract import EmulatorDeleteIn +from app.models.common_contract import ( + ApiModel, + ComboBoxItem, + ComboBoxOut, + InfoOut, + OutBase, +) +from app.api.common import error_out from app.models.info_contract import ( GetStageIn, NoticeOut, @@ -37,27 +44,18 @@ router = APIRouter(prefix="/api/info", tags=["信息获取"]) +class EmulatorIdBody(ApiModel): + emulatorId: str = Field(..., description="模拟器 ID") + +COMBOBOX_ITEMS_ADAPTER: TypeAdapter[list[ComboBoxItem]] = TypeAdapter( + list[ComboBoxItem] +) + + def _to_combobox_items(raw_data: object) -> list[ComboBoxItem]: - if not isinstance(raw_data, list): - return [] - - items: list[ComboBoxItem] = [] - for item_any in cast(list[object], raw_data): - if not isinstance(item_any, dict): - continue - item = cast(dict[str, Any], item_any) - label = item.get("label") - if not isinstance(label, str): - continue - value_raw = item.get("value") - value: str | None - if isinstance(value_raw, str) or value_raw is None: - value = value_raw - else: - value = str(value_raw) - items.append(ComboBoxItem(label=label, value=value)) - - return items + return COMBOBOX_ITEMS_ADAPTER.validate_python( + raw_data if isinstance(raw_data, list) else [] + ) @router.post( @@ -71,10 +69,9 @@ async def get_git_version() -> VersionOut: try: is_latest, commit_hash, commit_time = await Config.get_git_version() except Exception as e: - return VersionOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", + return error_out( + VersionOut, + e, if_need_update=False, current_time="unknown", current_hash="unknown", @@ -100,9 +97,7 @@ async def get_stage_combox( raw_data = cast(object, await Config.get_stage_info(stage.type)) data = _to_combobox_items(raw_data) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -118,9 +113,7 @@ async def get_script_combox() -> ComboBoxOut: raw_data = await Config.get_script_combox() data = _to_combobox_items(raw_data) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -136,9 +129,7 @@ async def get_task_combox() -> ComboBoxOut: raw_data = await Config.get_task_combox() data = _to_combobox_items(raw_data) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -154,9 +145,7 @@ async def get_plan_combox() -> ComboBoxOut: raw_data = await Config.get_plan_combox() data = _to_combobox_items(raw_data) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -172,9 +161,7 @@ async def get_emulator_combox() -> ComboBoxOut: raw_data = await Config.get_emulator_combox() data = _to_combobox_items(raw_data) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -186,7 +173,7 @@ async def get_emulator_combox() -> ComboBoxOut: status_code=200, ) async def get_emulator_devices_combox( - emulator: EmulatorDeleteIn = Body(...), + emulator: EmulatorIdBody = Body(...), ) -> ComboBoxOut: try: raw_data = cast( @@ -194,9 +181,7 @@ async def get_emulator_devices_combox( ) data = _to_combobox_items(raw_data) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -211,13 +196,7 @@ async def get_notice_info() -> NoticeOut: try: if_need_show, data = await Config.get_notice() except Exception as e: - return NoticeOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - if_need_show=False, - data={}, - ) + return error_out(NoticeOut, e, if_need_show=False, data={}) return NoticeOut(if_need_show=if_need_show, data=data) @@ -232,9 +211,7 @@ async def confirm_notice() -> OutBase: try: await Config.set("Data", "IfShowNotice", False) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(OutBase, e) return OutBase() @@ -263,9 +240,7 @@ async def get_web_config() -> InfoOut: try: data = await Config.get_web_config() except Exception as e: - return InfoOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={} - ) + return error_out(InfoOut, e, data={}) return InfoOut(data={"WebConfig": data}) @@ -283,10 +258,5 @@ async def get_overview() -> InfoOut: proxy = await Config.get_proxy_overview() except Exception as e: - return InfoOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - data={"Stage": [], "Proxy": []}, - ) + return error_out(InfoOut, e, data={"Stage": [], "Proxy": []}) return InfoOut(data={"Stage": stage, "Proxy": proxy}) diff --git a/app/api/ocr.py b/app/api/ocr.py index 8c8e22a3..a16798f4 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -22,14 +22,16 @@ from fastapi import APIRouter, Body -from pydantic import BaseModel, Field +from pydantic import Field from typing import Optional import base64 from io import BytesIO +from PIL import Image from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger -from app.models.common_contract import OutBase +from app.models.common_contract import ApiModel, OutBase +from app.api.common import error_out, run_api logger = get_logger("OCR API") @@ -37,8 +39,22 @@ # ========== 截图相关模型 ========== -class OCRScreenshotIn(BaseModel): +class OCRWindowIn(ApiModel): window_title: str = Field(..., description="窗口标题(用于查找窗口)") + + +class OCRRetryIn(OCRWindowIn): + interval: float = Field(default=0, description="截图间隔时间(秒)", ge=0) + retry_times: int = Field(default=1, description="重复截图次数", ge=1) + + +class OCRRetryThresholdIn(OCRRetryIn): + threshold: float = Field( + default=0.8, description="图像匹配阈值,范围 0-1", ge=0, le=1 + ) + + +class OCRScreenshotIn(OCRWindowIn): should_preprocess: bool = Field( default=True, description="是否预处理图片区域,True时排除边框和标题栏,False时使用完整窗口", @@ -59,7 +75,7 @@ class OCRScreenshotOut(OutBase): image_height: int = Field(..., description="截图高度") -class ADBScreenshotIn(BaseModel): +class ADBScreenshotIn(ApiModel): adb_path: str = Field(..., description="ADB 可执行文件的路径") serial: str = Field( ..., description="设备序列号,格式如 '127.0.0.1:5555' 或 'emulator-5554'" @@ -78,34 +94,16 @@ class ADBScreenshotOut(OutBase): # ========== 测试相关模型 ========== -class CheckImageIn(BaseModel): - window_title: str = Field(..., description="窗口标题(用于查找窗口)") +class CheckImageIn(OCRRetryThresholdIn): image_path: str = Field(..., description="要查找的图片路径") - interval: float = Field(default=0, description="截图间隔时间(秒)", ge=0) - retry_times: int = Field(default=1, description="重复截图次数", ge=1) - threshold: float = Field( - default=0.8, description="图像匹配阈值,范围 0-1", ge=0, le=1 - ) -class CheckImageAnyIn(BaseModel): - window_title: str = Field(..., description="窗口标题(用于查找窗口)") +class CheckImageAnyIn(OCRRetryThresholdIn): image_paths: list[str] = Field(..., description="要查找的图片路径列表") - interval: float = Field(default=0, description="截图间隔时间(秒)", ge=0) - retry_times: int = Field(default=1, description="重复截图次数", ge=1) - threshold: float = Field( - default=0.8, description="图像匹配阈值,范围 0-1", ge=0, le=1 - ) -class CheckImageAllIn(BaseModel): - window_title: str = Field(..., description="窗口标题(用于查找窗口)") +class CheckImageAllIn(OCRRetryThresholdIn): image_paths: list[str] = Field(..., description="要查找的图片路径列表") - interval: float = Field(default=0, description="截图间隔时间(秒)", ge=0) - retry_times: int = Field(default=1, description="重复截图次数", ge=1) - threshold: float = Field( - default=0.8, description="图像匹配阈值,范围 0-1", ge=0, le=1 - ) class CheckImageOut(OutBase): @@ -113,21 +111,12 @@ class CheckImageOut(OutBase): attempts: int = Field(..., description="实际尝试次数") -class ClickImageIn(BaseModel): - window_title: str = Field(..., description="窗口标题(用于查找窗口)") +class ClickImageIn(OCRRetryThresholdIn): image_path: str = Field(..., description="要查找并点击的图片路径") - interval: float = Field(default=0, description="截图间隔时间(秒)", ge=0) - retry_times: int = Field(default=1, description="重复截图次数", ge=1) - threshold: float = Field( - default=0.8, description="图像匹配阈值,范围 0-1", ge=0, le=1 - ) -class ClickTextIn(BaseModel): - window_title: str = Field(..., description="窗口标题(用于查找窗口)") +class ClickTextIn(OCRRetryIn): text: str = Field(..., description="要查找并点击的文字内容") - interval: float = Field(default=0, description="截图间隔时间(秒)", ge=0) - retry_times: int = Field(default=1, description="重复截图次数", ge=1) class ClickOut(OutBase): @@ -135,6 +124,12 @@ class ClickOut(OutBase): attempts: int = Field(..., description="实际尝试次数") +def _encode_image_base64(image: Image.Image) -> str: + buffer = BytesIO() + image.save(buffer, format="PNG") + return base64.b64encode(buffer.getvalue()).decode("utf-8") + + # ========== 截图接口 ========== @router.post( "/screenshot", @@ -158,7 +153,8 @@ async def get_screenshot(params: OCRScreenshotIn = Body(...)) -> OCRScreenshotOu Returns: OCRScreenshotOut: 包含Base64编码的截图和区域信息 """ - try: + + async def _success() -> OCRScreenshotOut: # 获取截图区域(如果没有提供自定义区域) if params.region is None: region = OCRTool.get_screenshot_region( @@ -174,16 +170,11 @@ async def get_screenshot(params: OCRScreenshotIn = Body(...)) -> OCRScreenshotOu region=region, ) - # 将PIL Image转换为Base64 - buffer = BytesIO() - screenshot_image.save(buffer, format="PNG") - image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") + image_base64 = _encode_image_base64(screenshot_image) logger.info(f"成功截取窗口 [{params.window_title}] 的截图,区域: {region}") return OCRScreenshotOut( - code=200, - status="success", message="截图成功", image_base64=image_base64, region=region, @@ -191,17 +182,16 @@ async def get_screenshot(params: OCRScreenshotIn = Body(...)) -> OCRScreenshotOu image_height=screenshot_image.height, ) - except Exception as e: - logger.error(f"截图失败: {type(e).__name__}: {str(e)}") - return OCRScreenshotOut( - code=500, - status="error", - message=f"截图失败: {type(e).__name__}: {str(e)}", - image_base64="", - region=(0, 0, 0, 0), - image_width=0, - image_height=0, - ) + return await run_api( + _success, + model_cls=OCRScreenshotOut, + message="截图失败", + image_base64="", + region=(0, 0, 0, 0), + image_width=0, + image_height=0, + on_error=lambda e: logger.error(f"截图失败: {type(e).__name__}: {str(e)}"), + ) @router.post( @@ -257,9 +247,9 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh except FileNotFoundError as e: logger.error(f"ADB 文件未找到: {str(e)}") - return ADBScreenshotOut( - code=404, - status="error", + return error_out( + ADBScreenshotOut, + e, message=f"ADB 文件未找到: {str(e)}", image_base64="", image_width=0, @@ -268,9 +258,9 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh ) except RuntimeError as e: logger.error(f"ADB 截图运行时错误: {str(e)}") - return ADBScreenshotOut( - code=500, - status="error", + return error_out( + ADBScreenshotOut, + e, message=f"ADB 截图失败: {str(e)}", image_base64="", image_width=0, @@ -279,9 +269,9 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh ) except Exception as e: logger.error(f"ADB 截图失败: {type(e).__name__}: {str(e)}") - return ADBScreenshotOut( - code=500, - status="error", + return error_out( + ADBScreenshotOut, + e, message=f"ADB 截图失败: {type(e).__name__}: {str(e)}", image_base64="", image_width=0, @@ -313,7 +303,8 @@ async def check_image(params: CheckImageIn = Body(...)) -> CheckImageOut: Returns: CheckImageOut: 包含查找结果和尝试次数 """ - try: + + async def _success() -> CheckImageOut: # 设置全局窗口标题 OCRTool.set_title(params.window_title) @@ -328,22 +319,19 @@ async def check_image(params: CheckImageIn = Body(...)) -> CheckImageOut: logger.info(f"图像检查完成: {params.image_path}, 结果: {found}") return CheckImageOut( - code=200, - status="success", message=f"图像检查完成,{'找到' if found else '未找到'}图像", found=found, attempts=params.retry_times, ) - except Exception as e: - logger.error(f"图像检查失败: {type(e).__name__}: {str(e)}") - return CheckImageOut( - code=500, - status="error", - message=f"图像检查失败: {type(e).__name__}: {str(e)}", - found=False, - attempts=0, - ) + return await run_api( + _success, + model_cls=CheckImageOut, + message="图像检查失败", + found=False, + attempts=0, + on_error=lambda e: logger.error(f"图像检查失败: {type(e).__name__}: {str(e)}"), + ) @router.post( @@ -368,7 +356,8 @@ async def check_image_any(params: CheckImageAnyIn = Body(...)) -> CheckImageOut: Returns: CheckImageOut: 包含查找结果和尝试次数 """ - try: + + async def _success() -> CheckImageOut: # 设置全局窗口标题 OCRTool.set_title(params.window_title) @@ -383,22 +372,21 @@ async def check_image_any(params: CheckImageAnyIn = Body(...)) -> CheckImageOut: logger.info(f"多图像检查(ANY)完成: {params.image_paths}, 结果: {found}") return CheckImageOut( - code=200, - status="success", message=f"多图像检查完成,{'找到任意一个' if found else '未找到任何'}图像", found=found, attempts=params.retry_times, ) - except Exception as e: - logger.error(f"多图像检查(ANY)失败: {type(e).__name__}: {str(e)}") - return CheckImageOut( - code=500, - status="error", - message=f"多图像检查失败: {type(e).__name__}: {str(e)}", - found=False, - attempts=0, - ) + return await run_api( + _success, + model_cls=CheckImageOut, + message="多图像检查失败", + found=False, + attempts=0, + on_error=lambda e: logger.error( + f"多图像检查(ANY)失败: {type(e).__name__}: {str(e)}" + ), + ) @router.post( @@ -423,7 +411,8 @@ async def check_image_all(params: CheckImageAllIn = Body(...)) -> CheckImageOut: Returns: CheckImageOut: 包含查找结果和尝试次数 """ - try: + + async def _success() -> CheckImageOut: # 设置全局窗口标题 OCRTool.set_title(params.window_title) @@ -438,22 +427,21 @@ async def check_image_all(params: CheckImageAllIn = Body(...)) -> CheckImageOut: logger.info(f"多图像检查(ALL)完成: {params.image_paths}, 结果: {found}") return CheckImageOut( - code=200, - status="success", message=f"多图像检查完成,{'找到所有' if found else '未找到所有'}图像", found=found, attempts=params.retry_times, ) - except Exception as e: - logger.error(f"多图像检查(ALL)失败: {type(e).__name__}: {str(e)}") - return CheckImageOut( - code=500, - status="error", - message=f"多图像检查失败: {type(e).__name__}: {str(e)}", - found=False, - attempts=0, - ) + return await run_api( + _success, + model_cls=CheckImageOut, + message="多图像检查失败", + found=False, + attempts=0, + on_error=lambda e: logger.error( + f"多图像检查(ALL)失败: {type(e).__name__}: {str(e)}" + ), + ) # ========== 测试接口:点击操作 ========== @@ -479,7 +467,8 @@ async def click_image(params: ClickImageIn = Body(...)) -> ClickOut: Returns: ClickOut: 包含点击结果和尝试次数 """ - try: + + async def _success() -> ClickOut: # 设置全局窗口标题 OCRTool.set_title(params.window_title) @@ -494,22 +483,19 @@ async def click_image(params: ClickImageIn = Body(...)) -> ClickOut: logger.info(f"图像点击完成: {params.image_path}, 结果: {success}") return ClickOut( - code=200, - status="success", message=f"图像点击{'成功' if success else '失败'}", success=success, attempts=params.retry_times, ) - except Exception as e: - logger.error(f"图像点击失败: {type(e).__name__}: {str(e)}") - return ClickOut( - code=500, - status="error", - message=f"图像点击失败: {type(e).__name__}: {str(e)}", - success=False, - attempts=0, - ) + return await run_api( + _success, + model_cls=ClickOut, + message="图像点击失败", + success=False, + attempts=0, + on_error=lambda e: logger.error(f"图像点击失败: {type(e).__name__}: {str(e)}"), + ) @router.post( @@ -533,7 +519,8 @@ async def click_text(params: ClickTextIn = Body(...)) -> ClickOut: Returns: ClickOut: 包含点击结果和尝试次数 """ - try: + + async def _success() -> ClickOut: # 设置全局窗口标题 OCRTool.set_title(params.window_title) @@ -545,19 +532,16 @@ async def click_text(params: ClickTextIn = Body(...)) -> ClickOut: logger.info(f"文字点击完成: '{params.text}', 结果: {success}") return ClickOut( - code=200, - status="success", message=f"文字点击{'成功' if success else '失败'}", success=success, attempts=params.retry_times, ) - except Exception as e: - logger.error(f"文字点击失败: {type(e).__name__}: {str(e)}") - return ClickOut( - code=500, - status="error", - message=f"文字点击失败: {type(e).__name__}: {str(e)}", - success=False, - attempts=0, - ) + return await run_api( + _success, + model_cls=ClickOut, + message="文字点击失败", + success=False, + attempts=0, + on_error=lambda e: logger.error(f"文字点击失败: {type(e).__name__}: {str(e)}"), + ) diff --git a/app/api/queue.py b/app/api/queue.py index e01a06a9..01c57dff 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -21,320 +21,458 @@ # Contact: DLmaster_361@163.com -from fastapi import APIRouter, Body +from typing import Annotated +from fastapi import APIRouter, Body, Path + +from app.api.common import api_delete, api_get, api_patch, api_post from app.core import Config from app.models.common_contract import ( + IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) from app.models.queue_contract import ( - QueueRead, QueueCreateOut, - QueueDeleteIn, - QueueGetIn, + QueueDetailOut, QueueGetOut, QueueIndexItem, - QueueItemRead, QueueItemCreateOut, - QueueItemDeleteIn, - QueueItemGetIn, + QueueItemDetailOut, QueueItemGetOut, QueueItemIndexItem, - QueueItemReorderIn, - QueueItemUpdateIn, - QueueReorderIn, - QueueSetInBase, - QueueUpdateIn, - TimeSetRead, + QueueItemPatch, + QueueItemRead, + QueuePatch, + QueueRead, TimeSetCreateOut, - TimeSetDeleteIn, - TimeSetGetIn, + TimeSetDetailOut, TimeSetGetOut, TimeSetIndexItem, - TimeSetReorderIn, - TimeSetUpdateIn, + TimeSetPatch, + TimeSetRead, ) -from app.api.ws_command import ws_command router = APIRouter(prefix="/api/queue", tags=["调度队列管理"]) +QueueIdPath = Annotated[str, Path(description="队列 ID")] +TimeSetIdPath = Annotated[str, Path(description="时间设置 ID")] +QueueItemIdPath = Annotated[str, Path(description="队列项 ID")] -@ws_command("queue.add") -@router.post( - "/add", - tags=["Add"], - summary="添加调度队列", - response_model=QueueCreateOut, - status_code=200, -) -async def add_queue() -> QueueCreateOut: - try: - uid, config = await Config.add_queue() - data = project_model(QueueRead, await config.toDict()) - except Exception as e: - return QueueCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - queueId="", - data=QueueRead(), - ) - return QueueCreateOut(queueId=str(uid), data=data) - - -@ws_command("queue.get") -@router.post( - "/get", - tags=["Get"], - summary="查询调度队列配置信息", - response_model=QueueGetOut, - status_code=200, -) -async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut: - try: - index, config = await Config.get_queue(queue.queueId) - index = project_model_list(QueueIndexItem, index) - data = project_model_map(QueueRead, config) - except Exception as e: - return QueueGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) - return QueueGetOut(index=index, data=data) - - -@router.post( - "/update", - tags=["Update"], - summary="更新调度队列配置信息", - response_model=OutBase, - status_code=200, -) -async def update_queue(queue: QueueUpdateIn = Body(...)) -> OutBase: - try: - await Config.update_queue( - queue.queueId, queue.data.model_dump(exclude_unset=True) - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + +async def _build_queue_collection_out() -> QueueGetOut: + index, data = await Config.get_queue(None) + return QueueGetOut( + index=project_model_list(QueueIndexItem, index), + data=project_model_map(QueueRead, data), + ) + + +async def _build_queue_detail_out(queue_id: str) -> QueueDetailOut: + _, data = await Config.get_queue(queue_id) + projected = project_model_map(QueueRead, data) + return QueueDetailOut(data=projected[queue_id]) + + +async def _build_queue_create_out() -> QueueCreateOut: + uid, config = await Config.add_queue() + return QueueCreateOut( + id=str(uid), + data=project_model(QueueRead, await config.toDict()), + ) + + +async def _update_queue_config(queue_id: str, data: QueuePatch) -> OutBase: + await Config.update_queue(queue_id, data.model_dump(exclude_unset=True)) return OutBase() -@router.post( - "/delete", - tags=["Delete"], - summary="删除调度队列", - response_model=OutBase, - status_code=200, -) -async def delete_queue(queue: QueueDeleteIn = Body(...)) -> OutBase: - try: - await Config.del_queue(queue.queueId) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def _delete_queue_config(queue_id: str) -> OutBase: + await Config.del_queue(queue_id) return OutBase() -@router.post( - "/order", - tags=["Update"], - summary="重新排序", - response_model=OutBase, - status_code=200, -) -async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_queue(script.indexList) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def _build_time_set_collection_out(queue_id: str) -> TimeSetGetOut: + index, data = await Config.get_time_set(queue_id, None) + return TimeSetGetOut( + index=project_model_list(TimeSetIndexItem, index), + data=project_model_map(TimeSetRead, data), + ) + + +async def _build_time_set_detail_out( + queue_id: str, time_set_id: str +) -> TimeSetDetailOut: + _, data = await Config.get_time_set(queue_id, time_set_id) + projected = project_model_map(TimeSetRead, data) + return TimeSetDetailOut(data=projected[time_set_id]) + + +async def _build_time_set_create_out(queue_id: str) -> TimeSetCreateOut: + uid, config = await Config.add_time_set(queue_id) + return TimeSetCreateOut( + id=str(uid), + data=project_model(TimeSetRead, await config.toDict()), + ) + + +async def _update_time_set_config( + queue_id: str, time_set_id: str, data: TimeSetPatch +) -> OutBase: + await Config.update_time_set(queue_id, time_set_id, data.model_dump(exclude_unset=True)) return OutBase() -@router.post( - "/time/get", - tags=["Get"], - summary="查询定时项", - response_model=TimeSetGetOut, - status_code=200, -) -async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut: - try: - index, data = await Config.get_time_set(time.queueId, time.timeSetId) - index = project_model_list(TimeSetIndexItem, index) - data = project_model_map(TimeSetRead, data) - except Exception as e: - return TimeSetGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) - return TimeSetGetOut(index=index, data=data) - - -@router.post( - "/time/add", - tags=["Add"], - summary="添加定时项", - response_model=TimeSetCreateOut, - status_code=200, -) -async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut: - uid, config = await Config.add_time_set(time.queueId) - data = project_model(TimeSetRead, await config.toDict()) - return TimeSetCreateOut(timeSetId=str(uid), data=data) - - -@router.post( - "/time/update", - tags=["Update"], - summary="更新定时项", - response_model=OutBase, - status_code=200, -) -async def update_time_set(time: TimeSetUpdateIn = Body(...)) -> OutBase: - try: - await Config.update_time_set( - time.queueId, time.timeSetId, time.data.model_dump(exclude_unset=True) - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def _delete_time_set_config(queue_id: str, time_set_id: str) -> OutBase: + await Config.del_time_set(queue_id, time_set_id) return OutBase() -@router.post( - "/time/delete", - tags=["Delete"], - summary="删除定时项", - response_model=OutBase, - status_code=200, -) -async def delete_time_set(time: TimeSetDeleteIn = Body(...)) -> OutBase: - try: - await Config.del_time_set(time.queueId, time.timeSetId) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def _build_queue_item_collection_out(queue_id: str) -> QueueItemGetOut: + index, data = await Config.get_queue_item(queue_id, None) + return QueueItemGetOut( + index=project_model_list(QueueItemIndexItem, index), + data=project_model_map(QueueItemRead, data), + ) + + +async def _build_queue_item_detail_out( + queue_id: str, queue_item_id: str +) -> QueueItemDetailOut: + _, data = await Config.get_queue_item(queue_id, queue_item_id) + projected = project_model_map(QueueItemRead, data) + return QueueItemDetailOut(data=projected[queue_item_id]) + + +async def _build_queue_item_create_out(queue_id: str) -> QueueItemCreateOut: + uid, config = await Config.add_queue_item(queue_id) + return QueueItemCreateOut( + id=str(uid), + data=project_model(QueueItemRead, await config.toDict()), + ) + + +async def _update_queue_item_config( + queue_id: str, queue_item_id: str, data: QueueItemPatch +) -> OutBase: + await Config.update_queue_item( + queue_id, queue_item_id, data.model_dump(exclude_unset=True) + ) return OutBase() -@router.post( - "/time/order", - tags=["Update"], - summary="重新排序定时项", - response_model=OutBase, - status_code=200, -) -async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_time_set(time.queueId, time.indexList) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def _delete_queue_item_config(queue_id: str, queue_item_id: str) -> OutBase: + await Config.del_queue_item(queue_id, queue_item_id) return OutBase() -@router.post( - "/item/get", - tags=["Get"], - summary="查询队列项", - response_model=QueueItemGetOut, - status_code=200, +@api_get( + router, + "", + model_cls=QueueGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询全部调度队列", + "response_model": QueueGetOut, + "status_code": 200, + }, ) -async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut: - try: - index, data = await Config.get_queue_item(item.queueId, item.queueItemId) - index = project_model_list(QueueItemIndexItem, index) - data = project_model_map(QueueItemRead, data) - except Exception as e: - return QueueItemGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) - return QueueItemGetOut(index=index, data=data) - - -@router.post( - "/item/add", - tags=["Add"], - summary="添加队列项", - response_model=QueueItemCreateOut, - status_code=200, +async def list_queues() -> QueueGetOut: + return await _build_queue_collection_out() + + +@api_post( + router, + "", + ws_endpoint="queue.add", + model_cls=QueueCreateOut, + id="", + data=QueueRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建调度队列", + "response_model": QueueCreateOut, + "status_code": 200, + }, ) -async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut: - uid, config = await Config.add_queue_item(item.queueId) - data = project_model(QueueItemRead, await config.toDict()) - return QueueItemCreateOut(queueItemId=str(uid), data=data) - - -@router.post( - "/item/update", - tags=["Update"], - summary="更新队列项", - response_model=OutBase, - status_code=200, +async def create_queue() -> QueueCreateOut: + return await _build_queue_create_out() + + +@api_patch( + router, + "/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序调度队列", + "response_model": OutBase, + "status_code": 200, + }, ) -async def update_item(item: QueueItemUpdateIn = Body(...)) -> OutBase: - try: - await Config.update_queue_item( - item.queueId, item.queueItemId, item.data.model_dump(exclude_unset=True) - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def reorder_queue(body: IndexOrderPatch = Body(...)) -> OutBase: + await Config.reorder_queue(body.indexList) return OutBase() -@router.post( - "/item/delete", - tags=["Delete"], - summary="删除队列项", - response_model=OutBase, - status_code=200, +@api_get( + router, + "/{queue_id}", + ws_endpoint="queue.get", + model_cls=QueueDetailOut, + data=QueueRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个调度队列", + "response_model": QueueDetailOut, + "status_code": 200, + }, +) +async def get_queue(queue_id: QueueIdPath) -> QueueDetailOut: + return await _build_queue_detail_out(queue_id) + + +@api_patch( + router, + "/{queue_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新调度队列", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_queue(queue_id: QueueIdPath, data: QueuePatch = Body(...)) -> OutBase: + return await _update_queue_config(queue_id, data) + + +@api_delete( + router, + "/{queue_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除调度队列", + "response_model": OutBase, + "status_code": 200, + }, ) -async def delete_item(item: QueueItemDeleteIn = Body(...)) -> OutBase: - try: - await Config.del_queue_item(item.queueId, item.queueItemId) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def delete_queue(queue_id: QueueIdPath) -> OutBase: + return await _delete_queue_config(queue_id) + + +@api_get( + router, + "/{queue_id}/times", + model_cls=TimeSetGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询队列下的全部定时项", + "response_model": TimeSetGetOut, + "status_code": 200, + }, +) +async def list_time_sets(queue_id: QueueIdPath) -> TimeSetGetOut: + return await _build_time_set_collection_out(queue_id) + + +@api_post( + router, + "/{queue_id}/times", + model_cls=TimeSetCreateOut, + id="", + data=TimeSetRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建定时项", + "response_model": TimeSetCreateOut, + "status_code": 200, + }, +) +async def create_time_set(queue_id: QueueIdPath) -> TimeSetCreateOut: + return await _build_time_set_create_out(queue_id) + + +@api_patch( + router, + "/{queue_id}/times/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序定时项", + "response_model": OutBase, + "status_code": 200, + }, +) +async def reorder_time_sets( + queue_id: QueueIdPath, body: IndexOrderPatch = Body(...) +) -> OutBase: + await Config.reorder_time_set(queue_id, body.indexList) return OutBase() -@router.post( - "/item/order", - tags=["Update"], - summary="重新排序队列项", - response_model=OutBase, - status_code=200, +@api_get( + router, + "/{queue_id}/times/{time_set_id}", + model_cls=TimeSetDetailOut, + data=TimeSetRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个定时项", + "response_model": TimeSetDetailOut, + "status_code": 200, + }, +) +async def get_time_set( + queue_id: QueueIdPath, time_set_id: TimeSetIdPath +) -> TimeSetDetailOut: + return await _build_time_set_detail_out(queue_id, time_set_id) + + +@api_patch( + router, + "/{queue_id}/times/{time_set_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新定时项", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_time_set( + queue_id: QueueIdPath, + time_set_id: TimeSetIdPath, + data: TimeSetPatch = Body(...), +) -> OutBase: + return await _update_time_set_config(queue_id, time_set_id, data) + + +@api_delete( + router, + "/{queue_id}/times/{time_set_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除定时项", + "response_model": OutBase, + "status_code": 200, + }, +) +async def delete_time_set( + queue_id: QueueIdPath, time_set_id: TimeSetIdPath +) -> OutBase: + return await _delete_time_set_config(queue_id, time_set_id) + + +@api_get( + router, + "/{queue_id}/items", + model_cls=QueueItemGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询队列下的全部队列项", + "response_model": QueueItemGetOut, + "status_code": 200, + }, +) +async def list_queue_items(queue_id: QueueIdPath) -> QueueItemGetOut: + return await _build_queue_item_collection_out(queue_id) + + +@api_post( + router, + "/{queue_id}/items", + model_cls=QueueItemCreateOut, + id="", + data=QueueItemRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建队列项", + "response_model": QueueItemCreateOut, + "status_code": 200, + }, ) -async def reorder_item(item: QueueItemReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_queue_item(item.queueId, item.indexList) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def create_queue_item(queue_id: QueueIdPath) -> QueueItemCreateOut: + return await _build_queue_item_create_out(queue_id) + + +@api_patch( + router, + "/{queue_id}/items/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序队列项", + "response_model": OutBase, + "status_code": 200, + }, +) +async def reorder_queue_items( + queue_id: QueueIdPath, body: IndexOrderPatch = Body(...) +) -> OutBase: + await Config.reorder_queue_item(queue_id, body.indexList) return OutBase() + + +@api_get( + router, + "/{queue_id}/items/{queue_item_id}", + model_cls=QueueItemDetailOut, + data=QueueItemRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个队列项", + "response_model": QueueItemDetailOut, + "status_code": 200, + }, +) +async def get_queue_item( + queue_id: QueueIdPath, queue_item_id: QueueItemIdPath +) -> QueueItemDetailOut: + return await _build_queue_item_detail_out(queue_id, queue_item_id) + + +@api_patch( + router, + "/{queue_id}/items/{queue_item_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新队列项", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_queue_item( + queue_id: QueueIdPath, + queue_item_id: QueueItemIdPath, + data: QueueItemPatch = Body(...), +) -> OutBase: + return await _update_queue_item_config(queue_id, queue_item_id, data) + + +@api_delete( + router, + "/{queue_id}/items/{queue_item_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除队列项", + "response_model": OutBase, + "status_code": 200, + }, +) +async def delete_queue_item( + queue_id: QueueIdPath, queue_item_id: QueueItemIdPath +) -> OutBase: + return await _delete_queue_item_config(queue_id, queue_item_id) diff --git a/app/api/scripts.py b/app/api/scripts.py index c0555e0f..e9bb9bcd 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -22,38 +22,36 @@ import uuid -from fastapi import APIRouter, Body +from typing import Annotated +from fastapi import APIRouter, Body, Path +from pydantic import TypeAdapter + +from app.api.common import api_delete, api_get, api_patch, api_post, error_out from app.core import Config from app.models.common_contract import ( ComboBoxItem, ComboBoxOut, + IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) from app.models.scripts_contract import ( + InfrastructureImportBody, ScriptCreateIn, ScriptCreateOut, - ScriptDeleteIn, - ScriptFileIn, - ScriptGetIn, + ScriptDetailOut, + ScriptFileBody, ScriptGetOut, ScriptIndexItem, - ScriptReorderIn, - ScriptUpdateIn, - ScriptUploadIn, - ScriptUrlIn, + ScriptUploadBody, + ScriptUrlBody, UserCreateOut, - UserDeleteIn, - UserGetIn, + UserDetailOut, UserGetOut, - UserInBase, UserIndexItem, - UserReorderIn, - UserSetIn, - UserUpdateIn, project_script_model, project_script_model_map, project_user_model, @@ -66,231 +64,292 @@ ) from app.models.setting_contract import ( WebhookCreateOut, - WebhookDeleteIn, - WebhookGetIn, + WebhookDetailOut, WebhookGetOut, - WebhookInBase, WebhookIndexItem, + WebhookPatch, WebhookRead, - WebhookReorderIn, - WebhookUpdateIn, ) + +COMBOBOX_ITEMS_ADAPTER: TypeAdapter[list[ComboBoxItem]] = TypeAdapter( + list[ComboBoxItem] +) + router = APIRouter(prefix="/api/scripts", tags=["脚本管理"]) +ScriptIdPath = Annotated[str, Path(description="脚本 ID")] +UserIdPath = Annotated[str, Path(description="用户 ID")] +WebhookIdPath = Annotated[str, Path(description="Webhook ID")] + + +async def _build_script_collection_out() -> ScriptGetOut: + index, data = await Config.get_script(None) + script_index = project_model_list(ScriptIndexItem, index) + return ScriptGetOut(index=script_index, data=project_script_model_map(script_index, data)) + + +async def _build_script_detail_out(script_id: str) -> ScriptDetailOut: + index, data = await Config.get_script(script_id) + script_index = project_model_list(ScriptIndexItem, index) + projected = project_script_model_map(script_index, data) + return ScriptDetailOut(data=projected[script_id]) + + +async def _build_user_collection_out(script_id: str) -> UserGetOut: + index, data = await Config.get_user(script_id, None) + user_index = project_model_list(UserIndexItem, index) + return UserGetOut(index=user_index, data=project_user_model_map(user_index, data)) + + +async def _build_user_detail_out(script_id: str, user_id: str) -> UserDetailOut: + index, data = await Config.get_user(script_id, user_id) + user_index = project_model_list(UserIndexItem, index) + projected = project_user_model_map(user_index, data) + return UserDetailOut(data=projected[user_id]) + + +async def _build_webhook_collection_out(script_id: str, user_id: str) -> WebhookGetOut: + index, data = await Config.get_webhook(script_id, user_id, None) + return WebhookGetOut( + index=project_model_list(WebhookIndexItem, index), + data=project_model_map(WebhookRead, data), + ) + + +async def _build_webhook_detail_out( + script_id: str, user_id: str, webhook_id: str +) -> WebhookDetailOut: + _, data = await Config.get_webhook(script_id, user_id, webhook_id) + projected = project_model_map(WebhookRead, data) + return WebhookDetailOut(data=projected[webhook_id]) + + +@api_get( + router, + "", + model_cls=ScriptGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询全部脚本", + "response_model": ScriptGetOut, + "status_code": 200, + }, +) +async def list_scripts() -> ScriptGetOut: + return await _build_script_collection_out() + @router.post( - "/add", + "", tags=["Add"], - summary="添加脚本", + summary="创建脚本", response_model=ScriptCreateOut, status_code=200, ) -async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: +async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: try: - uid, config = await Config.add_script(script.type, script.scriptId) + uid, config = await Config.add_script(script.type, script.copyFromId) data = project_script_model( script_contract_type_from_runtime(type(config).__name__), await config.toDict(), ) except Exception as e: - return ScriptCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - scriptId="", + return error_out( + ScriptCreateOut, + e, + id="", data=project_script_model( script_contract_type_from_create(script.type), {}, ), ) - return ScriptCreateOut(scriptId=str(uid), data=data) + return ScriptCreateOut(id=str(uid), data=data) -@router.post( - "/get", - tags=["Get"], - summary="查询脚本配置信息", - response_model=ScriptGetOut, - status_code=200, -) -async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut: - try: - index, data = await Config.get_script(script.scriptId) - index = project_model_list(ScriptIndexItem, index) - data = project_script_model_map(index, data) - except Exception as e: - return ScriptGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) - return ScriptGetOut(index=index, data=data) - - -@router.post( - "/update", - tags=["Update"], - summary="更新脚本配置信息", - response_model=OutBase, - status_code=200, +@api_patch( + router, + "/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序脚本", + "response_model": OutBase, + "status_code": 200, + }, ) -async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase: - try: - script_type = script_contract_type_from_runtime( - type(Config.ScriptConfig[uuid.UUID(script.scriptId)]).__name__ - ) - await Config.update_script( - script.scriptId, validate_script_patch_data(script_type, script.data) - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: + await Config.reorder_script(body.indexList) return OutBase() -@router.post( - "/delete", - tags=["Delete"], - summary="删除脚本", - response_model=OutBase, +@router.get( + "/{script_id}", + tags=["Get"], + summary="查询单个脚本", + response_model=ScriptDetailOut, status_code=200, ) -async def delete_script(script: ScriptDeleteIn = Body(...)) -> OutBase: +async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: try: - await Config.del_script(script.scriptId) + return await _build_script_detail_out(script_id) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + script_type = "GeneralConfig" + try: + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ + ) + except Exception: + pass + return error_out( + ScriptDetailOut, + e, + data=project_script_model(script_type, {}), + ) + + +@api_patch( + router, + "/{script_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新脚本配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_script( + script_id: ScriptIdPath, data: dict[str, object] = Body(...) +) -> OutBase: + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ + ) + await Config.update_script(script_id, validate_script_patch_data(script_type, data)) return OutBase() -@router.post( - "/order", - tags=["Update"], - summary="重新排序脚本", - response_model=OutBase, - status_code=200, +@api_delete( + router, + "/{script_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除脚本", + "response_model": OutBase, + "status_code": 200, + }, ) -async def reorder_script(script: ScriptReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_script(script.indexList) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def delete_script(script_id: ScriptIdPath) -> OutBase: + await Config.del_script(script_id) return OutBase() -@router.post( - "/import/file", - tags=["Update"], - summary="从文件加载脚本配置", - response_model=OutBase, - status_code=200, +@api_post( + router, + "/{script_id}/actions/import-file", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "从文件导入脚本配置", + "response_model": OutBase, + "status_code": 200, + }, ) -async def import_script_from_file(script: ScriptFileIn = Body(...)) -> OutBase: - try: - await Config.import_script_from_file(script.scriptId, script.jsonFile) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def import_script_from_file( + script_id: ScriptIdPath, body: ScriptFileBody = Body(...) +) -> OutBase: + await Config.import_script_from_file(script_id, body.path) return OutBase() -@router.post( - "/export/file", - tags=["Action"], - summary="导出脚本配置到文件", - response_model=OutBase, - status_code=200, +@api_post( + router, + "/{script_id}/actions/export-file", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "导出脚本配置到文件", + "response_model": OutBase, + "status_code": 200, + }, ) -async def export_script_to_file(script: ScriptFileIn = Body(...)) -> OutBase: - try: - await Config.export_script_to_file(script.scriptId, script.jsonFile) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def export_script_to_file( + script_id: ScriptIdPath, body: ScriptFileBody = Body(...) +) -> OutBase: + await Config.export_script_to_file(script_id, body.path) return OutBase() -@router.post( - "/import/web", - tags=["Update"], - summary="从网络加载脚本配置", - response_model=OutBase, - status_code=200, +@api_post( + router, + "/{script_id}/actions/import-web", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "从网络导入脚本配置", + "response_model": OutBase, + "status_code": 200, + }, ) -async def import_script_from_web(script: ScriptUrlIn = Body(...)) -> OutBase: - try: - await Config.import_script_from_web(script.scriptId, script.url) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def import_script_from_web( + script_id: ScriptIdPath, body: ScriptUrlBody = Body(...) +) -> OutBase: + await Config.import_script_from_web(script_id, body.url) return OutBase() -@router.post( - "/Upload/web", - tags=["Action"], - summary="上传脚本配置到网络", - response_model=OutBase, - status_code=200, +@api_post( + router, + "/{script_id}/actions/upload-web", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "上传脚本配置到网络", + "response_model": OutBase, + "status_code": 200, + }, ) -async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase: - try: - await Config.upload_script_to_web( - script.scriptId, script.config_name, script.author, script.description - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def upload_script_to_web( + script_id: ScriptIdPath, body: ScriptUploadBody = Body(...) +) -> OutBase: + await Config.upload_script_to_web( + script_id, body.config_name, body.author, body.description + ) return OutBase() -@router.post( - "/user/get", - tags=["Get"], - summary="查询用户", - response_model=UserGetOut, - status_code=200, +@api_get( + router, + "/{script_id}/users", + model_cls=UserGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询脚本下的全部用户", + "response_model": UserGetOut, + "status_code": 200, + }, ) -async def get_user(user: UserGetIn = Body(...)) -> UserGetOut: - try: - index, data = await Config.get_user(user.scriptId, user.userId) - index = project_model_list(UserIndexItem, index) - data = project_user_model_map(index, data) - except Exception as e: - return UserGetOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - index=[], - data={}, - ) - return UserGetOut(index=index, data=data) +async def list_users(script_id: ScriptIdPath) -> UserGetOut: + return await _build_user_collection_out(script_id) @router.post( - "/user/add", + "/{script_id}/users", tags=["Add"], - summary="添加用户", + summary="创建用户", response_model=UserCreateOut, status_code=200, ) -async def add_user(user: UserInBase = Body(...)) -> UserCreateOut: +async def create_user(script_id: ScriptIdPath) -> UserCreateOut: script_type = None try: - uid, config = await Config.add_user(user.scriptId) + uid, config = await Config.add_user(script_id) script_type = script_contract_type_from_runtime( - type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__ + type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ ) user_type = user_contract_type_from_script(script_type) data = project_user_model(user_type, await config.toDict()) @@ -300,212 +359,258 @@ async def add_user(user: UserInBase = Body(...)) -> UserCreateOut: if script_type is not None else "GeneralUserConfig" ) - return UserCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - userId="", + return error_out( + UserCreateOut, + e, + id="", data=project_user_model(user_type, {}), ) - return UserCreateOut(userId=str(uid), data=data) + return UserCreateOut(id=str(uid), data=data) -@router.post( - "/user/update", - tags=["Update"], - summary="更新用户配置信息", - response_model=OutBase, - status_code=200, +@api_patch( + router, + "/{script_id}/users/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序用户", + "response_model": OutBase, + "status_code": 200, + }, ) -async def update_user(user: UserUpdateIn = Body(...)) -> OutBase: - try: - script_type = script_contract_type_from_runtime( - type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__ - ) - user_type = user_contract_type_from_script(script_type) - await Config.update_user( - user.scriptId, - user.userId, - validate_user_patch_data(user_type, user.data), - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def reorder_users( + script_id: ScriptIdPath, body: IndexOrderPatch = Body(...) +) -> OutBase: + await Config.reorder_user(script_id, body.indexList) return OutBase() -@router.post( - "/user/delete", - tags=["Delete"], - summary="删除用户", - response_model=OutBase, +@router.get( + "/{script_id}/users/{user_id}", + tags=["Get"], + summary="查询单个用户", + response_model=UserDetailOut, status_code=200, ) -async def delete_user(user: UserDeleteIn = Body(...)) -> OutBase: +async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOut: try: - await Config.del_user(user.scriptId, user.userId) + return await _build_user_detail_out(script_id, user_id) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" + user_type = "GeneralUserConfig" + try: + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ + ) + user_type = user_contract_type_from_script(script_type) + except Exception: + pass + return error_out( + UserDetailOut, + e, + data=project_user_model(user_type, {}), ) + + +@api_patch( + router, + "/{script_id}/users/{user_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新用户配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_user( + script_id: ScriptIdPath, + user_id: UserIdPath, + data: dict[str, object] = Body(...), +) -> OutBase: + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ + ) + user_type = user_contract_type_from_script(script_type) + await Config.update_user(script_id, user_id, validate_user_patch_data(user_type, data)) return OutBase() -@router.post( - "/user/order", - tags=["Update"], - summary="重新排序用户", - response_model=OutBase, - status_code=200, +@api_delete( + router, + "/{script_id}/users/{user_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除用户", + "response_model": OutBase, + "status_code": 200, + }, ) -async def reorder_user(user: UserReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_user(user.scriptId, user.indexList) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def delete_user(script_id: ScriptIdPath, user_id: UserIdPath) -> OutBase: + await Config.del_user(script_id, user_id) return OutBase() -@router.post( - "/user/infrastructure", - tags=["Update"], - summary="导入基建配置文件", - response_model=OutBase, - status_code=200, +@api_post( + router, + "/{script_id}/users/{user_id}/actions/import-infrastructure", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "导入基建配置文件", + "response_model": OutBase, + "status_code": 200, + }, ) -async def import_infrastructure(user: UserSetIn = Body(...)) -> OutBase: - try: - await Config.set_infrastructure(user.scriptId, user.userId, user.jsonFile) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def import_infrastructure( + script_id: ScriptIdPath, + user_id: UserIdPath, + body: InfrastructureImportBody = Body(...), +) -> OutBase: + await Config.set_infrastructure(script_id, user_id, body.path) return OutBase() -@router.post( - "/user/combox/infrastructure", +@router.get( + "/{script_id}/users/{user_id}/infrastructure-options", tags=["Get"], summary="用户自定义基建排班可选项", response_model=ComboBoxOut, status_code=200, ) -async def get_user_combox_infrastructure(user: UserDeleteIn = Body(...)) -> ComboBoxOut: +async def get_user_infrastructure_options( + script_id: ScriptIdPath, user_id: UserIdPath +) -> ComboBoxOut: try: - raw_data = await Config.get_user_combox_infrastructure( - user.scriptId, user.userId - ) - data = [ComboBoxItem(**item) for item in raw_data] if raw_data else [] + raw_data = await Config.get_user_combox_infrastructure(script_id, user_id) + data = COMBOBOX_ITEMS_ADAPTER.validate_python(raw_data or []) except Exception as e: - return ComboBoxOut( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[] - ) + return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) -@router.post( - "/webhook/get", - tags=["Get"], - summary="查询 webhook 配置", - response_model=WebhookGetOut, - status_code=200, +@api_get( + router, + "/{script_id}/users/{user_id}/webhooks", + model_cls=WebhookGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询用户下的全部 Webhook", + "response_model": WebhookGetOut, + "status_code": 200, + }, ) -async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: - try: - index, data = await Config.get_webhook( - webhook.scriptId, webhook.userId, webhook.webhookId - ) - index = project_model_list(WebhookIndexItem, index) - data = project_model_map(WebhookRead, data) - except Exception as 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/add", - tags=["Add"], - summary="添加webhook项", - response_model=WebhookCreateOut, - status_code=200, +async def list_user_webhooks( + script_id: ScriptIdPath, user_id: UserIdPath +) -> WebhookGetOut: + return await _build_webhook_collection_out(script_id, user_id) + + +@api_post( + router, + "/{script_id}/users/{user_id}/webhooks", + model_cls=WebhookCreateOut, + id="", + data=WebhookRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建用户 Webhook", + "response_model": WebhookCreateOut, + "status_code": 200, + }, ) -async def add_webhook(webhook: WebhookInBase = Body(...)) -> WebhookCreateOut: - try: - uid, config = await Config.add_webhook(webhook.scriptId, webhook.userId) - data = project_model(WebhookRead, await config.toDict()) - except Exception as e: - return WebhookCreateOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - webhookId="", - data=WebhookRead(), - ) - return WebhookCreateOut(webhookId=str(uid), data=data) - - -@router.post( - "/webhook/update", - tags=["Update"], - summary="更新webhook项", - response_model=OutBase, - status_code=200, +async def create_user_webhook( + script_id: ScriptIdPath, user_id: UserIdPath +) -> WebhookCreateOut: + uid, config = await Config.add_webhook(script_id, user_id) + return WebhookCreateOut( + id=str(uid), + data=project_model(WebhookRead, await config.toDict()), + ) + + +@api_patch( + router, + "/{script_id}/users/{user_id}/webhooks/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序用户 Webhook", + "response_model": OutBase, + "status_code": 200, + }, ) -async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase: - try: - await Config.update_webhook( - webhook.scriptId, - webhook.userId, - 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)}" - ) +async def reorder_user_webhooks( + script_id: ScriptIdPath, + user_id: UserIdPath, + body: IndexOrderPatch = Body(...), +) -> OutBase: + await Config.reorder_webhook(script_id, user_id, body.indexList) return OutBase() -@router.post( - "/webhook/delete", - tags=["Delete"], - summary="删除webhook项", - response_model=OutBase, +@router.get( + "/{script_id}/users/{user_id}/webhooks/{webhook_id}", + tags=["Get"], + summary="查询单个用户 Webhook", + response_model=WebhookDetailOut, status_code=200, ) -async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase: +async def get_user_webhook( + script_id: ScriptIdPath, + user_id: UserIdPath, + webhook_id: WebhookIdPath, +) -> WebhookDetailOut: try: - await Config.del_webhook(webhook.scriptId, webhook.userId, webhook.webhookId) + return await _build_webhook_detail_out(script_id, user_id, webhook_id) except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + return error_out(WebhookDetailOut, e, data=WebhookRead()) + + +@api_patch( + router, + "/{script_id}/users/{user_id}/webhooks/{webhook_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新用户 Webhook", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_user_webhook( + script_id: ScriptIdPath, + user_id: UserIdPath, + webhook_id: WebhookIdPath, + data: WebhookPatch = Body(...), +) -> OutBase: + await Config.update_webhook( + script_id, + user_id, + webhook_id, + data.model_dump(exclude_unset=True, exclude_none=True), + ) return OutBase() -@router.post( - "/webhook/order", - tags=["Update"], - summary="重新排序webhook项", - response_model=OutBase, - status_code=200, +@api_delete( + router, + "/{script_id}/users/{user_id}/webhooks/{webhook_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除用户 Webhook", + "response_model": OutBase, + "status_code": 200, + }, ) -async def reorder_webhook(webhook: WebhookReorderIn = Body(...)) -> OutBase: - try: - await Config.reorder_webhook( - webhook.scriptId, webhook.userId, webhook.indexList - ) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) +async def delete_user_webhook( + script_id: ScriptIdPath, + user_id: UserIdPath, + webhook_id: WebhookIdPath, +) -> OutBase: + await Config.del_webhook(script_id, user_id, webhook_id) return OutBase() diff --git a/app/api/setting.py b/app/api/setting.py index db186df4..f69d7bf1 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -21,186 +21,245 @@ # Contact: DLmaster_361@163.com -from fastapi import APIRouter, Body +from typing import Annotated + +from fastapi import APIRouter, Body, Path + +from app.api.common import api_delete, api_get, api_patch, api_post from app.core import Config -from app.services import Notify +from app.models import Webhook as WebhookConfig from app.models.common_contract import ( + IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) from app.models.setting_contract import ( + GlobalConfigPatch, GlobalConfigRead, SettingGetOut, - SettingUpdateIn, WebhookCreateOut, - WebhookDeleteIn, - WebhookGetIn, + WebhookDetailOut, WebhookGetOut, WebhookIndexItem, + WebhookPatch, WebhookRead, - WebhookReorderIn, - WebhookTestIn, - WebhookUpdateIn, ) -from app.models import Webhook as WebhookConfig -from app.api.common import error_out +from app.services import Notify router = APIRouter(prefix="/api/setting", tags=["全局设置"]) +WebhookIdPath = Annotated[str, Path(description="Webhook ID")] -@router.post( - "/get", - tags=["Get"], - summary="查询配置", - response_model=SettingGetOut, - status_code=200, -) -async def get_scripts() -> SettingGetOut: - """查询配置""" - - try: - data = await Config.get_setting() - except Exception as e: - return error_out(SettingGetOut, e, data=GlobalConfigRead()) - return SettingGetOut(data=project_model(GlobalConfigRead, data)) - - -@router.post( - "/update", - tags=["Update"], - summary="更新配置", - response_model=OutBase, - status_code=200, -) -async def update_script(script: SettingUpdateIn = Body(...)) -> OutBase: - """更新配置""" - try: - data = script.data.model_dump(exclude_unset=True) - await Config.update_setting(data) +async def _build_setting_out() -> SettingGetOut: + return SettingGetOut(data=project_model(GlobalConfigRead, await Config.get_setting())) + - except Exception as e: - return error_out(OutBase, e) +async def _update_setting_config(data: GlobalConfigPatch) -> OutBase: + await Config.update_setting(data.model_dump(exclude_unset=True)) return OutBase() -@router.post( - "/test_notify", - tags=["Action"], - summary="测试通知", - response_model=OutBase, - status_code=200, -) -async def test_notify() -> OutBase: - """测试通知""" +async def _build_webhook_collection_out() -> WebhookGetOut: + index, data = await Config.get_webhook(None, None, None) + return WebhookGetOut( + index=project_model_list(WebhookIndexItem, index), + data=project_model_map(WebhookRead, data), + ) - try: - await Notify.send_test_notification() - except Exception as e: - return error_out(OutBase, e) + +async def _build_webhook_detail_out(webhook_id: str) -> WebhookDetailOut: + _, data = await Config.get_webhook(None, None, webhook_id) + projected = project_model_map(WebhookRead, data) + return WebhookDetailOut(data=projected[webhook_id]) + + +async def _build_webhook_create_out() -> WebhookCreateOut: + uid, config = await Config.add_webhook(None, None) + return WebhookCreateOut( + id=str(uid), + data=project_model(WebhookRead, await config.toDict()), + ) + + +async def _update_webhook_config(webhook_id: str, data: WebhookPatch) -> OutBase: + await Config.update_webhook(None, None, webhook_id, data.model_dump(exclude_unset=True)) return OutBase() -@router.post( - "/webhook/get", - tags=["Get"], - summary="查询 webhook 配置", - response_model=WebhookGetOut, - status_code=200, +async def _delete_webhook_config(webhook_id: str) -> OutBase: + await Config.del_webhook(None, None, webhook_id) + return OutBase() + + +async def _test_webhook_config(data: WebhookPatch) -> OutBase: + webhook_config = WebhookConfig() + await webhook_config.load(data.model_dump(exclude_unset=True)) + await Notify.WebhookPush( + "AUTO-MAS Webhook测试", + "这是一条测试消息,如果您收到此消息,说明Webhook配置正确!", + webhook_config, + ) + return OutBase() + + +@api_get( + router, + "", + model_cls=SettingGetOut, + data=GlobalConfigRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询全局配置", + "response_model": SettingGetOut, + "status_code": 200, + }, ) -async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut: - try: - index, data = await Config.get_webhook(None, None, webhook.webhookId) - index = project_model_list(WebhookIndexItem, index) - data = project_model_map(WebhookRead, data) - except Exception as e: - return error_out(WebhookGetOut, e, index=[], data={}) - return WebhookGetOut(index=index, data=data) - - -@router.post( - "/webhook/add", - tags=["Add"], - summary="添加webhook项", - response_model=WebhookCreateOut, - status_code=200, +async def get_setting() -> SettingGetOut: + return await _build_setting_out() + + +@api_patch( + router, + "", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新全局配置", + "response_model": OutBase, + "status_code": 200, + }, ) -async def add_webhook() -> WebhookCreateOut: - try: - uid, config = await Config.add_webhook(None, None) - data = project_model(WebhookRead, await config.toDict()) - except Exception as e: - return error_out(WebhookCreateOut, e, webhookId="", data=WebhookRead()) - return WebhookCreateOut(webhookId=str(uid), data=data) - - -@router.post( - "/webhook/update", - tags=["Update"], - summary="更新webhook项", - response_model=OutBase, - status_code=200, +async def update_setting(data: GlobalConfigPatch = Body(...)) -> OutBase: + return await _update_setting_config(data) + + +@api_post( + router, + "/actions/test-notify", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "测试通知", + "response_model": OutBase, + "status_code": 200, + }, ) -async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase: - try: - await Config.update_webhook( - None, None, webhook.webhookId, webhook.data.model_dump(exclude_unset=True) - ) - except Exception as e: - return error_out(OutBase, e) +async def test_notify() -> OutBase: + await Notify.send_test_notification() return OutBase() -@router.post( - "/webhook/delete", - tags=["Delete"], - summary="删除webhook项", - response_model=OutBase, - status_code=200, +@api_get( + router, + "/webhooks", + model_cls=WebhookGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询全部全局 Webhook 配置", + "response_model": WebhookGetOut, + "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 error_out(OutBase, e) - return OutBase() +async def list_webhooks() -> WebhookGetOut: + return await _build_webhook_collection_out() -@router.post( - "/webhook/order", - tags=["Update"], - summary="重新排序webhook项", - response_model=OutBase, - status_code=200, +@api_post( + router, + "/webhooks", + model_cls=WebhookCreateOut, + id="", + data=WebhookRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建全局 Webhook 配置", + "response_model": WebhookCreateOut, + "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 error_out(OutBase, e) - return OutBase() +async def create_webhook() -> WebhookCreateOut: + return await _build_webhook_create_out() -@router.post( - "/webhook/test", - tags=["Action"], - summary="测试Webhook配置", - response_model=OutBase, - status_code=200, +@api_patch( + router, + "/webhooks/order", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序全局 Webhook", + "response_model": OutBase, + "status_code": 200, + }, ) -async def test_webhook(webhook: WebhookTestIn = Body(...)) -> OutBase: - """测试自定义Webhook""" - - try: - webhook_config = WebhookConfig() - await webhook_config.load(webhook.data.model_dump(exclude_unset=True)) - await Notify.WebhookPush( - "AUTO-MAS Webhook测试", - "这是一条测试消息,如果您收到此消息,说明Webhook配置正确!", - webhook_config, - ) - except Exception as e: - return error_out(OutBase, e, message=f"Webhook测试失败: {str(e)}") +async def reorder_webhooks(body: IndexOrderPatch = Body(...)) -> OutBase: + await Config.reorder_webhook(None, None, body.indexList) return OutBase() + + +@api_post( + router, + "/webhooks/test", + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "测试指定 Webhook 配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def test_webhook(data: WebhookPatch = Body(...)) -> OutBase: + return await _test_webhook_config(data) + + +@api_get( + router, + "/webhooks/{webhook_id}", + model_cls=WebhookDetailOut, + data=WebhookRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个全局 Webhook 配置", + "response_model": WebhookDetailOut, + "status_code": 200, + }, +) +async def get_webhook(webhook_id: WebhookIdPath) -> WebhookDetailOut: + return await _build_webhook_detail_out(webhook_id) + + +@api_patch( + router, + "/webhooks/{webhook_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新全局 Webhook 配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_webhook( + webhook_id: WebhookIdPath, data: WebhookPatch = Body(...) +) -> OutBase: + return await _update_webhook_config(webhook_id, data) + + +@api_delete( + router, + "/webhooks/{webhook_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除全局 Webhook 配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def delete_webhook(webhook_id: WebhookIdPath) -> OutBase: + return await _delete_webhook_config(webhook_id) diff --git a/app/api/tools.py b/app/api/tools.py index 183cfab8..47bc0c01 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -22,45 +22,50 @@ from fastapi import APIRouter, Body + +from app.api.common import api_get, api_patch from app.core import Config from app.models.common_contract import OutBase, project_model -from app.models.tools_contract import ToolsConfigRead, ToolsGetOut, ToolsUpdateIn -from app.api.common import error_out +from app.models.tools_contract import ToolsConfigPatch, ToolsConfigRead, ToolsGetOut router = APIRouter(prefix="/api/tools", tags=["工具设置"]) -@router.post( - "/get", - tags=["Get"], - summary="查询工具配置", - response_model=ToolsGetOut, - status_code=200, -) -async def get_tools() -> ToolsGetOut: - """查询工具配置""" +async def _build_tools_out() -> ToolsGetOut: + return ToolsGetOut(data=project_model(ToolsConfigRead, await Config.get_tools())) + - try: - data = await Config.get_tools() - except Exception as e: - return error_out(ToolsGetOut, e, data=ToolsConfigRead()) - return ToolsGetOut(data=project_model(ToolsConfigRead, data)) +async def _update_tools_config(data: ToolsConfigPatch) -> OutBase: + await Config.update_tools(data.model_dump(exclude_unset=True)) + return OutBase() -@router.post( - "/update", - tags=["Update"], - summary="更新工具配置", - response_model=OutBase, - status_code=200, +@api_get( + router, + "", + model_cls=ToolsGetOut, + data=ToolsConfigRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询工具配置", + "response_model": ToolsGetOut, + "status_code": 200, + }, ) -async def update_tools(script: ToolsUpdateIn = Body(...)) -> OutBase: - """更新工具配置""" +async def get_tools() -> ToolsGetOut: + return await _build_tools_out() - try: - data = script.data.model_dump(exclude_unset=True) - await Config.update_tools(data) - except Exception as e: - return error_out(OutBase, e) - return OutBase() +@api_patch( + router, + "", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新工具配置", + "response_model": OutBase, + "status_code": 200, + }, +) +async def update_tools(data: ToolsConfigPatch = Body(...)) -> OutBase: + return await _update_tools_config(data) diff --git a/app/api/update.py b/app/api/update.py index a2d5b6ee..91543b81 100644 --- a/app/api/update.py +++ b/app/api/update.py @@ -22,75 +22,109 @@ import asyncio -from fastapi import APIRouter, Body +from contextlib import suppress +from typing import Annotated, Any + +from fastapi import APIRouter, Body, Depends from app.core import Config from app.services import Updater from app.models.common_contract import OutBase from app.models.update_contract import UpdateCheckIn, UpdateCheckOut +from app.api.common import api_get, api_post router = APIRouter(prefix="/api/update", tags=["软件更新"]) -@router.post( - "/check", - tags=["Get"], - summary="检查更新", - response_model=UpdateCheckOut, - status_code=200, -) -async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut: - try: - if_need, latest_version, update_info = await Updater.check_update( - current_version=version.current_version, if_force=version.if_force - ) - except Exception as e: - return UpdateCheckOut( - code=500, - status="error", - message=f"{type(e).__name__}: {str(e)}", - if_need_update=False, - latest_version="", - update_info={}, - ) +QueryUpdateCheckIn = Annotated[UpdateCheckIn, Depends()] + + +def _track_temp_task(task: asyncio.Task[Any]) -> None: + """统一跟踪临时后台任务,避免重复的列表维护代码。""" + + Config.temp_task.append(task) + + def _cleanup(done_task: asyncio.Task[Any]) -> None: + with suppress(ValueError): + Config.temp_task.remove(done_task) + + task.add_done_callback(_cleanup) + + +async def _build_update_check_out(version: UpdateCheckIn) -> UpdateCheckOut: + if_need, latest_version, update_info = await Updater.check_update( + current_version=version.current_version, if_force=version.if_force + ) return UpdateCheckOut( if_need_update=if_need, latest_version=latest_version, update_info=update_info ) -@router.post( +@api_post( + router, + "/check", + model_cls=UpdateCheckOut, + if_need_update=False, + latest_version="", + update_info={}, + route_kwargs={ + "tags": ["Get"], + "summary": "检查更新", + "response_model": UpdateCheckOut, + "status_code": 200, + }, +) +async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut: + return await _build_update_check_out(version) + + +@api_get( + router, + "/check", + model_cls=UpdateCheckOut, + if_need_update=False, + latest_version="", + update_info={}, + route_kwargs={ + "tags": ["Get"], + "summary": "按 REST 风格检查更新", + "response_model": UpdateCheckOut, + "status_code": 200, + }, +) +async def check_update_rest(version: QueryUpdateCheckIn) -> UpdateCheckOut: + return await _build_update_check_out(version) + + +@api_post( + router, "/download", - tags=["Action"], - summary="下载更新", - response_model=OutBase, - status_code=200, + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "下载更新", + "response_model": OutBase, + "status_code": 200, + }, ) async def download_update() -> OutBase: - try: - task = asyncio.create_task(Updater.download_update()) - Config.temp_task.append(task) - task.add_done_callback(lambda t: Config.temp_task.remove(t)) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + task = asyncio.create_task(Updater.download_update()) + _track_temp_task(task) return OutBase() -@router.post( +@api_post( + router, "/install", - tags=["Action"], - summary="安装更新", - response_model=OutBase, - status_code=200, + model_cls=OutBase, + route_kwargs={ + "tags": ["Action"], + "summary": "安装更新", + "response_model": OutBase, + "status_code": 200, + }, ) async def install_update() -> OutBase: - try: - task = asyncio.create_task(Updater.install_update()) - Config.temp_task.append(task) - task.add_done_callback(lambda t: Config.temp_task.remove(t)) - except Exception as e: - return OutBase( - code=500, status="error", message=f"{type(e).__name__}: {str(e)}" - ) + task = asyncio.create_task(Updater.install_update()) + _track_temp_task(task) return OutBase() diff --git a/app/api/ws_command.py b/app/api/ws_command.py index 13d407f4..abb74178 100644 --- a/app/api/ws_command.py +++ b/app/api/ws_command.py @@ -45,6 +45,20 @@ _ws_command_registry: dict[str, RegisteredWsCommand] = {} +def _failed_result(message: str, code: int) -> dict[str, Any]: + return {"success": False, "message": message, "code": code} + + +def _pack_result(data: dict[str, Any]) -> dict[str, Any]: + code = cast(int, data.get("code", 200)) + return { + "success": code == 200, + "data": data, + "code": code, + "message": data.get("message"), + } + + def ws_command( endpoint: str, ) -> Callable[[Callable[P, Any]], Callable[P, Any]]: @@ -111,7 +125,7 @@ async def execute_ws_command( # 检查命令是否存在 if endpoint not in _ws_command_registry: logger.warning(f"未找到命令: {endpoint}") - return {"success": False, "message": f"未找到命令: {endpoint}", "code": 404} + return _failed_result(f"未找到命令: {endpoint}", 404) func = _ws_command_registry[endpoint] @@ -140,11 +154,7 @@ async def execute_ws_command( result = await func(param_instance) except Exception as e: logger.error(f"构建参数模型失败: {type(e).__name__}: {e}") - return { - "success": False, - "message": f"参数错误: {str(e)}", - "code": 400, - } + return _failed_result(f"参数错误: {str(e)}", 400) elif params: # 普通参数,直接传递 result = await func(**params) @@ -155,20 +165,10 @@ async def execute_ws_command( # 处理返回结果 if isinstance(result, BaseModel): result_dict = result.model_dump() - return { - "success": result_dict.get("code", 200) == 200, - "data": result_dict, - "code": result_dict.get("code", 200), - "message": result_dict.get("message"), - } + return _pack_result(result_dict) elif isinstance(result, dict): result_dict = cast(dict[str, Any], result) - return { - "success": result_dict.get("code", 200) == 200, - "data": result_dict, - "code": result_dict.get("code", 200), - "message": result_dict.get("message"), - } + return _pack_result(result_dict) else: return {"success": True, "data": result, "code": 200} @@ -176,11 +176,7 @@ async def execute_ws_command( logger.error( f"执行命令 {endpoint} 失败: {type(e).__name__}: {str(e)}", exc_info=True ) - return { - "success": False, - "message": f"执行失败: {type(e).__name__}: {str(e)}", - "code": 500, - } + return _failed_result(f"执行失败: {type(e).__name__}: {str(e)}", 500) def get_ws_command_registry() -> dict[str, RegisteredWsCommand]: diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index 7d3af4ae..ba479b8d 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -50,6 +50,7 @@ WSClearHistoryIn, WSCommandsOut, ) +from app.api.common import run_api logger = get_logger("WS调试") @@ -75,7 +76,8 @@ async def create_client(request: WSClientCreateIn) -> WSClientCreateOut: - **reconnect_interval**: 重连间隔 - **max_reconnect_attempts**: 最大重连次数 """ - try: + + async def _success() -> WSClientCreateOut: client = await ws_client_manager.create_client( name=request.name, url=request.url, @@ -95,11 +97,13 @@ async def create_client(request: WSClientCreateIn) -> WSClientCreateOut: "is_connected": client.is_connected, }, ) - except Exception as e: - logger.error(f"创建客户端失败: {type(e).__name__}: {e}") - return WSClientCreateOut( - code=500, status="error", message=f"创建客户端失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientCreateOut, + message="创建客户端失败", + on_error=lambda e: logger.error(f"创建客户端失败: {type(e).__name__}: {e}"), + ) @router.post( @@ -116,7 +120,7 @@ async def connect_client(request: WSClientConnectIn) -> WSClientStatusOut: code=404, status="error", message=f"客户端 [{request.name}] 不存在" ) - try: + async def _success() -> WSClientStatusOut: success = await ws_client_manager.connect_client(request.name) client = ws_client_manager.get_client(request.name) @@ -141,11 +145,13 @@ async def connect_client(request: WSClientConnectIn) -> WSClientStatusOut: "is_connected": client.is_connected if client else False, }, ) - except Exception as e: - logger.error(f"连接客户端失败: {type(e).__name__}: {e}") - return WSClientStatusOut( - code=500, status="error", message=f"连接失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientStatusOut, + message="连接失败", + on_error=lambda e: logger.error(f"连接客户端失败: {type(e).__name__}: {e}"), + ) @router.post( @@ -162,7 +168,7 @@ async def disconnect_client(request: WSClientDisconnectIn) -> WSClientStatusOut: code=404, status="error", message=f"客户端 [{request.name}] 不存在" ) - try: + async def _success() -> WSClientStatusOut: await ws_client_manager.disconnect_client(request.name) return WSClientStatusOut( code=200, @@ -170,11 +176,13 @@ async def disconnect_client(request: WSClientDisconnectIn) -> WSClientStatusOut: message=f"客户端 [{request.name}] 已断开", data={"name": request.name, "is_connected": False}, ) - except Exception as e: - logger.error(f"断开客户端失败: {type(e).__name__}: {e}") - return WSClientStatusOut( - code=500, status="error", message=f"断开失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientStatusOut, + message="断开失败", + on_error=lambda e: logger.error(f"断开客户端失败: {type(e).__name__}: {e}"), + ) @router.post( @@ -201,16 +209,18 @@ async def remove_client(request: WSClientRemoveIn) -> WSClientStatusOut: message=f"客户端 [{request.name}] 是系统客户端,不可删除", ) - try: + async def _success() -> WSClientStatusOut: await ws_client_manager.remove_client(request.name) return WSClientStatusOut( code=200, status="success", message=f"客户端 [{request.name}] 已删除" ) - except Exception as e: - logger.error(f"删除客户端失败: {type(e).__name__}: {e}") - return WSClientStatusOut( - code=500, status="error", message=f"删除失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientStatusOut, + message="删除失败", + on_error=lambda e: logger.error(f"删除客户端失败: {type(e).__name__}: {e}"), + ) @router.post( @@ -282,7 +292,7 @@ async def send_message(request: WSClientSendIn) -> WSClientStatusOut: code=400, status="error", message=f"客户端 [{request.name}] 未连接" ) - try: + async def _success() -> WSClientStatusOut: success = await ws_client_manager.send_message(request.name, request.message) if success: return WSClientStatusOut( @@ -293,11 +303,13 @@ async def send_message(request: WSClientSendIn) -> WSClientStatusOut: ) else: return WSClientStatusOut(code=500, status="error", message="消息发送失败") - except Exception as e: - logger.error(f"发送消息失败: {type(e).__name__}: {e}") - return WSClientStatusOut( - code=500, status="error", message=f"发送失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientStatusOut, + message="发送失败", + on_error=lambda e: logger.error(f"发送消息失败: {type(e).__name__}: {e}"), + ) @router.post( @@ -322,7 +334,7 @@ async def send_json_message(request: WSClientSendJsonIn) -> WSClientStatusOut: message = {"id": request.msg_id, "type": request.msg_type, "data": request.data} - try: + async def _success() -> WSClientStatusOut: success = await ws_client_manager.send_message(request.name, message) if success: return WSClientStatusOut( @@ -333,11 +345,13 @@ async def send_json_message(request: WSClientSendJsonIn) -> WSClientStatusOut: ) else: return WSClientStatusOut(code=500, status="error", message="消息发送失败") - except Exception as e: - logger.error(f"发送消息失败: {type(e).__name__}: {e}") - return WSClientStatusOut( - code=500, status="error", message=f"发送失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientStatusOut, + message="发送失败", + on_error=lambda e: logger.error(f"发送消息失败: {type(e).__name__}: {e}"), + ) @router.post( @@ -365,7 +379,7 @@ async def send_auth(request: WSClientAuthIn) -> WSClientStatusOut: code=400, status="error", message=f"客户端 [{request.name}] 未连接" ) - try: + async def _success() -> WSClientStatusOut: success = await ws_client_manager.send_auth( name=request.name, token=request.token, @@ -381,11 +395,13 @@ async def send_auth(request: WSClientAuthIn) -> WSClientStatusOut: return WSClientStatusOut( code=500, status="error", message="认证消息发送失败" ) - except Exception as e: - logger.error(f"发送认证消息失败: {type(e).__name__}: {e}") - return WSClientStatusOut( - code=500, status="error", message=f"发送失败: {str(e)}" - ) + + return await run_api( + _success, + model_cls=WSClientStatusOut, + message="发送失败", + on_error=lambda e: logger.error(f"发送认证消息失败: {type(e).__name__}: {e}"), + ) @router.get( diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index 3c67f836..bca36834 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -9,7 +9,7 @@ from collections.abc import AsyncIterator from typing import Any, ClassVar, TypeVar, cast -from pydantic import BaseModel, ConfigDict, PrivateAttr +from pydantic import AliasChoices, AliasPath, BaseModel, ConfigDict, PrivateAttr from .base import ( MultipleConfig, @@ -51,6 +51,74 @@ def _normalize_mapping(value: Any) -> dict[str, Any]: return {str(key): item for key, item in mapping.items()} +def _resolve_alias_paths(validation_alias: Any) -> list[tuple[str, ...]]: + if validation_alias is None: + return [] + + if isinstance(validation_alias, str): + return [(validation_alias,)] + + if isinstance(validation_alias, AliasPath): + path = tuple( + item + for item in validation_alias.path + if isinstance(item, str) and item != "" + ) + return [path] if path else [] + + if isinstance(validation_alias, AliasChoices): + alias_paths: list[tuple[str, ...]] = [] + for choice in validation_alias.choices: + alias_paths.extend(_resolve_alias_paths(choice)) + return alias_paths + + return [] + + +def _try_resolve_alias_value( + raw: dict[str, Any], + group_name: str, + group_data: dict[str, Any], + validation_alias: Any, +) -> tuple[Any, bool]: + for alias_path in _resolve_alias_paths(validation_alias): + if not alias_path: + continue + + if len(alias_path) == 1: + alias_key = alias_path[0] + if alias_key in group_data: + return group_data[alias_key], True + continue + + current: Any + root_key = alias_path[0] + if root_key == group_name: + current = group_data + walk_keys = alias_path[1:] + else: + current = _normalize_mapping(raw.get(root_key, {})) + walk_keys = alias_path[1:] + + matched = True + for key in walk_keys: + if not isinstance(current, dict): + matched = False + break + + current_dict = cast(dict[str, Any], current) + if key not in current_dict: + matched = False + break + + current = current_dict[key] + + if matched: + return cast(Any, current), True + + return None, False + + FieldMarkerT = TypeVar("FieldMarkerT") @@ -459,6 +527,16 @@ async def load(self, data: dict[str, Any]) -> None: candidate = group_data[field_name] has_value = True else: + field_info = type(group_model).model_fields.get(field_name) + if field_info is not None: + candidate, has_value = _try_resolve_alias_value( + raw, + group_name, + group_data, + field_info.validation_alias, + ) + + if not has_value: legacy = self.LEGACY_FIELD_MAP.get((group_name, field_name)) if legacy is not None: legacy_group, legacy_name = legacy @@ -549,7 +627,10 @@ async def set(self, group: str, name: str, value: Any) -> None: if old_value != new_value: await self._queue_binding(group, name, new_value) - for (virtual_group, virtual_name), old_virtual_value in virtual_old_values.items(): + for ( + virtual_group, + virtual_name, + ), old_virtual_value in virtual_old_values.items(): new_virtual_value = self.get(virtual_group, virtual_name) if old_virtual_value != new_virtual_value: await self._queue_binding( diff --git a/app/models/common.py b/app/models/common.py index 2ae64dcc..c93cdf44 100644 --- a/app/models/common.py +++ b/app/models/common.py @@ -3,7 +3,7 @@ import calendar from typing import Annotated, Any, ClassVar, Literal, cast -from pydantic import BaseModel, Field, field_validator +from pydantic import AliasChoices, AliasPath, BaseModel, Field, field_validator from app.core.config.base import MultipleConfig from app.core.config.fields import RefField @@ -36,19 +36,26 @@ class EmulatorConfig(PydanticConfigBase): class InfoModel(BaseModel): Name: str = "新模拟器" - Type: EMULATOR_TYPES = "general" + Type: EMULATOR_TYPES = Field( + default="general", + validation_alias=AliasChoices("Type", AliasPath("Data", "Type")), + ) Path: str = "" - BossKey: JsonListString = "[ ]" - MaxWaitTime: int = Field(default=60, ge=1, le=9999) + BossKey: JsonListString = Field( + default="[ ]", + validation_alias=AliasChoices("BossKey", AliasPath("Data", "BossKey")), + ) + MaxWaitTime: int = Field( + default=60, + ge=1, + le=9999, + validation_alias=AliasChoices( + "MaxWaitTime", AliasPath("Data", "MaxWaitTime") + ), + ) Info: InfoModel = Field(default_factory=InfoModel) - LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = { - ("Info", "Type"): ("Data", "Type"), - ("Info", "BossKey"): ("Data", "BossKey"), - ("Info", "MaxWaitTime"): ("Data", "MaxWaitTime"), - } - class Webhook(PydanticConfigBase): """Webhook 配置""" diff --git a/app/models/common_contract.py b/app/models/common_contract.py index 85a8ccd8..63d40cf2 100644 --- a/app/models/common_contract.py +++ b/app/models/common_contract.py @@ -1,14 +1,20 @@ from __future__ import annotations from collections.abc import Iterable, Mapping +from functools import lru_cache from types import UnionType -from typing import Any, TypeAlias, TypeVar, Union, cast, get_args, get_origin +from typing import Annotated, Any, Generic, TypeAlias, TypeVar, Union, cast, get_args, get_origin -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, create_model + +from app.core.config.fields import VirtualField +from app.core.config.pydantic import PydanticConfigBase ModelT = TypeVar("ModelT", bound=BaseModel) MapKeyT = TypeVar("MapKeyT") +IndexT = TypeVar("IndexT") +DataT = TypeVar("DataT") RawModelSource: TypeAlias = Mapping[str, Any] | BaseModel @@ -37,6 +43,231 @@ class ComboBoxOut(OutBase): data: list[ComboBoxItem] = Field(..., description="下拉框选项") +class ResourceCollectionOut(OutBase, Generic[IndexT, DataT]): + index: list[IndexT] = Field(..., description="资源索引列表") + data: dict[str, DataT] = Field(..., description="资源数据字典") + + +class ResourceItemOut(OutBase, Generic[DataT]): + data: DataT = Field(..., description="资源数据") + + +class ResourceCreateOut(ResourceItemOut[DataT], Generic[DataT]): + id: str = Field(..., description="新创建资源的唯一 ID") + + +class IndexOrderPatch(ApiModel): + indexList: list[str] = Field(..., description="按新顺序排列的资源 ID 列表") + + +def _annotate(annotation: Any, metadata: tuple[object, ...]) -> Any: + if not metadata: + return annotation + return cast(Any, Annotated)[(annotation, *metadata)] + + +def _make_optional(annotation: Any) -> Any: + origin = get_origin(annotation) + if origin in (Union, UnionType) and type(None) in get_args(annotation): + return annotation + return annotation | None + + +def _clone_field_annotation( + field_info: Any, + annotation: Any, + *, + keep_virtual: bool, +) -> Any: + metadata = tuple( + item + for item in field_info.metadata + if keep_virtual or not isinstance(item, VirtualField) + ) + field_kwargs: dict[str, Any] = {} + if field_info.description is not None: + field_kwargs["description"] = field_info.description + if field_info.validation_alias is not None: + field_kwargs["validation_alias"] = field_info.validation_alias + if field_info.serialization_alias is not None: + field_kwargs["serialization_alias"] = field_info.serialization_alias + if field_info.alias is not None: + field_kwargs["alias"] = field_info.alias + if field_info.json_schema_extra is not None: + field_kwargs["json_schema_extra"] = field_info.json_schema_extra + if field_info.discriminator is not None: + field_kwargs["discriminator"] = field_info.discriminator + if field_kwargs: + metadata = (*metadata, Field(**field_kwargs)) + return _annotate(annotation, metadata) + + +def _model_field_definitions( + model_cls: type[BaseModel], + *, + optional_fields: bool, + keep_virtual: bool, +) -> dict[str, tuple[Any, Any]]: + field_definitions: dict[str, tuple[Any, Any]] = {} + + for field_name, field_info in model_cls.model_fields.items(): + if not keep_virtual and any( + isinstance(item, VirtualField) for item in field_info.metadata + ): + continue + + annotation = field_info.annotation + if optional_fields: + annotation = _make_optional(annotation) + + annotated = _clone_field_annotation( + field_info, + annotation, + keep_virtual=keep_virtual, + ) + + if optional_fields: + default = Field(default=None, description=field_info.description) + elif field_info.default_factory is not None: + default = Field( + default_factory=field_info.default_factory, + description=field_info.description, + ) + elif field_info.is_required(): + default = ... + else: + default = Field(default=field_info.default, description=field_info.description) + + field_definitions[field_name] = (annotated, default) + + return field_definitions + + +@lru_cache(maxsize=None) +def derive_group_read_model( + group_cls: type[BaseModel], + *, + model_name: str, +) -> type[ApiModel]: + return create_model( + model_name, + __base__=ApiModel, + **cast( + dict[str, Any], + _model_field_definitions( + group_cls, + optional_fields=False, + keep_virtual=True, + ), + ), + ) + + +@lru_cache(maxsize=None) +def derive_group_patch_model( + group_cls: type[BaseModel], + *, + model_name: str, +) -> type[ApiModel]: + return create_model( + model_name, + __base__=ApiModel, + **cast( + dict[str, Any], + _model_field_definitions( + group_cls, + optional_fields=True, + keep_virtual=False, + ), + ), + ) + + +@lru_cache(maxsize=None) +def derive_config_read_model( + config_cls: type[PydanticConfigBase], + *, + model_name: str, + include_groups: tuple[str, ...] | None = None, +) -> type[ApiModel]: + field_definitions: dict[str, tuple[Any, Any]] = {} + + allowed_groups = set(include_groups) if include_groups is not None else None + for group_name, field_info in config_cls.model_fields.items(): + if allowed_groups is not None and group_name not in allowed_groups: + continue + group_cls = field_info.annotation + if not isinstance(group_cls, type) or not issubclass(group_cls, BaseModel): + continue + group_model = derive_group_read_model( + group_cls, + model_name=f"{model_name}{group_name}", + ) + field_definitions[group_name] = ( + group_model, + Field(default_factory=group_model, description=field_info.description), + ) + + return create_model( + model_name, + __base__=ApiModel, + **cast(dict[str, Any], field_definitions), + ) + + +@lru_cache(maxsize=None) +def derive_config_patch_model( + config_cls: type[PydanticConfigBase], + *, + model_name: str, + include_groups: tuple[str, ...] | None = None, +) -> type[ApiModel]: + field_definitions: dict[str, tuple[Any, Any]] = {} + + allowed_groups = set(include_groups) if include_groups is not None else None + for group_name, field_info in config_cls.model_fields.items(): + if allowed_groups is not None and group_name not in allowed_groups: + continue + group_cls = field_info.annotation + if not isinstance(group_cls, type) or not issubclass(group_cls, BaseModel): + continue + group_model = derive_group_patch_model( + group_cls, + model_name=f"{model_name}{group_name}", + ) + field_definitions[group_name] = ( + group_model | None, + Field(default=None, description=field_info.description), + ) + + return create_model( + model_name, + __base__=ApiModel, + **cast(dict[str, Any], field_definitions), + ) + + +def derive_config_contracts( + config_cls: type[PydanticConfigBase], + *, + read_name: str, + patch_name: str, + include_groups: tuple[str, ...] | None = None, +) -> tuple[type[ApiModel], type[ApiModel]]: + return ( + derive_config_read_model( + config_cls, + model_name=read_name, + include_groups=include_groups, + ), + derive_config_patch_model( + config_cls, + model_name=patch_name, + include_groups=include_groups, + ), + ) + + def _normalize_source(raw: RawModelSource | None) -> dict[str, Any]: if raw is None: return {} @@ -178,6 +409,15 @@ def project_model_map( "InfoOut", "ComboBoxItem", "ComboBoxOut", + "ResourceCollectionOut", + "ResourceItemOut", + "ResourceCreateOut", + "IndexOrderPatch", + "derive_group_read_model", + "derive_group_patch_model", + "derive_config_read_model", + "derive_config_patch_model", + "derive_config_contracts", "project_model", "project_model_list", "project_model_map", diff --git a/app/models/emulator_contract.py b/app/models/emulator_contract.py index 556f6697..ba157024 100644 --- a/app/models/emulator_contract.py +++ b/app/models/emulator_contract.py @@ -4,87 +4,49 @@ from pydantic import Field -from .common_contract import ApiModel, OutBase +from .common import EmulatorConfig +from .common_contract import ( + ApiModel, + ResourceCollectionOut, + ResourceCreateOut, + ResourceItemOut, + derive_config_contracts, +) from .shared import DeviceInfo -class EmulatorConfigIndexItem(ApiModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["EmulatorConfig"] = Field(..., description="配置类型") - - -class EmulatorInfoRead(ApiModel): - Name: str = Field(default="新模拟器", description="模拟器名称") - Type: Literal["general", "mumu", "ldplayer"] = Field( - default="general", description="模拟器类型" - ) - Path: str = Field(default="", description="模拟器路径") - BossKey: str = Field(default="[ ]", description="老板键快捷键配置") - MaxWaitTime: int = Field(default=60, description="最大等待时间(秒)") - - -class EmulatorInfoPatch(ApiModel): - Name: str | None = Field(default=None, description="模拟器名称") - Type: Literal["general", "mumu", "ldplayer"] | None = Field( - default=None, description="模拟器类型" - ) - Path: str | None = Field(default=None, description="模拟器路径") - BossKey: str | None = Field(default=None, description="老板键快捷键配置") - MaxWaitTime: int | None = Field(default=None, description="最大等待时间(秒)") - - -class EmulatorRead(ApiModel): - Info: EmulatorInfoRead = Field( - default_factory=EmulatorInfoRead, description="模拟器基础信息" - ) - +_EmulatorReadBase, _EmulatorPatchBase = derive_config_contracts( + EmulatorConfig, + read_name="EmulatorRead", + patch_name="EmulatorPatch", + include_groups=("Info",), +) -class EmulatorPatch(ApiModel): - Info: EmulatorInfoPatch | None = Field(default=None, description="模拟器基础信息") +class EmulatorRead(_EmulatorReadBase): + pass -class EmulatorGetIn(ApiModel): - emulatorId: str | None = Field( - default=None, description="模拟器ID, 未携带时表示获取所有模拟器数据" - ) +class EmulatorPatch(_EmulatorPatchBase): + pass -class EmulatorGetOut(OutBase): - index: list[EmulatorConfigIndexItem] = Field(..., description="模拟器索引列表") - data: dict[str, EmulatorRead] = Field( - ..., description="模拟器数据字典, key来自于index列表的uid" - ) - -class EmulatorCreateOut(OutBase): - emulatorId: str = Field(..., description="新创建的模拟器 ID") - data: EmulatorRead = Field(..., description="模拟器配置数据") - - -class EmulatorUpdateIn(ApiModel): - emulatorId: str = Field(..., description="模拟器 ID") - data: EmulatorPatch = Field(..., description="模拟器更新数据") - - -class EmulatorDeleteIn(ApiModel): - emulatorId: str = Field(..., description="模拟器 ID") +class EmulatorConfigIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["EmulatorConfig"] = Field(..., description="配置类型") -class EmulatorReorderIn(ApiModel): - indexList: list[str] = Field(..., description="模拟器 ID列表, 按新顺序排列") +EmulatorGetOut = ResourceCollectionOut[EmulatorConfigIndexItem, EmulatorRead] +EmulatorDetailOut = ResourceItemOut[EmulatorRead] +EmulatorCreateOut = ResourceCreateOut[EmulatorRead] -class EmulatorOperateIn(ApiModel): - emulatorId: str = Field(..., description="模拟器 ID") - operate: Literal["open", "close", "show"] = Field(..., description="操作类型") +class EmulatorActionBody(ApiModel): index: str = Field(..., description="模拟器索引") -class EmulatorStatusOut(OutBase): - data: dict[str, dict[str, DeviceInfo]] = Field( - ..., - description="模拟器状态信息, 外层key为模拟器ID, 内层key为设备索引, value为设备信息", - ) +EmulatorStatusOut = ResourceItemOut[dict[str, dict[str, DeviceInfo]]] +EmulatorDeviceStatusOut = ResourceItemOut[dict[str, DeviceInfo]] class EmulatorSearchResult(ApiModel): @@ -93,24 +55,20 @@ class EmulatorSearchResult(ApiModel): name: str = Field(..., description="模拟器名称") -class EmulatorSearchOut(OutBase): - emulators: list[EmulatorSearchResult] = Field(..., description="搜索到的模拟器列表") +class EmulatorSearchOut(ResourceItemOut[list[EmulatorSearchResult]]): + data: list[EmulatorSearchResult] = Field(..., description="搜索到的模拟器列表") __all__ = [ - "EmulatorConfigIndexItem", - "EmulatorInfoRead", - "EmulatorInfoPatch", "EmulatorRead", "EmulatorPatch", - "EmulatorGetIn", + "EmulatorConfigIndexItem", "EmulatorGetOut", + "EmulatorDetailOut", "EmulatorCreateOut", - "EmulatorUpdateIn", - "EmulatorDeleteIn", - "EmulatorReorderIn", - "EmulatorOperateIn", + "EmulatorActionBody", "EmulatorStatusOut", + "EmulatorDeviceStatusOut", "EmulatorSearchResult", "EmulatorSearchOut", ] diff --git a/app/models/general_contract.py b/app/models/general_contract.py index f0e81a2a..5cb0f602 100644 --- a/app/models/general_contract.py +++ b/app/models/general_contract.py @@ -4,128 +4,50 @@ from pydantic import Field -from .common_contract import ApiModel +from .common_contract import derive_config_contracts +from .general import GeneralConfig as RuntimeGeneralConfig +from .general import GeneralUserConfig as RuntimeGeneralUserConfig -class GeneralUserConfigNotify(ApiModel): - Enabled: bool | None = Field(default=None, description="是否启用通知") - IfSendStatistic: bool | None = Field( - default=None, description="是否发送统计信息" - ) - IfSendMail: bool | None = Field(default=None, description="是否发送邮件通知") - ToAddress: str | None = Field(default=None, description="邮件接收地址") - IfServerChan: bool | None = Field( - default=None, description="是否使用Server酱推送" - ) - ServerChanKey: str | None = Field(default=None, description="ServerChanKey") +_GeneralConfigReadBase, _GeneralConfigPatchBase = derive_config_contracts( + RuntimeGeneralConfig, + read_name="GeneralConfig", + patch_name="GeneralConfigPatch", +) +_GeneralUserConfigReadBase, _GeneralUserConfigPatchBase = derive_config_contracts( + RuntimeGeneralUserConfig, + read_name="GeneralUserConfig", + patch_name="GeneralUserConfigPatch", +) -class GeneralUserConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="用户名") - Status: bool | None = Field(default=None, description="用户状态") - RemainedDay: int | None = Field(default=None, description="剩余天数") - IfScriptBeforeTask: bool | None = Field( - default=None, description="是否在任务前执行脚本" - ) - ScriptBeforeTask: str | None = Field(default=None, description="任务前脚本路径") - IfScriptAfterTask: bool | None = Field( - default=None, description="是否在任务后执行脚本" - ) - ScriptAfterTask: str | None = Field(default=None, description="任务后脚本路径") - Notes: str | None = Field(default=None, description="备注") - Tag: str | None = Field( - default=None, description="用户标签列表(JSON字符串,TagItem的dict列表)" +class GeneralConfig(_GeneralConfigReadBase): + type: Literal["GeneralConfig"] = Field( + default="GeneralConfig", description="配置类型" ) -class GeneralUserConfigData(ApiModel): - LastProxyDate: str | None = Field(default=None, description="上次代理日期") - ProxyTimes: int | None = Field(default=None, description="代理次数") +class GeneralConfigPatch(_GeneralConfigPatchBase): + type: Literal["GeneralConfig"] | None = Field( + default=None, description="配置类型" + ) -class GeneralUserConfig(ApiModel): +class GeneralUserConfig(_GeneralUserConfigReadBase): type: Literal["GeneralUserConfig"] = Field( default="GeneralUserConfig", description="配置类型" ) - Info: GeneralUserConfigInfo | None = Field(default=None, description="用户信息") - Data: GeneralUserConfigData | None = Field(default=None, description="用户数据") - Notify: GeneralUserConfigNotify | None = Field( - default=None, description="单独通知" - ) -class GeneralConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="脚本名称") - RootPath: str | None = Field(default=None, description="脚本根目录") - - -class GeneralConfigScript(ApiModel): - ScriptPath: str | None = Field(default=None, description="脚本可执行文件路径") - Arguments: str | None = Field(default=None, description="脚本启动附加命令参数") - IfTrackProcess: bool | None = Field( - default=None, description="是否追踪脚本子进程" - ) - TrackProcessName: str | None = Field(default=None, description="追踪进程名称") - TrackProcessExe: str | None = Field(default=None, description="追踪进程文件路径") - TrackProcessCmdline: str | None = Field( - default=None, description="追踪进程启动命令行参数" - ) - ConfigPath: str | None = Field(default=None, description="配置文件路径") - ConfigPathMode: Literal["File", "Folder"] | None = Field( - default=None, description="配置文件类型: 单个文件, 文件夹" - ) - UpdateConfigMode: Literal["Never", "Success", "Failure", "Always"] | None = Field( - default=None, - description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时", +class GeneralUserConfigPatch(_GeneralUserConfigPatchBase): + type: Literal["GeneralUserConfig"] | None = Field( + default=None, description="配置类型" ) - LogPath: str | None = Field(default=None, description="日志文件路径") - LogPathFormat: str | None = Field(default=None, description="日志文件名格式") - LogTimeStart: int | None = Field(default=None, description="日志时间戳开始位置") - LogTimeEnd: int | None = Field(default=None, description="日志时间戳结束位置") - LogTimeFormat: str | None = Field(default=None, description="日志时间戳格式") - SuccessLog: str | None = Field(default=None, description="成功时日志") - ErrorLog: str | None = Field(default=None, description="错误时日志") - - -class GeneralConfigGame(ApiModel): - Enabled: bool | None = Field(default=None, description="游戏/模拟器相关功能是否启用") - Type: Literal["Emulator", "Client", "URL"] | None = Field( - default=None, description="类型: 模拟器, PC端, URL协议" - ) - Path: str | None = Field(default=None, description="游戏/模拟器程序路径") - URL: str | None = Field(default=None, description="自定义协议URL") - ProcessName: str | None = Field(default=None, description="游戏进程名称") - Arguments: str | None = Field(default=None, description="游戏/模拟器启动参数") - WaitTime: int | None = Field(default=None, description="游戏/模拟器等待启动时间") - IfForceClose: bool | None = Field( - default=None, description="是否强制关闭游戏/模拟器进程" - ) - EmulatorId: str | None = Field(default=None, description="模拟器ID") - EmulatorIndex: str | None = Field(default=None, description="模拟器多开实例索引") - - -class GeneralConfigRun(ApiModel): - ProxyTimesLimit: int | None = Field(default=None, description="每日代理次数限制") - RunTimesLimit: int | None = Field(default=None, description="重试次数限制") - RunTimeLimit: int | None = Field(default=None, description="日志超时限制") - - -class GeneralConfig(ApiModel): - type: Literal["GeneralConfig"] = Field(default="GeneralConfig", description="配置类型") - Info: GeneralConfigInfo | None = Field(default=None, description="脚本基础信息") - Script: GeneralConfigScript | None = Field(default=None, description="脚本配置") - Game: GeneralConfigGame | None = Field(default=None, description="游戏配置") - Run: GeneralConfigRun | None = Field(default=None, description="运行配置") __all__ = [ - "GeneralUserConfigNotify", - "GeneralUserConfigInfo", - "GeneralUserConfigData", - "GeneralUserConfig", - "GeneralConfigInfo", - "GeneralConfigScript", - "GeneralConfigGame", - "GeneralConfigRun", "GeneralConfig", + "GeneralConfigPatch", + "GeneralUserConfig", + "GeneralUserConfigPatch", ] diff --git a/app/models/global_config.py b/app/models/global_config.py index 006286d0..4fba3bbc 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import Annotated, Any, Callable, Literal -from pydantic import BaseModel, Field, field_validator +from pydantic import AliasChoices, AliasPath, BaseModel, Field, field_validator from app.core.config.base import MultipleConfig from app.core.config.fields import VirtualField @@ -135,7 +135,10 @@ class DataModel(BaseModel): LastStatisticsUpload: YmdHmsString = "2000-01-01 00:00:00" LastStageUpdated: YmdHmsString = "2000-01-01 00:00:00" StageETag: str = "" - StageData: JsonDictString = "{ }" + StageData: JsonDictString = Field( + default="{ }", + validation_alias=AliasChoices("StageData", AliasPath("Data", "Stage")), + ) LastNoticeUpdated: YmdHmsString = "2000-01-01 00:00:00" NoticeETag: str = "" IfShowNotice: bool = True @@ -164,8 +167,6 @@ def _normalize_uid(cls, value: Any) -> str: Update: UpdateModel = Field(default_factory=UpdateModel) Data: DataModel = Field(default_factory=DataModel) - LEGACY_FIELD_MAP = {("Data", "StageData"): ("Data", "Stage")} - def __init__(self, **data: Any): super().__init__(**data) @@ -176,9 +177,7 @@ def __init__(self, **data: Any): self.PlanConfig: MultipleConfig[MaaPlanConfig] = MultipleConfig([MaaPlanConfig]) self.ScriptConfig: MultipleConfig[ MaaConfig | MaaEndConfig | SrcConfig | GeneralConfig - ] = MultipleConfig( - [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] - ) + ] = MultipleConfig([MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig]) self.QueueConfig: MultipleConfig[QueueConfig] = MultipleConfig([QueueConfig]) self.ToolsConfig = ToolsConfig() diff --git a/app/models/history_contract.py b/app/models/history_contract.py index 3f375666..fd3339f6 100644 --- a/app/models/history_contract.py +++ b/app/models/history_contract.py @@ -27,6 +27,10 @@ class HistoryData(ApiModel): error_info: dict[str, str] | None = Field( default=None, description="报错信息, key为时间戳, value为错误描述" ) + sanity: int | None = Field(default=None, description="当前理智值") + sanity_full_at: str | None = Field( + default=None, description="理智回满时间, 格式通常为 YYYY-MM-DD HH:MM:SS" + ) log_content: str | None = Field( default=None, description="日志内容, 仅在提取单条历史记录数据时返回" ) diff --git a/app/models/maa.py b/app/models/maa.py index 509d930e..e5f57d65 100644 --- a/app/models/maa.py +++ b/app/models/maa.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Annotated, Any, ClassVar, Callable, Literal -from pydantic import BaseModel, Field, field_validator +from pydantic import AliasChoices, AliasPath, BaseModel, Field, field_validator from app.core.config.base import MultipleConfig from app.core.config.fields import RefField, VirtualField @@ -116,7 +116,12 @@ class DataModel(BaseModel): ProxyTimes: int = Field(default=0, ge=0, le=9999) IfPassCheck: bool = True CustomInfrast: JsonDictString = "{ }" - InfrastIndex: str = "0" + InfrastIndex: str = Field( + default="0", + validation_alias=AliasChoices( + "InfrastIndex", AliasPath("Info", "InfrastIndex") + ), + ) @field_validator("LastProxyDate", "LastSklandDate", mode="before") @classmethod @@ -152,10 +157,6 @@ class NotifyModel(BaseModel): Task: TaskModel = Field(default_factory=TaskModel) Notify: NotifyModel = Field(default_factory=NotifyModel) - LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = { - ("Data", "InfrastIndex"): ("Info", "InfrastIndex") - } - def __init__(self, **data: Any): super().__init__(**data) self.Notify_CustomWebhooks = MultipleConfig([Webhook]) diff --git a/app/models/maa_contract.py b/app/models/maa_contract.py index 6556c24b..27227ec8 100644 --- a/app/models/maa_contract.py +++ b/app/models/maa_contract.py @@ -4,124 +4,66 @@ from pydantic import Field -from .common_contract import ApiModel - - -class MaaUserConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="用户名") - Id: str | None = Field(default=None, description="用户ID") - Mode: Literal["简洁", "详细"] | None = Field( - default=None, description="用户配置模式" - ) - StageMode: str | None = Field(default=None, description="关卡配置模式") - Server: Literal[ - "Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy" - ] | None = Field(default=None, description="服务器") - Status: bool | None = Field(default=None, description="用户状态") - RemainedDay: int | None = Field(default=None, description="剩余天数") - Annihilation: Literal[ - "Close", - "Annihilation", - "Chernobog@Annihilation", - "LungmenOutskirts@Annihilation", - "LungmenDowntown@Annihilation", - ] | None = Field(default=None, description="剿灭模式") - InfrastMode: Literal["Normal", "Rotation", "Custom"] | None = Field( - default=None, description="基建模式" - ) - InfrastName: str | None = Field(default=None, description="基建方案名称") - InfrastIndex: str | None = Field(default=None, description="基建方案索引") - Password: str | None = Field(default=None, description="密码") - Notes: str | None = Field(default=None, description="备注") - MedicineNumb: int | None = Field(default=None, description="吃理智药数量") - SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] | None = Field( - default=None, description="连战次数" - ) - Stage: str | None = Field(default=None, description="关卡选择") - Stage_1: str | None = Field(default=None, description="备选关卡 - 1") - Stage_2: str | None = Field(default=None, description="备选关卡 - 2") - Stage_3: str | None = Field(default=None, description="备选关卡 - 3") - Stage_Remain: str | None = Field(default=None, description="剩余理智关卡") - IfSkland: bool | None = Field(default=None, description="是否启用森空岛签到") - SklandToken: str | None = Field(default=None, description="SklandToken") - Tag: str | None = Field(default=None, description="状态标签列表") - - -class MaaUserConfigData(ApiModel): - IfPassCheck: bool | None = Field(default=None, description="是否通过人工排查") - - -class MaaUserConfigTask(ApiModel): - IfStartUp: bool | None = Field(default=None, description="开始唤醒") - IfRecruit: bool | None = Field(default=None, description="自动公招") - IfInfrast: bool | None = Field(default=None, description="基建换班") - IfFight: bool | None = Field(default=None, description="理智作战") - IfMall: bool | None = Field(default=None, description="信用收支") - IfAward: bool | None = Field(default=None, description="领取奖励") - IfRoguelike: bool | None = Field(default=None, description="自动肉鸽") - IfReclamation: bool | None = Field(default=None, description="生息演算") - - -class MaaUserConfigNotify(ApiModel): - Enabled: bool | None = Field(default=None, description="是否启用通知") - IfSendStatistic: bool | None = Field( - default=None, description="是否发送统计信息" - ) - IfSendSixStar: bool | None = Field(default=None, description="是否发送高资喜报") - IfSendMail: bool | None = Field(default=None, description="是否发送邮件通知") - ToAddress: str | None = Field(default=None, description="邮件接收地址") - IfServerChan: bool | None = Field( - default=None, description="是否使用Server酱推送" - ) - ServerChanKey: str | None = Field(default=None, description="ServerChanKey") +from .common_contract import derive_config_contracts +from .maa import MaaConfig as RuntimeMaaConfig +from .maa import MaaPlanConfig as RuntimeMaaPlanConfig +from .maa import MaaUserConfig as RuntimeMaaUserConfig + + +_MaaConfigReadBase, _MaaConfigPatchBase = derive_config_contracts( + RuntimeMaaConfig, + read_name="MaaConfig", + patch_name="MaaConfigPatch", +) +_MaaUserConfigReadBase, _MaaUserConfigPatchBase = derive_config_contracts( + RuntimeMaaUserConfig, + read_name="MaaUserConfig", + patch_name="MaaUserConfigPatch", +) +_MaaPlanConfigReadBase, _MaaPlanPatchBase = derive_config_contracts( + RuntimeMaaPlanConfig, + read_name="MaaPlanConfig", + patch_name="MaaPlanPatch", +) + + +class MaaConfig(_MaaConfigReadBase): + type: Literal["MaaConfig"] = Field(default="MaaConfig", description="配置类型") -class MaaUserConfig(ApiModel): - type: Literal["MaaUserConfig"] = Field(default="MaaUserConfig", description="配置类型") - Info: MaaUserConfigInfo | None = Field(default=None, description="基础信息") - Data: MaaUserConfigData | None = Field(default=None, description="用户数据") - Task: MaaUserConfigTask | None = Field(default=None, description="任务列表") - Notify: MaaUserConfigNotify | None = Field(default=None, description="单独通知") +class MaaConfigPatch(_MaaConfigPatchBase): + type: Literal["MaaConfig"] | None = Field(default=None, description="配置类型") -class MaaConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="脚本名称") - Path: str | None = Field(default=None, description="脚本路径") +class MaaUserConfig(_MaaUserConfigReadBase): + type: Literal["MaaUserConfig"] = Field( + default="MaaUserConfig", description="配置类型" + ) -class MaaConfigEmulator(ApiModel): - Id: str | None = Field(default=None, description="模拟器ID") - Index: str | None = Field(default=None, description="模拟器多开实例索引") +class MaaUserConfigPatch(_MaaUserConfigPatchBase): + type: Literal["MaaUserConfig"] | None = Field( + default=None, description="配置类型" + ) -class MaaConfigRun(ApiModel): - TaskTransitionMethod: Literal["NoAction", "ExitGame", "ExitEmulator"] | None = ( - Field(default=None, description="简洁任务间切换方式") - ) - ProxyTimesLimit: int | None = Field(default=None, description="每日代理次数限制") - RunTimesLimit: int | None = Field(default=None, description="重试次数限制") - AnnihilationTimeLimit: int | None = Field(default=None, description="剿灭超时限制") - RoutineTimeLimit: int | None = Field(default=None, description="日常超时限制") - AnnihilationAvoidWaste: bool | None = Field( - default=None, description="剿灭避免无代理卡浪费理智" +class MaaPlanConfig(_MaaPlanConfigReadBase): + type: Literal["MaaPlanConfig"] = Field( + default="MaaPlanConfig", description="配置类型" ) -class MaaConfig(ApiModel): - type: Literal["MaaConfig"] = Field(default="MaaConfig", description="配置类型") - Info: MaaConfigInfo | None = Field(default=None, description="脚本基础信息") - Emulator: MaaConfigEmulator | None = Field(default=None, description="模拟器配置") - Run: MaaConfigRun | None = Field(default=None, description="脚本运行配置") +class MaaPlanPatch(_MaaPlanPatchBase): + type: Literal["MaaPlanConfig"] | None = Field( + default=None, description="配置类型" + ) __all__ = [ - "MaaUserConfigInfo", - "MaaUserConfigData", - "MaaUserConfigTask", - "MaaUserConfigNotify", - "MaaUserConfig", - "MaaConfigInfo", - "MaaConfigEmulator", - "MaaConfigRun", "MaaConfig", + "MaaConfigPatch", + "MaaUserConfig", + "MaaUserConfigPatch", + "MaaPlanConfig", + "MaaPlanPatch", ] diff --git a/app/models/maaend_contract.py b/app/models/maaend_contract.py index d2a1f2f7..b4de92ce 100644 --- a/app/models/maaend_contract.py +++ b/app/models/maaend_contract.py @@ -4,106 +4,50 @@ from pydantic import Field -from .common_contract import ApiModel - - -class MaaEndUserConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="用户名") - Status: bool | None = Field(default=None, description="用户状态") - Id: str | None = Field(default=None, description="用户ID") - Password: str | None = Field(default=None, description="密码") - Mode: Literal["简洁", "详细"] | None = Field( - default=None, description="配置模式" - ) - Resource: Literal["官服"] | None = Field(default=None, description="资源名称") - RemainedDay: int | None = Field(default=None, description="剩余天数") - Notes: str | None = Field(default=None, description="备注") - IfSkland: bool | None = Field(default=None, description="是否启用森空岛签到") - SklandToken: str | None = Field(default=None, description="SklandToken") - Tag: str | None = Field(default=None, description="用户标签信息") - - -class MaaEndUserConfigTask(ApiModel): - ProtocolSpaceTab: Literal[ - "OperatorProgression", "WeaponProgression", "CrisisDrills" - ] | None = Field(default=None, description="协议空间选项卡") - OperatorProgression: Literal[ - "OperatorEXP", "Promotions", "T-Creds", "SkillUp" - ] | None = Field(default=None, description="干员养成任务") - WeaponProgression: Literal["WeaponEXP", "WeaponTune"] | None = Field( - default=None, description="武器养成任务" - ) - CrisisDrills: Literal[ - "AdvancedProgression1", - "AdvancedProgression2", - "AdvancedProgression3", - "AdvancedProgression4", - "AdvancedProgression5", - ] | None = Field(default=None, description="危境预演任务") - RewardsSetOption: Literal["RewardsSetA", "RewardsSetB"] | None = Field( - default=None, description="奖励套组选项" +from .common_contract import derive_config_contracts +from .maaend import MaaEndConfig as RuntimeMaaEndConfig +from .maaend import MaaEndUserConfig as RuntimeMaaEndUserConfig + + +_MaaEndConfigReadBase, _MaaEndConfigPatchBase = derive_config_contracts( + RuntimeMaaEndConfig, + read_name="MaaEndConfig", + patch_name="MaaEndConfigPatch", +) +_MaaEndUserConfigReadBase, _MaaEndUserConfigPatchBase = derive_config_contracts( + RuntimeMaaEndUserConfig, + read_name="MaaEndUserConfig", + patch_name="MaaEndUserConfigPatch", +) + + +class MaaEndConfig(_MaaEndConfigReadBase): + type: Literal["MaaEndConfig"] = Field( + default="MaaEndConfig", description="配置类型" ) -class MaaEndUserConfigNotify(ApiModel): - Enabled: bool | None = Field(default=None, description="是否启用通知") - IfSendStatistic: bool | None = Field( - default=None, description="是否发送统计信息" +class MaaEndConfigPatch(_MaaEndConfigPatchBase): + type: Literal["MaaEndConfig"] | None = Field( + default=None, description="配置类型" ) - IfSendMail: bool | None = Field(default=None, description="是否发送邮件") - ToAddress: str | None = Field(default=None, description="收件地址") - IfServerChan: bool | None = Field(default=None, description="是否启用Server酱") - ServerChanKey: str | None = Field(default=None, description="Server酱密钥") -class MaaEndUserConfig(ApiModel): +class MaaEndUserConfig(_MaaEndUserConfigReadBase): type: Literal["MaaEndUserConfig"] = Field( default="MaaEndUserConfig", description="配置类型" ) - Info: MaaEndUserConfigInfo | None = Field(default=None, description="用户信息") - Task: MaaEndUserConfigTask | None = Field(default=None, description="任务配置") - Notify: MaaEndUserConfigNotify | None = Field( - default=None, description="通知配置" - ) - -class MaaEndConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="脚本名称") - Path: str | None = Field(default=None, description="脚本路径") - -class MaaEndConfigRun(ApiModel): - RunTimeLimit: int | None = Field(default=None, description="运行时间限制(分钟)") - ProxyTimesLimit: int | None = Field(default=None, description="每日代理次数限制") - RunTimesLimit: int | None = Field(default=None, description="重试次数限制") - - -class MaaEndConfigGame(ApiModel): - ControllerType: Literal[ - "Win32-Window", "Win32-Window-Background", "Win32-Front", "ADB" - ] | None = Field(default=None, description="控制器类型") - Path: str | None = Field(default=None, description="终末地客户端路径") - Arguments: str | None = Field(default=None, description="游戏启动参数") - WaitTime: int | None = Field(default=None, description="游戏等待时间") - EmulatorId: str | None = Field(default=None, description="模拟器ID") - EmulatorIndex: str | None = Field(default=None, description="模拟器索引") - CloseOnFinish: bool | None = Field(default=None, description="结束后关闭游戏") - - -class MaaEndConfig(ApiModel): - type: Literal["MaaEndConfig"] = Field(default="MaaEndConfig", description="配置类型") - Info: MaaEndConfigInfo | None = Field(default=None, description="脚本信息") - Run: MaaEndConfigRun | None = Field(default=None, description="运行配置") - Game: MaaEndConfigGame | None = Field(default=None, description="游戏配置") +class MaaEndUserConfigPatch(_MaaEndUserConfigPatchBase): + type: Literal["MaaEndUserConfig"] | None = Field( + default=None, description="配置类型" + ) __all__ = [ - "MaaEndUserConfigInfo", - "MaaEndUserConfigTask", - "MaaEndUserConfigNotify", - "MaaEndUserConfig", - "MaaEndConfigInfo", - "MaaEndConfigRun", - "MaaEndConfigGame", "MaaEndConfig", + "MaaEndConfigPatch", + "MaaEndUserConfig", + "MaaEndUserConfigPatch", ] diff --git a/app/models/queue_contract.py b/app/models/queue_contract.py index 8c40610c..a14fbb48 100644 --- a/app/models/queue_contract.py +++ b/app/models/queue_contract.py @@ -4,259 +4,105 @@ from pydantic import Field -from .common_contract import ApiModel, OutBase +from .common import QueueConfig, QueueItem, TimeSet +from .common_contract import ( + ApiModel, + ResourceCollectionOut, + ResourceCreateOut, + ResourceItemOut, + derive_config_contracts, +) -class QueueIndexItem(ApiModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["QueueConfig"] = Field(..., description="配置类型") - - -class QueueItemIndexItem(ApiModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["QueueItem"] = Field(..., description="配置类型") - - -class TimeSetIndexItem(ApiModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["TimeSet"] = Field(..., description="配置类型") - - -class QueueItemInfoRead(ApiModel): - ScriptId: str = Field(default="-", description="任务所对应的脚本ID") - - -class QueueItemInfoPatch(ApiModel): - ScriptId: str | None = Field(default=None, description="任务所对应的脚本ID") - - -class QueueItemRead(ApiModel): - Info: QueueItemInfoRead = Field(default_factory=QueueItemInfoRead, description="队列项") - - -class QueueItemPatch(ApiModel): - Info: QueueItemInfoPatch | None = Field(default=None, description="队列项") - - -class TimeSetInfoRead(ApiModel): - Enabled: bool = Field(default=True, description="是否启用") - Days: list[ - Literal[ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] - ] = Field( - default_factory=lambda: [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ], - description="执行周期, 可多选", - ) - Time: str = Field(default="00:00", description="时间设置, 格式为HH:MM") - - -class TimeSetInfoPatch(ApiModel): - Enabled: bool | None = Field(default=None, description="是否启用") - Days: list[ - Literal[ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] - ] | None = Field(default=None, description="执行周期, 可多选") - Time: str | None = Field(default=None, description="时间设置, 格式为HH:MM") - - -class TimeSetRead(ApiModel): - Info: TimeSetInfoRead = Field(default_factory=TimeSetInfoRead, description="时间项") - - -class TimeSetPatch(ApiModel): - Info: TimeSetInfoPatch | None = Field(default=None, description="时间项") - - -class QueueInfoRead(ApiModel): - Name: str = Field(default="新队列", description="队列名称") - TimeEnabled: bool = Field(default=False, description="是否启用定时") - StartUpEnabled: bool = Field(default=False, description="是否启动时运行") - AfterAccomplish: Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] = Field(default="NoAction", description="完成后操作") - - -class QueueInfoPatch(ApiModel): - Name: str | None = Field(default=None, description="队列名称") - TimeEnabled: bool | None = Field(default=None, description="是否启用定时") - StartUpEnabled: bool | None = Field(default=None, description="是否启动时运行") - AfterAccomplish: Literal[ - "NoAction", - "Shutdown", - "ShutdownForce", - "Reboot", - "Hibernate", - "Sleep", - "KillSelf", - ] | None = Field(default=None, description="完成后操作") - - -class QueueRead(ApiModel): - Info: QueueInfoRead = Field(default_factory=QueueInfoRead, description="队列信息") - - -class QueuePatch(ApiModel): - Info: QueueInfoPatch | None = Field(default=None, description="队列信息") - - -class QueueCreateOut(OutBase): - queueId: str = Field(..., description="新创建的队列ID") - data: QueueRead = Field(..., description="队列配置数据") +_QueueReadBase, _QueuePatchBase = derive_config_contracts( + QueueConfig, + read_name="QueueRead", + patch_name="QueuePatch", + include_groups=("Info",), +) +_TimeSetReadBase, _TimeSetPatchBase = derive_config_contracts( + TimeSet, + read_name="TimeSetRead", + patch_name="TimeSetPatch", + include_groups=("Info",), +) +_QueueItemReadBase, _QueueItemPatchBase = derive_config_contracts( + QueueItem, + read_name="QueueItemRead", + patch_name="QueueItemPatch", + include_groups=("Info",), +) -class QueueGetIn(ApiModel): - queueId: str | None = Field( - default=None, description="队列ID, 未携带时表示获取所有队列数据" - ) +class QueueRead(_QueueReadBase): + pass -class QueueGetOut(OutBase): - index: list[QueueIndexItem] = Field(..., description="队列索引列表") - data: dict[str, QueueRead] = Field( - ..., description="队列数据字典, key来自于index列表的uid" - ) +class QueuePatch(_QueuePatchBase): + pass -class QueueUpdateIn(ApiModel): - queueId: str = Field(..., description="队列ID") - data: QueuePatch = Field(..., description="队列更新数据") +class TimeSetRead(_TimeSetReadBase): + pass -class QueueDeleteIn(ApiModel): - queueId: str = Field(..., description="队列ID") +class TimeSetPatch(_TimeSetPatchBase): + pass -class QueueReorderIn(ApiModel): - indexList: list[str] = Field(..., description="按新顺序排列的调度队列UID列表") +class QueueItemRead(_QueueItemReadBase): + pass -class QueueSetInBase(ApiModel): - queueId: str = Field(..., description="所属队列ID") +class QueueItemPatch(_QueueItemPatchBase): + pass -class TimeSetGetIn(QueueSetInBase): - timeSetId: str | None = Field( - default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据" - ) - - -class TimeSetGetOut(OutBase): - index: list[TimeSetIndexItem] = Field(..., description="时间设置索引列表") - data: dict[str, TimeSetRead] = Field( - ..., description="时间设置数据字典, key来自于index列表的uid" - ) - - -class TimeSetCreateOut(OutBase): - timeSetId: str = Field(..., description="新创建的时间设置ID") - data: TimeSetRead = Field(..., description="时间设置配置数据") - - -class TimeSetUpdateIn(QueueSetInBase): - timeSetId: str = Field(..., description="时间设置ID") - data: TimeSetPatch = Field(..., description="时间设置更新数据") - - -class TimeSetDeleteIn(QueueSetInBase): - timeSetId: str = Field(..., description="时间设置ID") - - -class TimeSetReorderIn(QueueSetInBase): - indexList: list[str] = Field(..., description="时间设置ID列表, 按新顺序排列") - - -class QueueItemGetIn(QueueSetInBase): - queueItemId: str | None = Field( - default=None, description="队列项ID, 未携带时表示获取所有队列项数据" - ) - - -class QueueItemGetOut(OutBase): - index: list[QueueItemIndexItem] = Field(..., description="队列项索引列表") - data: dict[str, QueueItemRead] = Field( - ..., description="队列项数据字典, key来自于index列表的uid" - ) +class QueueIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["QueueConfig"] = Field(..., description="配置类型") -class QueueItemCreateOut(OutBase): - queueItemId: str = Field(..., description="新创建的队列项ID") - data: QueueItemRead = Field(..., description="队列项配置数据") +class QueueItemIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["QueueItem"] = Field(..., description="配置类型") -class QueueItemUpdateIn(QueueSetInBase): - queueItemId: str = Field(..., description="队列项ID") - data: QueueItemPatch = Field(..., description="队列项更新数据") +class TimeSetIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["TimeSet"] = Field(..., description="配置类型") -class QueueItemDeleteIn(QueueSetInBase): - queueItemId: str = Field(..., description="队列项ID") +QueueCreateOut = ResourceCreateOut[QueueRead] +QueueDetailOut = ResourceItemOut[QueueRead] +QueueGetOut = ResourceCollectionOut[QueueIndexItem, QueueRead] +TimeSetCreateOut = ResourceCreateOut[TimeSetRead] +TimeSetDetailOut = ResourceItemOut[TimeSetRead] +TimeSetGetOut = ResourceCollectionOut[TimeSetIndexItem, TimeSetRead] -class QueueItemReorderIn(QueueSetInBase): - indexList: list[str] = Field(..., description="队列项ID列表, 按新顺序排列") +QueueItemCreateOut = ResourceCreateOut[QueueItemRead] +QueueItemDetailOut = ResourceItemOut[QueueItemRead] +QueueItemGetOut = ResourceCollectionOut[QueueItemIndexItem, QueueItemRead] __all__ = [ + "QueueRead", + "QueuePatch", + "TimeSetRead", + "TimeSetPatch", + "QueueItemRead", + "QueueItemPatch", "QueueIndexItem", "QueueItemIndexItem", "TimeSetIndexItem", - "QueueItemInfoRead", - "QueueItemInfoPatch", - "QueueItemRead", - "QueueItemPatch", - "TimeSetInfoRead", - "TimeSetInfoPatch", - "TimeSetRead", - "TimeSetPatch", - "QueueInfoRead", - "QueueInfoPatch", - "QueueRead", - "QueuePatch", "QueueCreateOut", - "QueueGetIn", + "QueueDetailOut", "QueueGetOut", - "QueueUpdateIn", - "QueueDeleteIn", - "QueueReorderIn", - "QueueSetInBase", - "TimeSetGetIn", - "TimeSetGetOut", "TimeSetCreateOut", - "TimeSetUpdateIn", - "TimeSetDeleteIn", - "TimeSetReorderIn", - "QueueItemGetIn", - "QueueItemGetOut", + "TimeSetDetailOut", + "TimeSetGetOut", "QueueItemCreateOut", - "QueueItemUpdateIn", - "QueueItemDeleteIn", - "QueueItemReorderIn", + "QueueItemDetailOut", + "QueueItemGetOut", ] diff --git a/app/models/scripts_contract.py b/app/models/scripts_contract.py index eb8651f5..c59287a1 100644 --- a/app/models/scripts_contract.py +++ b/app/models/scripts_contract.py @@ -5,20 +5,35 @@ from pydantic import Field, TypeAdapter -from .common_contract import ApiModel, OutBase, project_model -from .general_contract import GeneralConfig, GeneralUserConfig -from .maa_contract import MaaConfig, MaaUserConfig -from .maaend_contract import MaaEndConfig, MaaEndUserConfig -from .src_contract import SrcConfig, SrcUserConfig +from .common_contract import ( + ApiModel, + ResourceCollectionOut, + ResourceCreateOut, + ResourceItemOut, + project_model, +) +from .general_contract import ( + GeneralConfig, + GeneralConfigPatch, + GeneralUserConfig, + GeneralUserConfigPatch, +) +from .maa_contract import MaaConfig, MaaConfigPatch, MaaUserConfig, MaaUserConfigPatch +from .maaend_contract import ( + MaaEndConfig, + MaaEndConfigPatch, + MaaEndUserConfig, + MaaEndUserConfigPatch, +) +from .src_contract import SrcConfig, SrcConfigPatch, SrcUserConfig, SrcUserConfigPatch + ScriptConfigType = Literal["MaaConfig", "GeneralConfig", "SrcConfig", "MaaEndConfig"] UserConfigType = Literal[ "MaaUserConfig", "GeneralUserConfig", "SrcUserConfig", "MaaEndUserConfig" ] ScriptCreateType = Literal["MAA", "SRC", "General", "MaaEnd"] -JsonScalar: TypeAlias = str | int | float | bool | None -JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"] -PatchPayload: TypeAlias = dict[str, JsonValue] +PatchPayload: TypeAlias = dict[str, object] ScriptModel = MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig UserModel = MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig @@ -31,6 +46,18 @@ | type[GeneralUserConfig] | type[MaaEndUserConfig] ) +ScriptPatchClass = ( + type[MaaConfigPatch] + | type[SrcConfigPatch] + | type[GeneralConfigPatch] + | type[MaaEndConfigPatch] +) +UserPatchClass = ( + type[MaaUserConfigPatch] + | type[SrcUserConfigPatch] + | type[GeneralUserConfigPatch] + | type[MaaEndUserConfigPatch] +) ScriptReadData = Annotated[ MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig, @@ -47,12 +74,24 @@ "SrcConfig": SrcConfig, "MaaEndConfig": MaaEndConfig, } +SCRIPT_PATCH_BY_TYPE: dict[ScriptConfigType, ScriptPatchClass] = { + "MaaConfig": MaaConfigPatch, + "GeneralConfig": GeneralConfigPatch, + "SrcConfig": SrcConfigPatch, + "MaaEndConfig": MaaEndConfigPatch, +} USER_CONTRACT_BY_TYPE: dict[UserConfigType, UserModelClass] = { "MaaUserConfig": MaaUserConfig, "GeneralUserConfig": GeneralUserConfig, "SrcUserConfig": SrcUserConfig, "MaaEndUserConfig": MaaEndUserConfig, } +USER_PATCH_BY_TYPE: dict[UserConfigType, UserPatchClass] = { + "MaaUserConfig": MaaUserConfigPatch, + "GeneralUserConfig": GeneralUserConfigPatch, + "SrcUserConfig": SrcUserConfigPatch, + "MaaEndUserConfig": MaaEndUserConfigPatch, +} SCRIPT_CREATE_TO_CONFIG_TYPE: dict[ScriptCreateType, ScriptConfigType] = { "MAA": "MaaConfig", "SRC": "SrcConfig", @@ -65,7 +104,7 @@ "SrcConfig": "SrcUserConfig", "MaaEndConfig": "MaaEndUserConfig", } -PATCH_PAYLOAD_ADAPTER: TypeAdapter[PatchPayload] = TypeAdapter(dict[str, JsonValue]) +PATCH_PAYLOAD_ADAPTER: TypeAdapter[PatchPayload] = TypeAdapter(dict[str, object]) class ScriptIndexItem(ApiModel): @@ -82,101 +121,37 @@ class ScriptCreateIn(ApiModel): type: ScriptCreateType = Field( ..., description="脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本" ) - scriptId: str | None = Field( - default=None, description="直接从该脚本ID复制创建, 仅在复制创建时使用" - ) - - -class ScriptCreateOut(OutBase): - scriptId: str = Field(..., description="新创建的脚本ID") - data: ScriptReadData = Field(..., description="脚本配置数据") - - -class ScriptGetIn(ApiModel): - scriptId: str | None = Field( - default=None, description="脚本ID, 未携带时表示获取所有脚本数据" - ) - - -class ScriptGetOut(OutBase): - index: list[ScriptIndexItem] = Field(..., description="脚本索引列表") - data: dict[str, ScriptReadData] = Field( - ..., description="脚本数据字典, key来自于index列表的uid" - ) - - -class ScriptUpdateIn(ApiModel): - scriptId: str = Field(..., description="脚本ID") - data: PatchPayload = Field( - ..., description="脚本更新数据, 由后端根据 scriptId 选择对应 Patch 模型校验" + copyFromId: str | None = Field( + default=None, description="直接从该脚本 ID 复制创建, 仅复制创建时使用" ) -class ScriptDeleteIn(ApiModel): - scriptId: str = Field(..., description="脚本ID") +ScriptCreateOut = ResourceCreateOut[ScriptReadData] +ScriptDetailOut = ResourceItemOut[ScriptReadData] +ScriptGetOut = ResourceCollectionOut[ScriptIndexItem, ScriptReadData] -class ScriptReorderIn(ApiModel): - indexList: list[str] = Field(..., description="脚本ID列表, 按新顺序排列") +class ScriptFileBody(ApiModel): + path: str = Field(..., description="文件路径") -class ScriptFileIn(ApiModel): - scriptId: str = Field(..., description="脚本ID") - jsonFile: str = Field(..., description="配置文件路径") +class ScriptUrlBody(ApiModel): + url: str = Field(..., description="配置文件 URL") -class ScriptUrlIn(ApiModel): - scriptId: str = Field(..., description="脚本ID") - url: str = Field(..., description="配置文件URL") - - -class ScriptUploadIn(ApiModel): - scriptId: str = Field(..., description="脚本ID") +class ScriptUploadBody(ApiModel): config_name: str = Field(..., description="配置名称") author: str = Field(..., description="作者") description: str = Field(..., description="描述") -class UserInBase(ApiModel): - scriptId: str = Field(..., description="所属脚本ID") - - -class UserGetIn(UserInBase): - userId: str | None = Field( - default=None, description="用户ID, 未携带时表示获取所有用户数据" - ) - - -class UserGetOut(OutBase): - index: list[UserIndexItem] = Field(..., description="用户索引列表") - data: dict[str, UserReadData] = Field( - ..., description="用户数据字典, key来自于index列表的uid" - ) - - -class UserCreateOut(OutBase): - userId: str = Field(..., description="新创建的用户ID") - data: UserReadData = Field(..., description="用户配置数据") - - -class UserUpdateIn(UserInBase): - userId: str = Field(..., description="用户ID") - data: PatchPayload = Field( - ..., description="用户更新数据, 由后端根据 scriptId 选择对应 Patch 模型校验" - ) - +UserGetOut = ResourceCollectionOut[UserIndexItem, UserReadData] +UserDetailOut = ResourceItemOut[UserReadData] +UserCreateOut = ResourceCreateOut[UserReadData] -class UserDeleteIn(UserInBase): - userId: str = Field(..., description="用户ID") - -class UserReorderIn(UserInBase): - indexList: list[str] = Field(..., description="用户ID列表, 按新顺序排列") - - -class UserSetIn(UserInBase): - userId: str = Field(..., description="用户ID") - jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件") +class InfrastructureImportBody(ApiModel): + path: str = Field(..., description="JSON 文件路径, 用于导入自定义基建文件") def script_contract_type_from_create(create_type: ScriptCreateType) -> ScriptConfigType: @@ -211,7 +186,9 @@ def project_script_model_map( index_list: list[ScriptIndexItem], raw_map: Mapping[str, Mapping[str, Any] | ApiModel], ) -> dict[str, ScriptModel]: - index_map: dict[str, ScriptConfigType] = {item.uid: item.type for item in index_list} + index_map: dict[str, ScriptConfigType] = { + item.uid: item.type for item in index_list + } return { uid: project_script_model(index_map[uid], raw) for uid, raw in raw_map.items() @@ -232,55 +209,49 @@ def project_user_model_map( def validate_script_patch_data( - script_type: ScriptConfigType, raw: Mapping[str, JsonValue] + script_type: ScriptConfigType, raw: Mapping[str, object] ) -> dict[str, Any]: normalized = PATCH_PAYLOAD_ADAPTER.validate_python(raw) - validated = SCRIPT_CONTRACT_BY_TYPE[script_type].model_validate(normalized) - return validated.model_dump(exclude_unset=True, exclude={"type"}) + validated = SCRIPT_PATCH_BY_TYPE[script_type].model_validate(normalized) + return validated.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) def validate_user_patch_data( - user_type: UserConfigType, raw: Mapping[str, JsonValue] + user_type: UserConfigType, raw: Mapping[str, object] ) -> dict[str, Any]: normalized = PATCH_PAYLOAD_ADAPTER.validate_python(raw) - validated = USER_CONTRACT_BY_TYPE[user_type].model_validate(normalized) - return validated.model_dump(exclude_unset=True, exclude={"type"}) + validated = USER_PATCH_BY_TYPE[user_type].model_validate(normalized) + return validated.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) __all__ = [ "ScriptConfigType", "UserConfigType", "ScriptCreateType", - "JsonValue", "PatchPayload", "ScriptModel", "UserModel", "ScriptReadData", "UserReadData", "SCRIPT_CONTRACT_BY_TYPE", + "SCRIPT_PATCH_BY_TYPE", "USER_CONTRACT_BY_TYPE", + "USER_PATCH_BY_TYPE", "SCRIPT_CREATE_TO_CONFIG_TYPE", "SCRIPT_CONFIG_TO_USER_TYPE", "ScriptIndexItem", "UserIndexItem", "ScriptCreateIn", "ScriptCreateOut", - "ScriptGetIn", + "ScriptDetailOut", "ScriptGetOut", - "ScriptUpdateIn", - "ScriptDeleteIn", - "ScriptReorderIn", - "ScriptFileIn", - "ScriptUrlIn", - "ScriptUploadIn", - "UserInBase", - "UserGetIn", + "ScriptFileBody", + "ScriptUrlBody", + "ScriptUploadBody", "UserGetOut", + "UserDetailOut", "UserCreateOut", - "UserUpdateIn", - "UserDeleteIn", - "UserReorderIn", - "UserSetIn", + "InfrastructureImportBody", "script_contract_type_from_create", "script_contract_type_from_runtime", "user_contract_type_from_script", diff --git a/app/models/setting_contract.py b/app/models/setting_contract.py index 49ea15c2..711f094b 100644 --- a/app/models/setting_contract.py +++ b/app/models/setting_contract.py @@ -4,290 +4,65 @@ from pydantic import Field -from .common_contract import ApiModel, OutBase +from .common import Webhook +from .common_contract import ( + ApiModel, + ResourceCollectionOut, + ResourceCreateOut, + ResourceItemOut, + derive_config_contracts, +) +from .global_config import GlobalConfig -class WebhookIndexItem(ApiModel): - uid: str = Field(..., description="唯一标识符") - type: Literal["Webhook"] = Field(..., description="配置类型") - - -class WebhookInfoRead(ApiModel): - Name: str = Field(default="新自定义 Webhook 通知", description="Webhook名称") - Enabled: bool = Field(default=True, description="是否启用") - - -class WebhookInfoPatch(ApiModel): - Name: str | None = Field(default=None, description="Webhook名称") - Enabled: bool | None = Field(default=None, description="是否启用") - - -class WebhookDataRead(ApiModel): - Url: str = Field(default="", description="Webhook URL") - Template: str = Field(default="", description="消息模板") - Headers: str = Field(default="{ }", description="自定义请求头") - Method: Literal["POST", "GET"] = Field(default="POST", description="请求方法") - - -class WebhookDataPatch(ApiModel): - Url: str | None = Field(default=None, description="Webhook URL") - Template: str | None = Field(default=None, description="消息模板") - Headers: str | None = Field(default=None, description="自定义请求头") - Method: Literal["POST", "GET"] | None = Field( - default=None, description="请求方法" - ) - - -class WebhookRead(ApiModel): - Info: WebhookInfoRead = Field( - default_factory=WebhookInfoRead, description="Webhook基础信息" - ) - Data: WebhookDataRead = Field( - default_factory=WebhookDataRead, description="Webhook配置数据" - ) - - -class WebhookPatch(ApiModel): - Info: WebhookInfoPatch | None = Field(default=None, description="Webhook基础信息") - Data: WebhookDataPatch | None = Field(default=None, description="Webhook配置数据") - - -class GlobalConfigFunctionRead(ApiModel): - HistoryRetentionTime: Literal[7, 15, 30, 60, 90, 180, 365, 0] = Field( - default=0, description="历史记录保留时间, 0表示永久保存" - ) - IfAllowSleep: bool = Field(default=False, description="允许休眠") - IfSilence: bool = Field(default=False, description="静默模式") - IfAgreeBilibili: bool = Field(default=False, description="同意哔哩哔哩用户协议") - IfBlockAd: bool = Field(default=False, description="屏蔽模拟器广告") - - -class GlobalConfigFunctionPatch(ApiModel): - HistoryRetentionTime: Literal[7, 15, 30, 60, 90, 180, 365, 0] | None = Field( - default=None, description="历史记录保留时间, 0表示永久保存" - ) - IfAllowSleep: bool | None = Field(default=None, description="允许休眠") - IfSilence: bool | None = Field(default=None, description="静默模式") - IfAgreeBilibili: bool | None = Field( - default=None, description="同意哔哩哔哩用户协议" - ) - IfBlockAd: bool | None = Field(default=None, description="屏蔽模拟器广告") - - -class GlobalConfigVoiceRead(ApiModel): - Enabled: bool = Field(default=False, description="语音功能是否启用") - Type: Literal["simple", "noisy"] = Field( - default="simple", description="语音类型, simple为简洁, noisy为聒噪" - ) - - -class GlobalConfigVoicePatch(ApiModel): - Enabled: bool | None = Field(default=None, description="语音功能是否启用") - Type: Literal["simple", "noisy"] | None = Field( - default=None, description="语音类型, simple为简洁, noisy为聒噪" - ) - - -class GlobalConfigStartRead(ApiModel): - IfSelfStart: bool = Field(default=False, description="是否在系统启动时自动运行") - IfMinimizeDirectly: bool = Field( - default=False, description="启动时是否直接最小化到托盘而不显示主窗口" - ) - - -class GlobalConfigStartPatch(ApiModel): - IfSelfStart: bool | None = Field(default=None, description="是否在系统启动时自动运行") - IfMinimizeDirectly: bool | None = Field( - default=None, description="启动时是否直接最小化到托盘而不显示主窗口" - ) - +_WebhookReadBase, _WebhookPatchBase = derive_config_contracts( + Webhook, + read_name="WebhookRead", + patch_name="WebhookPatch", +) +_GlobalConfigReadBase, _GlobalConfigPatchBase = derive_config_contracts( + GlobalConfig, + read_name="GlobalConfigRead", + patch_name="GlobalConfigPatch", + include_groups=("Function", "Voice", "Start", "UI", "Notify", "Update"), +) -class GlobalConfigUIRead(ApiModel): - IfShowTray: bool = Field(default=False, description="是否常态显示托盘图标") - IfToTray: bool = Field(default=False, description="是否最小化到托盘") +class WebhookRead(_WebhookReadBase): + pass -class GlobalConfigUIPatch(ApiModel): - IfShowTray: bool | None = Field(default=None, description="是否常态显示托盘图标") - IfToTray: bool | None = Field(default=None, description="是否最小化到托盘") +class WebhookPatch(_WebhookPatchBase): + pass -class GlobalConfigNotifyRead(ApiModel): - SendTaskResultTime: Literal["不推送", "任何时刻", "仅失败时"] = Field( - default="不推送", description="任务结果推送时机" - ) - IfSendStatistic: bool = Field(default=False, description="是否发送统计信息") - IfSendSixStar: bool = Field(default=False, description="是否发送公招六星通知") - IfPushPlyer: bool = Field(default=False, description="是否推送系统通知") - IfSendMail: bool = Field(default=False, description="是否发送邮件通知") - IfKoishiSupport: bool = Field(default=False, description="是否启用Koishi支持") - KoishiServerAddress: str = Field( - default="ws://localhost:5140/AUTO_MAS", description="Koishi服务器地址" - ) - KoishiToken: str = Field(default="", description="Koishi Token") - SMTPServerAddress: str = Field(default="", description="SMTP服务器地址") - AuthorizationCode: str = Field(default="", description="SMTP授权码") - FromAddress: str = Field(default="", description="邮件发送地址") - ToAddress: str = Field(default="", description="邮件接收地址") - IfServerChan: bool = Field(default=False, description="是否使用ServerChan推送") - ServerChanKey: str = Field(default="", description="ServerChan推送密钥") +class GlobalConfigRead(_GlobalConfigReadBase): + pass -class GlobalConfigNotifyPatch(ApiModel): - SendTaskResultTime: Literal["不推送", "任何时刻", "仅失败时"] | None = Field( - default=None, description="任务结果推送时机" - ) - IfSendStatistic: bool | None = Field(default=None, description="是否发送统计信息") - IfSendSixStar: bool | None = Field(default=None, description="是否发送公招六星通知") - IfPushPlyer: bool | None = Field(default=None, description="是否推送系统通知") - IfSendMail: bool | None = Field(default=None, description="是否发送邮件通知") - IfKoishiSupport: bool | None = Field(default=None, description="是否启用Koishi支持") - KoishiServerAddress: str | None = Field(default=None, description="Koishi服务器地址") - KoishiToken: str | None = Field(default=None, description="Koishi Token") - SMTPServerAddress: str | None = Field(default=None, description="SMTP服务器地址") - AuthorizationCode: str | None = Field(default=None, description="SMTP授权码") - FromAddress: str | None = Field(default=None, description="邮件发送地址") - ToAddress: str | None = Field(default=None, description="邮件接收地址") - IfServerChan: bool | None = Field(default=None, description="是否使用ServerChan推送") - ServerChanKey: str | None = Field(default=None, description="ServerChan推送密钥") +class GlobalConfigPatch(_GlobalConfigPatchBase): + pass -class GlobalConfigUpdateRead(ApiModel): - IfAutoUpdate: bool = Field(default=False, description="是否自动更新") - Source: Literal["GitHub", "MirrorChyan", "AutoSite"] = Field( - default="GitHub", description="更新源: GitHub源, Mirror酱源, 自建源" - ) - Channel: Literal["stable", "beta"] = Field( - default="stable", description="更新渠道: 稳定版, 测试版" - ) - ProxyAddress: str = Field(default="", description="网络代理地址") - MirrorChyanCDK: str = Field(default="", description="Mirror酱CDK") - -class GlobalConfigUpdatePatch(ApiModel): - IfAutoUpdate: bool | None = Field(default=None, description="是否自动更新") - Source: Literal["GitHub", "MirrorChyan", "AutoSite"] | None = Field( - default=None, description="更新源: GitHub源, Mirror酱源, 自建源" - ) - Channel: Literal["stable", "beta"] | None = Field( - default=None, description="更新渠道: 稳定版, 测试版" - ) - ProxyAddress: str | None = Field(default=None, description="网络代理地址") - MirrorChyanCDK: str | None = Field(default=None, description="Mirror酱CDK") - - -class GlobalConfigRead(ApiModel): - Function: GlobalConfigFunctionRead = Field( - default_factory=GlobalConfigFunctionRead, description="功能相关配置" - ) - Voice: GlobalConfigVoiceRead = Field( - default_factory=GlobalConfigVoiceRead, description="语音相关配置" - ) - Start: GlobalConfigStartRead = Field( - default_factory=GlobalConfigStartRead, description="启动相关配置" - ) - UI: GlobalConfigUIRead = Field(default_factory=GlobalConfigUIRead, description="界面相关配置") - Notify: GlobalConfigNotifyRead = Field( - default_factory=GlobalConfigNotifyRead, description="通知相关配置" - ) - Update: GlobalConfigUpdateRead = Field( - default_factory=GlobalConfigUpdateRead, description="更新相关配置" - ) - - -class GlobalConfigPatch(ApiModel): - Function: GlobalConfigFunctionPatch | None = Field( - default=None, description="功能相关配置" - ) - Voice: GlobalConfigVoicePatch | None = Field(default=None, description="语音相关配置") - Start: GlobalConfigStartPatch | None = Field(default=None, description="启动相关配置") - UI: GlobalConfigUIPatch | None = Field(default=None, description="界面相关配置") - Notify: GlobalConfigNotifyPatch | None = Field(default=None, description="通知相关配置") - Update: GlobalConfigUpdatePatch | None = Field(default=None, description="更新相关配置") - - -class WebhookInBase(ApiModel): - scriptId: str | None = Field( - default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带" - ) - userId: str | None = Field( - default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带" - ) - - -class WebhookGetIn(WebhookInBase): - webhookId: str | None = Field( - default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据" - ) - - -class WebhookGetOut(OutBase): - index: list[WebhookIndexItem] = Field(..., description="Webhook索引列表") - data: dict[str, WebhookRead] = Field( - ..., description="Webhook数据字典, key来自于index列表的uid" - ) - - -class WebhookCreateOut(OutBase): - webhookId: str = Field(..., description="新创建的Webhook ID") - data: WebhookRead = Field(..., description="Webhook配置数据") - - -class WebhookUpdateIn(WebhookInBase): - webhookId: str = Field(..., description="Webhook ID") - data: WebhookPatch = Field(..., description="Webhook更新数据") - - -class WebhookDeleteIn(WebhookInBase): - webhookId: str = Field(..., description="Webhook ID") - - -class WebhookReorderIn(WebhookInBase): - indexList: list[str] = Field(..., description="Webhook ID列表, 按新顺序排列") - - -class WebhookTestIn(WebhookInBase): - data: WebhookPatch = Field(..., description="Webhook配置数据") - - -class SettingGetOut(OutBase): - data: GlobalConfigRead = Field(..., description="全局设置数据") +class WebhookIndexItem(ApiModel): + uid: str = Field(..., description="唯一标识符") + type: Literal["Webhook"] = Field(..., description="配置类型") -class SettingUpdateIn(ApiModel): - data: GlobalConfigPatch = Field(..., description="全局设置需要更新的数据") +WebhookGetOut = ResourceCollectionOut[WebhookIndexItem, WebhookRead] +WebhookDetailOut = ResourceItemOut[WebhookRead] +WebhookCreateOut = ResourceCreateOut[WebhookRead] +SettingGetOut = ResourceItemOut[GlobalConfigRead] __all__ = [ - "WebhookIndexItem", - "WebhookInfoRead", - "WebhookInfoPatch", - "WebhookDataRead", - "WebhookDataPatch", "WebhookRead", "WebhookPatch", - "GlobalConfigFunctionRead", - "GlobalConfigFunctionPatch", - "GlobalConfigVoiceRead", - "GlobalConfigVoicePatch", - "GlobalConfigStartRead", - "GlobalConfigStartPatch", - "GlobalConfigUIRead", - "GlobalConfigUIPatch", - "GlobalConfigNotifyRead", - "GlobalConfigNotifyPatch", - "GlobalConfigUpdateRead", - "GlobalConfigUpdatePatch", "GlobalConfigRead", "GlobalConfigPatch", - "WebhookInBase", - "WebhookGetIn", + "WebhookIndexItem", "WebhookGetOut", + "WebhookDetailOut", "WebhookCreateOut", - "WebhookUpdateIn", - "WebhookDeleteIn", - "WebhookReorderIn", - "WebhookTestIn", "SettingGetOut", - "SettingUpdateIn", ] diff --git a/app/models/src_contract.py b/app/models/src_contract.py index 670319bd..919757c4 100644 --- a/app/models/src_contract.py +++ b/app/models/src_contract.py @@ -4,201 +4,46 @@ from pydantic import Field -from .common_contract import ApiModel +from .common_contract import derive_config_contracts +from .src import SrcConfig as RuntimeSrcConfig +from .src import SrcUserConfig as RuntimeSrcUserConfig -class SrcUserConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="用户名称") - Status: bool | None = Field(default=None, description="是否启用") - Id: str | None = Field(default=None, description="用户ID") - Password: str | None = Field(default=None, description="密码") - Mode: Literal["简洁", "详细"] | None = Field( - default=None, description="脚本模式" - ) - Server: Literal[ - "CN-Official", - "CN-Bilibili", - "VN-Official", - "OVERSEA-America", - "OVERSEA-Asia", - "OVERSEA-Europe", - "OVERSEA-TWHKMO", - ] | None = Field(default=None, description="游戏服务器") - RemainedDay: int | None = Field(default=None, description="剩余天数") - Notes: str | None = Field(default=None, description="备注") - Tag: str | None = Field(default=None, description="用户标签信息") +_SrcConfigReadBase, _SrcConfigPatchBase = derive_config_contracts( + RuntimeSrcConfig, + read_name="SrcConfig", + patch_name="SrcConfigPatch", +) +_SrcUserConfigReadBase, _SrcUserConfigPatchBase = derive_config_contracts( + RuntimeSrcUserConfig, + read_name="SrcUserConfig", + patch_name="SrcUserConfigPatch", +) -class SrcUserConfigStage(ApiModel): - Channel: Literal["Relic", "Materials", "Ornament"] | None = Field( - default=None, description="关卡通道" - ) - Relic: Literal[ - "-", - "Cavern_of_Corrosion_Path_of_Possession", - "Cavern_of_Corrosion_Path_of_Hidden_Salvation", - "Cavern_of_Corrosion_Path_of_Thundersurge", - "Cavern_of_Corrosion_Path_of_Aria", - "Cavern_of_Corrosion_Path_of_Uncertainty", - "Cavern_of_Corrosion_Path_of_Cavalier", - "Cavern_of_Corrosion_Path_of_Dreamdive", - "Cavern_of_Corrosion_Path_of_Darkness", - "Cavern_of_Corrosion_Path_of_Elixir_Seekers", - "Cavern_of_Corrosion_Path_of_Conflagration", - "Cavern_of_Corrosion_Path_of_Holy_Hymn", - "Cavern_of_Corrosion_Path_of_Providence", - "Cavern_of_Corrosion_Path_of_Drifting", - "Cavern_of_Corrosion_Path_of_Jabbing_Punch", - "Cavern_of_Corrosion_Path_of_Gelid_Wind", - ] | None = Field(default=None, description="遗器关卡") - Materials: Literal[ - "-", - "Calyx_Golden_Memories_Planarcadia", - "Calyx_Golden_Aether_Planarcadia", - "Calyx_Golden_Treasures_Planarcadia", - "Calyx_Golden_Memories_Amphoreus", - "Calyx_Golden_Aether_Amphoreus", - "Calyx_Golden_Treasures_Amphoreus", - "Calyx_Golden_Memories_Penacony", - "Calyx_Golden_Aether_Penacony", - "Calyx_Golden_Treasures_Penacony", - "Calyx_Golden_Memories_The_Xianzhou_Luofu", - "Calyx_Golden_Aether_The_Xianzhou_Luofu", - "Calyx_Golden_Treasures_The_Xianzhou_Luofu", - "Calyx_Golden_Memories_Jarilo_VI", - "Calyx_Golden_Aether_Jarilo_VI", - "Calyx_Golden_Treasures_Jarilo_VI", - "Calyx_Crimson_Destruction_Herta_StorageZone", - "Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape", - "Calyx_Crimson_Preservation_Herta_SupplyZone", - "Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark", - "Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains", - "Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue", - "Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime", - "Calyx_Crimson_Abundance_Jarilo_BackwaterPass", - "Calyx_Crimson_Abundance_Luofu_FyxestrollGarden", - "Calyx_Crimson_Erudition_Jarilo_RivetTown", - "Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater", - "Calyx_Crimson_Harmony_Jarilo_RobotSettlement", - "Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape", - "Calyx_Crimson_Nihility_Jarilo_GreatMine", - "Calyx_Crimson_Nihility_Luofu_AlchemyCommission", - "Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos", - "Calyx_Crimson_Elation_Planarcadia_WorldEndTavern", - "Stagnant_Shadow_Quanta", - "Stagnant_Shadow_Gust", - "Stagnant_Shadow_Fulmination", - "Stagnant_Shadow_Blaze", - "Stagnant_Shadow_Spike", - "Stagnant_Shadow_Rime", - "Stagnant_Shadow_Mirage", - "Stagnant_Shadow_Icicle", - "Stagnant_Shadow_Doom", - "Stagnant_Shadow_Puppetry", - "Stagnant_Shadow_Abomination", - "Stagnant_Shadow_Scorch", - "Stagnant_Shadow_Celestial", - "Stagnant_Shadow_Perdition", - "Stagnant_Shadow_Nectar", - "Stagnant_Shadow_Roast", - "Stagnant_Shadow_Ire", - "Stagnant_Shadow_Duty", - "Stagnant_Shadow_Timbre", - "Stagnant_Shadow_Mechwolf", - "Stagnant_Shadow_Gloam", - "Stagnant_Shadow_Sloggyre", - "Stagnant_Shadow_Gelidmoon", - "Stagnant_Shadow_Deepsheaf", - "Stagnant_Shadow_Cinders", - "Stagnant_Shadow_Sirens", - "Stagnant_Shadow_Ashes", - "Stagnant_Shadow_Soundburst", - ] | None = Field(default=None, description="材料关卡") - Ornament: Literal[ - "-", - "Divergent_Universe_Within_the_West_Wind", - "Divergent_Universe_Moonlit_Blood", - "Divergent_Universe_Unceasing_Strife", - "Divergent_Universe_Famished_Worker", - "Divergent_Universe_Eternal_Comedy", - "Divergent_Universe_To_Sweet_Dreams", - "Divergent_Universe_Pouring_Blades", - "Divergent_Universe_Fruit_of_Evil", - "Divergent_Universe_Permafrost", - "Divergent_Universe_Gentle_Words", - "Divergent_Universe_Smelted_Heart", - "Divergent_Universe_Untoppled_Walls", - ] | None = Field(default=None, description="饰品关卡") - ExtractReservedTrailblazePower: bool | None = Field( - default=None, description="使用储备开拓力" - ) - UseFuel: bool | None = Field(default=None, description="使用燃料") - FuelReserve: int | None = Field(default=None, description="保留的燃料数量") - EchoOfWar: str | None = Field(default=None, description="历战余响关卡") - SimulatedUniverseWorld: str | None = Field( - default=None, description="模拟宇宙关卡" - ) +class SrcConfig(_SrcConfigReadBase): + type: Literal["SrcConfig"] = Field(default="SrcConfig", description="配置类型") -class SrcUserConfigData(ApiModel): - LastProxyDate: str | None = Field(default=None, description="上次代理日期") - ProxyTimes: int | None = Field(default=None, description="代理次数") - IfPassCheck: bool | None = Field(default=None, description="是否通过检查") +class SrcConfigPatch(_SrcConfigPatchBase): + type: Literal["SrcConfig"] | None = Field(default=None, description="配置类型") -class SrcUserConfigNotify(ApiModel): - Enabled: bool | None = Field(default=None, description="是否启用通知") - IfSendStatistic: bool | None = Field( - default=None, description="是否发送统计信息" +class SrcUserConfig(_SrcUserConfigReadBase): + type: Literal["SrcUserConfig"] = Field( + default="SrcUserConfig", description="配置类型" ) - IfSendMail: bool | None = Field(default=None, description="是否发送邮件") - ToAddress: str | None = Field(default=None, description="收件地址") - IfServerChan: bool | None = Field(default=None, description="是否启用Server酱") - ServerChanKey: str | None = Field(default=None, description="Server酱密钥") - - -class SrcUserConfig(ApiModel): - type: Literal["SrcUserConfig"] = Field(default="SrcUserConfig", description="配置类型") - Info: SrcUserConfigInfo | None = Field(default=None, description="基础信息") - Stage: SrcUserConfigStage | None = Field(default=None, description="关卡配置") - Data: SrcUserConfigData | None = Field(default=None, description="用户数据") - Notify: SrcUserConfigNotify | None = Field(default=None, description="单独通知") - -class SrcConfigInfo(ApiModel): - Name: str | None = Field(default=None, description="SRC脚本名称") - Path: str | None = Field(default=None, description="SRC路径") - -class SrcConfigEmulator(ApiModel): - Id: str | None = Field(default=None, description="模拟器ID") - Index: str | None = Field(default=None, description="模拟器索引") - - -class SrcConfigRun(ApiModel): - TaskTransitionMethod: Literal["ExitGame", "ExitEmulator"] | None = Field( - default=None, description="任务切换方式" +class SrcUserConfigPatch(_SrcUserConfigPatchBase): + type: Literal["SrcUserConfig"] | None = Field( + default=None, description="配置类型" ) - ProxyTimesLimit: int | None = Field(default=None, description="代理次数限制") - RunTimesLimit: int | None = Field(default=None, description="运行次数限制") - RunTimeLimit: int | None = Field(default=None, description="运行时间限制(分钟)") - - -class SrcConfig(ApiModel): - type: Literal["SrcConfig"] = Field(default="SrcConfig", description="配置类型") - Info: SrcConfigInfo | None = Field(default=None, description="脚本基础信息") - Emulator: SrcConfigEmulator | None = Field(default=None, description="模拟器配置") - Run: SrcConfigRun | None = Field(default=None, description="脚本运行配置") __all__ = [ - "SrcUserConfigInfo", - "SrcUserConfigStage", - "SrcUserConfigData", - "SrcUserConfigNotify", - "SrcUserConfig", - "SrcConfigInfo", - "SrcConfigEmulator", - "SrcConfigRun", "SrcConfig", + "SrcConfigPatch", + "SrcUserConfig", + "SrcUserConfigPatch", ] diff --git a/app/models/tools_contract.py b/app/models/tools_contract.py index 6921fd08..a9c428e9 100644 --- a/app/models/tools_contract.py +++ b/app/models/tools_contract.py @@ -1,56 +1,29 @@ from __future__ import annotations -from pydantic import Field +from .common_contract import ResourceItemOut, derive_config_contracts +from .global_config import ToolsConfig -from .common_contract import ApiModel, OutBase +_ToolsConfigReadBase, _ToolsConfigPatchBase = derive_config_contracts( + ToolsConfig, + read_name="ToolsConfigRead", + patch_name="ToolsConfigPatch", +) -class ToolsConfigArknightsPCRead(ApiModel): - Enabled: bool = Field(default=False, description="是否启用 ArknightsPC 工具") - PauseKey: str = Field(default="f10", description="暂停键位") - SelectDeployedKey: str = Field(default="w", description="选中已部署干员键位") - UseSkillKey: str = Field(default="r", description="释放技能键位") - RetreatKey: str = Field(default="t", description="撤退键位") - NextFrameKey: str = Field(default="f", description="下一帧键位") - AnotherQuitKey: str = Field(default="space", description="自定义退出、暂停键位") - Status: str = Field(default="-", description="工具状态 Tag") +class ToolsConfigRead(_ToolsConfigReadBase): + pass -class ToolsConfigArknightsPCPatch(ApiModel): - Enabled: bool | None = Field(default=None, description="是否启用 ArknightsPC 工具") - PauseKey: str | None = Field(default=None, description="暂停键位") - SelectDeployedKey: str | None = Field(default=None, description="选中已部署干员键位") - UseSkillKey: str | None = Field(default=None, description="释放技能键位") - RetreatKey: str | None = Field(default=None, description="撤退键位") - NextFrameKey: str | None = Field(default=None, description="下一帧键位") - AnotherQuitKey: str | None = Field(default=None, description="自定义退出、暂停键位") +class ToolsConfigPatch(_ToolsConfigPatchBase): + pass -class ToolsConfigRead(ApiModel): - ArknightsPC: ToolsConfigArknightsPCRead = Field( - default_factory=ToolsConfigArknightsPCRead, description="明日方舟PC工具配置" - ) - -class ToolsConfigPatch(ApiModel): - ArknightsPC: ToolsConfigArknightsPCPatch | None = Field( - default=None, description="明日方舟PC工具配置" - ) - - -class ToolsGetOut(OutBase): - data: ToolsConfigRead = Field(..., description="工具配置数据") - - -class ToolsUpdateIn(ApiModel): - data: ToolsConfigPatch = Field(..., description="工具配置需要更新的数据") +ToolsGetOut = ResourceItemOut[ToolsConfigRead] __all__ = [ - "ToolsConfigArknightsPCRead", - "ToolsConfigArknightsPCPatch", "ToolsConfigRead", "ToolsConfigPatch", "ToolsGetOut", - "ToolsUpdateIn", ] From 40891c66fa62e6d14c478cc2a20ee8b0a95c1cbb Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 5 Apr 2026 02:14:21 +0800 Subject: [PATCH 12/29] =?UTF-8?q?refactor(api):=20=E6=95=B4=E7=90=86api=20?= =?UTF-8?q?config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/emulator.py | 10 +- app/api/plan.py | 163 +++++++++++------- app/api/queue.py | 14 +- app/api/scripts.py | 29 ++-- app/api/setting.py | 10 +- app/models/common_contract.py | 113 +++++++++++-- app/models/general_contract.py | 8 +- app/models/maa_contract.py | 10 +- app/models/maaend_contract.py | 8 +- app/models/plan_contract.py | 295 +++++++++++++++++++++++++-------- app/models/scripts_contract.py | 55 ++++-- app/models/src_contract.py | 6 +- 12 files changed, 528 insertions(+), 193 deletions(-) diff --git a/app/api/emulator.py b/app/api/emulator.py index b756f7a0..fc08535f 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -27,7 +27,13 @@ from app.api.common import api_delete, api_get, api_patch, api_post from app.core import Config, EmulatorManager -from app.models.common_contract import IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map +from app.models.common_contract import ( + IndexOrderPatch, + OutBase, + project_model, + project_model_list, + project_model_map, +) from app.models.emulator_contract import ( EmulatorActionBody, EmulatorConfigIndexItem, @@ -147,7 +153,7 @@ async def create_emulator() -> EmulatorCreateOut: }, ) async def reorder_emulator(body: IndexOrderPatch = Body(...)) -> OutBase: - await Config.reorder_emulator(body.indexList) + await Config.reorder_emulator(body.index_list) return OutBase() diff --git a/app/api/plan.py b/app/api/plan.py index 986f8cb1..b4560787 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -21,10 +21,14 @@ # Contact: DLmaster_361@163.com -from fastapi import APIRouter, Body +from typing import Annotated +from fastapi import APIRouter, Body, Path + +from app.api.common import api_delete, api_get, api_patch, api_post, error_out from app.core import Config from app.models.common_contract import ( + IndexOrderPatch, OutBase, project_model, project_model_list, @@ -34,91 +38,138 @@ MaaPlanRead, PlanCreateIn, PlanCreateOut, - PlanDeleteIn, - PlanGetIn, + PlanDetailOut, PlanGetOut, PlanIndexItem, - PlanReorderIn, - PlanUpdateIn, + PlanUpdateBody, ) -from app.api.common import error_out router = APIRouter(prefix="/api/plan", tags=["计划管理"]) +PlanIdPath = Annotated[str, Path(description="计划 ID")] + + +async def _build_plan_collection_out() -> PlanGetOut: + index, data = await Config.get_plan(None) + return PlanGetOut( + index=project_model_list(PlanIndexItem, index), + data=project_model_map(MaaPlanRead, data), + ) + -@router.post( - "/add", - tags=["Add"], - summary="添加计划表", - response_model=PlanCreateOut, - status_code=200, +async def _build_plan_detail_out(plan_id: str) -> PlanDetailOut: + _, data = await Config.get_plan(plan_id) + projected = project_model_map(MaaPlanRead, data) + return PlanDetailOut(data=projected[plan_id]) + + +@api_post( + router, + "", + model_cls=PlanCreateOut, + id="", + data=MaaPlanRead(), + route_kwargs={ + "tags": ["Add"], + "summary": "创建计划表", + "response_model": PlanCreateOut, + "status_code": 200, + }, ) -async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: +async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: try: uid, config = await Config.add_plan(plan.type) data = project_model(MaaPlanRead, await config.toDict()) except Exception as e: - return error_out(PlanCreateOut, e, planId="", data=MaaPlanRead()) - return PlanCreateOut(planId=str(uid), data=data) - - -@router.post( - "/get", - tags=["Get"], - summary="查询计划表", - response_model=PlanGetOut, - status_code=200, + return error_out(PlanCreateOut, e, id="", data=MaaPlanRead()) + return PlanCreateOut(id=str(uid), data=data) + + +@api_get( + router, + "", + model_cls=PlanGetOut, + index=[], + data={}, + route_kwargs={ + "tags": ["Get"], + "summary": "查询全部计划表", + "response_model": PlanGetOut, + "status_code": 200, + }, ) -async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut: - try: - index, data = await Config.get_plan(plan.planId) - index = project_model_list(PlanIndexItem, index) - data = project_model_map(MaaPlanRead, data) - except Exception as e: - return error_out(PlanGetOut, e, index=[], data={}) - return PlanGetOut(index=index, data=data) - - -@router.post( - "/update", - tags=["Update"], - summary="更新计划表配置信息", - response_model=OutBase, - status_code=200, +async def list_plans() -> PlanGetOut: + return await _build_plan_collection_out() + + +@api_get( + router, + "/{plan_id}", + model_cls=PlanDetailOut, + data=MaaPlanRead(), + route_kwargs={ + "tags": ["Get"], + "summary": "查询单个计划表", + "response_model": PlanDetailOut, + "status_code": 200, + }, +) +async def get_plan(plan_id: PlanIdPath) -> PlanDetailOut: + return await _build_plan_detail_out(plan_id) + + +@api_patch( + router, + "/{plan_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "更新计划表", + "response_model": OutBase, + "status_code": 200, + }, ) -async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase: +async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> OutBase: try: - await Config.update_plan(plan.planId, plan.data.model_dump(exclude_unset=True)) + await Config.update_plan(plan_id, body.data.model_dump(exclude_unset=True)) except Exception as e: return error_out(OutBase, e) return OutBase() -@router.post( - "/delete", - tags=["Delete"], - summary="删除计划表", - response_model=OutBase, - status_code=200, +@api_delete( + router, + "/{plan_id}", + model_cls=OutBase, + route_kwargs={ + "tags": ["Delete"], + "summary": "删除计划表", + "response_model": OutBase, + "status_code": 200, + }, ) -async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase: +async def delete_plan(plan_id: PlanIdPath) -> OutBase: try: - await Config.del_plan(plan.planId) + await Config.del_plan(plan_id) except Exception as e: return error_out(OutBase, e) return OutBase() -@router.post( +@api_patch( + router, "/order", - tags=["Update"], - summary="重新排序计划表", - response_model=OutBase, - status_code=200, + model_cls=OutBase, + route_kwargs={ + "tags": ["Update"], + "summary": "重新排序计划表", + "response_model": OutBase, + "status_code": 200, + }, ) -async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase: +async def reorder_plan(body: IndexOrderPatch = Body(...)) -> OutBase: try: - await Config.reorder_plan(plan.indexList) + await Config.reorder_plan(body.index_list) except Exception as e: return error_out(OutBase, e) return OutBase() diff --git a/app/api/queue.py b/app/api/queue.py index 01c57dff..d4dfdbe3 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -121,7 +121,9 @@ async def _build_time_set_create_out(queue_id: str) -> TimeSetCreateOut: async def _update_time_set_config( queue_id: str, time_set_id: str, data: TimeSetPatch ) -> OutBase: - await Config.update_time_set(queue_id, time_set_id, data.model_dump(exclude_unset=True)) + await Config.update_time_set( + queue_id, time_set_id, data.model_dump(exclude_unset=True) + ) return OutBase() @@ -215,7 +217,7 @@ async def create_queue() -> QueueCreateOut: }, ) async def reorder_queue(body: IndexOrderPatch = Body(...)) -> OutBase: - await Config.reorder_queue(body.indexList) + await Config.reorder_queue(body.index_list) return OutBase() @@ -314,7 +316,7 @@ async def create_time_set(queue_id: QueueIdPath) -> TimeSetCreateOut: async def reorder_time_sets( queue_id: QueueIdPath, body: IndexOrderPatch = Body(...) ) -> OutBase: - await Config.reorder_time_set(queue_id, body.indexList) + await Config.reorder_time_set(queue_id, body.index_list) return OutBase() @@ -366,9 +368,7 @@ async def update_time_set( "status_code": 200, }, ) -async def delete_time_set( - queue_id: QueueIdPath, time_set_id: TimeSetIdPath -) -> OutBase: +async def delete_time_set(queue_id: QueueIdPath, time_set_id: TimeSetIdPath) -> OutBase: return await _delete_time_set_config(queue_id, time_set_id) @@ -420,7 +420,7 @@ async def create_queue_item(queue_id: QueueIdPath) -> QueueItemCreateOut: async def reorder_queue_items( queue_id: QueueIdPath, body: IndexOrderPatch = Body(...) ) -> OutBase: - await Config.reorder_queue_item(queue_id, body.indexList) + await Config.reorder_queue_item(queue_id, body.index_list) return OutBase() diff --git a/app/api/scripts.py b/app/api/scripts.py index e9bb9bcd..7ff975aa 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -39,6 +39,7 @@ project_model_map, ) from app.models.scripts_contract import ( + ScriptPatchBody, InfrastructureImportBody, ScriptCreateIn, ScriptCreateOut, @@ -48,6 +49,7 @@ ScriptIndexItem, ScriptUploadBody, ScriptUrlBody, + UserPatchBody, UserCreateOut, UserDetailOut, UserGetOut, @@ -59,8 +61,8 @@ script_contract_type_from_create, script_contract_type_from_runtime, user_contract_type_from_script, - validate_script_patch_data, - validate_user_patch_data, + dump_script_patch_data, + dump_user_patch_data, ) from app.models.setting_contract import ( WebhookCreateOut, @@ -85,7 +87,9 @@ async def _build_script_collection_out() -> ScriptGetOut: index, data = await Config.get_script(None) script_index = project_model_list(ScriptIndexItem, index) - return ScriptGetOut(index=script_index, data=project_script_model_map(script_index, data)) + return ScriptGetOut( + index=script_index, data=project_script_model_map(script_index, data) + ) async def _build_script_detail_out(script_id: str) -> ScriptDetailOut: @@ -180,7 +184,7 @@ async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: }, ) async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: - await Config.reorder_script(body.indexList) + await Config.reorder_script(body.index_list) return OutBase() @@ -221,12 +225,15 @@ async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: }, ) async def update_script( - script_id: ScriptIdPath, data: dict[str, object] = Body(...) + script_id: ScriptIdPath, + body: ScriptPatchBody = Body(...), ) -> OutBase: script_type = script_contract_type_from_runtime( type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ ) - await Config.update_script(script_id, validate_script_patch_data(script_type, data)) + await Config.update_script( + script_id, dump_script_patch_data(script_type, body.data) + ) return OutBase() @@ -382,7 +389,7 @@ async def create_user(script_id: ScriptIdPath) -> UserCreateOut: async def reorder_users( script_id: ScriptIdPath, body: IndexOrderPatch = Body(...) ) -> OutBase: - await Config.reorder_user(script_id, body.indexList) + await Config.reorder_user(script_id, body.index_list) return OutBase() @@ -426,13 +433,15 @@ async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOu async def update_user( script_id: ScriptIdPath, user_id: UserIdPath, - data: dict[str, object] = Body(...), + body: UserPatchBody = Body(...), ) -> OutBase: script_type = script_contract_type_from_runtime( type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ ) user_type = user_contract_type_from_script(script_type) - await Config.update_user(script_id, user_id, validate_user_patch_data(user_type, data)) + await Config.update_user( + script_id, user_id, dump_user_patch_data(user_type, body.data) + ) return OutBase() @@ -548,7 +557,7 @@ async def reorder_user_webhooks( user_id: UserIdPath, body: IndexOrderPatch = Body(...), ) -> OutBase: - await Config.reorder_webhook(script_id, user_id, body.indexList) + await Config.reorder_webhook(script_id, user_id, body.index_list) return OutBase() diff --git a/app/api/setting.py b/app/api/setting.py index f69d7bf1..4322f78e 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -54,7 +54,9 @@ async def _build_setting_out() -> SettingGetOut: - return SettingGetOut(data=project_model(GlobalConfigRead, await Config.get_setting())) + return SettingGetOut( + data=project_model(GlobalConfigRead, await Config.get_setting()) + ) async def _update_setting_config(data: GlobalConfigPatch) -> OutBase: @@ -85,7 +87,9 @@ async def _build_webhook_create_out() -> WebhookCreateOut: async def _update_webhook_config(webhook_id: str, data: WebhookPatch) -> OutBase: - await Config.update_webhook(None, None, webhook_id, data.model_dump(exclude_unset=True)) + await Config.update_webhook( + None, None, webhook_id, data.model_dump(exclude_unset=True) + ) return OutBase() @@ -198,7 +202,7 @@ async def create_webhook() -> WebhookCreateOut: }, ) async def reorder_webhooks(body: IndexOrderPatch = Body(...)) -> OutBase: - await Config.reorder_webhook(None, None, body.indexList) + await Config.reorder_webhook(None, None, body.index_list) return OutBase() diff --git a/app/models/common_contract.py b/app/models/common_contract.py index 63d40cf2..1048905c 100644 --- a/app/models/common_contract.py +++ b/app/models/common_contract.py @@ -2,10 +2,28 @@ from collections.abc import Iterable, Mapping from functools import lru_cache +import re from types import UnionType -from typing import Annotated, Any, Generic, TypeAlias, TypeVar, Union, cast, get_args, get_origin - -from pydantic import BaseModel, ConfigDict, Field, create_model +from typing import ( + Annotated, + Any, + Generic, + TypeAlias, + TypeVar, + Union, + cast, + get_args, + get_origin, +) + +from pydantic import ( + AliasChoices, + BaseModel, + ConfigDict, + Field, + create_model, + model_validator, +) from app.core.config.fields import VirtualField from app.core.config.pydantic import PydanticConfigBase @@ -21,7 +39,42 @@ class ApiModel(BaseModel): """API Contract 的统一基线。""" - model_config = ConfigDict(extra="forbid", validate_assignment=True) + model_config = ConfigDict( + extra="forbid", + validate_assignment=True, + populate_by_name=True, + validate_by_alias=True, + ) + + @staticmethod + def _to_snake(name: str) -> str: + normalized = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + normalized = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", normalized) + return normalized.replace("-", "_").lower() + + @model_validator(mode="before") + @classmethod + def _normalize_input_keys(cls, data: Any) -> Any: + if not isinstance(data, Mapping): + return data + + normalized: dict[str, Any] = {} + field_names = cls.model_fields.keys() + raw_mapping = cast(Mapping[object, Any], data) + for raw_key, value in raw_mapping.items(): + key = str(raw_key) + if key in field_names: + normalized[key] = value + continue + + snake_key = cls._to_snake(key) + if snake_key in field_names and snake_key not in normalized: + normalized[snake_key] = value + continue + + normalized[key] = value + + return normalized class OutBase(ApiModel): @@ -57,7 +110,12 @@ class ResourceCreateOut(ResourceItemOut[DataT], Generic[DataT]): class IndexOrderPatch(ApiModel): - indexList: list[str] = Field(..., description="按新顺序排列的资源 ID 列表") + index_list: list[str] = Field( + ..., + validation_alias=AliasChoices("index_list", "indexList"), + serialization_alias="indexList", + description="按新顺序排列的资源 ID 列表", + ) def _annotate(annotation: Any, metadata: tuple[object, ...]) -> Any: @@ -136,7 +194,9 @@ def _model_field_definitions( elif field_info.is_required(): default = ... else: - default = Field(default=field_info.default, description=field_info.description) + default = Field( + default=field_info.default, description=field_info.description + ) field_definitions[field_name] = (annotated, default) @@ -303,7 +363,9 @@ def _project_value(annotation: Any, value: Any) -> Any: if not isinstance(value, (list, tuple, set, frozenset)): return value - iterable_value = cast(list[Any] | tuple[Any, ...] | set[Any] | frozenset[Any], value) + iterable_value = cast( + list[Any] | tuple[Any, ...] | set[Any] | frozenset[Any], value + ) items = [_project_value(item_annotation, item) for item in iterable_value] if origin is set: return set(items) @@ -318,7 +380,9 @@ def _project_value(annotation: Any, value: Any) -> Any: tuple_value = cast(list[Any] | tuple[Any, ...], value) item_annotations = get_args(annotation) if len(item_annotations) == 2 and item_annotations[1] is Ellipsis: - return tuple(_project_value(item_annotations[0], item) for item in tuple_value) + return tuple( + _project_value(item_annotations[0], item) for item in tuple_value + ) return tuple( _project_value(item_annotation, item) @@ -334,9 +398,7 @@ def _project_value(annotation: Any, value: Any) -> Any: mapping_value = cast(Mapping[Any, Any], value) return { - _project_value(key_annotation, key): _project_value( - value_annotation, item - ) + _project_value(key_annotation, key): _project_value(value_annotation, item) for key, item in mapping_value.items() } @@ -354,9 +416,34 @@ def _project_model_data( projected: dict[str, Any] = {} for name in field_names: field = model_cls.model_fields.get(name) - if field is None or name not in source: + if field is None: continue - projected[name] = _project_value(field.annotation, source[name]) + + source_value: Any | None = None + has_value = False + if name in source: + source_value = source[name] + has_value = True + else: + if field.alias is not None and field.alias in source: + source_value = source[field.alias] + has_value = True + elif field.validation_alias is not None: + alias = field.validation_alias + if isinstance(alias, str) and alias in source: + source_value = source[alias] + has_value = True + elif isinstance(alias, AliasChoices): + for candidate in alias.choices: + if isinstance(candidate, str) and candidate in source: + source_value = source[candidate] + has_value = True + break + + if not has_value: + continue + + projected[name] = _project_value(field.annotation, source_value) return projected diff --git a/app/models/general_contract.py b/app/models/general_contract.py index 5cb0f602..603bf31d 100644 --- a/app/models/general_contract.py +++ b/app/models/general_contract.py @@ -28,8 +28,8 @@ class GeneralConfig(_GeneralConfigReadBase): class GeneralConfigPatch(_GeneralConfigPatchBase): - type: Literal["GeneralConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["GeneralConfig"] = Field( + default="GeneralConfig", description="配置类型" ) @@ -40,8 +40,8 @@ class GeneralUserConfig(_GeneralUserConfigReadBase): class GeneralUserConfigPatch(_GeneralUserConfigPatchBase): - type: Literal["GeneralUserConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["GeneralUserConfig"] = Field( + default="GeneralUserConfig", description="配置类型" ) diff --git a/app/models/maa_contract.py b/app/models/maa_contract.py index 27227ec8..f764d75c 100644 --- a/app/models/maa_contract.py +++ b/app/models/maa_contract.py @@ -32,7 +32,7 @@ class MaaConfig(_MaaConfigReadBase): class MaaConfigPatch(_MaaConfigPatchBase): - type: Literal["MaaConfig"] | None = Field(default=None, description="配置类型") + type: Literal["MaaConfig"] = Field(default="MaaConfig", description="配置类型") class MaaUserConfig(_MaaUserConfigReadBase): @@ -42,8 +42,8 @@ class MaaUserConfig(_MaaUserConfigReadBase): class MaaUserConfigPatch(_MaaUserConfigPatchBase): - type: Literal["MaaUserConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["MaaUserConfig"] = Field( + default="MaaUserConfig", description="配置类型" ) @@ -54,8 +54,8 @@ class MaaPlanConfig(_MaaPlanConfigReadBase): class MaaPlanPatch(_MaaPlanPatchBase): - type: Literal["MaaPlanConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["MaaPlanConfig"] = Field( + default="MaaPlanConfig", description="配置类型" ) diff --git a/app/models/maaend_contract.py b/app/models/maaend_contract.py index b4de92ce..62add6d4 100644 --- a/app/models/maaend_contract.py +++ b/app/models/maaend_contract.py @@ -28,8 +28,8 @@ class MaaEndConfig(_MaaEndConfigReadBase): class MaaEndConfigPatch(_MaaEndConfigPatchBase): - type: Literal["MaaEndConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["MaaEndConfig"] = Field( + default="MaaEndConfig", description="配置类型" ) @@ -40,8 +40,8 @@ class MaaEndUserConfig(_MaaEndUserConfigReadBase): class MaaEndUserConfigPatch(_MaaEndUserConfigPatchBase): - type: Literal["MaaEndUserConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["MaaEndUserConfig"] = Field( + default="MaaEndUserConfig", description="配置类型" ) diff --git a/app/models/plan_contract.py b/app/models/plan_contract.py index f7a6320d..8d08f478 100644 --- a/app/models/plan_contract.py +++ b/app/models/plan_contract.py @@ -2,9 +2,14 @@ from typing import Literal -from pydantic import Field +from pydantic import AliasChoices, Field -from .common_contract import ApiModel, OutBase +from .common_contract import ( + ApiModel, + ResourceCollectionOut, + ResourceCreateOut, + ResourceItemOut, +) class PlanIndexItem(ApiModel): @@ -13,96 +18,250 @@ class PlanIndexItem(ApiModel): class MaaPlanInfoRead(ApiModel): - Name: str = Field(default="新 MAA 计划表", description="计划表名称") - Mode: Literal["ALL", "Weekly"] = Field(default="ALL", description="计划表模式") + name: str = Field( + default="新 MAA 计划表", + validation_alias=AliasChoices("name", "Name"), + serialization_alias="Name", + description="计划表名称", + ) + mode: Literal["ALL", "Weekly"] = Field( + default="ALL", + validation_alias=AliasChoices("mode", "Mode"), + serialization_alias="Mode", + description="计划表模式", + ) class MaaPlanInfoPatch(ApiModel): - Name: str | None = Field(default=None, description="计划表名称") - Mode: Literal["ALL", "Weekly"] | None = Field( - default=None, description="计划表模式" + name: str | None = Field( + default=None, + validation_alias=AliasChoices("name", "Name"), + serialization_alias="Name", + description="计划表名称", + ) + mode: Literal["ALL", "Weekly"] | None = Field( + default=None, + validation_alias=AliasChoices("mode", "Mode"), + serialization_alias="Mode", + description="计划表模式", ) class MaaPlanDayRead(ApiModel): - MedicineNumb: int = Field(default=0, description="吃理智药") - SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = Field( - default="0", description="连战次数" + medicine_numb: int = Field( + default=0, + validation_alias=AliasChoices("medicine_numb", "MedicineNumb"), + serialization_alias="MedicineNumb", + description="吃理智药", + ) + series_numb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = Field( + default="0", + validation_alias=AliasChoices("series_numb", "SeriesNumb"), + serialization_alias="SeriesNumb", + description="连战次数", + ) + stage: str = Field( + default="-", + validation_alias=AliasChoices("stage", "Stage"), + serialization_alias="Stage", + description="关卡选择", + ) + stage_1: str = Field( + default="-", + validation_alias=AliasChoices("stage_1", "Stage_1"), + serialization_alias="Stage_1", + description="备选关卡 - 1", + ) + stage_2: str = Field( + default="-", + validation_alias=AliasChoices("stage_2", "Stage_2"), + serialization_alias="Stage_2", + description="备选关卡 - 2", + ) + stage_3: str = Field( + default="-", + validation_alias=AliasChoices("stage_3", "Stage_3"), + serialization_alias="Stage_3", + description="备选关卡 - 3", + ) + stage_remain: str = Field( + default="-", + validation_alias=AliasChoices("stage_remain", "Stage_Remain"), + serialization_alias="Stage_Remain", + description="剩余理智关卡", ) - Stage: str = Field(default="-", description="关卡选择") - Stage_1: str = Field(default="-", description="备选关卡 - 1") - Stage_2: str = Field(default="-", description="备选关卡 - 2") - Stage_3: str = Field(default="-", description="备选关卡 - 3") - Stage_Remain: str = Field(default="-", description="剩余理智关卡") class MaaPlanDayPatch(ApiModel): - MedicineNumb: int | None = Field(default=None, description="吃理智药") - SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] | None = Field( - default=None, description="连战次数" + medicine_numb: int | None = Field( + default=None, + validation_alias=AliasChoices("medicine_numb", "MedicineNumb"), + serialization_alias="MedicineNumb", + description="吃理智药", + ) + series_numb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] | None = Field( + default=None, + validation_alias=AliasChoices("series_numb", "SeriesNumb"), + serialization_alias="SeriesNumb", + description="连战次数", + ) + stage: str | None = Field( + default=None, + validation_alias=AliasChoices("stage", "Stage"), + serialization_alias="Stage", + description="关卡选择", + ) + stage_1: str | None = Field( + default=None, + validation_alias=AliasChoices("stage_1", "Stage_1"), + serialization_alias="Stage_1", + description="备选关卡 - 1", + ) + stage_2: str | None = Field( + default=None, + validation_alias=AliasChoices("stage_2", "Stage_2"), + serialization_alias="Stage_2", + description="备选关卡 - 2", + ) + stage_3: str | None = Field( + default=None, + validation_alias=AliasChoices("stage_3", "Stage_3"), + serialization_alias="Stage_3", + description="备选关卡 - 3", + ) + stage_remain: str | None = Field( + default=None, + validation_alias=AliasChoices("stage_remain", "Stage_Remain"), + serialization_alias="Stage_Remain", + description="剩余理智关卡", ) - Stage: str | None = Field(default=None, description="关卡选择") - Stage_1: str | None = Field(default=None, description="备选关卡 - 1") - Stage_2: str | None = Field(default=None, description="备选关卡 - 2") - Stage_3: str | None = Field(default=None, description="备选关卡 - 3") - Stage_Remain: str | None = Field(default=None, description="剩余理智关卡") class MaaPlanRead(ApiModel): - Info: MaaPlanInfoRead = Field(default_factory=MaaPlanInfoRead, description="基础信息") - ALL: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="全局") - Monday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周一") - Tuesday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周二") - Wednesday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周三") - Thursday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周四") - Friday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周五") - Saturday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周六") - Sunday: MaaPlanDayRead = Field(default_factory=MaaPlanDayRead, description="周日") + info: MaaPlanInfoRead = Field( + default_factory=MaaPlanInfoRead, + validation_alias=AliasChoices("info", "Info"), + serialization_alias="Info", + description="基础信息", + ) + all_days: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("all_days", "ALL"), + serialization_alias="ALL", + description="全局", + ) + monday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("monday", "Monday"), + serialization_alias="Monday", + description="周一", + ) + tuesday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("tuesday", "Tuesday"), + serialization_alias="Tuesday", + description="周二", + ) + wednesday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("wednesday", "Wednesday"), + serialization_alias="Wednesday", + description="周三", + ) + thursday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("thursday", "Thursday"), + serialization_alias="Thursday", + description="周四", + ) + friday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("friday", "Friday"), + serialization_alias="Friday", + description="周五", + ) + saturday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("saturday", "Saturday"), + serialization_alias="Saturday", + description="周六", + ) + sunday: MaaPlanDayRead = Field( + default_factory=MaaPlanDayRead, + validation_alias=AliasChoices("sunday", "Sunday"), + serialization_alias="Sunday", + description="周日", + ) class MaaPlanPatch(ApiModel): - Info: MaaPlanInfoPatch | None = Field(default=None, description="基础信息") - ALL: MaaPlanDayPatch | None = Field(default=None, description="全局") - Monday: MaaPlanDayPatch | None = Field(default=None, description="周一") - Tuesday: MaaPlanDayPatch | None = Field(default=None, description="周二") - Wednesday: MaaPlanDayPatch | None = Field(default=None, description="周三") - Thursday: MaaPlanDayPatch | None = Field(default=None, description="周四") - Friday: MaaPlanDayPatch | None = Field(default=None, description="周五") - Saturday: MaaPlanDayPatch | None = Field(default=None, description="周六") - Sunday: MaaPlanDayPatch | None = Field(default=None, description="周日") + info: MaaPlanInfoPatch | None = Field( + default=None, + validation_alias=AliasChoices("info", "Info"), + serialization_alias="Info", + description="基础信息", + ) + all_days: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("all_days", "ALL"), + serialization_alias="ALL", + description="全局", + ) + monday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("monday", "Monday"), + serialization_alias="Monday", + description="周一", + ) + tuesday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("tuesday", "Tuesday"), + serialization_alias="Tuesday", + description="周二", + ) + wednesday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("wednesday", "Wednesday"), + serialization_alias="Wednesday", + description="周三", + ) + thursday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("thursday", "Thursday"), + serialization_alias="Thursday", + description="周四", + ) + friday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("friday", "Friday"), + serialization_alias="Friday", + description="周五", + ) + saturday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("saturday", "Saturday"), + serialization_alias="Saturday", + description="周六", + ) + sunday: MaaPlanDayPatch | None = Field( + default=None, + validation_alias=AliasChoices("sunday", "Sunday"), + serialization_alias="Sunday", + description="周日", + ) class PlanCreateIn(ApiModel): type: Literal["MaaPlan"] = Field(..., description="计划类型") -class PlanCreateOut(OutBase): - planId: str = Field(..., description="新创建的计划ID") - data: MaaPlanRead = Field(..., description="计划配置数据") - - -class PlanGetIn(ApiModel): - planId: str | None = Field( - default=None, description="计划ID, 未携带时表示获取所有计划数据" - ) - - -class PlanGetOut(OutBase): - index: list[PlanIndexItem] = Field(..., description="计划索引列表") - data: dict[str, MaaPlanRead] = Field(..., description="计划列表或单个计划数据") - - -class PlanUpdateIn(ApiModel): - planId: str = Field(..., description="计划ID") +class PlanUpdateBody(ApiModel): data: MaaPlanPatch = Field(..., description="计划更新数据") -class PlanDeleteIn(ApiModel): - planId: str = Field(..., description="计划ID") - - -class PlanReorderIn(ApiModel): - indexList: list[str] = Field(..., description="计划ID列表, 按新顺序排列") +PlanCreateOut = ResourceCreateOut[MaaPlanRead] +PlanDetailOut = ResourceItemOut[MaaPlanRead] +PlanGetOut = ResourceCollectionOut[PlanIndexItem, MaaPlanRead] __all__ = [ @@ -115,9 +274,7 @@ class PlanReorderIn(ApiModel): "MaaPlanPatch", "PlanCreateIn", "PlanCreateOut", - "PlanGetIn", + "PlanDetailOut", "PlanGetOut", - "PlanUpdateIn", - "PlanDeleteIn", - "PlanReorderIn", + "PlanUpdateBody", ] diff --git a/app/models/scripts_contract.py b/app/models/scripts_contract.py index c59287a1..9dc978c9 100644 --- a/app/models/scripts_contract.py +++ b/app/models/scripts_contract.py @@ -1,9 +1,9 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Annotated, Any, Literal, TypeAlias +from typing import Annotated, Any, Literal -from pydantic import Field, TypeAdapter +from pydantic import Field from .common_contract import ( ApiModel, @@ -33,7 +33,6 @@ "MaaUserConfig", "GeneralUserConfig", "SrcUserConfig", "MaaEndUserConfig" ] ScriptCreateType = Literal["MAA", "SRC", "General", "MaaEnd"] -PatchPayload: TypeAlias = dict[str, object] ScriptModel = MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig UserModel = MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig @@ -104,7 +103,6 @@ "SrcConfig": "SrcUserConfig", "MaaEndConfig": "MaaEndUserConfig", } -PATCH_PAYLOAD_ADAPTER: TypeAdapter[PatchPayload] = TypeAdapter(dict[str, object]) class ScriptIndexItem(ApiModel): @@ -149,6 +147,26 @@ class ScriptUploadBody(ApiModel): UserDetailOut = ResourceItemOut[UserReadData] UserCreateOut = ResourceCreateOut[UserReadData] +ScriptPatchData = Annotated[ + MaaConfigPatch | SrcConfigPatch | GeneralConfigPatch | MaaEndConfigPatch, + Field(discriminator="type"), +] +UserPatchData = Annotated[ + MaaUserConfigPatch + | SrcUserConfigPatch + | GeneralUserConfigPatch + | MaaEndUserConfigPatch, + Field(discriminator="type"), +] + + +class ScriptPatchBody(ApiModel): + data: ScriptPatchData = Field(..., description="脚本 Patch 数据") + + +class UserPatchBody(ApiModel): + data: UserPatchData = Field(..., description="用户 Patch 数据") + class InfrastructureImportBody(ApiModel): path: str = Field(..., description="JSON 文件路径, 用于导入自定义基建文件") @@ -208,31 +226,32 @@ def project_user_model_map( } -def validate_script_patch_data( - script_type: ScriptConfigType, raw: Mapping[str, object] +def dump_script_patch_data( + script_type: ScriptConfigType, data: ScriptPatchData ) -> dict[str, Any]: - normalized = PATCH_PAYLOAD_ADAPTER.validate_python(raw) - validated = SCRIPT_PATCH_BY_TYPE[script_type].model_validate(normalized) - return validated.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) + if data.type != script_type: + raise ValueError(f"Patch 类型不匹配: 期望 {script_type}, 实际 {data.type}") + return data.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) -def validate_user_patch_data( - user_type: UserConfigType, raw: Mapping[str, object] +def dump_user_patch_data( + user_type: UserConfigType, data: UserPatchData ) -> dict[str, Any]: - normalized = PATCH_PAYLOAD_ADAPTER.validate_python(raw) - validated = USER_PATCH_BY_TYPE[user_type].model_validate(normalized) - return validated.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) + if data.type != user_type: + raise ValueError(f"Patch 类型不匹配: 期望 {user_type}, 实际 {data.type}") + return data.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) __all__ = [ "ScriptConfigType", "UserConfigType", "ScriptCreateType", - "PatchPayload", "ScriptModel", "UserModel", "ScriptReadData", "UserReadData", + "ScriptPatchData", + "UserPatchData", "SCRIPT_CONTRACT_BY_TYPE", "SCRIPT_PATCH_BY_TYPE", "USER_CONTRACT_BY_TYPE", @@ -248,9 +267,11 @@ def validate_user_patch_data( "ScriptFileBody", "ScriptUrlBody", "ScriptUploadBody", + "ScriptPatchBody", "UserGetOut", "UserDetailOut", "UserCreateOut", + "UserPatchBody", "InfrastructureImportBody", "script_contract_type_from_create", "script_contract_type_from_runtime", @@ -259,6 +280,6 @@ def validate_user_patch_data( "project_user_model", "project_script_model_map", "project_user_model_map", - "validate_script_patch_data", - "validate_user_patch_data", + "dump_script_patch_data", + "dump_user_patch_data", ] diff --git a/app/models/src_contract.py b/app/models/src_contract.py index 919757c4..154f8b98 100644 --- a/app/models/src_contract.py +++ b/app/models/src_contract.py @@ -26,7 +26,7 @@ class SrcConfig(_SrcConfigReadBase): class SrcConfigPatch(_SrcConfigPatchBase): - type: Literal["SrcConfig"] | None = Field(default=None, description="配置类型") + type: Literal["SrcConfig"] = Field(default="SrcConfig", description="配置类型") class SrcUserConfig(_SrcUserConfigReadBase): @@ -36,8 +36,8 @@ class SrcUserConfig(_SrcUserConfigReadBase): class SrcUserConfigPatch(_SrcUserConfigPatchBase): - type: Literal["SrcUserConfig"] | None = Field( - default=None, description="配置类型" + type: Literal["SrcUserConfig"] = Field( + default="SrcUserConfig", description="配置类型" ) From 01ec1d82b26d189bb3bfc8aac6e8c1cc0e51209c Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 5 Apr 2026 11:54:46 +0800 Subject: [PATCH 13/29] =?UTF-8?q?refactor(api):=20=E9=87=8D=E6=9E=84API?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=EF=BC=8C=E4=BC=98=E5=8C=96=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=B3=A8=E5=86=8C=E5=92=8C=E6=A8=A1=E5=9E=8B=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/common.py | 291 ++++++++++++++++++++++++++++++++++------ app/api/core.py | 6 +- app/api/dispatch.py | 18 +-- app/api/emulator.py | 133 ++++++------------ app/api/history.py | 9 +- app/api/info.py | 37 ++--- app/api/ocr.py | 24 ++-- app/api/plan.py | 81 ++++------- app/api/queue.py | 237 ++++++++++---------------------- app/api/scripts.py | 255 +++++++++++------------------------ app/api/setting.py | 133 ++++++------------ app/api/tools.py | 29 ++-- app/api/update.py | 55 +++----- app/api/ws_debug.py | 27 ++-- app/core/config/base.py | 15 +-- 15 files changed, 597 insertions(+), 753 deletions(-) diff --git a/app/api/common.py b/app/api/common.py index a7ad966b..ff0aa6fb 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable, Iterable from functools import wraps +import inspect from typing import Any, ParamSpec, TypeVar, cast from fastapi import APIRouter @@ -13,6 +14,26 @@ P = ParamSpec("P") +def _docstring_summary(func: Callable[..., Any]) -> str | None: + doc = inspect.getdoc(func) + if not doc: + return None + first_line = doc.splitlines()[0].strip() + return first_line or None + + +def _resolve_model_cls( + *, + model_cls: type[OutBase] | None, + response_model: type[Any] | None, +) -> type[OutBase]: + if model_cls is not None: + return model_cls + if isinstance(response_model, type) and issubclass(response_model, OutBase): + return response_model + raise TypeError("model_cls 为空时,response_model 必须是 OutBase 的子类") + + def error_out( model_cls: type[OutT], exc: Exception, @@ -78,86 +99,268 @@ def api_post( router: APIRouter, path: str, *, - model_cls: type[OutT], + model_cls: type[OutBase] | None = None, ws_endpoint: str | None = None, message: str | None = None, on_error: Callable[[Exception], None] | None = None, + response_model: type[Any] | None = None, route_kwargs: dict[str, object] | None = None, **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: - """统一 POST 路由注册装饰器。""" +) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + """统一 POST 路由注册装饰器(兼容入口)。""" - return api_route( - router, + return cast(Any, bind_api(router).post)( path, - methods=("POST",), model_cls=model_cls, ws_endpoint=ws_endpoint, message=message, on_error=on_error, + response_model=response_model, route_kwargs=route_kwargs, **fallback_kwargs, ) +class ApiRegistrar: + """接近 FastAPI 原生声明风格的 API 装饰器注册器。""" + + def __init__(self, router: APIRouter): + self.router = router + + def route( + self, + path: str, + *, + methods: Iterable[str], + model_cls: type[OutBase] | None = None, + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + tags: list[str] | None = None, + summary: str | None = None, + response_model: type[Any] | None = None, + status_code: int = 200, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, + ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + """统一路由注册:路由 + 守卫 + 可选 WS 命令。""" + + resolved_model_cls = _resolve_model_cls( + model_cls=model_cls, + response_model=response_model, + ) + + guard = api_guard( + model_cls=resolved_model_cls, + message=message, + on_error=on_error, + **fallback_kwargs, + ) + + def decorator(func: Callable[P, Awaitable[Any]]) -> Callable[P, Awaitable[Any]]: + resolved_summary = summary or _docstring_summary(func) + final_route_kwargs: dict[str, Any] = dict(route_kwargs or {}) + + final_route_kwargs.setdefault("status_code", status_code) + final_route_kwargs.setdefault( + "response_model", response_model or resolved_model_cls + ) + if tags is not None: + final_route_kwargs["tags"] = tags + if resolved_summary is not None: + final_route_kwargs.setdefault("summary", resolved_summary) + + route = self.router.api_route( + path, + methods=list(methods), + **final_route_kwargs, + ) + + wrapped = cast(Any, guard)(func) + if ws_endpoint is not None: + from app.api.ws_command import ws_command + + wrapped = ws_command(ws_endpoint)(wrapped) + return route(wrapped) + + return decorator + + def get( + self, + path: str, + *, + model_cls: type[OutBase] | None = None, + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + tags: list[str] | None = None, + summary: str | None = None, + response_model: type[Any] | None = None, + status_code: int = 200, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, + ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + return self.route( + path, + methods=("GET",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + tags=tags, + summary=summary, + response_model=response_model, + status_code=status_code, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + def post( + self, + path: str, + *, + model_cls: type[OutBase] | None = None, + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + tags: list[str] | None = None, + summary: str | None = None, + response_model: type[Any] | None = None, + status_code: int = 200, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, + ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + return self.route( + path, + methods=("POST",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + tags=tags, + summary=summary, + response_model=response_model, + status_code=status_code, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + def patch( + self, + path: str, + *, + model_cls: type[OutBase] | None = None, + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + tags: list[str] | None = None, + summary: str | None = None, + response_model: type[Any] | None = None, + status_code: int = 200, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, + ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + return self.route( + path, + methods=("PATCH",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + tags=tags, + summary=summary, + response_model=response_model, + status_code=status_code, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + def delete( + self, + path: str, + *, + model_cls: type[OutBase] | None = None, + ws_endpoint: str | None = None, + message: str | None = None, + on_error: Callable[[Exception], None] | None = None, + tags: list[str] | None = None, + summary: str | None = None, + response_model: type[Any] | None = None, + status_code: int = 200, + route_kwargs: dict[str, object] | None = None, + **fallback_kwargs: object, + ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + return self.route( + path, + methods=("DELETE",), + model_cls=model_cls, + ws_endpoint=ws_endpoint, + message=message, + on_error=on_error, + tags=tags, + summary=summary, + response_model=response_model, + status_code=status_code, + route_kwargs=route_kwargs, + **fallback_kwargs, + ) + + +def bind_api(router: APIRouter) -> ApiRegistrar: + """绑定一个 APIRouter 并返回声明式 API 注册器。""" + + return ApiRegistrar(router) + + def api_route( router: APIRouter, path: str, *, methods: Iterable[str], - model_cls: type[OutT], + model_cls: type[OutBase] | None = None, ws_endpoint: str | None = None, message: str | None = None, on_error: Callable[[Exception], None] | None = None, + response_model: type[Any] | None = None, route_kwargs: dict[str, object] | None = None, **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: - """统一路由注册装饰器:路由 + 守卫 + 可选 WS 命令。""" +) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + """统一路由注册装饰器(兼容入口)。""" - guard = api_guard( + return cast(Any, bind_api(router).route)( + path, + methods=methods, model_cls=model_cls, + ws_endpoint=ws_endpoint, message=message, on_error=on_error, + response_model=response_model, + route_kwargs=route_kwargs, **fallback_kwargs, ) - route = router.api_route( - path, - methods=list(methods), - **cast(dict[str, Any], route_kwargs or {}), - ) - - def decorator(func: Callable[P, Awaitable[OutT]]) -> Callable[P, Awaitable[OutT]]: - wrapped = guard(func) - if ws_endpoint is not None: - from app.api.ws_command import ws_command - - wrapped = ws_command(ws_endpoint)(wrapped) - return route(wrapped) - - return decorator def api_get( router: APIRouter, path: str, *, - model_cls: type[OutT], + model_cls: type[OutBase] | None = None, ws_endpoint: str | None = None, message: str | None = None, on_error: Callable[[Exception], None] | None = None, + response_model: type[Any] | None = None, route_kwargs: dict[str, object] | None = None, **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: - """统一 GET 路由注册装饰器。""" +) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + """统一 GET 路由注册装饰器(兼容入口)。""" - return api_route( - router, + return cast(Any, bind_api(router).get)( path, - methods=("GET",), model_cls=model_cls, ws_endpoint=ws_endpoint, message=message, on_error=on_error, + response_model=response_model, route_kwargs=route_kwargs, **fallback_kwargs, ) @@ -167,23 +370,23 @@ def api_patch( router: APIRouter, path: str, *, - model_cls: type[OutT], + model_cls: type[OutBase] | None = None, ws_endpoint: str | None = None, message: str | None = None, on_error: Callable[[Exception], None] | None = None, + response_model: type[Any] | None = None, route_kwargs: dict[str, object] | None = None, **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: - """统一 PATCH 路由注册装饰器。""" +) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + """统一 PATCH 路由注册装饰器(兼容入口)。""" - return api_route( - router, + return cast(Any, bind_api(router).patch)( path, - methods=("PATCH",), model_cls=model_cls, ws_endpoint=ws_endpoint, message=message, on_error=on_error, + response_model=response_model, route_kwargs=route_kwargs, **fallback_kwargs, ) @@ -193,23 +396,23 @@ def api_delete( router: APIRouter, path: str, *, - model_cls: type[OutT], + model_cls: type[OutBase] | None = None, ws_endpoint: str | None = None, message: str | None = None, on_error: Callable[[Exception], None] | None = None, + response_model: type[Any] | None = None, route_kwargs: dict[str, object] | None = None, **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: - """统一 DELETE 路由注册装饰器。""" +) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: + """统一 DELETE 路由注册装饰器(兼容入口)。""" - return api_route( - router, + return cast(Any, bind_api(router).delete)( path, - methods=("DELETE",), model_cls=model_cls, ws_endpoint=ws_endpoint, message=message, on_error=on_error, + response_model=response_model, route_kwargs=route_kwargs, **fallback_kwargs, ) @@ -221,6 +424,8 @@ def api_delete( "ComboBoxOut", "error_out", "run_api", + "bind_api", + "ApiRegistrar", "api_guard", "api_route", "api_get", diff --git a/app/api/core.py b/app/api/core.py index 4823ce48..632a64a1 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -32,10 +32,11 @@ from app.models.common_contract import OutBase from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command -from app.api.common import error_out +from app.api.common import bind_api, error_out from app.utils import get_logger router = APIRouter(prefix="/api/core", tags=["核心信息"]) +api = bind_api(router) logger = get_logger("DEV") @@ -99,11 +100,10 @@ async def connect_websocket(websocket: WebSocket): @ws_command("core.close") -@router.post( +@api.post( "/close", summary="关闭后端程序", response_model=OutBase, - status_code=200, ) async def close() -> OutBase: """关闭后端程序""" diff --git a/app/api/dispatch.py b/app/api/dispatch.py index 7992f929..504a5498 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -33,17 +33,17 @@ TaskCreateIn, TaskCreateOut, ) -from app.api.common import error_out +from app.api.common import bind_api, error_out router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) +api = bind_api(router) -@router.post( +@api.post( "/start", tags=["Action"], summary="添加任务", response_model=TaskCreateOut, - status_code=200, ) async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: try: @@ -53,12 +53,11 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: return TaskCreateOut(taskId=str(task_id)) -@router.post( +@api.post( "/stop", tags=["Action"], summary="中止任务", response_model=OutBase, - status_code=200, ) async def stop_task(task: DispatchIn = Body(...)) -> OutBase: try: @@ -68,12 +67,11 @@ async def stop_task(task: DispatchIn = Body(...)) -> OutBase: return OutBase() -@router.post( +@api.post( "/get/power", tags=["Get"], summary="获取电源标志", response_model=PowerOut, - status_code=200, ) async def get_power() -> PowerOut: try: @@ -83,12 +81,11 @@ async def get_power() -> PowerOut: return PowerOut(signal=signal) -@router.post( +@api.post( "/set/power", tags=["Action"], summary="设置电源标志", response_model=OutBase, - status_code=200, ) async def set_power(task: PowerIn = Body(...)) -> OutBase: try: @@ -98,12 +95,11 @@ async def set_power(task: PowerIn = Body(...)) -> OutBase: return OutBase() -@router.post( +@api.post( "/cancel/power", tags=["Action"], summary="取消电源任务", response_model=OutBase, - status_code=200, ) async def cancel_power_task() -> OutBase: try: diff --git a/app/api/emulator.py b/app/api/emulator.py index fc08535f..4a9acbde 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Body, Path -from app.api.common import api_delete, api_get, api_patch, api_post +from app.api.common import bind_api from app.core import Config, EmulatorManager from app.models.common_contract import ( IndexOrderPatch, @@ -49,6 +49,7 @@ ) router = APIRouter(prefix="/api/emulator", tags=["模拟器管理"]) +api = bind_api(router) EmulatorIdPath = Annotated[str, Path(description="模拟器 ID")] EmulatorActionPath = Annotated[ @@ -107,114 +108,79 @@ async def _build_emulator_search_out() -> EmulatorSearchOut: return EmulatorSearchOut(data=project_model_list(EmulatorSearchResult, emulators)) -@api_get( - router, +@api.get( "", - model_cls=EmulatorGetOut, + tags=["Get"], + summary="查询全部模拟器配置", + response_model=EmulatorGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询全部模拟器配置", - "response_model": EmulatorGetOut, - "status_code": 200, - }, ) async def list_emulators() -> EmulatorGetOut: return await _build_emulator_collection_out() -@api_post( - router, +@api.post( "", - model_cls=EmulatorCreateOut, + tags=["Add"], + summary="创建模拟器配置", + response_model=EmulatorCreateOut, id="", data=EmulatorRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建模拟器配置", - "response_model": EmulatorCreateOut, - "status_code": 200, - }, ) async def create_emulator() -> EmulatorCreateOut: return await _build_emulator_create_out() -@api_patch( - router, +@api.patch( "/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序模拟器", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序模拟器", + response_model=OutBase, ) async def reorder_emulator(body: IndexOrderPatch = Body(...)) -> OutBase: await Config.reorder_emulator(body.index_list) return OutBase() -@api_get( - router, +@api.get( "/detected", - model_cls=EmulatorSearchOut, + tags=["Get"], + summary="搜索已安装的模拟器", + response_model=EmulatorSearchOut, data=[], - route_kwargs={ - "tags": ["Get"], - "summary": "搜索已安装的模拟器", - "response_model": EmulatorSearchOut, - "status_code": 200, - }, ) async def detect_emulators() -> EmulatorSearchOut: return await _build_emulator_search_out() -@api_get( - router, +@api.get( "/status", - model_cls=EmulatorStatusOut, + tags=["Get"], + summary="查询全部模拟器状态", + response_model=EmulatorStatusOut, data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询全部模拟器状态", - "response_model": EmulatorStatusOut, - "status_code": 200, - }, ) async def get_emulator_statuses() -> EmulatorStatusOut: return await _build_emulator_status_out() -@api_get( - router, +@api.get( "/{emulator_id}", - model_cls=EmulatorDetailOut, + tags=["Get"], + summary="查询单个模拟器配置", + response_model=EmulatorDetailOut, data=EmulatorRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个模拟器配置", - "response_model": EmulatorDetailOut, - "status_code": 200, - }, ) async def get_emulator(emulator_id: EmulatorIdPath) -> EmulatorDetailOut: return await _build_emulator_detail_out(emulator_id) -@api_patch( - router, +@api.patch( "/{emulator_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新模拟器配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新模拟器配置", + response_model=OutBase, ) async def update_emulator( emulator_id: EmulatorIdPath, data: EmulatorPatch = Body(...) @@ -222,47 +188,32 @@ async def update_emulator( return await _update_emulator_config(emulator_id, data) -@api_delete( - router, +@api.delete( "/{emulator_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除模拟器配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除模拟器配置", + response_model=OutBase, ) async def delete_emulator(emulator_id: EmulatorIdPath) -> OutBase: return await _delete_emulator_config(emulator_id) -@api_get( - router, +@api.get( "/{emulator_id}/status", - model_cls=EmulatorDeviceStatusOut, + tags=["Get"], + summary="查询单个模拟器状态", + response_model=EmulatorDeviceStatusOut, data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个模拟器状态", - "response_model": EmulatorDeviceStatusOut, - "status_code": 200, - }, ) async def get_emulator_status(emulator_id: EmulatorIdPath) -> EmulatorDeviceStatusOut: return await _build_emulator_device_status_out(emulator_id) -@api_post( - router, +@api.post( "/{emulator_id}/actions/{action}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "执行模拟器动作", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="执行模拟器动作", + response_model=OutBase, ) async def operate_emulator( emulator_id: EmulatorIdPath, diff --git a/app/api/history.py b/app/api/history.py index 8e98b91c..76bb01cd 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -27,7 +27,7 @@ from pydantic import TypeAdapter from app.core import Config -from app.api.common import error_out +from app.api.common import bind_api, error_out from app.models.history_contract import ( HistoryData, HistoryDataGetIn, @@ -38,6 +38,7 @@ ) router = APIRouter(prefix="/api/history", tags=["历史记录"]) +api = bind_api(router) HISTORY_INDEX_ADAPTER: TypeAdapter[list[HistoryIndexItem]] = TypeAdapter( list[HistoryIndexItem] @@ -53,12 +54,11 @@ def _build_history_data(raw: dict[str, object]) -> HistoryData: return HistoryData.model_validate(data) -@router.post( +@api.post( "/search", tags=["Get"], summary="搜索历史记录总览信息", response_model=HistorySearchOut, - status_code=200, ) async def search_history(history: HistorySearchIn) -> HistorySearchOut: try: @@ -79,12 +79,11 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: return HistorySearchOut(data=data) -@router.post( +@api.post( "/data", tags=["Get"], summary="从指定文件内获取历史记录数据", response_model=HistoryDataGetOut, - status_code=200, ) async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut: try: diff --git a/app/api/info.py b/app/api/info.py index aacfa2ad..a0a81c2d 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -34,7 +34,7 @@ InfoOut, OutBase, ) -from app.api.common import error_out +from app.api.common import bind_api, error_out from app.models.info_contract import ( GetStageIn, NoticeOut, @@ -42,11 +42,13 @@ ) router = APIRouter(prefix="/api/info", tags=["信息获取"]) +api = bind_api(router) class EmulatorIdBody(ApiModel): emulatorId: str = Field(..., description="模拟器 ID") + COMBOBOX_ITEMS_ADAPTER: TypeAdapter[list[ComboBoxItem]] = TypeAdapter( list[ComboBoxItem] ) @@ -58,12 +60,11 @@ def _to_combobox_items(raw_data: object) -> list[ComboBoxItem]: ) -@router.post( +@api.post( "/version", tags=["Get"], summary="获取后端git版本信息", response_model=VersionOut, - status_code=200, ) async def get_git_version() -> VersionOut: try: @@ -83,12 +84,11 @@ async def get_git_version() -> VersionOut: ) -@router.post( +@api.post( "/combox/stage", tags=["Get"], summary="获取关卡号下拉框信息", response_model=ComboBoxOut, - status_code=200, ) async def get_stage_combox( stage: GetStageIn = Body(..., description="关卡号类型"), @@ -101,12 +101,11 @@ async def get_stage_combox( return ComboBoxOut(data=data) -@router.post( +@api.post( "/combox/script", tags=["Get"], summary="获取脚本下拉框信息", response_model=ComboBoxOut, - status_code=200, ) async def get_script_combox() -> ComboBoxOut: try: @@ -117,12 +116,11 @@ async def get_script_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@router.post( +@api.post( "/combox/task", tags=["Get"], summary="获取可选任务下拉框信息", response_model=ComboBoxOut, - status_code=200, ) async def get_task_combox() -> ComboBoxOut: try: @@ -133,12 +131,11 @@ async def get_task_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@router.post( +@api.post( "/combox/plan", tags=["Get"], summary="获取可选计划下拉框信息", response_model=ComboBoxOut, - status_code=200, ) async def get_plan_combox() -> ComboBoxOut: try: @@ -149,12 +146,11 @@ async def get_plan_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@router.post( +@api.post( "/combox/emulator", tags=["Get"], summary="获取可选模拟器下拉框信息", response_model=ComboBoxOut, - status_code=200, ) async def get_emulator_combox() -> ComboBoxOut: try: @@ -165,12 +161,11 @@ async def get_emulator_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@router.post( +@api.post( "/combox/emulator/devices", tags=["Get"], summary="获取可选模拟器多开实例下拉框信息", response_model=ComboBoxOut, - status_code=200, ) async def get_emulator_devices_combox( emulator: EmulatorIdBody = Body(...), @@ -185,12 +180,11 @@ async def get_emulator_devices_combox( return ComboBoxOut(data=data) -@router.post( +@api.post( "/notice/get", tags=["Get"], summary="获取通知信息", response_model=NoticeOut, - status_code=200, ) async def get_notice_info() -> NoticeOut: try: @@ -200,12 +194,11 @@ async def get_notice_info() -> NoticeOut: return NoticeOut(if_need_show=if_need_show, data=data) -@router.post( +@api.post( "/notice/confirm", tags=["Action"], summary="确认通知", response_model=OutBase, - status_code=200, ) async def confirm_notice() -> OutBase: try: @@ -229,12 +222,11 @@ async def confirm_notice() -> OutBase: # return InfoOut(data=data) -@router.post( +@api.post( "/webconfig", tags=["Get"], summary="获取配置分享中心的配置信息", response_model=InfoOut, - status_code=200, ) async def get_web_config() -> InfoOut: try: @@ -244,12 +236,11 @@ async def get_web_config() -> InfoOut: return InfoOut(data={"WebConfig": data}) -@router.post( +@api.post( "/get/overview", tags=["Get"], summary="信息总览", response_model=InfoOut, - status_code=200, ) async def get_overview() -> InfoOut: try: diff --git a/app/api/ocr.py b/app/api/ocr.py index a16798f4..79dab59f 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -31,11 +31,12 @@ from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger from app.models.common_contract import ApiModel, OutBase -from app.api.common import error_out, run_api +from app.api.common import bind_api, error_out, run_api logger = get_logger("OCR API") router = APIRouter(prefix="/api/ocr", tags=["OCR识别"]) +api = bind_api(router) # ========== 截图相关模型 ========== @@ -131,12 +132,11 @@ def _encode_image_base64(image: Image.Image) -> str: # ========== 截图接口 ========== -@router.post( +@api.post( "/screenshot", tags=["Get"], summary="获取窗口截图", response_model=OCRScreenshotOut, - status_code=200, ) async def get_screenshot(params: OCRScreenshotIn = Body(...)) -> OCRScreenshotOut: """ @@ -194,12 +194,11 @@ async def _success() -> OCRScreenshotOut: ) -@router.post( +@api.post( "/screenshot/adb", tags=["Get"], summary="通过ADB获取设备截图", response_model=ADBScreenshotOut, - status_code=200, ) async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreenshotOut: """ @@ -281,12 +280,11 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh # ========== 测试接口:检查图像 ========== -@router.post( +@api.post( "/check/image", tags=["Get"], summary="检查是否存在指定图像", response_model=CheckImageOut, - status_code=200, ) async def check_image(params: CheckImageIn = Body(...)) -> CheckImageOut: """ @@ -334,12 +332,11 @@ async def _success() -> CheckImageOut: ) -@router.post( +@api.post( "/check/image/any", tags=["Get"], summary="检查是否存在任意一个指定图像", response_model=CheckImageOut, - status_code=200, ) async def check_image_any(params: CheckImageAnyIn = Body(...)) -> CheckImageOut: """ @@ -389,12 +386,11 @@ async def _success() -> CheckImageOut: ) -@router.post( +@api.post( "/check/image/all", tags=["Get"], summary="检查是否存在所有指定图像", response_model=CheckImageOut, - status_code=200, ) async def check_image_all(params: CheckImageAllIn = Body(...)) -> CheckImageOut: """ @@ -445,12 +441,11 @@ async def _success() -> CheckImageOut: # ========== 测试接口:点击操作 ========== -@router.post( +@api.post( "/click/image", tags=["Action"], summary="点击指定图像位置", response_model=ClickOut, - status_code=200, ) async def click_image(params: ClickImageIn = Body(...)) -> ClickOut: """ @@ -498,12 +493,11 @@ async def _success() -> ClickOut: ) -@router.post( +@api.post( "/click/text", tags=["Action"], summary="点击指定文字位置", response_model=ClickOut, - status_code=200, ) async def click_text(params: ClickTextIn = Body(...)) -> ClickOut: """ diff --git a/app/api/plan.py b/app/api/plan.py index b4560787..9d8ba67a 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Body, Path -from app.api.common import api_delete, api_get, api_patch, api_post, error_out +from app.api.common import bind_api, error_out from app.core import Config from app.models.common_contract import ( IndexOrderPatch, @@ -45,6 +45,7 @@ ) router = APIRouter(prefix="/api/plan", tags=["计划管理"]) +api = bind_api(router) PlanIdPath = Annotated[str, Path(description="计划 ID")] @@ -63,18 +64,13 @@ async def _build_plan_detail_out(plan_id: str) -> PlanDetailOut: return PlanDetailOut(data=projected[plan_id]) -@api_post( - router, +@api.post( "", - model_cls=PlanCreateOut, + tags=["Add"], + summary="创建计划表", + response_model=PlanCreateOut, id="", data=MaaPlanRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建计划表", - "response_model": PlanCreateOut, - "status_code": 200, - }, ) async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: try: @@ -85,49 +81,34 @@ async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: return PlanCreateOut(id=str(uid), data=data) -@api_get( - router, +@api.get( "", - model_cls=PlanGetOut, + tags=["Get"], + summary="查询全部计划表", + response_model=PlanGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询全部计划表", - "response_model": PlanGetOut, - "status_code": 200, - }, ) async def list_plans() -> PlanGetOut: return await _build_plan_collection_out() -@api_get( - router, +@api.get( "/{plan_id}", - model_cls=PlanDetailOut, + tags=["Get"], + summary="查询单个计划表", + response_model=PlanDetailOut, data=MaaPlanRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个计划表", - "response_model": PlanDetailOut, - "status_code": 200, - }, ) async def get_plan(plan_id: PlanIdPath) -> PlanDetailOut: return await _build_plan_detail_out(plan_id) -@api_patch( - router, +@api.patch( "/{plan_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新计划表", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新计划表", + response_model=OutBase, ) async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> OutBase: try: @@ -137,16 +118,11 @@ async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> return OutBase() -@api_delete( - router, +@api.delete( "/{plan_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除计划表", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除计划表", + response_model=OutBase, ) async def delete_plan(plan_id: PlanIdPath) -> OutBase: try: @@ -156,16 +132,11 @@ async def delete_plan(plan_id: PlanIdPath) -> OutBase: return OutBase() -@api_patch( - router, +@api.patch( "/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序计划表", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序计划表", + response_model=OutBase, ) async def reorder_plan(body: IndexOrderPatch = Body(...)) -> OutBase: try: diff --git a/app/api/queue.py b/app/api/queue.py index d4dfdbe3..f8719d7b 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Body, Path -from app.api.common import api_delete, api_get, api_patch, api_post +from app.api.common import bind_api from app.core import Config from app.models.common_contract import ( IndexOrderPatch, @@ -56,6 +56,7 @@ ) router = APIRouter(prefix="/api/queue", tags=["调度队列管理"]) +api = bind_api(router) QueueIdPath = Annotated[str, Path(description="队列 ID")] TimeSetIdPath = Annotated[str, Path(description="时间设置 ID")] @@ -170,148 +171,103 @@ async def _delete_queue_item_config(queue_id: str, queue_item_id: str) -> OutBas return OutBase() -@api_get( - router, +@api.get( "", - model_cls=QueueGetOut, + tags=["Get"], + summary="查询全部调度队列", + response_model=QueueGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询全部调度队列", - "response_model": QueueGetOut, - "status_code": 200, - }, ) async def list_queues() -> QueueGetOut: return await _build_queue_collection_out() -@api_post( - router, +@api.post( "", ws_endpoint="queue.add", - model_cls=QueueCreateOut, + tags=["Add"], + summary="创建调度队列", + response_model=QueueCreateOut, id="", data=QueueRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建调度队列", - "response_model": QueueCreateOut, - "status_code": 200, - }, ) async def create_queue() -> QueueCreateOut: return await _build_queue_create_out() -@api_patch( - router, +@api.patch( "/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序调度队列", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序调度队列", + response_model=OutBase, ) async def reorder_queue(body: IndexOrderPatch = Body(...)) -> OutBase: await Config.reorder_queue(body.index_list) return OutBase() -@api_get( - router, +@api.get( "/{queue_id}", ws_endpoint="queue.get", - model_cls=QueueDetailOut, + tags=["Get"], + summary="查询单个调度队列", + response_model=QueueDetailOut, data=QueueRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个调度队列", - "response_model": QueueDetailOut, - "status_code": 200, - }, ) async def get_queue(queue_id: QueueIdPath) -> QueueDetailOut: return await _build_queue_detail_out(queue_id) -@api_patch( - router, +@api.patch( "/{queue_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新调度队列", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新调度队列", + response_model=OutBase, ) async def update_queue(queue_id: QueueIdPath, data: QueuePatch = Body(...)) -> OutBase: return await _update_queue_config(queue_id, data) -@api_delete( - router, +@api.delete( "/{queue_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除调度队列", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除调度队列", + response_model=OutBase, ) async def delete_queue(queue_id: QueueIdPath) -> OutBase: return await _delete_queue_config(queue_id) -@api_get( - router, +@api.get( "/{queue_id}/times", - model_cls=TimeSetGetOut, + tags=["Get"], + summary="查询队列下的全部定时项", + response_model=TimeSetGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询队列下的全部定时项", - "response_model": TimeSetGetOut, - "status_code": 200, - }, ) async def list_time_sets(queue_id: QueueIdPath) -> TimeSetGetOut: return await _build_time_set_collection_out(queue_id) -@api_post( - router, +@api.post( "/{queue_id}/times", - model_cls=TimeSetCreateOut, + tags=["Add"], + summary="创建定时项", + response_model=TimeSetCreateOut, id="", data=TimeSetRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建定时项", - "response_model": TimeSetCreateOut, - "status_code": 200, - }, ) async def create_time_set(queue_id: QueueIdPath) -> TimeSetCreateOut: return await _build_time_set_create_out(queue_id) -@api_patch( - router, +@api.patch( "/{queue_id}/times/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序定时项", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序定时项", + response_model=OutBase, ) async def reorder_time_sets( queue_id: QueueIdPath, body: IndexOrderPatch = Body(...) @@ -320,17 +276,12 @@ async def reorder_time_sets( return OutBase() -@api_get( - router, +@api.get( "/{queue_id}/times/{time_set_id}", - model_cls=TimeSetDetailOut, + tags=["Get"], + summary="查询单个定时项", + response_model=TimeSetDetailOut, data=TimeSetRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个定时项", - "response_model": TimeSetDetailOut, - "status_code": 200, - }, ) async def get_time_set( queue_id: QueueIdPath, time_set_id: TimeSetIdPath @@ -338,16 +289,11 @@ async def get_time_set( return await _build_time_set_detail_out(queue_id, time_set_id) -@api_patch( - router, +@api.patch( "/{queue_id}/times/{time_set_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新定时项", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新定时项", + response_model=OutBase, ) async def update_time_set( queue_id: QueueIdPath, @@ -357,65 +303,45 @@ async def update_time_set( return await _update_time_set_config(queue_id, time_set_id, data) -@api_delete( - router, +@api.delete( "/{queue_id}/times/{time_set_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除定时项", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除定时项", + response_model=OutBase, ) async def delete_time_set(queue_id: QueueIdPath, time_set_id: TimeSetIdPath) -> OutBase: return await _delete_time_set_config(queue_id, time_set_id) -@api_get( - router, +@api.get( "/{queue_id}/items", - model_cls=QueueItemGetOut, + tags=["Get"], + summary="查询队列下的全部队列项", + response_model=QueueItemGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询队列下的全部队列项", - "response_model": QueueItemGetOut, - "status_code": 200, - }, ) async def list_queue_items(queue_id: QueueIdPath) -> QueueItemGetOut: return await _build_queue_item_collection_out(queue_id) -@api_post( - router, +@api.post( "/{queue_id}/items", - model_cls=QueueItemCreateOut, + tags=["Add"], + summary="创建队列项", + response_model=QueueItemCreateOut, id="", data=QueueItemRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建队列项", - "response_model": QueueItemCreateOut, - "status_code": 200, - }, ) async def create_queue_item(queue_id: QueueIdPath) -> QueueItemCreateOut: return await _build_queue_item_create_out(queue_id) -@api_patch( - router, +@api.patch( "/{queue_id}/items/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序队列项", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序队列项", + response_model=OutBase, ) async def reorder_queue_items( queue_id: QueueIdPath, body: IndexOrderPatch = Body(...) @@ -424,17 +350,12 @@ async def reorder_queue_items( return OutBase() -@api_get( - router, +@api.get( "/{queue_id}/items/{queue_item_id}", - model_cls=QueueItemDetailOut, + tags=["Get"], + summary="查询单个队列项", + response_model=QueueItemDetailOut, data=QueueItemRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个队列项", - "response_model": QueueItemDetailOut, - "status_code": 200, - }, ) async def get_queue_item( queue_id: QueueIdPath, queue_item_id: QueueItemIdPath @@ -442,16 +363,11 @@ async def get_queue_item( return await _build_queue_item_detail_out(queue_id, queue_item_id) -@api_patch( - router, +@api.patch( "/{queue_id}/items/{queue_item_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新队列项", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新队列项", + response_model=OutBase, ) async def update_queue_item( queue_id: QueueIdPath, @@ -461,16 +377,11 @@ async def update_queue_item( return await _update_queue_item_config(queue_id, queue_item_id, data) -@api_delete( - router, +@api.delete( "/{queue_id}/items/{queue_item_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除队列项", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除队列项", + response_model=OutBase, ) async def delete_queue_item( queue_id: QueueIdPath, queue_item_id: QueueItemIdPath diff --git a/app/api/scripts.py b/app/api/scripts.py index 7ff975aa..7e6f52e4 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -27,7 +27,7 @@ from fastapi import APIRouter, Body, Path from pydantic import TypeAdapter -from app.api.common import api_delete, api_get, api_patch, api_post, error_out +from app.api.common import bind_api, error_out from app.core import Config from app.models.common_contract import ( ComboBoxItem, @@ -78,6 +78,7 @@ ) router = APIRouter(prefix="/api/scripts", tags=["脚本管理"]) +api = bind_api(router) ScriptIdPath = Annotated[str, Path(description="脚本 ID")] UserIdPath = Annotated[str, Path(description="用户 ID")] @@ -128,29 +129,23 @@ async def _build_webhook_detail_out( return WebhookDetailOut(data=projected[webhook_id]) -@api_get( - router, +@api.get( "", - model_cls=ScriptGetOut, + tags=["Get"], + summary="查询全部脚本", + response_model=ScriptGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询全部脚本", - "response_model": ScriptGetOut, - "status_code": 200, - }, ) async def list_scripts() -> ScriptGetOut: return await _build_script_collection_out() -@router.post( +@api.post( "", tags=["Add"], summary="创建脚本", response_model=ScriptCreateOut, - status_code=200, ) async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: try: @@ -172,28 +167,22 @@ async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: return ScriptCreateOut(id=str(uid), data=data) -@api_patch( - router, +@api.patch( "/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序脚本", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序脚本", + response_model=OutBase, ) async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: await Config.reorder_script(body.index_list) return OutBase() -@router.get( +@api.get( "/{script_id}", tags=["Get"], summary="查询单个脚本", response_model=ScriptDetailOut, - status_code=200, ) async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: try: @@ -213,16 +202,11 @@ async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: ) -@api_patch( - router, +@api.patch( "/{script_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新脚本配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新脚本配置", + response_model=OutBase, ) async def update_script( script_id: ScriptIdPath, @@ -237,32 +221,22 @@ async def update_script( return OutBase() -@api_delete( - router, +@api.delete( "/{script_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除脚本", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除脚本", + response_model=OutBase, ) async def delete_script(script_id: ScriptIdPath) -> OutBase: await Config.del_script(script_id) return OutBase() -@api_post( - router, +@api.post( "/{script_id}/actions/import-file", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "从文件导入脚本配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="从文件导入脚本配置", + response_model=OutBase, ) async def import_script_from_file( script_id: ScriptIdPath, body: ScriptFileBody = Body(...) @@ -271,16 +245,11 @@ async def import_script_from_file( return OutBase() -@api_post( - router, +@api.post( "/{script_id}/actions/export-file", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "导出脚本配置到文件", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="导出脚本配置到文件", + response_model=OutBase, ) async def export_script_to_file( script_id: ScriptIdPath, body: ScriptFileBody = Body(...) @@ -289,16 +258,11 @@ async def export_script_to_file( return OutBase() -@api_post( - router, +@api.post( "/{script_id}/actions/import-web", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "从网络导入脚本配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="从网络导入脚本配置", + response_model=OutBase, ) async def import_script_from_web( script_id: ScriptIdPath, body: ScriptUrlBody = Body(...) @@ -307,16 +271,11 @@ async def import_script_from_web( return OutBase() -@api_post( - router, +@api.post( "/{script_id}/actions/upload-web", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "上传脚本配置到网络", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="上传脚本配置到网络", + response_model=OutBase, ) async def upload_script_to_web( script_id: ScriptIdPath, body: ScriptUploadBody = Body(...) @@ -327,29 +286,23 @@ async def upload_script_to_web( return OutBase() -@api_get( - router, +@api.get( "/{script_id}/users", - model_cls=UserGetOut, + tags=["Get"], + summary="查询脚本下的全部用户", + response_model=UserGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询脚本下的全部用户", - "response_model": UserGetOut, - "status_code": 200, - }, ) async def list_users(script_id: ScriptIdPath) -> UserGetOut: return await _build_user_collection_out(script_id) -@router.post( +@api.post( "/{script_id}/users", tags=["Add"], summary="创建用户", response_model=UserCreateOut, - status_code=200, ) async def create_user(script_id: ScriptIdPath) -> UserCreateOut: script_type = None @@ -375,16 +328,11 @@ async def create_user(script_id: ScriptIdPath) -> UserCreateOut: return UserCreateOut(id=str(uid), data=data) -@api_patch( - router, +@api.patch( "/{script_id}/users/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序用户", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序用户", + response_model=OutBase, ) async def reorder_users( script_id: ScriptIdPath, body: IndexOrderPatch = Body(...) @@ -393,12 +341,11 @@ async def reorder_users( return OutBase() -@router.get( +@api.get( "/{script_id}/users/{user_id}", tags=["Get"], summary="查询单个用户", response_model=UserDetailOut, - status_code=200, ) async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOut: try: @@ -419,16 +366,11 @@ async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOu ) -@api_patch( - router, +@api.patch( "/{script_id}/users/{user_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新用户配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新用户配置", + response_model=OutBase, ) async def update_user( script_id: ScriptIdPath, @@ -445,32 +387,22 @@ async def update_user( return OutBase() -@api_delete( - router, +@api.delete( "/{script_id}/users/{user_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除用户", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除用户", + response_model=OutBase, ) async def delete_user(script_id: ScriptIdPath, user_id: UserIdPath) -> OutBase: await Config.del_user(script_id, user_id) return OutBase() -@api_post( - router, +@api.post( "/{script_id}/users/{user_id}/actions/import-infrastructure", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "导入基建配置文件", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="导入基建配置文件", + response_model=OutBase, ) async def import_infrastructure( script_id: ScriptIdPath, @@ -481,12 +413,11 @@ async def import_infrastructure( return OutBase() -@router.get( +@api.get( "/{script_id}/users/{user_id}/infrastructure-options", tags=["Get"], summary="用户自定义基建排班可选项", response_model=ComboBoxOut, - status_code=200, ) async def get_user_infrastructure_options( script_id: ScriptIdPath, user_id: UserIdPath @@ -499,18 +430,13 @@ async def get_user_infrastructure_options( return ComboBoxOut(data=data) -@api_get( - router, +@api.get( "/{script_id}/users/{user_id}/webhooks", - model_cls=WebhookGetOut, + tags=["Get"], + summary="查询用户下的全部 Webhook", + response_model=WebhookGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询用户下的全部 Webhook", - "response_model": WebhookGetOut, - "status_code": 200, - }, ) async def list_user_webhooks( script_id: ScriptIdPath, user_id: UserIdPath @@ -518,18 +444,13 @@ async def list_user_webhooks( return await _build_webhook_collection_out(script_id, user_id) -@api_post( - router, +@api.post( "/{script_id}/users/{user_id}/webhooks", - model_cls=WebhookCreateOut, + tags=["Add"], + summary="创建用户 Webhook", + response_model=WebhookCreateOut, id="", data=WebhookRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建用户 Webhook", - "response_model": WebhookCreateOut, - "status_code": 200, - }, ) async def create_user_webhook( script_id: ScriptIdPath, user_id: UserIdPath @@ -541,16 +462,11 @@ async def create_user_webhook( ) -@api_patch( - router, +@api.patch( "/{script_id}/users/{user_id}/webhooks/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序用户 Webhook", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序用户 Webhook", + response_model=OutBase, ) async def reorder_user_webhooks( script_id: ScriptIdPath, @@ -561,12 +477,11 @@ async def reorder_user_webhooks( return OutBase() -@router.get( +@api.get( "/{script_id}/users/{user_id}/webhooks/{webhook_id}", tags=["Get"], summary="查询单个用户 Webhook", response_model=WebhookDetailOut, - status_code=200, ) async def get_user_webhook( script_id: ScriptIdPath, @@ -579,16 +494,11 @@ async def get_user_webhook( return error_out(WebhookDetailOut, e, data=WebhookRead()) -@api_patch( - router, +@api.patch( "/{script_id}/users/{user_id}/webhooks/{webhook_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新用户 Webhook", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新用户 Webhook", + response_model=OutBase, ) async def update_user_webhook( script_id: ScriptIdPath, @@ -605,16 +515,11 @@ async def update_user_webhook( return OutBase() -@api_delete( - router, +@api.delete( "/{script_id}/users/{user_id}/webhooks/{webhook_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除用户 Webhook", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除用户 Webhook", + response_model=OutBase, ) async def delete_user_webhook( script_id: ScriptIdPath, diff --git a/app/api/setting.py b/app/api/setting.py index 4322f78e..2dd5683b 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Body, Path -from app.api.common import api_delete, api_get, api_patch, api_post +from app.api.common import bind_api from app.core import Config from app.models import Webhook as WebhookConfig from app.models.common_contract import ( @@ -49,6 +49,7 @@ from app.services import Notify router = APIRouter(prefix="/api/setting", tags=["全局设置"]) +api = bind_api(router) WebhookIdPath = Annotated[str, Path(description="Webhook ID")] @@ -109,144 +110,99 @@ async def _test_webhook_config(data: WebhookPatch) -> OutBase: return OutBase() -@api_get( - router, +@api.get( "", - model_cls=SettingGetOut, + tags=["Get"], + summary="查询全局配置", + response_model=SettingGetOut, data=GlobalConfigRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询全局配置", - "response_model": SettingGetOut, - "status_code": 200, - }, ) async def get_setting() -> SettingGetOut: return await _build_setting_out() -@api_patch( - router, +@api.patch( "", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新全局配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新全局配置", + response_model=OutBase, ) async def update_setting(data: GlobalConfigPatch = Body(...)) -> OutBase: return await _update_setting_config(data) -@api_post( - router, +@api.post( "/actions/test-notify", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "测试通知", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="测试通知", + response_model=OutBase, ) async def test_notify() -> OutBase: await Notify.send_test_notification() return OutBase() -@api_get( - router, +@api.get( "/webhooks", - model_cls=WebhookGetOut, + tags=["Get"], + summary="查询全部全局 Webhook 配置", + response_model=WebhookGetOut, index=[], data={}, - route_kwargs={ - "tags": ["Get"], - "summary": "查询全部全局 Webhook 配置", - "response_model": WebhookGetOut, - "status_code": 200, - }, ) async def list_webhooks() -> WebhookGetOut: return await _build_webhook_collection_out() -@api_post( - router, +@api.post( "/webhooks", - model_cls=WebhookCreateOut, + tags=["Add"], + summary="创建全局 Webhook 配置", + response_model=WebhookCreateOut, id="", data=WebhookRead(), - route_kwargs={ - "tags": ["Add"], - "summary": "创建全局 Webhook 配置", - "response_model": WebhookCreateOut, - "status_code": 200, - }, ) async def create_webhook() -> WebhookCreateOut: return await _build_webhook_create_out() -@api_patch( - router, +@api.patch( "/webhooks/order", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "重新排序全局 Webhook", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="重新排序全局 Webhook", + response_model=OutBase, ) async def reorder_webhooks(body: IndexOrderPatch = Body(...)) -> OutBase: await Config.reorder_webhook(None, None, body.index_list) return OutBase() -@api_post( - router, +@api.post( "/webhooks/test", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "测试指定 Webhook 配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="测试指定 Webhook 配置", + response_model=OutBase, ) async def test_webhook(data: WebhookPatch = Body(...)) -> OutBase: return await _test_webhook_config(data) -@api_get( - router, +@api.get( "/webhooks/{webhook_id}", - model_cls=WebhookDetailOut, + tags=["Get"], + summary="查询单个全局 Webhook 配置", + response_model=WebhookDetailOut, data=WebhookRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询单个全局 Webhook 配置", - "response_model": WebhookDetailOut, - "status_code": 200, - }, ) async def get_webhook(webhook_id: WebhookIdPath) -> WebhookDetailOut: return await _build_webhook_detail_out(webhook_id) -@api_patch( - router, +@api.patch( "/webhooks/{webhook_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新全局 Webhook 配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新全局 Webhook 配置", + response_model=OutBase, ) async def update_webhook( webhook_id: WebhookIdPath, data: WebhookPatch = Body(...) @@ -254,16 +210,11 @@ async def update_webhook( return await _update_webhook_config(webhook_id, data) -@api_delete( - router, +@api.delete( "/webhooks/{webhook_id}", - model_cls=OutBase, - route_kwargs={ - "tags": ["Delete"], - "summary": "删除全局 Webhook 配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Delete"], + summary="删除全局 Webhook 配置", + response_model=OutBase, ) async def delete_webhook(webhook_id: WebhookIdPath) -> OutBase: return await _delete_webhook_config(webhook_id) diff --git a/app/api/tools.py b/app/api/tools.py index 47bc0c01..03a24feb 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -23,12 +23,13 @@ from fastapi import APIRouter, Body -from app.api.common import api_get, api_patch +from app.api.common import bind_api from app.core import Config from app.models.common_contract import OutBase, project_model from app.models.tools_contract import ToolsConfigPatch, ToolsConfigRead, ToolsGetOut router = APIRouter(prefix="/api/tools", tags=["工具设置"]) +api = bind_api(router) async def _build_tools_out() -> ToolsGetOut: @@ -40,32 +41,22 @@ async def _update_tools_config(data: ToolsConfigPatch) -> OutBase: return OutBase() -@api_get( - router, +@api.get( "", - model_cls=ToolsGetOut, + tags=["Get"], + summary="查询工具配置", + response_model=ToolsGetOut, data=ToolsConfigRead(), - route_kwargs={ - "tags": ["Get"], - "summary": "查询工具配置", - "response_model": ToolsGetOut, - "status_code": 200, - }, ) async def get_tools() -> ToolsGetOut: return await _build_tools_out() -@api_patch( - router, +@api.patch( "", - model_cls=OutBase, - route_kwargs={ - "tags": ["Update"], - "summary": "更新工具配置", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Update"], + summary="更新工具配置", + response_model=OutBase, ) async def update_tools(data: ToolsConfigPatch = Body(...)) -> OutBase: return await _update_tools_config(data) diff --git a/app/api/update.py b/app/api/update.py index 91543b81..547c43f0 100644 --- a/app/api/update.py +++ b/app/api/update.py @@ -31,9 +31,10 @@ from app.services import Updater from app.models.common_contract import OutBase from app.models.update_contract import UpdateCheckIn, UpdateCheckOut -from app.api.common import api_get, api_post +from app.api.common import bind_api router = APIRouter(prefix="/api/update", tags=["软件更新"]) +api = bind_api(router) QueryUpdateCheckIn = Annotated[UpdateCheckIn, Depends()] @@ -60,52 +61,37 @@ async def _build_update_check_out(version: UpdateCheckIn) -> UpdateCheckOut: ) -@api_post( - router, +@api.post( "/check", - model_cls=UpdateCheckOut, + tags=["Get"], + summary="检查更新", + response_model=UpdateCheckOut, if_need_update=False, latest_version="", update_info={}, - route_kwargs={ - "tags": ["Get"], - "summary": "检查更新", - "response_model": UpdateCheckOut, - "status_code": 200, - }, ) async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut: return await _build_update_check_out(version) -@api_get( - router, +@api.get( "/check", - model_cls=UpdateCheckOut, + tags=["Get"], + summary="按 REST 风格检查更新", + response_model=UpdateCheckOut, if_need_update=False, latest_version="", update_info={}, - route_kwargs={ - "tags": ["Get"], - "summary": "按 REST 风格检查更新", - "response_model": UpdateCheckOut, - "status_code": 200, - }, ) async def check_update_rest(version: QueryUpdateCheckIn) -> UpdateCheckOut: return await _build_update_check_out(version) -@api_post( - router, +@api.post( "/download", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "下载更新", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="下载更新", + response_model=OutBase, ) async def download_update() -> OutBase: task = asyncio.create_task(Updater.download_update()) @@ -113,16 +99,11 @@ async def download_update() -> OutBase: return OutBase() -@api_post( - router, +@api.post( "/install", - model_cls=OutBase, - route_kwargs={ - "tags": ["Action"], - "summary": "安装更新", - "response_model": OutBase, - "status_code": 200, - }, + tags=["Action"], + summary="安装更新", + response_model=OutBase, ) async def install_update() -> OutBase: task = asyncio.create_task(Updater.install_update()) diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index ba479b8d..3223ca14 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -50,17 +50,18 @@ WSClearHistoryIn, WSCommandsOut, ) -from app.api.common import run_api +from app.api.common import bind_api, run_api logger = get_logger("WS调试") router = APIRouter(prefix="/api/ws_debug", tags=["WebSocket调试"]) +api = bind_api(router) # ============== API 路由 ============== -@router.post( +@api.post( "/client/create", summary="创建 WebSocket 客户端", response_model=WSClientCreateOut, @@ -106,7 +107,7 @@ async def _success() -> WSClientCreateOut: ) -@router.post( +@api.post( "/client/connect", summary="连接 WebSocket 客户端", response_model=WSClientStatusOut, @@ -154,7 +155,7 @@ async def _success() -> WSClientStatusOut: ) -@router.post( +@api.post( "/client/disconnect", summary="断开 WebSocket 客户端", response_model=WSClientStatusOut, @@ -185,7 +186,7 @@ async def _success() -> WSClientStatusOut: ) -@router.post( +@api.post( "/client/remove", summary="删除 WebSocket 客户端", response_model=WSClientStatusOut, @@ -223,7 +224,7 @@ async def _success() -> WSClientStatusOut: ) -@router.post( +@api.post( "/client/status", summary="获取客户端状态", response_model=WSClientStatusOut, @@ -254,7 +255,7 @@ async def get_client_status(request: WSClientStatusIn) -> WSClientStatusOut: ) -@router.get( +@api.get( "/client/list", summary="列出所有客户端", response_model=WSClientListOut, @@ -272,7 +273,7 @@ async def list_clients() -> WSClientListOut: ) -@router.post( +@api.post( "/message/send", summary="发送原始消息", response_model=WSClientStatusOut, @@ -312,7 +313,7 @@ async def _success() -> WSClientStatusOut: ) -@router.post( +@api.post( "/message/send_json", summary="发送格式化消息", response_model=WSClientStatusOut, @@ -354,7 +355,7 @@ async def _success() -> WSClientStatusOut: ) -@router.post( +@api.post( "/message/auth", summary="发送认证消息", response_model=WSClientStatusOut, @@ -404,7 +405,7 @@ async def _success() -> WSClientStatusOut: ) -@router.get( +@api.get( "/history", summary="获取消息历史", response_model=WSMessageHistoryOut, @@ -427,7 +428,7 @@ async def get_history(name: Optional[str] = None) -> WSMessageHistoryOut: ) -@router.post( +@api.post( "/history/clear", summary="清空消息历史", response_model=WSClientStatusOut, @@ -452,7 +453,7 @@ async def clear_history(request: WSClearHistoryIn) -> WSClientStatusOut: ) -@router.get( +@api.get( "/commands", summary="获取可用 WS 命令", response_model=WSCommandsOut, diff --git a/app/core/config/base.py b/app/core/config/base.py index b79a8951..e6a7e507 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -248,7 +248,7 @@ def _callback_identity(callback: CollectionEventSlot) -> object: """生成回调唯一标识。""" if inspect.ismethod(callback) and getattr(callback, "__self__", None) is not None: - return (id(callback.__self__), callback.__func__) + return id(callback.__self__), callback.__func__ return callback @@ -483,14 +483,11 @@ async def get(self, uid: uuid.UUID) -> dict[str, Any]: if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在。") - data: dict[str, Any] = { - "instances": [ - {"uid": str(current_uid), "type": type(self.data[current_uid]).__name__} - for current_uid in self.order - if current_uid == uid - ] - } - data[str(uid)] = await self.data[uid].toDict() + data: dict[str, Any] = {"instances": [ + {"uid": str(current_uid), "type": type(self.data[current_uid]).__name__} + for current_uid in self.order + if current_uid == uid + ], str(uid): await self.data[uid].toDict()} return data async def save(self) -> None: From 716f3e0d8324979442bcf0034881b721462a77af Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 5 Apr 2026 13:26:50 +0800 Subject: [PATCH 14/29] =?UTF-8?q?refactor(config):=20=E8=BF=81=E7=A7=BBcon?= =?UTF-8?q?tracts=E8=87=B3app/contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/MaaFW/ArknightWin32.py | 24 -- app/__init__.py | 6 +- app/api/common.py | 4 +- app/api/core.py | 2 +- app/api/dispatch.py | 4 +- app/api/emulator.py | 4 +- app/api/history.py | 2 +- app/api/info.py | 4 +- app/api/ocr.py | 2 +- app/api/plan.py | 4 +- app/api/queue.py | 4 +- app/api/scripts.py | 6 +- app/api/setting.py | 4 +- app/api/tools.py | 4 +- app/api/update.py | 4 +- app/api/ws_debug.py | 2 +- app/contracts/__init__.py | 63 ++++ app/{models => contracts}/common_contract.py | 0 .../dispatch_contract.py | 0 .../emulator_contract.py | 4 +- app/{models => contracts}/general_contract.py | 4 +- app/{models => contracts}/history_contract.py | 0 app/{models => contracts}/info_contract.py | 0 app/{models => contracts}/maa_contract.py | 6 +- app/{models => contracts}/maaend_contract.py | 4 +- app/{models => contracts}/plan_contract.py | 0 app/{models => contracts}/queue_contract.py | 2 +- app/{models => contracts}/scripts_contract.py | 0 app/{models => contracts}/setting_contract.py | 4 +- app/{models => contracts}/src_contract.py | 4 +- app/{models => contracts}/tools_contract.py | 2 +- app/{models => contracts}/update_contract.py | 0 app/{models => contracts}/ws_contract.py | 0 app/core/config/base.py | 318 ++++++++++------ app/core/config/fields.py | 90 ++++- app/core/config/types.py | 187 ++++++++-- app/core/maa_manager.py | 8 +- app/services/matomo.py | 5 +- app/services/notification.py | 33 +- app/services/system.py | 22 +- app/services/update.py | 28 +- app/task/MAA/ManualReview.py | 36 +- app/task/MAA/ScriptConfig.py | 1 - app/task/MAA/tools/UpdateMAA.py | 1 - app/task/MAA/tools/bilibili.py | 3 +- app/task/MAA/tools/notify.py | 6 +- app/task/MaaEnd/AutoProxy.py | 8 +- app/task/MaaEnd/ManualReview.py | 34 +- app/task/MaaEnd/ScriptConfig.py | 12 + app/task/MaaEnd/tools/parse_log.py | 100 ++--- app/task/SRC/AutoProxy.py | 2 +- app/task/SRC/ManualReview.py | 34 +- app/task/SRC/tools/notify.py | 6 +- app/task/SRC/tools/poor_yaml.py | 9 +- app/task/general/AutoProxy.py | 10 +- app/task/general/tools/notify.py | 6 +- app/utils/ImageUtils.py | 9 +- app/utils/LogMonitor.py | 24 +- app/utils/OCR/OCRtool.py | 353 ++++++++++++------ app/utils/__init__.py | 2 +- app/utils/emulator/ldplayer.py | 2 +- app/utils/emulator/tools.py | 4 +- app/utils/skland.py | 112 ++++-- app/utils/websocket.py | 74 +++- main.py | 6 +- requirements.txt | 1 + 66 files changed, 1149 insertions(+), 570 deletions(-) create mode 100644 app/contracts/__init__.py rename app/{models => contracts}/common_contract.py (100%) rename app/{models => contracts}/dispatch_contract.py (100%) rename app/{models => contracts}/emulator_contract.py (95%) rename app/{models => contracts}/general_contract.py (89%) rename app/{models => contracts}/history_contract.py (100%) rename app/{models => contracts}/info_contract.py (100%) rename app/{models => contracts}/maa_contract.py (89%) rename app/{models => contracts}/maaend_contract.py (90%) rename app/{models => contracts}/plan_contract.py (100%) rename app/{models => contracts}/queue_contract.py (97%) rename app/{models => contracts}/scripts_contract.py (100%) rename app/{models => contracts}/setting_contract.py (94%) rename app/{models => contracts}/src_contract.py (90%) rename app/{models => contracts}/tools_contract.py (91%) rename app/{models => contracts}/update_contract.py (100%) rename app/{models => contracts}/ws_contract.py (100%) diff --git a/app/MaaFW/ArknightWin32.py b/app/MaaFW/ArknightWin32.py index 33ed6413..a83dc3e4 100644 --- a/app/MaaFW/ArknightWin32.py +++ b/app/MaaFW/ArknightWin32.py @@ -29,7 +29,6 @@ import win32gui import pyautogui import pygetwindow -from loguru import logger from pynput import keyboard from maa.tasker import Tasker @@ -48,9 +47,7 @@ class _ArknightWin32Toolkit: - def __init__(self): - self.arknights_hwnd = -1 self.arknights_window = None @@ -64,7 +61,6 @@ def __init__(self): self.original_nice = self.p.nice() async def init(self) -> None: - pyautogui.PAUSE = 0 pyautogui.FAILSAFE = False @@ -103,7 +99,6 @@ async def scheduled_task(self) -> None: new_hwnd = win32gui.FindWindow(None, "明日方舟") if self.arknights_hwnd != new_hwnd: - self.arknights_hwnd = new_hwnd if new_hwnd == 0: @@ -209,7 +204,6 @@ async def click_pause_button(self) -> None: pyautogui.moveTo(cur_x, cur_y) def get_pause_position(self): - if not self.arknights_window: raise RuntimeError("未连接到明日方舟窗口") @@ -232,9 +226,7 @@ def get_pause_position(self): @MaaFWManager.resource.custom_action("PlaySelectDeployed[ArknightsPC]") class PlaySelectDeployed(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行战斗时选中已部署干员动作") try: @@ -257,9 +249,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("PauseSelectDeployed[ArknightsPC]") class PauseSelectDeployed(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行暂停时选中已部署干员动作") try: @@ -284,9 +274,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("PlaySkill[ArknightsPC]") class PlaySkill(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行战斗时释放技能动作") try: x, y = ArknightWin32Toolkit.get_pause_position() @@ -306,9 +294,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("PauseSkill[ArknightsPC]") class PauseSkill(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行暂停时释放技能动作") try: @@ -332,9 +318,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("PlayRetreat[ArknightsPC]") class PlayRetreat(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行战斗时撤退干员动作") try: @@ -355,9 +339,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("PauseRetreat[ArknightsPC]") class PauseRetreat(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行暂停时撤退干员动作") try: @@ -381,9 +363,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("NextFrame-0.2x[ArknightsPC]") class NextFrame_0_2x(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行0.2倍速下一帧动作") try: @@ -406,9 +386,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("NextFrame-1x[ArknightsPC]") class NextFrame_1x(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行1倍速下一帧动作") try: @@ -431,9 +409,7 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("NextFrame-2x[ArknightsPC]") class NextFrame_2x(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: - logger.info("开始执行2倍速下一帧动作") try: diff --git a/app/__init__.py b/app/__init__.py index 377b82eb..651424bb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -21,10 +21,6 @@ # Contact: DLmaster_361@163.com -from .api import * -from .core import * -from .models import * -from .services import * -from .utils import * +from . import api, core, models, services, utils __all__ = ["api", "core", "models", "services", "utils"] diff --git a/app/api/common.py b/app/api/common.py index ff0aa6fb..c13b04ad 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -7,7 +7,7 @@ from fastapi import APIRouter -from app.models.common_contract import ComboBoxItem, ComboBoxOut, OutBase +from app.contracts.common_contract import ComboBoxItem, ComboBoxOut, OutBase OutT = TypeVar("OutT", bound=OutBase) @@ -122,7 +122,7 @@ def api_post( class ApiRegistrar: - """接近 FastAPI 原生声明风格的 API 装饰器注册器。""" + """API 装饰器注册器。""" def __init__(self, router: APIRouter): self.router = router diff --git a/app/api/core.py b/app/api/core.py index 632a64a1..202ed975 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -29,7 +29,7 @@ from app.core import Config, Broadcast, TaskManager from app.services import System -from app.models.common_contract import OutBase +from app.contracts.common_contract import OutBase from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command from app.api.common import bind_api, error_out diff --git a/app/api/dispatch.py b/app/api/dispatch.py index 504a5498..f90157d5 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -25,8 +25,8 @@ from app.core import Config, TaskManager from app.services import System -from app.models.common_contract import OutBase -from app.models.dispatch_contract import ( +from app.contracts.common_contract import OutBase +from app.contracts.dispatch_contract import ( DispatchIn, PowerIn, PowerOut, diff --git a/app/api/emulator.py b/app/api/emulator.py index 4a9acbde..1ccc51c5 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -27,14 +27,14 @@ from app.api.common import bind_api from app.core import Config, EmulatorManager -from app.models.common_contract import ( +from app.contracts.common_contract import ( IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) -from app.models.emulator_contract import ( +from app.contracts.emulator_contract import ( EmulatorActionBody, EmulatorConfigIndexItem, EmulatorCreateOut, diff --git a/app/api/history.py b/app/api/history.py index 76bb01cd..47f98841 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -28,7 +28,7 @@ from app.core import Config from app.api.common import bind_api, error_out -from app.models.history_contract import ( +from app.contracts.history_contract import ( HistoryData, HistoryDataGetIn, HistoryDataGetOut, diff --git a/app/api/info.py b/app/api/info.py index a0a81c2d..0076320f 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -27,7 +27,7 @@ from pydantic import Field, TypeAdapter from app.core import Config -from app.models.common_contract import ( +from app.contracts.common_contract import ( ApiModel, ComboBoxItem, ComboBoxOut, @@ -35,7 +35,7 @@ OutBase, ) from app.api.common import bind_api, error_out -from app.models.info_contract import ( +from app.contracts.info_contract import ( GetStageIn, NoticeOut, VersionOut, diff --git a/app/api/ocr.py b/app/api/ocr.py index 79dab59f..bef08854 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -30,7 +30,7 @@ from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger -from app.models.common_contract import ApiModel, OutBase +from app.contracts.common_contract import ApiModel, OutBase from app.api.common import bind_api, error_out, run_api logger = get_logger("OCR API") diff --git a/app/api/plan.py b/app/api/plan.py index 9d8ba67a..37b5f591 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -27,14 +27,14 @@ from app.api.common import bind_api, error_out from app.core import Config -from app.models.common_contract import ( +from app.contracts.common_contract import ( IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) -from app.models.plan_contract import ( +from app.contracts.plan_contract import ( MaaPlanRead, PlanCreateIn, PlanCreateOut, diff --git a/app/api/queue.py b/app/api/queue.py index f8719d7b..8ac33d77 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -27,14 +27,14 @@ from app.api.common import bind_api from app.core import Config -from app.models.common_contract import ( +from app.contracts.common_contract import ( IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) -from app.models.queue_contract import ( +from app.contracts.queue_contract import ( QueueCreateOut, QueueDetailOut, QueueGetOut, diff --git a/app/api/scripts.py b/app/api/scripts.py index 7e6f52e4..68c8c006 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -29,7 +29,7 @@ from app.api.common import bind_api, error_out from app.core import Config -from app.models.common_contract import ( +from app.contracts.common_contract import ( ComboBoxItem, ComboBoxOut, IndexOrderPatch, @@ -38,7 +38,7 @@ project_model_list, project_model_map, ) -from app.models.scripts_contract import ( +from app.contracts.scripts_contract import ( ScriptPatchBody, InfrastructureImportBody, ScriptCreateIn, @@ -64,7 +64,7 @@ dump_script_patch_data, dump_user_patch_data, ) -from app.models.setting_contract import ( +from app.contracts.setting_contract import ( WebhookCreateOut, WebhookDetailOut, WebhookGetOut, diff --git a/app/api/setting.py b/app/api/setting.py index 2dd5683b..1d0273ad 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -28,14 +28,14 @@ from app.api.common import bind_api from app.core import Config from app.models import Webhook as WebhookConfig -from app.models.common_contract import ( +from app.contracts.common_contract import ( IndexOrderPatch, OutBase, project_model, project_model_list, project_model_map, ) -from app.models.setting_contract import ( +from app.contracts.setting_contract import ( GlobalConfigPatch, GlobalConfigRead, SettingGetOut, diff --git a/app/api/tools.py b/app/api/tools.py index 03a24feb..51a24495 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -25,8 +25,8 @@ from app.api.common import bind_api from app.core import Config -from app.models.common_contract import OutBase, project_model -from app.models.tools_contract import ToolsConfigPatch, ToolsConfigRead, ToolsGetOut +from app.contracts.common_contract import OutBase, project_model +from app.contracts.tools_contract import ToolsConfigPatch, ToolsConfigRead, ToolsGetOut router = APIRouter(prefix="/api/tools", tags=["工具设置"]) api = bind_api(router) diff --git a/app/api/update.py b/app/api/update.py index 547c43f0..e89884be 100644 --- a/app/api/update.py +++ b/app/api/update.py @@ -29,8 +29,8 @@ from app.core import Config from app.services import Updater -from app.models.common_contract import OutBase -from app.models.update_contract import UpdateCheckIn, UpdateCheckOut +from app.contracts.common_contract import OutBase +from app.contracts.update_contract import UpdateCheckIn, UpdateCheckOut from app.api.common import bind_api router = APIRouter(prefix="/api/update", tags=["软件更新"]) diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index 3223ca14..aae1697a 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -34,7 +34,7 @@ from app.utils.websocket import ws_client_manager from app.api.ws_command import list_ws_commands from app.utils.logger import get_logger -from app.models.ws_contract import ( +from app.contracts.ws_contract import ( WSClientCreateIn, WSClientCreateOut, WSClientConnectIn, diff --git a/app/contracts/__init__.py b/app/contracts/__init__.py new file mode 100644 index 00000000..10068b05 --- /dev/null +++ b/app/contracts/__init__.py @@ -0,0 +1,63 @@ +# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software +# Copyright © 2024-2025 DLmaster361 +# Copyright © 2025 MoeSnowyFox +# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with AUTO-MAS. If not, see . + +# Contact: DLmaster_361@163.com + +""" +API Contract 层 + +本模块包含所有 API 请求/响应模型,按领域拆分为独立文件。 + +设计原则: +1. Contract 只负责 API 边界定义,不包含业务逻辑 +2. 使用 Read/Patch/Create 分离读写模型 +3. 所有 Contract 继承 ApiModel,获得统一配置 +4. 字段命名使用 snake_case,通过 alias 兼容前端 +""" + +from .common_contract import ( + ApiModel, + ComboBoxItem, + ComboBoxOut, + IndexOrderPatch, + InfoOut, + OutBase, + ResourceCollectionOut, + ResourceCreateOut, + ResourceItemOut, + project_model, + project_model_list, + project_model_map, +) + +__all__ = [ + "ApiModel", + "OutBase", + "InfoOut", + "ComboBoxItem", + "ComboBoxOut", + "ResourceCollectionOut", + "ResourceItemOut", + "ResourceCreateOut", + "IndexOrderPatch", + "project_model", + "project_model_list", + "project_model_map", +] diff --git a/app/models/common_contract.py b/app/contracts/common_contract.py similarity index 100% rename from app/models/common_contract.py rename to app/contracts/common_contract.py diff --git a/app/models/dispatch_contract.py b/app/contracts/dispatch_contract.py similarity index 100% rename from app/models/dispatch_contract.py rename to app/contracts/dispatch_contract.py diff --git a/app/models/emulator_contract.py b/app/contracts/emulator_contract.py similarity index 95% rename from app/models/emulator_contract.py rename to app/contracts/emulator_contract.py index ba157024..d368c4a3 100644 --- a/app/models/emulator_contract.py +++ b/app/contracts/emulator_contract.py @@ -4,7 +4,7 @@ from pydantic import Field -from .common import EmulatorConfig +from app.models.common import EmulatorConfig from .common_contract import ( ApiModel, ResourceCollectionOut, @@ -12,7 +12,7 @@ ResourceItemOut, derive_config_contracts, ) -from .shared import DeviceInfo +from app.models.shared import DeviceInfo _EmulatorReadBase, _EmulatorPatchBase = derive_config_contracts( diff --git a/app/models/general_contract.py b/app/contracts/general_contract.py similarity index 89% rename from app/models/general_contract.py rename to app/contracts/general_contract.py index 603bf31d..ca4af434 100644 --- a/app/models/general_contract.py +++ b/app/contracts/general_contract.py @@ -5,8 +5,8 @@ from pydantic import Field from .common_contract import derive_config_contracts -from .general import GeneralConfig as RuntimeGeneralConfig -from .general import GeneralUserConfig as RuntimeGeneralUserConfig +from app.models.general import GeneralConfig as RuntimeGeneralConfig +from app.models.general import GeneralUserConfig as RuntimeGeneralUserConfig _GeneralConfigReadBase, _GeneralConfigPatchBase = derive_config_contracts( diff --git a/app/models/history_contract.py b/app/contracts/history_contract.py similarity index 100% rename from app/models/history_contract.py rename to app/contracts/history_contract.py diff --git a/app/models/info_contract.py b/app/contracts/info_contract.py similarity index 100% rename from app/models/info_contract.py rename to app/contracts/info_contract.py diff --git a/app/models/maa_contract.py b/app/contracts/maa_contract.py similarity index 89% rename from app/models/maa_contract.py rename to app/contracts/maa_contract.py index f764d75c..c8e514fa 100644 --- a/app/models/maa_contract.py +++ b/app/contracts/maa_contract.py @@ -5,9 +5,9 @@ from pydantic import Field from .common_contract import derive_config_contracts -from .maa import MaaConfig as RuntimeMaaConfig -from .maa import MaaPlanConfig as RuntimeMaaPlanConfig -from .maa import MaaUserConfig as RuntimeMaaUserConfig +from app.models.maa import MaaConfig as RuntimeMaaConfig +from app.models.maa import MaaPlanConfig as RuntimeMaaPlanConfig +from app.models.maa import MaaUserConfig as RuntimeMaaUserConfig _MaaConfigReadBase, _MaaConfigPatchBase = derive_config_contracts( diff --git a/app/models/maaend_contract.py b/app/contracts/maaend_contract.py similarity index 90% rename from app/models/maaend_contract.py rename to app/contracts/maaend_contract.py index 62add6d4..98f000bd 100644 --- a/app/models/maaend_contract.py +++ b/app/contracts/maaend_contract.py @@ -5,8 +5,8 @@ from pydantic import Field from .common_contract import derive_config_contracts -from .maaend import MaaEndConfig as RuntimeMaaEndConfig -from .maaend import MaaEndUserConfig as RuntimeMaaEndUserConfig +from app.models.maaend import MaaEndConfig as RuntimeMaaEndConfig +from app.models.maaend import MaaEndUserConfig as RuntimeMaaEndUserConfig _MaaEndConfigReadBase, _MaaEndConfigPatchBase = derive_config_contracts( diff --git a/app/models/plan_contract.py b/app/contracts/plan_contract.py similarity index 100% rename from app/models/plan_contract.py rename to app/contracts/plan_contract.py diff --git a/app/models/queue_contract.py b/app/contracts/queue_contract.py similarity index 97% rename from app/models/queue_contract.py rename to app/contracts/queue_contract.py index a14fbb48..ebef20e4 100644 --- a/app/models/queue_contract.py +++ b/app/contracts/queue_contract.py @@ -4,7 +4,7 @@ from pydantic import Field -from .common import QueueConfig, QueueItem, TimeSet +from app.models.common import QueueConfig, QueueItem, TimeSet from .common_contract import ( ApiModel, ResourceCollectionOut, diff --git a/app/models/scripts_contract.py b/app/contracts/scripts_contract.py similarity index 100% rename from app/models/scripts_contract.py rename to app/contracts/scripts_contract.py diff --git a/app/models/setting_contract.py b/app/contracts/setting_contract.py similarity index 94% rename from app/models/setting_contract.py rename to app/contracts/setting_contract.py index 711f094b..9bdda4f0 100644 --- a/app/models/setting_contract.py +++ b/app/contracts/setting_contract.py @@ -4,7 +4,8 @@ from pydantic import Field -from .common import Webhook +from app.models.common import Webhook +from app.models.global_config import GlobalConfig from .common_contract import ( ApiModel, ResourceCollectionOut, @@ -12,7 +13,6 @@ ResourceItemOut, derive_config_contracts, ) -from .global_config import GlobalConfig _WebhookReadBase, _WebhookPatchBase = derive_config_contracts( diff --git a/app/models/src_contract.py b/app/contracts/src_contract.py similarity index 90% rename from app/models/src_contract.py rename to app/contracts/src_contract.py index 154f8b98..d967f8b4 100644 --- a/app/models/src_contract.py +++ b/app/contracts/src_contract.py @@ -5,8 +5,8 @@ from pydantic import Field from .common_contract import derive_config_contracts -from .src import SrcConfig as RuntimeSrcConfig -from .src import SrcUserConfig as RuntimeSrcUserConfig +from app.models.src import SrcConfig as RuntimeSrcConfig +from app.models.src import SrcUserConfig as RuntimeSrcUserConfig _SrcConfigReadBase, _SrcConfigPatchBase = derive_config_contracts( diff --git a/app/models/tools_contract.py b/app/contracts/tools_contract.py similarity index 91% rename from app/models/tools_contract.py rename to app/contracts/tools_contract.py index a9c428e9..a40dcd25 100644 --- a/app/models/tools_contract.py +++ b/app/contracts/tools_contract.py @@ -1,7 +1,7 @@ from __future__ import annotations from .common_contract import ResourceItemOut, derive_config_contracts -from .global_config import ToolsConfig +from app.models.global_config import ToolsConfig _ToolsConfigReadBase, _ToolsConfigPatchBase = derive_config_contracts( diff --git a/app/models/update_contract.py b/app/contracts/update_contract.py similarity index 100% rename from app/models/update_contract.py rename to app/contracts/update_contract.py diff --git a/app/models/ws_contract.py b/app/contracts/ws_contract.py similarity index 100% rename from app/models/ws_contract.py rename to app/contracts/ws_contract.py diff --git a/app/core/config/base.py b/app/core/config/base.py index e6a7e507..7af75913 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -29,140 +29,153 @@ import tomllib import uuid import weakref -from contextlib import asynccontextmanager, suppress +from contextlib import asynccontextmanager from dataclasses import dataclass from pathlib import Path from collections.abc import AsyncIterator, Callable, Coroutine from typing import Any, Generic, Protocol, TypeVar, cast +import tomli_w # pyright: ignore[reportMissingImports] +from loguru import logger + PRIMARY_CONFIG_SUFFIX = ".toml" LEGACY_CONFIG_SUFFIX = ".json" -def _toml_key(key: str) -> str: - """生成兼容 UUID 等特殊字符的 TOML 键名。""" - - return json.dumps(str(key), ensure_ascii=False) - - -def _toml_path(path: tuple[str, ...]) -> str: - """生成 TOML 表头路径。""" - - return ".".join(_toml_key(part) for part in path) - - -def _toml_scalar(value: Any) -> str: - """序列化 TOML 标量值。""" - - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, int): - return str(value) - if isinstance(value, float): - return repr(value) - if value is None: - return '""' - return json.dumps(str(value), ensure_ascii=False) - - -def _toml_inline(value: Any) -> str: - """序列化内联 TOML 值,支持数组和内联表。""" - - if isinstance(value, dict): - mapping = cast(dict[object, Any], value) - items = ", ".join( - f"{_toml_key(str(key))} = {_toml_inline(item)}" - for key, item in mapping.items() - ) - return "{ " + items + " }" - if isinstance(value, list): - items = cast(list[Any], value) - return "[" + ", ".join(_toml_inline(item) for item in items) + "]" - return _toml_scalar(value) - - def dump_toml(data: dict[str, Any]) -> str: - """公共 TOML 序列化入口。""" + """ + 使用 tomli-w 库序列化 TOML 数据。 - lines: list[str] = [] + Args: + data: 要序列化的字典数据 - def emit_table(path: tuple[str, ...], obj: dict[str, Any]) -> None: - scalars: list[tuple[str, Any]] = [] - children: list[tuple[str, dict[str, Any]]] = [] + Returns: + TOML 格式的字符串 - for key, value in obj.items(): - if isinstance(value, dict): - children.append((str(key), cast(dict[str, Any], value))) - else: - scalars.append((str(key), value)) + Raises: + TypeError: 如果数据包含不可序列化的类型 + """ + try: + return cast(Any, tomli_w).dumps(data) + except (TypeError, ValueError) as e: + logger.error(f"TOML 序列化失败: {e}, 数据类型: {type(data)}") + raise - if path: - lines.append(f"[{_toml_path(path)}]") - for key, value in scalars: - lines.append(f"{_toml_key(key)} = {_toml_inline(value)}") +def _load_json_config(path: Path) -> dict[str, Any]: + """ + 加载 JSON 配置文件。 - if scalars and children: - lines.append("") + Args: + path: 配置文件路径 + + Returns: + 配置字典,如果文件为空或格式错误则返回空字典 + """ + try: + raw_text = path.read_text(encoding="utf-8") + if not raw_text.strip(): + logger.debug(f"配置文件为空: {path}") + return {} + + loaded = json.loads(raw_text) + if not isinstance(loaded, dict): + logger.warning(f"配置文件不是字典类型: {path}, 类型: {type(loaded)}") + return {} + + mapping = cast(dict[object, Any], loaded) + return {str(key): item for key, item in mapping.items()} + except json.JSONDecodeError as e: + logger.error(f"JSON 解析失败: {path}, 错误: {e}") + return {} + except Exception: + logger.exception(f"加载配置文件失败: {path}") + return {} - for index, (key, child) in enumerate(children): - emit_table((*path, key), child) - if index != len(children) - 1: - lines.append("") - emit_table((), data) - content = "\n".join(lines).strip() - return f"{content}\n" if content else "" +def _load_toml_config(path: Path) -> dict[str, Any]: + """ + 加载 TOML 配置文件。 + Args: + path: 配置文件路径 -def _load_json_config(path: Path) -> dict[str, Any]: - raw_text = path.read_text(encoding="utf-8") - if raw_text.strip() == "": + Returns: + 配置字典,如果文件为空或格式错误则返回空字典 + """ + try: + raw_text = path.read_text(encoding="utf-8") + if not raw_text.strip(): + logger.debug(f"配置文件为空: {path}") + return {} + + loaded = tomllib.loads(raw_text) + mapping = cast(dict[object, Any], loaded) + return {str(key): item for key, item in mapping.items()} + except tomllib.TOMLDecodeError as e: + logger.error(f"TOML 解析失败: {path}, 错误: {e}") return {} - - loaded = json.loads(raw_text) - if not isinstance(loaded, dict): + except Exception: + logger.exception(f"加载配置文件失败: {path}") return {} - mapping = cast(dict[object, Any], loaded) - return {str(key): item for key, item in mapping.items()} - -def _load_toml_config(path: Path) -> dict[str, Any]: - raw_text = path.read_text(encoding="utf-8") - if raw_text.strip() == "": - return {} +def _load_config_with_legacy_migration( + path: Path, +) -> tuple[dict[str, Any], Path | None]: + """ + 加载配置文件,支持从 JSON 迁移到 TOML。 - loaded = tomllib.loads(raw_text) - mapping = cast(dict[object, Any], loaded) - return {str(key): item for key, item in mapping.items()} + 优先级: + 1. 如果存在 .json 文件且 .toml 文件不存在或为空,则加载 .json + 2. 否则加载 .toml 文件 + 3. 如果 .toml 加载失败,回退到 .json + Args: + path: TOML 配置文件路径 -def _load_config_with_legacy_migration(path: Path) -> tuple[dict[str, Any], Path | None]: + Returns: + (配置字典, 旧版 JSON 文件路径或 None) + """ legacy_json_file = path.with_suffix(LEGACY_CONFIG_SUFFIX) + # 情况 1: JSON 存在且 TOML 不存在或为空 if legacy_json_file.exists() and (not path.exists() or path.stat().st_size == 0): - try: - return _load_json_config(legacy_json_file), legacy_json_file - except json.JSONDecodeError: - return {}, legacy_json_file + logger.info(f"从旧版 JSON 配置迁移: {legacy_json_file} -> {path}") + data = _load_json_config(legacy_json_file) + return data, legacy_json_file + # 情况 2: TOML 不存在 if not path.exists(): + logger.debug(f"配置文件不存在: {path}") return {}, legacy_json_file if legacy_json_file.exists() else None - try: - return _load_toml_config(path), legacy_json_file if legacy_json_file.exists() else None - except tomllib.TOMLDecodeError: - if legacy_json_file.exists(): - with suppress(json.JSONDecodeError): - return _load_json_config(legacy_json_file), legacy_json_file - return {}, legacy_json_file if legacy_json_file.exists() else None + # 情况 3: 尝试加载 TOML + data = _load_toml_config(path) + if data or not legacy_json_file.exists(): + return data, legacy_json_file if legacy_json_file.exists() else None + + # 情况 4: TOML 加载失败,回退到 JSON + logger.warning(f"TOML 加载失败,回退到 JSON: {legacy_json_file}") + return _load_json_config(legacy_json_file), legacy_json_file def _backup_legacy_config_if_needed( current_file: Path, legacy_file: Path | None ) -> None: + """ + 备份旧版 JSON 配置文件。 + + 仅在以下条件同时满足时备份: + 1. 旧版文件存在 + 2. 新版文件存在且非空 + 3. 备份文件不存在 + + Args: + current_file: 当前 TOML 配置文件路径 + legacy_file: 旧版 JSON 配置文件路径 + """ if legacy_file is None or not legacy_file.exists(): return if not current_file.exists() or current_file.stat().st_size == 0: @@ -170,7 +183,11 @@ def _backup_legacy_config_if_needed( legacy_backup = legacy_file.with_suffix(f"{legacy_file.suffix}.bak") if not legacy_backup.exists(): - legacy_file.replace(legacy_backup) + try: + legacy_file.replace(legacy_backup) + logger.info(f"已备份旧版配置: {legacy_file} -> {legacy_backup}") + except Exception as e: + logger.error(f"备份旧版配置失败: {legacy_file}, 错误: {e}") def load_config_with_legacy_migration( @@ -262,7 +279,10 @@ class _WeakCallbackSlot: @classmethod def build(cls, callback: CollectionEventSlot) -> "_WeakCallbackSlot": - if inspect.ismethod(callback) and getattr(callback, "__self__", None) is not None: + if ( + inspect.ismethod(callback) + and getattr(callback, "__self__", None) is not None + ): return cls( identity=_callback_identity(callback), weak_method=weakref.WeakMethod(callback), @@ -278,9 +298,7 @@ def resolve(self) -> CollectionEventSlot | None: return self.callback -async def _emit_collection_slots( - slots: list[_WeakCallbackSlot], event: Any -) -> None: +async def _emit_collection_slots(slots: list[_WeakCallbackSlot], event: Any) -> None: """依次触发容器事件回调,并清理失效弱引用。""" alive_slots: list[_WeakCallbackSlot] = [] @@ -348,24 +366,38 @@ def __str__(self) -> str: return f"MultipleConfig with {len(self.data)} items" async def connect(self, path: Path) -> None: - """将运行期配置连接到指定 TOML 文件。""" + """ + 将运行期配置连接到指定 TOML 文件。 + + Args: + path: 配置文件路径,必须以 .toml 结尾 + Raises: + ValueError: 如果文件扩展名不是 .toml 或配置已锁定 + """ if path.suffix != PRIMARY_CONFIG_SUFFIX: - raise ValueError("配置文件必须是带有 '.toml' 扩展名的 TOML 文件。") + raise ValueError(f"配置文件必须是 .toml 格式,当前: {path.suffix}") if self.is_locked: - raise ValueError("配置已锁定, 无法修改") + raise ValueError("配置已锁定,无法修改") + logger.info(f"连接配置文件: {path}") self.file = path if not self.file.exists(): self.file.parent.mkdir(parents=True, exist_ok=True) self.file.touch() + logger.debug(f"创建新配置文件: {self.file}") - data, legacy_file = _load_config_with_legacy_migration(self.file) - await self.load(data) - await self.add_save_method(self.save) - _backup_legacy_config_if_needed(self.file, legacy_file) + try: + data, legacy_file = _load_config_with_legacy_migration(self.file) + await self.load(data) + await self.add_save_method(self.save) + _backup_legacy_config_if_needed(self.file, legacy_file) + logger.info(f"配置加载成功: {path}, 项目数: {len(self.data)}") + except Exception: + logger.exception(f"配置加载失败: {path}") + raise async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] @@ -483,36 +515,65 @@ async def get(self, uid: uuid.UUID) -> dict[str, Any]: if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在。") - data: dict[str, Any] = {"instances": [ - {"uid": str(current_uid), "type": type(self.data[current_uid]).__name__} - for current_uid in self.order - if current_uid == uid - ], str(uid): await self.data[uid].toDict()} + data: dict[str, Any] = { + "instances": [ + {"uid": str(current_uid), "type": type(self.data[current_uid]).__name__} + for current_uid in self.order + if current_uid == uid + ], + str(uid): await self.data[uid].toDict(), + } return data async def save(self) -> None: - """保存当前多实例配置。""" + """ + 保存当前多实例配置到文件。 + + 如果在事务中,则延迟保存;否则立即写入文件。 + Raises: + ValueError: 如果文件路径未设置 + OSError: 如果文件写入失败 + """ if not self.file: - raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + raise ValueError("文件路径未设置,请先调用 connect() 方法") if self._transaction_depth > 0: self._pending_save = True + logger.debug(f"事务中,延迟保存: {self.file}") return - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.write_text( - dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) + try: + self.file.parent.mkdir(parents=True, exist_ok=True) + content = dump_toml(await self.toDict(if_decrypt=False)) + + # 原子写入:先写临时文件,再替换 + temp_file = self.file.with_suffix(f"{self.file.suffix}.tmp") + temp_file.write_text(content, encoding="utf-8") + temp_file.replace(self.file) + + logger.debug(f"配置保存成功: {self.file}, 大小: {len(content)} 字节") + except Exception: + logger.exception(f"配置保存失败: {self.file}") + raise async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: - """新增一个指定类型的子配置实例。""" + """ + 新增一个指定类型的子配置实例。 + + Args: + config_type: 配置类型,必须在允许的类型列表中 + Returns: + (新配置的 UUID, 配置实例) + + Raises: + ValueError: 如果配置类型不被允许或配置已锁定 + """ if config_type not in self.sub_config_type.values(): raise ValueError(f"配置类型 {config_type.__name__} 不被允许") if self.is_locked: - raise ValueError("配置已锁定, 无法修改") + raise ValueError("配置已锁定,无法修改") uid = uuid.uuid4() config = config_type() @@ -520,6 +581,8 @@ async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: self.order.append(uid) self.data[uid] = config + logger.info(f"新增配置: {config_type.__name__}, UID: {uid}") + for save_method in self._save_methods: await config.add_save_method(save_method) @@ -539,16 +602,25 @@ async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: return uid, config async def remove(self, uid: uuid.UUID) -> None: - """移除一个子配置实例。""" + """ + 移除一个子配置实例。 + + Args: + uid: 要移除的配置 UUID + Raises: + ValueError: 如果配置不存在、已锁定或父容器已锁定 + """ if self.is_locked: - raise ValueError("配置已锁定, 无法修改") + raise ValueError("配置已锁定,无法修改") if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在") if self.data[uid].is_locked: - raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除") + raise ValueError(f"配置项 '{uid}' 已锁定,无法移除") config = self.data[uid] + logger.info(f"移除配置: {type(config).__name__}, UID: {uid}") + await _emit_collection_slots( self._on_before_del_slots, MultipleConfigDeleteEvent(self, uid, config) ) diff --git a/app/core/config/fields.py b/app/core/config/fields.py index 136a0eae..67d0e2a5 100644 --- a/app/core/config/fields.py +++ b/app/core/config/fields.py @@ -1,29 +1,105 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Callable, Literal +from typing import Any, Literal, Protocol RefDeleteAction = Literal["restrict", "set_default", "cascade", "custom"] VirtualDependency = tuple[str, str] +class OnDeleteCallback(Protocol): + """引用字段删除回调协议。""" + + def __call__(self, owner: Any, deleted_value: Any) -> Any: + """ + 当引用的对象被删除时调用。 + + Args: + owner: 拥有引用字段的配置对象 + deleted_value: 被删除的引用值 + + Returns: + 新的字段值 + """ + ... + + @dataclass(frozen=True, slots=True) class RefField: - """声明式引用字段元数据。""" + """ + 声明式引用字段元数据。 + + 用于标记一个字段引用另一个配置对象,并定义引用完整性约束。 + + Attributes: + target: 引用目标的配置类型名称 + default: 引用失效时的默认值 + allow_values: 允许的特殊值(如 "-" 表示未选择) + on_delete: 删除策略 + on_delete_callback: 自定义删除回调(仅当 on_delete="custom" 时有效) + """ target: str default: Any allow_values: tuple[Any, ...] = () on_delete: RefDeleteAction = "set_default" - on_delete_callback: str | Callable[[Any, Any], Any] | None = None + on_delete_callback: OnDeleteCallback | str | None = None + + def __post_init__(self) -> None: + """验证字段配置。""" + if self.on_delete == "custom" and self.on_delete_callback is None: + raise ValueError("on_delete='custom' 时必须提供 on_delete_callback") + if self.on_delete != "custom" and self.on_delete_callback is not None: + raise ValueError("on_delete_callback 仅在 on_delete='custom' 时有效") + + +class VirtualFieldGetter(Protocol): + """虚拟字段 getter 协议。""" + + def __call__(self, owner: Any) -> Any: + """ + 获取虚拟字段的值。 + + Args: + owner: 拥有虚拟字段的配置对象 + + Returns: + 虚拟字段的计算值 + """ + ... + + +class VirtualFieldSetter(Protocol): + """虚拟字段 setter 协议。""" + + def __call__(self, owner: Any, value: Any) -> Any: + """ + 设置虚拟字段的值。 + + Args: + owner: 拥有虚拟字段的配置对象 + value: 要设置的值 + + Returns: + 实际设置的值 + """ + ... @dataclass(frozen=True, slots=True) class VirtualField: - """声明式虚拟字段元数据。""" + """ + 声明式虚拟字段元数据。 - getter: str | Callable[[Any], Any] - setter: str | Callable[[Any, Any], Any] | None = None - depends_on: tuple[VirtualDependency, ...] = field(default_factory=tuple) + 虚拟字段不直接存储数据,而是通过 getter/setter 计算或代理到其他字段。 + Attributes: + getter: 获取字段值的函数或方法名 + setter: 设置字段值的函数或方法名(可选) + depends_on: 依赖的字段列表,格式为 (group, field) + """ + + getter: VirtualFieldGetter | str + setter: VirtualFieldSetter | str | None = None + depends_on: tuple[VirtualDependency, ...] = field(default_factory=tuple) diff --git a/app/core/config/types.py b/app/core/config/types.py index 37f5ea43..8f7bf4f0 100644 --- a/app/core/config/types.py +++ b/app/core/config/types.py @@ -6,6 +6,7 @@ from urllib.parse import urlparse import pyautogui +from loguru import logger from pydantic import AfterValidator from app.utils.constants import DEFAULT_DATETIME @@ -17,101 +18,235 @@ class EncryptedFieldMarker: def _to_string(value: Any) -> str: + """ + 将任意值转换为字符串。 + + Args: + value: 要转换的值 + + Returns: + 字符串表示,None 返回空字符串 + """ + if value is None: + return "" if isinstance(value, str): return value return str(value) -def _validate_json_dict_string(value: str) -> str: +def _validate_json_dict_string(value: Any) -> str: + """ + 校验并规范化 JSON 字典字符串。 + + Args: + value: 输入值 + + Returns: + 有效的 JSON 字典字符串,失败时返回 "{ }" + + Raises: + ValidationError: 如果输入不是字符串且无法转换 + """ text = _to_string(value) + if not text: + return "{ }" + try: parsed = json.loads(text) - except json.JSONDecodeError: + if not isinstance(parsed, dict): + logger.warning(f"JSON 不是字典类型: {text[:50]}...") + return "{ }" + return text + except json.JSONDecodeError as e: + logger.warning(f"JSON 解析失败: {e}, 输入: {text[:50]}...") return "{ }" - return text if isinstance(parsed, dict) else "{ }" -def _validate_json_list_string(value: str) -> str: +def _validate_json_list_string(value: Any) -> str: + """ + 校验并规范化 JSON 列表字符串。 + + Args: + value: 输入值 + + Returns: + 有效的 JSON 列表字符串,失败时返回 "[ ]" + """ text = _to_string(value) + if not text: + return "[ ]" + try: parsed = json.loads(text) - except json.JSONDecodeError: + if not isinstance(parsed, list): + logger.warning(f"JSON 不是列表类型: {text[:50]}...") + return "[ ]" + return text + except json.JSONDecodeError as e: + logger.warning(f"JSON 解析失败: {e}, 输入: {text[:50]}...") return "[ ]" - return text if isinstance(parsed, list) else "[ ]" -def _validate_hhmm_string(value: str) -> str: +def _validate_hhmm_string(value: Any) -> str: + """ + 校验并规范化 HH:MM 时间字符串。 + + Args: + value: 输入值 + + Returns: + 有效的 HH:MM 格式字符串,失败时返回默认时间 + """ text = _to_string(value) + if not text: + return DEFAULT_DATETIME.strftime("%H:%M") + try: datetime.strptime(text, "%H:%M") return text - except ValueError: + except ValueError as e: + logger.warning(f"时间格式错误: {e}, 输入: {text}") return DEFAULT_DATETIME.strftime("%H:%M") -def _validate_ymd_hm_string(value: str) -> str: +def _validate_ymd_hm_string(value: Any) -> str: + """校验并规范化 YYYY-MM-DD HH:MM 日期时间字符串。""" text = _to_string(value) + if not text: + return DEFAULT_DATETIME.strftime("%Y-%m-%d %H:%M") + try: datetime.strptime(text, "%Y-%m-%d %H:%M") return text - except ValueError: + except ValueError as e: + logger.warning(f"日期时间格式错误: {e}, 输入: {text}") return DEFAULT_DATETIME.strftime("%Y-%m-%d %H:%M") -def _validate_ymd_string(value: str) -> str: +def _validate_ymd_string(value: Any) -> str: + """校验并规范化 YYYY-MM-DD 日期字符串。""" text = _to_string(value) + if not text: + return DEFAULT_DATETIME.strftime("%Y-%m-%d") + try: datetime.strptime(text, "%Y-%m-%d") return text - except ValueError: + except ValueError as e: + logger.warning(f"日期格式错误: {e}, 输入: {text}") return DEFAULT_DATETIME.strftime("%Y-%m-%d") -def _validate_ymd_hms_string(value: str) -> str: +def _validate_ymd_hms_string(value: Any) -> str: + """校验并规范化 YYYY-MM-DD HH:MM:SS 日期时间字符串。""" text = _to_string(value) + if not text: + return DEFAULT_DATETIME.strftime("%Y-%m-%d %H:%M:%S") + try: datetime.strptime(text, "%Y-%m-%d %H:%M:%S") return text - except ValueError: + except ValueError as e: + logger.warning(f"日期时间格式错误: {e}, 输入: {text}") return DEFAULT_DATETIME.strftime("%Y-%m-%d %H:%M:%S") -def _validate_url_string(value: str) -> str: +def _validate_url_string(value: Any) -> str: + """ + 校验并规范化 URL 字符串。 + + Args: + value: 输入值 + + Returns: + 有效的 URL 字符串,失败时返回空字符串 + """ text = _to_string(value) - if text == "": + if not text: return "" + try: parsed = urlparse(text) - except Exception: + if not parsed.scheme or not parsed.netloc: + logger.warning(f"URL 格式错误: {text}") + return "" + return text + except Exception as e: + logger.warning(f"URL 解析失败: {e}, 输入: {text}") return "" - return text if parsed.scheme and parsed.netloc else "" -def _validate_keyboard_key(value: str) -> str: +def _validate_keyboard_key(value: Any) -> str: + """ + 校验键盘按键字符串。 + + Args: + value: 输入值 + + Returns: + 有效的按键字符串,失败时返回空字符串 + """ text = _to_string(value).lower() - return text if text in pyautogui.KEYBOARD_KEYS else "" + if not text: + return "" + if text not in pyautogui.KEYBOARD_KEYS: + logger.warning(f"无效的键盘按键: {text}") + return "" -def _normalize_encrypted_string(value: str) -> str: + return text + + +def _normalize_encrypted_string(value: Any) -> str: + """ + 规范化加密字符串。 + + 如果输入已加密,则保持不变;否则加密。 + + Args: + value: 输入值 + + Returns: + 加密后的字符串 + """ text = _to_string(value) - if text == "": + if not text: return "" + try: + # 尝试解密,如果成功说明已加密 dpapi_decrypt(text) return text except Exception: - return dpapi_encrypt(text) + # 解密失败,说明是明文,需要加密 + try: + return dpapi_encrypt(text) + except Exception as e: + logger.error(f"加密失败: {e}, 输入长度: {len(text)}") + return "" def decrypt_encrypted_string(value: str) -> str: - if value == "": + """ + 解密加密字符串。 + + Args: + value: 加密的字符串 + + Returns: + 解密后的明文,失败时返回错误提示 + """ + if not value: return "" + try: return dpapi_decrypt(value) - except Exception: - return "数据损坏, 请重新设置" + except Exception as e: + logger.error(f"解密失败: {e}") + return "数据损坏,请重新设置" +# 类型别名定义 JsonDictString = Annotated[str, AfterValidator(_validate_json_dict_string)] JsonListString = Annotated[str, AfterValidator(_validate_json_list_string)] HHMMString = Annotated[str, AfterValidator(_validate_hhmm_string)] diff --git a/app/core/maa_manager.py b/app/core/maa_manager.py index a77ae1d8..558e3e45 100644 --- a/app/core/maa_manager.py +++ b/app/core/maa_manager.py @@ -51,9 +51,7 @@ class _MaaFWManager: - def __init__(self): - self.resource = Resource() (Config.config_path / "maa_option.json").write_text( @@ -90,8 +88,8 @@ async def do_job(job: Job | JobWithResult) -> Any: if job.failed: if isinstance(result, JobWithResult): raise RuntimeError(f"任务执行失败, 执行信息: {result.get()}") - elif isinstance(result, Job): - raise RuntimeError(f"任务执行失败") + else: + raise RuntimeError("任务执行失败") async def get_win32_tasker( self, @@ -287,7 +285,6 @@ async def reconnect_adb_tasker( @MaaFWManager.resource.custom_action("DisableLog") class DisableLog(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: """ 自定义动作: 临时禁用日志输出 @@ -307,7 +304,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool: @MaaFWManager.resource.custom_action("EnableLog") class EnableLog(CustomAction): - def run(self, context: Context, argv: CustomAction.RunArg) -> bool: """ 自定义动作: 启用日志输出 diff --git a/app/services/matomo.py b/app/services/matomo.py index 97995047..56f7f915 100644 --- a/app/services/matomo.py +++ b/app/services/matomo.py @@ -41,10 +41,9 @@ class _MatomoHandler: site_id = "3" def __init__(self): - self.session = None - async def _get_session(self): + async def _get_session(self) -> aiohttp.ClientSession: """获取HTTP会话""" if self.session is None or self.session.closed: @@ -103,8 +102,6 @@ async def send_event( """ try: session = await self._get_session() - if session is None: - return params = self._build_base_params(custom_vars) params.update({"e_c": category, "e_a": action, "e_n": name, "e_v": value}) diff --git a/app/services/notification.py b/app/services/notification.py index 644b1855..0fe1957c 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -1,7 +1,6 @@ # AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software # Copyright © 2024-2025 DLmaster361 # Copyright © 2025-2026 AUTO-MAS Team -import asyncio # This file is part of AUTO-MAS. @@ -26,13 +25,13 @@ import smtplib import httpx from datetime import datetime -from plyer import notification +from plyer import notification # pyright: ignore[reportMissingTypeStubs] from email.header import Header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr from pathlib import Path -from typing import Literal +from typing import Any, Literal from app.core import Config from app.models import Webhook @@ -114,10 +113,13 @@ async def send_mail( raise ValueError("邮件通知的接收邮箱格式错误或为空") # 定义邮件正文 + message: MIMEText | MIMEMultipart if mode == "文本": message = MIMEText(content, "plain", "utf-8") elif mode == "网页": message = MIMEMultipart("alternative") + else: + raise ValueError(f"不支持的邮件模式: {mode}") message["From"] = formataddr( ( Header("AUTO-MAS通知服务", "utf-8").encode(), @@ -228,7 +230,7 @@ async def WebhookPush(self, title: str, content: str, webhook: Webhook) -> None: template_obj = json.loads(template) # 递归替换JSON对象中的变量 - def replace_variables(obj): + def replace_variables(obj: Any) -> Any: if isinstance(obj, dict): return {k: replace_variables(v) for k, v in obj.items()} elif isinstance(obj, list): @@ -278,6 +280,7 @@ def replace_variables(obj): headers.update(json.loads(webhook.get("Data", "Headers"))) async with httpx.AsyncClient(proxy=Config.proxy, timeout=10) as client: + response: httpx.Response if webhook.get("Data", "Method") == "POST": if isinstance(data, dict): response = await client.post( @@ -287,20 +290,30 @@ def replace_variables(obj): response = await client.post( url=webhook.get("Data", "Url"), content=data, headers=headers ) + else: + response = await client.post( + url=webhook.get("Data", "Url"), + content=str(data), + headers=headers, + ) elif webhook.get("Data", "Method") == "GET": if isinstance(data, dict): # Flatten params to ensure all values are str or list of str - params = {} + params: dict[str, str] = {} for k, v in data.items(): if isinstance(v, (dict, list)): - params[k] = json.dumps(v, ensure_ascii=False) + params[str(k)] = json.dumps(v, ensure_ascii=False) else: - params[k] = str(v) + params[str(k)] = str(v) else: - params = {"message": str(data)} + params: dict[str, str] = {"message": str(data)} response = await client.get( url=webhook.get("Data", "Url"), params=params, headers=headers ) + else: + raise ValueError( + f"不支持的 Webhook 方法: {webhook.get('Data', 'Method')}" + ) # 检查响应 if response.status_code == 200: @@ -310,7 +323,7 @@ def replace_variables(obj): else: raise Exception(f"HTTP {response.status_code}: {response.text}") - async def _WebHookPush(self, title, content, webhook_url) -> None: + async def _WebHookPush(self, title: str, content: str, webhook_url: str) -> None: """ WebHook 推送通知 (即将弃用) @@ -420,7 +433,7 @@ async def send_koishi( if success: logger.success(f"Koishi 通知推送成功: {message[:50]}") else: - logger.error(f"Koishi 通知推送失败: 发送消息失败") + logger.error("Koishi 通知推送失败: 发送消息失败") return success diff --git a/app/services/system.py b/app/services/system.py index 350b4959..683ed9cd 100644 --- a/app/services/system.py +++ b/app/services/system.py @@ -39,13 +39,12 @@ class _SystemHandler: - ES_CONTINUOUS = 0x80000000 ES_SYSTEM_REQUIRED = 0x00000001 countdown = 60 def __init__(self) -> None: - self.power_task: Optional[asyncio.Task] = None + self.power_task: Optional[asyncio.Task[None]] = None async def set_Sleep(self, if_allow_sleep: bool) -> None: """ @@ -77,7 +76,6 @@ async def set_SelfStart(self, if_self_start: bool) -> None: """ if if_self_start and not await self.is_startup(): - # 创建任务计划 # 获取当前用户和时间 @@ -167,9 +165,7 @@ async def set_SelfStart(self, if_self_start: bool) -> None: Path(xml_file).unlink() elif not if_self_start and await self.is_startup(): - try: - result = await ProcessRunner.run_process( "schtasks", "/delete", "/tn", "AUTO-MAS_AutoStart", "/f" ) @@ -204,13 +200,10 @@ async def set_power( """ if sys.platform.startswith("win"): - if mode == "NoAction": - logger.info("不执行系统电源操作") elif mode == "Shutdown": - await self.kill_emulator_processes() logger.info("执行关机操作") subprocess.run(["shutdown", "/s", "/t", "0"]) @@ -220,25 +213,21 @@ async def set_power( subprocess.run(["shutdown", "/s", "/t", "0", "/f"]) elif mode == "Reboot": - await self.kill_emulator_processes() logger.info("执行重启操作") subprocess.run(["shutdown", "/r", "/t", "0"]) elif mode == "Hibernate": - logger.info("执行休眠操作") subprocess.run(["shutdown", "/h"]) elif mode == "Sleep": - logger.info("执行睡眠操作") subprocess.run( ["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"] ) elif mode == "KillSelf" and Config.server is not None: - logger.info("执行退出主程序操作") if not from_frontend: await Config.send_websocket_message( @@ -247,33 +236,26 @@ async def set_power( Config.server.should_exit = True elif sys.platform.startswith("linux"): - if mode == "NoAction": - logger.info("不执行系统电源操作") elif mode == "Shutdown": - logger.info("执行关机操作") subprocess.run(["shutdown", "-h", "now"]) elif mode == "Reboot": - logger.info("执行重启操作") subprocess.run(["shutdown", "-r", "now"]) elif mode == "Hibernate": - logger.info("执行休眠操作") subprocess.run(["systemctl", "hibernate"]) elif mode == "Sleep": - logger.info("执行睡眠操作") subprocess.run(["systemctl", "suspend"]) elif mode == "KillSelf" and Config.server is not None: - logger.info("执行退出主程序操作") if not from_frontend: await Config.send_websocket_message( @@ -380,7 +362,7 @@ async def kill_process(self, path: Path) -> None: logger.success(f"进程已中止: {path}") - async def search_pids(self, path: Path) -> list: + async def search_pids(self, path: Path) -> list[int]: """ 根据路径查找进程PID diff --git a/app/services/update.py b/app/services/update.py index cc047ee5..fab22490 100644 --- a/app/services/update.py +++ b/app/services/update.py @@ -30,7 +30,7 @@ import subprocess from packaging import version from datetime import datetime, timedelta -from typing import List, Dict, Optional +from typing import Any, List, Dict, Optional, cast from pathlib import Path from app.core import Config @@ -42,7 +42,6 @@ class _UpdateHandler: - def __init__(self) -> None: self.is_locked: bool = False self.remote_version: Optional[str] = None @@ -53,7 +52,6 @@ def __init__(self) -> None: async def check_update( self, current_version: str, if_force: bool = False ) -> tuple[bool, str, Dict[str, List[str]]]: - if ( not if_force and self.remote_version is not None @@ -71,6 +69,7 @@ async def check_update( ) logger.info("开始检查更新") + version_info: dict[str, Any] = {} # 使用 httpx 异步请求 async with httpx.AsyncClient( @@ -80,9 +79,9 @@ async def check_update( f"https://mirrorchyan.com/api/resources/AUTO_MAS/latest?user_agent=AutoMasGui&os=win&arch=x64¤t_version={current_version}&cdk={Config.get('Update', 'MirrorChyanCDK') if Config.get('Update', 'Source') == 'MirrorChyan' else ''}&channel={Config.get('Update', 'Channel')}" ) if response.status_code == 200: - version_info = response.json() + version_info = cast(dict[str, Any], response.json()) else: - result = response.json() + result = cast(dict[str, Any], response.json()) if result["code"] != 0: if result["code"] in MIRROR_ERROR_INFO: @@ -97,20 +96,21 @@ async def check_update( logger.success("获取版本信息成功") self.last_check_time = datetime.now() - self.remote_version = version_info["data"]["version_name"] + data = cast(dict[str, Any], version_info.get("data", {})) + version_name = data.get("version_name") + self.remote_version = str(version_name) if version_name is not None else None if self.remote_version is None: raise Exception("Mirror 酱未返回版本号, 请稍后重试") - if "url" in version_info["data"]: - self.mirror_chyan_download_url = version_info["data"]["url"] + if "url" in data: + self.mirror_chyan_download_url = cast(Optional[str], data.get("url")) if version.parse(self.remote_version) > version.parse(current_version): - # 版本更新信息 version_info_json: Dict[str, Dict[str, List[str]]] = json.loads( re.sub( r"^$", r"\1", - version_info["data"]["release_note"].splitlines()[0].strip(), + str(data.get("release_note", "")).splitlines()[0].strip(), ) ) @@ -120,7 +120,6 @@ async def check_update( for ver, info in version_info_json.items() if version.parse(ver) > version.parse(current_version) ]: - for key, value in v_i.items(): if key not in self.update_version_info: self.update_version_info[key] = [] @@ -132,7 +131,6 @@ async def check_update( return False, current_version, {} async def download_update(self) -> None: - logger.info("收到前端下载请求") if self.is_locked: @@ -171,11 +169,9 @@ async def download_update(self) -> None: return None if Config.get("Update", "Source") == "GitHub": - download_url = f"https://github.com/AUTO-MAS-Project/AUTO-MAS/releases/download/{self.remote_version}/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" elif Config.get("Update", "Source") == "MirrorChyan": - if self.mirror_chyan_download_url is None: logger.warning("MirrorChyan 未返回下载链接, 使用自建下载站") download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" @@ -200,7 +196,6 @@ async def download_update(self) -> None: check_times = 3 while check_times != 0: - try: # 清理可能存在的临时文件 if (Path.cwd() / "download.temp").exists(): @@ -285,7 +280,6 @@ async def download_update(self) -> None: break except Exception as e: - if check_times != -1: check_times -= 1 @@ -295,7 +289,6 @@ async def download_update(self) -> None: await asyncio.sleep(1) else: - if (Path.cwd() / "download.temp").exists(): (Path.cwd() / "download.temp").unlink() await Config.send_websocket_message( @@ -304,7 +297,6 @@ async def download_update(self) -> None: self.is_locked = False async def install_update(self): - if self.is_locked: await Config.send_websocket_message( id="Update", diff --git a/app/task/MAA/ManualReview.py b/app/task/MAA/ManualReview.py index b32b08ed..72a8b5dc 100644 --- a/app/task/MAA/ManualReview.py +++ b/app/task/MAA/ManualReview.py @@ -26,6 +26,7 @@ import shutil from pathlib import Path from datetime import datetime, timedelta +from typing import Any, cast from app.core import Config, Broadcast from app.models.task import TaskExecuteBase, ScriptItem, LogRecord @@ -80,7 +81,7 @@ async def check(self) -> str: async def prepare(self): self.maa_process_manager = ProcessManager() self.maa_log_monitor = LogMonitor((1, 20), "%Y-%m-%d %H:%M:%S", self.check_log) - self.message_queue = asyncio.Queue() + self.message_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() await Broadcast.subscribe(self.message_queue) self.wait_event = asyncio.Event() self.log_start_time = datetime.now() @@ -152,8 +153,13 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if not result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if not choice: break continue @@ -204,8 +210,13 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if not result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if not choice: break if self.run_book["SignIn"]: @@ -227,15 +238,20 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if choice: self.run_book["PassCheck"] = True - async def _wait_for_user_response(self, message_id: str): + async def _wait_for_user_response(self, message_id: str) -> dict[str, Any]: """等待用户交互响应""" logger.info(f"等待客户端回应消息: {message_id}") while True: - message = await self.message_queue.get() + message: dict[str, Any] = await self.message_queue.get() if message.get("id") == message_id and message.get("type") == "Response": self.message_queue.task_done() logger.success(f"收到客户端回应消息: {message_id}") @@ -245,7 +261,7 @@ async def _wait_for_user_response(self, message_id: str): async def set_maa(self, emulator_info: DeviceInfo): """配置MAA运行参数""" - logger.info(f"开始配置MAA运行参数: 人工排查") + logger.info("开始配置MAA运行参数: 人工排查") await self.maa_process_manager.kill() await System.kill_process(self.maa_exe_path) diff --git a/app/task/MAA/ScriptConfig.py b/app/task/MAA/ScriptConfig.py index 8db64064..3a3832e1 100644 --- a/app/task/MAA/ScriptConfig.py +++ b/app/task/MAA/ScriptConfig.py @@ -31,7 +31,6 @@ from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager -from app.utils.constants import MAA_TASKS logger = get_logger("MAA 脚本设置") diff --git a/app/task/MAA/tools/UpdateMAA.py b/app/task/MAA/tools/UpdateMAA.py index 36bade69..cf4b8517 100644 --- a/app/task/MAA/tools/UpdateMAA.py +++ b/app/task/MAA/tools/UpdateMAA.py @@ -24,7 +24,6 @@ from app.services import System from app.utils import ProcessRunner, get_logger -from app.utils.constants import MAA_TASKS logger = get_logger("MAA 更新工具") diff --git a/app/task/MAA/tools/bilibili.py b/app/task/MAA/tools/bilibili.py index 98b18067..b56c8d97 100644 --- a/app/task/MAA/tools/bilibili.py +++ b/app/task/MAA/tools/bilibili.py @@ -22,13 +22,14 @@ import json from pathlib import Path +from typing import Any from app.core import Config async def agree_bilibili(maa_tasks_path: Path, if_agree: bool): """向MAA写入Bilibili协议相关任务""" - data: dict = json.loads(maa_tasks_path.read_text(encoding="utf-8")) + data: dict[str, Any] = json.loads(maa_tasks_path.read_text(encoding="utf-8")) if if_agree and Config.get("Function", "IfAgreeBilibili"): data["BilibiliAgreement_AUTO"] = { "algorithm": "OcrDetect", diff --git a/app/task/MAA/tools/notify.py b/app/task/MAA/tools/notify.py index 4490491a..c43236d4 100644 --- a/app/task/MAA/tools/notify.py +++ b/app/task/MAA/tools/notify.py @@ -23,12 +23,16 @@ from app.services import Notify from app.utils import get_logger from app.models import MaaUserConfig +from typing import Any logger = get_logger("MAA 通知工具") async def push_notification( - mode: str, title: str, message: dict, user_config: MaaUserConfig | None + mode: str, + title: str, + message: dict[str, Any], + user_config: MaaUserConfig | None, ) -> None: """通过所有渠道推送通知""" diff --git a/app/task/MaaEnd/AutoProxy.py b/app/task/MaaEnd/AutoProxy.py index 884e14c0..e227d20f 100644 --- a/app/task/MaaEnd/AutoProxy.py +++ b/app/task/MaaEnd/AutoProxy.py @@ -34,7 +34,7 @@ from app.utils import get_logger, LogMonitor, ProcessManager, skland_sign_in from app.utils.constants import UTC4, UTC8, MAAEND_KILLPROC_TASK from .tools import login, parse_log, push_notification, wait_and_focus_window -from .ScriptConfig import CONFIG_FILE_NAME, _keep_single_instance, _replace_config_dir +from .ScriptConfig import CONFIG_FILE_NAME, keep_single_instance, replace_config_dir logger = get_logger("MaaEnd 自动代理") @@ -372,10 +372,10 @@ async def set_maaend(self, device_info: DeviceInfo | None) -> None: source_config_file = source_config_path / CONFIG_FILE_NAME if source_config_file.exists(): - _keep_single_instance(source_config_file) - _replace_config_dir(source_config_path, self.maaend_set_path) + keep_single_instance(source_config_file) + replace_config_dir(source_config_path, self.maaend_set_path) - maaend_set, maaend_instance = _keep_single_instance( + maaend_set, maaend_instance = keep_single_instance( self.maaend_set_path / CONFIG_FILE_NAME ) maaend_tasks = maaend_instance["tasks"] diff --git a/app/task/MaaEnd/ManualReview.py b/app/task/MaaEnd/ManualReview.py index 4002c4fc..c6f62251 100644 --- a/app/task/MaaEnd/ManualReview.py +++ b/app/task/MaaEnd/ManualReview.py @@ -23,6 +23,7 @@ import asyncio from pathlib import Path from datetime import datetime +from typing import Any, cast from app.core import Broadcast, Config from app.models.task import TaskExecuteBase, ScriptItem @@ -83,7 +84,7 @@ async def check(self) -> str: async def prepare(self): if self.emulator_manager is None: self.game_process_manager = ProcessManager() - self.message_queue = asyncio.Queue() + self.message_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() await Broadcast.subscribe(self.message_queue) self.wait_event = asyncio.Event() @@ -154,8 +155,13 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if not result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if not choice: break continue @@ -189,8 +195,13 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if not result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if not choice: break if self.run_book["SignIn"]: @@ -213,15 +224,20 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if choice: self.run_book["PassCheck"] = True - async def _wait_for_user_response(self, message_id: str): + async def _wait_for_user_response(self, message_id: str) -> dict[str, Any]: """等待用户交互响应""" logger.info(f"等待客户端回应消息: {message_id}") while True: - message = await self.message_queue.get() + message: dict[str, Any] = await self.message_queue.get() if message.get("id") == message_id and message.get("type") == "Response": self.message_queue.task_done() logger.success(f"收到客户端回应消息: {message_id}") diff --git a/app/task/MaaEnd/ScriptConfig.py b/app/task/MaaEnd/ScriptConfig.py index 20d7db77..a7b100a0 100644 --- a/app/task/MaaEnd/ScriptConfig.py +++ b/app/task/MaaEnd/ScriptConfig.py @@ -90,11 +90,23 @@ def _keep_single_instance(config_path: Path) -> tuple[dict[str, Any], dict[str, return data, selected_instance +def keep_single_instance(config_path: Path) -> tuple[dict[str, Any], dict[str, Any]]: + """公开封装:保留单实例配置。""" + + return _keep_single_instance(config_path) + + def _replace_config_dir(source_dir: Path, target_dir: Path) -> None: shutil.rmtree(target_dir, ignore_errors=True) shutil.copytree(source_dir, target_dir) +def replace_config_dir(source_dir: Path, target_dir: Path) -> None: + """公开封装:替换配置目录。""" + + _replace_config_dir(source_dir, target_dir) + + class ScriptConfigTask(TaskExecuteBase): """MaaEnd 脚本设置模式""" diff --git a/app/task/MaaEnd/tools/parse_log.py b/app/task/MaaEnd/tools/parse_log.py index 11f886f2..5f8a82f7 100644 --- a/app/task/MaaEnd/tools/parse_log.py +++ b/app/task/MaaEnd/tools/parse_log.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, cast TASK_EVENT_TEXT = { @@ -166,8 +166,8 @@ class RuntimeTaskStart: task_id: str -_AUX_CACHE_KEY: tuple[tuple[str, int, int], ...] | None = None -_AUX_CACHE_VALUE: AuxiliaryData | None = None +_aux_cache_key: tuple[tuple[str, int, int], ...] | None = None +_aux_cache_value: AuxiliaryData | None = None def _safe_load_json_dict(text: str) -> dict[str, Any] | None: @@ -177,7 +177,7 @@ def _safe_load_json_dict(text: str) -> dict[str, Any] | None: data = json.loads(text) except json.JSONDecodeError: return None - return data if isinstance(data, dict) else None + return cast(dict[str, Any], data) if isinstance(data, dict) else None def _read_lines(path: Path) -> list[str]: @@ -361,12 +361,14 @@ def _build_snapshot_tasks(raw_tasks: Any) -> tuple[SnapshotTask, ...]: return () tasks: list[SnapshotTask] = [] - for raw_task in raw_tasks: + raw_tasks_list = cast(list[Any], raw_tasks) + for raw_task in raw_tasks_list: if not isinstance(raw_task, dict): continue - task_id = raw_task.get("id") - task_name = raw_task.get("taskName") - enabled = raw_task.get("enabled") + raw_task_dict = cast(dict[str, Any], raw_task) + task_id = raw_task_dict.get("id") + task_name = raw_task_dict.get("taskName") + enabled = raw_task_dict.get("enabled") if isinstance(task_id, str) and isinstance(task_name, str): tasks.append( SnapshotTask( @@ -385,10 +387,12 @@ def _load_snapshots(config_data: dict[str, Any]) -> tuple[Snapshot, ...]: raw_instances = config_data.get("instances") if isinstance(raw_instances, list): - for raw_instance in raw_instances: + raw_instances_list = cast(list[Any], raw_instances) + for raw_instance in raw_instances_list: if not isinstance(raw_instance, dict): continue - instance_id = raw_instance.get("id") + raw_instance_dict = cast(dict[str, Any], raw_instance) + instance_id = raw_instance_dict.get("id") if not isinstance(instance_id, str): continue snapshots.append( @@ -396,17 +400,19 @@ def _load_snapshots(config_data: dict[str, Any]) -> tuple[Snapshot, ...]: instance_id=instance_id, source="instance", closed_at=None, - tasks=_build_snapshot_tasks(raw_instance.get("tasks")), + tasks=_build_snapshot_tasks(raw_instance_dict.get("tasks")), ) ) raw_recently_closed = config_data.get("recentlyClosed") if isinstance(raw_recently_closed, list): - for raw_snapshot in raw_recently_closed: + raw_recently_closed_list = cast(list[Any], raw_recently_closed) + for raw_snapshot in raw_recently_closed_list: if not isinstance(raw_snapshot, dict): continue - instance_id = raw_snapshot.get("id") - closed_at = raw_snapshot.get("closedAt") + raw_snapshot_dict = cast(dict[str, Any], raw_snapshot) + instance_id = raw_snapshot_dict.get("id") + closed_at = raw_snapshot_dict.get("closedAt") if not isinstance(instance_id, str): continue snapshots.append( @@ -414,7 +420,7 @@ def _load_snapshots(config_data: dict[str, Any]) -> tuple[Snapshot, ...]: instance_id=instance_id, source="recently_closed", closed_at=closed_at if isinstance(closed_at, int) else None, - tasks=_build_snapshot_tasks(raw_snapshot.get("tasks")), + tasks=_build_snapshot_tasks(raw_snapshot_dict.get("tasks")), ) ) @@ -526,7 +532,7 @@ def _load_go_service_mapping(lines: list[str]) -> dict[str, str]: def _build_auxiliary_data(root_dir: Path) -> AuxiliaryData: """读取固定文件,并缓存批次/快照等不会频繁变化的上下文。""" - global _AUX_CACHE_KEY, _AUX_CACHE_VALUE + global _aux_cache_key, _aux_cache_value config_path = root_dir / "config" / "mxu-MaaEnd.json" tauri_path = root_dir / "debug" / "mxu-tauri.log" @@ -543,8 +549,8 @@ def _build_auxiliary_data(root_dir: Path) -> AuxiliaryData: cache_key_parts.append((str(path.resolve()), -1, -1)) cache_key = tuple(cache_key_parts) - if _AUX_CACHE_KEY == cache_key and _AUX_CACHE_VALUE is not None: - return _AUX_CACHE_VALUE + if _aux_cache_key == cache_key and _aux_cache_value is not None: + return _aux_cache_value config_data: dict[str, Any] | None = None if config_path.is_file(): @@ -561,8 +567,8 @@ def _build_auxiliary_data(root_dir: Path) -> AuxiliaryData: task_id_to_entry=_load_go_service_mapping(_read_lines(go_service_path)), ) - _AUX_CACHE_KEY = cache_key - _AUX_CACHE_VALUE = auxiliary_data + _aux_cache_key = cache_key + _aux_cache_value = auxiliary_data return auxiliary_data @@ -576,7 +582,7 @@ def _collect_run_context(lines: list[str]) -> RunContext: for raw_line in lines: line = raw_line.rstrip("\r\n") - thread_id = _extract_thread_id(line) + _extract_thread_id(line) wrapped_agent_match = WRAPPED_AGENT_RE.match(line) if wrapped_agent_match is not None: @@ -1089,7 +1095,7 @@ def parse_log(root_dir: Path, lines: list[str]) -> list[str]: else "" ) current_selected_task_id = "" - current_maa_task_id = "" + current_maa_task_id: str = "" thread_selected_task_id: dict[str, str] = {} pending_panel_time = "" pending_panel_lines: list[str] = [] @@ -1164,17 +1170,18 @@ def flush_panel() -> None: task_id_alt_match = TEXT_TASK_ID_ALT_RE.search(message) task_id = "" if task_id_match is not None: - task_id = task_id_match.group(1).strip() + task_id = cast(str, task_id_match.group(1).strip()) elif task_id_alt_match is not None: - task_id = task_id_alt_match.group(1).strip() + task_id = cast(str, task_id_alt_match.group(1).strip()) if task_id: current_maa_task_id = task_id + current_maa_task_id_str: str = current_maa_task_id current_selected_task_id = _inherit_task_mapping_from_thread( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, thread_id=thread_id, current_selected_task_id=_resolve_selected_task_id( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, task_id_to_selected_task_id=task_id_to_selected_task_id, entry_to_selected_task_id=entry_to_selected_task_id, @@ -1186,11 +1193,11 @@ def flush_panel() -> None: message = _append_unresolved_diagnosis( message=message, timestamp_text=timestamp_text, - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, known_task_ids=known_task_ids, earliest_post_task_time=earliest_post_task_time, - should_diagnose=bool(current_maa_task_id), + should_diagnose=bool(current_maa_task_id_str), ) last_emit_key, last_emit_time = _emit_log_line( result, @@ -1216,11 +1223,12 @@ def flush_panel() -> None: entry = _extract_entry(details, simple_json_match.group(2)) if task_id: current_maa_task_id = task_id + current_maa_task_id_str = current_maa_task_id current_selected_task_id = _inherit_task_mapping_from_thread( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, thread_id=thread_id, current_selected_task_id=_resolve_selected_task_id( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, task_id_to_selected_task_id=task_id_to_selected_task_id, entry=entry, @@ -1243,7 +1251,7 @@ def flush_panel() -> None: message = _append_unresolved_diagnosis( message=message, timestamp_text=timestamp_text, - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, known_task_ids=known_task_ids, earliest_post_task_time=earliest_post_task_time, @@ -1280,11 +1288,12 @@ def flush_panel() -> None: entry = _extract_entry(json_line, line) if task_id: current_maa_task_id = task_id + current_maa_task_id_str = current_maa_task_id current_selected_task_id = _inherit_task_mapping_from_thread( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, thread_id=thread_id, current_selected_task_id=_resolve_selected_task_id( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, task_id_to_selected_task_id=task_id_to_selected_task_id, entry=entry, @@ -1304,7 +1313,7 @@ def flush_panel() -> None: message = _append_unresolved_diagnosis( message=message, timestamp_text=timestamp_text, - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, known_task_ids=known_task_ids, earliest_post_task_time=earliest_post_task_time, @@ -1334,11 +1343,12 @@ def flush_panel() -> None: entry = _extract_entry(details, event_match.group(3)) if task_id: current_maa_task_id = task_id + current_maa_task_id_str = current_maa_task_id current_selected_task_id = _inherit_task_mapping_from_thread( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, thread_id=thread_id, current_selected_task_id=_resolve_selected_task_id( - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, task_id_to_selected_task_id=task_id_to_selected_task_id, entry=entry, @@ -1374,7 +1384,8 @@ def flush_panel() -> None: ) param_action_text = "" if isinstance(details.get("param"), dict): - param_action = details["param"].get("action") + param_dict = cast(dict[str, Any], details["param"]) + param_action = param_dict.get("action") if isinstance(param_action, str): param_action_text = param_action if ( @@ -1392,19 +1403,24 @@ def flush_panel() -> None: focus = details.get("focus") if not isinstance(focus, dict): continue - focus_entry = focus.get(event_name) + focus_dict = cast(dict[str, Any], focus) + focus_entry = focus_dict.get(event_name) if isinstance(focus_entry, str): message = focus_entry elif isinstance(focus_entry, dict): - content = focus_entry.get("content") + focus_entry_dict = cast(dict[str, Any], focus_entry) + content = focus_entry_dict.get("content") if not isinstance(content, str) or not content: continue - display = focus_entry.get("display") + display = focus_entry_dict.get("display") display_list: list[str] = [] if isinstance(display, str): display_list = [display] elif isinstance(display, list): - display_list = [item for item in display if isinstance(item, str)] + display_items = cast(list[Any], display) + display_list = [ + item for item in display_items if isinstance(item, str) + ] if display_list and not any( item in ("log", "dialog", "modal") for item in display_list ): @@ -1425,7 +1441,7 @@ def flush_panel() -> None: message = _append_unresolved_diagnosis( message=message, timestamp_text=timestamp_text, - task_id=current_maa_task_id, + task_id=current_maa_task_id_str, current_selected_task_id=current_selected_task_id, known_task_ids=known_task_ids, earliest_post_task_time=earliest_post_task_time, diff --git a/app/task/SRC/AutoProxy.py b/app/task/SRC/AutoProxy.py index 38abe885..d555a41f 100644 --- a/app/task/SRC/AutoProxy.py +++ b/app/task/SRC/AutoProxy.py @@ -424,7 +424,7 @@ async def set_src(self, emulator_info: DeviceInfo) -> None: else None ), ) - logger.info(f"脚本运行参数配置完成: 自动代理") + logger.info("脚本运行参数配置完成: 自动代理") async def check_log(self, log_content: list[str], latest_time: datetime) -> None: """日志回调""" diff --git a/app/task/SRC/ManualReview.py b/app/task/SRC/ManualReview.py index 35d9679a..86ace2ce 100644 --- a/app/task/SRC/ManualReview.py +++ b/app/task/SRC/ManualReview.py @@ -24,6 +24,7 @@ import asyncio from pathlib import Path from datetime import datetime +from typing import Any, cast from app.core import Config, Broadcast from app.models.task import TaskExecuteBase, ScriptItem @@ -75,7 +76,7 @@ async def check(self) -> str: return "Pass" async def prepare(self): - self.message_queue = asyncio.Queue() + self.message_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() await Broadcast.subscribe(self.message_queue) self.wait_event = asyncio.Event() @@ -140,8 +141,13 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if not result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if not choice: break continue @@ -181,8 +187,13 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if not result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if not choice: break if self.run_book["SignIn"]: @@ -204,15 +215,20 @@ async def main_task(self): "options": ["是", "否"], }, ) - result = await self._wait_for_user_response(uid) - if result.get("data", {}).get("choice", False): + result: dict[str, Any] = await self._wait_for_user_response(uid) + data = result.get("data") + choice = False + if isinstance(data, dict): + data_dict = cast(dict[str, Any], data) + choice = bool(data_dict.get("choice")) + if choice: self.run_book["PassCheck"] = True - async def _wait_for_user_response(self, message_id: str): + async def _wait_for_user_response(self, message_id: str) -> dict[str, Any]: """等待用户交互响应""" logger.info(f"等待客户端回应消息: {message_id}") while True: - message = await self.message_queue.get() + message: dict[str, Any] = await self.message_queue.get() if message.get("id") == message_id and message.get("type") == "Response": self.message_queue.task_done() logger.success(f"收到客户端回应消息: {message_id}") diff --git a/app/task/SRC/tools/notify.py b/app/task/SRC/tools/notify.py index 2075bcfc..8e9b3d37 100644 --- a/app/task/SRC/tools/notify.py +++ b/app/task/SRC/tools/notify.py @@ -22,12 +22,16 @@ from app.services import Notify from app.utils import get_logger from app.models import SrcUserConfig +from typing import Any logger = get_logger("SRC通知工具") async def push_notification( - mode: str, title: str, message: dict, user_config: SrcUserConfig | None + mode: str, + title: str, + message: dict[str, Any], + user_config: SrcUserConfig | None, ) -> None: """通过所有渠道推送通知""" diff --git a/app/task/SRC/tools/poor_yaml.py b/app/task/SRC/tools/poor_yaml.py index a716d26f..a11e7f29 100644 --- a/app/task/SRC/tools/poor_yaml.py +++ b/app/task/SRC/tools/poor_yaml.py @@ -27,11 +27,12 @@ import re from pathlib import Path +from typing import Any from app.utils import decode_bytes -def poor_yaml_read(file: Path) -> dict: +def poor_yaml_read(file: Path) -> dict[str, Any]: """ Poor implementation to load yaml without pyyaml dependency, but with re @@ -42,7 +43,7 @@ def poor_yaml_read(file: Path) -> dict: dict: """ content = decode_bytes(file.read_bytes()) - data = {} + data: dict[str, Any] = {} regex = re.compile(r"^(.*?):(.*?)$") for line in content.splitlines(): line = line.strip("\n\r\t ").replace("\\", "/") @@ -65,7 +66,9 @@ def poor_yaml_read(file: Path) -> dict: return data -def poor_yaml_write(data: dict, file: Path, template_file: Path | None = None): +def poor_yaml_write( + data: dict[str, Any], file: Path, template_file: Path | None = None +) -> None: """ Args: data (dict): diff --git a/app/task/general/AutoProxy.py b/app/task/general/AutoProxy.py index 7741f7d7..b6524ed0 100644 --- a/app/task/general/AutoProxy.py +++ b/app/task/general/AutoProxy.py @@ -213,7 +213,7 @@ async def main_task(self): "脚本前任务", ) - self.script_info.log = f"正在启动游戏 / 模拟器" + self.script_info.log = "正在启动游戏 / 模拟器" # 启动游戏/模拟器 if self.game_manager is not None: try: @@ -240,7 +240,7 @@ async def main_task(self): await asyncio.sleep( self.script_config.get("Game", "WaitTime") ) - elif isinstance(self.game_manager, DeviceBase): + else: logger.info( f"启动模拟器: {self.script_config.get('Game', 'EmulatorIndex')}" ) @@ -406,7 +406,7 @@ async def kill_managed_process(self): "Game", "Type" ) == "Client" and self.script_config.get("Game", "IfForceClose"): await System.kill_process(self.game_path) - elif isinstance(self.game_manager, DeviceBase): + else: await self.game_manager.close( self.script_config.get("Game", "EmulatorIndex"), ) @@ -415,7 +415,7 @@ async def kill_managed_process(self): async def set_general(self) -> None: """配置通用脚本运行参数""" - logger.info(f"开始配置脚本运行参数: 自动代理") + logger.info("开始配置脚本运行参数: 自动代理") # 配置前关闭可能未正常退出的脚本进程 await System.kill_process(self.script_exe_path) @@ -436,7 +436,7 @@ async def set_general(self) -> None: self.script_config_path, ) - logger.info(f"脚本运行参数配置完成: 自动代理") + logger.info("脚本运行参数配置完成: 自动代理") async def check_log(self, log_content: list[str], latest_time: datetime) -> None: """日志回调""" diff --git a/app/task/general/tools/notify.py b/app/task/general/tools/notify.py index ffdcaf6f..057067dd 100644 --- a/app/task/general/tools/notify.py +++ b/app/task/general/tools/notify.py @@ -23,12 +23,16 @@ from app.services import Notify from app.utils import get_logger from app.models import GeneralUserConfig +from typing import Any logger = get_logger("通用通知工具") async def push_notification( - mode: str, title: str, message: dict, user_config: GeneralUserConfig | None + mode: str, + title: str, + message: dict[str, Any], + user_config: GeneralUserConfig | None, ) -> None: """通过所有渠道推送通知""" diff --git a/app/utils/ImageUtils.py b/app/utils/ImageUtils.py index 4778b9b9..5d0ecc21 100644 --- a/app/utils/ImageUtils.py +++ b/app/utils/ImageUtils.py @@ -23,31 +23,32 @@ import base64 import hashlib from pathlib import Path +from typing import Union from PIL import Image class ImageUtils: @staticmethod - def get_base64_from_file(image_path): + def get_base64_from_file(image_path: Union[str, Path]) -> str: """从本地文件读取并返回base64编码字符串""" with open(image_path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") @staticmethod - def calculate_md5_from_file(image_path): + def calculate_md5_from_file(image_path: Union[str, Path]) -> str: """从本地文件读取并返回md5值(hex字符串)""" with open(image_path, "rb") as f: return hashlib.md5(f.read()).hexdigest() @staticmethod - def calculate_md5_from_base64(base64_content): + def calculate_md5_from_base64(base64_content: str) -> str: """从base64字符串计算md5""" image_data = base64.b64decode(base64_content) return hashlib.md5(image_data).hexdigest() @staticmethod - def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path: + def compress_image_if_needed(image_path: Path, max_size_mb: float = 2) -> Path: """ 如果图片大于max_size_mb, 则压缩并覆盖原文件, 返回原始路径(Path对象) """ diff --git a/app/utils/LogMonitor.py b/app/utils/LogMonitor.py index 0d6b5f6d..683f9064 100644 --- a/app/utils/LogMonitor.py +++ b/app/utils/LogMonitor.py @@ -21,11 +21,12 @@ import asyncio import aiofiles +import os from contextlib import suppress from datetime import datetime, timedelta, date from copy import copy from pathlib import Path -from typing import Callable, Optional, Awaitable +from typing import Callable, Optional, Awaitable, Any, cast from .constants import TIME_FIELDS, ANSI_ESCAPE_RE from .logger import get_logger @@ -40,14 +41,14 @@ def strptime(date_string: str, format: str, default_date: datetime) -> datetime: date = datetime.strptime(date_string, format) # 构建参数字典 - datetime_kwargs = {} + datetime_kwargs: dict[str, int] = {} for format_code, field_name in TIME_FIELDS.items(): if format_code in format: - datetime_kwargs[field_name] = getattr(date, field_name) + datetime_kwargs[field_name] = int(getattr(date, field_name)) else: - datetime_kwargs[field_name] = getattr(default_date, field_name) + datetime_kwargs[field_name] = int(getattr(default_date, field_name)) - return datetime(**datetime_kwargs) + return datetime(**cast(dict[str, Any], datetime_kwargs)) class LogMonitor: @@ -68,7 +69,8 @@ def __init__( self.last_callback_time: datetime = datetime.now() self.log_contents: list[str] = [] self.latest_time = datetime.now() - self.task: Optional[asyncio.Task] = None + self.last_log: str = "" + self.task: Optional[asyncio.Task[None]] = None async def monitor_file( self, @@ -86,10 +88,10 @@ async def monitor_file( warned_mtime_date: date | None = None if_log_start = False offset = 0 - log_contents = [] + log_contents: list[str] = [] + log_stat: os.stat_result | None = None while True: - # 检查文件是否仍然存在 if not log_file_path.exists(): logger.warning(f"日志文件不存在: {log_file_path}") @@ -112,9 +114,8 @@ async def monitor_file( # 尝试读取文件 try: - # 发生日志轮转或文件被替换,重置监控状态并加载被轮换的旧日志 - if ( + if log_stat is not None and ( log_stat.st_ino != log_file_path.stat().st_ino or log_stat.st_size > log_file_path.stat().st_size ): @@ -141,7 +142,6 @@ async def monitor_file( log_stat = log_file_path.stat() if log_stat.st_size <= offset: - # 日志无变化超时调用回调 if datetime.now() - self.last_callback_time > timedelta(minutes=1): await self.do_callback() @@ -194,7 +194,6 @@ async def monitor_process(self, process: asyncio.subprocess.Process): self.log_contents = [] while True: - try: bline = await asyncio.wait_for(process.stdout.readline(), timeout=60) except asyncio.TimeoutError: @@ -226,7 +225,6 @@ async def do_callback(self): logger.error(f"回调函数执行失败: {e}") async def update_latest_timestamp(self, log: str, if_init: bool = False) -> None: - if if_init: self.last_log = log self.latest_time = datetime.now() diff --git a/app/utils/OCR/OCRtool.py b/app/utils/OCR/OCRtool.py index 8d461858..7872c421 100644 --- a/app/utils/OCR/OCRtool.py +++ b/app/utils/OCR/OCRtool.py @@ -5,23 +5,17 @@ from PIL import Image import win32con import win32gui -from rapidocr_onnxruntime import RapidOCR +from rapidocr_onnxruntime import RapidOCR # pyright: ignore[reportMissingTypeStubs] from mss import mss import subprocess from pathlib import Path +from typing import Any, cast from app.utils import get_logger from app.utils.exception import ( WindowsNotFoundException, WindowsNotFocusException, OCRNotFoundTitleException, - ADBFileNotFoundException, - ADBCommandFailedException, - ADBDeviceNotFoundException, - ADBConnectionFailedException, - ADBTimeoutException, - ADBScreenshotException, - ImageProcessException ) # OCR入门指南! @@ -44,6 +38,8 @@ # 你现在已经学会了OCR识别的基础知识了!快来试试吧! logger = get_logger("OCR模块") + + class OCRTool: # 默认宽高比 16:9,用于图像预处理 aspect_ratio_width = 16 @@ -61,7 +57,7 @@ class OCRTool: # 全局窗口标题,用于避免在方法调用时反复传入 title: str | None = None - def __init__(self, width=16, height=9, title: str | None = None): + def __init__(self, width: int = 16, height: int = 9, title: str | None = None): """ 初始化 OCR 引擎 @@ -111,7 +107,7 @@ def get_system_dpi_scaling(self) -> float: pass # 获取主显示器 DC - hdc = win32gui.GetDC(0) + hdc: Any = win32gui.GetDC(0) try: # 使用 ctypes 直接调用 GetDeviceCaps # LOGPIXELSX = 88 @@ -127,10 +123,11 @@ def get_system_dpi_scaling(self) -> float: logger.warning(f"获取系统 DPI 失败: {e},使用默认缩放比例 1.5") return 1.5 - # ========== 截图部分 ========== @classmethod - def get_screenshot_region(cls, title: str, should_preprocess: bool = True) -> tuple[int, int, int, int]: + def get_screenshot_region( + cls, title: str, should_preprocess: bool = True + ) -> tuple[int, int, int, int]: """ 根据给定的窗口标题获取截图区域。 @@ -199,12 +196,18 @@ def get_screenshot_region(cls, title: str, should_preprocess: bool = True) -> tu # 扣除边框后的实际可用宽高 available_width = width - left_offset * 2 # 左右两侧都要扣除 - available_height = height - top_offset - int(8 * cls.zoom) # 顶部标题栏 + 底部边框 + available_height = ( + height - top_offset - int(8 * cls.zoom) + ) # 顶部标题栏 + 底部边框 # 计算截图区域的宽度,将宽度调整为 相应 的倍数 - cls.area_width = available_width // cls.aspect_ratio_width * cls.aspect_ratio_width + cls.area_width = ( + available_width // cls.aspect_ratio_width * cls.aspect_ratio_width + ) # 计算截图区域的高度,将高度调整为 相应 的倍数 - cls.area_height = available_height // cls.aspect_ratio_height * cls.aspect_ratio_height + cls.area_height = ( + available_height // cls.aspect_ratio_height * cls.aspect_ratio_height + ) # 根据缩放比例调整顶部坐标,如果顶部坐标不为 0 则加上偏移量 cls.area_top = top + top_offset # 根据缩放比例调整左侧坐标,如果左侧坐标不为 0 则加上偏移量 @@ -232,7 +235,7 @@ def _verify_window_activated(cls, hwnd: int, title: str) -> None: return # 重试一次 - logger.warning(f"窗口未激活,再次尝试强制激活...") + logger.warning("窗口未激活,再次尝试强制激活...") cls._force_activate_window(hwnd) time.sleep(0.2) @@ -268,7 +271,9 @@ def _force_activate_window(hwnd: int) -> None: # 获取前台窗口的线程ID和进程ID if foreground_hwnd: - foreground_thread_id, _ = win32process.GetWindowThreadProcessId(foreground_hwnd) + foreground_thread_id, _ = win32process.GetWindowThreadProcessId( + foreground_hwnd + ) else: foreground_thread_id = 0 @@ -280,9 +285,13 @@ def _force_activate_window(hwnd: int) -> None: if foreground_thread_id != target_thread_id and foreground_thread_id != 0: try: # 附着到前台窗口的输入线程 - win32process.AttachThreadInput(foreground_thread_id, target_thread_id, True) + win32process.AttachThreadInput( + foreground_thread_id, target_thread_id, True + ) attached = True - logger.debug(f"已附着输入线程: {foreground_thread_id} -> {target_thread_id}") + logger.debug( + f"已附着输入线程: {foreground_thread_id} -> {target_thread_id}" + ) except Exception as e: logger.warning(f"附着输入线程失败: {e}") @@ -311,7 +320,9 @@ def _force_activate_window(hwnd: int) -> None: # 分离输入线程(必须在 finally 中执行,确保一定会分离) if attached: try: - win32process.AttachThreadInput(foreground_thread_id, target_thread_id, False) + win32process.AttachThreadInput( + foreground_thread_id, target_thread_id, False + ) logger.debug("已分离输入线程") except Exception as e: logger.warning(f"分离输入线程失败: {e}") @@ -320,7 +331,6 @@ def _force_activate_window(hwnd: int) -> None: logger.error(f"强制激活窗口出错: {e}") # 即使出错也不抛出异常,因为可能已经部分成功 - @staticmethod def _find_window_with_win32gui(title: str) -> int | None: """ @@ -334,7 +344,7 @@ def _find_window_with_win32gui(title: str) -> int | None: """ found_windows = [] - def enum_callback(hwnd, results): + def enum_callback(hwnd: int, results: list[tuple[int, str]]) -> None: if win32gui.IsWindowVisible(hwnd): window_title = win32gui.GetWindowText(hwnd) if title.lower() in window_title.lower(): @@ -355,7 +365,12 @@ def enum_callback(hwnd, results): return None @classmethod - def get_screenshot_with_pc(cls, title: str, should_preprocess: bool = True, region: tuple[int, int, int, int] | None = None) -> Image.Image: + def get_screenshot_with_pc( + cls, + title: str, + should_preprocess: bool = True, + region: tuple[int, int, int, int] | None = None, + ) -> Image.Image: """ 根据指定的窗口标题和区域获取截图,并对截图进行尺寸调整。 @@ -379,7 +394,9 @@ def get_screenshot_with_pc(cls, title: str, should_preprocess: bool = True, regi return cls._image_resize(pillow_img) @classmethod - def get_screenshot_with_adb(cls, adb_path: str, serial: str, use_screencap: bool = True) -> Image.Image: + def get_screenshot_with_adb( + cls, adb_path: str, serial: str, use_screencap: bool = True + ) -> Image.Image: """ 实验性,未测试 通过 ADB 端口获取设备截图。 @@ -434,23 +451,18 @@ def _ensure_adb_device_connected(adb_path: str, serial: str) -> None: # 检查设备是否已连接 try: cmd = [adb_path, "devices"] - result = subprocess.run( - cmd, - capture_output=True, - timeout=10, - check=False - ) + result = subprocess.run(cmd, capture_output=True, timeout=10, check=False) if result.returncode != 0: - raise RuntimeError(f"无法执行 adb devices 命令") + raise RuntimeError("无法执行 adb devices 命令") - devices_output = result.stdout.decode('utf-8', errors='ignore') + devices_output = result.stdout.decode("utf-8", errors="ignore") logger.debug(f"ADB devices 输出:\n{devices_output}") # 检查设备是否在列表中且状态为 device is_connected = False - for line in devices_output.split('\n'): - if serial in line and 'device' in line and 'offline' not in line: + for line in devices_output.split("\n"): + if serial in line and "device" in line and "offline" not in line: is_connected = True break @@ -459,35 +471,37 @@ def _ensure_adb_device_connected(adb_path: str, serial: str) -> None: return # 如果是网络设备格式(IP:Port),尝试连接 - if ':' in serial: + if ":" in serial: logger.info(f"设备 {serial} 未连接,尝试执行 adb connect...") connect_cmd = [adb_path, "connect", serial] connect_result = subprocess.run( - connect_cmd, - capture_output=True, - timeout=10, - check=False + connect_cmd, capture_output=True, timeout=10, check=False ) if connect_result.returncode != 0: - error_msg = connect_result.stderr.decode('utf-8', errors='ignore') + error_msg = connect_result.stderr.decode("utf-8", errors="ignore") raise RuntimeError(f"adb connect 失败: {error_msg}") - connect_output = connect_result.stdout.decode('utf-8', errors='ignore') + connect_output = connect_result.stdout.decode("utf-8", errors="ignore") logger.info(f"adb connect 输出: {connect_output}") # 再次检查是否连接成功 - if 'connected' in connect_output.lower() or 'already connected' in connect_output.lower(): + if ( + "connected" in connect_output.lower() + or "already connected" in connect_output.lower() + ): logger.info(f"设备 {serial} 连接成功") return else: raise RuntimeError(f"连接设备失败: {connect_output}") else: # USB 设备但未找到 - raise RuntimeError(f"设备 {serial} 未找到,请确保设备已连接并启用 USB 调试") + raise RuntimeError( + f"设备 {serial} 未找到,请确保设备已连接并启用 USB 调试" + ) except subprocess.TimeoutExpired: - raise RuntimeError(f"ADB 命令超时") + raise RuntimeError("ADB 命令超时") except Exception as e: logger.error(f"检查/连接设备失败: {e}") raise @@ -510,16 +524,17 @@ def _adb_screencap_png(adb_path: str, serial: str) -> Image.Image: try: # 执行 adb shell screencap -p 并直接捕获输出 cmd = [adb_path, "-s", serial, "shell", "screencap", "-p"] - result = subprocess.run( - cmd, - capture_output=True, - timeout=30, - check=False - ) + result = subprocess.run(cmd, capture_output=True, timeout=30, check=False) if result.returncode != 0: - error_msg = result.stderr.decode('utf-8', errors='ignore') if result.stderr else "未知错误" - raise RuntimeError(f"ADB screencap 命令失败 (返回码: {result.returncode}): {error_msg}") + error_msg = ( + result.stderr.decode("utf-8", errors="ignore") + if result.stderr + else "未知错误" + ) + raise RuntimeError( + f"ADB screencap 命令失败 (返回码: {result.returncode}): {error_msg}" + ) # 从二进制数据创建图像 image_data = result.stdout @@ -529,15 +544,18 @@ def _adb_screencap_png(adb_path: str, serial: str) -> Image.Image: # Windows 环境下需要处理换行符问题 # screencap -p 在 Windows 上会将 \n (0x0A) 转换为 \r\n (0x0D 0x0A) # 这会破坏 PNG 文件格式,需要将 \r\n 替换回 \n - image_data = image_data.replace(b'\r\n', b'\n') + image_data = image_data.replace(b"\r\n", b"\n") logger.debug(f"ADB screencap 返回数据大小: {len(image_data)} 字节") # 使用 PIL 从字节流加载图像 from io import BytesIO + try: pillow_img = Image.open(BytesIO(image_data)) - logger.info(f"成功通过 ADB screencap 获取截图 (设备: {serial}, 尺寸: {pillow_img.size})") + logger.info( + f"成功通过 ADB screencap 获取截图 (设备: {serial}, 尺寸: {pillow_img.size})" + ) return pillow_img except Exception as img_error: # 如果 PNG 方法失败,记录详细信息并尝试降级到 raw 方法 @@ -576,16 +594,17 @@ def _adb_screencap_raw(adb_path: str, serial: str) -> Image.Image: try: # 执行 adb shell screencap(原始格式) cmd = [adb_path, "-s", serial, "shell", "screencap"] - result = subprocess.run( - cmd, - capture_output=True, - timeout=30, - check=False - ) + result = subprocess.run(cmd, capture_output=True, timeout=30, check=False) if result.returncode != 0: - error_msg = result.stderr.decode('utf-8', errors='ignore') if result.stderr else "未知错误" - raise RuntimeError(f"ADB screencap raw 命令失败 (返回码: {result.returncode}): {error_msg}") + error_msg = ( + result.stderr.decode("utf-8", errors="ignore") + if result.stderr + else "未知错误" + ) + raise RuntimeError( + f"ADB screencap raw 命令失败 (返回码: {result.returncode}): {error_msg}" + ) raw_data = result.stdout if len(raw_data) < 12: @@ -593,9 +612,11 @@ def _adb_screencap_raw(adb_path: str, serial: str) -> Image.Image: # 解析头部信息(前 12 字节) # 格式: width (4 bytes), height (4 bytes), format (4 bytes) - width, height, pixel_format = struct.unpack(' Image.Image: # 计算预期的数据大小(RGBA 每像素 4 字节) expected_size = width * height * 4 + 12 if len(raw_data) < expected_size: - raise RuntimeError(f"ADB screencap raw 数据不完整 (预期: {expected_size}, 实际: {len(raw_data)})") + raise RuntimeError( + f"ADB screencap raw 数据不完整 (预期: {expected_size}, 实际: {len(raw_data)})" + ) # 提取像素数据(跳过前 12 字节的头部) - pixel_data = raw_data[12:12 + width * height * 4] + pixel_data = raw_data[12 : 12 + width * height * 4] # 创建 PIL 图像(RGBA 格式) - pillow_img = Image.frombytes('RGBA', (width, height), pixel_data) + pillow_img = Image.frombytes("RGBA", (width, height), pixel_data) # 转换为 RGB(去除 Alpha 通道) - pillow_img = pillow_img.convert('RGB') + pillow_img = pillow_img.convert("RGB") - logger.info(f"成功通过 ADB screencap raw 获取截图 (设备: {serial}, 尺寸: {pillow_img.size})") + logger.info( + f"成功通过 ADB screencap raw 获取截图 (设备: {serial}, 尺寸: {pillow_img.size})" + ) return pillow_img except subprocess.TimeoutExpired: @@ -641,12 +666,7 @@ def _capture_screenshot_mss(region: tuple[int, int, int, int]) -> Image.Image: left, top, width, height = region # MSS 使用 (left, top, right, bottom) 格式的 monitor 字典 - monitor = { - "left": left, - "top": top, - "width": width, - "height": height - } + monitor = {"left": left, "top": top, "width": width, "height": height} try: with mss() as sct: @@ -654,7 +674,9 @@ def _capture_screenshot_mss(region: tuple[int, int, int, int]) -> Image.Image: screenshot = sct.grab(monitor) # 转换为 Pillow 图像 pillow_img = Image.frombytes("RGB", screenshot.size, screenshot.rgb) - logger.debug(f"使用 MSS 成功截图: 区域={region}, 尺寸={pillow_img.size}") + logger.debug( + f"使用 MSS 成功截图: 区域={region}, 尺寸={pillow_img.size}" + ) return pillow_img except Exception as e: logger.error(f"MSS 截图失败: {e},尝试使用 pyautogui 作为备用方案") @@ -665,7 +687,6 @@ def _capture_screenshot_mss(region: tuple[int, int, int, int]) -> Image.Image: logger.error(f"pyautogui 备用截图也失败: {fallback_error}") raise - @classmethod def _image_resize(cls, pillow_image: Image.Image) -> Image.Image: """ @@ -678,18 +699,23 @@ def _image_resize(cls, pillow_image: Image.Image) -> Image.Image: Returns: Image.Image: 调整尺寸后的 Pillow 图像对象。 """ - if pillow_image.width % cls.area_width==0 and pillow_image.height % cls.area_height==0: + if ( + pillow_image.width % cls.area_width == 0 + and pillow_image.height % cls.area_height == 0 + ): return pillow_image cls.screenshot_proportion = 1920 / pillow_image.width resized_image = pillow_image.resize( - (int(pillow_image.width * cls.screenshot_proportion), - int(pillow_image.height * cls.screenshot_proportion)), - Image.Resampling.BICUBIC) + ( + int(pillow_image.width * cls.screenshot_proportion), + int(pillow_image.height * cls.screenshot_proportion), + ), + Image.Resampling.BICUBIC, + ) return resized_image - @classmethod - def _location_calculator(cls, x, y): + def _location_calculator(cls, x: float, y: float) -> tuple[float, float]: """ 根据截图缩放比例和截图区域偏移量,计算实际屏幕坐标。 @@ -701,11 +727,16 @@ def _location_calculator(cls, x, y): tuple[int, int]: 实际屏幕上的坐标 (x, y)。 """ cls.location_proportion = 1 / cls.screenshot_proportion - return x * cls.location_proportion + cls.area_left, y * cls.location_proportion + cls.area_top + return ( + x * cls.location_proportion + cls.area_left, + y * cls.location_proportion + cls.area_top, + ) # ========== 图像匹配与查找部分 ========== @classmethod - def _find_template_in_screenshot(cls, screenshot: Image.Image, template_path: str, threshold: float = 0.8) -> tuple[bool, tuple[int, int] | None]: + def _find_template_in_screenshot( + cls, screenshot: Image.Image, template_path: str, threshold: float = 0.8 + ) -> tuple[bool, tuple[int, int] | None]: """ 在截图中查找模板图像。 @@ -720,18 +751,19 @@ def _find_template_in_screenshot(cls, screenshot: Image.Image, template_path: st """ try: # 读取模板图像 - template = cv2.imread(template_path) + template = cast(Any, cv2.imread(template_path)) if template is None: logger.error(f"无法读取模板图像: {template_path}") return False, None # 将 Pillow 图像转换为 OpenCV 格式 import numpy as np + screenshot_cv = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) # 执行模板匹配 result = cv2.matchTemplate(screenshot_cv, template, cv2.TM_CCOEFF_NORMED) - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) + _, max_val, _, max_loc = cv2.minMaxLoc(result) # 判断是否匹配成功 if max_val >= threshold: @@ -739,10 +771,14 @@ def _find_template_in_screenshot(cls, screenshot: Image.Image, template_path: st template_h, template_w = template.shape[:2] center_x = max_loc[0] + template_w // 2 center_y = max_loc[1] + template_h // 2 - logger.debug(f"找到模板图像 {template_path},匹配度: {max_val:.2f},位置: ({center_x}, {center_y})") + logger.debug( + f"找到模板图像 {template_path},匹配度: {max_val:.2f},位置: ({center_x}, {center_y})" + ) return True, (center_x, center_y) else: - logger.debug(f"未找到模板图像 {template_path},最高匹配度: {max_val:.2f}") + logger.debug( + f"未找到模板图像 {template_path},最高匹配度: {max_val:.2f}" + ) return False, None except Exception as e: @@ -750,7 +786,14 @@ def _find_template_in_screenshot(cls, screenshot: Image.Image, template_path: st return False, None @classmethod - def check(cls, image_path: str, title: str | None = None, interval: float = 0, retry_times: int = 1, threshold: float = 0.8) -> bool: + def check( + cls, + image_path: str, + title: str | None = None, + interval: float = 0, + retry_times: int = 1, + threshold: float = 0.8, + ) -> bool: """ 截图并查找是否存在图片内的内容。 @@ -772,7 +815,9 @@ def check(cls, image_path: str, title: str | None = None, interval: float = 0, r # 使用传入的 title 或类的全局 title window_title = title or cls.title if not window_title: - raise OCRNotFoundTitleException("必须提供 title 参数或通过 set_title() 设置全局 title") + raise OCRNotFoundTitleException( + "必须提供 title 参数或通过 set_title() 设置全局 title" + ) for attempt in range(retry_times): try: @@ -780,7 +825,9 @@ def check(cls, image_path: str, title: str | None = None, interval: float = 0, r screenshot = cls.get_screenshot_with_pc(window_title) # 查找模板 - found, _ = cls._find_template_in_screenshot(screenshot, image_path, threshold) + found, _ = cls._find_template_in_screenshot( + screenshot, image_path, threshold + ) if found: logger.info(f"在第 {attempt + 1} 次尝试中找到图像: {image_path}") @@ -791,7 +838,9 @@ def check(cls, image_path: str, title: str | None = None, interval: float = 0, r time.sleep(interval) except Exception as e: - logger.error(f"check 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}") + logger.error( + f"check 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}" + ) if attempt < retry_times - 1 and interval > 0: time.sleep(interval) @@ -799,7 +848,14 @@ def check(cls, image_path: str, title: str | None = None, interval: float = 0, r return False @classmethod - def check_any(cls, image_paths: list[str], title: str | None = None, interval: float = 0, retry_times: int = 1, threshold: float = 0.8) -> bool: + def check_any( + cls, + image_paths: list[str], + title: str | None = None, + interval: float = 0, + retry_times: int = 1, + threshold: float = 0.8, + ) -> bool: """ 截图并查找是否存在列表中任意一张图片的内容。 @@ -821,7 +877,9 @@ def check_any(cls, image_paths: list[str], title: str | None = None, interval: f # 使用传入的 title 或类的全局 title window_title = title or cls.title if not window_title: - raise OCRNotFoundTitleException("必须提供 title 参数或通过 set_title() 设置全局 title") + raise OCRNotFoundTitleException( + "必须提供 title 参数或通过 set_title() 设置全局 title" + ) for attempt in range(retry_times): try: @@ -830,9 +888,13 @@ def check_any(cls, image_paths: list[str], title: str | None = None, interval: f # 遍历所有模板图像 for image_path in image_paths: - found, _ = cls._find_template_in_screenshot(screenshot, image_path, threshold) + found, _ = cls._find_template_in_screenshot( + screenshot, image_path, threshold + ) if found: - logger.info(f"在第 {attempt + 1} 次尝试中找到图像: {image_path}") + logger.info( + f"在第 {attempt + 1} 次尝试中找到图像: {image_path}" + ) return True # 如果不是最后一次尝试,等待间隔时间 @@ -840,7 +902,9 @@ def check_any(cls, image_paths: list[str], title: str | None = None, interval: f time.sleep(interval) except Exception as e: - logger.error(f"check_any 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}") + logger.error( + f"check_any 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}" + ) if attempt < retry_times - 1 and interval > 0: time.sleep(interval) @@ -848,7 +912,14 @@ def check_any(cls, image_paths: list[str], title: str | None = None, interval: f return False @classmethod - def check_all(cls, image_paths: list[str], title: str | None = None, interval: float = 0, retry_times: int = 1, threshold: float = 0.8) -> bool: + def check_all( + cls, + image_paths: list[str], + title: str | None = None, + interval: float = 0, + retry_times: int = 1, + threshold: float = 0.8, + ) -> bool: """ 截图并查找是否存在列表中所有图片的内容。 @@ -870,7 +941,9 @@ def check_all(cls, image_paths: list[str], title: str | None = None, interval: f # 使用传入的 title 或类的全局 title window_title = title or cls.title if not window_title: - raise OCRNotFoundTitleException("必须提供 title 参数或通过 set_title() 设置全局 title") + raise OCRNotFoundTitleException( + "必须提供 title 参数或通过 set_title() 设置全局 title" + ) for attempt in range(retry_times): try: @@ -880,10 +953,14 @@ def check_all(cls, image_paths: list[str], title: str | None = None, interval: f # 检查所有模板图像 found_all = True for image_path in image_paths: - found, _ = cls._find_template_in_screenshot(screenshot, image_path, threshold) + found, _ = cls._find_template_in_screenshot( + screenshot, image_path, threshold + ) if not found: found_all = False - logger.debug(f"第 {attempt + 1} 次尝试中未找到图像: {image_path}") + logger.debug( + f"第 {attempt + 1} 次尝试中未找到图像: {image_path}" + ) break if found_all: @@ -895,7 +972,9 @@ def check_all(cls, image_paths: list[str], title: str | None = None, interval: f time.sleep(interval) except Exception as e: - logger.error(f"check_all 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}") + logger.error( + f"check_all 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}" + ) if attempt < retry_times - 1 and interval > 0: time.sleep(interval) @@ -904,7 +983,14 @@ def check_all(cls, image_paths: list[str], title: str | None = None, interval: f # ========== 点击操作部分 ========== @classmethod - def click_img(cls, image_path: str, title: str | None = None, interval: float = 0, retry_times: int = 1, threshold: float = 0.8) -> bool: + def click_img( + cls, + image_path: str, + title: str | None = None, + interval: float = 0, + retry_times: int = 1, + threshold: float = 0.8, + ) -> bool: """ 点击与图像一致的位置的坐标。 @@ -926,7 +1012,9 @@ def click_img(cls, image_path: str, title: str | None = None, interval: float = # 使用传入的 title 或类的全局 title window_title = title or cls.title if not window_title: - raise OCRNotFoundTitleException("必须提供 title 参数或通过 set_title() 设置全局 title") + raise OCRNotFoundTitleException( + "必须提供 title 参数或通过 set_title() 设置全局 title" + ) for attempt in range(retry_times): try: @@ -934,15 +1022,21 @@ def click_img(cls, image_path: str, title: str | None = None, interval: float = screenshot = cls.get_screenshot_with_pc(window_title) # 查找模板 - found, position = cls._find_template_in_screenshot(screenshot, image_path, threshold) + found, position = cls._find_template_in_screenshot( + screenshot, image_path, threshold + ) if found and position: # 将截图坐标转换为实际屏幕坐标 - screen_x, screen_y = cls._location_calculator(position[0], position[1]) + screen_x, screen_y = cls._location_calculator( + position[0], position[1] + ) # 执行点击 - pyautogui.click(screen_x, screen_y) - logger.info(f"成功点击图像 {image_path} 的位置: ({screen_x}, {screen_y})") + pyautogui.click(int(screen_x), int(screen_y)) + logger.info( + f"成功点击图像 {image_path} 的位置: ({screen_x}, {screen_y})" + ) return True # 如果不是最后一次尝试,等待间隔时间 @@ -950,7 +1044,9 @@ def click_img(cls, image_path: str, title: str | None = None, interval: float = time.sleep(interval) except Exception as e: - logger.error(f"click_img 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}") + logger.error( + f"click_img 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}" + ) if attempt < retry_times - 1 and interval > 0: time.sleep(interval) @@ -958,7 +1054,13 @@ def click_img(cls, image_path: str, title: str | None = None, interval: float = return False @classmethod - def click_txt(cls, text: str, title: str | None = None, interval: float = 0, retry_times: int = 1) -> bool: + def click_txt( + cls, + text: str, + title: str | None = None, + interval: float = 0, + retry_times: int = 1, + ) -> bool: """ 点击与文字一致的位置。 @@ -980,7 +1082,9 @@ def click_txt(cls, text: str, title: str | None = None, interval: float = 0, ret # 使用传入的 title 或类的全局 title window_title = title or cls.title if not window_title: - raise OCRNotFoundTitleException("必须提供 title 参数或通过 set_title() 设置全局 title") + raise OCRNotFoundTitleException( + "必须提供 title 参数或通过 set_title() 设置全局 title" + ) for attempt in range(retry_times): try: @@ -991,7 +1095,7 @@ def click_txt(cls, text: str, title: str | None = None, interval: float = 0, ret screenshot_np = np.array(screenshot) # 使用 OCR 识别文字 - result, elapse = cls().ocr_engine(screenshot_np) + result, _elapsed = cls().ocr_engine(screenshot_np) if result is None: logger.debug(f"第 {attempt + 1} 次尝试中未识别到任何文字") @@ -1000,22 +1104,28 @@ def click_txt(cls, text: str, title: str | None = None, interval: float = 0, ret continue # 遍历识别结果,查找匹配的文字 - for line in result: - detected_text = line[1] # OCR 识别的文字 + for line in cast(list[list[Any]], result): + detected_text = str(line[1]) # OCR 识别的文字 if text in detected_text: # 获取文字区域的边界框坐标 - box = line[0] # [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] + box = cast( + list[list[float]], line[0] + ) # [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] # 计算中心点 center_x = int((box[0][0] + box[2][0]) / 2) center_y = int((box[0][1] + box[2][1]) / 2) # 将截图坐标转换为实际屏幕坐标 - screen_x, screen_y = cls._location_calculator(center_x, center_y) + screen_x, screen_y = cls._location_calculator( + center_x, center_y + ) # 执行点击 - pyautogui.click(screen_x, screen_y) - logger.info(f"成功点击文字 '{text}' 的位置: ({screen_x}, {screen_y})") + pyautogui.click(int(screen_x), int(screen_y)) + logger.info( + f"成功点击文字 '{text}' 的位置: ({screen_x}, {screen_y})" + ) return True logger.debug(f"第 {attempt + 1} 次尝试中未找到文字: {text}") @@ -1025,10 +1135,11 @@ def click_txt(cls, text: str, title: str | None = None, interval: float = 0, ret time.sleep(interval) except Exception as e: - logger.error(f"click_txt 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}") + logger.error( + f"click_txt 方法执行失败 (尝试 {attempt + 1}/{retry_times}): {e}" + ) if attempt < retry_times - 1 and interval > 0: time.sleep(interval) logger.info(f"在 {retry_times} 次尝试后未能点击文字: {text}") return False - diff --git a/app/utils/__init__.py b/app/utils/__init__.py index e8f4abf5..20fc155e 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -21,7 +21,7 @@ # Contact: DLmaster_361@163.com -from .constants import * +from . import constants from .logger import get_logger from .ImageUtils import ImageUtils from .LogMonitor import LogMonitor, strptime diff --git a/app/utils/emulator/ldplayer.py b/app/utils/emulator/ldplayer.py index 7249c378..1c0df885 100644 --- a/app/utils/emulator/ldplayer.py +++ b/app/utils/emulator/ldplayer.py @@ -67,7 +67,7 @@ def __init__(self, config: EmulatorConfig) -> None: self.emulator_path = Path(config.get("Info", "Path")) - async def open(self, idx: str, package_name="") -> DeviceInfo: + async def open(self, idx: str, package_name: str = "") -> DeviceInfo: logger.info(f"开始启动模拟器 {idx} - {package_name}") status = DeviceStatus.UNKNOWN # 初始化status变量 diff --git a/app/utils/emulator/tools.py b/app/utils/emulator/tools.py index 28480e3f..3e619792 100644 --- a/app/utils/emulator/tools.py +++ b/app/utils/emulator/tools.py @@ -26,7 +26,7 @@ from maa.toolkit import Toolkit from contextlib import suppress from pathlib import Path -from typing import List, Dict +from typing import List, Dict, Any from app.utils.constants import EMULATOR_PATH_BOOK from app.utils import get_logger @@ -98,7 +98,7 @@ async def search_all_emulators() -> List[Dict[str, str]]: return found_emulators -async def _search_emulator(config: Dict) -> str: +async def _search_emulator(config: Dict[str, Any]) -> str: """搜索单类模拟器""" # 1. 从注册表搜索 diff --git a/app/utils/skland.py b/app/utils/skland.py index 1a4ffd90..4181965e 100644 --- a/app/utils/skland.py +++ b/app/utils/skland.py @@ -45,7 +45,7 @@ from Crypto.Cipher import PKCS1_v1_5, AES, DES from Crypto.Util.Padding import pad -from typing import Dict, Any +from typing import Dict, Any, cast from .constants import SKLAND_SM_CONFIG, BROWSER_ENV, DES_RULE from .logger import get_logger @@ -53,7 +53,7 @@ logger = get_logger("森空岛签到任务") -def get_proxy(proxy: str | None = None) -> str | None: +def get_proxy(proxy: str | None = None) -> Any: if proxy is not None: return proxy @@ -81,7 +81,7 @@ def get_sm_id() -> str: def get_tn(obj: Dict[str, Any]) -> str: """计算tn值""" sorted_keys = sorted(obj.keys()) - result_list = [] + result_list: list[str] = [] for key in sorted_keys: v = obj[key] @@ -119,7 +119,7 @@ def encrypt_des(message: str, key: str) -> str: message_bytes = str(message).encode() while len(message_bytes) % 8 != 0: message_bytes += b"\0" - cipher = DES.new(key_bytes, DES.MODE_ECB) + cipher = cast(Any, DES).new(key_bytes, DES.MODE_ECB) encrypted = cipher.encrypt(message_bytes) return base64.b64encode(encrypted).decode() @@ -138,7 +138,7 @@ def encrypt_aes(message: str, key: str) -> str: """AES CBC加密""" iv = b"0102030405060708" key_bytes = key.encode()[:16].ljust(16, b"\0") - cipher = AES.new(key_bytes, AES.MODE_CBC, iv) + cipher = cast(Any, AES).new(key_bytes, AES.MODE_CBC, iv) padded_data = pad(message.encode(), AES.block_size) encrypted = cipher.encrypt(padded_data) return encrypted.hex() @@ -148,7 +148,7 @@ def encrypt_object_by_des_rules( obj: Dict[str, Any], rules: Dict[str, Dict[str, Any]] ) -> Dict[str, Any]: """根据DES规则加密对象""" - result = {} + result: Dict[str, Any] = {} for key, value in obj.items(): if key in rules: @@ -246,7 +246,7 @@ async def skland_sign_in( token: str, app_code: str = "arknights", proxy: str | None = None, -) -> dict: +) -> dict[str, Any]: """森空岛签到""" grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant" @@ -271,8 +271,11 @@ async def skland_sign_in( } def generate_signature( - token_for_sign: str, path, body_or_query, custom_header=None - ): + token_for_sign: str, + path: str, + body_or_query: str, + custom_header: dict[str, str] | None = None, + ) -> tuple[str, dict[str, str]]: """生成请求签名""" t = str(int(time.time() * 1000 - 2000))[:-3] token_bytes = token_for_sign.encode("utf-8") @@ -284,7 +287,13 @@ def generate_signature( md5_hash_value = hashlib.md5(hex_s.encode("utf-8")).hexdigest() return md5_hash_value, header_ca - async def get_sign_header(url: str, method, body, old_header, sign_token): + async def get_sign_header( + url: str, + method: str, + body: dict[str, Any] | None, + old_header: dict[str, str], + sign_token: str, + ) -> dict[str, str]: """获取带签名的请求头""" h = json.loads(json.dumps(old_header)) p = parse.urlparse(url) @@ -313,7 +322,7 @@ async def get_sign_header(url: str, method, body, old_header, sign_token): return h - def copy_header(cred, token=None): + def copy_header(cred: str, token: str | None = None) -> dict[str, str]: """复制请求头并添加cred和token""" v = json.loads(json.dumps(header)) v["cred"] = cred @@ -321,7 +330,7 @@ def copy_header(cred, token=None): v["token"] = token return v - async def login_by_token(token_code): + async def login_by_token(token_code: str) -> tuple[str, str]: """使用token一步步拿到cred和sign_token""" try: t = json.loads(token_code) @@ -331,7 +340,7 @@ async def login_by_token(token_code): grant_code = await get_grant_code(token_code) return await get_cred(grant_code) - async def get_cred(grant): + async def get_cred(grant: str) -> tuple[str, str]: """通过grant code获取cred和sign_token""" device_id = await get_cached_device_id(proxy) @@ -359,7 +368,7 @@ async def get_cred(grant): cred = rsp["data"]["cred"] return cred, sign_token - async def get_grant_code(token_value): + async def get_grant_code(token_value: str) -> str: """通过token获取grant code""" async with httpx.AsyncClient(proxy=get_proxy(proxy)) as client: response = await client.post( @@ -374,9 +383,9 @@ async def get_grant_code(token_value): ) return rsp["data"]["code"] - async def get_binding_list(cred, sign_token): + async def get_binding_list(cred: str, sign_token: str) -> list[dict[str, Any]]: """查询已绑定的角色列表""" - v = [] + v: list[dict[str, Any]] = [] async with httpx.AsyncClient(proxy=get_proxy(proxy)) as client: response = await client.get( binding_url, @@ -400,7 +409,12 @@ async def get_binding_list(cred, sign_token): v.extend(item.get("bindingList")) return v - async def check_attendance_today(cred, sign_token, uid, game_id) -> bool: + async def check_attendance_today( + cred: str, + sign_token: str, + uid: str, + game_id: str | int, + ) -> bool: """检查今天是否已经签到""" query_url = f"{arknights_sign_url}?uid={uid}&gameId={game_id}" @@ -435,10 +449,12 @@ async def check_attendance_today(cred, sign_token, uid, game_id) -> bool: logger.warning(f"检查签到状态异常: {e}") return False - async def sign_for_arknights(cred, sign_token) -> dict: + async def sign_for_arknights(cred: str, sign_token: str) -> dict[str, Any]: """方舟签到""" characters = await get_binding_list(cred, sign_token) - result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)} + success_list: list[str] = [] + duplicate_list: list[str] = [] + failed_list: list[str] = [] for character in characters: character_name = ( @@ -447,8 +463,14 @@ async def sign_for_arknights(cred, sign_token) -> dict: uid = character.get("uid") game_id = character.get("channelMasterId") + if not isinstance(uid, str) or not isinstance(game_id, (str, int)): + failed_list.append(character_name) + logger.error(f"{character_name} 缺少有效 uid 或 gameId,跳过签到") + await asyncio.sleep(1) + continue + if await check_attendance_today(cred, sign_token, uid, game_id): - result["重复"].append(character_name) + duplicate_list.append(character_name) logger.info(f"{character_name} 今天已经签到过了") await asyncio.sleep(1) continue @@ -476,24 +498,31 @@ async def sign_for_arknights(cred, sign_token) -> dict: if rsp["code"] != 0: if rsp.get("message") == "请勿重复签到!": - result["重复"].append(character_name) + duplicate_list.append(character_name) logger.info(f"{character_name} 重复签到") else: - result["失败"].append(character_name) + failed_list.append(character_name) logger.error(f"{character_name} 签到失败: {rsp.get('message')}") else: - result["成功"].append(character_name) + success_list.append(character_name) logger.info(f"{character_name} 签到成功") except Exception as e: - result["失败"].append(character_name) + failed_list.append(character_name) logger.error(f"{character_name} 签到异常: {e}") await asyncio.sleep(3) - return result + return { + "成功": success_list, + "重复": duplicate_list, + "失败": failed_list, + "总计": len(characters), + } - async def do_sign_for_endfield(cred, sign_token, role: dict): + async def do_sign_for_endfield( + cred: str, sign_token: str, role: dict[str, Any] + ) -> dict[str, Any]: headers = await get_sign_header( endfield_sign_url, "post", @@ -514,16 +543,21 @@ async def do_sign_for_endfield(cred, sign_token, role: dict): response = await client.post(endfield_sign_url, headers=headers) return response.json() - async def sign_for_endfield(cred, sign_token) -> dict: + async def sign_for_endfield(cred: str, sign_token: str) -> dict[str, Any]: """终末地签到""" characters = await get_binding_list(cred, sign_token) - result = {"成功": [], "重复": [], "失败": [], "总计": 0} + success_list: list[str] = [] + duplicate_list: list[str] = [] + failed_list: list[str] = [] + total_count = 0 for character in characters: roles = character.get("roles") or [] game_name = character.get("gameName") channel_name = character.get("channelName") - result["总计"] += len(roles) + if not isinstance(roles, list): + continue + total_count += len(roles) for role in roles: nickname = str(role.get("nickname") or "").strip() @@ -533,11 +567,14 @@ async def sign_for_endfield(cred, sign_token) -> dict: rsp = await do_sign_for_endfield(cred, sign_token, role) if rsp.get("code") != 0: message = rsp.get("message", "") - if "请勿重复签到" in message or "Please do not sign in again!" in message: - result["重复"].append(character_name) + if ( + "请勿重复签到" in message + or "Please do not sign in again!" in message + ): + duplicate_list.append(character_name) logger.info(f"{character_name} 重复签到") else: - result["失败"].append(character_name) + failed_list.append(character_name) logger.error(f"{character_name} 签到失败: {message}") else: award_ids = rsp.get("data", {}).get("awardIds", []) @@ -554,15 +591,20 @@ async def sign_for_endfield(cred, sign_token) -> dict: logger.info( f"[{game_name}] {character_name} 签到成功: {'、'.join(awards)}" ) - result["成功"].append(character_name) + success_list.append(character_name) logger.info(f"{character_name} 签到成功") except Exception as e: - result["失败"].append(character_name) + failed_list.append(character_name) logger.error(f"{character_name} 签到异常: {e}") await asyncio.sleep(3) - return result + return { + "成功": success_list, + "重复": duplicate_list, + "失败": failed_list, + "总计": total_count, + } try: cred, sign_token = await login_by_token(token) diff --git a/app/utils/websocket.py b/app/utils/websocket.py index 2de90bbe..cc845e1c 100644 --- a/app/utils/websocket.py +++ b/app/utils/websocket.py @@ -86,7 +86,7 @@ def __init__( self._last_ping = 0.0 self._last_pong = 0.0 self._reconnect_count = 0 - self._tasks: list[asyncio.Task] = [] + self._tasks: list[asyncio.Task[Any]] = [] self._auth_token: Optional[str] = auth_token @property @@ -128,7 +128,7 @@ async def connect(self) -> bool: self.logger.error(f"WebSocket 连接失败: {type(e).__name__}: {e}") return False - async def disconnect(self): + async def disconnect(self) -> None: """断开 WebSocket 连接""" self._running = False @@ -167,6 +167,10 @@ async def send(self, message: Dict[str, Any]) -> bool: self.logger.warning("WebSocket 未连接,无法发送消息") return False + if self._connection is None: + self.logger.warning("连接对象为空,无法发送消息") + return False + try: await self._connection.send(json.dumps(message)) return True @@ -187,7 +191,7 @@ async def _send_pong(self): await self.send(message) self.logger.debug("已发送 Pong") - async def _handle_message(self, raw_message: str): + async def _handle_message(self, raw_message: str) -> None: """ 处理接收到的消息 @@ -282,10 +286,16 @@ async def _receive_loop(self): """消息接收循环""" while self._running and self.is_connected: try: + conn = self._connection + if conn is None: + break message = await asyncio.wait_for( - self._connection.recv(), timeout=self.ping_interval + conn.recv(), timeout=self.ping_interval ) - await self._handle_message(message) + if isinstance(message, bytes): + await self._handle_message(message.decode("utf-8", errors="ignore")) + else: + await self._handle_message(message) except asyncio.TimeoutError: # 接收超时,检查心跳状态 @@ -375,7 +385,7 @@ async def run(self): self._tasks = [receive_task, heartbeat_task] # 等待任一任务结束 - done, pending = await asyncio.wait( + _done, pending = await asyncio.wait( self._tasks, return_when=asyncio.FIRST_COMPLETED ) @@ -439,7 +449,7 @@ async def run_once(self): self._tasks = [receive_task, heartbeat_task] # 等待任一任务结束 - done, pending = await asyncio.wait( + _done, pending = await asyncio.wait( self._tasks, return_when=asyncio.FIRST_COMPLETED ) @@ -494,7 +504,7 @@ class WSClientManager: def __init__(self): self._clients: Dict[str, WebSocketClient] = {} self._system_clients: set[str] = set() # 系统客户端名称集合 - self._tasks: Dict[str, asyncio.Task] = {} + self._tasks: Dict[str, asyncio.Task[Any]] = {} self._message_history: Dict[str, List[Dict[str, Any]]] = {} self._max_history_per_client = 200 self._debug_connections: List[Any] = [] # WebSocket 连接列表 @@ -514,7 +524,7 @@ def is_system_client(self, name: str) -> bool: def list_clients(self) -> Dict[str, Dict[str, Any]]: """列出所有客户端及其状态""" - result = {} + result: Dict[str, Dict[str, Any]] = {} for name, client in self._clients.items(): result[name] = { "name": name, @@ -727,7 +737,7 @@ async def _broadcast_event(self, event: Dict[str, Any]): async def _broadcast(self, data: Dict[str, Any]): """广播数据给所有调试前端""" - disconnected = [] + disconnected: List[Any] = [] for ws in self._debug_connections: try: await ws.send_json(data) @@ -818,7 +828,7 @@ async def init_system_client_koishi(self) -> bool: return False ws_url = self.http_to_ws_url(http_url) - token = Config.get("Notify", "KoishiToken") + Config.get("Notify", "KoishiToken") self._logger.info(f"正在初始化 Koishi 系统客户端: {ws_url}") @@ -844,7 +854,7 @@ async def init_system_client_koishi(self) -> bool: # 认证已在 on_connect 回调中自动处理 return True else: - self._logger.warning(f"Koishi 系统客户端连接失败,将在后台持续重连") + self._logger.warning("Koishi 系统客户端连接失败,将在后台持续重连") return False except Exception as e: @@ -860,7 +870,6 @@ async def update_system_client_koishi(self) -> bool: Returns: bool: 是否成功更新 """ - from app.core import Config # 如果客户端存在,先断开 if self.has_client(self.KOISHI_CLIENT_NAME): @@ -885,7 +894,15 @@ async def create_ws_client( port: int = 5140, path: str = "/ws", use_ssl: bool = False, - **kwargs, + ping_interval: float = 15.0, + ping_timeout: float = 30.0, + reconnect_interval: float = 5.0, + max_reconnect_attempts: int = -1, + on_message: Optional[Callable[[Dict[str, Any]], Any]] = None, + on_connect: Optional[Callable[[], Any]] = None, + on_disconnect: Optional[Callable[[], Any]] = None, + name: Optional[str] = None, + auth_token: Optional[str] = None, ) -> WebSocketClient: """ 创建 WebSocket 客户端实例 @@ -895,14 +912,33 @@ async def create_ws_client( port: 服务器端口 path: WebSocket 路径 use_ssl: 是否使用 SSL - **kwargs: 传递给 WebSocketClient 的其他参数 + ping_interval: 心跳间隔 + ping_timeout: 心跳超时 + reconnect_interval: 重连间隔 + max_reconnect_attempts: 最大重连次数 + on_message: 收到消息回调 + on_connect: 连接成功回调 + on_disconnect: 断开连接回调 + name: 客户端名称 + auth_token: 认证 token Returns: WebSocketClient: 客户端实例 """ protocol = "wss" if use_ssl else "ws" url = f"{protocol}://{host}:{port}{path}" - return WebSocketClient(url=url, **kwargs) + return WebSocketClient( + url=url, + ping_interval=ping_interval, + ping_timeout=ping_timeout, + reconnect_interval=reconnect_interval, + max_reconnect_attempts=max_reconnect_attempts, + on_message=on_message, + on_connect=on_connect, + on_disconnect=on_disconnect, + name=name, + auth_token=auth_token, + ) # 使用示例 @@ -931,7 +967,9 @@ async def send_messages(): # 等待客户端连接成功 while not client1.is_connected: await asyncio.sleep(0.1) - await client1._authenticate(token="123456") + await client1.send( + {"id": "Client", "type": "auth", "data": {"token": "123456"}} + ) # 发送测试消息 for i in range(5): @@ -945,7 +983,7 @@ async def send_messages(): if success: print(f"[发送成功] -> Server1: {message}") else: - print(f"[发送失败] -> Server1") + print("[发送失败] -> Server1") await asyncio.sleep(3) # 每3秒发送一次 diff --git a/main.py b/main.py index 7b48a5e9..6b0b5f50 100644 --- a/main.py +++ b/main.py @@ -31,13 +31,13 @@ if str(current_dir) not in sys.path: sys.path.insert(0, str(current_dir)) -from app.utils import get_logger, sanitize_log_message +from app.utils import get_logger, sanitize_log_message # noqa: E402 logger = get_logger("主程序") class InterceptHandler(logging.Handler): - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: # 获取对应 loguru 的 level try: level = logger.level(record.levelname).name @@ -64,7 +64,7 @@ def is_admin() -> bool: @logger.catch -def main(): +def main() -> None: if is_admin(): import asyncio import uvicorn diff --git a/requirements.txt b/requirements.txt index 4f25a513..5d39cec1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ loguru==0.7.3 fastapi==0.116.1 pydantic==2.11.7 uvicorn==0.35.0 +tomli-w==1.1.0 websockets==15.0.1 aiofiles==24.1.0 aiohttp==3.13.3 From 6e6cdd588503e11690ed8d5043dc9e9e45ac7dd3 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Sun, 5 Apr 2026 23:30:42 +0800 Subject: [PATCH 15/29] =?UTF-8?q?refactor(config):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=BA=93=E4=BB=A3=E6=9B=BF=E9=83=A8=E5=88=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/common.py | 14 +- app/api/core.py | 4 +- app/api/dispatch.py | 12 +- app/api/history.py | 6 +- app/api/info.py | 24 +-- app/api/ocr.py | 4 +- app/api/plan.py | 10 +- app/api/scripts.py | 18 +- app/api/ws_command.py | 5 +- app/api/ws_debug.py | 4 +- app/core/__init__.py | 14 +- app/core/broadcast.py | 6 +- app/core/config/base.py | 334 ++++++++++++++++++-------------- app/core/config/manager.py | 112 ++++++----- app/core/config/pydantic.py | 228 +++++++++++----------- app/core/config/types.py | 38 ++-- app/core/task_manager.py | 124 +++++++----- app/models/global_config.py | 2 +- app/models/task.py | 36 +++- app/services/notification.py | 19 +- app/services/update.py | 302 +++++++++++++++-------------- app/task/SRC/tools/poor_yaml.py | 85 +++----- app/utils/OCR/OCRtool.py | 16 +- app/utils/websocket.py | 130 ++++++------- requirements.txt | 3 + 25 files changed, 847 insertions(+), 703 deletions(-) diff --git a/app/api/common.py b/app/api/common.py index c13b04ad..f977bd20 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -3,6 +3,7 @@ from collections.abc import Awaitable, Callable, Iterable from functools import wraps import inspect +import asyncio from typing import Any, ParamSpec, TypeVar, cast from fastapi import APIRouter @@ -14,6 +15,17 @@ P = ParamSpec("P") +RECOVERABLE_EXCEPTIONS: tuple[type[Exception], ...] = ( + ValueError, + TypeError, + KeyError, + RuntimeError, + LookupError, + OSError, + asyncio.TimeoutError, +) + + def _docstring_summary(func: Callable[..., Any]) -> str | None: doc = inspect.getdoc(func) if not doc: @@ -59,7 +71,7 @@ async def run_api( ) -> OutT: try: return await success_factory() - except Exception as exc: + except RECOVERABLE_EXCEPTIONS as exc: if on_error is not None: on_error(exc) return error_out( diff --git a/app/api/core.py b/app/api/core.py index 202ed975..3a032acd 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -32,7 +32,7 @@ from app.contracts.common_contract import OutBase from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command -from app.api.common import bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out from app.utils import get_logger router = APIRouter(prefix="/api/core", tags=["核心信息"]) @@ -112,6 +112,6 @@ async def close() -> OutBase: if Config.websocket is not None: await Config.websocket.close(code=1000, reason="正常关闭") await System.set_power("KillSelf", from_frontend=True) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() diff --git a/app/api/dispatch.py b/app/api/dispatch.py index f90157d5..27e7171f 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -33,7 +33,7 @@ TaskCreateIn, TaskCreateOut, ) -from app.api.common import bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) api = bind_api(router) @@ -48,7 +48,7 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: try: task_id = await TaskManager.add_task(task.mode, task.taskId) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(TaskCreateOut, e, taskId="") return TaskCreateOut(taskId=str(task_id)) @@ -62,7 +62,7 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: async def stop_task(task: DispatchIn = Body(...)) -> OutBase: try: await TaskManager.stop_task(task.taskId) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() @@ -76,7 +76,7 @@ async def stop_task(task: DispatchIn = Body(...)) -> OutBase: async def get_power() -> PowerOut: try: signal = Config.power_sign - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(PowerOut, e, signal="NoAction") return PowerOut(signal=signal) @@ -90,7 +90,7 @@ async def get_power() -> PowerOut: async def set_power(task: PowerIn = Body(...)) -> OutBase: try: Config.power_sign = task.signal - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() @@ -104,6 +104,6 @@ async def set_power(task: PowerIn = Body(...)) -> OutBase: async def cancel_power_task() -> OutBase: try: await System.cancel_power_task() - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() diff --git a/app/api/history.py b/app/api/history.py index 47f98841..a939288b 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -27,7 +27,7 @@ from pydantic import TypeAdapter from app.core import Config -from app.api.common import bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out from app.contracts.history_contract import ( HistoryData, HistoryDataGetIn, @@ -74,7 +74,7 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: record = await Config.merge_statistic_info(records) current_users[user] = _build_history_data(record) data[date] = current_users - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(HistorySearchOut, e, data={}) return HistorySearchOut(data=data) @@ -92,6 +92,6 @@ async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryData raw_data.pop("index", None) raw_data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8") data = _build_history_data(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(HistoryDataGetOut, e, data=HistoryData()) return HistoryDataGetOut(data=data) diff --git a/app/api/info.py b/app/api/info.py index 0076320f..7824691e 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -34,7 +34,7 @@ InfoOut, OutBase, ) -from app.api.common import bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out from app.contracts.info_contract import ( GetStageIn, NoticeOut, @@ -69,7 +69,7 @@ def _to_combobox_items(raw_data: object) -> list[ComboBoxItem]: async def get_git_version() -> VersionOut: try: is_latest, commit_hash, commit_time = await Config.get_git_version() - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out( VersionOut, e, @@ -96,7 +96,7 @@ async def get_stage_combox( try: raw_data = cast(object, await Config.get_stage_info(stage.type)) data = _to_combobox_items(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -111,7 +111,7 @@ async def get_script_combox() -> ComboBoxOut: try: raw_data = await Config.get_script_combox() data = _to_combobox_items(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -126,7 +126,7 @@ async def get_task_combox() -> ComboBoxOut: try: raw_data = await Config.get_task_combox() data = _to_combobox_items(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -141,7 +141,7 @@ async def get_plan_combox() -> ComboBoxOut: try: raw_data = await Config.get_plan_combox() data = _to_combobox_items(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -156,7 +156,7 @@ async def get_emulator_combox() -> ComboBoxOut: try: raw_data = await Config.get_emulator_combox() data = _to_combobox_items(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -175,7 +175,7 @@ async def get_emulator_devices_combox( object, await Config.get_emulator_devices_combox(emulator.emulatorId) ) data = _to_combobox_items(raw_data) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -189,7 +189,7 @@ async def get_emulator_devices_combox( async def get_notice_info() -> NoticeOut: try: if_need_show, data = await Config.get_notice() - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(NoticeOut, e, if_need_show=False, data={}) return NoticeOut(if_need_show=if_need_show, data=data) @@ -203,7 +203,7 @@ async def get_notice_info() -> NoticeOut: async def confirm_notice() -> OutBase: try: await Config.set("Data", "IfShowNotice", False) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() @@ -231,7 +231,7 @@ async def confirm_notice() -> OutBase: async def get_web_config() -> InfoOut: try: data = await Config.get_web_config() - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(InfoOut, e, data={}) return InfoOut(data={"WebConfig": data}) @@ -248,6 +248,6 @@ async def get_overview() -> InfoOut: stage = cast(dict[str, Any], raw_stage if isinstance(raw_stage, dict) else {}) proxy = await Config.get_proxy_overview() - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(InfoOut, e, data={"Stage": [], "Proxy": []}) return InfoOut(data={"Stage": stage, "Proxy": proxy}) diff --git a/app/api/ocr.py b/app/api/ocr.py index bef08854..4589a070 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -31,7 +31,7 @@ from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger from app.contracts.common_contract import ApiModel, OutBase -from app.api.common import bind_api, error_out, run_api +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out, run_api logger = get_logger("OCR API") @@ -266,7 +266,7 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh image_height=0, serial=params.serial, ) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: logger.error(f"ADB 截图失败: {type(e).__name__}: {str(e)}") return error_out( ADBScreenshotOut, diff --git a/app/api/plan.py b/app/api/plan.py index 37b5f591..e39076f0 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Body, Path -from app.api.common import bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out from app.core import Config from app.contracts.common_contract import ( IndexOrderPatch, @@ -76,7 +76,7 @@ async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: try: uid, config = await Config.add_plan(plan.type) data = project_model(MaaPlanRead, await config.toDict()) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(PlanCreateOut, e, id="", data=MaaPlanRead()) return PlanCreateOut(id=str(uid), data=data) @@ -113,7 +113,7 @@ async def get_plan(plan_id: PlanIdPath) -> PlanDetailOut: async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> OutBase: try: await Config.update_plan(plan_id, body.data.model_dump(exclude_unset=True)) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() @@ -127,7 +127,7 @@ async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> async def delete_plan(plan_id: PlanIdPath) -> OutBase: try: await Config.del_plan(plan_id) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() @@ -141,6 +141,6 @@ async def delete_plan(plan_id: PlanIdPath) -> OutBase: async def reorder_plan(body: IndexOrderPatch = Body(...)) -> OutBase: try: await Config.reorder_plan(body.index_list) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() diff --git a/app/api/scripts.py b/app/api/scripts.py index 68c8c006..fd0bd8ba 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -27,7 +27,7 @@ from fastapi import APIRouter, Body, Path from pydantic import TypeAdapter -from app.api.common import bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out from app.core import Config from app.contracts.common_contract import ( ComboBoxItem, @@ -154,7 +154,7 @@ async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: script_contract_type_from_runtime(type(config).__name__), await config.toDict(), ) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out( ScriptCreateOut, e, @@ -187,13 +187,13 @@ async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: try: return await _build_script_detail_out(script_id) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: script_type = "GeneralConfig" try: script_type = script_contract_type_from_runtime( type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ ) - except Exception: + except RECOVERABLE_EXCEPTIONS: pass return error_out( ScriptDetailOut, @@ -313,7 +313,7 @@ async def create_user(script_id: ScriptIdPath) -> UserCreateOut: ) user_type = user_contract_type_from_script(script_type) data = project_user_model(user_type, await config.toDict()) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: user_type = ( user_contract_type_from_script(script_type) if script_type is not None @@ -350,14 +350,14 @@ async def reorder_users( async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOut: try: return await _build_user_detail_out(script_id, user_id) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: user_type = "GeneralUserConfig" try: script_type = script_contract_type_from_runtime( type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ ) user_type = user_contract_type_from_script(script_type) - except Exception: + except RECOVERABLE_EXCEPTIONS: pass return error_out( UserDetailOut, @@ -425,7 +425,7 @@ async def get_user_infrastructure_options( try: raw_data = await Config.get_user_combox_infrastructure(script_id, user_id) data = COMBOBOX_ITEMS_ADAPTER.validate_python(raw_data or []) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(ComboBoxOut, e, data=[]) return ComboBoxOut(data=data) @@ -490,7 +490,7 @@ async def get_user_webhook( ) -> WebhookDetailOut: try: return await _build_webhook_detail_out(script_id, user_id, webhook_id) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: return error_out(WebhookDetailOut, e, data=WebhookRead()) diff --git a/app/api/ws_command.py b/app/api/ws_command.py index abb74178..a4767348 100644 --- a/app/api/ws_command.py +++ b/app/api/ws_command.py @@ -32,6 +32,7 @@ from typing import Any, Callable, ParamSpec, TypeAlias, TypeVar, cast from pydantic import BaseModel +from app.api.common import RECOVERABLE_EXCEPTIONS from app.utils.logger import get_logger logger = get_logger("WS命令") @@ -152,7 +153,7 @@ async def execute_ws_command( try: param_instance = param_type(**(params or {})) result = await func(param_instance) - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: logger.error(f"构建参数模型失败: {type(e).__name__}: {e}") return _failed_result(f"参数错误: {str(e)}", 400) elif params: @@ -172,7 +173,7 @@ async def execute_ws_command( else: return {"success": True, "data": result, "code": 200} - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: logger.error( f"执行命令 {endpoint} 失败: {type(e).__name__}: {str(e)}", exc_info=True ) diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index aae1697a..15a37494 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -50,7 +50,7 @@ WSClearHistoryIn, WSCommandsOut, ) -from app.api.common import bind_api, run_api +from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, run_api logger = get_logger("WS调试") @@ -505,7 +505,7 @@ async def websocket_live(websocket: WebSocket): await websocket.send_text("pong") except WebSocketDisconnect: break - except Exception as e: + except RECOVERABLE_EXCEPTIONS as e: logger.error(f"WebSocket 错误: {e}") break diff --git a/app/core/__init__.py b/app/core/__init__.py index 22c874d9..66e4a81d 100644 --- a/app/core/__init__.py +++ b/app/core/__init__.py @@ -21,14 +21,18 @@ # Contact: DLmaster_361@163.com +from typing import TYPE_CHECKING + from .broadcast import Broadcast -from .config import Config from .emulator_manager import EmulatorManager from .task_manager import TaskManager from .maa_manager import MaaFWManager from .timer import MainTimer +if TYPE_CHECKING: + from .config import Config as Config + __all__ = [ "Broadcast", "Config", @@ -37,3 +41,11 @@ "EmulatorManager", "MaaFWManager", ] + + +def __getattr__(name: str): + if name == "Config": + from .config import Config + + return Config + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/core/broadcast.py b/app/core/broadcast.py index 0d60c3e0..24b960b0 100644 --- a/app/core/broadcast.py +++ b/app/core/broadcast.py @@ -22,7 +22,7 @@ import asyncio from copy import deepcopy -from typing import Any, Set +from typing import Any, Set, cast from app.utils import get_logger @@ -45,8 +45,10 @@ async def unsubscribe(self, queue: asyncio.Queue[Any]) -> None: async def put(self, item: Any) -> None: """向所有订阅者广播消息""" logger.debug(f"向所有订阅者广播消息: {item}") + should_copy = isinstance(item, (dict, list, set, tuple)) for subscriber in self.__subscribers: - await subscriber.put(deepcopy(item)) + payload = deepcopy(cast(Any, item)) if should_copy else item + await subscriber.put(payload) Broadcast = _Broadcast() diff --git a/app/core/config/base.py b/app/core/config/base.py index 7af75913..2040f58f 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -29,14 +29,19 @@ import tomllib import uuid import weakref +import os from contextlib import asynccontextmanager from dataclasses import dataclass from pathlib import Path from collections.abc import AsyncIterator, Callable, Coroutine from typing import Any, Generic, Protocol, TypeVar, cast +from importlib import import_module -import tomli_w # pyright: ignore[reportMissingImports] from loguru import logger +from filelock import FileLock, Timeout + + +tomli_w = import_module("tomli_w") PRIMARY_CONFIG_SUFFIX = ".toml" @@ -63,6 +68,44 @@ def dump_toml(data: dict[str, Any]) -> str: raise +def atomic_write_text(path: Path, content: str, encoding: str = "utf-8") -> None: + """原子写入文本文件,使用跨进程文件锁 + 写后校验。""" + + temp_file = path.with_suffix(f"{path.suffix}.tmp") + lock_file = path.with_suffix(f"{path.suffix}.lock") + file_lock = FileLock(str(lock_file), timeout=10) + try: + with file_lock: + path.parent.mkdir(parents=True, exist_ok=True) + temp_file.write_text(content, encoding=encoding) + + verify_content = temp_file.read_text(encoding=encoding) + if verify_content != content: + raise OSError(f"配置写入校验失败: {path}") + + if os.name == "nt" and path.exists(): + backup_file = path.with_suffix(f"{path.suffix}.bak") + try: + path.replace(backup_file) + except OSError as backup_error: + logger.warning( + f"旧配置备份失败,尝试直接覆盖: {path}, 错误: {backup_error}" + ) + + temp_file.replace(path) + except Timeout as e: + logger.error(f"获取配置文件锁超时: {path}, 错误: {e}") + raise + finally: + if temp_file.exists(): + try: + temp_file.unlink() + except OSError as cleanup_error: + logger.warning( + f"清理临时配置文件失败: {temp_file}, 错误: {cleanup_error}" + ) + + def _load_json_config(path: Path) -> dict[str, Any]: """ 加载 JSON 配置文件。 @@ -89,8 +132,8 @@ def _load_json_config(path: Path) -> dict[str, Any]: except json.JSONDecodeError as e: logger.error(f"JSON 解析失败: {path}, 错误: {e}") return {} - except Exception: - logger.exception(f"加载配置文件失败: {path}") + except OSError as e: + logger.error(f"读取 JSON 配置失败: {path}, 错误: {e}") return {} @@ -116,8 +159,8 @@ def _load_toml_config(path: Path) -> dict[str, Any]: except tomllib.TOMLDecodeError as e: logger.error(f"TOML 解析失败: {path}, 错误: {e}") return {} - except Exception: - logger.exception(f"加载配置文件失败: {path}") + except OSError as e: + logger.error(f"读取 TOML 配置失败: {path}, 错误: {e}") return {} @@ -186,7 +229,7 @@ def _backup_legacy_config_if_needed( try: legacy_file.replace(legacy_backup) logger.info(f"已备份旧版配置: {legacy_file} -> {legacy_backup}") - except Exception as e: + except OSError as e: logger.error(f"备份旧版配置失败: {legacy_file}, 错误: {e}") @@ -342,6 +385,7 @@ def __init__(self, sub_config_type: list[type[T]]): self._on_before_del_slots: list[_WeakCallbackSlot] = [] self._on_del_slots: list[_WeakCallbackSlot] = [] self._on_reorder_slots: list[_WeakCallbackSlot] = [] + self._mutex = asyncio.Lock() def __getitem__(self, key: uuid.UUID) -> T: if key not in self.data: @@ -381,23 +425,24 @@ async def connect(self, path: Path) -> None: if self.is_locked: raise ValueError("配置已锁定,无法修改") - logger.info(f"连接配置文件: {path}") - self.file = path + async with self._mutex: + logger.info(f"连接配置文件: {path}") + self.file = path - if not self.file.exists(): - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.touch() - logger.debug(f"创建新配置文件: {self.file}") + if not self.file.exists(): + self.file.parent.mkdir(parents=True, exist_ok=True) + self.file.touch() + logger.debug(f"创建新配置文件: {self.file}") - try: - data, legacy_file = _load_config_with_legacy_migration(self.file) - await self.load(data) - await self.add_save_method(self.save) - _backup_legacy_config_if_needed(self.file, legacy_file) - logger.info(f"配置加载成功: {path}, 项目数: {len(self.data)}") - except Exception: - logger.exception(f"配置加载失败: {path}") - raise + try: + data, legacy_file = _load_config_with_legacy_migration(self.file) + await self.load(data) + await self.add_save_method(self.save) + _backup_legacy_config_if_needed(self.file, legacy_file) + logger.info(f"配置加载成功: {path}, 项目数: {len(self.data)}") + except (OSError, ValueError, TypeError) as e: + logger.error(f"配置加载失败: {path}, 错误: {e}") + raise async def add_save_method( self, save_method: Callable[[], Coroutine[Any, Any, None]] @@ -427,64 +472,71 @@ async def _flush_pending_changes(self) -> None: if self._pending_save and self.file: self._pending_save = False - self.file.parent.mkdir(parents=True, exist_ok=True) - self.file.write_text( - dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) + await self._save_unlocked() if self._pending_sync: self._pending_sync = False if self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) - async def load(self, data: dict[str, Any]) -> None: - """从字典加载多实例配置数据。""" - - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") - - self.order = [] - self.data = {} - - instances = data.get("instances") - if not isinstance(instances, list): - return - instances_list = cast(list[object], instances) - - for instance in instances_list: - if not isinstance(instance, dict): - continue - instance_dict = cast(dict[object, Any], instance) + async def _save_unlocked(self) -> None: + """在已持有互斥锁时保存配置。""" - uid_str = instance_dict.get("uid") - type_name = instance_dict.get("type") - if not isinstance(uid_str, str) or not isinstance(type_name, str): - continue - if type_name not in self.sub_config_type: - continue - - instance_data = data.get(uid_str) - if not isinstance(instance_data, dict): - continue - instance_data_dict = cast(dict[str, Any], instance_data) + if not self.file: + raise ValueError("文件路径未设置,请先调用 connect() 方法") - try: - uid = uuid.UUID(uid_str) - except (TypeError, ValueError): - continue + content = dump_toml(await self.toDict(if_decrypt=False)) + atomic_write_text(self.file, content) + logger.debug(f"配置保存成功: {self.file}, 大小: {len(content)} 字节") - config = self.sub_config_type[type_name]() - config.bind_owner_collection(self, uid) - self.order.append(uid) - self.data[uid] = config - await config.load(instance_data_dict) + async def load(self, data: dict[str, Any]) -> None: + """从字典加载多实例配置数据。""" - if self.file: - await self.save() + async with self._mutex: + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") + + self.order = [] + self.data = {} + + instances = data.get("instances") + if not isinstance(instances, list): + return + instances_list = cast(list[object], instances) + + for instance in instances_list: + if not isinstance(instance, dict): + continue + instance_dict = cast(dict[object, Any], instance) + + uid_str = instance_dict.get("uid") + type_name = instance_dict.get("type") + if not isinstance(uid_str, str) or not isinstance(type_name, str): + continue + if type_name not in self.sub_config_type: + continue + + instance_data = data.get(uid_str) + if not isinstance(instance_data, dict): + continue + instance_data_dict = cast(dict[str, Any], instance_data) + + try: + uid = uuid.UUID(uid_str) + except (TypeError, ValueError): + continue + + config = self.sub_config_type[type_name]() + config.bind_owner_collection(self, uid) + self.order.append(uid) + self.data[uid] = config + await config.load(instance_data_dict) + + if self.file: + await self._save_unlocked() - if self._save_methods: - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) async def toDict( self, if_decrypt: bool = True, regenerate_uuids: bool = False @@ -543,19 +595,12 @@ async def save(self) -> None: logger.debug(f"事务中,延迟保存: {self.file}") return - try: - self.file.parent.mkdir(parents=True, exist_ok=True) - content = dump_toml(await self.toDict(if_decrypt=False)) - - # 原子写入:先写临时文件,再替换 - temp_file = self.file.with_suffix(f"{self.file.suffix}.tmp") - temp_file.write_text(content, encoding="utf-8") - temp_file.replace(self.file) - - logger.debug(f"配置保存成功: {self.file}, 大小: {len(content)} 字节") - except Exception: - logger.exception(f"配置保存失败: {self.file}") - raise + async with self._mutex: + try: + await self._save_unlocked() + except (OSError, ValueError, TypeError) as e: + logger.error(f"配置保存失败: {self.file}, 错误: {e}") + raise async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: """ @@ -570,36 +615,37 @@ async def add(self, config_type: type[T]) -> tuple[uuid.UUID, T]: Raises: ValueError: 如果配置类型不被允许或配置已锁定 """ - if config_type not in self.sub_config_type.values(): - raise ValueError(f"配置类型 {config_type.__name__} 不被允许") - if self.is_locked: - raise ValueError("配置已锁定,无法修改") - - uid = uuid.uuid4() - config = config_type() - config.bind_owner_collection(self, uid) - self.order.append(uid) - self.data[uid] = config + async with self._mutex: + if config_type not in self.sub_config_type.values(): + raise ValueError(f"配置类型 {config_type.__name__} 不被允许") + if self.is_locked: + raise ValueError("配置已锁定,无法修改") + + uid = uuid.uuid4() + config = config_type() + config.bind_owner_collection(self, uid) + self.order.append(uid) + self.data[uid] = config - logger.info(f"新增配置: {config_type.__name__}, UID: {uid}") + logger.info(f"新增配置: {config_type.__name__}, UID: {uid}") - for save_method in self._save_methods: - await config.add_save_method(save_method) + for save_method in self._save_methods: + await config.add_save_method(save_method) - if self.file: - await config.add_save_method(self.save) - await self.save() + if self.file: + await config.add_save_method(self.save) + await self._save_unlocked() - if self._transaction_depth > 0: - self._pending_sync = self._pending_sync or bool(self._save_methods) - elif self._save_methods: - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) - await _emit_collection_slots( - self._on_add_slots, MultipleConfigAddEvent(self, uid, config) - ) + await _emit_collection_slots( + self._on_add_slots, MultipleConfigAddEvent(self, uid, config) + ) - return uid, config + return uid, config async def remove(self, uid: uuid.UUID) -> None: """ @@ -611,56 +657,58 @@ async def remove(self, uid: uuid.UUID) -> None: Raises: ValueError: 如果配置不存在、已锁定或父容器已锁定 """ - if self.is_locked: - raise ValueError("配置已锁定,无法修改") - if uid not in self.data: - raise ValueError(f"配置项 '{uid}' 不存在") - if self.data[uid].is_locked: - raise ValueError(f"配置项 '{uid}' 已锁定,无法移除") - - config = self.data[uid] - logger.info(f"移除配置: {type(config).__name__}, UID: {uid}") - - await _emit_collection_slots( - self._on_before_del_slots, MultipleConfigDeleteEvent(self, uid, config) - ) + async with self._mutex: + if self.is_locked: + raise ValueError("配置已锁定,无法修改") + if uid not in self.data: + raise ValueError(f"配置项 '{uid}' 不存在") + if self.data[uid].is_locked: + raise ValueError(f"配置项 '{uid}' 已锁定,无法移除") + + config = self.data[uid] + logger.info(f"移除配置: {type(config).__name__}, UID: {uid}") + + await _emit_collection_slots( + self._on_before_del_slots, MultipleConfigDeleteEvent(self, uid, config) + ) - self.data.pop(uid) - self.order.remove(uid) + self.data.pop(uid) + self.order.remove(uid) - if self.file: - await self.save() + if self.file: + await self._save_unlocked() - if self._transaction_depth > 0: - self._pending_sync = self._pending_sync or bool(self._save_methods) - elif self._save_methods: - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) - await _emit_collection_slots( - self._on_del_slots, MultipleConfigDeleteEvent(self, uid, config) - ) + await _emit_collection_slots( + self._on_del_slots, MultipleConfigDeleteEvent(self, uid, config) + ) async def setOrder(self, order: list[uuid.UUID]) -> None: # noqa: N802 """设置子配置实例顺序。""" - if set(order) != set(self.data.keys()): - raise ValueError("顺序与当前配置项不匹配") - if self.is_locked: - raise ValueError("配置已锁定, 无法修改") + async with self._mutex: + if set(order) != set(self.data.keys()): + raise ValueError("顺序与当前配置项不匹配") + if self.is_locked: + raise ValueError("配置已锁定, 无法修改") - self.order = order + self.order = order - if self.file: - await self.save() + if self.file: + await self._save_unlocked() - if self._transaction_depth > 0: - self._pending_sync = self._pending_sync or bool(self._save_methods) - elif self._save_methods: - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) - await _emit_collection_slots( - self._on_reorder_slots, MultipleConfigReorderEvent(self, list(order)) - ) + await _emit_collection_slots( + self._on_reorder_slots, MultipleConfigReorderEvent(self, list(order)) + ) async def lock(self) -> None: """锁定当前管理器及全部子配置。""" diff --git a/app/core/config/manager.py b/app/core/config/manager.py index c00e2d58..38d9a4d7 100644 --- a/app/core/config/manager.py +++ b/app/core/config/manager.py @@ -19,7 +19,6 @@ # along with AUTO-MAS. If not, see . # Contact: DLmaster_361@163.com -# pyright: reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingParameterType=false, reportInvalidTypeForm=false, reportGeneralTypeIssues=false import os import re @@ -36,27 +35,16 @@ from collections import defaultdict from jinja2 import Environment, FileSystemLoader from datetime import datetime, timedelta, date -from typing import Literal, Optional, Dict, Any, List, ClassVar +from typing import Literal, Optional, Dict, Any, List, ClassVar, cast import uuid import json -from app.models import ( - GeneralConfig, - MaaConfig, - SrcConfig, - MaaEndConfig, - MaaPlanConfig, - QueueConfig, - QueueItem, - MaaUserConfig, - SrcUserConfig, - MaaEndUserConfig, - GeneralUserConfig, - GlobalConfig, - Webhook, - TimeSet, - EmulatorConfig, -) +from app.models.common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook +from app.models.general import GeneralConfig, GeneralUserConfig +from app.models.global_config import GlobalConfig +from app.models.maa import MaaConfig, MaaPlanConfig, MaaUserConfig +from app.models.maaend import MaaEndConfig, MaaEndUserConfig +from app.models.src import SrcConfig, SrcUserConfig from .base import dump_toml from app.models.shared import WebSocketMessage from app.utils.constants import ( @@ -116,7 +104,7 @@ def __init__(self) -> None: self.repo = Repo(Path.cwd()) else: self.repo = None - except Exception as e: + except (OSError, ValueError) as e: logger.warning(f"Git仓库初始化失败: {e}") self.repo = None @@ -155,6 +143,8 @@ async def _connect_runtime_configs(self) -> None: await self.ToolsConfig.connect(self._resolve_config_path("ToolsConfig")) def _read_mapping_config(self, path: Path) -> dict[str, Any]: + """读取 TOML/JSON 字典配置文件并返回映射对象。""" + if not path.exists(): return {} @@ -166,14 +156,20 @@ def _read_mapping_config(self, path: Path) -> dict[str, Any]: data = tomllib.loads(text) else: data = json.loads(text) - return data if isinstance(data, dict) else {} + if isinstance(data, dict): + return cast(dict[str, Any], data) + return {} def _write_mapping_config(self, path: Path, data: dict[str, Any]) -> None: + """将字典配置写入 TOML/JSON 文件。""" + if path.suffix == ".toml": path.write_text(dump_toml(data), encoding="utf-8") return - path.write_text(json.dumps(data, ensure_ascii=False, indent=4), encoding="utf-8") + path.write_text( + json.dumps(data, ensure_ascii=False, indent=4), encoding="utf-8" + ) async def init_config(self) -> None: """初始化配置管理""" @@ -544,7 +540,7 @@ def _get_git_info(): f"origin/{self.repo.active_branch.name}" ) is_latest = bool(current_commit.hexsha == remote_commit.hexsha) - except Exception as e: + except (ValueError, OSError) as e: logger.warning(f"无法获取远程分支信息: {e}") is_latest = False @@ -947,13 +943,16 @@ async def get_user_combox_infrastructure( logger.info("开始获取用户自定义基建排班下拉框信息") - data = [] + data: list[dict[str, str]] = [] for i, plan in enumerate( json.loads( script_config.UserData[user_uid].get("Data", "CustomInfrast") ).get("plans", []) ): - data.append({"label": plan.get("name", f"排班 {i+1}"), "value": str(i)}) + plan_mapping = cast(dict[str, Any], plan) + data.append( + {"label": str(plan_mapping.get("name", f"排班 {i+1}")), "value": str(i)} + ) logger.success("用户自定义基建排班下拉框信息获取成功") @@ -1408,7 +1407,7 @@ def proxy(self) -> Optional[httpx.Proxy]: try: return httpx.Proxy(proxy_addr) - except Exception as e: + except ValueError as e: logger.warning(f"代理配置无效: {proxy_addr}, 错误: {e}") return None @@ -1427,19 +1426,25 @@ async def get_stage_info( "Sunday", "Info", ], - ): + ) -> dict[str, Any] | list[dict[str, str]]: """获取关卡信息""" - if json.loads(self.get("Data", "Stage")) != {}: + stage_cache = cast(dict[str, Any], json.loads(self.get("Data", "Stage"))) + if stage_cache != {}: task = asyncio.create_task(self.get_stage()) self.temp_task.append(task) - task.add_done_callback(lambda t: self.temp_task.remove(t)) + task.add_done_callback( + lambda t: self.temp_task.remove(t) if t in self.temp_task else None + ) else: - await self.get_stage() + refreshed = await self.get_stage() + stage_cache = cast( + dict[str, Any], refreshed if refreshed is not None else {} + ) if type == "Info": today = datetime.now(tz=UTC4).isoweekday() - res_stage_info = [] + res_stage_info: list[dict[str, Any]] = [] for stage in RESOURCE_STAGE_INFO: days = stage.get("days") if ( @@ -1449,22 +1454,23 @@ async def get_stage_info( ): res_stage_info.append(RESOURCE_STAGE_DROP_INFO[stage["value"]]) return { - "Activity": json.loads(self.get("Data", "Stage")).get("Info", []), + "Activity": stage_cache.get("Info", []), "Resource": res_stage_info, } elif type == "User": - data = json.loads(self.get("Data", "Stage")).get("ALL", []) + data = cast(list[dict[str, str]], stage_cache.get("ALL", [])) for combox in data: combox["label"] = RESOURCE_STAGE_DATE_TEXT.get( combox["value"], combox["label"] ) return data elif type == "Today": - return json.loads(self.get("Data", "Stage")).get( - datetime.now(tz=UTC4).strftime("%A"), [] + return cast( + list[dict[str, str]], + stage_cache.get(datetime.now(tz=UTC4).strftime("%A"), []), ) else: - return json.loads(self.get("Data", "Stage")).get(type, []) + return cast(list[dict[str, str]], stage_cache.get(type, [])) async def get_proxy_overview(self) -> Dict[str, Any]: """获取代理情况概览信息""" @@ -1476,13 +1482,15 @@ async def get_proxy_overview(self) -> Dict[str, Any]: ) if datetime.now(tz=UTC4).strftime("%Y-%m-%d") not in history_index: return {} + today_records = history_index[datetime.now(tz=UTC4).strftime("%Y-%m-%d")] + merged_list = await asyncio.gather( + *(self.merge_statistic_info(v) for v in today_records.values()) + ) history_data = { - k: await self.merge_statistic_info(v) - for k, v in history_index[ - datetime.now(tz=UTC4).strftime("%Y-%m-%d") - ].items() + user: merged + for user, merged in zip(today_records.keys(), merged_list, strict=False) } - overview = {} + overview: dict[str, dict[str, Any]] = {} for user, data in history_data.items(): index_data = data.get("index", []) if index_data: @@ -1555,7 +1563,7 @@ async def get_stage(self) -> Optional[Dict[str, List[Dict[str, str]]]]: ) else: logger.warning(f"无法从MAA服务器获取活动关卡信息:{response.text}") - except Exception as e: + except (httpx.HTTPError, ValueError, json.JSONDecodeError) as e: logger.warning(f"无法从MAA服务器获取活动关卡信息: {e}") return json.loads(self.get("Data", "Stage")) @@ -1621,7 +1629,9 @@ async def get_emulator_combox(self): logger.success("模拟器下拉框信息获取成功") return data - async def get_emulator_devices_combox(self, emulator_id: str): + async def get_emulator_devices_combox( + self, emulator_id: str + ) -> list[dict[str, str]]: """获取模拟器多开实例下拉框信息""" logger.info("开始获取模拟器下拉框信息") @@ -1630,7 +1640,7 @@ async def get_emulator_devices_combox(self, emulator_id: str): logger.info("通用模拟器不支持扫描多开实例, 返回空列表") return [] - data = [{"label": "未选择", "value": "-"}] + data: list[dict[str, str]] = [{"label": "未选择", "value": "-"}] from ..emulator_manager import EmulatorManager @@ -1694,7 +1704,7 @@ async def get_notice(self) -> tuple[bool, Dict[str, str]]: logger.warning( f"无法从 AUTO-MAS 服务器获取公告信息:{response.text}" ) - except Exception as e: + except (httpx.HTTPError, ValueError, json.JSONDecodeError) as e: logger.warning(f"无法从 AUTO-MAS 服务器获取公告信息: {e}") return self.get("Data", "IfShowNotice"), json.loads( @@ -1727,7 +1737,7 @@ async def get_web_config(self): f"无法从 AUTO-MAS 服务器获取配置分享中心信息:{response.text}" ) remote_web_config = None - except Exception as e: + except (httpx.HTTPError, ValueError, json.JSONDecodeError) as e: logger.warning(f"无法从 AUTO-MAS 服务器获取配置分享中心信息: {e}") remote_web_config = None @@ -1818,7 +1828,7 @@ async def save_maa_log( all_stage_drops: dict[str, dict[str, int]] = {} # 查找所有Fight任务的开始和结束位置 - fight_tasks = [] + fight_tasks: list[tuple[int, int]] = [] for i, line in enumerate(logs): if "开始任务: Fight" in line or "开始任务: 理智作战" in line: # 查找对应的任务结束位置 @@ -1843,7 +1853,7 @@ async def save_maa_log( task_logs = logs[start_idx : end_idx + 1] # 查找任务中的最后一次掉落统计 - last_drop_stats = {} + last_drop_stats: dict[str, int] = {} current_stage = None for line in task_logs: @@ -2000,7 +2010,7 @@ async def merge_statistic_info( for json_file in statistic_path_list: try: single_data = json.loads(json_file.read_text(encoding="utf-8")) - except Exception as e: + except (OSError, json.JSONDecodeError) as e: logger.warning( f"无法解析文件 {json_file}, 错误信息: {type(e).__name__}: {str(e)}" ) @@ -2128,8 +2138,8 @@ async def search_history( user_folder.with_suffix("").glob("*.json") ) - except ValueError: - logger.exception(f"非日期格式的目录: {date_folder}") + except ValueError as e: + logger.warning(f"非日期格式的目录: {date_folder}, 错误: {e}") logger.success(f"历史记录搜索完成, 共计 {len(history_dict)} 条记录") diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index bca36834..e2f52371 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -14,6 +14,7 @@ from .base import ( MultipleConfig, MultipleConfigDeleteEvent, + atomic_write_text, backup_legacy_config_if_needed, dump_toml, load_config_with_legacy_migration, @@ -201,6 +202,7 @@ class PydanticConfigBase(BaseModel): ) _owner_collection: MultipleConfig[Any] | None = PrivateAttr(default=None) _owner_uid: uuid.UUID | None = PrivateAttr(default=None) + _mutex: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock) @property def file(self) -> Path | None: @@ -463,17 +465,20 @@ async def _flush_pending_changes(self) -> None: if self._pending_save and self._file: self._pending_save = False - self._file.parent.mkdir(parents=True, exist_ok=True) - self._file.write_text( - dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) + await self._save_unlocked() if self._pending_sync: self._pending_sync = False if self._save_methods: await asyncio.gather(*(_() for _ in self._save_methods)) + async def _save_unlocked(self) -> None: + if not self._file: + raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") + + content = dump_toml(await self.toDict(if_decrypt=False)) + atomic_write_text(self._file, content) + async def connect(self, path: Path) -> None: if path.suffix != ".toml": raise ValueError("配置文件必须是扩展名为 '.toml' 的 TOML 文件") @@ -500,65 +505,67 @@ async def add_save_method(self, save_method: SaveMethod) -> None: await sub_config.add_save_method(save_method) async def load(self, data: dict[str, Any]) -> None: - if self._is_locked: - raise ValueError("配置已锁定, 无法修改") - - raw: dict[str, Any] = dict(data) - - sub_configs = _normalize_mapping(raw.pop("SubConfigsInfo", {})) - - for name, sub_config in self._multiple_config_index().items(): - data_for_sub = sub_configs.get(name) - if isinstance(data_for_sub, dict): - await sub_config.load(_normalize_mapping(data_for_sub)) - - for group_name, group_model in self._group_index().items(): - group_data = _normalize_mapping(raw.get(group_name, {})) - - default_group = type(group_model)() - for field_name in list(type(group_model).model_fields.keys()): - if self._get_virtual_field(group_name, field_name) is not None: - continue - - candidate: Any = None - has_value = False - - if field_name in group_data: - candidate = group_data[field_name] - has_value = True - else: - field_info = type(group_model).model_fields.get(field_name) - if field_info is not None: - candidate, has_value = _try_resolve_alias_value( - raw, - group_name, - group_data, - field_info.validation_alias, - ) - - if not has_value: - legacy = self.LEGACY_FIELD_MAP.get((group_name, field_name)) - if legacy is not None: - legacy_group, legacy_name = legacy - legacy_data = _normalize_mapping(raw.get(legacy_group, {})) - if legacy_name in legacy_data: - candidate = legacy_data[legacy_name] - has_value = True - - if not has_value: - continue - - candidate = self._normalize_value(group_name, field_name, candidate) - try: - setattr(group_model, field_name, candidate) - except Exception: - setattr(group_model, field_name, getattr(default_group, field_name)) - - if self._file: - await self.save() + async with self._mutex: + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") + + raw: dict[str, Any] = dict(data) + + sub_configs = _normalize_mapping(raw.pop("SubConfigsInfo", {})) + + for name, sub_config in self._multiple_config_index().items(): + data_for_sub = sub_configs.get(name) + if isinstance(data_for_sub, dict): + await sub_config.load(_normalize_mapping(data_for_sub)) + + for group_name, group_model in self._group_index().items(): + group_data = _normalize_mapping(raw.get(group_name, {})) + + for field_name in list(type(group_model).model_fields.keys()): + if self._get_virtual_field(group_name, field_name) is not None: + continue + + candidate: Any = None + has_value = False + + if field_name in group_data: + candidate = group_data[field_name] + has_value = True + else: + field_info = type(group_model).model_fields.get(field_name) + if field_info is not None: + candidate, has_value = _try_resolve_alias_value( + raw, + group_name, + group_data, + field_info.validation_alias, + ) + + if not has_value: + legacy = self.LEGACY_FIELD_MAP.get((group_name, field_name)) + if legacy is not None: + legacy_group, legacy_name = legacy + legacy_data = _normalize_mapping(raw.get(legacy_group, {})) + if legacy_name in legacy_data: + candidate = legacy_data[legacy_name] + has_value = True + + if not has_value: + continue + + candidate = self._normalize_value(group_name, field_name, candidate) + try: + setattr(group_model, field_name, candidate) + except (TypeError, ValueError) as e: + raise ValueError( + f"加载配置项失败: {group_name}.{field_name}={candidate!r}" + ) from e + + if self._file: + await self._save_unlocked() - if self._save_methods: - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) async def toDict( self, if_decrypt: bool = True, regenerate_uuids: bool = False @@ -594,56 +601,56 @@ def get(self, group: str, name: str) -> Any: return value async def set(self, group: str, name: str, value: Any) -> None: - group_model = self._group_index().get(group) - if group_model is None or not hasattr(group_model, name): - raise AttributeError(f"配置项 '{group}.{name}' 不存在") + async with self._mutex: + group_model = self._group_index().get(group) + if group_model is None or not hasattr(group_model, name): + raise AttributeError(f"配置项 '{group}.{name}' 不存在") - if self._is_locked: - raise ValueError("配置已锁定, 无法修改") - - virtual_field = self._get_virtual_field(group, name) - if virtual_field is not None: - await self._set_virtual_value(group, name, value) - return + if self._is_locked: + raise ValueError("配置已锁定, 无法修改") - old_value = getattr(group_model, name) - virtual_old_values = { - (virtual_group, virtual_name): self._get_virtual_value( - virtual_group, virtual_name, virtual_field - ) - for virtual_group, virtual_name, virtual_field in self._iter_virtual_dependents( - (group, name) - ) - } - value = self._normalize_value(group, name, value) + virtual_field = self._get_virtual_field(group, name) + if virtual_field is not None: + await self._set_virtual_value(group, name, value) + return - default_group = type(group_model)() - try: - setattr(group_model, name, value) - except Exception: - setattr(group_model, name, getattr(default_group, name)) - - new_value = getattr(group_model, name) - if old_value != new_value: - await self._queue_binding(group, name, new_value) - - for ( - virtual_group, - virtual_name, - ), old_virtual_value in virtual_old_values.items(): - new_virtual_value = self.get(virtual_group, virtual_name) - if old_virtual_value != new_virtual_value: - await self._queue_binding( - virtual_group, virtual_name, new_virtual_value + old_value = getattr(group_model, name) + virtual_old_values = { + (virtual_group, virtual_name): self._get_virtual_value( + virtual_group, virtual_name, virtual_field ) + for virtual_group, virtual_name, virtual_field in self._iter_virtual_dependents( + (group, name) + ) + } + value = self._normalize_value(group, name, value) + + try: + setattr(group_model, name, value) + except (TypeError, ValueError) as e: + raise ValueError(f"设置配置项失败: {group}.{name}={value!r}") from e + + new_value = getattr(group_model, name) + if old_value != new_value: + await self._queue_binding(group, name, new_value) + + for ( + virtual_group, + virtual_name, + ), old_virtual_value in virtual_old_values.items(): + new_virtual_value = self.get(virtual_group, virtual_name) + if old_virtual_value != new_virtual_value: + await self._queue_binding( + virtual_group, virtual_name, new_virtual_value + ) - if self._file: - await self.save() + if self._file: + await self._save_unlocked() - if self._transaction_depth > 0: - self._pending_sync = self._pending_sync or bool(self._save_methods) - elif self._save_methods: - await asyncio.gather(*(_() for _ in self._save_methods)) + if self._transaction_depth > 0: + self._pending_sync = self._pending_sync or bool(self._save_methods) + elif self._save_methods: + await asyncio.gather(*(_() for _ in self._save_methods)) async def set_many(self, values: dict[str, dict[str, Any]]) -> None: """批量更新多个配置项。""" @@ -697,11 +704,8 @@ async def save(self) -> None: self._pending_save = True return - self._file.parent.mkdir(parents=True, exist_ok=True) - self._file.write_text( - dump_toml(await self.toDict(if_decrypt=False)), - encoding="utf-8", - ) + async with self._mutex: + await self._save_unlocked() async def lock(self) -> None: self._is_locked = True diff --git a/app/core/config/types.py b/app/core/config/types.py index 8f7bf4f0..611f6ce0 100644 --- a/app/core/config/types.py +++ b/app/core/config/types.py @@ -49,17 +49,17 @@ def _validate_json_dict_string(value: Any) -> str: """ text = _to_string(value) if not text: - return "{ }" + raise ValueError("JSON 字典字符串不能为空") try: parsed = json.loads(text) if not isinstance(parsed, dict): logger.warning(f"JSON 不是字典类型: {text[:50]}...") - return "{ }" + raise ValueError("JSON 不是字典类型") return text except json.JSONDecodeError as e: logger.warning(f"JSON 解析失败: {e}, 输入: {text[:50]}...") - return "{ }" + raise ValueError("JSON 字典字符串解析失败") from e def _validate_json_list_string(value: Any) -> str: @@ -74,17 +74,17 @@ def _validate_json_list_string(value: Any) -> str: """ text = _to_string(value) if not text: - return "[ ]" + raise ValueError("JSON 列表字符串不能为空") try: parsed = json.loads(text) if not isinstance(parsed, list): logger.warning(f"JSON 不是列表类型: {text[:50]}...") - return "[ ]" + raise ValueError("JSON 不是列表类型") return text except json.JSONDecodeError as e: logger.warning(f"JSON 解析失败: {e}, 输入: {text[:50]}...") - return "[ ]" + raise ValueError("JSON 列表字符串解析失败") from e def _validate_hhmm_string(value: Any) -> str: @@ -163,17 +163,19 @@ def _validate_url_string(value: Any) -> str: """ text = _to_string(value) if not text: - return "" + raise ValueError("URL 不能为空") try: parsed = urlparse(text) if not parsed.scheme or not parsed.netloc: logger.warning(f"URL 格式错误: {text}") - return "" + raise ValueError("URL 格式错误") return text - except Exception as e: + except ValueError: + raise + except TypeError as e: logger.warning(f"URL 解析失败: {e}, 输入: {text}") - return "" + raise ValueError("URL 解析失败") from e def _validate_keyboard_key(value: Any) -> str: @@ -188,11 +190,11 @@ def _validate_keyboard_key(value: Any) -> str: """ text = _to_string(value).lower() if not text: - return "" + raise ValueError("键盘按键不能为空") if text not in pyautogui.KEYBOARD_KEYS: logger.warning(f"无效的键盘按键: {text}") - return "" + raise ValueError(f"无效的键盘按键: {text}") return text @@ -211,19 +213,19 @@ def _normalize_encrypted_string(value: Any) -> str: """ text = _to_string(value) if not text: - return "" + raise ValueError("加密字段不能为空") try: # 尝试解密,如果成功说明已加密 dpapi_decrypt(text) return text - except Exception: + except ValueError: # 解密失败,说明是明文,需要加密 try: return dpapi_encrypt(text) - except Exception as e: + except (ValueError, TypeError) as e: logger.error(f"加密失败: {e}, 输入长度: {len(text)}") - return "" + raise ValueError("加密失败") from e def decrypt_encrypted_string(value: str) -> str: @@ -241,9 +243,9 @@ def decrypt_encrypted_string(value: str) -> str: try: return dpapi_decrypt(value) - except Exception as e: + except ValueError as e: logger.error(f"解密失败: {e}") - return "数据损坏,请重新设置" + raise ValueError("数据损坏,请重新设置") from e # 类型别名定义 diff --git a/app/core/task_manager.py b/app/core/task_manager.py index 9c015806..82f7cded 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -37,8 +37,9 @@ class TaskInfo(TaskItem): - async def on_change(self): + """任务状态变化后推送 WebSocket 增量更新。""" + await Config.send_websocket_message( id=self.task_id, type="Update", @@ -53,13 +54,13 @@ async def on_change(self): class Task(TaskExecuteBase): - def __init__(self, task_info: TaskInfo): super().__init__() self.task_info = task_info self.is_closing = False async def prepare(self): + """根据任务模式解析脚本清单并初始化运行项。""" # 初始化任务列表 script_ids = ( @@ -91,6 +92,7 @@ async def prepare(self): ) async def main_task(self): + """串行执行任务中每个脚本项。""" await self.prepare() @@ -155,6 +157,7 @@ async def main_task(self): await self.spawn(task_item) async def final_task(self) -> None: + """任务收尾:上报结果并处理电源动作信号。""" logger.info(f"任务结束: {self.task_info.task_id}") @@ -165,7 +168,6 @@ async def final_task(self) -> None: ) if self.task_info.mode == "AutoProxy" and self.task_info.queue_id is not None: - if Config.power_sign == "NoAction": Config.power_sign = Config.QueueConfig[ uuid.UUID(self.task_info.queue_id) @@ -175,6 +177,8 @@ async def final_task(self) -> None: ) async def on_crash(self, e: Exception) -> None: + """任务异常回调:记录日志并向前端推送错误信息。""" + logger.exception(f"任务 {self.task_info.task_id} 出现异常: {e}") await Config.send_websocket_message( id=self.task_info.task_id, @@ -191,6 +195,29 @@ def __init__(self): self.task_info: Dict[uuid.UUID, TaskInfo] = {} self.task_handler: Dict[uuid.UUID, Task] = {} + self._mutex = asyncio.Lock() + self._background_tasks: set[asyncio.Task[None]] = set() + + def _track_background_task(self, task: asyncio.Task[None]) -> None: + """跟踪后台任务,避免异常静默与任务泄漏。""" + + self._background_tasks.add(task) + + def _done(done_task: asyncio.Task[None]) -> None: + self._background_tasks.discard(done_task) + if done_task.cancelled(): + return + exc = done_task.exception() + if exc is not None: + done_task.get_loop().call_exception_handler( + { + "message": "TaskManager 后台任务执行失败", + "exception": exc, + "task": done_task, + } + ) + + task.add_done_callback(_done) async def add_task( self, @@ -212,61 +239,61 @@ async def add_task( uid = uuid.UUID(id) - if mode == "ScriptConfig": - if uid in Config.ScriptConfig: + async with self._mutex: + if mode == "ScriptConfig": + if uid in Config.ScriptConfig: + task_uid = uuid.uuid4() + queue_id = None + script_uid = uid + user_uid = "Default" + else: + for script_id, script in Config.ScriptConfig.items(): + if uid in script.UserData: + task_uid = uuid.uuid4() + queue_id = None + script_uid = script_id + user_uid = uid + break + else: + raise ValueError(f"任务 {uid} 无法找到对应脚本配置") + elif uid in Config.QueueConfig: + task_uid = uuid.uuid4() + queue_id = uid + script_uid = None + user_uid = None + elif uid in Config.ScriptConfig: task_uid = uuid.uuid4() queue_id = None script_uid = uid - user_uid = "Default" + user_uid = None else: - for script_id, script in Config.ScriptConfig.items(): - if uid in script.UserData: - task_uid = uuid.uuid4() - queue_id = None - script_uid = script_id - user_uid = uid - break - else: - raise ValueError(f"任务 {uid} 无法找到对应脚本配置") - elif uid in Config.QueueConfig: - task_uid = uuid.uuid4() - queue_id = uid - script_uid = None - user_uid = None - elif uid in Config.ScriptConfig: - task_uid = uuid.uuid4() - queue_id = None - script_uid = uid - user_uid = None - else: - raise ValueError(f"任务 {uid} 无法找到对应脚本配置") + raise ValueError(f"任务 {uid} 无法找到对应脚本配置") - if script_uid is not None and Config.ScriptConfig[script_uid].is_locked: - raise RuntimeError( - f"任务 {Config.ScriptConfig[script_uid].get('Info', 'Name')} 已在运行" - ) + if script_uid is not None and Config.ScriptConfig[script_uid].is_locked: + raise RuntimeError( + f"任务 {Config.ScriptConfig[script_uid].get('Info', 'Name')} 已在运行" + ) - logger.info(f"创建任务: {task_uid}, 模式: {mode}") - if new_task_info: - new_task_info["newTask"] = str(task_uid) - await Config.send_websocket_message( - id="TaskManager", type="Signal", data=new_task_info + logger.info(f"创建任务: {task_uid}, 模式: {mode}") + if new_task_info: + new_task_info["newTask"] = str(task_uid) + await Config.send_websocket_message( + id="TaskManager", type="Signal", data=new_task_info + ) + self.task_info[task_uid] = TaskInfo( + mode=mode, + task_id=str(task_uid), + queue_id=str(queue_id) if queue_id else None, + script_id=str(script_uid) if script_uid else None, + user_id=str(user_uid) if user_uid else None, ) - self.task_info[task_uid] = TaskInfo( - mode=mode, - task_id=str(task_uid), - queue_id=str(queue_id) if queue_id else None, - script_id=str(script_uid) if script_uid else None, - user_id=str(user_uid) if user_uid else None, - ) - self.task_handler[task_uid] = Task(self.task_info[task_uid]) - self.task_handler[task_uid].execute() - asyncio.create_task(self.clean_task(task_uid)) + self.task_handler[task_uid] = Task(self.task_info[task_uid]) + self.task_handler[task_uid].execute() + self._track_background_task(asyncio.create_task(self.clean_task(task_uid))) - return task_uid + return task_uid async def clean_task(self, task_uid: uuid.UUID) -> None: - await self.task_handler[task_uid].accomplish.wait() power_enabled = bool(self.task_info[task_uid].mode != "ScriptConfig") self.task_info.pop(task_uid, None) @@ -324,7 +351,6 @@ async def start_startup_queue(self): logger.info("开始运行启动时任务") for uid, queue in Config.QueueConfig.items(): - if queue.get("Info", "StartUpEnabled"): logger.info(f"启动时需要运行的队列:{uid}") await TaskManager.add_task( diff --git a/app/models/global_config.py b/app/models/global_config.py index 4fba3bbc..0348c8c6 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -231,7 +231,7 @@ def getStage(self) -> str: # noqa: N802 "Activity": side_story["Activity"], } ) - except Exception: + except (json.JSONDecodeError, KeyError, TypeError, ValueError): return "{ }" stage_data: dict[str, Any] = {"Info": activity_stage_drop_info} diff --git a/app/models/task.py b/app/models/task.py index 1b796663..3b94cff5 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -44,6 +44,10 @@ def _default_script_list() -> list[ScriptItem]: return [] +def _default_pending_tasks() -> set[asyncio.Task[Any]]: + return set() + + @dataclass class LogRecord: content: list[str] = field(default_factory=_default_content) @@ -66,7 +70,7 @@ def __setattr__(self, name: str, value: Any) -> None: if name in ("user_id", "name", "status") and self._task_item_ref is not None: ti = self._task_item_ref() if ti is not None: - asyncio.create_task(ti.on_change()) + ti.create_tracked_task(ti.on_change()) @property def result(self) -> str: @@ -102,7 +106,7 @@ def __setattr__(self, name: str, value: Any) -> None: object.__setattr__(user, "_task_item_ref", self._task_item_ref) if name not in ("_task_item_ref",) and self.task_info is not None: - asyncio.create_task(self.task_info.on_change()) + self.task_info.create_tracked_task(self.task_info.on_change()) @property def task_info(self) -> Optional[TaskItem]: @@ -133,6 +137,9 @@ class TaskItem(ABC): default_factory=_default_script_list ) # 脚本信息列表 current_index: int = -1 # 当前执行的脚本索引,-1 表示未开始 + _pending_tasks: set[asyncio.Task[Any]] = field( + default_factory=_default_pending_tasks, init=False + ) def __setattr__(self, name: str, value: Any) -> None: super().__setattr__(name, value) @@ -150,6 +157,31 @@ def _bind_task_item(self, item: ScriptItem) -> None: for user in item.user_list: object.__setattr__(user, "_task_item_ref", ti_ref) + def create_tracked_task(self, coro: Any) -> asyncio.Task[Any]: + """创建并跟踪异步任务,避免任务异常被静默吞掉。""" + + task = asyncio.create_task(coro) + self._pending_tasks.add(task) + + def _finalize(done_task: asyncio.Task[Any]) -> None: + self._pending_tasks.discard(done_task) + if done_task.cancelled(): + return + exception = done_task.exception() + if exception is not None: + # 在事件循环中转抛,便于统一异常处理器捕获 + loop = done_task.get_loop() + loop.call_exception_handler( + { + "message": "TaskItem 子任务执行失败", + "exception": exception, + "task": done_task, + } + ) + + task.add_done_callback(_finalize) + return task + @abstractmethod async def on_change(self) -> None: """统一回调入口""" diff --git a/app/services/notification.py b/app/services/notification.py index 0fe1957c..a234f3cf 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -25,19 +25,20 @@ import smtplib import httpx from datetime import datetime -from plyer import notification # pyright: ignore[reportMissingTypeStubs] +from importlib import import_module from email.header import Header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, cast from app.core import Config from app.models import Webhook from app.utils import get_logger, ImageUtils logger = get_logger("通知服务") +notification = import_module("plyer").notification class Notification: @@ -232,9 +233,14 @@ async def WebhookPush(self, title: str, content: str, webhook: Webhook) -> None: # 递归替换JSON对象中的变量 def replace_variables(obj: Any) -> Any: if isinstance(obj, dict): - return {k: replace_variables(v) for k, v in obj.items()} + obj_mapping = cast(dict[str, Any], obj) + return { + key: replace_variables(value) + for key, value in obj_mapping.items() + } elif isinstance(obj, list): - return [replace_variables(item) for item in obj] + obj_list = cast(list[Any], obj) + return [replace_variables(item) for item in obj_list] elif isinstance(obj, str): result = obj for key, value in template_vars.items(): @@ -283,8 +289,9 @@ def replace_variables(obj: Any) -> Any: response: httpx.Response if webhook.get("Data", "Method") == "POST": if isinstance(data, dict): + payload = cast(dict[str, Any], data) response = await client.post( - url=webhook.get("Data", "Url"), json=data, headers=headers + url=webhook.get("Data", "Url"), json=payload, headers=headers ) elif isinstance(data, str): response = await client.post( @@ -300,7 +307,7 @@ def replace_variables(obj: Any) -> Any: if isinstance(data, dict): # Flatten params to ensure all values are str or list of str params: dict[str, str] = {} - for k, v in data.items(): + for k, v in cast(dict[str, Any], data).items(): if isinstance(v, (dict, list)): params[str(k)] = json.dumps(v, ensure_ascii=False) else: diff --git a/app/services/update.py b/app/services/update.py index fab22490..ebd3b04f 100644 --- a/app/services/update.py +++ b/app/services/update.py @@ -32,6 +32,13 @@ from datetime import datetime, timedelta from typing import Any, List, Dict, Optional, cast from pathlib import Path +from tenacity import ( + AsyncRetrying, + RetryCallState, + retry_if_exception_type, + stop_after_attempt, + wait_exponential, +) from app.core import Config from app.utils.constants import MIRROR_ERROR_INFO @@ -41,9 +48,17 @@ logger = get_logger("更新服务") +def _log_retry_before_sleep(retry_state: RetryCallState) -> None: + """输出重试等待日志。""" + + attempt = retry_state.attempt_number + reason = retry_state.outcome.exception() if retry_state.outcome else None + logger.warning(f"下载失败,准备第 {attempt + 1} 次重试,原因: {reason}") + + class _UpdateHandler: def __init__(self) -> None: - self.is_locked: bool = False + self._operation_lock = asyncio.Lock() self.remote_version: Optional[str] = None self.last_check_time: Optional[datetime] = None self.update_version_info: Optional[Dict[str, List[str]]] = None @@ -52,6 +67,8 @@ def __init__(self) -> None: async def check_update( self, current_version: str, if_force: bool = False ) -> tuple[bool, str, Dict[str, List[str]]]: + """检查更新并返回是否有新版本、远端版本号与变更摘要。""" + if ( not if_force and self.remote_version is not None @@ -131,104 +148,93 @@ async def check_update( return False, current_version, {} async def download_update(self) -> None: + """下载更新包并通过 WebSocket 上报进度。""" + logger.info("收到前端下载请求") - if self.is_locked: + if self._operation_lock.locked(): await Config.send_websocket_message( id="Update", type="Signal", data={"Failed": "已有更新任务在进行中, 请勿重复操作"}, ) return None + async with self._operation_lock: + if self.remote_version is None: + await Config.send_websocket_message( + id="Update", + type="Signal", + data={"Failed": "未检测到可用的远程版本, 请先检查更新"}, + ) + return None - self.is_locked = True - - if self.remote_version is None: - await Config.send_websocket_message( - id="Update", - type="Signal", - data={"Failed": "未检测到可用的远程版本, 请先检查更新"}, - ) - self.is_locked = False - return None + if (Path.cwd() / f"UpdatePack_{self.remote_version}.zip").exists(): + logger.info( + f"更新包已存在: {Path.cwd() / f'UpdatePack_{self.remote_version}.zip'}" + ) + await Config.send_websocket_message( + id="Update", + type="Signal", + data={ + "Accomplish": str( + Path.cwd() / f"UpdatePack_{self.remote_version}.zip" + ) + }, + ) + return None - if (Path.cwd() / f"UpdatePack_{self.remote_version}.zip").exists(): - logger.info( - f"更新包已存在: {Path.cwd() / f'UpdatePack_{self.remote_version}.zip'}" - ) - await Config.send_websocket_message( - id="Update", - type="Signal", - data={ - "Accomplish": str( - Path.cwd() / f"UpdatePack_{self.remote_version}.zip" - ) - }, - ) - self.is_locked = False - return None + if Config.get("Update", "Source") == "GitHub": + download_url = f"https://github.com/AUTO-MAS-Project/AUTO-MAS/releases/download/{self.remote_version}/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" - if Config.get("Update", "Source") == "GitHub": - download_url = f"https://github.com/AUTO-MAS-Project/AUTO-MAS/releases/download/{self.remote_version}/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" + elif Config.get("Update", "Source") == "MirrorChyan": + if self.mirror_chyan_download_url is None: + logger.warning("MirrorChyan 未返回下载链接, 使用自建下载站") + download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" + else: + download_url = self.mirror_chyan_download_url - elif Config.get("Update", "Source") == "MirrorChyan": - if self.mirror_chyan_download_url is None: - logger.warning("MirrorChyan 未返回下载链接, 使用自建下载站") + elif Config.get("Update", "Source") == "AutoSite": download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" - else: - download_url = self.mirror_chyan_download_url - - elif Config.get("Update", "Source") == "AutoSite": - download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS-Lite-Setup-{self.remote_version}-x64.zip" - else: - await Config.send_websocket_message( - id="Update", - type="Signal", - data={ - "Failed": f"未知的下载源: {Config.get('Update', 'Source')}, 请检查配置文件" - }, - ) - self.is_locked = False - return None + else: + await Config.send_websocket_message( + id="Update", + type="Signal", + data={ + "Failed": f"未知的下载源: {Config.get('Update', 'Source')}, 请检查配置文件" + }, + ) + return None - logger.info(f"开始下载: {download_url}") + logger.info(f"开始下载: {download_url}") - check_times = 3 - while check_times != 0: - try: + async def _download_once() -> None: # 清理可能存在的临时文件 if (Path.cwd() / "download.temp").exists(): (Path.cwd() / "download.temp").unlink() start_time = time.time() + downloaded_size = 0 - # 使用 httpx 异步流式下载 async with httpx.AsyncClient(follow_redirects=True) as client: async with client.stream( "GET", download_url, timeout=30.0 ) as response: status_code = response.status_code - if status_code not in [200, 206]: - if check_times != -1: - check_times -= 1 - - logger.warning( - f"连接失败: {download_url}, 状态码: {status_code}, 剩余重试次数: {check_times}" + raise httpx.HTTPStatusError( + f"下载响应状态码异常: {status_code}", + request=response.request, + response=response, ) - await asyncio.sleep(1) - continue logger.info(f"连接成功: {download_url}, 状态码: {status_code}") file_size = int(response.headers.get("content-length", 0) or 0) - downloaded_size = 0 last_download_size = 0 - speed = 0 + speed = 0.0 last_time = time.time() - # 使用 aiofiles 异步写入临时文件 async with aiofiles.open( Path.cwd() / "download.temp", "wb" ) as f: @@ -238,7 +244,6 @@ async def download_update(self) -> None: await f.write(chunk) downloaded_size += len(chunk) - # 更新指定线程的下载进度, 每秒更新一次 if time.time() - last_time >= 1.0: elapsed = time.time() - last_time if elapsed <= 0: @@ -259,7 +264,6 @@ async def download_update(self) -> None: }, ) - # 重命名临时文件为最终包 (Path.cwd() / "download.temp").rename( Path.cwd() / f"UpdatePack_{self.remote_version}.zip" ) @@ -276,28 +280,33 @@ async def download_update(self) -> None: ) }, ) - self.is_locked = False - break - except Exception as e: - if check_times != -1: - check_times -= 1 - - logger.exception( - f"下载出错: {download_url}, 错误信息: {e}, 剩余重试次数: {check_times}" + try: + async for attempt in AsyncRetrying( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=1, max=10), + retry=retry_if_exception_type( + (httpx.HTTPError, OSError, asyncio.TimeoutError) + ), + before_sleep=_log_retry_before_sleep, + reraise=True, + ): + with attempt: + await _download_once() + except (httpx.HTTPError, OSError, asyncio.TimeoutError) as e: + if (Path.cwd() / "download.temp").exists(): + (Path.cwd() / "download.temp").unlink() + logger.exception(f"下载出错: {download_url}, 错误信息: {e}") + await Config.send_websocket_message( + id="Update", + type="Signal", + data={"Failed": f"下载失败: {download_url}"}, ) - await asyncio.sleep(1) - - else: - if (Path.cwd() / "download.temp").exists(): - (Path.cwd() / "download.temp").unlink() - await Config.send_websocket_message( - id="Update", type="Signal", data={"Failed": f"下载失败: {download_url}"} - ) - self.is_locked = False async def install_update(self): - if self.is_locked: + """解压并安装已下载的更新包。""" + + if self._operation_lock.locked(): await Config.send_websocket_message( id="Update", type="Signal", @@ -305,77 +314,74 @@ async def install_update(self): ) return None - logger.info("开始应用更新") - self.is_locked = True + async with self._operation_lock: + logger.info("开始应用更新") - versions = { - version.parse(match.group(1)): f.name - for f in Path.cwd().glob("UpdatePack_*.zip") - if (match := re.match(r"UpdatePack_(.+)\.zip$", f.name)) - } - logger.info(f"检测到的更新包: {versions.values()}") + versions = { + version.parse(match.group(1)): f.name + for f in Path.cwd().glob("UpdatePack_*.zip") + if (match := re.match(r"UpdatePack_(.+)\.zip$", f.name)) + } + logger.info(f"检测到的更新包: {versions.values()}") - if not versions: - await Config.send_websocket_message( - id="Update", - type="Signal", - data={"Failed": "未检测到更新包, 请先下载更新"}, - ) - self.is_locked = False - return None + if not versions: + await Config.send_websocket_message( + id="Update", + type="Signal", + data={"Failed": "未检测到更新包, 请先下载更新"}, + ) + return None - update_package = Path.cwd() / versions[max(versions)] + update_package = Path.cwd() / versions[max(versions)] - logger.info(f"开始解压: {update_package} 到 {Path.cwd()}") + logger.info(f"开始解压: {update_package} 到 {Path.cwd()}") - try: - with zipfile.ZipFile(update_package, "r") as zip_ref: - zip_ref.extractall(Path.cwd()) - except Exception as e: - logger.error(f"解压失败, {type(e).__name__}: {e}") - await Config.send_websocket_message( - id="Update", - type="Info", - data={"Error": f"解压失败, {type(e).__name__}: {e}"}, + try: + with zipfile.ZipFile(update_package, "r") as zip_ref: + zip_ref.extractall(Path.cwd()) + except (OSError, zipfile.BadZipFile, RuntimeError) as e: + logger.error(f"解压失败, {type(e).__name__}: {e}") + await Config.send_websocket_message( + id="Update", + type="Info", + data={"Error": f"解压失败, {type(e).__name__}: {e}"}, + ) + return None + + logger.success(f"解压完成: {update_package} 到 {Path.cwd()}") + + logger.info("正在删除临时文件与旧更新包文件") + if (Path.cwd() / "changes.json").exists(): + (Path.cwd() / "changes.json").unlink() + for f in versions.values(): + if (Path.cwd() / f).exists(): + (Path.cwd() / f).unlink() + + logger.info("正在清理旧版本注册表项") + await ProcessRunner.run_process( + "reg", + "delete", + r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{D116A92A-E174-4699-B777-61C5FD837B19}_is1", + "/f", ) - self.is_locked = False - return None - - logger.success(f"解压完成: {update_package} 到 {Path.cwd()}") - - logger.info("正在删除临时文件与旧更新包文件") - if (Path.cwd() / "changes.json").exists(): - (Path.cwd() / "changes.json").unlink() - for f in versions.values(): - if (Path.cwd() / f).exists(): - (Path.cwd() / f).unlink() - - logger.info("正在清理旧版本注册表项") - await ProcessRunner.run_process( - "reg", - "delete", - r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{D116A92A-E174-4699-B777-61C5FD837B19}_is1", - "/f", - ) - logger.success("清理完成") - - logger.info("启动更新程序") - self.is_locked = False - subprocess.Popen( - [ - Path.cwd() / "AUTO-MAS-Setup.exe", - "/SP-", - "/SILENT", - "/NOCANCEL", - "/FORCECLOSEAPPLICATIONS", - "/LANG=Chinese", - f"/DIR={Path.cwd()}", - ], - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.DETACHED_PROCESS - | subprocess.CREATE_NO_WINDOW, - ) - await System.set_power("KillSelf") + logger.success("清理完成") + + logger.info("启动更新程序") + subprocess.Popen( + [ + Path.cwd() / "AUTO-MAS-Setup.exe", + "/SP-", + "/SILENT", + "/NOCANCEL", + "/FORCECLOSEAPPLICATIONS", + "/LANG=Chinese", + f"/DIR={Path.cwd()}", + ], + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.DETACHED_PROCESS + | subprocess.CREATE_NO_WINDOW, + ) + await System.set_power("KillSelf") Updater = _UpdateHandler() diff --git a/app/task/SRC/tools/poor_yaml.py b/app/task/SRC/tools/poor_yaml.py index a11e7f29..dc98805a 100644 --- a/app/task/SRC/tools/poor_yaml.py +++ b/app/task/SRC/tools/poor_yaml.py @@ -25,68 +25,43 @@ # Contact: DLmaster_361@163.com -import re from pathlib import Path -from typing import Any +from typing import Any, cast -from app.utils import decode_bytes +from ruamel.yaml import YAML + + +_YAML = YAML(typ="safe") +_YAML.default_flow_style = False + + +def _load_yaml_mapping(path: Path) -> dict[str, Any]: + loaded: Any = cast(Any, _YAML).load(path.read_text(encoding="utf-8")) + if loaded is None: + return {} + if not isinstance(loaded, dict): + raise ValueError(f"YAML 根节点必须是字典: {path}") + mapping = cast(dict[object, Any], loaded) + return {str(key): value for key, value in mapping.items()} def poor_yaml_read(file: Path) -> dict[str, Any]: - """ - Poor implementation to load yaml without pyyaml dependency, but with re - - Args: - file (Path): yaml file path - - Returns: - dict: - """ - content = decode_bytes(file.read_bytes()) - data: dict[str, Any] = {} - regex = re.compile(r"^(.*?):(.*?)$") - for line in content.splitlines(): - line = line.strip("\n\r\t ").replace("\\", "/") - if line.startswith("#"): - continue - result = re.match(regex, line) - if result: - k, v = result.group(1), result.group(2).strip("\n\r\t' ") - if v: - if v.lower() == "null": - v = None - elif v.lower() == "false": - v = False - elif v.lower() == "true": - v = True - elif v.isdigit(): - v = int(v) - data[k] = v - - return data + """读取 YAML 文件并返回字典对象。""" + + return _load_yaml_mapping(file) def poor_yaml_write( data: dict[str, Any], file: Path, template_file: Path | None = None ) -> None: - """ - Args: - data (dict): - file (Path): yaml file path - template_file (Path | None): template file path - """ - if template_file is None: - template_file = file - text = decode_bytes(template_file.read_bytes()) - text = text.replace("\\", "/") - - for key, value in data.items(): - if value is None: - value = "null" - elif value is True: - value = "true" - elif value is False: - value = "false" - text = re.sub(f"{key}:.*?\n", f"{key}: {value}\n", text) - - file.write_text(text, encoding="utf-8") + """写入 YAML 文件;若提供模板则先加载模板并合并。""" + + merged: dict[str, Any] = {} + if template_file is not None and template_file.exists(): + merged = _load_yaml_mapping(template_file) + + merged.update(data) + + file.parent.mkdir(parents=True, exist_ok=True) + with file.open("w", encoding="utf-8") as output: + cast(Any, _YAML).dump(merged, output) diff --git a/app/utils/OCR/OCRtool.py b/app/utils/OCR/OCRtool.py index 7872c421..e0d78c28 100644 --- a/app/utils/OCR/OCRtool.py +++ b/app/utils/OCR/OCRtool.py @@ -5,7 +5,7 @@ from PIL import Image import win32con import win32gui -from rapidocr_onnxruntime import RapidOCR # pyright: ignore[reportMissingTypeStubs] +from importlib import import_module from mss import mss import subprocess from pathlib import Path @@ -38,6 +38,7 @@ # 你现在已经学会了OCR识别的基础知识了!快来试试吧! logger = get_logger("OCR模块") +RapidOCR = cast(Any, import_module("rapidocr_onnxruntime")).RapidOCR class OCRTool: @@ -101,13 +102,16 @@ def get_system_dpi_scaling(self) -> float: try: # 启用高 DPI 感知,避免始终返回 96 try: - ctypes.windll.shcore.SetProcessDpiAwareness(2) # type: ignore[attr-defined] + shcore = getattr(ctypes.windll, "shcore", None) + set_dpi_awareness = getattr(shcore, "SetProcessDpiAwareness", None) + if callable(set_dpi_awareness): + set_dpi_awareness(2) except (AttributeError, OSError): # Windows 8.1 以下系统没有该函数 pass # 获取主显示器 DC - hdc: Any = win32gui.GetDC(0) + hdc = cast(int, cast(Any, win32gui).GetDC(0)) try: # 使用 ctypes 直接调用 GetDeviceCaps # LOGPIXELSX = 88 @@ -285,7 +289,7 @@ def _force_activate_window(hwnd: int) -> None: if foreground_thread_id != target_thread_id and foreground_thread_id != 0: try: # 附着到前台窗口的输入线程 - win32process.AttachThreadInput( + cast(Any, win32process).AttachThreadInput( foreground_thread_id, target_thread_id, True ) attached = True @@ -320,7 +324,7 @@ def _force_activate_window(hwnd: int) -> None: # 分离输入线程(必须在 finally 中执行,确保一定会分离) if attached: try: - win32process.AttachThreadInput( + cast(Any, win32process).AttachThreadInput( foreground_thread_id, target_thread_id, False ) logger.debug("已分离输入线程") @@ -342,7 +346,7 @@ def _find_window_with_win32gui(title: str) -> int | None: Returns: int | None: 找到的窗口句柄,未找到返回 None """ - found_windows = [] + found_windows: list[tuple[int, str]] = [] def enum_callback(hwnd: int, results: list[tuple[int, str]]) -> None: if win32gui.IsWindowVisible(hwnd): diff --git a/app/utils/websocket.py b/app/utils/websocket.py index cc845e1c..000e7da9 100644 --- a/app/utils/websocket.py +++ b/app/utils/websocket.py @@ -24,13 +24,34 @@ import time import asyncio import json -from typing import Optional, Callable, Any, Dict, List +from collections import deque +from typing import Optional, Callable, Any, Dict, Deque from websockets.asyncio.client import connect, ClientConnection from websockets.exceptions import ConnectionClosed +from tenacity import ( + AsyncRetrying, + RetryCallState, + retry_if_exception_type, + stop_after_attempt, + stop_never, + wait_exponential, +) from app.utils.logger import get_logger + +_retry_logger = get_logger("WS重连") + + +def _log_ws_retry_before_sleep(retry_state: RetryCallState) -> None: + """输出 WebSocket 重连重试日志。""" + + attempt = retry_state.attempt_number + reason = retry_state.outcome.exception() if retry_state.outcome else None + _retry_logger.warning(f"连接失败,准备第 {attempt + 1} 次重试,原因: {reason}") + + # ============== WebSocket 客户端实例 ============== @@ -85,7 +106,6 @@ def __init__( self._running = False self._last_ping = 0.0 self._last_pong = 0.0 - self._reconnect_count = 0 self._tasks: list[asyncio.Task[Any]] = [] self._auth_token: Optional[str] = auth_token @@ -109,7 +129,6 @@ async def connect(self) -> bool: ) self._last_ping = time.monotonic() self._last_pong = time.monotonic() - self._reconnect_count = 0 self.logger.info(f"WebSocket 连接成功: {self.url}") @@ -337,47 +356,44 @@ async def _heartbeat_loop(self): self.logger.error(f"心跳循环异常: {type(e).__name__}: {e}") break - def _get_backoff_delay(self) -> float: - """ - 计算指数退避延迟时间 + async def _connect_or_raise(self) -> None: + """建立连接,失败时抛出异常以触发 tenacity 重试。""" - Returns: - float: 延迟时间(秒),最大60秒 - """ - # 指数退避: base_interval * 2^(reconnect_count - 1) - delay = self.reconnect_interval * (2 ** (self._reconnect_count - 1)) - # 限制最大延迟为60秒 - return min(delay, 60.0) + connected = await self.connect() + if not connected: + raise ConnectionError(f"连接失败: {self.url}") async def run(self): """ - 运行 WebSocket 客户端(包含自动重连,使用指数退避策略) + 运行 WebSocket 客户端(包含自动重连,使用 tenacity 退避策略) """ self._running = True while self._running: - # 尝试连接 - if not await self.connect(): - self._reconnect_count += 1 - - if ( - self.max_reconnect_attempts != -1 - and self._reconnect_count > self.max_reconnect_attempts - ): - self.logger.error( - f"已达到最大重连次数 ({self.max_reconnect_attempts}),停止重连" - ) - break - - delay = self._get_backoff_delay() - self.logger.info( - f"{delay:.1f}秒后尝试重连... (第 {self._reconnect_count} 次)" - ) - await asyncio.sleep(delay) - continue + retry_stop = ( + stop_never + if self.max_reconnect_attempts == -1 + else stop_after_attempt(self.max_reconnect_attempts) + ) + connected = False + async for attempt in AsyncRetrying( + stop=retry_stop, + wait=wait_exponential( + multiplier=self.reconnect_interval, + min=self.reconnect_interval, + max=60, + ), + retry=retry_if_exception_type(ConnectionError), + before_sleep=_log_ws_retry_before_sleep, + reraise=False, + ): + with attempt: + await self._connect_or_raise() + connected = True - # 连接成功,重置重连计数 - self._reconnect_count = 0 + if not connected: + self.logger.error("连接重试已耗尽,停止客户端") + break # 启动接收和心跳任务 receive_task = asyncio.create_task(self._receive_loop()) @@ -415,22 +431,7 @@ async def run(self): # 检查是否需要重连 if not self._running: break - - self._reconnect_count += 1 - if ( - self.max_reconnect_attempts != -1 - and self._reconnect_count > self.max_reconnect_attempts - ): - self.logger.error( - f"已达到最大重连次数 ({self.max_reconnect_attempts}),停止重连" - ) - break - - delay = self._get_backoff_delay() - self.logger.info( - f"{delay:.1f}秒后尝试重连... (第 {self._reconnect_count} 次)" - ) - await asyncio.sleep(delay) + self.logger.info("连接中断,将按重试策略重新建立连接") self.logger.info("WebSocket 客户端已停止") @@ -505,9 +506,9 @@ def __init__(self): self._clients: Dict[str, WebSocketClient] = {} self._system_clients: set[str] = set() # 系统客户端名称集合 self._tasks: Dict[str, asyncio.Task[Any]] = {} - self._message_history: Dict[str, List[Dict[str, Any]]] = {} + self._message_history: Dict[str, Deque[Dict[str, Any]]] = {} self._max_history_per_client = 200 - self._debug_connections: List[Any] = [] # WebSocket 连接列表 + self._debug_connections: list[Any] = [] # WebSocket 连接列表 self._logger = get_logger("WS管理器") def get_client(self, name: str) -> Optional[WebSocketClient]: @@ -593,7 +594,7 @@ async def on_disconnect(): ) self._clients[name] = client - self._message_history[name] = [] + self._message_history[name] = deque(maxlen=self._max_history_per_client) self._logger.info(f"已创建 WebSocket 客户端: {name} -> {url}") return client @@ -712,16 +713,12 @@ async def send_auth( async def _record_message(self, name: str, direction: str, data: Dict[str, Any]): """记录消息""" if name not in self._message_history: - self._message_history[name] = [] + self._message_history[name] = deque(maxlen=self._max_history_per_client) record = {"direction": direction, "timestamp": time.time(), "data": data} self._message_history[name].append(record) - # 限制历史记录数量 - if len(self._message_history[name]) > self._max_history_per_client: - self._message_history[name].pop(0) - # 广播给调试前端 await self._broadcast_message(name, record) @@ -737,7 +734,7 @@ async def _broadcast_event(self, event: Dict[str, Any]): async def _broadcast(self, data: Dict[str, Any]): """广播数据给所有调试前端""" - disconnected: List[Any] = [] + disconnected: list[Any] = [] for ws in self._debug_connections: try: await ws.send_json(data) @@ -768,20 +765,23 @@ async def _auto_auth_koishi(self): def get_message_history( self, name: Optional[str] = None - ) -> Dict[str, List[Dict[str, Any]]]: + ) -> Dict[str, list[Dict[str, Any]]]: """获取消息历史""" if name: - return {name: self._message_history.get(name, [])} - return self._message_history.copy() + return {name: list(self._message_history.get(name, deque()))} + return { + client_name: list(records) + for client_name, records in self._message_history.items() + } def clear_message_history(self, name: Optional[str] = None): """清空消息历史""" if name: if name in self._message_history: - self._message_history[name] = [] + self._message_history[name].clear() else: for key in self._message_history: - self._message_history[key] = [] + self._message_history[key].clear() def add_debug_connection(self, ws: Any): """添加调试前端连接""" diff --git a/requirements.txt b/requirements.txt index 5d39cec1..df5d7975 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,6 @@ opencv-python==4.10.0.84 maafw==5.8.1 mss==9.0.2 fastapi-mcp==0.4.0 +ruamel.yaml==0.18.16 +tenacity==9.1.2 +filelock==3.19.1 From af6c846c2e373024ce7eab089d2ecf902c10d744 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 6 Apr 2026 19:41:48 +0800 Subject: [PATCH 16/29] =?UTF-8?q?revert(config):=20=E5=9B=9E=E9=80=80api?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=20=E4=BF=AE=E6=94=B9config=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/common.py | 357 +---------------------------- app/api/core.py | 5 +- app/api/dispatch.py | 13 +- app/api/emulator.py | 117 +++------- app/api/history.py | 7 +- app/api/info.py | 25 +- app/api/ocr.py | 28 +-- app/api/plan.py | 47 ++-- app/api/queue.py | 242 ++++++------------- app/api/scripts.py | 139 +++++------ app/api/setting.py | 132 ++++------- app/api/tools.py | 28 +-- app/api/update.py | 17 +- app/api/ws_debug.py | 27 ++- app/contracts/__init__.py | 4 +- app/contracts/common_contract.py | 163 ++++++++----- app/contracts/emulator_contract.py | 16 +- app/contracts/general_contract.py | 30 +-- app/contracts/maa_contract.py | 42 +--- app/contracts/maaend_contract.py | 30 +-- app/contracts/queue_contract.py | 44 ++-- app/contracts/scripts_contract.py | 54 ++--- app/contracts/setting_contract.py | 30 +-- app/contracts/src_contract.py | 28 +-- app/contracts/tools_contract.py | 16 +- 25 files changed, 488 insertions(+), 1153 deletions(-) diff --git a/app/api/common.py b/app/api/common.py index f977bd20..a31da36e 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -1,12 +1,9 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Awaitable, Callable from functools import wraps -import inspect import asyncio -from typing import Any, ParamSpec, TypeVar, cast - -from fastapi import APIRouter +from typing import ParamSpec, TypeVar from app.contracts.common_contract import ComboBoxItem, ComboBoxOut, OutBase @@ -26,26 +23,6 @@ ) -def _docstring_summary(func: Callable[..., Any]) -> str | None: - doc = inspect.getdoc(func) - if not doc: - return None - first_line = doc.splitlines()[0].strip() - return first_line or None - - -def _resolve_model_cls( - *, - model_cls: type[OutBase] | None, - response_model: type[Any] | None, -) -> type[OutBase]: - if model_cls is not None: - return model_cls - if isinstance(response_model, type) and issubclass(response_model, OutBase): - return response_model - raise TypeError("model_cls 为空时,response_model 必须是 OutBase 的子类") - - def error_out( model_cls: type[OutT], exc: Exception, @@ -107,341 +84,11 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> OutT: return decorator -def api_post( - router: APIRouter, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - response_model: type[Any] | None = None, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - """统一 POST 路由注册装饰器(兼容入口)。""" - - return cast(Any, bind_api(router).post)( - path, - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - response_model=response_model, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - -class ApiRegistrar: - """API 装饰器注册器。""" - - def __init__(self, router: APIRouter): - self.router = router - - def route( - self, - path: str, - *, - methods: Iterable[str], - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - tags: list[str] | None = None, - summary: str | None = None, - response_model: type[Any] | None = None, - status_code: int = 200, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, - ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - """统一路由注册:路由 + 守卫 + 可选 WS 命令。""" - - resolved_model_cls = _resolve_model_cls( - model_cls=model_cls, - response_model=response_model, - ) - - guard = api_guard( - model_cls=resolved_model_cls, - message=message, - on_error=on_error, - **fallback_kwargs, - ) - - def decorator(func: Callable[P, Awaitable[Any]]) -> Callable[P, Awaitable[Any]]: - resolved_summary = summary or _docstring_summary(func) - final_route_kwargs: dict[str, Any] = dict(route_kwargs or {}) - - final_route_kwargs.setdefault("status_code", status_code) - final_route_kwargs.setdefault( - "response_model", response_model or resolved_model_cls - ) - if tags is not None: - final_route_kwargs["tags"] = tags - if resolved_summary is not None: - final_route_kwargs.setdefault("summary", resolved_summary) - - route = self.router.api_route( - path, - methods=list(methods), - **final_route_kwargs, - ) - - wrapped = cast(Any, guard)(func) - if ws_endpoint is not None: - from app.api.ws_command import ws_command - - wrapped = ws_command(ws_endpoint)(wrapped) - return route(wrapped) - - return decorator - - def get( - self, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - tags: list[str] | None = None, - summary: str | None = None, - response_model: type[Any] | None = None, - status_code: int = 200, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, - ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - return self.route( - path, - methods=("GET",), - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - tags=tags, - summary=summary, - response_model=response_model, - status_code=status_code, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - def post( - self, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - tags: list[str] | None = None, - summary: str | None = None, - response_model: type[Any] | None = None, - status_code: int = 200, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, - ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - return self.route( - path, - methods=("POST",), - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - tags=tags, - summary=summary, - response_model=response_model, - status_code=status_code, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - def patch( - self, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - tags: list[str] | None = None, - summary: str | None = None, - response_model: type[Any] | None = None, - status_code: int = 200, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, - ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - return self.route( - path, - methods=("PATCH",), - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - tags=tags, - summary=summary, - response_model=response_model, - status_code=status_code, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - def delete( - self, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - tags: list[str] | None = None, - summary: str | None = None, - response_model: type[Any] | None = None, - status_code: int = 200, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, - ) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - return self.route( - path, - methods=("DELETE",), - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - tags=tags, - summary=summary, - response_model=response_model, - status_code=status_code, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - -def bind_api(router: APIRouter) -> ApiRegistrar: - """绑定一个 APIRouter 并返回声明式 API 注册器。""" - - return ApiRegistrar(router) - - -def api_route( - router: APIRouter, - path: str, - *, - methods: Iterable[str], - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - response_model: type[Any] | None = None, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - """统一路由注册装饰器(兼容入口)。""" - - return cast(Any, bind_api(router).route)( - path, - methods=methods, - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - response_model=response_model, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - -def api_get( - router: APIRouter, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - response_model: type[Any] | None = None, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - """统一 GET 路由注册装饰器(兼容入口)。""" - - return cast(Any, bind_api(router).get)( - path, - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - response_model=response_model, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - -def api_patch( - router: APIRouter, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - response_model: type[Any] | None = None, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - """统一 PATCH 路由注册装饰器(兼容入口)。""" - - return cast(Any, bind_api(router).patch)( - path, - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - response_model=response_model, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - -def api_delete( - router: APIRouter, - path: str, - *, - model_cls: type[OutBase] | None = None, - ws_endpoint: str | None = None, - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - response_model: type[Any] | None = None, - route_kwargs: dict[str, object] | None = None, - **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[Any]]], Callable[P, Awaitable[Any]]]: - """统一 DELETE 路由注册装饰器(兼容入口)。""" - - return cast(Any, bind_api(router).delete)( - path, - model_cls=model_cls, - ws_endpoint=ws_endpoint, - message=message, - on_error=on_error, - response_model=response_model, - route_kwargs=route_kwargs, - **fallback_kwargs, - ) - - __all__ = [ "OutBase", "ComboBoxItem", "ComboBoxOut", "error_out", "run_api", - "bind_api", - "ApiRegistrar", "api_guard", - "api_route", - "api_get", - "api_post", - "api_patch", - "api_delete", ] diff --git a/app/api/core.py b/app/api/core.py index 3a032acd..8c96d8d7 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -32,11 +32,10 @@ from app.contracts.common_contract import OutBase from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.utils import get_logger router = APIRouter(prefix="/api/core", tags=["核心信息"]) -api = bind_api(router) logger = get_logger("DEV") @@ -100,7 +99,7 @@ async def connect_websocket(websocket: WebSocket): @ws_command("core.close") -@api.post( +@router.post( "/close", summary="关闭后端程序", response_model=OutBase, diff --git a/app/api/dispatch.py b/app/api/dispatch.py index 27e7171f..da20bf3b 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -33,13 +33,12 @@ TaskCreateIn, TaskCreateOut, ) -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) -api = bind_api(router) -@api.post( +@router.post( "/start", tags=["Action"], summary="添加任务", @@ -53,7 +52,7 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: return TaskCreateOut(taskId=str(task_id)) -@api.post( +@router.post( "/stop", tags=["Action"], summary="中止任务", @@ -67,7 +66,7 @@ async def stop_task(task: DispatchIn = Body(...)) -> OutBase: return OutBase() -@api.post( +@router.post( "/get/power", tags=["Get"], summary="获取电源标志", @@ -81,7 +80,7 @@ async def get_power() -> PowerOut: return PowerOut(signal=signal) -@api.post( +@router.post( "/set/power", tags=["Action"], summary="设置电源标志", @@ -95,7 +94,7 @@ async def set_power(task: PowerIn = Body(...)) -> OutBase: return OutBase() -@api.post( +@router.post( "/cancel/power", tags=["Action"], summary="取消电源任务", diff --git a/app/api/emulator.py b/app/api/emulator.py index 1ccc51c5..32890735 100644 --- a/app/api/emulator.py +++ b/app/api/emulator.py @@ -25,11 +25,12 @@ from fastapi import APIRouter, Body, Path -from app.api.common import bind_api + from app.core import Config, EmulatorManager from app.contracts.common_contract import ( IndexOrderPatch, OutBase, + dump_writable_data, project_model, project_model_list, project_model_map, @@ -41,7 +42,6 @@ EmulatorDetailOut, EmulatorDeviceStatusOut, EmulatorGetOut, - EmulatorPatch, EmulatorRead, EmulatorSearchOut, EmulatorSearchResult, @@ -49,7 +49,6 @@ ) router = APIRouter(prefix="/api/emulator", tags=["模拟器管理"]) -api = bind_api(router) EmulatorIdPath = Annotated[str, Path(description="模拟器 ID")] EmulatorActionPath = Annotated[ @@ -58,81 +57,35 @@ ] -async def _build_emulator_collection_out() -> EmulatorGetOut: - index, data = await Config.get_emulator(None) - return EmulatorGetOut( - index=project_model_list(EmulatorConfigIndexItem, index), - data=project_model_map(EmulatorRead, data), - ) - - -async def _build_emulator_detail_out(emulator_id: str) -> EmulatorDetailOut: - _, data = await Config.get_emulator(emulator_id) - projected = project_model_map(EmulatorRead, data) - return EmulatorDetailOut(data=projected[emulator_id]) - - -async def _build_emulator_create_out() -> EmulatorCreateOut: - uid, config = await Config.add_emulator() - return EmulatorCreateOut( - id=str(uid), - data=project_model(EmulatorRead, await config.toDict()), - ) - - -async def _update_emulator_config(emulator_id: str, data: EmulatorPatch) -> OutBase: - await Config.update_emulator(emulator_id, data.model_dump(exclude_unset=True)) - return OutBase() - - -async def _delete_emulator_config(emulator_id: str) -> OutBase: - await Config.del_emulator(emulator_id) - return OutBase() - - -async def _build_emulator_status_out() -> EmulatorStatusOut: - return EmulatorStatusOut(data=await EmulatorManager.get_status(None)) - - -async def _build_emulator_device_status_out( - emulator_id: str, -) -> EmulatorDeviceStatusOut: - statuses = await EmulatorManager.get_status(emulator_id) - return EmulatorDeviceStatusOut(data=statuses.get(emulator_id, {})) - - -async def _build_emulator_search_out() -> EmulatorSearchOut: - from app.utils import search_all_emulators - - emulators = await search_all_emulators() - return EmulatorSearchOut(data=project_model_list(EmulatorSearchResult, emulators)) - - -@api.get( +@router.get( "", tags=["Get"], summary="查询全部模拟器配置", response_model=EmulatorGetOut, - index=[], - data={}, ) async def list_emulators() -> EmulatorGetOut: - return await _build_emulator_collection_out() + index, data = await Config.get_emulator(None) + return EmulatorGetOut( + index=project_model_list(EmulatorConfigIndexItem, index), + data=project_model_map(EmulatorRead, data), + ) -@api.post( +@router.post( "", tags=["Add"], summary="创建模拟器配置", response_model=EmulatorCreateOut, - id="", - data=EmulatorRead(), ) async def create_emulator() -> EmulatorCreateOut: - return await _build_emulator_create_out() + uid, config = await Config.add_emulator() + return EmulatorCreateOut( + id=str(uid), + data=project_model(EmulatorRead, await config.toDict()), + ) -@api.patch( +@router.patch( "/order", tags=["Update"], summary="重新排序模拟器", @@ -143,73 +96,77 @@ async def reorder_emulator(body: IndexOrderPatch = Body(...)) -> OutBase: return OutBase() -@api.get( +@router.get( "/detected", tags=["Get"], summary="搜索已安装的模拟器", response_model=EmulatorSearchOut, - data=[], ) async def detect_emulators() -> EmulatorSearchOut: - return await _build_emulator_search_out() + from app.utils import search_all_emulators + + emulators = await search_all_emulators() + return EmulatorSearchOut(data=project_model_list(EmulatorSearchResult, emulators)) -@api.get( +@router.get( "/status", tags=["Get"], summary="查询全部模拟器状态", response_model=EmulatorStatusOut, - data={}, ) async def get_emulator_statuses() -> EmulatorStatusOut: - return await _build_emulator_status_out() + return EmulatorStatusOut(data=await EmulatorManager.get_status(None)) -@api.get( +@router.get( "/{emulator_id}", tags=["Get"], summary="查询单个模拟器配置", response_model=EmulatorDetailOut, - data=EmulatorRead(), ) async def get_emulator(emulator_id: EmulatorIdPath) -> EmulatorDetailOut: - return await _build_emulator_detail_out(emulator_id) + _, data = await Config.get_emulator(emulator_id) + projected = project_model_map(EmulatorRead, data) + return EmulatorDetailOut(data=projected[emulator_id]) -@api.patch( +@router.patch( "/{emulator_id}", tags=["Update"], summary="更新模拟器配置", response_model=OutBase, ) async def update_emulator( - emulator_id: EmulatorIdPath, data: EmulatorPatch = Body(...) + emulator_id: EmulatorIdPath, data: EmulatorRead = Body(...) ) -> OutBase: - return await _update_emulator_config(emulator_id, data) + await Config.update_emulator(emulator_id, dump_writable_data(data)) + return OutBase() -@api.delete( +@router.delete( "/{emulator_id}", tags=["Delete"], summary="删除模拟器配置", response_model=OutBase, ) async def delete_emulator(emulator_id: EmulatorIdPath) -> OutBase: - return await _delete_emulator_config(emulator_id) + await Config.del_emulator(emulator_id) + return OutBase() -@api.get( +@router.get( "/{emulator_id}/status", tags=["Get"], summary="查询单个模拟器状态", response_model=EmulatorDeviceStatusOut, - data={}, ) async def get_emulator_status(emulator_id: EmulatorIdPath) -> EmulatorDeviceStatusOut: - return await _build_emulator_device_status_out(emulator_id) + statuses = await EmulatorManager.get_status(emulator_id) + return EmulatorDeviceStatusOut(data=statuses.get(emulator_id, {})) -@api.post( +@router.post( "/{emulator_id}/actions/{action}", tags=["Action"], summary="执行模拟器动作", diff --git a/app/api/history.py b/app/api/history.py index a939288b..ae7271dc 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -27,7 +27,7 @@ from pydantic import TypeAdapter from app.core import Config -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.contracts.history_contract import ( HistoryData, HistoryDataGetIn, @@ -38,7 +38,6 @@ ) router = APIRouter(prefix="/api/history", tags=["历史记录"]) -api = bind_api(router) HISTORY_INDEX_ADAPTER: TypeAdapter[list[HistoryIndexItem]] = TypeAdapter( list[HistoryIndexItem] @@ -54,7 +53,7 @@ def _build_history_data(raw: dict[str, object]) -> HistoryData: return HistoryData.model_validate(data) -@api.post( +@router.post( "/search", tags=["Get"], summary="搜索历史记录总览信息", @@ -79,7 +78,7 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: return HistorySearchOut(data=data) -@api.post( +@router.post( "/data", tags=["Get"], summary="从指定文件内获取历史记录数据", diff --git a/app/api/info.py b/app/api/info.py index 7824691e..58600537 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -34,7 +34,7 @@ InfoOut, OutBase, ) -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.contracts.info_contract import ( GetStageIn, NoticeOut, @@ -42,7 +42,6 @@ ) router = APIRouter(prefix="/api/info", tags=["信息获取"]) -api = bind_api(router) class EmulatorIdBody(ApiModel): @@ -60,7 +59,7 @@ def _to_combobox_items(raw_data: object) -> list[ComboBoxItem]: ) -@api.post( +@router.post( "/version", tags=["Get"], summary="获取后端git版本信息", @@ -84,7 +83,7 @@ async def get_git_version() -> VersionOut: ) -@api.post( +@router.post( "/combox/stage", tags=["Get"], summary="获取关卡号下拉框信息", @@ -101,7 +100,7 @@ async def get_stage_combox( return ComboBoxOut(data=data) -@api.post( +@router.post( "/combox/script", tags=["Get"], summary="获取脚本下拉框信息", @@ -116,7 +115,7 @@ async def get_script_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@api.post( +@router.post( "/combox/task", tags=["Get"], summary="获取可选任务下拉框信息", @@ -131,7 +130,7 @@ async def get_task_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@api.post( +@router.post( "/combox/plan", tags=["Get"], summary="获取可选计划下拉框信息", @@ -146,7 +145,7 @@ async def get_plan_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@api.post( +@router.post( "/combox/emulator", tags=["Get"], summary="获取可选模拟器下拉框信息", @@ -161,7 +160,7 @@ async def get_emulator_combox() -> ComboBoxOut: return ComboBoxOut(data=data) -@api.post( +@router.post( "/combox/emulator/devices", tags=["Get"], summary="获取可选模拟器多开实例下拉框信息", @@ -180,7 +179,7 @@ async def get_emulator_devices_combox( return ComboBoxOut(data=data) -@api.post( +@router.post( "/notice/get", tags=["Get"], summary="获取通知信息", @@ -194,7 +193,7 @@ async def get_notice_info() -> NoticeOut: return NoticeOut(if_need_show=if_need_show, data=data) -@api.post( +@router.post( "/notice/confirm", tags=["Action"], summary="确认通知", @@ -222,7 +221,7 @@ async def confirm_notice() -> OutBase: # return InfoOut(data=data) -@api.post( +@router.post( "/webconfig", tags=["Get"], summary="获取配置分享中心的配置信息", @@ -236,7 +235,7 @@ async def get_web_config() -> InfoOut: return InfoOut(data={"WebConfig": data}) -@api.post( +@router.post( "/get/overview", tags=["Get"], summary="信息总览", diff --git a/app/api/ocr.py b/app/api/ocr.py index 4589a070..7fa8c434 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -26,17 +26,15 @@ from typing import Optional import base64 from io import BytesIO -from PIL import Image from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger from app.contracts.common_contract import ApiModel, OutBase -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out, run_api +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out, run_api logger = get_logger("OCR API") router = APIRouter(prefix="/api/ocr", tags=["OCR识别"]) -api = bind_api(router) # ========== 截图相关模型 ========== @@ -125,14 +123,8 @@ class ClickOut(OutBase): attempts: int = Field(..., description="实际尝试次数") -def _encode_image_base64(image: Image.Image) -> str: - buffer = BytesIO() - image.save(buffer, format="PNG") - return base64.b64encode(buffer.getvalue()).decode("utf-8") - - # ========== 截图接口 ========== -@api.post( +@router.post( "/screenshot", tags=["Get"], summary="获取窗口截图", @@ -170,7 +162,9 @@ async def _success() -> OCRScreenshotOut: region=region, ) - image_base64 = _encode_image_base64(screenshot_image) + buffer = BytesIO() + screenshot_image.save(buffer, format="PNG") + image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") logger.info(f"成功截取窗口 [{params.window_title}] 的截图,区域: {region}") @@ -194,7 +188,7 @@ async def _success() -> OCRScreenshotOut: ) -@api.post( +@router.post( "/screenshot/adb", tags=["Get"], summary="通过ADB获取设备截图", @@ -280,7 +274,7 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh # ========== 测试接口:检查图像 ========== -@api.post( +@router.post( "/check/image", tags=["Get"], summary="检查是否存在指定图像", @@ -332,7 +326,7 @@ async def _success() -> CheckImageOut: ) -@api.post( +@router.post( "/check/image/any", tags=["Get"], summary="检查是否存在任意一个指定图像", @@ -386,7 +380,7 @@ async def _success() -> CheckImageOut: ) -@api.post( +@router.post( "/check/image/all", tags=["Get"], summary="检查是否存在所有指定图像", @@ -441,7 +435,7 @@ async def _success() -> CheckImageOut: # ========== 测试接口:点击操作 ========== -@api.post( +@router.post( "/click/image", tags=["Action"], summary="点击指定图像位置", @@ -493,7 +487,7 @@ async def _success() -> ClickOut: ) -@api.post( +@router.post( "/click/text", tags=["Action"], summary="点击指定文字位置", diff --git a/app/api/plan.py b/app/api/plan.py index e39076f0..0ed1789a 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -25,11 +25,12 @@ from fastapi import APIRouter, Body, Path -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.core import Config from app.contracts.common_contract import ( IndexOrderPatch, OutBase, + dump_writable_data, project_model, project_model_list, project_model_map, @@ -45,32 +46,15 @@ ) router = APIRouter(prefix="/api/plan", tags=["计划管理"]) -api = bind_api(router) PlanIdPath = Annotated[str, Path(description="计划 ID")] -async def _build_plan_collection_out() -> PlanGetOut: - index, data = await Config.get_plan(None) - return PlanGetOut( - index=project_model_list(PlanIndexItem, index), - data=project_model_map(MaaPlanRead, data), - ) - - -async def _build_plan_detail_out(plan_id: str) -> PlanDetailOut: - _, data = await Config.get_plan(plan_id) - projected = project_model_map(MaaPlanRead, data) - return PlanDetailOut(data=projected[plan_id]) - - -@api.post( +@router.post( "", tags=["Add"], summary="创建计划表", response_model=PlanCreateOut, - id="", - data=MaaPlanRead(), ) async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: try: @@ -81,30 +65,33 @@ async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: return PlanCreateOut(id=str(uid), data=data) -@api.get( +@router.get( "", tags=["Get"], summary="查询全部计划表", response_model=PlanGetOut, - index=[], - data={}, ) async def list_plans() -> PlanGetOut: - return await _build_plan_collection_out() + index, data = await Config.get_plan(None) + return PlanGetOut( + index=project_model_list(PlanIndexItem, index), + data=project_model_map(MaaPlanRead, data), + ) -@api.get( +@router.get( "/{plan_id}", tags=["Get"], summary="查询单个计划表", response_model=PlanDetailOut, - data=MaaPlanRead(), ) async def get_plan(plan_id: PlanIdPath) -> PlanDetailOut: - return await _build_plan_detail_out(plan_id) + _, data = await Config.get_plan(plan_id) + projected = project_model_map(MaaPlanRead, data) + return PlanDetailOut(data=projected[plan_id]) -@api.patch( +@router.patch( "/{plan_id}", tags=["Update"], summary="更新计划表", @@ -112,13 +99,13 @@ async def get_plan(plan_id: PlanIdPath) -> PlanDetailOut: ) async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> OutBase: try: - await Config.update_plan(plan_id, body.data.model_dump(exclude_unset=True)) + await Config.update_plan(plan_id, dump_writable_data(body.data)) except RECOVERABLE_EXCEPTIONS as e: return error_out(OutBase, e) return OutBase() -@api.delete( +@router.delete( "/{plan_id}", tags=["Delete"], summary="删除计划表", @@ -132,7 +119,7 @@ async def delete_plan(plan_id: PlanIdPath) -> OutBase: return OutBase() -@api.patch( +@router.patch( "/order", tags=["Update"], summary="重新排序计划表", diff --git a/app/api/queue.py b/app/api/queue.py index 8ac33d77..1fc9f11c 100644 --- a/app/api/queue.py +++ b/app/api/queue.py @@ -25,11 +25,12 @@ from fastapi import APIRouter, Body, Path -from app.api.common import bind_api +from app.api.ws_command import ws_command from app.core import Config from app.contracts.common_contract import ( IndexOrderPatch, OutBase, + dump_writable_data, project_model, project_model_list, project_model_map, @@ -43,160 +44,52 @@ QueueItemDetailOut, QueueItemGetOut, QueueItemIndexItem, - QueueItemPatch, QueueItemRead, - QueuePatch, QueueRead, TimeSetCreateOut, TimeSetDetailOut, TimeSetGetOut, TimeSetIndexItem, - TimeSetPatch, TimeSetRead, ) router = APIRouter(prefix="/api/queue", tags=["调度队列管理"]) -api = bind_api(router) QueueIdPath = Annotated[str, Path(description="队列 ID")] TimeSetIdPath = Annotated[str, Path(description="时间设置 ID")] QueueItemIdPath = Annotated[str, Path(description="队列项 ID")] -async def _build_queue_collection_out() -> QueueGetOut: - index, data = await Config.get_queue(None) - return QueueGetOut( - index=project_model_list(QueueIndexItem, index), - data=project_model_map(QueueRead, data), - ) - - -async def _build_queue_detail_out(queue_id: str) -> QueueDetailOut: - _, data = await Config.get_queue(queue_id) - projected = project_model_map(QueueRead, data) - return QueueDetailOut(data=projected[queue_id]) - - -async def _build_queue_create_out() -> QueueCreateOut: - uid, config = await Config.add_queue() - return QueueCreateOut( - id=str(uid), - data=project_model(QueueRead, await config.toDict()), - ) - - -async def _update_queue_config(queue_id: str, data: QueuePatch) -> OutBase: - await Config.update_queue(queue_id, data.model_dump(exclude_unset=True)) - return OutBase() - - -async def _delete_queue_config(queue_id: str) -> OutBase: - await Config.del_queue(queue_id) - return OutBase() - - -async def _build_time_set_collection_out(queue_id: str) -> TimeSetGetOut: - index, data = await Config.get_time_set(queue_id, None) - return TimeSetGetOut( - index=project_model_list(TimeSetIndexItem, index), - data=project_model_map(TimeSetRead, data), - ) - - -async def _build_time_set_detail_out( - queue_id: str, time_set_id: str -) -> TimeSetDetailOut: - _, data = await Config.get_time_set(queue_id, time_set_id) - projected = project_model_map(TimeSetRead, data) - return TimeSetDetailOut(data=projected[time_set_id]) - - -async def _build_time_set_create_out(queue_id: str) -> TimeSetCreateOut: - uid, config = await Config.add_time_set(queue_id) - return TimeSetCreateOut( - id=str(uid), - data=project_model(TimeSetRead, await config.toDict()), - ) - - -async def _update_time_set_config( - queue_id: str, time_set_id: str, data: TimeSetPatch -) -> OutBase: - await Config.update_time_set( - queue_id, time_set_id, data.model_dump(exclude_unset=True) - ) - return OutBase() - - -async def _delete_time_set_config(queue_id: str, time_set_id: str) -> OutBase: - await Config.del_time_set(queue_id, time_set_id) - return OutBase() - - -async def _build_queue_item_collection_out(queue_id: str) -> QueueItemGetOut: - index, data = await Config.get_queue_item(queue_id, None) - return QueueItemGetOut( - index=project_model_list(QueueItemIndexItem, index), - data=project_model_map(QueueItemRead, data), - ) - - -async def _build_queue_item_detail_out( - queue_id: str, queue_item_id: str -) -> QueueItemDetailOut: - _, data = await Config.get_queue_item(queue_id, queue_item_id) - projected = project_model_map(QueueItemRead, data) - return QueueItemDetailOut(data=projected[queue_item_id]) - - -async def _build_queue_item_create_out(queue_id: str) -> QueueItemCreateOut: - uid, config = await Config.add_queue_item(queue_id) - return QueueItemCreateOut( - id=str(uid), - data=project_model(QueueItemRead, await config.toDict()), - ) - - -async def _update_queue_item_config( - queue_id: str, queue_item_id: str, data: QueueItemPatch -) -> OutBase: - await Config.update_queue_item( - queue_id, queue_item_id, data.model_dump(exclude_unset=True) - ) - return OutBase() - - -async def _delete_queue_item_config(queue_id: str, queue_item_id: str) -> OutBase: - await Config.del_queue_item(queue_id, queue_item_id) - return OutBase() - - -@api.get( +@router.get( "", tags=["Get"], summary="查询全部调度队列", response_model=QueueGetOut, - index=[], - data={}, ) async def list_queues() -> QueueGetOut: - return await _build_queue_collection_out() + index, data = await Config.get_queue(None) + return QueueGetOut( + index=project_model_list(QueueIndexItem, index), + data=project_model_map(QueueRead, data), + ) -@api.post( +@ws_command("queue.add") +@router.post( "", - ws_endpoint="queue.add", tags=["Add"], summary="创建调度队列", response_model=QueueCreateOut, - id="", - data=QueueRead(), ) async def create_queue() -> QueueCreateOut: - return await _build_queue_create_out() + uid, config = await Config.add_queue() + return QueueCreateOut( + id=str(uid), + data=project_model(QueueRead, await config.toDict()), + ) -@api.patch( +@router.patch( "/order", tags=["Update"], summary="重新排序调度队列", @@ -207,63 +100,70 @@ async def reorder_queue(body: IndexOrderPatch = Body(...)) -> OutBase: return OutBase() -@api.get( +@ws_command("queue.get") +@router.get( "/{queue_id}", - ws_endpoint="queue.get", tags=["Get"], summary="查询单个调度队列", response_model=QueueDetailOut, - data=QueueRead(), ) async def get_queue(queue_id: QueueIdPath) -> QueueDetailOut: - return await _build_queue_detail_out(queue_id) + _, data = await Config.get_queue(queue_id) + projected = project_model_map(QueueRead, data) + return QueueDetailOut(data=projected[queue_id]) -@api.patch( +@router.patch( "/{queue_id}", tags=["Update"], summary="更新调度队列", response_model=OutBase, ) -async def update_queue(queue_id: QueueIdPath, data: QueuePatch = Body(...)) -> OutBase: - return await _update_queue_config(queue_id, data) +async def update_queue(queue_id: QueueIdPath, data: QueueRead = Body(...)) -> OutBase: + await Config.update_queue(queue_id, dump_writable_data(data)) + return OutBase() -@api.delete( +@router.delete( "/{queue_id}", tags=["Delete"], summary="删除调度队列", response_model=OutBase, ) async def delete_queue(queue_id: QueueIdPath) -> OutBase: - return await _delete_queue_config(queue_id) + await Config.del_queue(queue_id) + return OutBase() -@api.get( +@router.get( "/{queue_id}/times", tags=["Get"], summary="查询队列下的全部定时项", response_model=TimeSetGetOut, - index=[], - data={}, ) async def list_time_sets(queue_id: QueueIdPath) -> TimeSetGetOut: - return await _build_time_set_collection_out(queue_id) + index, data = await Config.get_time_set(queue_id, None) + return TimeSetGetOut( + index=project_model_list(TimeSetIndexItem, index), + data=project_model_map(TimeSetRead, data), + ) -@api.post( +@router.post( "/{queue_id}/times", tags=["Add"], summary="创建定时项", response_model=TimeSetCreateOut, - id="", - data=TimeSetRead(), ) async def create_time_set(queue_id: QueueIdPath) -> TimeSetCreateOut: - return await _build_time_set_create_out(queue_id) + uid, config = await Config.add_time_set(queue_id) + return TimeSetCreateOut( + id=str(uid), + data=project_model(TimeSetRead, await config.toDict()), + ) -@api.patch( +@router.patch( "/{queue_id}/times/order", tags=["Update"], summary="重新排序定时项", @@ -276,20 +176,21 @@ async def reorder_time_sets( return OutBase() -@api.get( +@router.get( "/{queue_id}/times/{time_set_id}", tags=["Get"], summary="查询单个定时项", response_model=TimeSetDetailOut, - data=TimeSetRead(), ) async def get_time_set( queue_id: QueueIdPath, time_set_id: TimeSetIdPath ) -> TimeSetDetailOut: - return await _build_time_set_detail_out(queue_id, time_set_id) + _, data = await Config.get_time_set(queue_id, time_set_id) + projected = project_model_map(TimeSetRead, data) + return TimeSetDetailOut(data=projected[time_set_id]) -@api.patch( +@router.patch( "/{queue_id}/times/{time_set_id}", tags=["Update"], summary="更新定时项", @@ -298,46 +199,52 @@ async def get_time_set( async def update_time_set( queue_id: QueueIdPath, time_set_id: TimeSetIdPath, - data: TimeSetPatch = Body(...), + data: TimeSetRead = Body(...), ) -> OutBase: - return await _update_time_set_config(queue_id, time_set_id, data) + await Config.update_time_set(queue_id, time_set_id, dump_writable_data(data)) + return OutBase() -@api.delete( +@router.delete( "/{queue_id}/times/{time_set_id}", tags=["Delete"], summary="删除定时项", response_model=OutBase, ) async def delete_time_set(queue_id: QueueIdPath, time_set_id: TimeSetIdPath) -> OutBase: - return await _delete_time_set_config(queue_id, time_set_id) + await Config.del_time_set(queue_id, time_set_id) + return OutBase() -@api.get( +@router.get( "/{queue_id}/items", tags=["Get"], summary="查询队列下的全部队列项", response_model=QueueItemGetOut, - index=[], - data={}, ) async def list_queue_items(queue_id: QueueIdPath) -> QueueItemGetOut: - return await _build_queue_item_collection_out(queue_id) + index, data = await Config.get_queue_item(queue_id, None) + return QueueItemGetOut( + index=project_model_list(QueueItemIndexItem, index), + data=project_model_map(QueueItemRead, data), + ) -@api.post( +@router.post( "/{queue_id}/items", tags=["Add"], summary="创建队列项", response_model=QueueItemCreateOut, - id="", - data=QueueItemRead(), ) async def create_queue_item(queue_id: QueueIdPath) -> QueueItemCreateOut: - return await _build_queue_item_create_out(queue_id) + uid, config = await Config.add_queue_item(queue_id) + return QueueItemCreateOut( + id=str(uid), + data=project_model(QueueItemRead, await config.toDict()), + ) -@api.patch( +@router.patch( "/{queue_id}/items/order", tags=["Update"], summary="重新排序队列项", @@ -350,20 +257,21 @@ async def reorder_queue_items( return OutBase() -@api.get( +@router.get( "/{queue_id}/items/{queue_item_id}", tags=["Get"], summary="查询单个队列项", response_model=QueueItemDetailOut, - data=QueueItemRead(), ) async def get_queue_item( queue_id: QueueIdPath, queue_item_id: QueueItemIdPath ) -> QueueItemDetailOut: - return await _build_queue_item_detail_out(queue_id, queue_item_id) + _, data = await Config.get_queue_item(queue_id, queue_item_id) + projected = project_model_map(QueueItemRead, data) + return QueueItemDetailOut(data=projected[queue_item_id]) -@api.patch( +@router.patch( "/{queue_id}/items/{queue_item_id}", tags=["Update"], summary="更新队列项", @@ -372,12 +280,13 @@ async def get_queue_item( async def update_queue_item( queue_id: QueueIdPath, queue_item_id: QueueItemIdPath, - data: QueueItemPatch = Body(...), + data: QueueItemRead = Body(...), ) -> OutBase: - return await _update_queue_item_config(queue_id, queue_item_id, data) + await Config.update_queue_item(queue_id, queue_item_id, dump_writable_data(data)) + return OutBase() -@api.delete( +@router.delete( "/{queue_id}/items/{queue_item_id}", tags=["Delete"], summary="删除队列项", @@ -386,4 +295,5 @@ async def update_queue_item( async def delete_queue_item( queue_id: QueueIdPath, queue_item_id: QueueItemIdPath ) -> OutBase: - return await _delete_queue_item_config(queue_id, queue_item_id) + await Config.del_queue_item(queue_id, queue_item_id) + return OutBase() diff --git a/app/api/scripts.py b/app/api/scripts.py index fd0bd8ba..3a6289d5 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -27,13 +27,14 @@ from fastapi import APIRouter, Body, Path from pydantic import TypeAdapter -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, error_out +from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.core import Config from app.contracts.common_contract import ( ComboBoxItem, ComboBoxOut, IndexOrderPatch, OutBase, + dump_writable_data, project_model, project_model_list, project_model_map, @@ -69,7 +70,6 @@ WebhookDetailOut, WebhookGetOut, WebhookIndexItem, - WebhookPatch, WebhookRead, ) @@ -78,70 +78,27 @@ ) router = APIRouter(prefix="/api/scripts", tags=["脚本管理"]) -api = bind_api(router) ScriptIdPath = Annotated[str, Path(description="脚本 ID")] UserIdPath = Annotated[str, Path(description="用户 ID")] WebhookIdPath = Annotated[str, Path(description="Webhook ID")] -async def _build_script_collection_out() -> ScriptGetOut: - index, data = await Config.get_script(None) - script_index = project_model_list(ScriptIndexItem, index) - return ScriptGetOut( - index=script_index, data=project_script_model_map(script_index, data) - ) - - -async def _build_script_detail_out(script_id: str) -> ScriptDetailOut: - index, data = await Config.get_script(script_id) - script_index = project_model_list(ScriptIndexItem, index) - projected = project_script_model_map(script_index, data) - return ScriptDetailOut(data=projected[script_id]) - - -async def _build_user_collection_out(script_id: str) -> UserGetOut: - index, data = await Config.get_user(script_id, None) - user_index = project_model_list(UserIndexItem, index) - return UserGetOut(index=user_index, data=project_user_model_map(user_index, data)) - - -async def _build_user_detail_out(script_id: str, user_id: str) -> UserDetailOut: - index, data = await Config.get_user(script_id, user_id) - user_index = project_model_list(UserIndexItem, index) - projected = project_user_model_map(user_index, data) - return UserDetailOut(data=projected[user_id]) - - -async def _build_webhook_collection_out(script_id: str, user_id: str) -> WebhookGetOut: - index, data = await Config.get_webhook(script_id, user_id, None) - return WebhookGetOut( - index=project_model_list(WebhookIndexItem, index), - data=project_model_map(WebhookRead, data), - ) - - -async def _build_webhook_detail_out( - script_id: str, user_id: str, webhook_id: str -) -> WebhookDetailOut: - _, data = await Config.get_webhook(script_id, user_id, webhook_id) - projected = project_model_map(WebhookRead, data) - return WebhookDetailOut(data=projected[webhook_id]) - - -@api.get( +@router.get( "", tags=["Get"], summary="查询全部脚本", response_model=ScriptGetOut, - index=[], - data={}, ) async def list_scripts() -> ScriptGetOut: - return await _build_script_collection_out() + index, data = await Config.get_script(None) + script_index = project_model_list(ScriptIndexItem, index) + return ScriptGetOut( + index=script_index, data=project_script_model_map(script_index, data) + ) -@api.post( +@router.post( "", tags=["Add"], summary="创建脚本", @@ -167,7 +124,7 @@ async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: return ScriptCreateOut(id=str(uid), data=data) -@api.patch( +@router.patch( "/order", tags=["Update"], summary="重新排序脚本", @@ -178,7 +135,7 @@ async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: return OutBase() -@api.get( +@router.get( "/{script_id}", tags=["Get"], summary="查询单个脚本", @@ -186,7 +143,10 @@ async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: ) async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: try: - return await _build_script_detail_out(script_id) + index, data = await Config.get_script(script_id) + script_index = project_model_list(ScriptIndexItem, index) + projected = project_script_model_map(script_index, data) + return ScriptDetailOut(data=projected[script_id]) except RECOVERABLE_EXCEPTIONS as e: script_type = "GeneralConfig" try: @@ -202,7 +162,7 @@ async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: ) -@api.patch( +@router.patch( "/{script_id}", tags=["Update"], summary="更新脚本配置", @@ -221,7 +181,7 @@ async def update_script( return OutBase() -@api.delete( +@router.delete( "/{script_id}", tags=["Delete"], summary="删除脚本", @@ -232,7 +192,7 @@ async def delete_script(script_id: ScriptIdPath) -> OutBase: return OutBase() -@api.post( +@router.post( "/{script_id}/actions/import-file", tags=["Action"], summary="从文件导入脚本配置", @@ -245,7 +205,7 @@ async def import_script_from_file( return OutBase() -@api.post( +@router.post( "/{script_id}/actions/export-file", tags=["Action"], summary="导出脚本配置到文件", @@ -258,7 +218,7 @@ async def export_script_to_file( return OutBase() -@api.post( +@router.post( "/{script_id}/actions/import-web", tags=["Action"], summary="从网络导入脚本配置", @@ -271,7 +231,7 @@ async def import_script_from_web( return OutBase() -@api.post( +@router.post( "/{script_id}/actions/upload-web", tags=["Action"], summary="上传脚本配置到网络", @@ -286,19 +246,19 @@ async def upload_script_to_web( return OutBase() -@api.get( +@router.get( "/{script_id}/users", tags=["Get"], summary="查询脚本下的全部用户", response_model=UserGetOut, - index=[], - data={}, ) async def list_users(script_id: ScriptIdPath) -> UserGetOut: - return await _build_user_collection_out(script_id) + index, data = await Config.get_user(script_id, None) + user_index = project_model_list(UserIndexItem, index) + return UserGetOut(index=user_index, data=project_user_model_map(user_index, data)) -@api.post( +@router.post( "/{script_id}/users", tags=["Add"], summary="创建用户", @@ -328,7 +288,7 @@ async def create_user(script_id: ScriptIdPath) -> UserCreateOut: return UserCreateOut(id=str(uid), data=data) -@api.patch( +@router.patch( "/{script_id}/users/order", tags=["Update"], summary="重新排序用户", @@ -341,7 +301,7 @@ async def reorder_users( return OutBase() -@api.get( +@router.get( "/{script_id}/users/{user_id}", tags=["Get"], summary="查询单个用户", @@ -349,7 +309,10 @@ async def reorder_users( ) async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOut: try: - return await _build_user_detail_out(script_id, user_id) + index, data = await Config.get_user(script_id, user_id) + user_index = project_model_list(UserIndexItem, index) + projected = project_user_model_map(user_index, data) + return UserDetailOut(data=projected[user_id]) except RECOVERABLE_EXCEPTIONS as e: user_type = "GeneralUserConfig" try: @@ -366,7 +329,7 @@ async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOu ) -@api.patch( +@router.patch( "/{script_id}/users/{user_id}", tags=["Update"], summary="更新用户配置", @@ -387,7 +350,7 @@ async def update_user( return OutBase() -@api.delete( +@router.delete( "/{script_id}/users/{user_id}", tags=["Delete"], summary="删除用户", @@ -398,7 +361,7 @@ async def delete_user(script_id: ScriptIdPath, user_id: UserIdPath) -> OutBase: return OutBase() -@api.post( +@router.post( "/{script_id}/users/{user_id}/actions/import-infrastructure", tags=["Action"], summary="导入基建配置文件", @@ -413,7 +376,7 @@ async def import_infrastructure( return OutBase() -@api.get( +@router.get( "/{script_id}/users/{user_id}/infrastructure-options", tags=["Get"], summary="用户自定义基建排班可选项", @@ -430,27 +393,27 @@ async def get_user_infrastructure_options( return ComboBoxOut(data=data) -@api.get( +@router.get( "/{script_id}/users/{user_id}/webhooks", tags=["Get"], summary="查询用户下的全部 Webhook", response_model=WebhookGetOut, - index=[], - data={}, ) async def list_user_webhooks( script_id: ScriptIdPath, user_id: UserIdPath ) -> WebhookGetOut: - return await _build_webhook_collection_out(script_id, user_id) + index, data = await Config.get_webhook(script_id, user_id, None) + return WebhookGetOut( + index=project_model_list(WebhookIndexItem, index), + data=project_model_map(WebhookRead, data), + ) -@api.post( +@router.post( "/{script_id}/users/{user_id}/webhooks", tags=["Add"], summary="创建用户 Webhook", response_model=WebhookCreateOut, - id="", - data=WebhookRead(), ) async def create_user_webhook( script_id: ScriptIdPath, user_id: UserIdPath @@ -462,7 +425,7 @@ async def create_user_webhook( ) -@api.patch( +@router.patch( "/{script_id}/users/{user_id}/webhooks/order", tags=["Update"], summary="重新排序用户 Webhook", @@ -477,7 +440,7 @@ async def reorder_user_webhooks( return OutBase() -@api.get( +@router.get( "/{script_id}/users/{user_id}/webhooks/{webhook_id}", tags=["Get"], summary="查询单个用户 Webhook", @@ -489,12 +452,14 @@ async def get_user_webhook( webhook_id: WebhookIdPath, ) -> WebhookDetailOut: try: - return await _build_webhook_detail_out(script_id, user_id, webhook_id) + _, data = await Config.get_webhook(script_id, user_id, webhook_id) + projected = project_model_map(WebhookRead, data) + return WebhookDetailOut(data=projected[webhook_id]) except RECOVERABLE_EXCEPTIONS as e: return error_out(WebhookDetailOut, e, data=WebhookRead()) -@api.patch( +@router.patch( "/{script_id}/users/{user_id}/webhooks/{webhook_id}", tags=["Update"], summary="更新用户 Webhook", @@ -504,18 +469,18 @@ async def update_user_webhook( script_id: ScriptIdPath, user_id: UserIdPath, webhook_id: WebhookIdPath, - data: WebhookPatch = Body(...), + data: WebhookRead = Body(...), ) -> OutBase: await Config.update_webhook( script_id, user_id, webhook_id, - data.model_dump(exclude_unset=True, exclude_none=True), + dump_writable_data(data), ) return OutBase() -@api.delete( +@router.delete( "/{script_id}/users/{user_id}/webhooks/{webhook_id}", tags=["Delete"], summary="删除用户 Webhook", diff --git a/app/api/setting.py b/app/api/setting.py index 1d0273ad..9b75bb09 100644 --- a/app/api/setting.py +++ b/app/api/setting.py @@ -25,113 +25,57 @@ from fastapi import APIRouter, Body, Path -from app.api.common import bind_api + from app.core import Config from app.models import Webhook as WebhookConfig from app.contracts.common_contract import ( IndexOrderPatch, OutBase, + dump_writable_data, project_model, project_model_list, project_model_map, ) from app.contracts.setting_contract import ( - GlobalConfigPatch, GlobalConfigRead, SettingGetOut, WebhookCreateOut, WebhookDetailOut, WebhookGetOut, WebhookIndexItem, - WebhookPatch, WebhookRead, ) from app.services import Notify router = APIRouter(prefix="/api/setting", tags=["全局设置"]) -api = bind_api(router) WebhookIdPath = Annotated[str, Path(description="Webhook ID")] -async def _build_setting_out() -> SettingGetOut: - return SettingGetOut( - data=project_model(GlobalConfigRead, await Config.get_setting()) - ) - - -async def _update_setting_config(data: GlobalConfigPatch) -> OutBase: - await Config.update_setting(data.model_dump(exclude_unset=True)) - return OutBase() - - -async def _build_webhook_collection_out() -> WebhookGetOut: - index, data = await Config.get_webhook(None, None, None) - return WebhookGetOut( - index=project_model_list(WebhookIndexItem, index), - data=project_model_map(WebhookRead, data), - ) - - -async def _build_webhook_detail_out(webhook_id: str) -> WebhookDetailOut: - _, data = await Config.get_webhook(None, None, webhook_id) - projected = project_model_map(WebhookRead, data) - return WebhookDetailOut(data=projected[webhook_id]) - - -async def _build_webhook_create_out() -> WebhookCreateOut: - uid, config = await Config.add_webhook(None, None) - return WebhookCreateOut( - id=str(uid), - data=project_model(WebhookRead, await config.toDict()), - ) - - -async def _update_webhook_config(webhook_id: str, data: WebhookPatch) -> OutBase: - await Config.update_webhook( - None, None, webhook_id, data.model_dump(exclude_unset=True) - ) - return OutBase() - - -async def _delete_webhook_config(webhook_id: str) -> OutBase: - await Config.del_webhook(None, None, webhook_id) - return OutBase() - - -async def _test_webhook_config(data: WebhookPatch) -> OutBase: - webhook_config = WebhookConfig() - await webhook_config.load(data.model_dump(exclude_unset=True)) - await Notify.WebhookPush( - "AUTO-MAS Webhook测试", - "这是一条测试消息,如果您收到此消息,说明Webhook配置正确!", - webhook_config, - ) - return OutBase() - - -@api.get( +@router.get( "", tags=["Get"], summary="查询全局配置", response_model=SettingGetOut, - data=GlobalConfigRead(), ) async def get_setting() -> SettingGetOut: - return await _build_setting_out() + return SettingGetOut( + data=project_model(GlobalConfigRead, await Config.get_setting()) + ) -@api.patch( +@router.patch( "", tags=["Update"], summary="更新全局配置", response_model=OutBase, ) -async def update_setting(data: GlobalConfigPatch = Body(...)) -> OutBase: - return await _update_setting_config(data) +async def update_setting(data: GlobalConfigRead = Body(...)) -> OutBase: + await Config.update_setting(dump_writable_data(data)) + return OutBase() -@api.post( +@router.post( "/actions/test-notify", tags=["Action"], summary="测试通知", @@ -142,31 +86,35 @@ async def test_notify() -> OutBase: return OutBase() -@api.get( +@router.get( "/webhooks", tags=["Get"], summary="查询全部全局 Webhook 配置", response_model=WebhookGetOut, - index=[], - data={}, ) async def list_webhooks() -> WebhookGetOut: - return await _build_webhook_collection_out() + index, data = await Config.get_webhook(None, None, None) + return WebhookGetOut( + index=project_model_list(WebhookIndexItem, index), + data=project_model_map(WebhookRead, data), + ) -@api.post( +@router.post( "/webhooks", tags=["Add"], summary="创建全局 Webhook 配置", response_model=WebhookCreateOut, - id="", - data=WebhookRead(), ) async def create_webhook() -> WebhookCreateOut: - return await _build_webhook_create_out() + uid, config = await Config.add_webhook(None, None) + return WebhookCreateOut( + id=str(uid), + data=project_model(WebhookRead, await config.toDict()), + ) -@api.patch( +@router.patch( "/webhooks/order", tags=["Update"], summary="重新排序全局 Webhook", @@ -177,44 +125,54 @@ async def reorder_webhooks(body: IndexOrderPatch = Body(...)) -> OutBase: return OutBase() -@api.post( +@router.post( "/webhooks/test", tags=["Action"], summary="测试指定 Webhook 配置", response_model=OutBase, ) -async def test_webhook(data: WebhookPatch = Body(...)) -> OutBase: - return await _test_webhook_config(data) +async def test_webhook(data: WebhookRead = Body(...)) -> OutBase: + webhook_config = WebhookConfig() + await webhook_config.load(dump_writable_data(data)) + await Notify.WebhookPush( + "AUTO-MAS Webhook测试", + "这是一条测试消息,如果您收到此消息,说明Webhook配置正确!", + webhook_config, + ) + return OutBase() -@api.get( +@router.get( "/webhooks/{webhook_id}", tags=["Get"], summary="查询单个全局 Webhook 配置", response_model=WebhookDetailOut, - data=WebhookRead(), ) async def get_webhook(webhook_id: WebhookIdPath) -> WebhookDetailOut: - return await _build_webhook_detail_out(webhook_id) + _, data = await Config.get_webhook(None, None, webhook_id) + projected = project_model_map(WebhookRead, data) + return WebhookDetailOut(data=projected[webhook_id]) -@api.patch( +@router.patch( "/webhooks/{webhook_id}", tags=["Update"], summary="更新全局 Webhook 配置", response_model=OutBase, ) async def update_webhook( - webhook_id: WebhookIdPath, data: WebhookPatch = Body(...) + webhook_id: WebhookIdPath, data: WebhookRead = Body(...) ) -> OutBase: - return await _update_webhook_config(webhook_id, data) + await Config.update_webhook(None, None, webhook_id, dump_writable_data(data)) + return OutBase() -@api.delete( +@router.delete( "/webhooks/{webhook_id}", tags=["Delete"], summary="删除全局 Webhook 配置", response_model=OutBase, ) async def delete_webhook(webhook_id: WebhookIdPath) -> OutBase: - return await _delete_webhook_config(webhook_id) + await Config.del_webhook(None, None, webhook_id) + return OutBase() diff --git a/app/api/tools.py b/app/api/tools.py index 51a24495..40c59af4 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -23,40 +23,30 @@ from fastapi import APIRouter, Body -from app.api.common import bind_api + from app.core import Config -from app.contracts.common_contract import OutBase, project_model -from app.contracts.tools_contract import ToolsConfigPatch, ToolsConfigRead, ToolsGetOut +from app.contracts.common_contract import OutBase, dump_writable_data, project_model +from app.contracts.tools_contract import ToolsConfigRead, ToolsGetOut router = APIRouter(prefix="/api/tools", tags=["工具设置"]) -api = bind_api(router) - -async def _build_tools_out() -> ToolsGetOut: - return ToolsGetOut(data=project_model(ToolsConfigRead, await Config.get_tools())) - -async def _update_tools_config(data: ToolsConfigPatch) -> OutBase: - await Config.update_tools(data.model_dump(exclude_unset=True)) - return OutBase() - - -@api.get( +@router.get( "", tags=["Get"], summary="查询工具配置", response_model=ToolsGetOut, - data=ToolsConfigRead(), ) async def get_tools() -> ToolsGetOut: - return await _build_tools_out() + return ToolsGetOut(data=project_model(ToolsConfigRead, await Config.get_tools())) -@api.patch( +@router.patch( "", tags=["Update"], summary="更新工具配置", response_model=OutBase, ) -async def update_tools(data: ToolsConfigPatch = Body(...)) -> OutBase: - return await _update_tools_config(data) +async def update_tools(data: ToolsConfigRead = Body(...)) -> OutBase: + await Config.update_tools(dump_writable_data(data)) + return OutBase() diff --git a/app/api/update.py b/app/api/update.py index e89884be..157ad6c0 100644 --- a/app/api/update.py +++ b/app/api/update.py @@ -31,10 +31,9 @@ from app.services import Updater from app.contracts.common_contract import OutBase from app.contracts.update_contract import UpdateCheckIn, UpdateCheckOut -from app.api.common import bind_api + router = APIRouter(prefix="/api/update", tags=["软件更新"]) -api = bind_api(router) QueryUpdateCheckIn = Annotated[UpdateCheckIn, Depends()] @@ -61,33 +60,27 @@ async def _build_update_check_out(version: UpdateCheckIn) -> UpdateCheckOut: ) -@api.post( +@router.post( "/check", tags=["Get"], summary="检查更新", response_model=UpdateCheckOut, - if_need_update=False, - latest_version="", - update_info={}, ) async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut: return await _build_update_check_out(version) -@api.get( +@router.get( "/check", tags=["Get"], summary="按 REST 风格检查更新", response_model=UpdateCheckOut, - if_need_update=False, - latest_version="", - update_info={}, ) async def check_update_rest(version: QueryUpdateCheckIn) -> UpdateCheckOut: return await _build_update_check_out(version) -@api.post( +@router.post( "/download", tags=["Action"], summary="下载更新", @@ -99,7 +92,7 @@ async def download_update() -> OutBase: return OutBase() -@api.post( +@router.post( "/install", tags=["Action"], summary="安装更新", diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index 15a37494..772b387b 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -50,18 +50,17 @@ WSClearHistoryIn, WSCommandsOut, ) -from app.api.common import RECOVERABLE_EXCEPTIONS, bind_api, run_api +from app.api.common import RECOVERABLE_EXCEPTIONS, run_api logger = get_logger("WS调试") router = APIRouter(prefix="/api/ws_debug", tags=["WebSocket调试"]) -api = bind_api(router) # ============== API 路由 ============== -@api.post( +@router.post( "/client/create", summary="创建 WebSocket 客户端", response_model=WSClientCreateOut, @@ -107,7 +106,7 @@ async def _success() -> WSClientCreateOut: ) -@api.post( +@router.post( "/client/connect", summary="连接 WebSocket 客户端", response_model=WSClientStatusOut, @@ -155,7 +154,7 @@ async def _success() -> WSClientStatusOut: ) -@api.post( +@router.post( "/client/disconnect", summary="断开 WebSocket 客户端", response_model=WSClientStatusOut, @@ -186,7 +185,7 @@ async def _success() -> WSClientStatusOut: ) -@api.post( +@router.post( "/client/remove", summary="删除 WebSocket 客户端", response_model=WSClientStatusOut, @@ -224,7 +223,7 @@ async def _success() -> WSClientStatusOut: ) -@api.post( +@router.post( "/client/status", summary="获取客户端状态", response_model=WSClientStatusOut, @@ -255,7 +254,7 @@ async def get_client_status(request: WSClientStatusIn) -> WSClientStatusOut: ) -@api.get( +@router.get( "/client/list", summary="列出所有客户端", response_model=WSClientListOut, @@ -273,7 +272,7 @@ async def list_clients() -> WSClientListOut: ) -@api.post( +@router.post( "/message/send", summary="发送原始消息", response_model=WSClientStatusOut, @@ -313,7 +312,7 @@ async def _success() -> WSClientStatusOut: ) -@api.post( +@router.post( "/message/send_json", summary="发送格式化消息", response_model=WSClientStatusOut, @@ -355,7 +354,7 @@ async def _success() -> WSClientStatusOut: ) -@api.post( +@router.post( "/message/auth", summary="发送认证消息", response_model=WSClientStatusOut, @@ -405,7 +404,7 @@ async def _success() -> WSClientStatusOut: ) -@api.get( +@router.get( "/history", summary="获取消息历史", response_model=WSMessageHistoryOut, @@ -428,7 +427,7 @@ async def get_history(name: Optional[str] = None) -> WSMessageHistoryOut: ) -@api.post( +@router.post( "/history/clear", summary="清空消息历史", response_model=WSClientStatusOut, @@ -453,7 +452,7 @@ async def clear_history(request: WSClearHistoryIn) -> WSClientStatusOut: ) -@api.get( +@router.get( "/commands", summary="获取可用 WS 命令", response_model=WSCommandsOut, diff --git a/app/contracts/__init__.py b/app/contracts/__init__.py index 10068b05..58d03e69 100644 --- a/app/contracts/__init__.py +++ b/app/contracts/__init__.py @@ -27,7 +27,7 @@ 设计原则: 1. Contract 只负责 API 边界定义,不包含业务逻辑 -2. 使用 Read/Patch/Create 分离读写模型 +2. 使用单一 Contract 模型 + readOnly/writeOnly 字段标记 3. 所有 Contract 继承 ApiModel,获得统一配置 4. 字段命名使用 snake_case,通过 alias 兼容前端 """ @@ -42,6 +42,7 @@ ResourceCollectionOut, ResourceCreateOut, ResourceItemOut, + dump_writable_data, project_model, project_model_list, project_model_map, @@ -57,6 +58,7 @@ "ResourceItemOut", "ResourceCreateOut", "IndexOrderPatch", + "dump_writable_data", "project_model", "project_model_list", "project_model_map", diff --git a/app/contracts/common_contract.py b/app/contracts/common_contract.py index 1048905c..183a5a01 100644 --- a/app/contracts/common_contract.py +++ b/app/contracts/common_contract.py @@ -136,6 +136,8 @@ def _clone_field_annotation( annotation: Any, *, keep_virtual: bool, + read_only: bool = False, + write_only: bool = False, ) -> Any: metadata = tuple( item @@ -151,8 +153,20 @@ def _clone_field_annotation( field_kwargs["serialization_alias"] = field_info.serialization_alias if field_info.alias is not None: field_kwargs["alias"] = field_info.alias - if field_info.json_schema_extra is not None: - field_kwargs["json_schema_extra"] = field_info.json_schema_extra + json_schema_extra: dict[str, Any] = {} + raw_schema_extra: Any = field_info.json_schema_extra + if isinstance(raw_schema_extra, Mapping): + schema_extra_mapping = cast( + Mapping[str, Any], + raw_schema_extra, + ) + json_schema_extra.update(dict(schema_extra_mapping)) + if read_only: + json_schema_extra["readOnly"] = True + if write_only: + json_schema_extra["writeOnly"] = True + if json_schema_extra: + field_kwargs["json_schema_extra"] = json_schema_extra if field_info.discriminator is not None: field_kwargs["discriminator"] = field_info.discriminator if field_kwargs: @@ -165,6 +179,7 @@ def _model_field_definitions( *, optional_fields: bool, keep_virtual: bool, + mark_virtual_as_read_only: bool = False, ) -> dict[str, tuple[Any, Any]]: field_definitions: dict[str, tuple[Any, Any]] = {} @@ -178,10 +193,13 @@ def _model_field_definitions( if optional_fields: annotation = _make_optional(annotation) + is_virtual = any(isinstance(item, VirtualField) for item in field_info.metadata) + annotated = _clone_field_annotation( field_info, annotation, keep_virtual=keep_virtual, + read_only=mark_virtual_as_read_only and is_virtual, ) if optional_fields: @@ -204,7 +222,7 @@ def _model_field_definitions( @lru_cache(maxsize=None) -def derive_group_read_model( +def derive_group_contract_model( group_cls: type[BaseModel], *, model_name: str, @@ -216,35 +234,34 @@ def derive_group_read_model( dict[str, Any], _model_field_definitions( group_cls, - optional_fields=False, + optional_fields=True, keep_virtual=True, + mark_virtual_as_read_only=True, ), ), ) +@lru_cache(maxsize=None) +def derive_group_read_model( + group_cls: type[BaseModel], + *, + model_name: str, +) -> type[ApiModel]: + return derive_group_contract_model(group_cls, model_name=model_name) + + @lru_cache(maxsize=None) def derive_group_patch_model( group_cls: type[BaseModel], *, model_name: str, ) -> type[ApiModel]: - return create_model( - model_name, - __base__=ApiModel, - **cast( - dict[str, Any], - _model_field_definitions( - group_cls, - optional_fields=True, - keep_virtual=False, - ), - ), - ) + return derive_group_contract_model(group_cls, model_name=model_name) @lru_cache(maxsize=None) -def derive_config_read_model( +def derive_config_contract_model( config_cls: type[PydanticConfigBase], *, model_name: str, @@ -259,13 +276,13 @@ def derive_config_read_model( group_cls = field_info.annotation if not isinstance(group_cls, type) or not issubclass(group_cls, BaseModel): continue - group_model = derive_group_read_model( + group_model = derive_group_contract_model( group_cls, model_name=f"{model_name}{group_name}", ) field_definitions[group_name] = ( - group_model, - Field(default_factory=group_model, description=field_info.description), + group_model | None, + Field(default=None, description=field_info.description), ) return create_model( @@ -276,34 +293,30 @@ def derive_config_read_model( @lru_cache(maxsize=None) -def derive_config_patch_model( +def derive_config_read_model( config_cls: type[PydanticConfigBase], *, model_name: str, include_groups: tuple[str, ...] | None = None, ) -> type[ApiModel]: - field_definitions: dict[str, tuple[Any, Any]] = {} + return derive_config_contract_model( + config_cls, + model_name=model_name, + include_groups=include_groups, + ) - allowed_groups = set(include_groups) if include_groups is not None else None - for group_name, field_info in config_cls.model_fields.items(): - if allowed_groups is not None and group_name not in allowed_groups: - continue - group_cls = field_info.annotation - if not isinstance(group_cls, type) or not issubclass(group_cls, BaseModel): - continue - group_model = derive_group_patch_model( - group_cls, - model_name=f"{model_name}{group_name}", - ) - field_definitions[group_name] = ( - group_model | None, - Field(default=None, description=field_info.description), - ) - return create_model( - model_name, - __base__=ApiModel, - **cast(dict[str, Any], field_definitions), +@lru_cache(maxsize=None) +def derive_config_patch_model( + config_cls: type[PydanticConfigBase], + *, + model_name: str, + include_groups: tuple[str, ...] | None = None, +) -> type[ApiModel]: + return derive_config_contract_model( + config_cls, + model_name=model_name, + include_groups=include_groups, ) @@ -314,18 +327,61 @@ def derive_config_contracts( patch_name: str, include_groups: tuple[str, ...] | None = None, ) -> tuple[type[ApiModel], type[ApiModel]]: - return ( - derive_config_read_model( - config_cls, - model_name=read_name, - include_groups=include_groups, - ), - derive_config_patch_model( - config_cls, - model_name=patch_name, - include_groups=include_groups, - ), + model = derive_config_contract_model( + config_cls, + model_name=read_name, + include_groups=include_groups, ) + return (model, model) + + +def _extract_model_type(annotation: Any) -> type[BaseModel] | None: + origin = get_origin(annotation) + if origin is None: + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + return annotation + return None + + if origin in (Union, UnionType): + for arg in get_args(annotation): + if arg is type(None): + continue + model_type = _extract_model_type(arg) + if model_type is not None: + return model_type + return None + + +def _prune_read_only_data( + model_cls: type[BaseModel], payload: Mapping[str, Any] +) -> dict[str, Any]: + result: dict[str, Any] = {} + for field_name, raw_value in payload.items(): + field_info = model_cls.model_fields.get(field_name) + if field_info is None: + continue + + extras = field_info.json_schema_extra + if ( + isinstance(extras, Mapping) + and cast(Mapping[str, Any], extras).get("readOnly") is True + ): + continue + + value = raw_value + nested_model = _extract_model_type(field_info.annotation) + if nested_model is not None and isinstance(value, Mapping): + value = _prune_read_only_data(nested_model, cast(Mapping[str, Any], value)) + + result[field_name] = value + return result + + +def dump_writable_data(data: BaseModel) -> dict[str, Any]: + """导出可写字段,自动剔除 readOnly 字段。""" + + payload = data.model_dump(exclude_unset=True, exclude_none=True) + return _prune_read_only_data(type(data), payload) def _normalize_source(raw: RawModelSource | None) -> dict[str, Any]: @@ -502,9 +558,12 @@ def project_model_map( "IndexOrderPatch", "derive_group_read_model", "derive_group_patch_model", + "derive_group_contract_model", "derive_config_read_model", "derive_config_patch_model", + "derive_config_contract_model", "derive_config_contracts", + "dump_writable_data", "project_model", "project_model_list", "project_model_map", diff --git a/app/contracts/emulator_contract.py b/app/contracts/emulator_contract.py index d368c4a3..abdfb57b 100644 --- a/app/contracts/emulator_contract.py +++ b/app/contracts/emulator_contract.py @@ -10,25 +10,20 @@ ResourceCollectionOut, ResourceCreateOut, ResourceItemOut, - derive_config_contracts, + derive_config_contract_model, ) from app.models.shared import DeviceInfo -_EmulatorReadBase, _EmulatorPatchBase = derive_config_contracts( +_EmulatorBase = derive_config_contract_model( EmulatorConfig, - read_name="EmulatorRead", - patch_name="EmulatorPatch", + model_name="EmulatorRead", include_groups=("Info",), ) -class EmulatorRead(_EmulatorReadBase): - pass - - -class EmulatorPatch(_EmulatorPatchBase): - pass +class EmulatorRead(_EmulatorBase): + """模拟器配置读取/写入模型。""" class EmulatorConfigIndexItem(ApiModel): @@ -61,7 +56,6 @@ class EmulatorSearchOut(ResourceItemOut[list[EmulatorSearchResult]]): __all__ = [ "EmulatorRead", - "EmulatorPatch", "EmulatorConfigIndexItem", "EmulatorGetOut", "EmulatorDetailOut", diff --git a/app/contracts/general_contract.py b/app/contracts/general_contract.py index ca4af434..6206c7b6 100644 --- a/app/contracts/general_contract.py +++ b/app/contracts/general_contract.py @@ -4,42 +4,28 @@ from pydantic import Field -from .common_contract import derive_config_contracts +from .common_contract import derive_config_contract_model from app.models.general import GeneralConfig as RuntimeGeneralConfig from app.models.general import GeneralUserConfig as RuntimeGeneralUserConfig -_GeneralConfigReadBase, _GeneralConfigPatchBase = derive_config_contracts( +_GeneralConfigBase = derive_config_contract_model( RuntimeGeneralConfig, - read_name="GeneralConfig", - patch_name="GeneralConfigPatch", + model_name="GeneralConfig", ) -_GeneralUserConfigReadBase, _GeneralUserConfigPatchBase = derive_config_contracts( +_GeneralUserConfigBase = derive_config_contract_model( RuntimeGeneralUserConfig, - read_name="GeneralUserConfig", - patch_name="GeneralUserConfigPatch", + model_name="GeneralUserConfig", ) -class GeneralConfig(_GeneralConfigReadBase): +class GeneralConfig(_GeneralConfigBase): type: Literal["GeneralConfig"] = Field( default="GeneralConfig", description="配置类型" ) -class GeneralConfigPatch(_GeneralConfigPatchBase): - type: Literal["GeneralConfig"] = Field( - default="GeneralConfig", description="配置类型" - ) - - -class GeneralUserConfig(_GeneralUserConfigReadBase): - type: Literal["GeneralUserConfig"] = Field( - default="GeneralUserConfig", description="配置类型" - ) - - -class GeneralUserConfigPatch(_GeneralUserConfigPatchBase): +class GeneralUserConfig(_GeneralUserConfigBase): type: Literal["GeneralUserConfig"] = Field( default="GeneralUserConfig", description="配置类型" ) @@ -47,7 +33,5 @@ class GeneralUserConfigPatch(_GeneralUserConfigPatchBase): __all__ = [ "GeneralConfig", - "GeneralConfigPatch", "GeneralUserConfig", - "GeneralUserConfigPatch", ] diff --git a/app/contracts/maa_contract.py b/app/contracts/maa_contract.py index c8e514fa..59ab36bb 100644 --- a/app/contracts/maa_contract.py +++ b/app/contracts/maa_contract.py @@ -4,56 +4,37 @@ from pydantic import Field -from .common_contract import derive_config_contracts +from .common_contract import derive_config_contract_model from app.models.maa import MaaConfig as RuntimeMaaConfig from app.models.maa import MaaPlanConfig as RuntimeMaaPlanConfig from app.models.maa import MaaUserConfig as RuntimeMaaUserConfig -_MaaConfigReadBase, _MaaConfigPatchBase = derive_config_contracts( +_MaaConfigBase = derive_config_contract_model( RuntimeMaaConfig, - read_name="MaaConfig", - patch_name="MaaConfigPatch", + model_name="MaaConfig", ) -_MaaUserConfigReadBase, _MaaUserConfigPatchBase = derive_config_contracts( +_MaaUserConfigBase = derive_config_contract_model( RuntimeMaaUserConfig, - read_name="MaaUserConfig", - patch_name="MaaUserConfigPatch", + model_name="MaaUserConfig", ) -_MaaPlanConfigReadBase, _MaaPlanPatchBase = derive_config_contracts( +_MaaPlanConfigBase = derive_config_contract_model( RuntimeMaaPlanConfig, - read_name="MaaPlanConfig", - patch_name="MaaPlanPatch", + model_name="MaaPlanConfig", ) -class MaaConfig(_MaaConfigReadBase): +class MaaConfig(_MaaConfigBase): type: Literal["MaaConfig"] = Field(default="MaaConfig", description="配置类型") -class MaaConfigPatch(_MaaConfigPatchBase): - type: Literal["MaaConfig"] = Field(default="MaaConfig", description="配置类型") - - -class MaaUserConfig(_MaaUserConfigReadBase): +class MaaUserConfig(_MaaUserConfigBase): type: Literal["MaaUserConfig"] = Field( default="MaaUserConfig", description="配置类型" ) -class MaaUserConfigPatch(_MaaUserConfigPatchBase): - type: Literal["MaaUserConfig"] = Field( - default="MaaUserConfig", description="配置类型" - ) - - -class MaaPlanConfig(_MaaPlanConfigReadBase): - type: Literal["MaaPlanConfig"] = Field( - default="MaaPlanConfig", description="配置类型" - ) - - -class MaaPlanPatch(_MaaPlanPatchBase): +class MaaPlanConfig(_MaaPlanConfigBase): type: Literal["MaaPlanConfig"] = Field( default="MaaPlanConfig", description="配置类型" ) @@ -61,9 +42,6 @@ class MaaPlanPatch(_MaaPlanPatchBase): __all__ = [ "MaaConfig", - "MaaConfigPatch", "MaaUserConfig", - "MaaUserConfigPatch", "MaaPlanConfig", - "MaaPlanPatch", ] diff --git a/app/contracts/maaend_contract.py b/app/contracts/maaend_contract.py index 98f000bd..2030f282 100644 --- a/app/contracts/maaend_contract.py +++ b/app/contracts/maaend_contract.py @@ -4,42 +4,28 @@ from pydantic import Field -from .common_contract import derive_config_contracts +from .common_contract import derive_config_contract_model from app.models.maaend import MaaEndConfig as RuntimeMaaEndConfig from app.models.maaend import MaaEndUserConfig as RuntimeMaaEndUserConfig -_MaaEndConfigReadBase, _MaaEndConfigPatchBase = derive_config_contracts( +_MaaEndConfigBase = derive_config_contract_model( RuntimeMaaEndConfig, - read_name="MaaEndConfig", - patch_name="MaaEndConfigPatch", + model_name="MaaEndConfig", ) -_MaaEndUserConfigReadBase, _MaaEndUserConfigPatchBase = derive_config_contracts( +_MaaEndUserConfigBase = derive_config_contract_model( RuntimeMaaEndUserConfig, - read_name="MaaEndUserConfig", - patch_name="MaaEndUserConfigPatch", + model_name="MaaEndUserConfig", ) -class MaaEndConfig(_MaaEndConfigReadBase): +class MaaEndConfig(_MaaEndConfigBase): type: Literal["MaaEndConfig"] = Field( default="MaaEndConfig", description="配置类型" ) -class MaaEndConfigPatch(_MaaEndConfigPatchBase): - type: Literal["MaaEndConfig"] = Field( - default="MaaEndConfig", description="配置类型" - ) - - -class MaaEndUserConfig(_MaaEndUserConfigReadBase): - type: Literal["MaaEndUserConfig"] = Field( - default="MaaEndUserConfig", description="配置类型" - ) - - -class MaaEndUserConfigPatch(_MaaEndUserConfigPatchBase): +class MaaEndUserConfig(_MaaEndUserConfigBase): type: Literal["MaaEndUserConfig"] = Field( default="MaaEndUserConfig", description="配置类型" ) @@ -47,7 +33,5 @@ class MaaEndUserConfigPatch(_MaaEndUserConfigPatchBase): __all__ = [ "MaaEndConfig", - "MaaEndConfigPatch", "MaaEndUserConfig", - "MaaEndUserConfigPatch", ] diff --git a/app/contracts/queue_contract.py b/app/contracts/queue_contract.py index ebef20e4..cd218580 100644 --- a/app/contracts/queue_contract.py +++ b/app/contracts/queue_contract.py @@ -10,52 +10,37 @@ ResourceCollectionOut, ResourceCreateOut, ResourceItemOut, - derive_config_contracts, + derive_config_contract_model, ) -_QueueReadBase, _QueuePatchBase = derive_config_contracts( +_QueueBase = derive_config_contract_model( QueueConfig, - read_name="QueueRead", - patch_name="QueuePatch", + model_name="QueueRead", include_groups=("Info",), ) -_TimeSetReadBase, _TimeSetPatchBase = derive_config_contracts( +_TimeSetBase = derive_config_contract_model( TimeSet, - read_name="TimeSetRead", - patch_name="TimeSetPatch", + model_name="TimeSetRead", include_groups=("Info",), ) -_QueueItemReadBase, _QueueItemPatchBase = derive_config_contracts( +_QueueItemBase = derive_config_contract_model( QueueItem, - read_name="QueueItemRead", - patch_name="QueueItemPatch", + model_name="QueueItemRead", include_groups=("Info",), ) -class QueueRead(_QueueReadBase): - pass +class QueueRead(_QueueBase): + """队列配置读取/写入模型。""" -class QueuePatch(_QueuePatchBase): - pass +class TimeSetRead(_TimeSetBase): + """时间集合读取/写入模型。""" -class TimeSetRead(_TimeSetReadBase): - pass - - -class TimeSetPatch(_TimeSetPatchBase): - pass - - -class QueueItemRead(_QueueItemReadBase): - pass - - -class QueueItemPatch(_QueueItemPatchBase): - pass +class QueueItemRead(_QueueItemBase): + """任务项读取/写入模型。""" class QueueIndexItem(ApiModel): @@ -88,11 +73,8 @@ class TimeSetIndexItem(ApiModel): __all__ = [ "QueueRead", - "QueuePatch", "TimeSetRead", - "TimeSetPatch", "QueueItemRead", - "QueueItemPatch", "QueueIndexItem", "QueueItemIndexItem", "TimeSetIndexItem", diff --git a/app/contracts/scripts_contract.py b/app/contracts/scripts_contract.py index 9dc978c9..7c103b4d 100644 --- a/app/contracts/scripts_contract.py +++ b/app/contracts/scripts_contract.py @@ -10,22 +10,19 @@ ResourceCollectionOut, ResourceCreateOut, ResourceItemOut, + dump_writable_data, project_model, ) from .general_contract import ( GeneralConfig, - GeneralConfigPatch, GeneralUserConfig, - GeneralUserConfigPatch, ) -from .maa_contract import MaaConfig, MaaConfigPatch, MaaUserConfig, MaaUserConfigPatch +from .maa_contract import MaaConfig, MaaUserConfig from .maaend_contract import ( MaaEndConfig, - MaaEndConfigPatch, MaaEndUserConfig, - MaaEndUserConfigPatch, ) -from .src_contract import SrcConfig, SrcConfigPatch, SrcUserConfig, SrcUserConfigPatch +from .src_contract import SrcConfig, SrcUserConfig ScriptConfigType = Literal["MaaConfig", "GeneralConfig", "SrcConfig", "MaaEndConfig"] @@ -45,18 +42,8 @@ | type[GeneralUserConfig] | type[MaaEndUserConfig] ) -ScriptPatchClass = ( - type[MaaConfigPatch] - | type[SrcConfigPatch] - | type[GeneralConfigPatch] - | type[MaaEndConfigPatch] -) -UserPatchClass = ( - type[MaaUserConfigPatch] - | type[SrcUserConfigPatch] - | type[GeneralUserConfigPatch] - | type[MaaEndUserConfigPatch] -) +ScriptPatchClass = ScriptModelClass +UserPatchClass = UserModelClass ScriptReadData = Annotated[ MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig, @@ -74,10 +61,10 @@ "MaaEndConfig": MaaEndConfig, } SCRIPT_PATCH_BY_TYPE: dict[ScriptConfigType, ScriptPatchClass] = { - "MaaConfig": MaaConfigPatch, - "GeneralConfig": GeneralConfigPatch, - "SrcConfig": SrcConfigPatch, - "MaaEndConfig": MaaEndConfigPatch, + "MaaConfig": MaaConfig, + "GeneralConfig": GeneralConfig, + "SrcConfig": SrcConfig, + "MaaEndConfig": MaaEndConfig, } USER_CONTRACT_BY_TYPE: dict[UserConfigType, UserModelClass] = { "MaaUserConfig": MaaUserConfig, @@ -86,10 +73,10 @@ "MaaEndUserConfig": MaaEndUserConfig, } USER_PATCH_BY_TYPE: dict[UserConfigType, UserPatchClass] = { - "MaaUserConfig": MaaUserConfigPatch, - "GeneralUserConfig": GeneralUserConfigPatch, - "SrcUserConfig": SrcUserConfigPatch, - "MaaEndUserConfig": MaaEndUserConfigPatch, + "MaaUserConfig": MaaUserConfig, + "GeneralUserConfig": GeneralUserConfig, + "SrcUserConfig": SrcUserConfig, + "MaaEndUserConfig": MaaEndUserConfig, } SCRIPT_CREATE_TO_CONFIG_TYPE: dict[ScriptCreateType, ScriptConfigType] = { "MAA": "MaaConfig", @@ -148,14 +135,11 @@ class ScriptUploadBody(ApiModel): UserCreateOut = ResourceCreateOut[UserReadData] ScriptPatchData = Annotated[ - MaaConfigPatch | SrcConfigPatch | GeneralConfigPatch | MaaEndConfigPatch, + MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig, Field(discriminator="type"), ] UserPatchData = Annotated[ - MaaUserConfigPatch - | SrcUserConfigPatch - | GeneralUserConfigPatch - | MaaEndUserConfigPatch, + MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig, Field(discriminator="type"), ] @@ -231,7 +215,9 @@ def dump_script_patch_data( ) -> dict[str, Any]: if data.type != script_type: raise ValueError(f"Patch 类型不匹配: 期望 {script_type}, 实际 {data.type}") - return data.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) + writable = dump_writable_data(data) + writable.pop("type", None) + return writable def dump_user_patch_data( @@ -239,7 +225,9 @@ def dump_user_patch_data( ) -> dict[str, Any]: if data.type != user_type: raise ValueError(f"Patch 类型不匹配: 期望 {user_type}, 实际 {data.type}") - return data.model_dump(exclude_unset=True, exclude_none=True, exclude={"type"}) + writable = dump_writable_data(data) + writable.pop("type", None) + return writable __all__ = [ diff --git a/app/contracts/setting_contract.py b/app/contracts/setting_contract.py index 9bdda4f0..22d349ad 100644 --- a/app/contracts/setting_contract.py +++ b/app/contracts/setting_contract.py @@ -11,37 +11,27 @@ ResourceCollectionOut, ResourceCreateOut, ResourceItemOut, - derive_config_contracts, + derive_config_contract_model, ) -_WebhookReadBase, _WebhookPatchBase = derive_config_contracts( +_WebhookBase = derive_config_contract_model( Webhook, - read_name="WebhookRead", - patch_name="WebhookPatch", + model_name="WebhookRead", ) -_GlobalConfigReadBase, _GlobalConfigPatchBase = derive_config_contracts( +_GlobalConfigBase = derive_config_contract_model( GlobalConfig, - read_name="GlobalConfigRead", - patch_name="GlobalConfigPatch", + model_name="GlobalConfigRead", include_groups=("Function", "Voice", "Start", "UI", "Notify", "Update"), ) -class WebhookRead(_WebhookReadBase): - pass +class WebhookRead(_WebhookBase): + """Webhook 配置读取/写入模型。""" -class WebhookPatch(_WebhookPatchBase): - pass - - -class GlobalConfigRead(_GlobalConfigReadBase): - pass - - -class GlobalConfigPatch(_GlobalConfigPatchBase): - pass +class GlobalConfigRead(_GlobalConfigBase): + """全局配置读取/写入模型。""" class WebhookIndexItem(ApiModel): @@ -57,9 +47,7 @@ class WebhookIndexItem(ApiModel): __all__ = [ "WebhookRead", - "WebhookPatch", "GlobalConfigRead", - "GlobalConfigPatch", "WebhookIndexItem", "WebhookGetOut", "WebhookDetailOut", diff --git a/app/contracts/src_contract.py b/app/contracts/src_contract.py index d967f8b4..26769fbb 100644 --- a/app/contracts/src_contract.py +++ b/app/contracts/src_contract.py @@ -4,38 +4,26 @@ from pydantic import Field -from .common_contract import derive_config_contracts +from .common_contract import derive_config_contract_model from app.models.src import SrcConfig as RuntimeSrcConfig from app.models.src import SrcUserConfig as RuntimeSrcUserConfig -_SrcConfigReadBase, _SrcConfigPatchBase = derive_config_contracts( +_SrcConfigBase = derive_config_contract_model( RuntimeSrcConfig, - read_name="SrcConfig", - patch_name="SrcConfigPatch", + model_name="SrcConfig", ) -_SrcUserConfigReadBase, _SrcUserConfigPatchBase = derive_config_contracts( +_SrcUserConfigBase = derive_config_contract_model( RuntimeSrcUserConfig, - read_name="SrcUserConfig", - patch_name="SrcUserConfigPatch", + model_name="SrcUserConfig", ) -class SrcConfig(_SrcConfigReadBase): +class SrcConfig(_SrcConfigBase): type: Literal["SrcConfig"] = Field(default="SrcConfig", description="配置类型") -class SrcConfigPatch(_SrcConfigPatchBase): - type: Literal["SrcConfig"] = Field(default="SrcConfig", description="配置类型") - - -class SrcUserConfig(_SrcUserConfigReadBase): - type: Literal["SrcUserConfig"] = Field( - default="SrcUserConfig", description="配置类型" - ) - - -class SrcUserConfigPatch(_SrcUserConfigPatchBase): +class SrcUserConfig(_SrcUserConfigBase): type: Literal["SrcUserConfig"] = Field( default="SrcUserConfig", description="配置类型" ) @@ -43,7 +31,5 @@ class SrcUserConfigPatch(_SrcUserConfigPatchBase): __all__ = [ "SrcConfig", - "SrcConfigPatch", "SrcUserConfig", - "SrcUserConfigPatch", ] diff --git a/app/contracts/tools_contract.py b/app/contracts/tools_contract.py index a40dcd25..bb7d2cfd 100644 --- a/app/contracts/tools_contract.py +++ b/app/contracts/tools_contract.py @@ -1,22 +1,17 @@ from __future__ import annotations -from .common_contract import ResourceItemOut, derive_config_contracts +from .common_contract import ResourceItemOut, derive_config_contract_model from app.models.global_config import ToolsConfig -_ToolsConfigReadBase, _ToolsConfigPatchBase = derive_config_contracts( +_ToolsConfigBase = derive_config_contract_model( ToolsConfig, - read_name="ToolsConfigRead", - patch_name="ToolsConfigPatch", + model_name="ToolsConfigRead", ) -class ToolsConfigRead(_ToolsConfigReadBase): - pass - - -class ToolsConfigPatch(_ToolsConfigPatchBase): - pass +class ToolsConfigRead(_ToolsConfigBase): + """工具配置读取/写入模型。""" ToolsGetOut = ResourceItemOut[ToolsConfigRead] @@ -24,6 +19,5 @@ class ToolsConfigPatch(_ToolsConfigPatchBase): __all__ = [ "ToolsConfigRead", - "ToolsConfigPatch", "ToolsGetOut", ] From 6b0ad36018445d511e91050370e268d7802b34ec Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 6 Apr 2026 21:52:33 +0800 Subject: [PATCH 17/29] =?UTF-8?q?refactor(api):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=86=97=E4=BD=99=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96API=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/common.py | 87 ----------------------- app/api/core.py | 10 +-- app/api/dispatch.py | 26 ++----- app/api/history.py | 41 +++++------ app/api/info.py | 82 ++++++---------------- app/api/ocr.py | 158 ++++++++++++++---------------------------- app/api/plan.py | 23 ++---- app/api/scripts.py | 110 +++++++---------------------- app/api/ws_command.py | 5 +- app/api/ws_debug.py | 82 ++++++++++------------ main.py | 120 +++++++++++++++++++++++++++++++- 11 files changed, 283 insertions(+), 461 deletions(-) diff --git a/app/api/common.py b/app/api/common.py index a31da36e..9335b324 100644 --- a/app/api/common.py +++ b/app/api/common.py @@ -1,94 +1,7 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Callable -from functools import wraps -import asyncio -from typing import ParamSpec, TypeVar - from app.contracts.common_contract import ComboBoxItem, ComboBoxOut, OutBase - -OutT = TypeVar("OutT", bound=OutBase) -P = ParamSpec("P") - - -RECOVERABLE_EXCEPTIONS: tuple[type[Exception], ...] = ( - ValueError, - TypeError, - KeyError, - RuntimeError, - LookupError, - OSError, - asyncio.TimeoutError, -) - - -def error_out( - model_cls: type[OutT], - exc: Exception, - *, - message: str | None = None, - **kwargs: object, -) -> OutT: - return model_cls( - code=500, - status="error", - message=message or f"{type(exc).__name__}: {str(exc)}", - **kwargs, - ) - - -async def run_api( - success_factory: Callable[[], Awaitable[OutT]], - *, - model_cls: type[OutT], - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - **fallback_kwargs: object, -) -> OutT: - try: - return await success_factory() - except RECOVERABLE_EXCEPTIONS as exc: - if on_error is not None: - on_error(exc) - return error_out( - model_cls, - exc, - message=message, - **fallback_kwargs, - ) - - -def api_guard( - *, - model_cls: type[OutT], - message: str | None = None, - on_error: Callable[[Exception], None] | None = None, - **fallback_kwargs: object, -) -> Callable[[Callable[P, Awaitable[OutT]]], Callable[P, Awaitable[OutT]]]: - """为 FastAPI 路由提供统一异常包装的装饰器。""" - - def decorator(func: Callable[P, Awaitable[OutT]]) -> Callable[P, Awaitable[OutT]]: - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> OutT: - return await run_api( - lambda: func(*args, **kwargs), - model_cls=model_cls, - message=message, - on_error=on_error, - **fallback_kwargs, - ) - - return wrapper - - return decorator - - __all__ = [ "OutBase", "ComboBoxItem", "ComboBoxOut", - "error_out", - "run_api", - "api_guard", ] diff --git a/app/api/core.py b/app/api/core.py index 8c96d8d7..8e5dbbe3 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -32,7 +32,6 @@ from app.contracts.common_contract import OutBase from app.models.shared import WebSocketMessage from app.api.ws_command import ws_command -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.utils import get_logger router = APIRouter(prefix="/api/core", tags=["核心信息"]) @@ -107,10 +106,7 @@ async def connect_websocket(websocket: WebSocket): async def close() -> OutBase: """关闭后端程序""" - try: - if Config.websocket is not None: - await Config.websocket.close(code=1000, reason="正常关闭") - await System.set_power("KillSelf", from_frontend=True) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + if Config.websocket is not None: + await Config.websocket.close(code=1000, reason="正常关闭") + await System.set_power("KillSelf", from_frontend=True) return OutBase() diff --git a/app/api/dispatch.py b/app/api/dispatch.py index da20bf3b..edc9a902 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -33,7 +33,6 @@ TaskCreateIn, TaskCreateOut, ) -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) @@ -45,10 +44,7 @@ response_model=TaskCreateOut, ) async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: - try: - task_id = await TaskManager.add_task(task.mode, task.taskId) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(TaskCreateOut, e, taskId="") + task_id = await TaskManager.add_task(task.mode, task.taskId) return TaskCreateOut(taskId=str(task_id)) @@ -59,10 +55,7 @@ async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut: response_model=OutBase, ) async def stop_task(task: DispatchIn = Body(...)) -> OutBase: - try: - await TaskManager.stop_task(task.taskId) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + await TaskManager.stop_task(task.taskId) return OutBase() @@ -73,10 +66,7 @@ async def stop_task(task: DispatchIn = Body(...)) -> OutBase: response_model=PowerOut, ) async def get_power() -> PowerOut: - try: - signal = Config.power_sign - except RECOVERABLE_EXCEPTIONS as e: - return error_out(PowerOut, e, signal="NoAction") + signal = Config.power_sign return PowerOut(signal=signal) @@ -87,10 +77,7 @@ async def get_power() -> PowerOut: response_model=OutBase, ) async def set_power(task: PowerIn = Body(...)) -> OutBase: - try: - Config.power_sign = task.signal - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + Config.power_sign = task.signal return OutBase() @@ -101,8 +88,5 @@ async def set_power(task: PowerIn = Body(...)) -> OutBase: response_model=OutBase, ) async def cancel_power_task() -> OutBase: - try: - await System.cancel_power_task() - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + await System.cancel_power_task() return OutBase() diff --git a/app/api/history.py b/app/api/history.py index ae7271dc..663786b3 100644 --- a/app/api/history.py +++ b/app/api/history.py @@ -27,7 +27,6 @@ from pydantic import TypeAdapter from app.core import Config -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.contracts.history_contract import ( HistoryData, HistoryDataGetIn, @@ -60,21 +59,18 @@ def _build_history_data(raw: dict[str, object]) -> HistoryData: response_model=HistorySearchOut, ) async def search_history(history: HistorySearchIn) -> HistorySearchOut: - try: - raw_data = await Config.search_history( - history.mode, - datetime.strptime(history.start_date, "%Y-%m-%d").date(), - datetime.strptime(history.end_date, "%Y-%m-%d").date(), - ) - data: dict[str, dict[str, HistoryData]] = {} - for date, users in raw_data.items(): - current_users: dict[str, HistoryData] = {} - for user, records in users.items(): - record = await Config.merge_statistic_info(records) - current_users[user] = _build_history_data(record) - data[date] = current_users - except RECOVERABLE_EXCEPTIONS as e: - return error_out(HistorySearchOut, e, data={}) + raw_data = await Config.search_history( + history.mode, + datetime.strptime(history.start_date, "%Y-%m-%d").date(), + datetime.strptime(history.end_date, "%Y-%m-%d").date(), + ) + data: dict[str, dict[str, HistoryData]] = {} + for date, users in raw_data.items(): + current_users: dict[str, HistoryData] = {} + for user, records in users.items(): + record = await Config.merge_statistic_info(records) + current_users[user] = _build_history_data(record) + data[date] = current_users return HistorySearchOut(data=data) @@ -85,12 +81,9 @@ async def search_history(history: HistorySearchIn) -> HistorySearchOut: response_model=HistoryDataGetOut, ) async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut: - try: - path = Path(history.jsonPath) - raw_data = await Config.merge_statistic_info([path]) - raw_data.pop("index", None) - raw_data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8") - data = _build_history_data(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(HistoryDataGetOut, e, data=HistoryData()) + path = Path(history.jsonPath) + raw_data = await Config.merge_statistic_info([path]) + raw_data.pop("index", None) + raw_data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8") + data = _build_history_data(raw_data) return HistoryDataGetOut(data=data) diff --git a/app/api/info.py b/app/api/info.py index 58600537..a55cd065 100644 --- a/app/api/info.py +++ b/app/api/info.py @@ -34,7 +34,6 @@ InfoOut, OutBase, ) -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.contracts.info_contract import ( GetStageIn, NoticeOut, @@ -66,16 +65,7 @@ def _to_combobox_items(raw_data: object) -> list[ComboBoxItem]: response_model=VersionOut, ) async def get_git_version() -> VersionOut: - try: - is_latest, commit_hash, commit_time = await Config.get_git_version() - except RECOVERABLE_EXCEPTIONS as e: - return error_out( - VersionOut, - e, - if_need_update=False, - current_time="unknown", - current_hash="unknown", - ) + is_latest, commit_hash, commit_time = await Config.get_git_version() return VersionOut( if_need_update=not is_latest, current_time=commit_time, @@ -92,11 +82,8 @@ async def get_git_version() -> VersionOut: async def get_stage_combox( stage: GetStageIn = Body(..., description="关卡号类型"), ) -> ComboBoxOut: - try: - raw_data = cast(object, await Config.get_stage_info(stage.type)) - data = _to_combobox_items(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = cast(object, await Config.get_stage_info(stage.type)) + data = _to_combobox_items(raw_data) return ComboBoxOut(data=data) @@ -107,11 +94,8 @@ async def get_stage_combox( response_model=ComboBoxOut, ) async def get_script_combox() -> ComboBoxOut: - try: - raw_data = await Config.get_script_combox() - data = _to_combobox_items(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = await Config.get_script_combox() + data = _to_combobox_items(raw_data) return ComboBoxOut(data=data) @@ -122,11 +106,8 @@ async def get_script_combox() -> ComboBoxOut: response_model=ComboBoxOut, ) async def get_task_combox() -> ComboBoxOut: - try: - raw_data = await Config.get_task_combox() - data = _to_combobox_items(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = await Config.get_task_combox() + data = _to_combobox_items(raw_data) return ComboBoxOut(data=data) @@ -137,11 +118,8 @@ async def get_task_combox() -> ComboBoxOut: response_model=ComboBoxOut, ) async def get_plan_combox() -> ComboBoxOut: - try: - raw_data = await Config.get_plan_combox() - data = _to_combobox_items(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = await Config.get_plan_combox() + data = _to_combobox_items(raw_data) return ComboBoxOut(data=data) @@ -152,11 +130,8 @@ async def get_plan_combox() -> ComboBoxOut: response_model=ComboBoxOut, ) async def get_emulator_combox() -> ComboBoxOut: - try: - raw_data = await Config.get_emulator_combox() - data = _to_combobox_items(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = await Config.get_emulator_combox() + data = _to_combobox_items(raw_data) return ComboBoxOut(data=data) @@ -169,13 +144,10 @@ async def get_emulator_combox() -> ComboBoxOut: async def get_emulator_devices_combox( emulator: EmulatorIdBody = Body(...), ) -> ComboBoxOut: - try: - raw_data = cast( - object, await Config.get_emulator_devices_combox(emulator.emulatorId) - ) - data = _to_combobox_items(raw_data) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = cast( + object, await Config.get_emulator_devices_combox(emulator.emulatorId) + ) + data = _to_combobox_items(raw_data) return ComboBoxOut(data=data) @@ -186,10 +158,7 @@ async def get_emulator_devices_combox( response_model=NoticeOut, ) async def get_notice_info() -> NoticeOut: - try: - if_need_show, data = await Config.get_notice() - except RECOVERABLE_EXCEPTIONS as e: - return error_out(NoticeOut, e, if_need_show=False, data={}) + if_need_show, data = await Config.get_notice() return NoticeOut(if_need_show=if_need_show, data=data) @@ -200,10 +169,7 @@ async def get_notice_info() -> NoticeOut: response_model=OutBase, ) async def confirm_notice() -> OutBase: - try: - await Config.set("Data", "IfShowNotice", False) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + await Config.set("Data", "IfShowNotice", False) return OutBase() @@ -228,10 +194,7 @@ async def confirm_notice() -> OutBase: response_model=InfoOut, ) async def get_web_config() -> InfoOut: - try: - data = await Config.get_web_config() - except RECOVERABLE_EXCEPTIONS as e: - return error_out(InfoOut, e, data={}) + data = await Config.get_web_config() return InfoOut(data={"WebConfig": data}) @@ -242,11 +205,8 @@ async def get_web_config() -> InfoOut: response_model=InfoOut, ) async def get_overview() -> InfoOut: - try: - raw_stage = cast(object, await Config.get_stage_info("Info")) - stage = cast(dict[str, Any], raw_stage if isinstance(raw_stage, dict) else {}) + raw_stage = cast(object, await Config.get_stage_info("Info")) + stage = cast(dict[str, Any], raw_stage if isinstance(raw_stage, dict) else {}) - proxy = await Config.get_proxy_overview() - except RECOVERABLE_EXCEPTIONS as e: - return error_out(InfoOut, e, data={"Stage": [], "Proxy": []}) + proxy = await Config.get_proxy_overview() return InfoOut(data={"Stage": stage, "Proxy": proxy}) diff --git a/app/api/ocr.py b/app/api/ocr.py index 7fa8c434..23449dc0 100644 --- a/app/api/ocr.py +++ b/app/api/ocr.py @@ -21,22 +21,26 @@ # Contact: DLmaster_361@163.com -from fastapi import APIRouter, Body +from fastapi import APIRouter, Body, HTTPException from pydantic import Field -from typing import Optional +from typing import NoReturn, Optional import base64 from io import BytesIO from app.utils.OCR.OCRtool import OCRTool from app.utils import get_logger from app.contracts.common_contract import ApiModel, OutBase -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out, run_api logger = get_logger("OCR API") router = APIRouter(prefix="/api/ocr", tags=["OCR识别"]) +def _raise_ocr_http_error(prefix: str, exc: Exception) -> NoReturn: + logger.error(f"{prefix}: {type(exc).__name__}: {exc}") + raise HTTPException(status_code=500, detail=f"{prefix}: {exc}") from exc + + # ========== 截图相关模型 ========== class OCRWindowIn(ApiModel): window_title: str = Field(..., description="窗口标题(用于查找窗口)") @@ -176,16 +180,10 @@ async def _success() -> OCRScreenshotOut: image_height=screenshot_image.height, ) - return await run_api( - _success, - model_cls=OCRScreenshotOut, - message="截图失败", - image_base64="", - region=(0, 0, 0, 0), - image_width=0, - image_height=0, - on_error=lambda e: logger.error(f"截图失败: {type(e).__name__}: {str(e)}"), - ) + try: + return await _success() + except Exception as e: + _raise_ocr_http_error("截图失败", e) @router.post( @@ -212,65 +210,35 @@ async def get_screenshot_adb(params: ADBScreenshotIn = Body(...)) -> ADBScreensh ADBScreenshotOut: 包含Base64编码的截图和设备信息 """ try: - # 使用 OCRTool 通过 ADB 获取截图 screenshot_image = OCRTool.get_screenshot_with_adb( adb_path=params.adb_path, serial=params.serial, use_screencap=params.use_screencap, ) - - # 将PIL Image转换为Base64 - buffer = BytesIO() - screenshot_image.save(buffer, format="PNG") - image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") - - logger.info( - f"成功通过 ADB 截取设备 [{params.serial}] 的截图,尺寸: {screenshot_image.size}" - ) - - return ADBScreenshotOut( - code=200, - status="success", - message="ADB 截图成功", - image_base64=image_base64, - image_width=screenshot_image.width, - image_height=screenshot_image.height, - serial=params.serial, - ) - except FileNotFoundError as e: logger.error(f"ADB 文件未找到: {str(e)}") - return error_out( - ADBScreenshotOut, - e, - message=f"ADB 文件未找到: {str(e)}", - image_base64="", - image_width=0, - image_height=0, - serial=params.serial, - ) + raise HTTPException(status_code=400, detail=f"ADB 文件未找到: {str(e)}") from e except RuntimeError as e: logger.error(f"ADB 截图运行时错误: {str(e)}") - return error_out( - ADBScreenshotOut, - e, - message=f"ADB 截图失败: {str(e)}", - image_base64="", - image_width=0, - image_height=0, - serial=params.serial, - ) - except RECOVERABLE_EXCEPTIONS as e: - logger.error(f"ADB 截图失败: {type(e).__name__}: {str(e)}") - return error_out( - ADBScreenshotOut, - e, - message=f"ADB 截图失败: {type(e).__name__}: {str(e)}", - image_base64="", - image_width=0, - image_height=0, - serial=params.serial, - ) + raise HTTPException(status_code=500, detail=f"ADB 截图失败: {str(e)}") from e + + buffer = BytesIO() + screenshot_image.save(buffer, format="PNG") + image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") + + logger.info( + f"成功通过 ADB 截取设备 [{params.serial}] 的截图,尺寸: {screenshot_image.size}" + ) + + return ADBScreenshotOut( + code=200, + status="success", + message="ADB 截图成功", + image_base64=image_base64, + image_width=screenshot_image.width, + image_height=screenshot_image.height, + serial=params.serial, + ) # ========== 测试接口:检查图像 ========== @@ -316,14 +284,10 @@ async def _success() -> CheckImageOut: attempts=params.retry_times, ) - return await run_api( - _success, - model_cls=CheckImageOut, - message="图像检查失败", - found=False, - attempts=0, - on_error=lambda e: logger.error(f"图像检查失败: {type(e).__name__}: {str(e)}"), - ) + try: + return await _success() + except Exception as e: + _raise_ocr_http_error("图像检查失败", e) @router.post( @@ -368,16 +332,10 @@ async def _success() -> CheckImageOut: attempts=params.retry_times, ) - return await run_api( - _success, - model_cls=CheckImageOut, - message="多图像检查失败", - found=False, - attempts=0, - on_error=lambda e: logger.error( - f"多图像检查(ANY)失败: {type(e).__name__}: {str(e)}" - ), - ) + try: + return await _success() + except Exception as e: + _raise_ocr_http_error("多图像检查失败", e) @router.post( @@ -422,16 +380,10 @@ async def _success() -> CheckImageOut: attempts=params.retry_times, ) - return await run_api( - _success, - model_cls=CheckImageOut, - message="多图像检查失败", - found=False, - attempts=0, - on_error=lambda e: logger.error( - f"多图像检查(ALL)失败: {type(e).__name__}: {str(e)}" - ), - ) + try: + return await _success() + except Exception as e: + _raise_ocr_http_error("多图像检查失败", e) # ========== 测试接口:点击操作 ========== @@ -477,14 +429,10 @@ async def _success() -> ClickOut: attempts=params.retry_times, ) - return await run_api( - _success, - model_cls=ClickOut, - message="图像点击失败", - success=False, - attempts=0, - on_error=lambda e: logger.error(f"图像点击失败: {type(e).__name__}: {str(e)}"), - ) + try: + return await _success() + except Exception as e: + _raise_ocr_http_error("图像点击失败", e) @router.post( @@ -525,11 +473,7 @@ async def _success() -> ClickOut: attempts=params.retry_times, ) - return await run_api( - _success, - model_cls=ClickOut, - message="文字点击失败", - success=False, - attempts=0, - on_error=lambda e: logger.error(f"文字点击失败: {type(e).__name__}: {str(e)}"), - ) + try: + return await _success() + except Exception as e: + _raise_ocr_http_error("文字点击失败", e) diff --git a/app/api/plan.py b/app/api/plan.py index 0ed1789a..aefa49e7 100644 --- a/app/api/plan.py +++ b/app/api/plan.py @@ -25,7 +25,6 @@ from fastapi import APIRouter, Body, Path -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.core import Config from app.contracts.common_contract import ( IndexOrderPatch, @@ -57,11 +56,8 @@ response_model=PlanCreateOut, ) async def create_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut: - try: - uid, config = await Config.add_plan(plan.type) - data = project_model(MaaPlanRead, await config.toDict()) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(PlanCreateOut, e, id="", data=MaaPlanRead()) + uid, config = await Config.add_plan(plan.type) + data = project_model(MaaPlanRead, await config.toDict()) return PlanCreateOut(id=str(uid), data=data) @@ -98,10 +94,7 @@ async def get_plan(plan_id: PlanIdPath) -> PlanDetailOut: response_model=OutBase, ) async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> OutBase: - try: - await Config.update_plan(plan_id, dump_writable_data(body.data)) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + await Config.update_plan(plan_id, dump_writable_data(body.data)) return OutBase() @@ -112,10 +105,7 @@ async def update_plan(plan_id: PlanIdPath, body: PlanUpdateBody = Body(...)) -> response_model=OutBase, ) async def delete_plan(plan_id: PlanIdPath) -> OutBase: - try: - await Config.del_plan(plan_id) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + await Config.del_plan(plan_id) return OutBase() @@ -126,8 +116,5 @@ async def delete_plan(plan_id: PlanIdPath) -> OutBase: response_model=OutBase, ) async def reorder_plan(body: IndexOrderPatch = Body(...)) -> OutBase: - try: - await Config.reorder_plan(body.index_list) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(OutBase, e) + await Config.reorder_plan(body.index_list) return OutBase() diff --git a/app/api/scripts.py b/app/api/scripts.py index 3a6289d5..b3664b36 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -27,7 +27,6 @@ from fastapi import APIRouter, Body, Path from pydantic import TypeAdapter -from app.api.common import RECOVERABLE_EXCEPTIONS, error_out from app.core import Config from app.contracts.common_contract import ( ComboBoxItem, @@ -59,7 +58,6 @@ project_script_model_map, project_user_model, project_user_model_map, - script_contract_type_from_create, script_contract_type_from_runtime, user_contract_type_from_script, dump_script_patch_data, @@ -105,22 +103,11 @@ async def list_scripts() -> ScriptGetOut: response_model=ScriptCreateOut, ) async def create_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut: - try: - uid, config = await Config.add_script(script.type, script.copyFromId) - data = project_script_model( - script_contract_type_from_runtime(type(config).__name__), - await config.toDict(), - ) - except RECOVERABLE_EXCEPTIONS as e: - return error_out( - ScriptCreateOut, - e, - id="", - data=project_script_model( - script_contract_type_from_create(script.type), - {}, - ), - ) + uid, config = await Config.add_script(script.type, script.copyFromId) + data = project_script_model( + script_contract_type_from_runtime(type(config).__name__), + await config.toDict(), + ) return ScriptCreateOut(id=str(uid), data=data) @@ -142,24 +129,10 @@ async def reorder_scripts(body: IndexOrderPatch = Body(...)) -> OutBase: response_model=ScriptDetailOut, ) async def get_script(script_id: ScriptIdPath) -> ScriptDetailOut: - try: - index, data = await Config.get_script(script_id) - script_index = project_model_list(ScriptIndexItem, index) - projected = project_script_model_map(script_index, data) - return ScriptDetailOut(data=projected[script_id]) - except RECOVERABLE_EXCEPTIONS as e: - script_type = "GeneralConfig" - try: - script_type = script_contract_type_from_runtime( - type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ - ) - except RECOVERABLE_EXCEPTIONS: - pass - return error_out( - ScriptDetailOut, - e, - data=project_script_model(script_type, {}), - ) + index, data = await Config.get_script(script_id) + script_index = project_model_list(ScriptIndexItem, index) + projected = project_script_model_map(script_index, data) + return ScriptDetailOut(data=projected[script_id]) @router.patch( @@ -265,26 +238,12 @@ async def list_users(script_id: ScriptIdPath) -> UserGetOut: response_model=UserCreateOut, ) async def create_user(script_id: ScriptIdPath) -> UserCreateOut: - script_type = None - try: - uid, config = await Config.add_user(script_id) - script_type = script_contract_type_from_runtime( - type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ - ) - user_type = user_contract_type_from_script(script_type) - data = project_user_model(user_type, await config.toDict()) - except RECOVERABLE_EXCEPTIONS as e: - user_type = ( - user_contract_type_from_script(script_type) - if script_type is not None - else "GeneralUserConfig" - ) - return error_out( - UserCreateOut, - e, - id="", - data=project_user_model(user_type, {}), - ) + uid, config = await Config.add_user(script_id) + script_type = script_contract_type_from_runtime( + type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ + ) + user_type = user_contract_type_from_script(script_type) + data = project_user_model(user_type, await config.toDict()) return UserCreateOut(id=str(uid), data=data) @@ -308,25 +267,10 @@ async def reorder_users( response_model=UserDetailOut, ) async def get_user(script_id: ScriptIdPath, user_id: UserIdPath) -> UserDetailOut: - try: - index, data = await Config.get_user(script_id, user_id) - user_index = project_model_list(UserIndexItem, index) - projected = project_user_model_map(user_index, data) - return UserDetailOut(data=projected[user_id]) - except RECOVERABLE_EXCEPTIONS as e: - user_type = "GeneralUserConfig" - try: - script_type = script_contract_type_from_runtime( - type(Config.ScriptConfig[uuid.UUID(script_id)]).__name__ - ) - user_type = user_contract_type_from_script(script_type) - except RECOVERABLE_EXCEPTIONS: - pass - return error_out( - UserDetailOut, - e, - data=project_user_model(user_type, {}), - ) + index, data = await Config.get_user(script_id, user_id) + user_index = project_model_list(UserIndexItem, index) + projected = project_user_model_map(user_index, data) + return UserDetailOut(data=projected[user_id]) @router.patch( @@ -385,11 +329,8 @@ async def import_infrastructure( async def get_user_infrastructure_options( script_id: ScriptIdPath, user_id: UserIdPath ) -> ComboBoxOut: - try: - raw_data = await Config.get_user_combox_infrastructure(script_id, user_id) - data = COMBOBOX_ITEMS_ADAPTER.validate_python(raw_data or []) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(ComboBoxOut, e, data=[]) + raw_data = await Config.get_user_combox_infrastructure(script_id, user_id) + data = COMBOBOX_ITEMS_ADAPTER.validate_python(raw_data or []) return ComboBoxOut(data=data) @@ -451,12 +392,9 @@ async def get_user_webhook( user_id: UserIdPath, webhook_id: WebhookIdPath, ) -> WebhookDetailOut: - try: - _, data = await Config.get_webhook(script_id, user_id, webhook_id) - projected = project_model_map(WebhookRead, data) - return WebhookDetailOut(data=projected[webhook_id]) - except RECOVERABLE_EXCEPTIONS as e: - return error_out(WebhookDetailOut, e, data=WebhookRead()) + _, data = await Config.get_webhook(script_id, user_id, webhook_id) + projected = project_model_map(WebhookRead, data) + return WebhookDetailOut(data=projected[webhook_id]) @router.patch( diff --git a/app/api/ws_command.py b/app/api/ws_command.py index a4767348..abb74178 100644 --- a/app/api/ws_command.py +++ b/app/api/ws_command.py @@ -32,7 +32,6 @@ from typing import Any, Callable, ParamSpec, TypeAlias, TypeVar, cast from pydantic import BaseModel -from app.api.common import RECOVERABLE_EXCEPTIONS from app.utils.logger import get_logger logger = get_logger("WS命令") @@ -153,7 +152,7 @@ async def execute_ws_command( try: param_instance = param_type(**(params or {})) result = await func(param_instance) - except RECOVERABLE_EXCEPTIONS as e: + except Exception as e: logger.error(f"构建参数模型失败: {type(e).__name__}: {e}") return _failed_result(f"参数错误: {str(e)}", 400) elif params: @@ -173,7 +172,7 @@ async def execute_ws_command( else: return {"success": True, "data": result, "code": 200} - except RECOVERABLE_EXCEPTIONS as e: + except Exception as e: logger.error( f"执行命令 {endpoint} 失败: {type(e).__name__}: {str(e)}", exc_info=True ) diff --git a/app/api/ws_debug.py b/app/api/ws_debug.py index 772b387b..801cc7f7 100644 --- a/app/api/ws_debug.py +++ b/app/api/ws_debug.py @@ -28,8 +28,8 @@ 支持:创建客户端、连接、断开、发送消息、鉴权等 """ -from typing import Optional -from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from typing import NoReturn, Optional +from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect from app.utils.websocket import ws_client_manager from app.api.ws_command import list_ws_commands @@ -50,13 +50,17 @@ WSClearHistoryIn, WSCommandsOut, ) -from app.api.common import RECOVERABLE_EXCEPTIONS, run_api logger = get_logger("WS调试") router = APIRouter(prefix="/api/ws_debug", tags=["WebSocket调试"]) +def _raise_ws_http_error(prefix: str, exc: Exception) -> NoReturn: + logger.error(f"{prefix}: {type(exc).__name__}: {exc}") + raise HTTPException(status_code=500, detail=f"{prefix}: {exc}") from exc + + # ============== API 路由 ============== @@ -98,12 +102,10 @@ async def _success() -> WSClientCreateOut: }, ) - return await run_api( - _success, - model_cls=WSClientCreateOut, - message="创建客户端失败", - on_error=lambda e: logger.error(f"创建客户端失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("创建客户端失败", e) @router.post( @@ -146,12 +148,10 @@ async def _success() -> WSClientStatusOut: }, ) - return await run_api( - _success, - model_cls=WSClientStatusOut, - message="连接失败", - on_error=lambda e: logger.error(f"连接客户端失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("连接客户端失败", e) @router.post( @@ -177,12 +177,10 @@ async def _success() -> WSClientStatusOut: data={"name": request.name, "is_connected": False}, ) - return await run_api( - _success, - model_cls=WSClientStatusOut, - message="断开失败", - on_error=lambda e: logger.error(f"断开客户端失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("断开客户端失败", e) @router.post( @@ -215,12 +213,10 @@ async def _success() -> WSClientStatusOut: code=200, status="success", message=f"客户端 [{request.name}] 已删除" ) - return await run_api( - _success, - model_cls=WSClientStatusOut, - message="删除失败", - on_error=lambda e: logger.error(f"删除客户端失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("删除客户端失败", e) @router.post( @@ -304,12 +300,10 @@ async def _success() -> WSClientStatusOut: else: return WSClientStatusOut(code=500, status="error", message="消息发送失败") - return await run_api( - _success, - model_cls=WSClientStatusOut, - message="发送失败", - on_error=lambda e: logger.error(f"发送消息失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("发送消息失败", e) @router.post( @@ -346,12 +340,10 @@ async def _success() -> WSClientStatusOut: else: return WSClientStatusOut(code=500, status="error", message="消息发送失败") - return await run_api( - _success, - model_cls=WSClientStatusOut, - message="发送失败", - on_error=lambda e: logger.error(f"发送消息失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("发送消息失败", e) @router.post( @@ -396,12 +388,10 @@ async def _success() -> WSClientStatusOut: code=500, status="error", message="认证消息发送失败" ) - return await run_api( - _success, - model_cls=WSClientStatusOut, - message="发送失败", - on_error=lambda e: logger.error(f"发送认证消息失败: {type(e).__name__}: {e}"), - ) + try: + return await _success() + except Exception as e: + _raise_ws_http_error("发送认证消息失败", e) @router.get( @@ -504,7 +494,7 @@ async def websocket_live(websocket: WebSocket): await websocket.send_text("pong") except WebSocketDisconnect: break - except RECOVERABLE_EXCEPTIONS as e: + except Exception as e: logger.error(f"WebSocket 错误: {e}") break diff --git a/main.py b/main.py index 6b0b5f50..4c711203 100644 --- a/main.py +++ b/main.py @@ -68,10 +68,17 @@ def main() -> None: if is_admin(): import asyncio import uvicorn - from fastapi import FastAPI + from fastapi import FastAPI, HTTPException, Request + from fastapi.exceptions import RequestValidationError + from fastapi.routing import APIRoute from fastapi_mcp import FastApiMCP + from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from contextlib import asynccontextmanager + from typing import Any, cast, get_args, get_origin + from pydantic import BaseModel + from pydantic_core import PydanticUndefined + from app.contracts.common_contract import OutBase @asynccontextmanager async def lifespan(app: FastAPI): @@ -137,6 +144,117 @@ async def lifespan(app: FastAPI): lifespan=lifespan, ) + def _as_error_message(detail: object) -> str: + if isinstance(detail, str): + return detail + return str(detail) + + def _annotation_fallback(annotation: Any) -> Any: + origin = get_origin(annotation) + args = get_args(annotation) + + if origin is not None and type(None) in args: + return None + + if origin in (list, set, tuple): + return [] + if origin is dict: + return {} + + if annotation is str: + return "" + if annotation is int: + return 0 + if annotation is float: + return 0.0 + if annotation is bool: + return False + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + return {} + return None + + def _resolve_response_model(request: Request) -> type[OutBase]: + route = request.scope.get("route") + if isinstance(route, APIRoute): + model = route.response_model + if isinstance(model, type) and issubclass(model, OutBase): + return model + return OutBase + + def _build_error_payload( + request: Request, + *, + code: int, + message: str, + ) -> dict[str, Any]: + model_cls = _resolve_response_model(request) + if model_cls is OutBase: + return OutBase(code=code, status="error", message=message).model_dump() + + payload: dict[str, Any] = { + "code": code, + "status": "error", + "message": message, + } + + for field_name, field_info in model_cls.model_fields.items(): + if field_name in payload: + continue + + if field_info.default is not PydanticUndefined: + payload[field_name] = field_info.default + continue + + if field_info.default_factory is not None: + payload[field_name] = field_info.get_default( + call_default_factory=True, + validated_data=payload, + ) + continue + + payload[field_name] = _annotation_fallback(field_info.annotation) + + try: + return model_cls.model_validate(payload).model_dump() + except Exception: + return model_cls.model_construct(**payload).model_dump() + + async def handle_http_exception(_: Request, exc: HTTPException) -> JSONResponse: + payload = _build_error_payload( + _, + code=exc.status_code, + message=_as_error_message(exc.detail), + ) + return JSONResponse(status_code=200, content=payload) + + async def handle_validation_exception( + _: Request, exc: RequestValidationError + ) -> JSONResponse: + payload = _build_error_payload( + _, + code=422, + message=f"RequestValidationError: {str(exc)}", + ) + return JSONResponse(status_code=200, content=payload) + + async def handle_unexpected_exception( + _: Request, exc: Exception + ) -> JSONResponse: + logger.exception("未处理异常", exc_info=exc) + payload = _build_error_payload( + _, + code=500, + message=f"{type(exc).__name__}: {str(exc)}", + ) + return JSONResponse(status_code=200, content=payload) + + app.add_exception_handler(HTTPException, cast(Any, handle_http_exception)) + app.add_exception_handler( + RequestValidationError, + cast(Any, handle_validation_exception), + ) + app.add_exception_handler(Exception, cast(Any, handle_unexpected_exception)) + app.add_middleware( CORSMiddleware, allow_origins=["*"], # 允许所有域名跨域访问 From 4959d7d430e524533f3659199aa6932ec045b839 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 6 Apr 2026 23:06:47 +0800 Subject: [PATCH 18/29] =?UTF-8?q?feat(config):=20=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E5=A4=9A=E7=A7=8D=E8=AF=AD=E6=B3=95=E7=B3=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config/__init__.py | 23 +++ app/core/config/manager.py | 34 ++-- app/core/config/pydantic.py | 375 +++++++++++++++++++++++++++++++---- app/core/config/shortcuts.py | 249 +++++++++++++++++++++++ app/core/config/types.py | 18 +- app/models/common.py | 19 +- app/models/general.py | 44 ++-- app/models/global_config.py | 15 +- app/models/maa.py | 79 +++----- app/models/maaend.py | 49 ++--- app/models/src.py | 47 ++--- 11 files changed, 726 insertions(+), 226 deletions(-) create mode 100644 app/core/config/shortcuts.py diff --git a/app/core/config/__init__.py b/app/core/config/__init__.py index 7adcd81f..d5f3aa6f 100644 --- a/app/core/config/__init__.py +++ b/app/core/config/__init__.py @@ -9,12 +9,24 @@ ) from .fields import RefField, VirtualField from .pydantic import PydanticConfigBase +from .shortcuts import ( + config, + encrypted, + ref, + relates_to, + singleton, + sub_configs, + virtual, +) from .types import ( + DayCount, EncryptedString, HHMMString, JsonDictString, JsonListString, KeyboardKeyString, + NonNegativeInt, + PositiveInt, UrlString, YmdHmString, YmdHmsString, @@ -33,6 +45,7 @@ def __getattr__(name: str): return {"AppConfig": AppConfig, "Config": Config}[name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + __all__ = [ "MultipleConfig", "MultipleConfigAddEvent", @@ -42,6 +55,13 @@ def __getattr__(name: str): "RefField", "VirtualField", "PydanticConfigBase", + "ref", + "virtual", + "encrypted", + "singleton", + "sub_configs", + "relates_to", + "config", "JsonDictString", "JsonListString", "HHMMString", @@ -51,6 +71,9 @@ def __getattr__(name: str): "UrlString", "KeyboardKeyString", "EncryptedString", + "NonNegativeInt", + "PositiveInt", + "DayCount", "decrypt_encrypted_string", "AppConfig", "Config", diff --git a/app/core/config/manager.py b/app/core/config/manager.py index 38d9a4d7..1e46f8dc 100644 --- a/app/core/config/manager.py +++ b/app/core/config/manager.py @@ -556,7 +556,7 @@ async def add_script( self, script: Literal["MAA", "SRC", "General", "MaaEnd"], script_id: str | None = None, - ) -> tuple[uuid.UUID, MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig]: + ) -> tuple[uuid.UUID, Any]: """添加脚本配置""" logger.info(f"添加脚本配置: {script}, 从 {script_id} 复制") @@ -693,7 +693,7 @@ async def export_script_to_file(self, script_id: str, jsonFile: str): raise TypeError(f"脚本 {script_id} 不是通用脚本配置") temp = await self.ScriptConfig[uid].toDict(if_decrypt=False) - temp.pop("SubConfigsInfo", None) + temp.pop("sub_configs_info", None) temp = await self.remove_privacy_info(temp, Path(file_path).stem) file_path.write_text( @@ -761,7 +761,7 @@ async def upload_script_to_web( raise TypeError(f"脚本 {script_id} 不是通用脚本配置") temp = await self.ScriptConfig[uid].toDict(if_decrypt=False) - temp.pop("SubConfigsInfo", None) + temp.pop("sub_configs_info", None) temp = await self.remove_privacy_info(temp, config_name) files = { @@ -799,24 +799,24 @@ async def remove_privacy_info( ) -> dict[str, Any]: """移除配置中可能存在的隐私信息""" - confg["Info"]["Name"] = name - for path in ["ScriptPath", "ConfigPath", "LogPath", "TrackProcessExe"]: - if Path(confg["Script"][path]).is_relative_to( - Path(confg["Info"]["RootPath"]) + confg["info"]["name"] = name + for path in ["script_path", "config_path", "log_path", "track_process_exe"]: + if Path(confg["script"][path]).is_relative_to( + Path(confg["info"]["root_path"]) ): - confg["Script"][path] = str( + confg["script"][path] = str( Path(r"C:/脚本根目录") - / Path(confg["Script"][path]).relative_to( - Path(confg["Info"]["RootPath"]) + / Path(confg["script"][path]).relative_to( + Path(confg["info"]["root_path"]) ) ) - if sys.platform == "win32" and Path(confg["Script"][path]).is_relative_to( + if sys.platform == "win32" and Path(confg["script"][path]).is_relative_to( Path(os.environ["APPDATA"]) ): - confg["Script"][path] = ( - f"%APPDATA%/{Path(confg['Script'][path]).relative_to(Path(os.environ['APPDATA']))}" + confg["script"][path] = ( + f"%APPDATA%/{Path(confg['script'][path]).relative_to(Path(os.environ['APPDATA']))}" ) - confg["Info"]["RootPath"] = str(Path(r"C:/脚本根目录")) + confg["info"]["root_path"] = str(Path(r"C:/脚本根目录")) return confg @@ -839,11 +839,7 @@ async def get_user( index = data.pop("instances", []) return list(index), data - async def add_user( - self, script_id: str - ) -> tuple[ - uuid.UUID, MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig - ]: + async def add_user(self, script_id: str) -> tuple[uuid.UUID, Any]: """添加用户配置""" logger.info(f"{script_id} 添加用户配置") diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index e2f52371..82c81f73 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -1,7 +1,10 @@ from __future__ import annotations import asyncio +import ast import inspect +import re +import textwrap import uuid from contextlib import asynccontextmanager from pathlib import Path @@ -43,6 +46,12 @@ def _default_registered_ref_targets() -> set[str]: return set() +def _default_virtual_dependencies_cache() -> ( + dict[tuple[str, str], tuple[tuple[str, str], ...]] +): + return {} + + def _normalize_mapping(value: Any) -> dict[str, Any]: """将任意映射值规范化为 `dict[str, Any]`。""" @@ -52,6 +61,26 @@ def _normalize_mapping(value: Any) -> dict[str, Any]: return {str(key): item for key, item in mapping.items()} +_SNAKE_1 = re.compile(r"(.)([A-Z][a-z]+)") +_SNAKE_2 = re.compile(r"([a-z0-9])([A-Z])") + + +def _to_snake_case(name: str) -> str: + """将 ``PascalCase``/``camelCase`` 名称转换为 ``snake_case``。 + + 该函数用于统一配置协议键名输出,并在输入解析时做名称归一化匹配。 + + Args: + name: 原始名称。 + + Returns: + snake_case 格式名称。 + """ + + first_pass = _SNAKE_1.sub(r"\1_\2", name) + return _SNAKE_2.sub(r"\1_\2", first_pass).lower() + + def _resolve_alias_paths(validation_alias: Any) -> list[tuple[str, ...]]: if validation_alias is None: return [] @@ -158,7 +187,7 @@ def _export_group_model( for field_name in type(group_model).model_fields: virtual_field = _get_field_marker(group_model, field_name, VirtualField) if virtual_field is not None: - data[field_name] = owner.get_virtual_value( + data[_to_snake_case(field_name)] = owner.get_virtual_value( group_name, field_name, virtual_field ) continue @@ -166,14 +195,16 @@ def _export_group_model( value = getattr(group_model, field_name) if isinstance(value, BaseModel): - data[field_name] = _export_group_model(owner, field_name, value, if_decrypt) + data[_to_snake_case(field_name)] = _export_group_model( + owner, field_name, value, if_decrypt + ) continue if if_decrypt and _is_encrypted_field(group_model, field_name): - data[field_name] = decrypt_encrypted_string(str(value)) + data[_to_snake_case(field_name)] = decrypt_encrypted_string(str(value)) continue - data[field_name] = value + data[_to_snake_case(field_name)] = value return data @@ -203,6 +234,11 @@ class PydanticConfigBase(BaseModel): _owner_collection: MultipleConfig[Any] | None = PrivateAttr(default=None) _owner_uid: uuid.UUID | None = PrivateAttr(default=None) _mutex: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock) + _virtual_dependencies_cache: dict[tuple[str, str], tuple[tuple[str, str], ...]] = ( + PrivateAttr( # type: ignore[assignment] + default_factory=_default_virtual_dependencies_cache + ) + ) @property def file(self) -> Path | None: @@ -213,8 +249,191 @@ def is_locked(self) -> bool: return self._is_locked def model_post_init(self, __context: Any) -> None: + self._build_virtual_dependency_cache() self._register_ref_bindings() + def _resolve_group_name(self, group: str) -> str: + """将调用侧传入的分组名解析为模型中的真实分组名。 + + 支持两类匹配: + - 精确匹配(如 ``Info``) + - snake_case 等价匹配(如 ``info``) + + Args: + group: 外部输入的分组名。 + + Returns: + 配置模型中的真实分组名。 + + Raises: + AttributeError: 分组不存在时抛出。 + """ + + groups = self._group_index() + if group in groups: + return group + + snake = _to_snake_case(group) + for candidate in groups: + if _to_snake_case(candidate) == snake: + return candidate + + raise AttributeError(f"配置分组 '{group}' 不存在") + + def _resolve_field_name(self, group: str, name: str) -> str: + """将字段名解析为指定分组中的真实字段名。 + + 先解析分组,再在该分组下执行字段名精确匹配与 snake_case 等价匹配。 + + Args: + group: 分组名(可为原名或 snake_case)。 + name: 字段名(可为原名或 snake_case)。 + + Returns: + 配置模型中的真实字段名。 + + Raises: + AttributeError: 分组或字段不存在时抛出。 + """ + + resolved_group = self._resolve_group_name(group) + group_model = self._group_index().get(resolved_group) + if group_model is None: + raise AttributeError(f"配置分组 '{group}' 不存在") + + fields = type(group_model).model_fields + if name in fields: + return name + + snake = _to_snake_case(name) + for candidate in fields: + if _to_snake_case(candidate) == snake: + return candidate + + raise AttributeError(f"配置项 '{group}.{name}' 不存在") + + def _normalize_dependency(self, dependency: tuple[str, str]) -> tuple[str, str]: + """将虚拟字段依赖项规范化为真实 ``(group, field)`` 对。 + + 主要用于把手工声明依赖或自动推导依赖统一为内部可比较的标准形式。 + + Args: + dependency: 原始依赖项。 + + Returns: + 规范化后的依赖项。 + """ + + group, name = dependency + resolved_group = self._resolve_group_name(group) + resolved_name = self._resolve_field_name(resolved_group, name) + return resolved_group, resolved_name + + def _infer_virtual_dependencies(self, getter: str) -> tuple[tuple[str, str], ...]: + """从虚拟字段 getter 源码中自动推导依赖字段。 + + 推导策略: + - 解析方法 AST; + - 扫描 ``self.get("Group", "Field")`` 形式调用; + - 对提取结果执行名称归一化与去重。 + + 注意:若源码不可获取、语法不可解析或调用参数非常量字符串, + 对应依赖将被安全忽略。 + + Args: + getter: getter 方法名。 + + Returns: + 推导出的依赖项元组。 + """ + + method = getattr(type(self), getter, None) + if method is None: + return () + + try: + source = inspect.getsource(method) + except (OSError, TypeError): + return () + + try: + tree = ast.parse(textwrap.dedent(source)) + except SyntaxError: + return () + + dependencies: list[tuple[str, str]] = [] + seen: set[tuple[str, str]] = set() + + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if not isinstance(node.func, ast.Attribute): + continue + if node.func.attr != "get": + continue + if ( + not isinstance(node.func.value, ast.Name) + or node.func.value.id != "self" + ): + continue + if len(node.args) < 2: + continue + + arg_group = node.args[0] + arg_name = node.args[1] + if not ( + isinstance(arg_group, ast.Constant) + and isinstance(arg_group.value, str) + and isinstance(arg_name, ast.Constant) + and isinstance(arg_name.value, str) + ): + continue + + try: + dep = self._normalize_dependency((arg_group.value, arg_name.value)) + except AttributeError: + continue + + if dep in seen: + continue + seen.add(dep) + dependencies.append(dep) + + return tuple(dependencies) + + def _build_virtual_dependency_cache(self) -> None: + """构建虚拟字段依赖缓存。 + + 缓存键为 ``(group, field)``,值为该虚拟字段依赖列表。 + 规则: + - 优先使用显式 ``depends_on``; + - 否则对字符串 getter 执行自动推导; + - 其他情况依赖为空。 + + 缓存在 ``model_post_init`` 阶段构建,用于 ``set`` 触发时高效判断 + 哪些虚拟字段需要重新计算并派发绑定事件。 + """ + + self._virtual_dependencies_cache = {} + + for group_name, group_model in self._group_index().items(): + for field_name in type(group_model).model_fields: + virtual_field = _get_field_marker(group_model, field_name, VirtualField) + if virtual_field is None: + continue + + if virtual_field.depends_on: + deps = tuple( + self._normalize_dependency(dep) + for dep in virtual_field.depends_on + ) + elif isinstance(virtual_field.getter, str): + deps = self._infer_virtual_dependencies(virtual_field.getter) + else: + deps = () + + self._virtual_dependencies_cache[(group_name, field_name)] = deps + def _multiple_config_index(self) -> dict[str, MultipleConfig[Any]]: result: dict[str, MultipleConfig[Any]] = {} for name, value in self.__dict__.items(): @@ -287,7 +506,10 @@ def _iter_virtual_dependents(self, dependency: tuple[str, str]): virtual_field = _get_field_marker(group_model, field_name, VirtualField) if virtual_field is None: continue - if dependency in virtual_field.depends_on: + deps = self._virtual_dependencies_cache.get( + (group_name, field_name), () + ) + if dependency in deps: yield group_name, field_name, virtual_field def _get_virtual_value( @@ -505,21 +727,41 @@ async def add_save_method(self, save_method: SaveMethod) -> None: await sub_config.add_save_method(save_method) async def load(self, data: dict[str, Any]) -> None: + """从外部字典加载配置,支持新旧命名协议混读。 + + 支持内容: + - 分组与字段的 PascalCase / snake_case 双格式读取; + - ``sub_configs_info`` 与 ``SubConfigsInfo`` 兼容; + - 字段 ``validation_alias`` 回退解析; + - ``LEGACY_FIELD_MAP`` 旧字段映射回填。 + + 加载完成后会触发持久化与级联保存方法,保证内存态与磁盘态一致。 + + Args: + data: 待加载的原始配置字典。 + """ + async with self._mutex: if self._is_locked: raise ValueError("配置已锁定, 无法修改") raw: dict[str, Any] = dict(data) - sub_configs = _normalize_mapping(raw.pop("SubConfigsInfo", {})) + sub_configs = _normalize_mapping( + raw.pop("sub_configs_info", raw.pop("SubConfigsInfo", {})) + ) for name, sub_config in self._multiple_config_index().items(): - data_for_sub = sub_configs.get(name) + data_for_sub = sub_configs.get( + _to_snake_case(name), sub_configs.get(name) + ) if isinstance(data_for_sub, dict): await sub_config.load(_normalize_mapping(data_for_sub)) for group_name, group_model in self._group_index().items(): - group_data = _normalize_mapping(raw.get(group_name, {})) + group_data = _normalize_mapping( + raw.get(_to_snake_case(group_name), raw.get(group_name, {})) + ) for field_name in list(type(group_model).model_fields.keys()): if self._get_virtual_field(group_name, field_name) is not None: @@ -531,6 +773,9 @@ async def load(self, data: dict[str, Any]) -> None: if field_name in group_data: candidate = group_data[field_name] has_value = True + elif _to_snake_case(field_name) in group_data: + candidate = group_data[_to_snake_case(field_name)] + has_value = True else: field_info = type(group_model).model_fields.get(field_name) if field_info is not None: @@ -570,69 +815,123 @@ async def load(self, data: dict[str, Any]) -> None: async def toDict( self, if_decrypt: bool = True, regenerate_uuids: bool = False ) -> dict[str, Any]: + """将当前配置导出为 snake_case 协议字典。 + + 导出规则: + - 所有分组键与字段键均转换为 snake_case; + - 虚拟字段导出为实时计算值; + - 子配置统一放入 ``sub_configs_info``; + - 是否解密由 ``if_decrypt`` 控制。 + + Args: + if_decrypt: 是否在导出时自动解密加密字段。 + regenerate_uuids: 透传给子配置容器的 UUID 重生参数。 + + Returns: + 可序列化的配置字典。 + """ + data: dict[str, Any] = {} for group_name, group_model in self._group_index().items(): - data[group_name] = _export_group_model( + data[_to_snake_case(group_name)] = _export_group_model( self, group_name, group_model, if_decrypt ) for name, item in self._multiple_config_index().items(): - if "SubConfigsInfo" not in data: - data["SubConfigsInfo"] = {} - data["SubConfigsInfo"][name] = await item.toDict( + if "sub_configs_info" not in data: + data["sub_configs_info"] = {} + data["sub_configs_info"][_to_snake_case(name)] = await item.toDict( if_decrypt, regenerate_uuids ) return data def get(self, group: str, name: str) -> Any: - group_model = self._group_index().get(group) - if group_model is None or not hasattr(group_model, name): + """读取单个配置项,支持 snake_case 与原字段名。 + + 读取流程: + 1. 解析分组和字段真实名称; + 2. 若为虚拟字段,返回 getter 计算结果; + 3. 若为加密字段,返回自动解密值; + 4. 否则返回原值。 + + Args: + group: 分组名。 + name: 字段名。 + + Returns: + 配置值。 + """ + + resolved_group = self._resolve_group_name(group) + resolved_name = self._resolve_field_name(resolved_group, name) + + group_model = self._group_index().get(resolved_group) + if group_model is None or not hasattr(group_model, resolved_name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") - virtual_field = self._get_virtual_field(group, name) + virtual_field = self._get_virtual_field(resolved_group, resolved_name) if virtual_field is not None: - return self._get_virtual_value(group, name, virtual_field) + return self._get_virtual_value(resolved_group, resolved_name, virtual_field) - value = getattr(group_model, name) - if _is_encrypted_field(group_model, name): + value = getattr(group_model, resolved_name) + if _is_encrypted_field(group_model, resolved_name): return decrypt_encrypted_string(str(value)) return value async def set(self, group: str, name: str, value: Any) -> None: + """设置单个配置项,并联动依赖虚拟字段与绑定回调。 + + 关键行为: + - 支持 snake_case 名称解析; + - 对引用字段执行归一化(UUID/默认值校验); + - 计算受影响虚拟字段的前后值差异,按需触发绑定事件; + - 根据事务状态决定立即保存或延迟提交。 + + Args: + group: 分组名。 + name: 字段名。 + value: 新值。 + """ + async with self._mutex: - group_model = self._group_index().get(group) - if group_model is None or not hasattr(group_model, name): + resolved_group = self._resolve_group_name(group) + resolved_name = self._resolve_field_name(resolved_group, name) + + group_model = self._group_index().get(resolved_group) + if group_model is None or not hasattr(group_model, resolved_name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") if self._is_locked: raise ValueError("配置已锁定, 无法修改") - virtual_field = self._get_virtual_field(group, name) + virtual_field = self._get_virtual_field(resolved_group, resolved_name) if virtual_field is not None: - await self._set_virtual_value(group, name, value) + await self._set_virtual_value(resolved_group, resolved_name, value) return - old_value = getattr(group_model, name) + old_value = getattr(group_model, resolved_name) virtual_old_values = { (virtual_group, virtual_name): self._get_virtual_value( virtual_group, virtual_name, virtual_field ) for virtual_group, virtual_name, virtual_field in self._iter_virtual_dependents( - (group, name) + (resolved_group, resolved_name) ) } - value = self._normalize_value(group, name, value) + value = self._normalize_value(resolved_group, resolved_name, value) try: - setattr(group_model, name, value) + setattr(group_model, resolved_name, value) except (TypeError, ValueError) as e: - raise ValueError(f"设置配置项失败: {group}.{name}={value!r}") from e + raise ValueError( + f"设置配置项失败: {resolved_group}.{resolved_name}={value!r}" + ) from e - new_value = getattr(group_model, name) + new_value = getattr(group_model, resolved_name) if old_value != new_value: - await self._queue_binding(group, name, new_value) + await self._queue_binding(resolved_group, resolved_name, new_value) for ( virtual_group, @@ -661,28 +960,34 @@ async def set_many(self, values: dict[str, dict[str, Any]]) -> None: await self.set(group, name, value) def bind(self, group: str, name: str, slot: Slot) -> None: - group_model = self._group_index().get(group) - if group_model is None or not hasattr(group_model, name): + resolved_group = self._resolve_group_name(group) + resolved_name = self._resolve_field_name(resolved_group, name) + + group_model = self._group_index().get(resolved_group) + if group_model is None or not hasattr(group_model, resolved_name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") if self._is_locked: raise ValueError("配置已锁定, 无法修改") - key = (group, name) + key = (resolved_group, resolved_name) if key not in self._bindings: self._bindings[key] = [] if slot not in self._bindings[key]: self._bindings[key].append(slot) def unbind(self, group: str, name: str, slot: Slot) -> None: - group_model = self._group_index().get(group) - if group_model is None or not hasattr(group_model, name): + resolved_group = self._resolve_group_name(group) + resolved_name = self._resolve_field_name(resolved_group, name) + + group_model = self._group_index().get(resolved_group) + if group_model is None or not hasattr(group_model, resolved_name): raise AttributeError(f"配置项 '{group}.{name}' 不存在") if self._is_locked: raise ValueError("配置已锁定, 无法修改") - key = (group, name) + key = (resolved_group, resolved_name) if key in self._bindings and slot in self._bindings[key]: self._bindings[key].remove(slot) diff --git a/app/core/config/shortcuts.py b/app/core/config/shortcuts.py new file mode 100644 index 00000000..fb16974c --- /dev/null +++ b/app/core/config/shortcuts.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, cast + +from .base import MultipleConfig +from .fields import ( + OnDeleteCallback, + RefDeleteAction, + RefField, + VirtualDependency, + VirtualField, + VirtualFieldGetter, + VirtualFieldSetter, +) +from .types import EncryptedFieldMarker + + +@dataclass(frozen=True, slots=True) +class SubConfigSpec: + """子配置挂载描述。""" + + types: tuple[type[Any], ...] + singleton: bool = False + + +def _normalize_sub_config_spec(value: Any) -> SubConfigSpec: + """将多种子配置声明形式统一规整为 ``SubConfigSpec``。 + + 支持以下输入: + - 已构造的 ``SubConfigSpec`` + - ``list[type]`` / ``tuple[type, ...]``(表示多实例子配置集合) + - 单个 ``type``(会转为仅含一个类型的多实例声明) + + Args: + value: 装饰器参数中声明的子配置描述。 + + Returns: + 规范化后的 ``SubConfigSpec``。 + + Raises: + TypeError: 传入类型不受支持时抛出。 + """ + + if isinstance(value, SubConfigSpec): + return value + + if isinstance(value, list): + return SubConfigSpec(types=tuple(cast(list[type[Any]], value)), singleton=False) + + if isinstance(value, tuple): + return SubConfigSpec(types=cast(tuple[type[Any], ...], value), singleton=False) + + if isinstance(value, type): + return SubConfigSpec(types=(value,), singleton=False) + + raise TypeError(f"不支持的子配置声明类型: {type(value)!r}") + + +def ref( + target: str, + *, + default: Any, + allow_values: tuple[Any, ...] = (), + on_delete: RefDeleteAction = "set_default", + on_delete_callback: OnDeleteCallback | str | None = None, +) -> RefField: + """创建引用字段元数据(RefField 语法糖)。""" + + return RefField( + target=target, + default=default, + allow_values=allow_values, + on_delete=on_delete, + on_delete_callback=on_delete_callback, + ) + + +def virtual( + getter: VirtualFieldGetter | str, + *, + setter: VirtualFieldSetter | str | None = None, + depends_on: tuple[VirtualDependency, ...] = (), +) -> VirtualField: + """创建虚拟字段元数据(VirtualField 语法糖)。""" + + return VirtualField(getter=getter, setter=setter, depends_on=depends_on) + + +def encrypted() -> EncryptedFieldMarker: + """创建加密字段标记(EncryptedFieldMarker 语法糖)。""" + + return EncryptedFieldMarker() + + +def singleton(config_type: type[Any]) -> SubConfigSpec: + """声明单例子配置。""" + + return SubConfigSpec(types=(config_type,), singleton=True) + + +def sub_configs(**configs: Any): + """声明配置类上的子配置挂载信息。 + + 该装饰器仅负责“记录元数据”,实际实例化发生在 ``@config`` 注入的 + ``model_post_init`` 阶段。 + + Example: + ``@sub_configs(UserData=[UserConfig], Tools=singleton(ToolsConfig))`` + + Args: + **configs: 属性名到子配置声明的映射。 + + Returns: + 类装饰器,返回原类并附加 ``__config_sub_configs__`` 元数据。 + """ + + normalized = { + name: _normalize_sub_config_spec(spec) for name, spec in configs.items() + } + + def _decorator(cls: type[Any]) -> type[Any]: + inherited = dict(getattr(cls, "__config_sub_configs__", {})) + inherited.update(normalized) + setattr(cls, "__config_sub_configs__", inherited) + return cls + + return _decorator + + +def relates_to(**targets: str): + """声明配置类的引用目标映射。 + + 该映射用于在实例初始化时把 ``related_config`` 中的逻辑别名指向 + 实例属性上的 ``MultipleConfig`` 容器。 + + Example: + ``@relates_to(PlanConfig="PlanConfig", ScriptConfig="ScriptConfig")`` + + Args: + **targets: 逻辑别名 -> 实例属性名。 + + Returns: + 类装饰器,返回原类并附加 ``__config_relates_to__`` 元数据。 + """ + + def _decorator(cls: type[Any]) -> type[Any]: + inherited = dict(getattr(cls, "__config_relates_to__", {})) + inherited.update(targets) + setattr(cls, "__config_relates_to__", inherited) + return cls + + return _decorator + + +def _init_sub_configs(instance: Any) -> None: + """根据 ``__config_sub_configs__`` 元数据初始化子配置属性。 + + 行为规则: + - 若实例已存在同名属性,则跳过(避免覆盖手工注入对象); + - ``singleton=True`` 时直接构造单个子配置实例; + - 否则构造 ``MultipleConfig`` 容器。 + + Args: + instance: 当前配置对象实例。 + """ + + instance_type = cast(type[Any], type(instance)) + specs: dict[str, SubConfigSpec] = dict( + cast( + dict[str, SubConfigSpec], + getattr(instance_type, "__config_sub_configs__", {}), + ) + ) + + for name, spec in specs.items(): + if hasattr(instance, name): + continue + + if spec.singleton: + setattr(instance, name, spec.types[0]()) + else: + setattr(instance, name, MultipleConfig(list(spec.types))) + + +def _init_related_targets(instance: Any) -> None: + """将关系映射写入类级 ``related_config``。 + + 仅当目标属性是 ``MultipleConfig`` 时才会写入,避免错误类型污染 + 引用解析表。 + + Args: + instance: 当前配置对象实例。 + """ + + instance_type = cast(type[Any], type(instance)) + mapping: dict[str, str] = dict( + cast(dict[str, str], getattr(instance_type, "__config_relates_to__", {})) + ) + related = getattr(instance_type, "related_config", None) + if not isinstance(related, dict): + return + + for alias, attr_name in mapping.items(): + target = getattr(instance, attr_name, None) + if isinstance(target, MultipleConfig): + related[alias] = target + + +def config(cls: type[Any]) -> type[Any]: + """配置类总装饰器:在 ``model_post_init`` 阶段完成运行期装配。 + + 装配顺序: + 1. 初始化子配置(``_init_sub_configs``); + 2. 建立引用目标映射(``_init_related_targets``); + 3. 调用原有 ``model_post_init``(如果存在)。 + + 这样可以确保原有后置初始化逻辑执行时,子配置和引用关系已可用。 + + Args: + cls: 被装饰的配置类。 + + Returns: + 注入初始化逻辑后的原类。 + """ + + original_model_post_init = getattr(cls, "model_post_init", None) + + def _model_post_init(self: Any, context: Any) -> None: + _init_sub_configs(self) + _init_related_targets(self) + + if callable(original_model_post_init): + original_model_post_init(self, context) + + setattr(cls, "model_post_init", _model_post_init) + return cls + + +__all__ = [ + "SubConfigSpec", + "ref", + "virtual", + "encrypted", + "singleton", + "sub_configs", + "relates_to", + "config", +] diff --git a/app/core/config/types.py b/app/core/config/types.py index 611f6ce0..430c97f0 100644 --- a/app/core/config/types.py +++ b/app/core/config/types.py @@ -7,7 +7,7 @@ import pyautogui from loguru import logger -from pydantic import AfterValidator +from pydantic import AfterValidator, Field from app.utils.constants import DEFAULT_DATETIME from app.utils.security import dpapi_decrypt, dpapi_encrypt @@ -256,12 +256,19 @@ def decrypt_encrypted_string(value: str) -> str: YmdString = Annotated[str, AfterValidator(_validate_ymd_string)] YmdHmsString = Annotated[str, AfterValidator(_validate_ymd_hms_string)] UrlString = Annotated[str, AfterValidator(_validate_url_string)] +""" URL 字符串类型 """ KeyboardKeyString = Annotated[str, AfterValidator(_validate_keyboard_key)] +""" 键盘按键字符串类型 (必须是 pyautogui 支持的按键) """ EncryptedString = Annotated[ - str, - EncryptedFieldMarker(), - AfterValidator(_normalize_encrypted_string), + str, EncryptedFieldMarker(), AfterValidator(_normalize_encrypted_string) ] +""" 加密字符串类型 """ +NonNegativeInt = Annotated[int, Field(ge=0)] +""" 非负整数类型(0及以上)""" +PositiveInt = Annotated[int, Field(ge=1)] +""" 正整数类型(1及以上)""" +DayCount = Annotated[int, Field(ge=-1, le=9999)] +""" 天数类型(-1为永久,0为今天)""" __all__ = [ @@ -274,6 +281,9 @@ def decrypt_encrypted_string(value: str) -> str: "UrlString", "KeyboardKeyString", "EncryptedString", + "NonNegativeInt", + "PositiveInt", + "DayCount", "EncryptedFieldMarker", "decrypt_encrypted_string", ] diff --git a/app/models/common.py b/app/models/common.py index c93cdf44..a09048eb 100644 --- a/app/models/common.py +++ b/app/models/common.py @@ -6,12 +6,13 @@ from pydantic import AliasChoices, AliasPath, BaseModel, Field, field_validator from app.core.config.base import MultipleConfig -from app.core.config.fields import RefField from app.core.config.pydantic import PydanticConfigBase +from app.core.config.shortcuts import config, ref, sub_configs from app.core.config.types import ( HHMMString, JsonDictString, JsonListString, + PositiveInt, UrlString, YmdHmString, ) @@ -31,6 +32,7 @@ HTTP_METHOD = Literal["POST", "GET"] +@config class EmulatorConfig(PydanticConfigBase): """模拟器配置""" @@ -45,9 +47,8 @@ class InfoModel(BaseModel): default="[ ]", validation_alias=AliasChoices("BossKey", AliasPath("Data", "BossKey")), ) - MaxWaitTime: int = Field( + MaxWaitTime: PositiveInt = Field( default=60, - ge=1, le=9999, validation_alias=AliasChoices( "MaxWaitTime", AliasPath("Data", "MaxWaitTime") @@ -57,6 +58,7 @@ class InfoModel(BaseModel): Info: InfoModel = Field(default_factory=InfoModel) +@config class Webhook(PydanticConfigBase): """Webhook 配置""" @@ -74,6 +76,7 @@ class DataModel(BaseModel): Data: DataModel = Field(default_factory=DataModel) +@config class QueueItem(PydanticConfigBase): """队列项配置""" @@ -82,7 +85,7 @@ class QueueItem(PydanticConfigBase): class InfoModel(BaseModel): ScriptId: Annotated[ str, - RefField( + ref( "ScriptConfig", default="-", allow_values=("-",), @@ -93,6 +96,7 @@ class InfoModel(BaseModel): Info: InfoModel = Field(default_factory=InfoModel) +@config class TimeSet(PydanticConfigBase): """时间设置配置""" @@ -118,6 +122,8 @@ def _validate_days(cls, value: Any) -> list[str]: Info: InfoModel = Field(default_factory=InfoModel) +@config +@sub_configs(TimeSet=[TimeSet], QueueItem=[QueueItem]) class QueueConfig(PydanticConfigBase): """队列配置""" @@ -133,10 +139,5 @@ class DataModel(BaseModel): Info: InfoModel = Field(default_factory=InfoModel) Data: DataModel = Field(default_factory=DataModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.TimeSet: MultipleConfig[TimeSet] = MultipleConfig([TimeSet]) - self.QueueItem: MultipleConfig[QueueItem] = MultipleConfig([QueueItem]) - __all__ = ["EmulatorConfig", "Webhook", "QueueItem", "TimeSet", "QueueConfig"] diff --git a/app/models/general.py b/app/models/general.py index d2094e69..8a60df9a 100644 --- a/app/models/general.py +++ b/app/models/general.py @@ -9,19 +9,21 @@ from app.utils.constants import UTC4 from app.core.config.base import MultipleConfig -from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase -from app.core.config.types import UrlString +from app.core.config.shortcuts import config, ref, sub_configs, virtual +from app.core.config.types import DayCount, NonNegativeInt, PositiveInt, UrlString from .common import Webhook +@config +@sub_configs(Notify_CustomWebhooks=[Webhook]) class GeneralUserConfig(PydanticConfigBase): """通用脚本用户配置""" class InfoModel(BaseModel): Name: str = "新用户" Status: bool = True - RemainedDay: int = Field(default=-1, ge=-1, le=9999) + RemainedDay: DayCount = -1 IfScriptBeforeTask: bool = False ScriptBeforeTask: str = str(Path.cwd()) IfScriptAfterTask: bool = False @@ -29,20 +31,12 @@ class InfoModel(BaseModel): Notes: str = "无" Tag: Annotated[ str, - VirtualField( - "getTags", - depends_on=( - ("Data", "LastProxyDate"), - ("Data", "ProxyTimes"), - ("Info", "RemainedDay"), - ("Info", "Notes"), - ), - ), + virtual("getTags"), ] = "[ ]" class DataModel(BaseModel): LastProxyDate: str = "2000-01-01" - ProxyTimes: int = Field(default=0, ge=0, le=9999) + ProxyTimes: NonNegativeInt = Field(default=0, le=9999) @field_validator("LastProxyDate", mode="before") @classmethod @@ -66,10 +60,6 @@ class NotifyModel(BaseModel): Data: DataModel = Field(default_factory=DataModel) Notify: NotifyModel = Field(default_factory=NotifyModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def getTags(self) -> str: # noqa: N802 """生成通用用户标签列表""" tags: list[dict[str, str]] = [] @@ -122,6 +112,8 @@ def getTags(self) -> str: # noqa: N802 return json.dumps(tags, ensure_ascii=False) +@config +@sub_configs(UserData=[GeneralUserConfig]) class GeneralConfig(PydanticConfigBase): """通用配置""" @@ -143,8 +135,8 @@ class ScriptModel(BaseModel): UpdateConfigMode: Literal["Never", "Success", "Failure", "Always"] = "Never" LogPath: str = str(Path.cwd()) LogPathFormat: str = "%Y-%m-%d" - LogTimeStart: int = Field(default=1, ge=1, le=9999) - LogTimeEnd: int = Field(default=1, ge=1, le=9999) + LogTimeStart: PositiveInt = Field(default=1, le=9999) + LogTimeEnd: PositiveInt = Field(default=1, le=9999) LogTimeFormat: str = "%Y-%m-%d %H:%M:%S" SuccessLog: str = "" ErrorLog: str = "" @@ -156,11 +148,11 @@ class GameModel(BaseModel): URL: UrlString = "" ProcessName: str = "" Arguments: str = "" - WaitTime: int = Field(default=0, ge=0, le=9999) + WaitTime: NonNegativeInt = Field(default=0, le=9999) IfForceClose: bool = False EmulatorId: Annotated[ str, - RefField( + ref( "EmulatorConfig", default="-", allow_values=("-",), @@ -170,18 +162,14 @@ class GameModel(BaseModel): EmulatorIndex: str = "-" class RunModel(BaseModel): - ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) - RunTimesLimit: int = Field(default=3, ge=1, le=9999) - RunTimeLimit: int = Field(default=10, ge=1, le=9999) + ProxyTimesLimit: NonNegativeInt = Field(default=0, le=9999) + RunTimesLimit: PositiveInt = Field(default=3, le=9999) + RunTimeLimit: PositiveInt = Field(default=10, le=9999) Info: InfoModel = Field(default_factory=InfoModel) Script: ScriptModel = Field(default_factory=ScriptModel) Game: GameModel = Field(default_factory=GameModel) Run: RunModel = Field(default_factory=RunModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.UserData = MultipleConfig([GeneralUserConfig]) - __all__ = ["GeneralUserConfig", "GeneralConfig"] diff --git a/app/models/global_config.py b/app/models/global_config.py index 0348c8c6..7afd54aa 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -9,8 +9,8 @@ from pydantic import AliasChoices, AliasPath, BaseModel, Field, field_validator from app.core.config.base import MultipleConfig -from app.core.config.fields import VirtualField from app.core.config.pydantic import PydanticConfigBase +from app.core.config.shortcuts import virtual from app.core.config.types import ( EncryptedString, JsonDictString, @@ -41,10 +41,7 @@ class ArknightsPCModel(BaseModel): AnotherQuitKey: KeyboardKeyString = "space" Status: Annotated[ str, - VirtualField( - "arknights_pc_status", - depends_on=(("ArknightsPC", "Enabled"),), - ), + virtual("arknights_pc_status"), ] = "-" ArknightsPC: ArknightsPCModel = Field(default_factory=ArknightsPCModel) @@ -147,7 +144,7 @@ class DataModel(BaseModel): WebConfig: JsonListString = "[ ]" Stage: Annotated[ str, - VirtualField("getStage", depends_on=(("Data", "StageData"),)), + virtual("getStage"), ] = "-" @field_validator("UID", mode="before") @@ -175,9 +172,9 @@ def __init__(self, **data: Any): [EmulatorConfig] ) self.PlanConfig: MultipleConfig[MaaPlanConfig] = MultipleConfig([MaaPlanConfig]) - self.ScriptConfig: MultipleConfig[ - MaaConfig | MaaEndConfig | SrcConfig | GeneralConfig - ] = MultipleConfig([MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig]) + self.ScriptConfig: MultipleConfig[Any] = MultipleConfig( + [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] + ) self.QueueConfig: MultipleConfig[QueueConfig] = MultipleConfig([QueueConfig]) self.ToolsConfig = ToolsConfig() diff --git a/app/models/maa.py b/app/models/maa.py index e5f57d65..d7045d09 100644 --- a/app/models/maa.py +++ b/app/models/maa.py @@ -9,9 +9,15 @@ from pydantic import AliasChoices, AliasPath, BaseModel, Field, field_validator from app.core.config.base import MultipleConfig -from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase -from app.core.config.types import EncryptedString, JsonDictString +from app.core.config.shortcuts import config, ref, sub_configs, virtual +from app.core.config.types import ( + DayCount, + EncryptedString, + JsonDictString, + NonNegativeInt, + PositiveInt, +) from app.utils.constants import MAA_STAGE_KEY, RESOURCE_STAGE_INFO, UTC4, UTC8 from .common import Webhook @@ -24,6 +30,8 @@ def getValue(self, if_decrypt: bool = True) -> Any: # noqa: N802 return self._value_getter() +@config +@sub_configs(Notify_CustomWebhooks=[Webhook]) class MaaUserConfig(PydanticConfigBase): """MAA用户配置""" @@ -36,7 +44,7 @@ class InfoModel(BaseModel): Mode: Literal["简洁", "详细"] = "简洁" StageMode: Annotated[ str, - RefField( + ref( "PlanConfig", default="Fixed", allow_values=("Fixed",), @@ -47,7 +55,7 @@ class InfoModel(BaseModel): "Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy" ] = "Official" Status: bool = True - RemainedDay: int = Field(default=-1, ge=-1, le=9999) + RemainedDay: DayCount = -1 Annihilation: Literal[ "Close", "Annihilation", @@ -58,24 +66,14 @@ class InfoModel(BaseModel): InfrastMode: Literal["Normal", "Rotation", "Custom"] = "Normal" InfrastName: Annotated[ str, - VirtualField( - "getInfrastName", - depends_on=(("Info", "InfrastMode"), ("Data", "CustomInfrast")), - ), + virtual("getInfrastName"), ] = "-" InfrastIndex: Annotated[ str, - VirtualField( - "getInfrastIndex", - depends_on=( - ("Info", "InfrastMode"), - ("Data", "CustomInfrast"), - ("Data", "InfrastIndex"), - ), - ), + virtual("getInfrastIndex"), ] = "-" Notes: str = "无" - MedicineNumb: int = Field(default=0, ge=0, le=9999) + MedicineNumb: NonNegativeInt = 0 SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = "0" Stage: str = "-" Stage_1: str = "-" @@ -86,34 +84,13 @@ class InfoModel(BaseModel): SklandToken: EncryptedString = "" Tag: Annotated[ str, - VirtualField( - "getTags", - depends_on=( - ("Data", "IfPassCheck"), - ("Data", "LastProxyDate"), - ("Data", "ProxyTimes"), - ("Info", "IfSkland"), - ("Data", "LastSklandDate"), - ("Info", "RemainedDay"), - ("Task", "IfInfrast"), - ("Info", "InfrastMode"), - ("Data", "CustomInfrast"), - ("Data", "InfrastIndex"), - ("Info", "StageMode"), - ("Info", "Stage"), - ("Info", "Stage_1"), - ("Info", "Stage_2"), - ("Info", "Stage_3"), - ("Info", "Stage_Remain"), - ("Info", "Notes"), - ), - ), + virtual("getTags"), ] = "[ ]" class DataModel(BaseModel): LastProxyDate: str = "2000-01-01" LastSklandDate: str = "2000-01-01" - ProxyTimes: int = Field(default=0, ge=0, le=9999) + ProxyTimes: NonNegativeInt = 0 IfPassCheck: bool = True CustomInfrast: JsonDictString = "{ }" InfrastIndex: str = Field( @@ -157,10 +134,6 @@ class NotifyModel(BaseModel): Task: TaskModel = Field(default_factory=TaskModel) Notify: NotifyModel = Field(default_factory=NotifyModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def getInfrastName(self) -> str: # noqa: N802 if self.get("Info", "InfrastMode") != "Custom": return "未使用自定义基建模式" @@ -328,6 +301,8 @@ def get_stage_zh(stage: str) -> str: return stage +@config +@sub_configs(UserData=[MaaUserConfig]) class MaaConfig(PydanticConfigBase): """MAA配置""" @@ -340,7 +315,7 @@ class InfoModel(BaseModel): class EmulatorModel(BaseModel): Id: Annotated[ str, - RefField( + ref( "EmulatorConfig", default="-", allow_values=("-",), @@ -353,20 +328,16 @@ class RunModel(BaseModel): TaskTransitionMethod: Literal["NoAction", "ExitGame", "ExitEmulator"] = ( "ExitEmulator" ) - ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) - RunTimesLimit: int = Field(default=3, ge=1, le=9999) - AnnihilationTimeLimit: int = Field(default=40, ge=1, le=9999) - RoutineTimeLimit: int = Field(default=10, ge=1, le=9999) + ProxyTimesLimit: NonNegativeInt = Field(default=0, le=9999) + RunTimesLimit: PositiveInt = Field(default=3, le=9999) + AnnihilationTimeLimit: PositiveInt = Field(default=40, le=9999) + RoutineTimeLimit: PositiveInt = Field(default=10, le=9999) AnnihilationAvoidWaste: bool = False Info: InfoModel = Field(default_factory=InfoModel) Emulator: EmulatorModel = Field(default_factory=EmulatorModel) Run: RunModel = Field(default_factory=RunModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.UserData = MultipleConfig([MaaUserConfig]) - class MaaPlanConfig(PydanticConfigBase): """MAA计划表配置""" @@ -376,7 +347,7 @@ class InfoModel(BaseModel): Mode: Literal["ALL", "Weekly"] = "ALL" class DayPlanModel(BaseModel): - MedicineNumb: int = Field(default=0, ge=0, le=9999) + MedicineNumb: NonNegativeInt = Field(default=0, le=9999) SeriesNumb: Literal["0", "6", "5", "4", "3", "2", "1", "-1"] = "0" Stage: str = "-" Stage_1: str = "-" diff --git a/app/models/maaend.py b/app/models/maaend.py index b263b5a9..18fdb8d2 100644 --- a/app/models/maaend.py +++ b/app/models/maaend.py @@ -8,13 +8,15 @@ from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig -from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase -from app.core.config.types import EncryptedString +from app.core.config.shortcuts import config, ref, sub_configs, virtual +from app.core.config.types import DayCount, EncryptedString, NonNegativeInt, PositiveInt from app.utils.constants import MAAEND_STAGE_BOOK, MAAEND_STAGE_WITH_AB, UTC4, UTC8 from .common import Webhook +@config +@sub_configs(Notify_CustomWebhooks=[Webhook]) class MaaEndUserConfig(PydanticConfigBase): """MaaEnd用户配置""" @@ -25,30 +27,13 @@ class InfoModel(BaseModel): Password: EncryptedString = "" Mode: Literal["简洁", "详细"] = "简洁" Resource: Literal["官服"] = "官服" - RemainedDay: int = Field(default=-1, ge=-1, le=9999) + RemainedDay: DayCount = -1 Notes: str = "无" IfSkland: bool = False SklandToken: EncryptedString = "" Tag: Annotated[ str, - VirtualField( - "getTags", - depends_on=( - ("Data", "IfPassCheck"), - ("Data", "LastProxyStatus"), - ("Data", "LastProxyDate"), - ("Data", "ProxyTimes"), - ("Info", "IfSkland"), - ("Data", "LastSklandDate"), - ("Info", "RemainedDay"), - ("Task", "ProtocolSpaceTab"), - ("Task", "OperatorProgression"), - ("Task", "WeaponProgression"), - ("Task", "CrisisDrills"), - ("Task", "RewardsSetOption"), - ("Info", "Notes"), - ), - ), + virtual("getTags"), ] = "[ ]" class TaskModel(BaseModel): @@ -70,7 +55,7 @@ class TaskModel(BaseModel): class DataModel(BaseModel): LastProxyDate: str = "2000-01-01" - ProxyTimes: int = Field(default=0, ge=0, le=9999) + ProxyTimes: NonNegativeInt = Field(default=0, le=9999) LastProxyStatus: Literal["未知", "成功", "失败"] = "未知" LastSklandDate: str = "2000-01-01" IfPassCheck: bool = True @@ -98,10 +83,6 @@ class NotifyModel(BaseModel): Data: DataModel = Field(default_factory=DataModel) Notify: NotifyModel = Field(default_factory=NotifyModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def getTags(self) -> str: # noqa: N802 """生成用户标签列表,返回JSON字符串格式的TagItem列表""" tags: list[dict[str, str]] = [] @@ -185,6 +166,8 @@ def getTags(self) -> str: # noqa: N802 return json.dumps(tags, ensure_ascii=False) +@config +@sub_configs(UserData=[MaaEndUserConfig]) class MaaEndConfig(PydanticConfigBase): """MaaEnd配置""" @@ -195,9 +178,9 @@ class InfoModel(BaseModel): Path: str = str(Path.cwd()) class RunModel(BaseModel): - RunTimeLimit: int = Field(default=10, ge=1, le=9999) - ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) - RunTimesLimit: int = Field(default=3, ge=1, le=9999) + RunTimeLimit: PositiveInt = Field(default=10, le=9999) + ProxyTimesLimit: NonNegativeInt = Field(default=0, le=9999) + RunTimesLimit: PositiveInt = Field(default=3, le=9999) class GameModel(BaseModel): ControllerType: Literal[ @@ -205,10 +188,10 @@ class GameModel(BaseModel): ] = "Win32-Window" Path: str = str(Path.cwd()) Arguments: str = "" - WaitTime: int = Field(default=0, ge=0, le=9999) + WaitTime: NonNegativeInt = Field(default=0, le=9999) EmulatorId: Annotated[ str, - RefField( + ref( "EmulatorConfig", default="-", allow_values=("-",), @@ -222,9 +205,5 @@ class GameModel(BaseModel): Run: RunModel = Field(default_factory=RunModel) Game: GameModel = Field(default_factory=GameModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.UserData = MultipleConfig([MaaEndUserConfig]) - __all__ = ["MaaEndUserConfig", "MaaEndConfig"] diff --git a/app/models/src.py b/app/models/src.py index a2ee853d..48cd4c7a 100644 --- a/app/models/src.py +++ b/app/models/src.py @@ -8,9 +8,9 @@ from pydantic import BaseModel, Field, field_validator from app.core.config.base import MultipleConfig -from app.core.config.fields import RefField, VirtualField from app.core.config.pydantic import PydanticConfigBase -from app.core.config.types import EncryptedString +from app.core.config.shortcuts import config, ref, sub_configs, virtual +from app.core.config.types import DayCount, EncryptedString, NonNegativeInt, PositiveInt from app.utils.constants import STARRAIL_STAGE_BOOK, UTC4 from .common import Webhook @@ -136,6 +136,8 @@ ) +@config +@sub_configs(Notify_CustomWebhooks=[Webhook]) class SrcUserConfig(PydanticConfigBase): """SRC用户配置""" @@ -156,26 +158,11 @@ class InfoModel(BaseModel): "OVERSEA-Europe", "OVERSEA-TWHKMO", ] = "CN-Official" - RemainedDay: int = Field(default=-1, ge=-1, le=9999) + RemainedDay: DayCount = -1 Notes: str = "无" Tag: Annotated[ str, - VirtualField( - "getTags", - depends_on=( - ("Data", "IfPassCheck"), - ("Data", "LastProxyDate"), - ("Data", "ProxyTimes"), - ("Info", "RemainedDay"), - ("Stage", "Channel"), - ("Stage", "Relic"), - ("Stage", "Materials"), - ("Stage", "Ornament"), - ("Stage", "EchoOfWar"), - ("Stage", "SimulatedUniverseWorld"), - ("Info", "Notes"), - ), - ), + virtual("getTags"), ] = "[ ]" class StageModel(BaseModel): @@ -185,7 +172,7 @@ class StageModel(BaseModel): Ornament: str = "-" ExtractReservedTrailblazePower: bool = False UseFuel: bool = False - FuelReserve: int = Field(default=5, ge=0, le=9999) + FuelReserve: NonNegativeInt = Field(default=5, le=9999) EchoOfWar: str = "-" SimulatedUniverseWorld: str = "-" @@ -221,7 +208,7 @@ def _validate_sim_world(cls, value: Any) -> str: class DataModel(BaseModel): LastProxyDate: str = "2000-01-01" - ProxyTimes: int = Field(default=0, ge=0, le=9999) + ProxyTimes: NonNegativeInt = Field(default=0, le=9999) IfPassCheck: bool = True @field_validator("LastProxyDate", mode="before") @@ -247,10 +234,6 @@ class NotifyModel(BaseModel): Data: DataModel = Field(default_factory=DataModel) Notify: NotifyModel = Field(default_factory=NotifyModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.Notify_CustomWebhooks = MultipleConfig([Webhook]) - def getTags(self) -> str: # noqa: N802 """生成用户标签列表,返回JSON字符串格式的TagItem列表""" tags: list[dict[str, str]] = [] @@ -325,6 +308,8 @@ def getTags(self) -> str: # noqa: N802 return json.dumps(tags, ensure_ascii=False) +@config +@sub_configs(UserData=[SrcUserConfig]) class SrcConfig(PydanticConfigBase): """SRC配置""" @@ -337,7 +322,7 @@ class InfoModel(BaseModel): class EmulatorModel(BaseModel): Id: Annotated[ str, - RefField( + ref( "EmulatorConfig", default="-", allow_values=("-",), @@ -348,17 +333,13 @@ class EmulatorModel(BaseModel): class RunModel(BaseModel): TaskTransitionMethod: Literal["ExitGame", "ExitEmulator"] = "ExitGame" - ProxyTimesLimit: int = Field(default=0, ge=0, le=9999) - RunTimesLimit: int = Field(default=3, ge=1, le=9999) - RunTimeLimit: int = Field(default=10, ge=1, le=9999) + ProxyTimesLimit: NonNegativeInt = Field(default=0, le=9999) + RunTimesLimit: PositiveInt = Field(default=3, le=9999) + RunTimeLimit: PositiveInt = Field(default=10, le=9999) Info: InfoModel = Field(default_factory=InfoModel) Emulator: EmulatorModel = Field(default_factory=EmulatorModel) Run: RunModel = Field(default_factory=RunModel) - def __init__(self, **data: Any): - super().__init__(**data) - self.UserData = MultipleConfig([SrcUserConfig]) - __all__ = ["SrcUserConfig", "SrcConfig"] From 5fd043c0d8891eb8f0d526f6f63a445600f59685 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Mon, 6 Apr 2026 23:22:25 +0800 Subject: [PATCH 19/29] =?UTF-8?q?refactor(config):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=BF=AD=E4=BB=A3=E5=99=A8=E7=94=9F=E6=88=90=E6=98=9F=E6=9C=9F?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=AE=9A=E4=B9=89=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/contracts/plan_contract.py | 151 +++++++++++++++------------------ 1 file changed, 67 insertions(+), 84 deletions(-) diff --git a/app/contracts/plan_contract.py b/app/contracts/plan_contract.py index 8d08f478..8346c711 100644 --- a/app/contracts/plan_contract.py +++ b/app/contracts/plan_contract.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterator from typing import Literal from pydantic import AliasChoices, Field @@ -12,6 +13,58 @@ ) +WEEKDAY_META: tuple[tuple[str, str, str], ...] = ( + ("monday", "Monday", "周一"), + ("tuesday", "Tuesday", "周二"), + ("wednesday", "Wednesday", "周三"), + ("thursday", "Thursday", "周四"), + ("friday", "Friday", "周五"), + ("saturday", "Saturday", "周六"), + ("sunday", "Sunday", "周日"), +) + + +def _iter_weekday_fields( + day_model: type[ApiModel], *, optional: bool +) -> Iterator[tuple[str, object, object]]: + """按 `WEEKDAY_META` 生成周字段定义。 + + Args: + day_model: 星期字段对应的数据模型类型(如 `MaaPlanDayRead` / `MaaPlanDayPatch`)。 + optional: 是否生成可选字段。 + - `False`:字段类型为 `day_model`,使用 `default_factory=day_model`。 + - `True`:字段类型为 `day_model | None`,使用 `default=None`。 + + Yields: + 三元组 `(field_name, field_type, field_info)`,用于在类体内批量写入 + `__annotations__` 和字段默认值。 + """ + for day_key, day_alias, day_desc in WEEKDAY_META: + if optional: + yield ( + day_key, + day_model | None, + Field( + default=None, + validation_alias=AliasChoices(day_key, day_alias), + serialization_alias=day_alias, + description=day_desc, + ), + ) + continue + + yield ( + day_key, + day_model, + Field( + default_factory=day_model, + validation_alias=AliasChoices(day_key, day_alias), + serialization_alias=day_alias, + description=day_desc, + ), + ) + + class PlanIndexItem(ApiModel): uid: str = Field(..., description="唯一标识符") type: Literal["MaaPlanConfig"] = Field(..., description="配置类型") @@ -150,48 +203,13 @@ class MaaPlanRead(ApiModel): serialization_alias="ALL", description="全局", ) - monday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("monday", "Monday"), - serialization_alias="Monday", - description="周一", - ) - tuesday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("tuesday", "Tuesday"), - serialization_alias="Tuesday", - description="周二", - ) - wednesday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("wednesday", "Wednesday"), - serialization_alias="Wednesday", - description="周三", - ) - thursday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("thursday", "Thursday"), - serialization_alias="Thursday", - description="周四", - ) - friday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("friday", "Friday"), - serialization_alias="Friday", - description="周五", - ) - saturday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("saturday", "Saturday"), - serialization_alias="Saturday", - description="周六", - ) - sunday: MaaPlanDayRead = Field( - default_factory=MaaPlanDayRead, - validation_alias=AliasChoices("sunday", "Sunday"), - serialization_alias="Sunday", - description="周日", - ) + + # 自动展开的星期项 + for _day_key, _day_type, _day_field in _iter_weekday_fields( + MaaPlanDayRead, optional=False + ): + __annotations__[_day_key] = _day_type + locals()[_day_key] = _day_field class MaaPlanPatch(ApiModel): @@ -207,48 +225,13 @@ class MaaPlanPatch(ApiModel): serialization_alias="ALL", description="全局", ) - monday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("monday", "Monday"), - serialization_alias="Monday", - description="周一", - ) - tuesday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("tuesday", "Tuesday"), - serialization_alias="Tuesday", - description="周二", - ) - wednesday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("wednesday", "Wednesday"), - serialization_alias="Wednesday", - description="周三", - ) - thursday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("thursday", "Thursday"), - serialization_alias="Thursday", - description="周四", - ) - friday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("friday", "Friday"), - serialization_alias="Friday", - description="周五", - ) - saturday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("saturday", "Saturday"), - serialization_alias="Saturday", - description="周六", - ) - sunday: MaaPlanDayPatch | None = Field( - default=None, - validation_alias=AliasChoices("sunday", "Sunday"), - serialization_alias="Sunday", - description="周日", - ) + + # 自动展开的星期项 + for _day_key, _day_type, _day_field in _iter_weekday_fields( + MaaPlanDayPatch, optional=True + ): + __annotations__[_day_key] = _day_type + locals()[_day_key] = _day_field class PlanCreateIn(ApiModel): From 39e38fb386bafe9a04ccc9890331850027640740 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Tue, 7 Apr 2026 00:15:17 +0800 Subject: [PATCH 20/29] =?UTF-8?q?refactor(config):=20=E5=A2=9E=E5=8A=A0ski?= =?UTF-8?q?p=5Fvirtual=E5=8F=82=E6=95=B0=E4=BB=A5=E4=BC=98=E5=8C=96toDict?= =?UTF-8?q?=E6=96=B9=E6=B3=95=EF=BC=8C=E6=94=B9=E8=BF=9B=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=92=8C=E5=BB=BA=E8=AE=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config/base.py | 33 +++++++--- app/core/config/pydantic.py | 121 ++++++++++++++++++++++++++++++++---- 2 files changed, 133 insertions(+), 21 deletions(-) diff --git a/app/core/config/base.py b/app/core/config/base.py index 2040f58f..a9f23fae 100644 --- a/app/core/config/base.py +++ b/app/core/config/base.py @@ -33,8 +33,8 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from pathlib import Path -from collections.abc import AsyncIterator, Callable, Coroutine -from typing import Any, Generic, Protocol, TypeVar, cast +from collections.abc import AsyncIterator, Callable, Coroutine, Iterator +from typing import Any, Generic, Protocol, TypeVar, cast, overload from importlib import import_module from loguru import logger @@ -260,7 +260,10 @@ async def add_save_method( async def load(self, data: dict[str, Any]) -> None: ... async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False + self, + if_decrypt: bool = True, + regenerate_uuids: bool = False, + skip_virtual: bool = False, ) -> dict[str, Any]: ... async def lock(self) -> None: ... @@ -539,7 +542,10 @@ async def load(self, data: dict[str, Any]) -> None: await asyncio.gather(*(_() for _ in self._save_methods)) async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False + self, + if_decrypt: bool = True, + regenerate_uuids: bool = False, + skip_virtual: bool = False, ) -> dict[str, Any]: """将全部子配置序列化为统一字典结构。""" @@ -556,14 +562,23 @@ async def toDict( for uid, config in self.items(): data[str(uuid_book[uid])] = await config.toDict( - if_decrypt, regenerate_uuids + if_decrypt, regenerate_uuids, skip_virtual ) return data - async def get(self, uid: uuid.UUID) -> dict[str, Any]: + @overload + async def get(self, uid: uuid.UUID) -> dict[str, Any]: ... + + @overload + async def get(self, uid: None = None) -> dict[str, Any]: ... + + async def get(self, uid: uuid.UUID | None = None) -> dict[str, Any]: """获取指定 UID 的单个配置。""" + if uid is None: + return await self.toDict() + if uid not in self.data: raise ValueError(f"配置项 '{uid}' 不存在。") @@ -724,12 +739,12 @@ async def unlock(self) -> None: for item in self.values(): await item.unlock() - def keys(self): + def keys(self) -> Iterator[uuid.UUID]: """返回全部 UID。""" return iter(tuple(self.order)) - def values(self): + def values(self) -> Iterator[T]: """按顺序返回全部子配置实例。""" if not self.data: @@ -737,7 +752,7 @@ def values(self): order_snapshot = tuple(self.order) return iter(tuple(self.data[uid] for uid in order_snapshot if uid in self.data)) - def items(self): + def items(self) -> Iterator[tuple[uuid.UUID, T]]: """按顺序返回 `(uid, config)` 对。""" order_snapshot = tuple(self.order) diff --git a/app/core/config/pydantic.py b/app/core/config/pydantic.py index 82c81f73..3db32f0d 100644 --- a/app/core/config/pydantic.py +++ b/app/core/config/pydantic.py @@ -2,6 +2,7 @@ import asyncio import ast +import difflib import inspect import re import textwrap @@ -52,6 +53,10 @@ def _default_virtual_dependencies_cache() -> ( return {} +def _default_ref_validation_cache() -> dict[tuple[str, str, tuple[str, ...], str], Any]: + return {} + + def _normalize_mapping(value: Any) -> dict[str, Any]: """将任意映射值规范化为 `dict[str, Any]`。""" @@ -179,6 +184,7 @@ def _export_group_model( group_name: str, group_model: BaseModel, if_decrypt: bool, + skip_virtual: bool, ) -> dict[str, Any]: """将分组模型导出为字典,并按需解密加密字段。""" @@ -187,6 +193,8 @@ def _export_group_model( for field_name in type(group_model).model_fields: virtual_field = _get_field_marker(group_model, field_name, VirtualField) if virtual_field is not None: + if skip_virtual: + continue data[_to_snake_case(field_name)] = owner.get_virtual_value( group_name, field_name, virtual_field ) @@ -196,7 +204,7 @@ def _export_group_model( if isinstance(value, BaseModel): data[_to_snake_case(field_name)] = _export_group_model( - owner, field_name, value, if_decrypt + owner, field_name, value, if_decrypt, skip_virtual ) continue @@ -216,6 +224,12 @@ class PydanticConfigBase(BaseModel): LEGACY_FIELD_MAP: ClassVar[dict[tuple[str, str], tuple[str, str]]] = {} related_config: ClassVar[dict[str, MultipleConfig[Any]]] = {} + _class_virtual_dependencies: ClassVar[ + dict[ + type["PydanticConfigBase"], + dict[tuple[str, str], tuple[tuple[str, str], ...]], + ] + ] = {} _file: Path | None = PrivateAttr(default=None) _is_locked: bool = PrivateAttr(default=False) _save_methods: list[SaveMethod] = PrivateAttr(default_factory=_default_save_methods) @@ -235,10 +249,12 @@ class PydanticConfigBase(BaseModel): _owner_uid: uuid.UUID | None = PrivateAttr(default=None) _mutex: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock) _virtual_dependencies_cache: dict[tuple[str, str], tuple[tuple[str, str], ...]] = ( - PrivateAttr( # type: ignore[assignment] - default_factory=_default_virtual_dependencies_cache - ) + PrivateAttr(default_factory=_default_virtual_dependencies_cache) ) + _ref_validation_cache: dict[tuple[str, str, tuple[str, ...], str], Any] = ( + PrivateAttr(default_factory=_default_ref_validation_cache) + ) + _ref_validation_cache_enabled: bool = PrivateAttr(default=False) @property def file(self) -> Path | None: @@ -252,6 +268,34 @@ def model_post_init(self, __context: Any) -> None: self._build_virtual_dependency_cache() self._register_ref_bindings() + def _suggest_candidates(self, value: str, candidates: list[str]) -> list[str]: + if not candidates: + return [] + + snake = _to_snake_case(value) + exact_snake_matches = [ + candidate for candidate in candidates if _to_snake_case(candidate) == snake + ] + if exact_snake_matches: + return exact_snake_matches[:3] + + close_matches = difflib.get_close_matches(value, candidates, n=3, cutoff=0.5) + if close_matches: + return close_matches + + snake_matches = difflib.get_close_matches( + snake, + [_to_snake_case(candidate) for candidate in candidates], + n=3, + cutoff=0.5, + ) + mapped: list[str] = [] + for snake_match in snake_matches: + for candidate in candidates: + if _to_snake_case(candidate) == snake_match and candidate not in mapped: + mapped.append(candidate) + return mapped[:3] + def _resolve_group_name(self, group: str) -> str: """将调用侧传入的分组名解析为模型中的真实分组名。 @@ -278,7 +322,14 @@ def _resolve_group_name(self, group: str) -> str: if _to_snake_case(candidate) == snake: return candidate - raise AttributeError(f"配置分组 '{group}' 不存在") + suggestions = self._suggest_candidates(group, list(groups.keys())) + if suggestions: + raise AttributeError( + f"配置分组 '{group}' 不存在。你可能想用: {', '.join(suggestions)}" + ) + raise AttributeError( + f"配置分组 '{group}' 不存在。可用分组: {', '.join(groups.keys())}" + ) def _resolve_field_name(self, group: str, name: str) -> str: """将字段名解析为指定分组中的真实字段名。 @@ -310,7 +361,17 @@ def _resolve_field_name(self, group: str, name: str) -> str: if _to_snake_case(candidate) == snake: return candidate - raise AttributeError(f"配置项 '{group}.{name}' 不存在") + field_candidates = list(fields.keys()) + suggestions = self._suggest_candidates(name, field_candidates) + if suggestions: + raise AttributeError( + f"配置项 '{group}.{name}' 不存在。你可能想用: " + f"{resolved_group}.{suggestions[0]}" + ) + raise AttributeError( + f"配置项 '{group}.{name}' 不存在。" + f"可用字段: {', '.join(field_candidates)}" + ) def _normalize_dependency(self, dependency: tuple[str, str]) -> tuple[str, str]: """将虚拟字段依赖项规范化为真实 ``(group, field)`` 对。 @@ -414,7 +475,13 @@ def _build_virtual_dependency_cache(self) -> None: 哪些虚拟字段需要重新计算并派发绑定事件。 """ - self._virtual_dependencies_cache = {} + cls = type(self) + class_cache = cls._class_virtual_dependencies.get(cls) + if class_cache is not None: + self._virtual_dependencies_cache = class_cache + return + + class_cache = {} for group_name, group_model in self._group_index().items(): for field_name in type(group_model).model_fields: @@ -432,7 +499,10 @@ def _build_virtual_dependency_cache(self) -> None: else: deps = () - self._virtual_dependencies_cache[(group_name, field_name)] = deps + class_cache[(group_name, field_name)] = deps + + cls._class_virtual_dependencies[cls] = class_cache + self._virtual_dependencies_cache = class_cache def _multiple_config_index(self) -> dict[str, MultipleConfig[Any]]: result: dict[str, MultipleConfig[Any]] = {} @@ -452,9 +522,25 @@ def _group_index(self) -> dict[str, BaseModel]: def _normalize_value(self, group: str, name: str, value: Any) -> Any: ref_field = self._get_ref_field(group, name) if ref_field is not None: + if self._ref_validation_cache_enabled: + return self._normalize_ref_value_cached(ref_field, value) return self._normalize_ref_value(ref_field, value) return value + def _normalize_ref_value_cached(self, spec: RefField, value: Any) -> Any: + cache_key = ( + spec.target, + str(spec.default), + tuple(str(item) for item in spec.allow_values), + str(value), + ) + if cache_key in self._ref_validation_cache: + return self._ref_validation_cache[cache_key] + + normalized = self._normalize_ref_value(spec, value) + self._ref_validation_cache[cache_key] = normalized + return normalized + def _bind_owner_collection( self, collection: MultipleConfig[Any], uid: uuid.UUID ) -> None: @@ -674,6 +760,11 @@ async def _flush_pending_bindings(self) -> None: async def transaction(self) -> AsyncIterator["PydanticConfigBase"]: """开启一个延迟保存事务。""" + is_outermost = self._transaction_depth == 0 + if is_outermost: + self._ref_validation_cache_enabled = True + self._ref_validation_cache.clear() + self._transaction_depth += 1 try: yield self @@ -681,6 +772,9 @@ async def transaction(self) -> AsyncIterator["PydanticConfigBase"]: self._transaction_depth -= 1 if self._transaction_depth == 0: await self._flush_pending_changes() + if is_outermost: + self._ref_validation_cache_enabled = False + self._ref_validation_cache.clear() async def _flush_pending_changes(self) -> None: await self._flush_pending_bindings() @@ -698,7 +792,7 @@ async def _save_unlocked(self) -> None: if not self._file: raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件") - content = dump_toml(await self.toDict(if_decrypt=False)) + content = dump_toml(await self.toDict(if_decrypt=False, skip_virtual=True)) atomic_write_text(self._file, content) async def connect(self, path: Path) -> None: @@ -813,7 +907,10 @@ async def load(self, data: dict[str, Any]) -> None: await asyncio.gather(*(_() for _ in self._save_methods)) async def toDict( - self, if_decrypt: bool = True, regenerate_uuids: bool = False + self, + if_decrypt: bool = True, + regenerate_uuids: bool = False, + skip_virtual: bool = False, ) -> dict[str, Any]: """将当前配置导出为 snake_case 协议字典。 @@ -835,14 +932,14 @@ async def toDict( for group_name, group_model in self._group_index().items(): data[_to_snake_case(group_name)] = _export_group_model( - self, group_name, group_model, if_decrypt + self, group_name, group_model, if_decrypt, skip_virtual ) for name, item in self._multiple_config_index().items(): if "sub_configs_info" not in data: data["sub_configs_info"] = {} data["sub_configs_info"][_to_snake_case(name)] = await item.toDict( - if_decrypt, regenerate_uuids + if_decrypt, regenerate_uuids, skip_virtual ) return data From da57cda69027f54ffc079cf7da1c92dc6945f4c9 Mon Sep 17 00:00:00 2001 From: MoeSnowyFox Date: Fri, 17 Apr 2026 02:57:35 +0800 Subject: [PATCH 21/29] =?UTF-8?q?refactor(api):=20=E7=94=9F=E6=88=90API?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E4=BB=B6=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/__init__.py | 2 + app/contracts/emulator_contract.py | 21 +- app/contracts/plan_contract.py | 27 +- app/contracts/queue_contract.py | 39 +- app/contracts/scripts_contract.py | 26 +- app/contracts/setting_contract.py | 18 +- app/contracts/tools_contract.py | 3 +- app/core/__init__.py | 32 +- app/core/config/manager.py | 67 + app/core/config/shortcuts.py | 47 +- app/core/task_manager.py | 2 +- app/core/timer.py | 2 +- app/models/__init__.py | 94 +- app/models/global_config.py | 4 + app/services/matomo.py | 2 +- app/services/notification.py | 8 +- app/services/system.py | 34 +- app/services/update.py | 4 +- app/task/MAA/tools/notify.py | 35 +- app/task/MaaEnd/tools/notify.py | 26 +- app/task/SRC/tools/notify.py | 18 +- app/task/general/tools/notify.py | 3 +- app/utils/__init__.py | 100 +- app/utils/emulator/general.py | 39 +- app/utils/emulator/ldplayer.py | 44 +- app/utils/emulator/mumu.py | 86 +- frontend/eslint.config.mjs | 22 +- frontend/openapi-ts.config.mjs | 12 + frontend/openapi.json | 12121 ++++++++++++++++ frontend/package.json | 6 +- frontend/src/api/constants.ts | 55 + frontend/src/api/core/ApiError.ts | 25 - frontend/src/api/core/ApiRequestOptions.ts | 17 - frontend/src/api/core/ApiResult.ts | 11 - frontend/src/api/core/CancelablePromise.ts | 131 - frontend/src/api/core/OpenAPI.ts | 32 - frontend/src/api/core/request.ts | 323 - frontend/src/api/gateways/dispatch.ts | 30 + frontend/src/api/gateways/emulator.ts | 62 + frontend/src/api/gateways/history.ts | 15 + frontend/src/api/gateways/info.ts | 62 + frontend/src/api/gateways/ocr.ts | 48 + frontend/src/api/gateways/plan.ts | 38 + frontend/src/api/gateways/queue.ts | 122 + frontend/src/api/gateways/script.ts | 147 + frontend/src/api/gateways/setting.ts | 108 + frontend/src/api/gateways/tools.ts | 12 + frontend/src/api/gateways/update.ts | 20 + frontend/src/api/gateways/wsDebug.ts | 64 + frontend/src/api/generated/client.gen.ts | 16 + .../src/api/generated/client/client.gen.ts | 268 + frontend/src/api/generated/client/index.ts | 26 + .../src/api/generated/client/types.gen.ts | 268 + .../src/api/generated/client/utils.gen.ts | 332 + frontend/src/api/generated/core/auth.gen.ts | 42 + .../api/generated/core/bodySerializer.gen.ts | 100 + frontend/src/api/generated/core/params.gen.ts | 176 + .../api/generated/core/pathSerializer.gen.ts | 181 + .../generated/core/queryKeySerializer.gen.ts | 136 + .../generated/core/serverSentEvents.gen.ts | 264 + frontend/src/api/generated/core/types.gen.ts | 118 + frontend/src/api/generated/core/utils.gen.ts | 143 + frontend/src/api/generated/index.ts | 4 + frontend/src/api/generated/sdk.gen.ts | 1487 ++ frontend/src/api/generated/types.gen.ts | 7887 ++++++++++ frontend/src/api/index.ts | 328 +- frontend/src/api/models/ADBScreenshotIn.ts | 19 - frontend/src/api/models/ADBScreenshotOut.ts | 35 - frontend/src/api/models/CheckImageAllIn.ts | 27 - frontend/src/api/models/CheckImageAnyIn.ts | 27 - frontend/src/api/models/CheckImageIn.ts | 27 - frontend/src/api/models/CheckImageOut.ts | 27 - frontend/src/api/models/ClickImageIn.ts | 27 - frontend/src/api/models/ClickOut.ts | 27 - frontend/src/api/models/ClickTextIn.ts | 23 - frontend/src/api/models/ComboBoxItem.ts | 15 - frontend/src/api/models/ComboBoxOut.ts | 24 - frontend/src/api/models/DeviceInfo.ts | 22 - frontend/src/api/models/DispatchIn.ts | 11 - frontend/src/api/models/EmulatorConfig.ts | 12 - .../src/api/models/EmulatorConfigIndexItem.ts | 15 - .../src/api/models/EmulatorConfig_Info.ts | 27 - frontend/src/api/models/EmulatorCreateOut.ts | 28 - frontend/src/api/models/EmulatorDeleteIn.ts | 11 - frontend/src/api/models/EmulatorGetIn.ts | 11 - frontend/src/api/models/EmulatorGetOut.ts | 29 - frontend/src/api/models/EmulatorOperateIn.ts | 29 - frontend/src/api/models/EmulatorReorderIn.ts | 11 - frontend/src/api/models/EmulatorSearchOut.ts | 24 - .../src/api/models/EmulatorSearchResult.ts | 19 - frontend/src/api/models/EmulatorStatusOut.ts | 24 - frontend/src/api/models/EmulatorUpdateIn.ts | 16 - frontend/src/api/models/GeneralConfig.ts | 27 - frontend/src/api/models/GeneralConfig_Game.ts | 47 - frontend/src/api/models/GeneralConfig_Info.ts | 15 - frontend/src/api/models/GeneralConfig_Run.ts | 19 - .../src/api/models/GeneralConfig_Script.ts | 71 - frontend/src/api/models/GeneralUserConfig.ts | 22 - .../src/api/models/GeneralUserConfig_Data.ts | 15 - .../src/api/models/GeneralUserConfig_Info.ts | 43 - .../api/models/GeneralUserConfig_Notify.ts | 31 - frontend/src/api/models/GetStageIn.ts | 28 - frontend/src/api/models/GlobalConfig.ts | 37 - .../src/api/models/GlobalConfig_Function.ts | 27 - .../src/api/models/GlobalConfig_Notify.ts | 63 - frontend/src/api/models/GlobalConfig_Start.ts | 15 - frontend/src/api/models/GlobalConfig_UI.ts | 15 - .../src/api/models/GlobalConfig_Update.ts | 27 - frontend/src/api/models/GlobalConfig_Voice.ts | 15 - .../src/api/models/HTTPValidationError.ts | 9 - frontend/src/api/models/HistoryData.ts | 28 - frontend/src/api/models/HistoryDataGetIn.ts | 11 - frontend/src/api/models/HistoryDataGetOut.ts | 24 - frontend/src/api/models/HistoryIndexItem.ts | 28 - frontend/src/api/models/HistorySearchIn.ts | 29 - frontend/src/api/models/HistorySearchOut.ts | 24 - frontend/src/api/models/InfoOut.ts | 23 - frontend/src/api/models/MaaConfig.ts | 22 - frontend/src/api/models/MaaConfig_Emulator.ts | 15 - frontend/src/api/models/MaaConfig_Info.ts | 15 - frontend/src/api/models/MaaConfig_Run.ts | 31 - frontend/src/api/models/MaaEndConfig.ts | 22 - frontend/src/api/models/MaaEndConfig_Game.ts | 35 - frontend/src/api/models/MaaEndConfig_Info.ts | 15 - frontend/src/api/models/MaaEndConfig_Run.ts | 19 - frontend/src/api/models/MaaEndUserConfig.ts | 22 - .../src/api/models/MaaEndUserConfig_Info.ts | 43 - .../src/api/models/MaaEndUserConfig_Notify.ts | 31 - .../src/api/models/MaaEndUserConfig_Task.ts | 27 - frontend/src/api/models/MaaPlanConfig.ts | 45 - frontend/src/api/models/MaaPlanConfig_Info.ts | 15 - frontend/src/api/models/MaaPlanConfig_Item.ts | 35 - frontend/src/api/models/MaaUserConfig.ts | 27 - frontend/src/api/models/MaaUserConfig_Data.ts | 11 - frontend/src/api/models/MaaUserConfig_Info.ts | 99 - .../src/api/models/MaaUserConfig_Notify.ts | 35 - frontend/src/api/models/MaaUserConfig_Task.ts | 39 - frontend/src/api/models/NoticeOut.ts | 27 - frontend/src/api/models/OCRScreenshotIn.ts | 27 - frontend/src/api/models/OCRScreenshotOut.ts | 35 - frontend/src/api/models/OutBase.ts | 19 - frontend/src/api/models/PlanCreateIn.ts | 8 - frontend/src/api/models/PlanCreateOut.ts | 28 - frontend/src/api/models/PlanDeleteIn.ts | 11 - frontend/src/api/models/PlanGetIn.ts | 11 - frontend/src/api/models/PlanGetOut.ts | 29 - frontend/src/api/models/PlanIndexItem.ts | 15 - frontend/src/api/models/PlanReorderIn.ts | 11 - frontend/src/api/models/PlanUpdateIn.ts | 16 - frontend/src/api/models/PowerIn.ts | 25 - frontend/src/api/models/PowerOut.ts | 37 - frontend/src/api/models/QueueConfig.ts | 12 - frontend/src/api/models/QueueConfig_Info.ts | 23 - frontend/src/api/models/QueueCreateOut.ts | 28 - frontend/src/api/models/QueueDeleteIn.ts | 11 - frontend/src/api/models/QueueGetIn.ts | 11 - frontend/src/api/models/QueueGetOut.ts | 29 - frontend/src/api/models/QueueIndexItem.ts | 15 - frontend/src/api/models/QueueItem.ts | 12 - frontend/src/api/models/QueueItemCreateOut.ts | 28 - frontend/src/api/models/QueueItemDeleteIn.ts | 15 - frontend/src/api/models/QueueItemGetIn.ts | 15 - frontend/src/api/models/QueueItemGetOut.ts | 29 - frontend/src/api/models/QueueItemIndexItem.ts | 15 - frontend/src/api/models/QueueItemReorderIn.ts | 15 - frontend/src/api/models/QueueItemUpdateIn.ts | 20 - frontend/src/api/models/QueueItem_Info.ts | 11 - frontend/src/api/models/QueueReorderIn.ts | 11 - frontend/src/api/models/QueueSetInBase.ts | 11 - frontend/src/api/models/QueueUpdateIn.ts | 16 - frontend/src/api/models/ScriptCreateIn.ts | 26 - frontend/src/api/models/ScriptCreateOut.ts | 31 - frontend/src/api/models/ScriptDeleteIn.ts | 11 - frontend/src/api/models/ScriptFileIn.ts | 15 - frontend/src/api/models/ScriptGetIn.ts | 11 - frontend/src/api/models/ScriptGetOut.ts | 32 - frontend/src/api/models/ScriptIndexItem.ts | 26 - frontend/src/api/models/ScriptReorderIn.ts | 11 - frontend/src/api/models/ScriptUpdateIn.ts | 19 - frontend/src/api/models/ScriptUploadIn.ts | 23 - frontend/src/api/models/ScriptUrlIn.ts | 15 - frontend/src/api/models/SettingGetOut.ts | 24 - frontend/src/api/models/SettingUpdateIn.ts | 12 - frontend/src/api/models/SrcConfig.ts | 22 - frontend/src/api/models/SrcConfig_Emulator.ts | 15 - frontend/src/api/models/SrcConfig_Info.ts | 15 - frontend/src/api/models/SrcConfig_Run.ts | 23 - frontend/src/api/models/SrcUserConfig.ts | 27 - frontend/src/api/models/SrcUserConfig_Data.ts | 19 - frontend/src/api/models/SrcUserConfig_Info.ts | 43 - .../src/api/models/SrcUserConfig_Notify.ts | 31 - .../src/api/models/SrcUserConfig_Stage.ts | 43 - frontend/src/api/models/TaskCreateIn.ts | 25 - frontend/src/api/models/TaskCreateOut.ts | 23 - frontend/src/api/models/TimeSet.ts | 12 - frontend/src/api/models/TimeSetCreateOut.ts | 28 - frontend/src/api/models/TimeSetDeleteIn.ts | 15 - frontend/src/api/models/TimeSetGetIn.ts | 15 - frontend/src/api/models/TimeSetGetOut.ts | 29 - frontend/src/api/models/TimeSetIndexItem.ts | 15 - frontend/src/api/models/TimeSetReorderIn.ts | 15 - frontend/src/api/models/TimeSetUpdateIn.ts | 20 - frontend/src/api/models/TimeSet_Info.ts | 19 - frontend/src/api/models/ToolsConfig.ts | 12 - .../src/api/models/ToolsConfig_ArknightsPC.ts | 39 - frontend/src/api/models/ToolsGetOut.ts | 24 - frontend/src/api/models/ToolsUpdateIn.ts | 12 - frontend/src/api/models/UpdateCheckIn.ts | 15 - frontend/src/api/models/UpdateCheckOut.ts | 31 - frontend/src/api/models/UserCreateOut.ts | 31 - frontend/src/api/models/UserDeleteIn.ts | 15 - frontend/src/api/models/UserGetIn.ts | 15 - frontend/src/api/models/UserGetOut.ts | 32 - frontend/src/api/models/UserInBase.ts | 11 - frontend/src/api/models/UserIndexItem.ts | 26 - frontend/src/api/models/UserReorderIn.ts | 15 - frontend/src/api/models/UserSetIn.ts | 19 - frontend/src/api/models/UserUpdateIn.ts | 23 - frontend/src/api/models/ValidationError.ts | 10 - frontend/src/api/models/VersionOut.ts | 31 - frontend/src/api/models/WSClearHistoryIn.ts | 14 - frontend/src/api/models/WSClientAuthIn.ts | 26 - frontend/src/api/models/WSClientConnectIn.ts | 14 - frontend/src/api/models/WSClientCreateIn.ts | 34 - frontend/src/api/models/WSClientCreateOut.ts | 26 - .../src/api/models/WSClientDisconnectIn.ts | 14 - frontend/src/api/models/WSClientListOut.ts | 26 - frontend/src/api/models/WSClientRemoveIn.ts | 14 - frontend/src/api/models/WSClientSendIn.ts | 18 - frontend/src/api/models/WSClientSendJsonIn.ts | 26 - frontend/src/api/models/WSClientStatusIn.ts | 14 - frontend/src/api/models/WSClientStatusOut.ts | 26 - frontend/src/api/models/WSCommandsOut.ts | 26 - .../src/api/models/WSMessageHistoryOut.ts | 26 - frontend/src/api/models/Webhook.ts | 17 - frontend/src/api/models/WebhookCreateOut.ts | 28 - frontend/src/api/models/WebhookDeleteIn.ts | 19 - frontend/src/api/models/WebhookGetIn.ts | 19 - frontend/src/api/models/WebhookGetOut.ts | 29 - frontend/src/api/models/WebhookInBase.ts | 15 - frontend/src/api/models/WebhookIndexItem.ts | 15 - frontend/src/api/models/WebhookReorderIn.ts | 19 - frontend/src/api/models/WebhookTestIn.ts | 20 - frontend/src/api/models/WebhookUpdateIn.ts | 24 - frontend/src/api/models/Webhook_Data.ts | 23 - frontend/src/api/models/Webhook_Info.ts | 15 - frontend/src/api/runtime.ts | 17 + frontend/src/api/services/ActionService.ts | 272 - frontend/src/api/services/AddService.ts | 169 - frontend/src/api/services/DeleteService.ts | 189 - frontend/src/api/services/GetService.ts | 647 - frontend/src/api/services/OcrService.ts | 238 - frontend/src/api/services/Service.ts | 1489 -- frontend/src/api/services/UpdateService.ts | 470 - frontend/src/api/services/WebSocketService.ts | 266 - .../src/components/GlobalPowerCountdown.vue | 32 +- frontend/src/components/NoticeModal.vue | 66 +- frontend/src/components/ScriptTable.vue | 1 - .../src/components/UpdateDownloadModal.vue | 244 +- frontend/src/components/WebhookManager.vue | 258 +- frontend/src/composables/useAudioPlayer.ts | 60 +- frontend/src/composables/usePlanApi.ts | 38 +- .../src/composables/usePlanDataCoordinator.ts | 230 +- frontend/src/composables/useScriptApi.ts | 61 +- frontend/src/composables/useSettingsApi.ts | 14 +- frontend/src/composables/useTemplateApi.ts | 23 +- frontend/src/composables/useToolsApi.ts | 96 +- frontend/src/composables/useUpdateChecker.ts | 10 +- frontend/src/composables/useUserApi.ts | 61 +- frontend/src/composables/useVersionService.ts | 57 +- frontend/src/main.ts | 50 +- frontend/src/types/script.ts | 27 - .../EditView/Script/GeneralScriptEdit.vue | 441 +- .../views/EditView/Script/MAAScriptEdit.vue | 163 +- .../EditView/Script/MaaEndScriptEdit.vue | 173 +- .../views/EditView/Script/SRCScriptEdit.vue | 135 +- .../views/EditView/User/GeneralUserEdit.vue | 202 +- .../src/views/EditView/User/MAAUserEdit.vue | 159 +- .../views/EditView/User/MaaEndUserEdit.vue | 11 +- .../src/views/EditView/User/SRCUserEdit.vue | 71 +- frontend/src/views/Emulator.vue | 268 +- frontend/src/views/Home.vue | 132 +- frontend/src/views/OCRdev.vue | 194 +- frontend/src/views/Scripts.vue | 23 +- frontend/src/views/WSdev.vue | 269 +- frontend/src/views/history/useHistoryLogic.ts | 66 +- .../queue/components/QueueItemManager.vue | 65 +- .../views/queue/components/TimeSetManager.vue | 117 +- frontend/src/views/queue/index.vue | 109 +- .../views/scheduler/SchedulerTaskControl.vue | 56 +- .../src/views/scheduler/schedulerConstants.ts | 41 +- .../src/views/scheduler/useSchedulerLogic.ts | 232 +- frontend/src/views/setting/TabBasic.vue | 6 +- frontend/src/views/setting/TabFunction.vue | 195 +- frontend/src/views/setting/TabNotify.vue | 177 +- frontend/src/views/setting/index.vue | 66 +- frontend/src/views/tools/TabArknightsPC.vue | 766 +- frontend/src/views/tools/index.vue | 6 +- frontend/yarn.lock | 382 +- main.py | 2 + 300 files changed, 28678 insertions(+), 11083 deletions(-) create mode 100644 frontend/openapi-ts.config.mjs create mode 100644 frontend/openapi.json create mode 100644 frontend/src/api/constants.ts delete mode 100644 frontend/src/api/core/ApiError.ts delete mode 100644 frontend/src/api/core/ApiRequestOptions.ts delete mode 100644 frontend/src/api/core/ApiResult.ts delete mode 100644 frontend/src/api/core/CancelablePromise.ts delete mode 100644 frontend/src/api/core/OpenAPI.ts delete mode 100644 frontend/src/api/core/request.ts create mode 100644 frontend/src/api/gateways/dispatch.ts create mode 100644 frontend/src/api/gateways/emulator.ts create mode 100644 frontend/src/api/gateways/history.ts create mode 100644 frontend/src/api/gateways/info.ts create mode 100644 frontend/src/api/gateways/ocr.ts create mode 100644 frontend/src/api/gateways/plan.ts create mode 100644 frontend/src/api/gateways/queue.ts create mode 100644 frontend/src/api/gateways/script.ts create mode 100644 frontend/src/api/gateways/setting.ts create mode 100644 frontend/src/api/gateways/tools.ts create mode 100644 frontend/src/api/gateways/update.ts create mode 100644 frontend/src/api/gateways/wsDebug.ts create mode 100644 frontend/src/api/generated/client.gen.ts create mode 100644 frontend/src/api/generated/client/client.gen.ts create mode 100644 frontend/src/api/generated/client/index.ts create mode 100644 frontend/src/api/generated/client/types.gen.ts create mode 100644 frontend/src/api/generated/client/utils.gen.ts create mode 100644 frontend/src/api/generated/core/auth.gen.ts create mode 100644 frontend/src/api/generated/core/bodySerializer.gen.ts create mode 100644 frontend/src/api/generated/core/params.gen.ts create mode 100644 frontend/src/api/generated/core/pathSerializer.gen.ts create mode 100644 frontend/src/api/generated/core/queryKeySerializer.gen.ts create mode 100644 frontend/src/api/generated/core/serverSentEvents.gen.ts create mode 100644 frontend/src/api/generated/core/types.gen.ts create mode 100644 frontend/src/api/generated/core/utils.gen.ts create mode 100644 frontend/src/api/generated/index.ts create mode 100644 frontend/src/api/generated/sdk.gen.ts create mode 100644 frontend/src/api/generated/types.gen.ts delete mode 100644 frontend/src/api/models/ADBScreenshotIn.ts delete mode 100644 frontend/src/api/models/ADBScreenshotOut.ts delete mode 100644 frontend/src/api/models/CheckImageAllIn.ts delete mode 100644 frontend/src/api/models/CheckImageAnyIn.ts delete mode 100644 frontend/src/api/models/CheckImageIn.ts delete mode 100644 frontend/src/api/models/CheckImageOut.ts delete mode 100644 frontend/src/api/models/ClickImageIn.ts delete mode 100644 frontend/src/api/models/ClickOut.ts delete mode 100644 frontend/src/api/models/ClickTextIn.ts delete mode 100644 frontend/src/api/models/ComboBoxItem.ts delete mode 100644 frontend/src/api/models/ComboBoxOut.ts delete mode 100644 frontend/src/api/models/DeviceInfo.ts delete mode 100644 frontend/src/api/models/DispatchIn.ts delete mode 100644 frontend/src/api/models/EmulatorConfig.ts delete mode 100644 frontend/src/api/models/EmulatorConfigIndexItem.ts delete mode 100644 frontend/src/api/models/EmulatorConfig_Info.ts delete mode 100644 frontend/src/api/models/EmulatorCreateOut.ts delete mode 100644 frontend/src/api/models/EmulatorDeleteIn.ts delete mode 100644 frontend/src/api/models/EmulatorGetIn.ts delete mode 100644 frontend/src/api/models/EmulatorGetOut.ts delete mode 100644 frontend/src/api/models/EmulatorOperateIn.ts delete mode 100644 frontend/src/api/models/EmulatorReorderIn.ts delete mode 100644 frontend/src/api/models/EmulatorSearchOut.ts delete mode 100644 frontend/src/api/models/EmulatorSearchResult.ts delete mode 100644 frontend/src/api/models/EmulatorStatusOut.ts delete mode 100644 frontend/src/api/models/EmulatorUpdateIn.ts delete mode 100644 frontend/src/api/models/GeneralConfig.ts delete mode 100644 frontend/src/api/models/GeneralConfig_Game.ts delete mode 100644 frontend/src/api/models/GeneralConfig_Info.ts delete mode 100644 frontend/src/api/models/GeneralConfig_Run.ts delete mode 100644 frontend/src/api/models/GeneralConfig_Script.ts delete mode 100644 frontend/src/api/models/GeneralUserConfig.ts delete mode 100644 frontend/src/api/models/GeneralUserConfig_Data.ts delete mode 100644 frontend/src/api/models/GeneralUserConfig_Info.ts delete mode 100644 frontend/src/api/models/GeneralUserConfig_Notify.ts delete mode 100644 frontend/src/api/models/GetStageIn.ts delete mode 100644 frontend/src/api/models/GlobalConfig.ts delete mode 100644 frontend/src/api/models/GlobalConfig_Function.ts delete mode 100644 frontend/src/api/models/GlobalConfig_Notify.ts delete mode 100644 frontend/src/api/models/GlobalConfig_Start.ts delete mode 100644 frontend/src/api/models/GlobalConfig_UI.ts delete mode 100644 frontend/src/api/models/GlobalConfig_Update.ts delete mode 100644 frontend/src/api/models/GlobalConfig_Voice.ts delete mode 100644 frontend/src/api/models/HTTPValidationError.ts delete mode 100644 frontend/src/api/models/HistoryData.ts delete mode 100644 frontend/src/api/models/HistoryDataGetIn.ts delete mode 100644 frontend/src/api/models/HistoryDataGetOut.ts delete mode 100644 frontend/src/api/models/HistoryIndexItem.ts delete mode 100644 frontend/src/api/models/HistorySearchIn.ts delete mode 100644 frontend/src/api/models/HistorySearchOut.ts delete mode 100644 frontend/src/api/models/InfoOut.ts delete mode 100644 frontend/src/api/models/MaaConfig.ts delete mode 100644 frontend/src/api/models/MaaConfig_Emulator.ts delete mode 100644 frontend/src/api/models/MaaConfig_Info.ts delete mode 100644 frontend/src/api/models/MaaConfig_Run.ts delete mode 100644 frontend/src/api/models/MaaEndConfig.ts delete mode 100644 frontend/src/api/models/MaaEndConfig_Game.ts delete mode 100644 frontend/src/api/models/MaaEndConfig_Info.ts delete mode 100644 frontend/src/api/models/MaaEndConfig_Run.ts delete mode 100644 frontend/src/api/models/MaaEndUserConfig.ts delete mode 100644 frontend/src/api/models/MaaEndUserConfig_Info.ts delete mode 100644 frontend/src/api/models/MaaEndUserConfig_Notify.ts delete mode 100644 frontend/src/api/models/MaaEndUserConfig_Task.ts delete mode 100644 frontend/src/api/models/MaaPlanConfig.ts delete mode 100644 frontend/src/api/models/MaaPlanConfig_Info.ts delete mode 100644 frontend/src/api/models/MaaPlanConfig_Item.ts delete mode 100644 frontend/src/api/models/MaaUserConfig.ts delete mode 100644 frontend/src/api/models/MaaUserConfig_Data.ts delete mode 100644 frontend/src/api/models/MaaUserConfig_Info.ts delete mode 100644 frontend/src/api/models/MaaUserConfig_Notify.ts delete mode 100644 frontend/src/api/models/MaaUserConfig_Task.ts delete mode 100644 frontend/src/api/models/NoticeOut.ts delete mode 100644 frontend/src/api/models/OCRScreenshotIn.ts delete mode 100644 frontend/src/api/models/OCRScreenshotOut.ts delete mode 100644 frontend/src/api/models/OutBase.ts delete mode 100644 frontend/src/api/models/PlanCreateIn.ts delete mode 100644 frontend/src/api/models/PlanCreateOut.ts delete mode 100644 frontend/src/api/models/PlanDeleteIn.ts delete mode 100644 frontend/src/api/models/PlanGetIn.ts delete mode 100644 frontend/src/api/models/PlanGetOut.ts delete mode 100644 frontend/src/api/models/PlanIndexItem.ts delete mode 100644 frontend/src/api/models/PlanReorderIn.ts delete mode 100644 frontend/src/api/models/PlanUpdateIn.ts delete mode 100644 frontend/src/api/models/PowerIn.ts delete mode 100644 frontend/src/api/models/PowerOut.ts delete mode 100644 frontend/src/api/models/QueueConfig.ts delete mode 100644 frontend/src/api/models/QueueConfig_Info.ts delete mode 100644 frontend/src/api/models/QueueCreateOut.ts delete mode 100644 frontend/src/api/models/QueueDeleteIn.ts delete mode 100644 frontend/src/api/models/QueueGetIn.ts delete mode 100644 frontend/src/api/models/QueueGetOut.ts delete mode 100644 frontend/src/api/models/QueueIndexItem.ts delete mode 100644 frontend/src/api/models/QueueItem.ts delete mode 100644 frontend/src/api/models/QueueItemCreateOut.ts delete mode 100644 frontend/src/api/models/QueueItemDeleteIn.ts delete mode 100644 frontend/src/api/models/QueueItemGetIn.ts delete mode 100644 frontend/src/api/models/QueueItemGetOut.ts delete mode 100644 frontend/src/api/models/QueueItemIndexItem.ts delete mode 100644 frontend/src/api/models/QueueItemReorderIn.ts delete mode 100644 frontend/src/api/models/QueueItemUpdateIn.ts delete mode 100644 frontend/src/api/models/QueueItem_Info.ts delete mode 100644 frontend/src/api/models/QueueReorderIn.ts delete mode 100644 frontend/src/api/models/QueueSetInBase.ts delete mode 100644 frontend/src/api/models/QueueUpdateIn.ts delete mode 100644 frontend/src/api/models/ScriptCreateIn.ts delete mode 100644 frontend/src/api/models/ScriptCreateOut.ts delete mode 100644 frontend/src/api/models/ScriptDeleteIn.ts delete mode 100644 frontend/src/api/models/ScriptFileIn.ts delete mode 100644 frontend/src/api/models/ScriptGetIn.ts delete mode 100644 frontend/src/api/models/ScriptGetOut.ts delete mode 100644 frontend/src/api/models/ScriptIndexItem.ts delete mode 100644 frontend/src/api/models/ScriptReorderIn.ts delete mode 100644 frontend/src/api/models/ScriptUpdateIn.ts delete mode 100644 frontend/src/api/models/ScriptUploadIn.ts delete mode 100644 frontend/src/api/models/ScriptUrlIn.ts delete mode 100644 frontend/src/api/models/SettingGetOut.ts delete mode 100644 frontend/src/api/models/SettingUpdateIn.ts delete mode 100644 frontend/src/api/models/SrcConfig.ts delete mode 100644 frontend/src/api/models/SrcConfig_Emulator.ts delete mode 100644 frontend/src/api/models/SrcConfig_Info.ts delete mode 100644 frontend/src/api/models/SrcConfig_Run.ts delete mode 100644 frontend/src/api/models/SrcUserConfig.ts delete mode 100644 frontend/src/api/models/SrcUserConfig_Data.ts delete mode 100644 frontend/src/api/models/SrcUserConfig_Info.ts delete mode 100644 frontend/src/api/models/SrcUserConfig_Notify.ts delete mode 100644 frontend/src/api/models/SrcUserConfig_Stage.ts delete mode 100644 frontend/src/api/models/TaskCreateIn.ts delete mode 100644 frontend/src/api/models/TaskCreateOut.ts delete mode 100644 frontend/src/api/models/TimeSet.ts delete mode 100644 frontend/src/api/models/TimeSetCreateOut.ts delete mode 100644 frontend/src/api/models/TimeSetDeleteIn.ts delete mode 100644 frontend/src/api/models/TimeSetGetIn.ts delete mode 100644 frontend/src/api/models/TimeSetGetOut.ts delete mode 100644 frontend/src/api/models/TimeSetIndexItem.ts delete mode 100644 frontend/src/api/models/TimeSetReorderIn.ts delete mode 100644 frontend/src/api/models/TimeSetUpdateIn.ts delete mode 100644 frontend/src/api/models/TimeSet_Info.ts delete mode 100644 frontend/src/api/models/ToolsConfig.ts delete mode 100644 frontend/src/api/models/ToolsConfig_ArknightsPC.ts delete mode 100644 frontend/src/api/models/ToolsGetOut.ts delete mode 100644 frontend/src/api/models/ToolsUpdateIn.ts delete mode 100644 frontend/src/api/models/UpdateCheckIn.ts delete mode 100644 frontend/src/api/models/UpdateCheckOut.ts delete mode 100644 frontend/src/api/models/UserCreateOut.ts delete mode 100644 frontend/src/api/models/UserDeleteIn.ts delete mode 100644 frontend/src/api/models/UserGetIn.ts delete mode 100644 frontend/src/api/models/UserGetOut.ts delete mode 100644 frontend/src/api/models/UserInBase.ts delete mode 100644 frontend/src/api/models/UserIndexItem.ts delete mode 100644 frontend/src/api/models/UserReorderIn.ts delete mode 100644 frontend/src/api/models/UserSetIn.ts delete mode 100644 frontend/src/api/models/UserUpdateIn.ts delete mode 100644 frontend/src/api/models/ValidationError.ts delete mode 100644 frontend/src/api/models/VersionOut.ts delete mode 100644 frontend/src/api/models/WSClearHistoryIn.ts delete mode 100644 frontend/src/api/models/WSClientAuthIn.ts delete mode 100644 frontend/src/api/models/WSClientConnectIn.ts delete mode 100644 frontend/src/api/models/WSClientCreateIn.ts delete mode 100644 frontend/src/api/models/WSClientCreateOut.ts delete mode 100644 frontend/src/api/models/WSClientDisconnectIn.ts delete mode 100644 frontend/src/api/models/WSClientListOut.ts delete mode 100644 frontend/src/api/models/WSClientRemoveIn.ts delete mode 100644 frontend/src/api/models/WSClientSendIn.ts delete mode 100644 frontend/src/api/models/WSClientSendJsonIn.ts delete mode 100644 frontend/src/api/models/WSClientStatusIn.ts delete mode 100644 frontend/src/api/models/WSClientStatusOut.ts delete mode 100644 frontend/src/api/models/WSCommandsOut.ts delete mode 100644 frontend/src/api/models/WSMessageHistoryOut.ts delete mode 100644 frontend/src/api/models/Webhook.ts delete mode 100644 frontend/src/api/models/WebhookCreateOut.ts delete mode 100644 frontend/src/api/models/WebhookDeleteIn.ts delete mode 100644 frontend/src/api/models/WebhookGetIn.ts delete mode 100644 frontend/src/api/models/WebhookGetOut.ts delete mode 100644 frontend/src/api/models/WebhookInBase.ts delete mode 100644 frontend/src/api/models/WebhookIndexItem.ts delete mode 100644 frontend/src/api/models/WebhookReorderIn.ts delete mode 100644 frontend/src/api/models/WebhookTestIn.ts delete mode 100644 frontend/src/api/models/WebhookUpdateIn.ts delete mode 100644 frontend/src/api/models/Webhook_Data.ts delete mode 100644 frontend/src/api/models/Webhook_Info.ts create mode 100644 frontend/src/api/runtime.ts delete mode 100644 frontend/src/api/services/ActionService.ts delete mode 100644 frontend/src/api/services/AddService.ts delete mode 100644 frontend/src/api/services/DeleteService.ts delete mode 100644 frontend/src/api/services/GetService.ts delete mode 100644 frontend/src/api/services/OcrService.ts delete mode 100644 frontend/src/api/services/Service.ts delete mode 100644 frontend/src/api/services/UpdateService.ts delete mode 100644 frontend/src/api/services/WebSocketService.ts diff --git a/app/api/__init__.py b/app/api/__init__.py index e3a78254..13313e4d 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -30,6 +30,7 @@ from .dispatch import router as dispatch_router from .history import router as history_router from .tools import router as tools_router +from .plugins import router as plugins_router from .setting import router as setting_router from .update import router as update_router from .ocr import router as ocr_router @@ -45,6 +46,7 @@ "dispatch_router", "history_router", "tools_router", + "plugins_router", "setting_router", "update_router", "ocr_router", diff --git a/app/contracts/emulator_contract.py b/app/contracts/emulator_contract.py index abdfb57b..295e7010 100644 --- a/app/contracts/emulator_contract.py +++ b/app/contracts/emulator_contract.py @@ -31,17 +31,28 @@ class EmulatorConfigIndexItem(ApiModel): type: Literal["EmulatorConfig"] = Field(..., description="配置类型") -EmulatorGetOut = ResourceCollectionOut[EmulatorConfigIndexItem, EmulatorRead] -EmulatorDetailOut = ResourceItemOut[EmulatorRead] -EmulatorCreateOut = ResourceCreateOut[EmulatorRead] +class EmulatorGetOut(ResourceCollectionOut[EmulatorConfigIndexItem, EmulatorRead]): + """模拟器列表响应模型""" + + +class EmulatorDetailOut(ResourceItemOut[EmulatorRead]): + """模拟器详情响应模型""" + + +class EmulatorCreateOut(ResourceCreateOut[EmulatorRead]): + """模拟器创建响应模型""" class EmulatorActionBody(ApiModel): index: str = Field(..., description="模拟器索引") -EmulatorStatusOut = ResourceItemOut[dict[str, dict[str, DeviceInfo]]] -EmulatorDeviceStatusOut = ResourceItemOut[dict[str, DeviceInfo]] +class EmulatorStatusOut(ResourceItemOut[dict[str, dict[str, DeviceInfo]]]): + """模拟器状态响应模型""" + + +class EmulatorDeviceStatusOut(ResourceItemOut[dict[str, DeviceInfo]]): + """模拟器设备状态响应模型""" class EmulatorSearchResult(ApiModel): diff --git a/app/contracts/plan_contract.py b/app/contracts/plan_contract.py index 8346c711..d1d5f822 100644 --- a/app/contracts/plan_contract.py +++ b/app/contracts/plan_contract.py @@ -205,11 +205,12 @@ class MaaPlanRead(ApiModel): ) # 自动展开的星期项 - for _day_key, _day_type, _day_field in _iter_weekday_fields( + for wk_key, wk_type, wk_field in _iter_weekday_fields( MaaPlanDayRead, optional=False ): - __annotations__[_day_key] = _day_type - locals()[_day_key] = _day_field + __annotations__[wk_key] = wk_type + locals()[wk_key] = wk_field + del wk_key, wk_type, wk_field class MaaPlanPatch(ApiModel): @@ -227,11 +228,12 @@ class MaaPlanPatch(ApiModel): ) # 自动展开的星期项 - for _day_key, _day_type, _day_field in _iter_weekday_fields( + for wk_key, wk_type, wk_field in _iter_weekday_fields( MaaPlanDayPatch, optional=True ): - __annotations__[_day_key] = _day_type - locals()[_day_key] = _day_field + __annotations__[wk_key] = wk_type + locals()[wk_key] = wk_field + del wk_key, wk_type, wk_field class PlanCreateIn(ApiModel): @@ -242,9 +244,16 @@ class PlanUpdateBody(ApiModel): data: MaaPlanPatch = Field(..., description="计划更新数据") -PlanCreateOut = ResourceCreateOut[MaaPlanRead] -PlanDetailOut = ResourceItemOut[MaaPlanRead] -PlanGetOut = ResourceCollectionOut[PlanIndexItem, MaaPlanRead] +class PlanCreateOut(ResourceCreateOut[MaaPlanRead]): + """计划创建响应模型""" + + +class PlanDetailOut(ResourceItemOut[MaaPlanRead]): + """计划详情响应模型""" + + +class PlanGetOut(ResourceCollectionOut[PlanIndexItem, MaaPlanRead]): + """计划列表响应模型""" __all__ = [ diff --git a/app/contracts/queue_contract.py b/app/contracts/queue_contract.py index cd218580..089bf000 100644 --- a/app/contracts/queue_contract.py +++ b/app/contracts/queue_contract.py @@ -58,17 +58,38 @@ class TimeSetIndexItem(ApiModel): type: Literal["TimeSet"] = Field(..., description="配置类型") -QueueCreateOut = ResourceCreateOut[QueueRead] -QueueDetailOut = ResourceItemOut[QueueRead] -QueueGetOut = ResourceCollectionOut[QueueIndexItem, QueueRead] +class QueueCreateOut(ResourceCreateOut[QueueRead]): + """队列创建响应模型""" -TimeSetCreateOut = ResourceCreateOut[TimeSetRead] -TimeSetDetailOut = ResourceItemOut[TimeSetRead] -TimeSetGetOut = ResourceCollectionOut[TimeSetIndexItem, TimeSetRead] -QueueItemCreateOut = ResourceCreateOut[QueueItemRead] -QueueItemDetailOut = ResourceItemOut[QueueItemRead] -QueueItemGetOut = ResourceCollectionOut[QueueItemIndexItem, QueueItemRead] +class QueueDetailOut(ResourceItemOut[QueueRead]): + """队列详情响应模型""" + + +class QueueGetOut(ResourceCollectionOut[QueueIndexItem, QueueRead]): + """队列列表响应模型""" + +class TimeSetCreateOut(ResourceCreateOut[TimeSetRead]): + """时间集创建响应模型""" + + +class TimeSetDetailOut(ResourceItemOut[TimeSetRead]): + """时间集详情响应模型""" + + +class TimeSetGetOut(ResourceCollectionOut[TimeSetIndexItem, TimeSetRead]): + """时间集列表响应模型""" + +class QueueItemCreateOut(ResourceCreateOut[QueueItemRead]): + """队列项创建响应模型""" + + +class QueueItemDetailOut(ResourceItemOut[QueueItemRead]): + """队列项详情响应模型""" + + +class QueueItemGetOut(ResourceCollectionOut[QueueItemIndexItem, QueueItemRead]): + """队列项列表响应模型""" __all__ = [ diff --git a/app/contracts/scripts_contract.py b/app/contracts/scripts_contract.py index 7c103b4d..3b18da41 100644 --- a/app/contracts/scripts_contract.py +++ b/app/contracts/scripts_contract.py @@ -111,9 +111,16 @@ class ScriptCreateIn(ApiModel): ) -ScriptCreateOut = ResourceCreateOut[ScriptReadData] -ScriptDetailOut = ResourceItemOut[ScriptReadData] -ScriptGetOut = ResourceCollectionOut[ScriptIndexItem, ScriptReadData] +class ScriptCreateOut(ResourceCreateOut[ScriptReadData]): + """脚本创建响应模型""" + + +class ScriptDetailOut(ResourceItemOut[ScriptReadData]): + """脚本详情响应模型""" + + +class ScriptGetOut(ResourceCollectionOut[ScriptIndexItem, ScriptReadData]): + """脚本列表响应模型""" class ScriptFileBody(ApiModel): @@ -130,9 +137,16 @@ class ScriptUploadBody(ApiModel): description: str = Field(..., description="描述") -UserGetOut = ResourceCollectionOut[UserIndexItem, UserReadData] -UserDetailOut = ResourceItemOut[UserReadData] -UserCreateOut = ResourceCreateOut[UserReadData] +class UserGetOut(ResourceCollectionOut[UserIndexItem, UserReadData]): + """用户列表响应模型""" + + +class UserDetailOut(ResourceItemOut[UserReadData]): + """用户详情响应模型""" + + +class UserCreateOut(ResourceCreateOut[UserReadData]): + """用户创建响应模型""" ScriptPatchData = Annotated[ MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig, diff --git a/app/contracts/setting_contract.py b/app/contracts/setting_contract.py index 22d349ad..302e58fe 100644 --- a/app/contracts/setting_contract.py +++ b/app/contracts/setting_contract.py @@ -39,10 +39,20 @@ class WebhookIndexItem(ApiModel): type: Literal["Webhook"] = Field(..., description="配置类型") -WebhookGetOut = ResourceCollectionOut[WebhookIndexItem, WebhookRead] -WebhookDetailOut = ResourceItemOut[WebhookRead] -WebhookCreateOut = ResourceCreateOut[WebhookRead] -SettingGetOut = ResourceItemOut[GlobalConfigRead] +class WebhookGetOut(ResourceCollectionOut[WebhookIndexItem, WebhookRead]): + """Webhook 列表响应模型""" + + +class WebhookDetailOut(ResourceItemOut[WebhookRead]): + """Webhook 详情响应模型""" + + +class WebhookCreateOut(ResourceCreateOut[WebhookRead]): + """Webhook 创建响应模型""" + + +class SettingGetOut(ResourceItemOut[GlobalConfigRead]): + """全局设置响应模型""" __all__ = [ diff --git a/app/contracts/tools_contract.py b/app/contracts/tools_contract.py index bb7d2cfd..9e883562 100644 --- a/app/contracts/tools_contract.py +++ b/app/contracts/tools_contract.py @@ -14,7 +14,8 @@ class ToolsConfigRead(_ToolsConfigBase): """工具配置读取/写入模型。""" -ToolsGetOut = ResourceItemOut[ToolsConfigRead] +class ToolsGetOut(ResourceItemOut[ToolsConfigRead]): + """工具配置响应模型""" __all__ = [ diff --git a/app/core/__init__.py b/app/core/__init__.py index 66e4a81d..2762570b 100644 --- a/app/core/__init__.py +++ b/app/core/__init__.py @@ -23,14 +23,12 @@ from typing import TYPE_CHECKING -from .broadcast import Broadcast -from .emulator_manager import EmulatorManager -from .task_manager import TaskManager -from .maa_manager import MaaFWManager - -from .timer import MainTimer - if TYPE_CHECKING: + from .broadcast import Broadcast as Broadcast + from .emulator_manager import EmulatorManager as EmulatorManager + from .task_manager import TaskManager as TaskManager + from .maa_manager import MaaFWManager as MaaFWManager + from .timer import MainTimer as MainTimer from .config import Config as Config __all__ = [ @@ -44,6 +42,26 @@ def __getattr__(name: str): + if name == "Broadcast": + from .broadcast import Broadcast + + return Broadcast + if name == "EmulatorManager": + from .emulator_manager import EmulatorManager + + return EmulatorManager + if name == "TaskManager": + from .task_manager import TaskManager + + return TaskManager + if name == "MaaFWManager": + from .maa_manager import MaaFWManager + + return MaaFWManager + if name == "MainTimer": + from .timer import MainTimer + + return MainTimer if name == "Config": from .config import Config diff --git a/app/core/config/manager.py b/app/core/config/manager.py index 1e46f8dc..b6c17bc6 100644 --- a/app/core/config/manager.py +++ b/app/core/config/manager.py @@ -44,6 +44,7 @@ from app.models.global_config import GlobalConfig from app.models.maa import MaaConfig, MaaPlanConfig, MaaUserConfig from app.models.maaend import MaaEndConfig, MaaEndUserConfig +from app.models.plugin import PluginInstanceConfig from app.models.src import SrcConfig, SrcUserConfig from .base import dump_toml from app.models.shared import WebSocketMessage @@ -139,6 +140,7 @@ async def _connect_runtime_configs(self) -> None: await self.EmulatorConfig.connect(self._resolve_config_path("EmulatorConfig")) await self.PlanConfig.connect(self._resolve_config_path("PlanConfig")) await self.ScriptConfig.connect(self._resolve_config_path("ScriptConfig")) + await self.PluginConfig.connect(self._resolve_config_path("PluginConfig")) await self.QueueConfig.connect(self._resolve_config_path("QueueConfig")) await self.ToolsConfig.connect(self._resolve_config_path("ToolsConfig")) @@ -1390,6 +1392,71 @@ async def reorder_webhook( .Notify_CustomWebhooks.setOrder(list(map(uuid.UUID, index_list))) ) + async def get_plugin( + self, plugin_id: Optional[str] + ) -> tuple[list[dict[str, str]], dict[str, Any]]: + """获取插件实例配置。""" + + logger.info(f"获取插件实例配置: {plugin_id}") + + if plugin_id is None: + data = await self.PluginConfig.toDict() + else: + data = await self.PluginConfig.get(uuid.UUID(plugin_id)) + + index = data.pop("instances", []) + return list(index), data + + async def add_plugin( + self, + plugin_name: str, + name: Optional[str] = None, + enabled: bool = True, + config: Optional[dict[str, Any]] = None, + ) -> tuple[uuid.UUID, PluginInstanceConfig]: + """创建插件实例配置。""" + + logger.info(f"创建插件实例配置: {plugin_name}") + uid, plugin_config = await self.PluginConfig.add(PluginInstanceConfig) + + await plugin_config.set_many( + { + "Info": { + "Plugin": plugin_name, + "Id": uid.hex[:5], + "Enabled": enabled, + "Name": name or f"{plugin_name} 实例", + }, + "Data": { + "Config": config or {}, + }, + } + ) + + return uid, plugin_config + + async def update_plugin( + self, + plugin_id: str, + data: Dict[str, Dict[str, Any]], + ) -> None: + """更新插件实例配置。""" + + logger.info(f"更新插件实例配置: {plugin_id}") + await self.PluginConfig[uuid.UUID(plugin_id)].set_many(data) + + async def del_plugin(self, plugin_id: str) -> None: + """删除插件实例配置。""" + + logger.info(f"删除插件实例配置: {plugin_id}") + await self.PluginConfig.remove(uuid.UUID(plugin_id)) + + async def reorder_plugin(self, index_list: list[str]) -> None: + """重排插件实例配置。""" + + logger.info(f"重排插件实例配置: {index_list}") + await self.PluginConfig.setOrder(list(map(uuid.UUID, index_list))) + @property def proxy(self) -> Optional[httpx.Proxy]: """获取代理设置,返回适用于 httpx 的代理对象""" diff --git a/app/core/config/shortcuts.py b/app/core/config/shortcuts.py index fb16974c..584dd35d 100644 --- a/app/core/config/shortcuts.py +++ b/app/core/config/shortcuts.py @@ -1,8 +1,10 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast +if TYPE_CHECKING: + _ClsT = TypeVar("_ClsT") from .base import MultipleConfig from .fields import ( OnDeleteCallback, @@ -207,34 +209,37 @@ def _init_related_targets(instance: Any) -> None: related[alias] = target -def config(cls: type[Any]) -> type[Any]: - """配置类总装饰器:在 ``model_post_init`` 阶段完成运行期装配。 +if TYPE_CHECKING: + def config(cls: _ClsT) -> _ClsT: ... # type: ignore[misc] +else: + def config(cls: type[Any]) -> type[Any]: + """配置类总装饰器:在 ``model_post_init`` 阶段完成运行期装配。 - 装配顺序: - 1. 初始化子配置(``_init_sub_configs``); - 2. 建立引用目标映射(``_init_related_targets``); - 3. 调用原有 ``model_post_init``(如果存在)。 + 装配顺序: + 1. 初始化子配置(``_init_sub_configs``); + 2. 建立引用目标映射(``_init_related_targets``); + 3. 调用原有 ``model_post_init``(如果存在)。 - 这样可以确保原有后置初始化逻辑执行时,子配置和引用关系已可用。 + 这样可以确保原有后置初始化逻辑执行时,子配置和引用关系已可用。 - Args: - cls: 被装饰的配置类。 + Args: + cls: 被装饰的配置类。 - Returns: - 注入初始化逻辑后的原类。 - """ + Returns: + 注入初始化逻辑后的原类。 + """ - original_model_post_init = getattr(cls, "model_post_init", None) + original_model_post_init = getattr(cls, "model_post_init", None) - def _model_post_init(self: Any, context: Any) -> None: - _init_sub_configs(self) - _init_related_targets(self) + def _model_post_init(self: Any, context: Any) -> None: + _init_sub_configs(self) + _init_related_targets(self) - if callable(original_model_post_init): - original_model_post_init(self, context) + if callable(original_model_post_init): + original_model_post_init(self, context) - setattr(cls, "model_post_init", _model_post_init) - return cls + setattr(cls, "model_post_init", _model_post_init) + return cls __all__ = [ diff --git a/app/core/task_manager.py b/app/core/task_manager.py index 82f7cded..a4417c8a 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -29,7 +29,6 @@ from app.services import System from app.models.task import TaskItem, ScriptItem, UserItem, TaskExecuteBase from app.utils import get_logger -from app.task import MaaManager, SrcManager, GeneralManager, MaaEndManager from app.utils.constants import POWER_SIGN_MAP @@ -134,6 +133,7 @@ async def main_task(self): script_item.status = "运行" logger.info(f"任务开始: {current_script_uid}") + from app.task import MaaManager, SrcManager, GeneralManager, MaaEndManager if isinstance(Config.ScriptConfig[current_script_uid], MaaConfig): task_item = MaaManager(script_item) elif isinstance(Config.ScriptConfig[current_script_uid], SrcConfig): diff --git a/app/core/timer.py b/app/core/timer.py index 4ad7b422..2bc60322 100644 --- a/app/core/timer.py +++ b/app/core/timer.py @@ -23,7 +23,6 @@ from datetime import datetime from app.services import Matomo -from app.MaaFW import ArknightWin32Toolkit from app.utils import get_logger from .config import Config from .task_manager import TaskManager @@ -69,6 +68,7 @@ async def second_task(self): await self.timed_start() if Config.ToolsConfig.get("ArknightsPC", "Enabled"): + from app.MaaFW import ArknightWin32Toolkit await ArknightWin32Toolkit.scheduled_task() await asyncio.sleep(1) diff --git a/app/models/__init__.py b/app/models/__init__.py index 182b2893..e25b548f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,10 +1,14 @@ -from . import emulator, task -from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook -from .general import GeneralConfig, GeneralUserConfig -from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig -from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig -from .maaend import MaaEndConfig, MaaEndUserConfig -from .src import SrcConfig, SrcUserConfig +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook + from .general import GeneralConfig, GeneralUserConfig + from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig + from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig + from .maaend import MaaEndConfig, MaaEndUserConfig + from .plugin import PluginInstanceConfig + from .src import SrcConfig, SrcUserConfig + __all__ = [ "EmulatorConfig", @@ -17,6 +21,7 @@ "MaaConfig", "MaaEndUserConfig", "MaaEndConfig", + "PluginInstanceConfig", "SrcUserConfig", "SrcConfig", "GeneralUserConfig", @@ -24,6 +29,77 @@ "ToolsConfig", "GlobalConfig", "CLASS_BOOK", - "emulator", - "task", ] + + +def __getattr__(name: str): + if name in {"emulator", "task"}: + import importlib + + return importlib.import_module(f"{__name__}.{name}") + + if name in { + "EmulatorConfig", + "QueueConfig", + "QueueItem", + "TimeSet", + "Webhook", + }: + from .common import EmulatorConfig, QueueConfig, QueueItem, TimeSet, Webhook + + return { + "EmulatorConfig": EmulatorConfig, + "QueueConfig": QueueConfig, + "QueueItem": QueueItem, + "TimeSet": TimeSet, + "Webhook": Webhook, + }[name] + + if name in {"GeneralConfig", "GeneralUserConfig"}: + from .general import GeneralConfig, GeneralUserConfig + + return { + "GeneralConfig": GeneralConfig, + "GeneralUserConfig": GeneralUserConfig, + }[name] + + if name in {"CLASS_BOOK", "GlobalConfig", "ToolsConfig"}: + from .global_config import CLASS_BOOK, GlobalConfig, ToolsConfig + + return { + "CLASS_BOOK": CLASS_BOOK, + "GlobalConfig": GlobalConfig, + "ToolsConfig": ToolsConfig, + }[name] + + if name in {"MaaConfig", "MaaPlanConfig", "MaaUserConfig"}: + from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig + + return { + "MaaConfig": MaaConfig, + "MaaPlanConfig": MaaPlanConfig, + "MaaUserConfig": MaaUserConfig, + }[name] + + if name in {"MaaEndConfig", "MaaEndUserConfig"}: + from .maaend import MaaEndConfig, MaaEndUserConfig + + return { + "MaaEndConfig": MaaEndConfig, + "MaaEndUserConfig": MaaEndUserConfig, + }[name] + + if name == "PluginInstanceConfig": + from .plugin import PluginInstanceConfig + + return PluginInstanceConfig + + if name in {"SrcConfig", "SrcUserConfig"}: + from .src import SrcConfig, SrcUserConfig + + return { + "SrcConfig": SrcConfig, + "SrcUserConfig": SrcUserConfig, + }[name] + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/models/global_config.py b/app/models/global_config.py index 7afd54aa..54d88947 100644 --- a/app/models/global_config.py +++ b/app/models/global_config.py @@ -25,6 +25,7 @@ from .general import GeneralConfig from .maa import MaaConfig, MaaPlanConfig, MaaUserConfig from .maaend import MaaEndConfig +from .plugin import PluginInstanceConfig from .src import SrcConfig @@ -175,6 +176,9 @@ def __init__(self, **data: Any): self.ScriptConfig: MultipleConfig[Any] = MultipleConfig( [MaaConfig, MaaEndConfig, SrcConfig, GeneralConfig] ) + self.PluginConfig: MultipleConfig[PluginInstanceConfig] = MultipleConfig( + [PluginInstanceConfig] + ) self.QueueConfig: MultipleConfig[QueueConfig] = MultipleConfig([QueueConfig]) self.ToolsConfig = ToolsConfig() diff --git a/app/services/matomo.py b/app/services/matomo.py index 56f7f915..4b8e7da1 100644 --- a/app/services/matomo.py +++ b/app/services/matomo.py @@ -28,7 +28,6 @@ import time from typing import Dict, Any, Optional -from app.core import Config from app.utils import get_logger logger = get_logger("信息上报") @@ -58,6 +57,7 @@ async def close(self): def _build_base_params(self, custom_vars: Optional[Dict[str, Any]] = None): """构建基础参数""" + from app.core import Config params = { "idsite": self.site_id, "rec": "1", diff --git a/app/services/notification.py b/app/services/notification.py index a234f3cf..1218f16b 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -33,7 +33,6 @@ from pathlib import Path from typing import Any, Literal, cast -from app.core import Config from app.models import Webhook from app.utils import get_logger, ImageUtils @@ -58,6 +57,7 @@ async def push_plyer(self, title: str, message: str, ticker: str, t: int) -> Non 通知持续时间 """ + from app.core import Config if not Config.get("Notify", "IfPushPlyer"): return @@ -94,6 +94,7 @@ async def send_mail( 收件人地址 """ + from app.core import Config if Config.get("Notify", "SMTPServerAddress") == "": raise ValueError("邮件通知的SMTP服务器地址不能为空") if Config.get("Notify", "AuthorizationCode") == "": @@ -177,6 +178,7 @@ async def ServerChanPush(self, title: str, content: str, send_key: str) -> None: params = {"title": title, "desp": content} headers = {"Content-Type": "application/json;charset=utf-8"} + from app.core import Config async with httpx.AsyncClient(proxy=Config.proxy) as client: response = await client.post(url, json=params, headers=headers) result = response.json() @@ -285,6 +287,7 @@ def replace_variables(obj: Any) -> Any: headers = {"Content-Type": "application/json"} headers.update(json.loads(webhook.get("Data", "Headers"))) + from app.core import Config async with httpx.AsyncClient(proxy=Config.proxy, timeout=10) as client: response: httpx.Response if webhook.get("Data", "Method") == "POST": @@ -345,6 +348,7 @@ async def _WebHookPush(self, title: str, content: str, webhook_url: str) -> None content = f"{title}\n{content}" data = {"msgtype": "text", "text": {"content": content}} + from app.core import Config async with httpx.AsyncClient(proxy=Config.proxy) as client: response = await client.post(url=webhook_url, json=data) info = response.json() @@ -383,6 +387,7 @@ async def CompanyWebHookBotPushImage( "image": {"base64": image_base64, "md5": image_md5}, } + from app.core import Config async with httpx.AsyncClient(proxy=Config.proxy) as client: response = await client.post(url=webhook_url, json=data) info = response.json() @@ -458,6 +463,7 @@ async def send_test_notification(self) -> None: ) # 发送邮件通知 + from app.core import Config if Config.get("Notify", "IfSendMail"): await self.send_mail( "文本", diff --git a/app/services/system.py b/app/services/system.py index 683ed9cd..d5917245 100644 --- a/app/services/system.py +++ b/app/services/system.py @@ -32,7 +32,6 @@ from pathlib import Path from typing import Literal, Optional -from app.core import Config from app.utils import ProcessRunner, get_logger logger = get_logger("系统服务") @@ -227,13 +226,15 @@ async def set_power( ["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"] ) - elif mode == "KillSelf" and Config.server is not None: - logger.info("执行退出主程序操作") - if not from_frontend: - await Config.send_websocket_message( - id="Main", type="Signal", data={"RequestClose": "请求前端关闭"} - ) - Config.server.should_exit = True + elif mode == "KillSelf": + from app.core import Config + if Config.server is not None: + logger.info("执行退出主程序操作") + if not from_frontend: + await Config.send_websocket_message( + id="Main", type="Signal", data={"RequestClose": "请求前端关闭"} + ) + Config.server.should_exit = True elif sys.platform.startswith("linux"): if mode == "NoAction": @@ -255,13 +256,15 @@ async def set_power( logger.info("执行睡眠操作") subprocess.run(["systemctl", "suspend"]) - elif mode == "KillSelf" and Config.server is not None: - logger.info("执行退出主程序操作") - if not from_frontend: - await Config.send_websocket_message( - id="Main", type="Signal", data={"RequestClose": "请求前端关闭"} - ) - Config.server.should_exit = True + elif mode == "KillSelf": + from app.core import Config + if Config.server is not None: + logger.info("执行退出主程序操作") + if not from_frontend: + await Config.send_websocket_message( + id="Main", type="Signal", data={"RequestClose": "请求前端关闭"} + ) + Config.server.should_exit = True async def _power_task( self, @@ -284,6 +287,7 @@ async def start_power_task(self): """开始电源任务""" if self.power_task is None or self.power_task.done(): + from app.core import Config self.power_task = asyncio.create_task(self._power_task(Config.power_sign)) logger.info( f"电源任务已启动, {self.countdown}秒后执行: {Config.power_sign}" diff --git a/app/services/update.py b/app/services/update.py index ebd3b04f..4f1275f2 100644 --- a/app/services/update.py +++ b/app/services/update.py @@ -40,7 +40,6 @@ wait_exponential, ) -from app.core import Config from app.utils.constants import MIRROR_ERROR_INFO from app.utils import ProcessRunner, get_logger from .system import System @@ -85,6 +84,7 @@ async def check_update( self.update_version_info, ) + from app.core import Config logger.info("开始检查更新") version_info: dict[str, Any] = {} @@ -150,6 +150,7 @@ async def check_update( async def download_update(self) -> None: """下载更新包并通过 WebSocket 上报进度。""" + from app.core import Config logger.info("收到前端下载请求") if self._operation_lock.locked(): @@ -306,6 +307,7 @@ async def _download_once() -> None: async def install_update(self): """解压并安装已下载的更新包。""" + from app.core import Config if self._operation_lock.locked(): await Config.send_websocket_message( id="Update", diff --git a/app/task/MAA/tools/notify.py b/app/task/MAA/tools/notify.py index c43236d4..21e66c11 100644 --- a/app/task/MAA/tools/notify.py +++ b/app/task/MAA/tools/notify.py @@ -22,7 +22,6 @@ from app.core import Config from app.services import Notify from app.utils import get_logger -from app.models import MaaUserConfig from typing import Any logger = get_logger("MAA 通知工具") @@ -32,7 +31,7 @@ async def push_notification( mode: str, title: str, message: dict[str, Any], - user_config: MaaUserConfig | None, + user_config: Any | None, ) -> None: """通过所有渠道推送通知""" @@ -70,7 +69,7 @@ async def push_notification( if Config.get("Notify", "IfKoishiSupport"): await Notify.send_koishi(f"{title}\n\n{message_text}\nAUTO-MAS 敬上") elif mode == "统计信息": - formatted = [] + formatted: list[str] = [] if "drop_statistics" in message: for stage, items in message["drop_statistics"].items(): formatted.append(f"掉落统计({stage}):") @@ -118,28 +117,29 @@ async def push_notification( and user_config.get("Notify", "Enabled") and user_config.get("Notify", "IfSendStatistic") ): - if user_config.get("Notify", "IfSendMail"): - if user_config.get("Notify", "ToAddress"): + user_config_any = user_config + if user_config_any.get("Notify", "IfSendMail"): + if user_config_any.get("Notify", "ToAddress"): await Notify.send_mail( "网页", title, message_html, - user_config.get("Notify", "ToAddress"), + user_config_any.get("Notify", "ToAddress"), ) else: logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知") - if user_config.get("Notify", "IfServerChan"): - if user_config.get("Notify", "ServerChanKey"): + if user_config_any.get("Notify", "IfServerChan"): + if user_config_any.get("Notify", "ServerChanKey"): await Notify.ServerChanPush( title, f"{serverchan_message}\nAUTO-MAS 敬上", - user_config.get("Notify", "ServerChanKey"), + user_config_any.get("Notify", "ServerChanKey"), ) else: logger.error( "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知" ) - for webhook in user_config.Notify_CustomWebhooks.values(): + for webhook in user_config_any.Notify_CustomWebhooks.values(): await Notify.WebhookPush( title, f"{message_text}\nAUTO-MAS 敬上", webhook ) @@ -168,26 +168,27 @@ async def push_notification( and user_config.get("Notify", "Enabled") and user_config.get("Notify", "IfSendSixStar") ): - if user_config.get("Notify", "IfSendMail"): - if user_config.get("Notify", "ToAddress"): + user_config_any = user_config + if user_config_any.get("Notify", "IfSendMail"): + if user_config_any.get("Notify", "ToAddress"): await Notify.send_mail( "网页", title, message_html, - user_config.get("Notify", "ToAddress"), + user_config_any.get("Notify", "ToAddress"), ) else: logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知") - if user_config.get("Notify", "IfServerChan"): - if user_config.get("Notify", "ServerChanKey"): + if user_config_any.get("Notify", "IfServerChan"): + if user_config_any.get("Notify", "ServerChanKey"): await Notify.ServerChanPush( title, "好羡慕~\nAUTO-MAS 敬上", - user_config.get("Notify", "ServerChanKey"), + user_config_any.get("Notify", "ServerChanKey"), ) else: logger.error( "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知" ) - for webhook in user_config.Notify_CustomWebhooks.values(): + for webhook in user_config_any.Notify_CustomWebhooks.values(): await Notify.WebhookPush(title, "好羡慕~\nAUTO-MAS 敬上", webhook) diff --git a/app/task/MaaEnd/tools/notify.py b/app/task/MaaEnd/tools/notify.py index c591b76f..be1a0ee4 100644 --- a/app/task/MaaEnd/tools/notify.py +++ b/app/task/MaaEnd/tools/notify.py @@ -18,10 +18,9 @@ # Contact: DLmaster_361@163.com -from typing import Any, cast +from typing import Any from app.core import Config -from app.models import MaaEndUserConfig from app.services import Notify from app.utils import get_logger @@ -32,11 +31,9 @@ async def push_notification( mode: str, title: str, message: dict[str, Any], - user_config: MaaEndUserConfig | None, + user_config: Any | None, ) -> None: """通过所有渠道推送通知。""" - - config = cast(Any, Config) logger.info(f"开始推送通知, 模式: {mode}, 标题: {title}") if mode == "代理结果" and ( @@ -51,7 +48,7 @@ async def push_notification( f"已完成数: {message['completed_count']}, 未完成数: {message['uncompleted_count']}\n\n" f"{message['result']}" ) - notify_env = config.notify_env + notify_env = Config.notify_env template = notify_env.get_template("general_result.html") message_html = str(template.render(message)) serverchan_message = message_text.replace("\n", "\n\n") @@ -81,7 +78,7 @@ async def push_notification( f"MaaEnd执行结果: {message['user_result']}\n\n" ) - notify_env = config.notify_env + notify_env = Config.notify_env template = notify_env.get_template("general_statistics.html") message_html = str(template.render(message)) serverchan_message = message_text.replace("\n", "\n\n") @@ -112,30 +109,31 @@ async def push_notification( and user_config.get("Notify", "Enabled") and user_config.get("Notify", "IfSendStatistic") ): - if user_config.get("Notify", "IfSendMail"): - if user_config.get("Notify", "ToAddress"): + user_config_any = user_config + if user_config_any.get("Notify", "IfSendMail"): + if user_config_any.get("Notify", "ToAddress"): await Notify.send_mail( "网页", title, message_html, - user_config.get("Notify", "ToAddress"), + user_config_any.get("Notify", "ToAddress"), ) else: logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知") - if user_config.get("Notify", "IfServerChan"): - if user_config.get("Notify", "ServerChanKey"): + if user_config_any.get("Notify", "IfServerChan"): + if user_config_any.get("Notify", "ServerChanKey"): await Notify.ServerChanPush( title, f"{serverchan_message}\n\nAUTO-MAS 敬上", - user_config.get("Notify", "ServerChanKey"), + user_config_any.get("Notify", "ServerChanKey"), ) else: logger.error( "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知" ) - for webhook in user_config.Notify_CustomWebhooks.values(): + for webhook in user_config_any.Notify_CustomWebhooks.values(): await Notify.WebhookPush( title, f"{message_text}\n\nAUTO-MAS 敬上", webhook ) diff --git a/app/task/SRC/tools/notify.py b/app/task/SRC/tools/notify.py index 8e9b3d37..df7acf69 100644 --- a/app/task/SRC/tools/notify.py +++ b/app/task/SRC/tools/notify.py @@ -21,7 +21,6 @@ from app.core import Config from app.services import Notify from app.utils import get_logger -from app.models import SrcUserConfig from typing import Any logger = get_logger("SRC通知工具") @@ -31,7 +30,7 @@ async def push_notification( mode: str, title: str, message: dict[str, Any], - user_config: SrcUserConfig | None, + user_config: Any | None, ) -> None: """通过所有渠道推送通知""" @@ -123,25 +122,26 @@ async def push_notification( and user_config.get("Notify", "Enabled") and user_config.get("Notify", "IfSendStatistic") ): + user_config_any = user_config # 发送邮件通知 - if user_config.get("Notify", "IfSendMail"): - if user_config.get("Notify", "ToAddress"): + if user_config_any.get("Notify", "IfSendMail"): + if user_config_any.get("Notify", "ToAddress"): await Notify.send_mail( "网页", title, message_html, - user_config.get("Notify", "ToAddress"), + user_config_any.get("Notify", "ToAddress"), ) else: logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知") # 发送ServerChan通知 - if user_config.get("Notify", "IfServerChan"): - if user_config.get("Notify", "ServerChanKey"): + if user_config_any.get("Notify", "IfServerChan"): + if user_config_any.get("Notify", "ServerChanKey"): await Notify.ServerChanPush( title, f"{serverchan_message}\n\nAUTO-MAS 敬上", - user_config.get("Notify", "ServerChanKey"), + user_config_any.get("Notify", "ServerChanKey"), ) else: logger.error( @@ -149,7 +149,7 @@ async def push_notification( ) # 推送CompanyWebHookBot通知 - for webhook in user_config.Notify_CustomWebhooks.values(): + for webhook in user_config_any.Notify_CustomWebhooks.values(): await Notify.WebhookPush( title, f"{message_text}\n\nAUTO-MAS 敬上", webhook ) diff --git a/app/task/general/tools/notify.py b/app/task/general/tools/notify.py index 057067dd..51bc47e4 100644 --- a/app/task/general/tools/notify.py +++ b/app/task/general/tools/notify.py @@ -22,7 +22,6 @@ from app.core import Config from app.services import Notify from app.utils import get_logger -from app.models import GeneralUserConfig from typing import Any logger = get_logger("通用通知工具") @@ -32,7 +31,7 @@ async def push_notification( mode: str, title: str, message: dict[str, Any], - user_config: GeneralUserConfig | None, + user_config: Any | None, ) -> None: """通过所有渠道推送通知""" diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 20fc155e..e5e8a607 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1,57 +1,43 @@ -# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software -# Copyright © 2024-2025 DLmaster361 -# Copyright © 2025 MoeSnowyFox -# Copyright © 2025-2026 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 Affero 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with AUTO-MAS. If not, see . - -# Contact: DLmaster_361@163.com - - -from . import constants -from .logger import get_logger -from .ImageUtils import ImageUtils -from .LogMonitor import LogMonitor, strptime -from .ProcessManager import ProcessManager, ProcessRunner, ProcessInfo, ProcessResult -from .security import dpapi_encrypt, dpapi_decrypt, sanitize_log_message -from .skland import skland_sign_in -from .emulator import MumuManager, LDManager, search_all_emulators, EMULATOR_TYPE_BOOK -from .tools import decode_bytes, busy_wait -from .websocket import WebSocketClient, create_ws_client - -__all__ = [ - "constants", - "get_logger", - "ImageUtils", - "LogMonitor", - "ProcessManager", - "ProcessRunner", - "ProcessInfo", - "ProcessResult", - "dpapi_encrypt", - "dpapi_decrypt", - "sanitize_log_message", - "skland_sign_in", - "strptime", - "MumuManager", - "LDManager", - "search_all_emulators", - "EMULATOR_TYPE_BOOK", - "decode_bytes", - "busy_wait", - "WebSocketClient", - "create_ws_client", -] +from __future__ import annotations + +from importlib import import_module +from typing import Any + + +_EXPORTS: dict[str, tuple[str, str]] = { + "constants": ("app.utils.constants", ""), + "get_logger": ("app.utils.logger", "get_logger"), + "ImageUtils": ("app.utils.ImageUtils", "ImageUtils"), + "LogMonitor": ("app.utils.LogMonitor", "LogMonitor"), + "strptime": ("app.utils.LogMonitor", "strptime"), + "ProcessManager": ("app.utils.ProcessManager", "ProcessManager"), + "ProcessRunner": ("app.utils.ProcessManager", "ProcessRunner"), + "ProcessInfo": ("app.utils.ProcessManager", "ProcessInfo"), + "ProcessResult": ("app.utils.ProcessManager", "ProcessResult"), + "dpapi_encrypt": ("app.utils.security", "dpapi_encrypt"), + "dpapi_decrypt": ("app.utils.security", "dpapi_decrypt"), + "sanitize_log_message": ("app.utils.security", "sanitize_log_message"), + "skland_sign_in": ("app.utils.skland", "skland_sign_in"), + "MumuManager": ("app.utils.emulator", "MumuManager"), + "LDManager": ("app.utils.emulator", "LDManager"), + "search_all_emulators": ("app.utils.emulator", "search_all_emulators"), + "EMULATOR_TYPE_BOOK": ("app.utils.emulator", "EMULATOR_TYPE_BOOK"), + "decode_bytes": ("app.utils.tools", "decode_bytes"), + "busy_wait": ("app.utils.tools", "busy_wait"), + "WebSocketClient": ("app.utils.websocket", "WebSocketClient"), + "create_ws_client": ("app.utils.websocket", "create_ws_client"), +} + + +__all__ = list(_EXPORTS) + + +def __getattr__(name: str) -> Any: + if name not in _EXPORTS: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + module_name, attr_name = _EXPORTS[name] + module = import_module(module_name) + value = module if not attr_name else getattr(module, attr_name) + globals()[name] = value + return value diff --git a/app/utils/emulator/general.py b/app/utils/emulator/general.py index c4a71127..a12777a2 100644 --- a/app/utils/emulator/general.py +++ b/app/utils/emulator/general.py @@ -28,11 +28,11 @@ import keyboard from datetime import datetime, timedelta from pathlib import Path -from typing import Dict, Any +from typing import Dict +from app.models.common import EmulatorConfig from app.utils.ProcessManager import ProcessManager from app.models.emulator import DeviceStatus, DeviceBase, DeviceInfo -from app.models import EmulatorConfig from app.utils import get_logger logger = get_logger("通用模拟器管理") @@ -44,16 +44,24 @@ class GeneralDeviceManager(DeviceBase): """ def __init__(self, config: EmulatorConfig) -> None: - if not Path(config.get("Info", "Path")).exists(): - raise FileNotFoundError(f"模拟器文件不存在: {config.get('Info', 'Path')}") + if not Path(str(config.get("Info", "Path"))).exists(): + raise FileNotFoundError( + f"模拟器文件不存在: {str(config.get('Info', 'Path'))}" + ) - if config.get("Info", "Type") != "general": + if str(config.get("Info", "Type")) != "general": raise ValueError("配置的模拟器类型不是通用类型") self.config = config - self.emulator_path = Path(config.get("Info", "Path")) + self.emulator_path = Path(str(config.get("Info", "Path"))) self.process_managers: Dict[str, ProcessManager] = {} - self.device_info: Dict[str, Dict[str, Any]] = {} + self.device_info: Dict[str, Dict[str, object]] = {} + + def _info_str(self, key: str) -> str: + return str(self.config.get("Info", key)) + + def _info_int(self, key: str) -> int: + return int(self.config.get("Info", key)) async def open(self, idx: str, package_name: str = "") -> DeviceInfo: # 检查是否已经在运行 @@ -72,7 +80,7 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: await self.process_managers[idx].open_process(self.emulator_path, *args) # 等待进程启动 - await asyncio.sleep(self.config.get("Info", "MaxWaitTime")) + await asyncio.sleep(self._info_int("MaxWaitTime")) return (await self.getInfo(idx))[idx] @@ -87,9 +95,7 @@ async def close(self, idx: str) -> DeviceStatus: # 等待进程完全停止 t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): if not await self.process_managers[idx].is_running(): return DeviceStatus.OFFLINE @@ -107,12 +113,12 @@ async def getStatus(self, idx: str) -> DeviceStatus: return DeviceStatus.OFFLINE async def getInfo(self, idx: str | None) -> Dict[str, DeviceInfo]: - data = {} + data: Dict[str, DeviceInfo] = {} for index in self.process_managers: if idx is not None and index != idx: continue data[index] = DeviceInfo( - title=f"{self.config.get('Info', 'Name')}_{index}", + title=f"{self._info_str('Name')}_{index}", status=await self.getStatus(index), adb_address=self.parse_index(index)[1], ) @@ -125,9 +131,7 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: return status t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): # 检查窗口可见性是否符合预期 if self.process_managers[idx].main_pid is not None and ( win32gui.IsWindowVisible(self.process_managers[idx].main_pid) @@ -138,8 +142,7 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: try: keyboard.press_and_release( "+".join( - _.strip().lower() - for _ in json.loads(self.config.get("Info", "BossKey")) + _.strip().lower() for _ in json.loads(self._info_str("BossKey")) ) ) # 老板键 except Exception as e: diff --git a/app/utils/emulator/ldplayer.py b/app/utils/emulator/ldplayer.py index 1c0df885..b208351c 100644 --- a/app/utils/emulator/ldplayer.py +++ b/app/utils/emulator/ldplayer.py @@ -29,8 +29,8 @@ from pydantic import BaseModel from pathlib import Path +from app.models.common import EmulatorConfig from app.models.emulator import DeviceStatus, DeviceInfo, DeviceBase -from app.models import EmulatorConfig from app.utils import ProcessRunner, get_logger logger = get_logger("雷电模拟器管理") @@ -55,26 +55,30 @@ class LDManager(DeviceBase): """ def __init__(self, config: EmulatorConfig) -> None: - if not Path(config.get("Info", "Path")).exists(): + if not Path(str(config.get("Info", "Path"))).exists(): raise FileNotFoundError( - f"LDPlayerManager.exe文件不存在: {config.get('Info', 'Path')}" + f"LDPlayerManager.exe文件不存在: {str(config.get('Info', 'Path'))}" ) - if config.get("Info", "Type") != "ldplayer": + if str(config.get("Info", "Type")) != "ldplayer": raise ValueError("配置的模拟器类型不是ldplayer") self.config = config - self.emulator_path = Path(config.get("Info", "Path")) + self.emulator_path = Path(str(config.get("Info", "Path"))) + + def _info_str(self, key: str) -> str: + return str(self.config.get("Info", key)) + + def _info_int(self, key: str) -> int: + return int(self.config.get("Info", key)) async def open(self, idx: str, package_name: str = "") -> DeviceInfo: logger.info(f"开始启动模拟器 {idx} - {package_name}") status = DeviceStatus.UNKNOWN # 初始化status变量 t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): status = await self.getStatus(idx) if status == DeviceStatus.ONLINE: return (await self.getInfo(idx))[idx] @@ -91,7 +95,7 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: "--index", idx, *(["--packagename", f'"{package_name}"'] if package_name else []), - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) # 参考命令 dnconsole.exe launch --index 0 @@ -100,15 +104,12 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: raise RuntimeError(f"命令执行失败: {result.stdout}") t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): status = await self.getStatus(idx) if status == DeviceStatus.ONLINE: await asyncio.sleep( 30 - if package_name != "" - and self.config.get("Info", "MaxWaitTime") > 60 + if package_name != "" and self._info_int("MaxWaitTime") > 60 else 3 ) # 等待模拟器的 ADB 等服务完全启动, 低性能设备额外等待应用启动 return (await self.getInfo(idx))[idx] @@ -130,7 +131,7 @@ async def close(self, idx: str) -> DeviceStatus: "quit", "--index", idx, - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) # 参考命令 dnconsole.exe quit --index 0 @@ -138,9 +139,7 @@ async def close(self, idx: str) -> DeviceStatus: if result.returncode != 0: raise RuntimeError(f"命令执行失败: {result.stdout}") t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): status = await self.getStatus(idx) if status == DeviceStatus.OFFLINE: return DeviceStatus.OFFLINE @@ -203,9 +202,7 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: result = (await self.get_device_info(idx))[idx] t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): # 检查窗口可见性是否符合预期 if win32gui.IsWindowVisible(result.top_hwnd) == is_visible: return status @@ -213,8 +210,7 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: try: keyboard.press_and_release( "+".join( - _.strip().lower() - for _ in json.loads(self.config.get("Info", "BossKey")) + _.strip().lower() for _ in json.loads(self._info_str("BossKey")) ) ) # 老板键 except Exception as e: @@ -231,7 +227,7 @@ async def get_device_info(self, idx: str | None) -> dict[str, LDPlayerDevice]: result = await ProcessRunner.run_process( self.emulator_path, "list2", - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) diff --git a/app/utils/emulator/mumu.py b/app/utils/emulator/mumu.py index 80245f99..f3749473 100644 --- a/app/utils/emulator/mumu.py +++ b/app/utils/emulator/mumu.py @@ -29,9 +29,10 @@ from contextlib import suppress from datetime import datetime, timedelta from pathlib import Path +from typing import Any, cast +from app.models.common import EmulatorConfig from app.models.emulator import DeviceStatus, DeviceInfo, DeviceBase -from app.models import EmulatorConfig from app.utils import ProcessRunner, get_logger @@ -44,28 +45,32 @@ class MumuManager(DeviceBase): """ def __init__(self, config: EmulatorConfig) -> None: - if not (Path(config.get("Info", "Path"))).exists(): + if not (Path(str(config.get("Info", "Path")))).exists(): raise FileNotFoundError( - f"MuMuManager.exe文件不存在: {config.get('Info', 'Path')}" + f"MuMuManager.exe文件不存在: {str(config.get('Info', 'Path'))}" ) - if config.get("Info", "Type") != "mumu": + if str(config.get("Info", "Type")) != "mumu": raise ValueError("配置的模拟器类型不是mumu") self.config = config - self.emulator_path = Path(config.get("Info", "Path")) + self.emulator_path = Path(str(config.get("Info", "Path"))) + + def _info_str(self, key: str) -> str: + return str(self.config.get("Info", key)) + + def _info_int(self, key: str) -> int: + return int(self.config.get("Info", key)) async def open(self, idx: str, package_name: str = "") -> DeviceInfo: logger.info(f"开始启动模拟器 {idx} - {package_name}") - from app.core import Config + from app.core.config import Config status = DeviceStatus.UNKNOWN # 初始化status变量 t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): status = await self.getStatus(idx) if status == DeviceStatus.ONLINE: return (await self.getInfo(idx))[idx] @@ -85,7 +90,7 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: idx, "launch", *(["-pkg", package_name] if package_name else []), - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) # 参考命令 MuMuManager.exe control -v 2 launch @@ -94,9 +99,7 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: raise RuntimeError(f"命令执行失败: {result.stdout}") t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): status = await self.getStatus(idx) if if_close_mumu_nx: if_close_mumu_nx = not await self.close_mumu_nx_window() @@ -105,8 +108,7 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: elif status == DeviceStatus.ONLINE: await asyncio.sleep( 30 - if package_name != "" - and self.config.get("Info", "MaxWaitTime") > 60 + if package_name != "" and self._info_int("MaxWaitTime") > 60 else 3 ) # 等待模拟器的 ADB 等服务完全启动, 低性能设备额外等待应用启动 return (await self.getInfo(idx))[idx] @@ -128,7 +130,7 @@ async def close(self, idx: str) -> DeviceStatus: "-v", idx, "shutdown", - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) # 参考命令 MuMuManager.exe control -v 2 shutdown @@ -137,9 +139,7 @@ async def close(self, idx: str) -> DeviceStatus: raise RuntimeError(f"命令执行失败: {result.stdout}") t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): + while datetime.now() - t < timedelta(seconds=self._info_int("MaxWaitTime")): status = await self.getStatus(idx) if status == DeviceStatus.OFFLINE: return DeviceStatus.OFFLINE @@ -158,14 +158,19 @@ async def getStatus(self, idx: str, data: str | None = None) -> DeviceStatus: logger.error(f"获取模拟器 {idx} 信息失败: {e}") return DeviceStatus.ERROR try: - data_json = json.loads(data) + parsed: Any = json.loads(data) except json.JSONDecodeError as e: logger.error(f"JSON解析错误: {e}") return DeviceStatus.UNKNOWN - if data_json["is_android_started"]: + if not isinstance(parsed, dict): + return DeviceStatus.UNKNOWN + + data_json = cast(dict[str, Any], parsed) + + if bool(data_json.get("is_android_started", False)): return DeviceStatus.ONLINE - elif data_json["is_process_started"]: + elif bool(data_json.get("is_process_started", False)): return DeviceStatus.STARTING else: return DeviceStatus.OFFLINE @@ -173,16 +178,18 @@ async def getStatus(self, idx: str, data: str | None = None) -> DeviceStatus: async def getInfo(self, idx: str | None) -> dict[str, DeviceInfo]: data = await self.get_device_info(idx or "all") - data_json = json.loads(data) + parsed: Any = json.loads(data) result: dict[str, DeviceInfo] = {} - if not data_json: + if not isinstance(parsed, dict) or not parsed: return result - if isinstance(data_json, dict) and "index" in data_json and "name" in data_json: - index = data_json["index"] - name = data_json["name"] + data_json = cast(dict[str, Any], parsed) + + if "index" in data_json and "name" in data_json: + index = str(data_json["index"]) + name = str(data_json["name"]) status = await self.getStatus(index, data) adb_address = ( f"{data_json.get('adb_host_ip')}:{data_json.get('adb_port')}" @@ -194,16 +201,23 @@ async def getInfo(self, idx: str | None) -> dict[str, DeviceInfo]: title=name, status=status, adb_address=adb_address ) - elif isinstance(data_json, dict): + else: for value in data_json.values(): - if isinstance(value, dict) and "index" in value and "name" in value: - index = value["index"] - name = value["name"] + value_map = ( + cast(dict[str, Any], value) if isinstance(value, dict) else None + ) + if ( + value_map is not None + and "index" in value_map + and "name" in value_map + ): + index = str(value_map["index"]) + name = str(value_map["name"]) status = await self.getStatus(index) adb_address = ( - f"{value.get('adb_host_ip')}:{value.get('adb_port')}" - if value.get("adb_host_ip", None) - and value.get("adb_port", None) + f"{value_map.get('adb_host_ip')}:{value_map.get('adb_port')}" + if value_map.get("adb_host_ip", None) + and value_map.get("adb_port", None) else "Unknown" ) result[index] = DeviceInfo( @@ -224,7 +238,7 @@ async def setVisible(self, idx: str, is_visible: bool) -> DeviceStatus: "-v", idx, "show_window" if is_visible else "hide_window", - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) if result.returncode != 0: @@ -238,7 +252,7 @@ async def get_device_info(self, idx: str) -> str: "info", "-v", idx, - timeout=self.config.get("Info", "MaxWaitTime"), + timeout=self._info_int("MaxWaitTime"), if_merge_std=True, ) if result.returncode != 0: diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 1e03225d..d95439c6 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -40,6 +40,26 @@ export default [ 'warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, ], + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: [ + '@/api/generated', + '@/api/generated/*', + '@/api/models', + '@/api/models/*', + '@/api/services', + '@/api/services/*', + '@/api/core', + '@/api/core/*', + ], + message: '请从 @/api 顶层出口导入,不要直接依赖生成层或旧目录', + }, + ], + }, + ], }, }, @@ -109,7 +129,7 @@ export default [ '**/*.d.ts', '**/*.js', // 忽略自动生成的 API 文件 - 'src/api/*', + 'src/api/generated/**', ], }, ] diff --git a/frontend/openapi-ts.config.mjs b/frontend/openapi-ts.config.mjs new file mode 100644 index 00000000..08127404 --- /dev/null +++ b/frontend/openapi-ts.config.mjs @@ -0,0 +1,12 @@ +import { defineConfig } from '@hey-api/openapi-ts' + +export default defineConfig({ + input: './openapi.json', + output: './src/api/generated', + client: 'legacy/axios', + useOptions: false, + exportCore: true, + exportServices: true, + exportModels: true, + exportSchemas: false, +}) diff --git a/frontend/openapi.json b/frontend/openapi.json new file mode 100644 index 00000000..19e4d0c3 --- /dev/null +++ b/frontend/openapi.json @@ -0,0 +1,12121 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "AUTO-MAS", + "description": "API for managing automation scripts, plans, and tasks", + "version": "1.0.0" + }, + "paths": { + "/api/core/close": { + "post": { + "tags": [ + "核心信息" + ], + "summary": "关闭后端程序", + "description": "关闭后端程序", + "operationId": "close_api_core_close_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + } + } + } + }, + "/api/info/version": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取后端git版本信息", + "operationId": "get_git_version_api_info_version_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionOut" + } + } + } + } + } + } + }, + "/api/info/combox/stage": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取关卡号下拉框信息", + "operationId": "get_stage_combox_api_info_combox_stage_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetStageIn", + "description": "关卡号类型" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/info/combox/script": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取脚本下拉框信息", + "operationId": "get_script_combox_api_info_combox_script_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + } + } + } + }, + "/api/info/combox/task": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取可选任务下拉框信息", + "operationId": "get_task_combox_api_info_combox_task_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + } + } + } + }, + "/api/info/combox/plan": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取可选计划下拉框信息", + "operationId": "get_plan_combox_api_info_combox_plan_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + } + } + } + }, + "/api/info/combox/emulator": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取可选模拟器下拉框信息", + "operationId": "get_emulator_combox_api_info_combox_emulator_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + } + } + } + }, + "/api/info/combox/emulator/devices": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取可选模拟器多开实例下拉框信息", + "operationId": "get_emulator_devices_combox_api_info_combox_emulator_devices_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorIdBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/info/notice/get": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取通知信息", + "operationId": "get_notice_info_api_info_notice_get_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoticeOut" + } + } + } + } + } + } + }, + "/api/info/notice/confirm": { + "post": { + "tags": [ + "信息获取", + "Action" + ], + "summary": "确认通知", + "operationId": "confirm_notice_api_info_notice_confirm_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + } + } + } + }, + "/api/info/webconfig": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "获取配置分享中心的配置信息", + "operationId": "get_web_config_api_info_webconfig_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InfoOut" + } + } + } + } + } + } + }, + "/api/info/get/overview": { + "post": { + "tags": [ + "信息获取", + "Get" + ], + "summary": "信息总览", + "operationId": "get_overview_api_info_get_overview_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InfoOut" + } + } + } + } + } + } + }, + "/api/scripts": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "查询全部脚本", + "operationId": "list_scripts_api_scripts_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptGetOut" + } + } + } + } + } + }, + "post": { + "tags": [ + "脚本管理", + "Add" + ], + "summary": "创建脚本", + "operationId": "create_script_api_scripts_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptCreateIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/order": { + "patch": { + "tags": [ + "脚本管理", + "Update" + ], + "summary": "重新排序脚本", + "operationId": "reorder_scripts_api_scripts_order_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "查询单个脚本", + "operationId": "get_script_api_scripts__script_id__get", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "脚本管理", + "Update" + ], + "summary": "更新脚本配置", + "operationId": "update_script_api_scripts__script_id__patch", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptPatchBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "脚本管理", + "Delete" + ], + "summary": "删除脚本", + "operationId": "delete_script_api_scripts__script_id__delete", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/actions/import-file": { + "post": { + "tags": [ + "脚本管理", + "Action" + ], + "summary": "从文件导入脚本配置", + "operationId": "import_script_from_file_api_scripts__script_id__actions_import_file_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptFileBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/actions/export-file": { + "post": { + "tags": [ + "脚本管理", + "Action" + ], + "summary": "导出脚本配置到文件", + "operationId": "export_script_to_file_api_scripts__script_id__actions_export_file_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptFileBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/actions/import-web": { + "post": { + "tags": [ + "脚本管理", + "Action" + ], + "summary": "从网络导入脚本配置", + "operationId": "import_script_from_web_api_scripts__script_id__actions_import_web_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptUrlBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/actions/upload-web": { + "post": { + "tags": [ + "脚本管理", + "Action" + ], + "summary": "上传脚本配置到网络", + "operationId": "upload_script_to_web_api_scripts__script_id__actions_upload_web_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScriptUploadBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "查询脚本下的全部用户", + "operationId": "list_users_api_scripts__script_id__users_get", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGetOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "脚本管理", + "Add" + ], + "summary": "创建用户", + "operationId": "create_user_api_scripts__script_id__users_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/order": { + "patch": { + "tags": [ + "脚本管理", + "Update" + ], + "summary": "重新排序用户", + "operationId": "reorder_users_api_scripts__script_id__users_order_patch", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/{user_id}": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "查询单个用户", + "operationId": "get_user_api_scripts__script_id__users__user_id__get", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "脚本管理", + "Update" + ], + "summary": "更新用户配置", + "operationId": "update_user_api_scripts__script_id__users__user_id__patch", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPatchBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "脚本管理", + "Delete" + ], + "summary": "删除用户", + "operationId": "delete_user_api_scripts__script_id__users__user_id__delete", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/{user_id}/actions/import-infrastructure": { + "post": { + "tags": [ + "脚本管理", + "Action" + ], + "summary": "导入基建配置文件", + "operationId": "import_infrastructure_api_scripts__script_id__users__user_id__actions_import_infrastructure_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InfrastructureImportBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/{user_id}/infrastructure-options": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "用户自定义基建排班可选项", + "operationId": "get_user_infrastructure_options_api_scripts__script_id__users__user_id__infrastructure_options_get", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComboBoxOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/{user_id}/webhooks": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "查询用户下的全部 Webhook", + "operationId": "list_user_webhooks_api_scripts__script_id__users__user_id__webhooks_get", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookGetOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "脚本管理", + "Add" + ], + "summary": "创建用户 Webhook", + "operationId": "create_user_webhook_api_scripts__script_id__users__user_id__webhooks_post", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/{user_id}/webhooks/order": { + "patch": { + "tags": [ + "脚本管理", + "Update" + ], + "summary": "重新排序用户 Webhook", + "operationId": "reorder_user_webhooks_api_scripts__script_id__users__user_id__webhooks_order_patch", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}": { + "get": { + "tags": [ + "脚本管理", + "Get" + ], + "summary": "查询单个用户 Webhook", + "operationId": "get_user_webhook_api_scripts__script_id__users__user_id__webhooks__webhook_id__get", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + }, + { + "name": "webhook_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Webhook ID", + "title": "Webhook Id" + }, + "description": "Webhook ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "脚本管理", + "Update" + ], + "summary": "更新用户 Webhook", + "operationId": "update_user_webhook_api_scripts__script_id__users__user_id__webhooks__webhook_id__patch", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + }, + { + "name": "webhook_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Webhook ID", + "title": "Webhook Id" + }, + "description": "Webhook ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookRead" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "脚本管理", + "Delete" + ], + "summary": "删除用户 Webhook", + "operationId": "delete_user_webhook_api_scripts__script_id__users__user_id__webhooks__webhook_id__delete", + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "脚本 ID", + "title": "Script Id" + }, + "description": "脚本 ID" + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "用户 ID", + "title": "User Id" + }, + "description": "用户 ID" + }, + { + "name": "webhook_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Webhook ID", + "title": "Webhook Id" + }, + "description": "Webhook ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/plan": { + "get": { + "tags": [ + "计划管理", + "Get" + ], + "summary": "查询全部计划表", + "operationId": "list_plans_api_plan_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGetOut" + } + } + } + } + } + }, + "post": { + "tags": [ + "计划管理", + "Add" + ], + "summary": "创建计划表", + "operationId": "create_plan_api_plan_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanCreateIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/plan/{plan_id}": { + "get": { + "tags": [ + "计划管理", + "Get" + ], + "summary": "查询单个计划表", + "operationId": "get_plan_api_plan__plan_id__get", + "parameters": [ + { + "name": "plan_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "计划 ID", + "title": "Plan Id" + }, + "description": "计划 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "计划管理", + "Update" + ], + "summary": "更新计划表", + "operationId": "update_plan_api_plan__plan_id__patch", + "parameters": [ + { + "name": "plan_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "计划 ID", + "title": "Plan Id" + }, + "description": "计划 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanUpdateBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "计划管理", + "Delete" + ], + "summary": "删除计划表", + "operationId": "delete_plan_api_plan__plan_id__delete", + "parameters": [ + { + "name": "plan_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "计划 ID", + "title": "Plan Id" + }, + "description": "计划 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/plan/order": { + "patch": { + "tags": [ + "计划管理", + "Update" + ], + "summary": "重新排序计划表", + "operationId": "reorder_plan_api_plan_order_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/emulator": { + "get": { + "tags": [ + "模拟器管理", + "Get" + ], + "summary": "查询全部模拟器配置", + "operationId": "list_emulators_api_emulator_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorGetOut" + } + } + } + } + } + }, + "post": { + "tags": [ + "模拟器管理", + "Add" + ], + "summary": "创建模拟器配置", + "operationId": "create_emulator_api_emulator_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorCreateOut" + } + } + } + } + } + } + }, + "/api/emulator/order": { + "patch": { + "tags": [ + "模拟器管理", + "Update" + ], + "summary": "重新排序模拟器", + "operationId": "reorder_emulator_api_emulator_order_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/emulator/detected": { + "get": { + "tags": [ + "模拟器管理", + "Get" + ], + "summary": "搜索已安装的模拟器", + "operationId": "detect_emulators_api_emulator_detected_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorSearchOut" + } + } + } + } + } + } + }, + "/api/emulator/status": { + "get": { + "tags": [ + "模拟器管理", + "Get" + ], + "summary": "查询全部模拟器状态", + "operationId": "get_emulator_statuses_api_emulator_status_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorStatusOut" + } + } + } + } + } + } + }, + "/api/emulator/{emulator_id}": { + "get": { + "tags": [ + "模拟器管理", + "Get" + ], + "summary": "查询单个模拟器配置", + "operationId": "get_emulator_api_emulator__emulator_id__get", + "parameters": [ + { + "name": "emulator_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "模拟器 ID", + "title": "Emulator Id" + }, + "description": "模拟器 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "模拟器管理", + "Update" + ], + "summary": "更新模拟器配置", + "operationId": "update_emulator_api_emulator__emulator_id__patch", + "parameters": [ + { + "name": "emulator_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "模拟器 ID", + "title": "Emulator Id" + }, + "description": "模拟器 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorRead" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "模拟器管理", + "Delete" + ], + "summary": "删除模拟器配置", + "operationId": "delete_emulator_api_emulator__emulator_id__delete", + "parameters": [ + { + "name": "emulator_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "模拟器 ID", + "title": "Emulator Id" + }, + "description": "模拟器 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/emulator/{emulator_id}/status": { + "get": { + "tags": [ + "模拟器管理", + "Get" + ], + "summary": "查询单个模拟器状态", + "operationId": "get_emulator_status_api_emulator__emulator_id__status_get", + "parameters": [ + { + "name": "emulator_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "模拟器 ID", + "title": "Emulator Id" + }, + "description": "模拟器 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorDeviceStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/emulator/{emulator_id}/actions/{action}": { + "post": { + "tags": [ + "模拟器管理", + "Action" + ], + "summary": "执行模拟器动作", + "operationId": "operate_emulator_api_emulator__emulator_id__actions__action__post", + "parameters": [ + { + "name": "emulator_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "模拟器 ID", + "title": "Emulator Id" + }, + "description": "模拟器 ID" + }, + { + "name": "action", + "in": "path", + "required": true, + "schema": { + "enum": [ + "open", + "close", + "show" + ], + "type": "string", + "description": "模拟器动作", + "title": "Action" + }, + "description": "模拟器动作" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmulatorActionBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue": { + "get": { + "tags": [ + "调度队列管理", + "Get" + ], + "summary": "查询全部调度队列", + "operationId": "list_queues_api_queue_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueGetOut" + } + } + } + } + } + }, + "post": { + "tags": [ + "调度队列管理", + "Add" + ], + "summary": "创建调度队列", + "operationId": "create_queue_api_queue_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueCreateOut" + } + } + } + } + } + } + }, + "/api/queue/order": { + "patch": { + "tags": [ + "调度队列管理", + "Update" + ], + "summary": "重新排序调度队列", + "operationId": "reorder_queue_api_queue_order_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}": { + "get": { + "tags": [ + "调度队列管理", + "Get" + ], + "summary": "查询单个调度队列", + "operationId": "get_queue_api_queue__queue_id__get", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "调度队列管理", + "Update" + ], + "summary": "更新调度队列", + "operationId": "update_queue_api_queue__queue_id__patch", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueRead" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "调度队列管理", + "Delete" + ], + "summary": "删除调度队列", + "operationId": "delete_queue_api_queue__queue_id__delete", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}/times": { + "get": { + "tags": [ + "调度队列管理", + "Get" + ], + "summary": "查询队列下的全部定时项", + "operationId": "list_time_sets_api_queue__queue_id__times_get", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeSetGetOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "调度队列管理", + "Add" + ], + "summary": "创建定时项", + "operationId": "create_time_set_api_queue__queue_id__times_post", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeSetCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}/times/order": { + "patch": { + "tags": [ + "调度队列管理", + "Update" + ], + "summary": "重新排序定时项", + "operationId": "reorder_time_sets_api_queue__queue_id__times_order_patch", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}/times/{time_set_id}": { + "get": { + "tags": [ + "调度队列管理", + "Get" + ], + "summary": "查询单个定时项", + "operationId": "get_time_set_api_queue__queue_id__times__time_set_id__get", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + }, + { + "name": "time_set_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "时间设置 ID", + "title": "Time Set Id" + }, + "description": "时间设置 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeSetDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "调度队列管理", + "Update" + ], + "summary": "更新定时项", + "operationId": "update_time_set_api_queue__queue_id__times__time_set_id__patch", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + }, + { + "name": "time_set_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "时间设置 ID", + "title": "Time Set Id" + }, + "description": "时间设置 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeSetRead" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "调度队列管理", + "Delete" + ], + "summary": "删除定时项", + "operationId": "delete_time_set_api_queue__queue_id__times__time_set_id__delete", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + }, + { + "name": "time_set_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "时间设置 ID", + "title": "Time Set Id" + }, + "description": "时间设置 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}/items": { + "get": { + "tags": [ + "调度队列管理", + "Get" + ], + "summary": "查询队列下的全部队列项", + "operationId": "list_queue_items_api_queue__queue_id__items_get", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueItemGetOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "调度队列管理", + "Add" + ], + "summary": "创建队列项", + "operationId": "create_queue_item_api_queue__queue_id__items_post", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueItemCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}/items/order": { + "patch": { + "tags": [ + "调度队列管理", + "Update" + ], + "summary": "重新排序队列项", + "operationId": "reorder_queue_items_api_queue__queue_id__items_order_patch", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/queue/{queue_id}/items/{queue_item_id}": { + "get": { + "tags": [ + "调度队列管理", + "Get" + ], + "summary": "查询单个队列项", + "operationId": "get_queue_item_api_queue__queue_id__items__queue_item_id__get", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + }, + { + "name": "queue_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列项 ID", + "title": "Queue Item Id" + }, + "description": "队列项 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueItemDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "调度队列管理", + "Update" + ], + "summary": "更新队列项", + "operationId": "update_queue_item_api_queue__queue_id__items__queue_item_id__patch", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + }, + { + "name": "queue_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列项 ID", + "title": "Queue Item Id" + }, + "description": "队列项 ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueItemRead" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "调度队列管理", + "Delete" + ], + "summary": "删除队列项", + "operationId": "delete_queue_item_api_queue__queue_id__items__queue_item_id__delete", + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列 ID", + "title": "Queue Id" + }, + "description": "队列 ID" + }, + { + "name": "queue_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "队列项 ID", + "title": "Queue Item Id" + }, + "description": "队列项 ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/dispatch/start": { + "post": { + "tags": [ + "任务调度", + "Action" + ], + "summary": "添加任务", + "operationId": "add_task_api_dispatch_start_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskCreateIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/dispatch/stop": { + "post": { + "tags": [ + "任务调度", + "Action" + ], + "summary": "中止任务", + "operationId": "stop_task_api_dispatch_stop_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DispatchIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/dispatch/get/power": { + "post": { + "tags": [ + "任务调度", + "Get" + ], + "summary": "获取电源标志", + "operationId": "get_power_api_dispatch_get_power_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PowerOut" + } + } + } + } + } + } + }, + "/api/dispatch/set/power": { + "post": { + "tags": [ + "任务调度", + "Action" + ], + "summary": "设置电源标志", + "operationId": "set_power_api_dispatch_set_power_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PowerIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/dispatch/cancel/power": { + "post": { + "tags": [ + "任务调度", + "Action" + ], + "summary": "取消电源任务", + "operationId": "cancel_power_task_api_dispatch_cancel_power_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + } + } + } + }, + "/api/history/search": { + "post": { + "tags": [ + "历史记录", + "Get" + ], + "summary": "搜索历史记录总览信息", + "operationId": "search_history_api_history_search_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistorySearchIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistorySearchOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/history/data": { + "post": { + "tags": [ + "历史记录", + "Get" + ], + "summary": "从指定文件内获取历史记录数据", + "operationId": "get_history_data_api_history_data_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryDataGetIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryDataGetOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/tools": { + "get": { + "tags": [ + "工具设置", + "Get" + ], + "summary": "查询工具配置", + "operationId": "get_tools_api_tools_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolsGetOut" + } + } + } + } + } + }, + "patch": { + "tags": [ + "工具设置", + "Update" + ], + "summary": "更新工具配置", + "operationId": "update_tools_api_tools_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolsConfigRead" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/setting": { + "get": { + "tags": [ + "全局设置", + "Get" + ], + "summary": "查询全局配置", + "operationId": "get_setting_api_setting_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SettingGetOut" + } + } + } + } + } + }, + "patch": { + "tags": [ + "全局设置", + "Update" + ], + "summary": "更新全局配置", + "operationId": "update_setting_api_setting_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalConfigRead" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/setting/actions/test-notify": { + "post": { + "tags": [ + "全局设置", + "Action" + ], + "summary": "测试通知", + "operationId": "test_notify_api_setting_actions_test_notify_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + } + } + } + }, + "/api/setting/webhooks": { + "get": { + "tags": [ + "全局设置", + "Get" + ], + "summary": "查询全部全局 Webhook 配置", + "operationId": "list_webhooks_api_setting_webhooks_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookGetOut" + } + } + } + } + } + }, + "post": { + "tags": [ + "全局设置", + "Add" + ], + "summary": "创建全局 Webhook 配置", + "operationId": "create_webhook_api_setting_webhooks_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookCreateOut" + } + } + } + } + } + } + }, + "/api/setting/webhooks/order": { + "patch": { + "tags": [ + "全局设置", + "Update" + ], + "summary": "重新排序全局 Webhook", + "operationId": "reorder_webhooks_api_setting_webhooks_order_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexOrderPatch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/setting/webhooks/test": { + "post": { + "tags": [ + "全局设置", + "Action" + ], + "summary": "测试指定 Webhook 配置", + "operationId": "test_webhook_api_setting_webhooks_test_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookRead" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/setting/webhooks/{webhook_id}": { + "get": { + "tags": [ + "全局设置", + "Get" + ], + "summary": "查询单个全局 Webhook 配置", + "operationId": "get_webhook_api_setting_webhooks__webhook_id__get", + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Webhook ID", + "title": "Webhook Id" + }, + "description": "Webhook ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDetailOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "全局设置", + "Update" + ], + "summary": "更新全局 Webhook 配置", + "operationId": "update_webhook_api_setting_webhooks__webhook_id__patch", + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Webhook ID", + "title": "Webhook Id" + }, + "description": "Webhook ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookRead" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "全局设置", + "Delete" + ], + "summary": "删除全局 Webhook 配置", + "operationId": "delete_webhook_api_setting_webhooks__webhook_id__delete", + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Webhook ID", + "title": "Webhook Id" + }, + "description": "Webhook ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/update/check": { + "post": { + "tags": [ + "软件更新", + "Get" + ], + "summary": "检查更新", + "operationId": "check_update_api_update_check_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCheckIn" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCheckOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "软件更新", + "Get" + ], + "summary": "按 REST 风格检查更新", + "operationId": "check_update_rest_api_update_check_get", + "parameters": [ + { + "name": "current_version", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Current Version" + } + }, + { + "name": "if_force", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "If Force" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCheckOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/update/download": { + "post": { + "tags": [ + "软件更新", + "Action" + ], + "summary": "下载更新", + "operationId": "download_update_api_update_download_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + } + } + } + }, + "/api/update/install": { + "post": { + "tags": [ + "软件更新", + "Action" + ], + "summary": "安装更新", + "operationId": "install_update_api_update_install_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutBase" + } + } + } + } + } + } + }, + "/api/ocr/screenshot": { + "post": { + "tags": [ + "OCR识别", + "Get" + ], + "summary": "获取窗口截图", + "description": "根据窗口标题获取截图,返回Base64编码的图像数据\n\nArgs:\n params: 截图参数\n - window_title: 窗口标题关键字\n - should_preprocess: 是否预处理图片区域(默认True)\n - aspect_ratio_width: 宽高比宽度(默认16)\n - aspect_ratio_height: 宽高比高度(默认9)\n - region: 自定义截图区域,格式为 (left, top, width, height)\n\nReturns:\n OCRScreenshotOut: 包含Base64编码的截图和区域信息", + "operationId": "get_screenshot_api_ocr_screenshot_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OCRScreenshotIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OCRScreenshotOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ocr/screenshot/adb": { + "post": { + "tags": [ + "OCR识别", + "Get" + ], + "summary": "通过ADB获取设备截图", + "description": "通过 ADB 端口获取 Android 设备/模拟器截图,返回Base64编码的图像数据\n\n支持两种截图方法:\n1. screencap PNG 方法(推荐):速度快,直接获取 PNG 图像\n2. screencap raw 方法:获取原始像素数据,适用于某些不支持 PNG 的设备\n\nArgs:\n params: ADB 截图参数\n - adb_path: ADB 可执行文件的路径\n - serial: 设备序列号,格式如 \"127.0.0.1:5555\" 或 \"emulator-5554\"\n - use_screencap: 是否使用 screencap PNG 方法(默认True)\n\nReturns:\n ADBScreenshotOut: 包含Base64编码的截图和设备信息", + "operationId": "get_screenshot_adb_api_ocr_screenshot_adb_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ADBScreenshotIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ADBScreenshotOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ocr/check/image": { + "post": { + "tags": [ + "OCR识别", + "Get" + ], + "summary": "检查是否存在指定图像", + "description": "截图并查找是否存在图片内的内容\n\nArgs:\n params: 检查图像参数\n - window_title: 窗口标题关键字\n - image_path: 要查找的图片路径\n - interval: 截图间隔时间(秒),默认为 0\n - retry_times: 重复截图次数,默认为 1\n - threshold: 图像匹配阈值,范围 0-1,默认 0.8\n\nReturns:\n CheckImageOut: 包含查找结果和尝试次数", + "operationId": "check_image_api_ocr_check_image_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImageIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImageOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ocr/check/image/any": { + "post": { + "tags": [ + "OCR识别", + "Get" + ], + "summary": "检查是否存在任意一个指定图像", + "description": "截图并查找是否存在列表中任意一张图片的内容\n\nArgs:\n params: 检查图像参数\n - window_title: 窗口标题关键字\n - image_paths: 要查找的图片路径列表\n - interval: 截图间隔时间(秒),默认为 0\n - retry_times: 重复截图次数,默认为 1\n - threshold: 图像匹配阈值,范围 0-1,默认 0.8\n\nReturns:\n CheckImageOut: 包含查找结果和尝试次数", + "operationId": "check_image_any_api_ocr_check_image_any_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImageAnyIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImageOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ocr/check/image/all": { + "post": { + "tags": [ + "OCR识别", + "Get" + ], + "summary": "检查是否存在所有指定图像", + "description": "截图并查找是否存在列表中所有图片的内容\n\nArgs:\n params: 检查图像参数\n - window_title: 窗口标题关键字\n - image_paths: 要查找的图片路径列表\n - interval: 截图间隔时间(秒),默认为 0\n - retry_times: 重复截图次数,默认为 1\n - threshold: 图像匹配阈值,范围 0-1,默认 0.8\n\nReturns:\n CheckImageOut: 包含查找结果和尝试次数", + "operationId": "check_image_all_api_ocr_check_image_all_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImageAllIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImageOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ocr/click/image": { + "post": { + "tags": [ + "OCR识别", + "Action" + ], + "summary": "点击指定图像位置", + "description": "截图、查找并点击与图像一致的位置\n\nArgs:\n params: 点击图像参数\n - window_title: 窗口标题关键字\n - image_path: 要查找并点击的图片路径\n - interval: 截图间隔时间(秒),默认为 0\n - retry_times: 重复截图次数,默认为 1\n - threshold: 图像匹配阈值,范围 0-1,默认 0.8\n\nReturns:\n ClickOut: 包含点击结果和尝试次数", + "operationId": "click_image_api_ocr_click_image_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClickImageIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClickOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ocr/click/text": { + "post": { + "tags": [ + "OCR识别", + "Action" + ], + "summary": "点击指定文字位置", + "description": "截图、OCR识别并点击与文字一致的位置\n\nArgs:\n params: 点击文字参数\n - window_title: 窗口标题关键字\n - text: 要查找并点击的文字内容\n - interval: 截图间隔时间(秒),默认为 0\n - retry_times: 重复截图次数,默认为 1\n\nReturns:\n ClickOut: 包含点击结果和尝试次数", + "operationId": "click_text_api_ocr_click_text_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClickTextIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClickOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/client/create": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "创建 WebSocket 客户端", + "description": "创建一个新的 WebSocket 客户端实例\n\n- **name**: 客户端唯一名称\n- **url**: WebSocket 服务器地址\n- **ping_interval**: 心跳发送间隔\n- **ping_timeout**: 心跳超时时间\n- **reconnect_interval**: 重连间隔\n- **max_reconnect_attempts**: 最大重连次数", + "operationId": "create_client_api_ws_debug_client_create_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientCreateIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientCreateOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/client/connect": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "连接 WebSocket 客户端", + "description": "启动指定客户端的连接(非阻塞)", + "operationId": "connect_client_api_ws_debug_client_connect_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientConnectIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/client/disconnect": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "断开 WebSocket 客户端", + "description": "断开指定客户端的连接", + "operationId": "disconnect_client_api_ws_debug_client_disconnect_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientDisconnectIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/client/remove": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "删除 WebSocket 客户端", + "description": "删除指定客户端(会自动断开连接)\n\n注意:系统客户端(如 Koishi)不可删除", + "operationId": "remove_client_api_ws_debug_client_remove_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientRemoveIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/client/status": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "获取客户端状态", + "description": "获取指定客户端的状态信息", + "operationId": "get_client_status_api_ws_debug_client_status_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/client/list": { + "get": { + "tags": [ + "WebSocket调试" + ], + "summary": "列出所有客户端", + "description": "获取所有已创建的 WebSocket 客户端列表及状态", + "operationId": "list_clients_api_ws_debug_client_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientListOut" + } + } + } + } + } + } + }, + "/api/ws_debug/message/send": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "发送原始消息", + "description": "发送原始 JSON 消息到指定客户端连接的服务器", + "operationId": "send_message_api_ws_debug_message_send_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientSendIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/message/send_json": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "发送格式化消息", + "description": "发送格式化的 JSON 消息(自动组装 id、type、data 结构)", + "operationId": "send_json_message_api_ws_debug_message_send_json_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientSendJsonIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/message/auth": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "发送认证消息", + "description": "发送认证消息到服务器\n\n- **name**: 客户端名称\n- **token**: 认证 Token\n- **auth_type**: 认证消息类型,默认 \"auth\"\n- **extra_data**: 额外的认证数据", + "operationId": "send_auth_api_ws_debug_message_auth_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientAuthIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/history": { + "get": { + "tags": [ + "WebSocket调试" + ], + "summary": "获取消息历史", + "description": "获取消息历史记录\n\n- **name**: 客户端名称,为空则获取所有客户端的历史", + "operationId": "get_history_api_ws_debug_history_get", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSMessageHistoryOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/history/clear": { + "post": { + "tags": [ + "WebSocket调试" + ], + "summary": "清空消息历史", + "description": "清空消息历史记录\n\n- **name**: 客户端名称,为空则清空所有", + "operationId": "clear_history_api_ws_debug_history_clear_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClearHistoryIn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSClientStatusOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/ws_debug/commands": { + "get": { + "tags": [ + "WebSocket调试" + ], + "summary": "获取可用 WS 命令", + "description": "获取所有已注册的 WebSocket 命令端点", + "operationId": "get_commands_api_ws_debug_commands_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WSCommandsOut" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ADBScreenshotIn": { + "properties": { + "adb_path": { + "type": "string", + "title": "Adb Path", + "description": "ADB 可执行文件的路径" + }, + "serial": { + "type": "string", + "title": "Serial", + "description": "设备序列号,格式如 '127.0.0.1:5555' 或 'emulator-5554'" + }, + "use_screencap": { + "type": "boolean", + "title": "Use Screencap", + "description": "是否使用 screencap PNG 方法,False 时使用 screencap raw 方法", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "adb_path", + "serial" + ], + "title": "ADBScreenshotIn" + }, + "ADBScreenshotOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "image_base64": { + "type": "string", + "title": "Image Base64", + "description": "截图的Base64编码(PNG格式)" + }, + "image_width": { + "type": "integer", + "title": "Image Width", + "description": "截图宽度" + }, + "image_height": { + "type": "integer", + "title": "Image Height", + "description": "截图高度" + }, + "serial": { + "type": "string", + "title": "Serial", + "description": "设备序列号" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "image_base64", + "image_width", + "image_height", + "serial" + ], + "title": "ADBScreenshotOut" + }, + "CheckImageAllIn": { + "properties": { + "window_title": { + "type": "string", + "title": "Window Title", + "description": "窗口标题(用于查找窗口)" + }, + "interval": { + "type": "number", + "minimum": 0.0, + "title": "Interval", + "description": "截图间隔时间(秒)", + "default": 0 + }, + "retry_times": { + "type": "integer", + "minimum": 1.0, + "title": "Retry Times", + "description": "重复截图次数", + "default": 1 + }, + "threshold": { + "type": "number", + "maximum": 1.0, + "minimum": 0.0, + "title": "Threshold", + "description": "图像匹配阈值,范围 0-1", + "default": 0.8 + }, + "image_paths": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Paths", + "description": "要查找的图片路径列表" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "window_title", + "image_paths" + ], + "title": "CheckImageAllIn" + }, + "CheckImageAnyIn": { + "properties": { + "window_title": { + "type": "string", + "title": "Window Title", + "description": "窗口标题(用于查找窗口)" + }, + "interval": { + "type": "number", + "minimum": 0.0, + "title": "Interval", + "description": "截图间隔时间(秒)", + "default": 0 + }, + "retry_times": { + "type": "integer", + "minimum": 1.0, + "title": "Retry Times", + "description": "重复截图次数", + "default": 1 + }, + "threshold": { + "type": "number", + "maximum": 1.0, + "minimum": 0.0, + "title": "Threshold", + "description": "图像匹配阈值,范围 0-1", + "default": 0.8 + }, + "image_paths": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Paths", + "description": "要查找的图片路径列表" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "window_title", + "image_paths" + ], + "title": "CheckImageAnyIn" + }, + "CheckImageIn": { + "properties": { + "window_title": { + "type": "string", + "title": "Window Title", + "description": "窗口标题(用于查找窗口)" + }, + "interval": { + "type": "number", + "minimum": 0.0, + "title": "Interval", + "description": "截图间隔时间(秒)", + "default": 0 + }, + "retry_times": { + "type": "integer", + "minimum": 1.0, + "title": "Retry Times", + "description": "重复截图次数", + "default": 1 + }, + "threshold": { + "type": "number", + "maximum": 1.0, + "minimum": 0.0, + "title": "Threshold", + "description": "图像匹配阈值,范围 0-1", + "default": 0.8 + }, + "image_path": { + "type": "string", + "title": "Image Path", + "description": "要查找的图片路径" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "window_title", + "image_path" + ], + "title": "CheckImageIn" + }, + "CheckImageOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "found": { + "type": "boolean", + "title": "Found", + "description": "是否找到图像" + }, + "attempts": { + "type": "integer", + "title": "Attempts", + "description": "实际尝试次数" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "found", + "attempts" + ], + "title": "CheckImageOut" + }, + "ClickImageIn": { + "properties": { + "window_title": { + "type": "string", + "title": "Window Title", + "description": "窗口标题(用于查找窗口)" + }, + "interval": { + "type": "number", + "minimum": 0.0, + "title": "Interval", + "description": "截图间隔时间(秒)", + "default": 0 + }, + "retry_times": { + "type": "integer", + "minimum": 1.0, + "title": "Retry Times", + "description": "重复截图次数", + "default": 1 + }, + "threshold": { + "type": "number", + "maximum": 1.0, + "minimum": 0.0, + "title": "Threshold", + "description": "图像匹配阈值,范围 0-1", + "default": 0.8 + }, + "image_path": { + "type": "string", + "title": "Image Path", + "description": "要查找并点击的图片路径" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "window_title", + "image_path" + ], + "title": "ClickImageIn" + }, + "ClickOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "success": { + "type": "boolean", + "title": "Success", + "description": "是否成功点击" + }, + "attempts": { + "type": "integer", + "title": "Attempts", + "description": "实际尝试次数" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "success", + "attempts" + ], + "title": "ClickOut" + }, + "ClickTextIn": { + "properties": { + "window_title": { + "type": "string", + "title": "Window Title", + "description": "窗口标题(用于查找窗口)" + }, + "interval": { + "type": "number", + "minimum": 0.0, + "title": "Interval", + "description": "截图间隔时间(秒)", + "default": 0 + }, + "retry_times": { + "type": "integer", + "minimum": 1.0, + "title": "Retry Times", + "description": "重复截图次数", + "default": 1 + }, + "text": { + "type": "string", + "title": "Text", + "description": "要查找并点击的文字内容" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "window_title", + "text" + ], + "title": "ClickTextIn" + }, + "ComboBoxItem": { + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "展示值" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value", + "description": "实际值" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "label", + "value" + ], + "title": "ComboBoxItem" + }, + "ComboBoxOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "items": { + "$ref": "#/components/schemas/ComboBoxItem" + }, + "type": "array", + "title": "Data", + "description": "下拉框选项" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "ComboBoxOut" + }, + "DeviceInfo": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "设备标题/名称" + }, + "status": { + "type": "integer", + "title": "Status", + "description": "设备状态, 参考 DeviceStatus 枚举值" + }, + "adb_address": { + "type": "string", + "title": "Adb Address", + "description": "ADB连接地址" + } + }, + "type": "object", + "required": [ + "title", + "status", + "adb_address" + ], + "title": "DeviceInfo", + "description": "API 层使用的设备信息模型。" + }, + "DispatchIn": { + "properties": { + "taskId": { + "type": "string", + "title": "Taskid", + "description": "目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "taskId" + ], + "title": "DispatchIn" + }, + "EmulatorActionBody": { + "properties": { + "index": { + "type": "string", + "title": "Index", + "description": "模拟器索引" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index" + ], + "title": "EmulatorActionBody" + }, + "EmulatorConfigIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "const": "EmulatorConfig", + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "EmulatorConfigIndexItem" + }, + "EmulatorCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/EmulatorRead", + "description": "资源数据" + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "EmulatorCreateOut", + "description": "模拟器创建响应模型" + }, + "EmulatorDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/EmulatorRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "EmulatorDetailOut", + "description": "模拟器详情响应模型" + }, + "EmulatorDeviceStatusOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/DeviceInfo" + }, + "type": "object", + "title": "Data", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "EmulatorDeviceStatusOut", + "description": "模拟器设备状态响应模型" + }, + "EmulatorGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/EmulatorConfigIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/EmulatorRead" + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "EmulatorGetOut", + "description": "模拟器列表响应模型" + }, + "EmulatorIdBody": { + "properties": { + "emulatorId": { + "type": "string", + "title": "Emulatorid", + "description": "模拟器 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "emulatorId" + ], + "title": "EmulatorIdBody" + }, + "EmulatorRead": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmulatorReadInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "EmulatorRead", + "description": "模拟器配置读取/写入模型。" + }, + "EmulatorReadInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Type": { + "anyOf": [ + { + "type": "string", + "enum": [ + "general", + "mumu", + "ldplayer" + ] + }, + { + "type": "null" + } + ], + "title": "Type" + }, + "Path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "BossKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bosskey" + }, + "MaxWaitTime": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Maxwaittime" + } + }, + "additionalProperties": false, + "type": "object", + "title": "EmulatorReadInfo" + }, + "EmulatorSearchOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "items": { + "$ref": "#/components/schemas/EmulatorSearchResult" + }, + "type": "array", + "title": "Data", + "description": "搜索到的模拟器列表" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "EmulatorSearchOut" + }, + "EmulatorSearchResult": { + "properties": { + "type": { + "type": "string", + "title": "Type", + "description": "模拟器类型" + }, + "path": { + "type": "string", + "title": "Path", + "description": "模拟器路径" + }, + "name": { + "type": "string", + "title": "Name", + "description": "模拟器名称" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "path", + "name" + ], + "title": "EmulatorSearchResult" + }, + "EmulatorStatusOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "additionalProperties": { + "additionalProperties": { + "$ref": "#/components/schemas/DeviceInfo" + }, + "type": "object" + }, + "type": "object", + "title": "Data", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "EmulatorStatusOut", + "description": "模拟器状态响应模型" + }, + "GeneralConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Script": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralConfigScript" + }, + { + "type": "null" + } + ] + }, + "Game": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralConfigGame" + }, + { + "type": "null" + } + ] + }, + "Run": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralConfigRun" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "GeneralConfig", + "title": "Type", + "description": "配置类型", + "default": "GeneralConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralConfig" + }, + "GeneralConfigGame": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "Type": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Emulator", + "Client", + "URL" + ] + }, + { + "type": "null" + } + ], + "title": "Type" + }, + "Path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "URL": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "ProcessName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Processname" + }, + "Arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Arguments" + }, + "WaitTime": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Waittime" + }, + "IfForceClose": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifforceclose" + }, + "EmulatorId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Emulatorid" + }, + "EmulatorIndex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Emulatorindex" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralConfigGame" + }, + "GeneralConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "RootPath": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Rootpath" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralConfigInfo" + }, + "GeneralConfigRun": { + "properties": { + "ProxyTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimeslimit" + }, + "RunTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimeslimit" + }, + "RunTimeLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimelimit" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralConfigRun" + }, + "GeneralConfigScript": { + "properties": { + "ScriptPath": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scriptpath" + }, + "Arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Arguments" + }, + "IfTrackProcess": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Iftrackprocess" + }, + "TrackProcessName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Trackprocessname" + }, + "TrackProcessExe": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Trackprocessexe" + }, + "TrackProcessCmdline": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Trackprocesscmdline" + }, + "ConfigPath": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Configpath" + }, + "ConfigPathMode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "File", + "Folder" + ] + }, + { + "type": "null" + } + ], + "title": "Configpathmode" + }, + "UpdateConfigMode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Never", + "Success", + "Failure", + "Always" + ] + }, + { + "type": "null" + } + ], + "title": "Updateconfigmode" + }, + "LogPath": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logpath" + }, + "LogPathFormat": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logpathformat" + }, + "LogTimeStart": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Logtimestart" + }, + "LogTimeEnd": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Logtimeend" + }, + "LogTimeFormat": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logtimeformat" + }, + "SuccessLog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Successlog" + }, + "ErrorLog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Errorlog" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralConfigScript" + }, + "GeneralUserConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralUserConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Data": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralUserConfigData" + }, + { + "type": "null" + } + ] + }, + "Notify": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeneralUserConfigNotify" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "GeneralUserConfig", + "title": "Type", + "description": "配置类型", + "default": "GeneralUserConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralUserConfig" + }, + "GeneralUserConfigData": { + "properties": { + "LastProxyDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lastproxydate" + }, + "ProxyTimes": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimes" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralUserConfigData" + }, + "GeneralUserConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Status": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "RemainedDay": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": -1.0 + }, + { + "type": "null" + } + ], + "title": "Remainedday" + }, + "IfScriptBeforeTask": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifscriptbeforetask" + }, + "ScriptBeforeTask": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scriptbeforetask" + }, + "IfScriptAfterTask": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifscriptaftertask" + }, + "ScriptAfterTask": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scriptaftertask" + }, + "Notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + }, + "Tag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tag", + "readOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralUserConfigInfo" + }, + "GeneralUserConfigNotify": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "IfSendStatistic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendstatistic" + }, + "IfSendMail": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendmail" + }, + "ToAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Toaddress" + }, + "IfServerChan": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifserverchan" + }, + "ServerChanKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Serverchankey" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GeneralUserConfigNotify" + }, + "GetStageIn": { + "properties": { + "type": { + "type": "string", + "enum": [ + "User", + "Today", + "ALL", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "title": "Type", + "description": "选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "GetStageIn" + }, + "GlobalConfigRead": { + "properties": { + "Function": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlobalConfigReadFunction" + }, + { + "type": "null" + } + ] + }, + "Voice": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlobalConfigReadVoice" + }, + { + "type": "null" + } + ] + }, + "Start": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlobalConfigReadStart" + }, + { + "type": "null" + } + ] + }, + "UI": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlobalConfigReadUI" + }, + { + "type": "null" + } + ] + }, + "Notify": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlobalConfigReadNotify" + }, + { + "type": "null" + } + ] + }, + "Update": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlobalConfigReadUpdate" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigRead", + "description": "全局配置读取/写入模型。" + }, + "GlobalConfigReadFunction": { + "properties": { + "HistoryRetentionTime": { + "anyOf": [ + { + "type": "integer", + "enum": [ + 7, + 15, + 30, + 60, + 90, + 180, + 365, + 0 + ] + }, + { + "type": "null" + } + ], + "title": "Historyretentiontime" + }, + "IfAllowSleep": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifallowsleep" + }, + "IfSilence": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsilence" + }, + "IfAgreeBilibili": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifagreebilibili" + }, + "IfBlockAd": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifblockad" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigReadFunction" + }, + "GlobalConfigReadNotify": { + "properties": { + "SendTaskResultTime": { + "anyOf": [ + { + "type": "string", + "enum": [ + "不推送", + "任何时刻", + "仅失败时" + ] + }, + { + "type": "null" + } + ], + "title": "Sendtaskresulttime" + }, + "IfSendStatistic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendstatistic" + }, + "IfSendSixStar": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendsixstar" + }, + "IfPushPlyer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifpushplyer" + }, + "IfSendMail": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendmail" + }, + "IfKoishiSupport": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifkoishisupport" + }, + "KoishiServerAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Koishiserveraddress" + }, + "KoishiToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Koishitoken" + }, + "SMTPServerAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Smtpserveraddress" + }, + "AuthorizationCode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorizationcode" + }, + "FromAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fromaddress" + }, + "ToAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Toaddress" + }, + "IfServerChan": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifserverchan" + }, + "ServerChanKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Serverchankey" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigReadNotify" + }, + "GlobalConfigReadStart": { + "properties": { + "IfSelfStart": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifselfstart" + }, + "IfMinimizeDirectly": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifminimizedirectly" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigReadStart" + }, + "GlobalConfigReadUI": { + "properties": { + "IfShowTray": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifshowtray" + }, + "IfToTray": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Iftotray" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigReadUI" + }, + "GlobalConfigReadUpdate": { + "properties": { + "IfAutoUpdate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifautoupdate" + }, + "Source": { + "anyOf": [ + { + "type": "string", + "enum": [ + "GitHub", + "MirrorChyan", + "AutoSite" + ] + }, + { + "type": "null" + } + ], + "title": "Source" + }, + "Channel": { + "anyOf": [ + { + "type": "string", + "enum": [ + "stable", + "beta" + ] + }, + { + "type": "null" + } + ], + "title": "Channel" + }, + "ProxyAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxyaddress" + }, + "MirrorChyanCDK": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Mirrorchyancdk" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigReadUpdate" + }, + "GlobalConfigReadVoice": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "Type": { + "anyOf": [ + { + "type": "string", + "enum": [ + "simple", + "noisy" + ] + }, + { + "type": "null" + } + ], + "title": "Type" + } + }, + "additionalProperties": false, + "type": "object", + "title": "GlobalConfigReadVoice" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HistoryData": { + "properties": { + "index": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/HistoryIndexItem" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Index", + "description": "历史记录索引列表" + }, + "recruit_statistics": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Recruit Statistics", + "description": "公招统计数据, key为星级, value为对应的公招数量" + }, + "drop_statistics": { + "anyOf": [ + { + "additionalProperties": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Drop Statistics", + "description": "掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }" + }, + "error_info": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Error Info", + "description": "报错信息, key为时间戳, value为错误描述" + }, + "sanity": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sanity", + "description": "当前理智值" + }, + "sanity_full_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sanity Full At", + "description": "理智回满时间, 格式通常为 YYYY-MM-DD HH:MM:SS" + }, + "log_content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Log Content", + "description": "日志内容, 仅在提取单条历史记录数据时返回" + } + }, + "additionalProperties": false, + "type": "object", + "title": "HistoryData" + }, + "HistoryDataGetIn": { + "properties": { + "jsonPath": { + "type": "string", + "title": "Jsonpath", + "description": "需要提取数据的历史记录JSON文件" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "jsonPath" + ], + "title": "HistoryDataGetIn" + }, + "HistoryDataGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/HistoryData", + "description": "历史记录数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "HistoryDataGetOut" + }, + "HistoryIndexItem": { + "properties": { + "date": { + "type": "string", + "title": "Date", + "description": "日期" + }, + "status": { + "type": "string", + "enum": [ + "DONE", + "ERROR" + ], + "title": "Status", + "description": "状态" + }, + "jsonFile": { + "type": "string", + "title": "Jsonfile", + "description": "对应JSON文件" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "date", + "status", + "jsonFile" + ], + "title": "HistoryIndexItem" + }, + "HistorySearchIn": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "DAILY", + "WEEKLY", + "MONTHLY" + ], + "title": "Mode", + "description": "合并模式" + }, + "start_date": { + "type": "string", + "title": "Start Date", + "description": "开始日期, 格式YYYY-MM-DD" + }, + "end_date": { + "type": "string", + "title": "End Date", + "description": "结束日期, 格式YYYY-MM-DD" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "start_date", + "end_date" + ], + "title": "HistorySearchIn" + }, + "HistorySearchOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "additionalProperties": { + "additionalProperties": { + "$ref": "#/components/schemas/HistoryData" + }, + "type": "object" + }, + "type": "object", + "title": "Data", + "description": "历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "HistorySearchOut" + }, + "IndexOrderPatch": { + "properties": { + "index_list": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Index List", + "description": "按新顺序排列的资源 ID 列表" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index_list" + ], + "title": "IndexOrderPatch" + }, + "InfoOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "additionalProperties": true, + "type": "object", + "title": "Data", + "description": "收到的服务器数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "InfoOut" + }, + "InfrastructureImportBody": { + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "JSON 文件路径, 用于导入自定义基建文件" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "path" + ], + "title": "InfrastructureImportBody" + }, + "MaaConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Emulator": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaConfigEmulator" + }, + { + "type": "null" + } + ] + }, + "Run": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaConfigRun" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "MaaConfig", + "title": "Type", + "description": "配置类型", + "default": "MaaConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaConfig" + }, + "MaaConfigEmulator": { + "properties": { + "Id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "Index": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Index" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaConfigEmulator" + }, + "MaaConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaConfigInfo" + }, + "MaaConfigRun": { + "properties": { + "TaskTransitionMethod": { + "anyOf": [ + { + "type": "string", + "enum": [ + "NoAction", + "ExitGame", + "ExitEmulator" + ] + }, + { + "type": "null" + } + ], + "title": "Tasktransitionmethod" + }, + "ProxyTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimeslimit" + }, + "RunTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimeslimit" + }, + "AnnihilationTimeLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Annihilationtimelimit" + }, + "RoutineTimeLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Routinetimelimit" + }, + "AnnihilationAvoidWaste": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Annihilationavoidwaste" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaConfigRun" + }, + "MaaEndConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Run": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndConfigRun" + }, + { + "type": "null" + } + ] + }, + "Game": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndConfigGame" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "MaaEndConfig", + "title": "Type", + "description": "配置类型", + "default": "MaaEndConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndConfig" + }, + "MaaEndConfigGame": { + "properties": { + "ControllerType": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Win32-Window", + "Win32-Front", + "Win32-Window-Background", + "ADB" + ] + }, + { + "type": "null" + } + ], + "title": "Controllertype" + }, + "Path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "Arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Arguments" + }, + "WaitTime": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Waittime" + }, + "EmulatorId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Emulatorid" + }, + "EmulatorIndex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Emulatorindex" + }, + "CloseOnFinish": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Closeonfinish" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndConfigGame" + }, + "MaaEndConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndConfigInfo" + }, + "MaaEndConfigRun": { + "properties": { + "RunTimeLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimelimit" + }, + "ProxyTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimeslimit" + }, + "RunTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimeslimit" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndConfigRun" + }, + "MaaEndUserConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndUserConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Task": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndUserConfigTask" + }, + { + "type": "null" + } + ] + }, + "Data": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndUserConfigData" + }, + { + "type": "null" + } + ] + }, + "Notify": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaEndUserConfigNotify" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "MaaEndUserConfig", + "title": "Type", + "description": "配置类型", + "default": "MaaEndUserConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndUserConfig" + }, + "MaaEndUserConfigData": { + "properties": { + "LastProxyDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lastproxydate" + }, + "ProxyTimes": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimes" + }, + "LastProxyStatus": { + "anyOf": [ + { + "type": "string", + "enum": [ + "未知", + "成功", + "失败" + ] + }, + { + "type": "null" + } + ], + "title": "Lastproxystatus" + }, + "LastSklandDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lastsklanddate" + }, + "IfPassCheck": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifpasscheck" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndUserConfigData" + }, + "MaaEndUserConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Status": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "Id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "Password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Password" + }, + "Mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "简洁", + "详细" + ] + }, + { + "type": "null" + } + ], + "title": "Mode" + }, + "Resource": { + "anyOf": [ + { + "type": "string", + "const": "官服" + }, + { + "type": "null" + } + ], + "title": "Resource" + }, + "RemainedDay": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": -1.0 + }, + { + "type": "null" + } + ], + "title": "Remainedday" + }, + "Notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + }, + "IfSkland": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifskland" + }, + "SklandToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sklandtoken" + }, + "Tag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tag", + "readOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndUserConfigInfo" + }, + "MaaEndUserConfigNotify": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "IfSendStatistic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendstatistic" + }, + "IfSendMail": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendmail" + }, + "ToAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Toaddress" + }, + "IfServerChan": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifserverchan" + }, + "ServerChanKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Serverchankey" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndUserConfigNotify" + }, + "MaaEndUserConfigTask": { + "properties": { + "ProtocolSpaceTab": { + "anyOf": [ + { + "type": "string", + "enum": [ + "OperatorProgression", + "WeaponProgression", + "CrisisDrills" + ] + }, + { + "type": "null" + } + ], + "title": "Protocolspacetab" + }, + "OperatorProgression": { + "anyOf": [ + { + "type": "string", + "enum": [ + "OperatorEXP", + "Promotions", + "T-Creds", + "SkillUp" + ] + }, + { + "type": "null" + } + ], + "title": "Operatorprogression" + }, + "WeaponProgression": { + "anyOf": [ + { + "type": "string", + "enum": [ + "WeaponEXP", + "WeaponTune" + ] + }, + { + "type": "null" + } + ], + "title": "Weaponprogression" + }, + "CrisisDrills": { + "anyOf": [ + { + "type": "string", + "enum": [ + "AdvancedProgression1", + "AdvancedProgression2", + "AdvancedProgression3", + "AdvancedProgression4", + "AdvancedProgression5" + ] + }, + { + "type": "null" + } + ], + "title": "Crisisdrills" + }, + "RewardsSetOption": { + "anyOf": [ + { + "type": "string", + "enum": [ + "RewardsSetA", + "RewardsSetB" + ] + }, + { + "type": "null" + } + ], + "title": "Rewardssetoption" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaEndUserConfigTask" + }, + "MaaPlanDayPatch": { + "properties": { + "medicine_numb": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Medicine Numb", + "description": "吃理智药" + }, + "series_numb": { + "anyOf": [ + { + "type": "string", + "enum": [ + "0", + "6", + "5", + "4", + "3", + "2", + "1", + "-1" + ] + }, + { + "type": "null" + } + ], + "title": "Series Numb", + "description": "连战次数" + }, + "stage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage", + "description": "关卡选择" + }, + "stage_1": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage 1", + "description": "备选关卡 - 1" + }, + "stage_2": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage 2", + "description": "备选关卡 - 2" + }, + "stage_3": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage 3", + "description": "备选关卡 - 3" + }, + "stage_remain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage Remain", + "description": "剩余理智关卡" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaPlanDayPatch" + }, + "MaaPlanDayRead": { + "properties": { + "MedicineNumb": { + "type": "integer", + "title": "Medicinenumb", + "description": "吃理智药", + "default": 0 + }, + "SeriesNumb": { + "type": "string", + "enum": [ + "0", + "6", + "5", + "4", + "3", + "2", + "1", + "-1" + ], + "title": "Seriesnumb", + "description": "连战次数", + "default": "0" + }, + "Stage": { + "type": "string", + "title": "Stage", + "description": "关卡选择", + "default": "-" + }, + "Stage_1": { + "type": "string", + "title": "Stage 1", + "description": "备选关卡 - 1", + "default": "-" + }, + "Stage_2": { + "type": "string", + "title": "Stage 2", + "description": "备选关卡 - 2", + "default": "-" + }, + "Stage_3": { + "type": "string", + "title": "Stage 3", + "description": "备选关卡 - 3", + "default": "-" + }, + "Stage_Remain": { + "type": "string", + "title": "Stage Remain", + "description": "剩余理智关卡", + "default": "-" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaPlanDayRead" + }, + "MaaPlanInfoPatch": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "计划表名称" + }, + "mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ALL", + "Weekly" + ] + }, + { + "type": "null" + } + ], + "title": "Mode", + "description": "计划表模式" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaPlanInfoPatch" + }, + "MaaPlanInfoRead": { + "properties": { + "Name": { + "type": "string", + "title": "Name", + "description": "计划表名称", + "default": "新 MAA 计划表" + }, + "Mode": { + "type": "string", + "enum": [ + "ALL", + "Weekly" + ], + "title": "Mode", + "description": "计划表模式", + "default": "ALL" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaPlanInfoRead" + }, + "MaaPlanPatch": { + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanInfoPatch" + }, + { + "type": "null" + } + ], + "description": "基础信息" + }, + "all_days": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "全局" + }, + "monday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周一" + }, + "tuesday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周二" + }, + "wednesday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周三" + }, + "thursday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周四" + }, + "friday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周五" + }, + "saturday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周六" + }, + "sunday": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaPlanDayPatch" + }, + { + "type": "null" + } + ], + "description": "周日" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaPlanPatch" + }, + "MaaPlanRead": { + "properties": { + "Info": { + "$ref": "#/components/schemas/MaaPlanInfoRead", + "description": "基础信息" + }, + "ALL": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "全局" + }, + "Monday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周一" + }, + "Tuesday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周二" + }, + "Wednesday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周三" + }, + "Thursday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周四" + }, + "Friday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周五" + }, + "Saturday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周六" + }, + "Sunday": { + "$ref": "#/components/schemas/MaaPlanDayRead", + "description": "周日" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaPlanRead" + }, + "MaaUserConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaUserConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Data": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaUserConfigData" + }, + { + "type": "null" + } + ] + }, + "Task": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaUserConfigTask" + }, + { + "type": "null" + } + ] + }, + "Notify": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaaUserConfigNotify" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "MaaUserConfig", + "title": "Type", + "description": "配置类型", + "default": "MaaUserConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaUserConfig" + }, + "MaaUserConfigData": { + "properties": { + "LastProxyDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lastproxydate" + }, + "LastSklandDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lastsklanddate" + }, + "ProxyTimes": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimes" + }, + "IfPassCheck": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifpasscheck" + }, + "CustomInfrast": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Custominfrast" + }, + "InfrastIndex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Infrastindex" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaUserConfigData" + }, + "MaaUserConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "Password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Password" + }, + "Mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "简洁", + "详细" + ] + }, + { + "type": "null" + } + ], + "title": "Mode" + }, + "StageMode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stagemode" + }, + "Server": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Official", + "Bilibili", + "YoStarEN", + "YoStarJP", + "YoStarKR", + "txwy" + ] + }, + { + "type": "null" + } + ], + "title": "Server" + }, + "Status": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "RemainedDay": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": -1.0 + }, + { + "type": "null" + } + ], + "title": "Remainedday" + }, + "Annihilation": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Close", + "Annihilation", + "Chernobog@Annihilation", + "LungmenOutskirts@Annihilation", + "LungmenDowntown@Annihilation" + ] + }, + { + "type": "null" + } + ], + "title": "Annihilation" + }, + "InfrastMode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Normal", + "Rotation", + "Custom" + ] + }, + { + "type": "null" + } + ], + "title": "Infrastmode" + }, + "InfrastName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Infrastname", + "readOnly": true + }, + "InfrastIndex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Infrastindex", + "readOnly": true + }, + "Notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + }, + "MedicineNumb": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Medicinenumb" + }, + "SeriesNumb": { + "anyOf": [ + { + "type": "string", + "enum": [ + "0", + "6", + "5", + "4", + "3", + "2", + "1", + "-1" + ] + }, + { + "type": "null" + } + ], + "title": "Seriesnumb" + }, + "Stage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage" + }, + "Stage_1": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage 1" + }, + "Stage_2": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage 2" + }, + "Stage_3": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage 3" + }, + "Stage_Remain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stage Remain" + }, + "IfSkland": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifskland" + }, + "SklandToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sklandtoken" + }, + "Tag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tag", + "readOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaUserConfigInfo" + }, + "MaaUserConfigNotify": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "IfSendStatistic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendstatistic" + }, + "IfSendSixStar": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendsixstar" + }, + "IfSendMail": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendmail" + }, + "ToAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Toaddress" + }, + "IfServerChan": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifserverchan" + }, + "ServerChanKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Serverchankey" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaUserConfigNotify" + }, + "MaaUserConfigTask": { + "properties": { + "IfStartUp": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifstartup" + }, + "IfFight": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Iffight" + }, + "IfInfrast": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifinfrast" + }, + "IfRecruit": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifrecruit" + }, + "IfMall": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifmall" + }, + "IfAward": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifaward" + }, + "IfRoguelike": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifroguelike" + }, + "IfReclamation": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifreclamation" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MaaUserConfigTask" + }, + "NoticeOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "if_need_show": { + "type": "boolean", + "title": "If Need Show", + "description": "是否需要显示公告" + }, + "data": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Data", + "description": "公告信息, key为公告标题, value为公告内容" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "if_need_show", + "data" + ], + "title": "NoticeOut" + }, + "OCRScreenshotIn": { + "properties": { + "window_title": { + "type": "string", + "title": "Window Title", + "description": "窗口标题(用于查找窗口)" + }, + "should_preprocess": { + "type": "boolean", + "title": "Should Preprocess", + "description": "是否预处理图片区域,True时排除边框和标题栏,False时使用完整窗口", + "default": true + }, + "aspect_ratio_width": { + "type": "integer", + "title": "Aspect Ratio Width", + "description": "宽高比宽度", + "default": 16 + }, + "aspect_ratio_height": { + "type": "integer", + "title": "Aspect Ratio Height", + "description": "宽高比高度", + "default": 9 + }, + "region": { + "anyOf": [ + { + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + } + ], + "type": "array", + "maxItems": 4, + "minItems": 4 + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "自定义截图区域 (left, top, width, height)" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "window_title" + ], + "title": "OCRScreenshotIn" + }, + "OCRScreenshotOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "image_base64": { + "type": "string", + "title": "Image Base64", + "description": "截图的Base64编码(PNG格式)" + }, + "region": { + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + } + ], + "type": "array", + "maxItems": 4, + "minItems": 4, + "title": "Region", + "description": "实际使用的截图区域 (left, top, width, height)" + }, + "image_width": { + "type": "integer", + "title": "Image Width", + "description": "截图宽度" + }, + "image_height": { + "type": "integer", + "title": "Image Height", + "description": "截图高度" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "image_base64", + "region", + "image_width", + "image_height" + ], + "title": "OCRScreenshotOut" + }, + "OutBase": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + } + }, + "additionalProperties": false, + "type": "object", + "title": "OutBase" + }, + "PlanCreateIn": { + "properties": { + "type": { + "type": "string", + "const": "MaaPlan", + "title": "Type", + "description": "计划类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "PlanCreateIn" + }, + "PlanCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/MaaPlanRead", + "description": "资源数据" + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "PlanCreateOut", + "description": "计划创建响应模型" + }, + "PlanDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/MaaPlanRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "PlanDetailOut", + "description": "计划详情响应模型" + }, + "PlanGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/PlanIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/MaaPlanRead" + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "PlanGetOut", + "description": "计划列表响应模型" + }, + "PlanIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "const": "MaaPlanConfig", + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "PlanIndexItem" + }, + "PlanUpdateBody": { + "properties": { + "data": { + "$ref": "#/components/schemas/MaaPlanPatch", + "description": "计划更新数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "PlanUpdateBody" + }, + "PowerIn": { + "properties": { + "signal": { + "type": "string", + "enum": [ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf" + ], + "title": "Signal", + "description": "电源操作信号" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "signal" + ], + "title": "PowerIn" + }, + "PowerOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "signal": { + "type": "string", + "enum": [ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf" + ], + "title": "Signal", + "description": "电源操作信号" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "signal" + ], + "title": "PowerOut" + }, + "QueueCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/QueueRead", + "description": "资源数据" + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "QueueCreateOut", + "description": "队列创建响应模型" + }, + "QueueDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/QueueRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "QueueDetailOut", + "description": "队列详情响应模型" + }, + "QueueGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/QueueIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/QueueRead" + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "QueueGetOut", + "description": "队列列表响应模型" + }, + "QueueIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "const": "QueueConfig", + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "QueueIndexItem" + }, + "QueueItemCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/QueueItemRead", + "description": "资源数据" + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "QueueItemCreateOut", + "description": "队列项创建响应模型" + }, + "QueueItemDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/QueueItemRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "QueueItemDetailOut", + "description": "队列项详情响应模型" + }, + "QueueItemGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/QueueItemIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/QueueItemRead" + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "QueueItemGetOut", + "description": "队列项列表响应模型" + }, + "QueueItemIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "const": "QueueItem", + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "QueueItemIndexItem" + }, + "QueueItemRead": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueueItemReadInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "QueueItemRead", + "description": "任务项读取/写入模型。" + }, + "QueueItemReadInfo": { + "properties": { + "ScriptId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scriptid" + } + }, + "additionalProperties": false, + "type": "object", + "title": "QueueItemReadInfo" + }, + "QueueRead": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueueReadInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "QueueRead", + "description": "队列配置读取/写入模型。" + }, + "QueueReadInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "TimeEnabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Timeenabled" + }, + "StartUpEnabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Startupenabled" + }, + "AfterAccomplish": { + "anyOf": [ + { + "type": "string", + "enum": [ + "NoAction", + "Shutdown", + "ShutdownForce", + "Reboot", + "Hibernate", + "Sleep", + "KillSelf" + ] + }, + { + "type": "null" + } + ], + "title": "Afteraccomplish" + } + }, + "additionalProperties": false, + "type": "object", + "title": "QueueReadInfo" + }, + "ScriptCreateIn": { + "properties": { + "type": { + "type": "string", + "enum": [ + "MAA", + "SRC", + "General", + "MaaEnd" + ], + "title": "Type", + "description": "脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本" + }, + "copyFromId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Copyfromid", + "description": "直接从该脚本 ID 复制创建, 仅复制创建时使用" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "ScriptCreateIn" + }, + "ScriptCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaConfig" + }, + { + "$ref": "#/components/schemas/SrcConfig" + }, + { + "$ref": "#/components/schemas/GeneralConfig" + }, + { + "$ref": "#/components/schemas/MaaEndConfig" + } + ], + "title": "Data", + "description": "资源数据", + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralConfig": "#/components/schemas/GeneralConfig", + "MaaConfig": "#/components/schemas/MaaConfig", + "MaaEndConfig": "#/components/schemas/MaaEndConfig", + "SrcConfig": "#/components/schemas/SrcConfig" + } + } + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "ScriptCreateOut", + "description": "脚本创建响应模型" + }, + "ScriptDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaConfig" + }, + { + "$ref": "#/components/schemas/SrcConfig" + }, + { + "$ref": "#/components/schemas/GeneralConfig" + }, + { + "$ref": "#/components/schemas/MaaEndConfig" + } + ], + "title": "Data", + "description": "资源数据", + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralConfig": "#/components/schemas/GeneralConfig", + "MaaConfig": "#/components/schemas/MaaConfig", + "MaaEndConfig": "#/components/schemas/MaaEndConfig", + "SrcConfig": "#/components/schemas/SrcConfig" + } + } + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "ScriptDetailOut", + "description": "脚本详情响应模型" + }, + "ScriptFileBody": { + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "文件路径" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "path" + ], + "title": "ScriptFileBody" + }, + "ScriptGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/ScriptIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaConfig" + }, + { + "$ref": "#/components/schemas/SrcConfig" + }, + { + "$ref": "#/components/schemas/GeneralConfig" + }, + { + "$ref": "#/components/schemas/MaaEndConfig" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralConfig": "#/components/schemas/GeneralConfig", + "MaaConfig": "#/components/schemas/MaaConfig", + "MaaEndConfig": "#/components/schemas/MaaEndConfig", + "SrcConfig": "#/components/schemas/SrcConfig" + } + } + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "ScriptGetOut", + "description": "脚本列表响应模型" + }, + "ScriptIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "enum": [ + "MaaConfig", + "GeneralConfig", + "SrcConfig", + "MaaEndConfig" + ], + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "ScriptIndexItem" + }, + "ScriptPatchBody": { + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaConfig" + }, + { + "$ref": "#/components/schemas/SrcConfig" + }, + { + "$ref": "#/components/schemas/GeneralConfig" + }, + { + "$ref": "#/components/schemas/MaaEndConfig" + } + ], + "title": "Data", + "description": "脚本 Patch 数据", + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralConfig": "#/components/schemas/GeneralConfig", + "MaaConfig": "#/components/schemas/MaaConfig", + "MaaEndConfig": "#/components/schemas/MaaEndConfig", + "SrcConfig": "#/components/schemas/SrcConfig" + } + } + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "ScriptPatchBody" + }, + "ScriptUploadBody": { + "properties": { + "config_name": { + "type": "string", + "title": "Config Name", + "description": "配置名称" + }, + "author": { + "type": "string", + "title": "Author", + "description": "作者" + }, + "description": { + "type": "string", + "title": "Description", + "description": "描述" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "config_name", + "author", + "description" + ], + "title": "ScriptUploadBody" + }, + "ScriptUrlBody": { + "properties": { + "url": { + "type": "string", + "title": "Url", + "description": "配置文件 URL" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "url" + ], + "title": "ScriptUrlBody" + }, + "SettingGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/GlobalConfigRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "SettingGetOut", + "description": "全局设置响应模型" + }, + "SrcConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Emulator": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcConfigEmulator" + }, + { + "type": "null" + } + ] + }, + "Run": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcConfigRun" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "SrcConfig", + "title": "Type", + "description": "配置类型", + "default": "SrcConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcConfig" + }, + "SrcConfigEmulator": { + "properties": { + "Id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "Index": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Index" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcConfigEmulator" + }, + "SrcConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcConfigInfo" + }, + "SrcConfigRun": { + "properties": { + "TaskTransitionMethod": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ExitGame", + "ExitEmulator" + ] + }, + { + "type": "null" + } + ], + "title": "Tasktransitionmethod" + }, + "ProxyTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimeslimit" + }, + "RunTimesLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimeslimit" + }, + "RunTimeLimit": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Runtimelimit" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcConfigRun" + }, + "SrcUserConfig": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcUserConfigInfo" + }, + { + "type": "null" + } + ] + }, + "Stage": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcUserConfigStage" + }, + { + "type": "null" + } + ] + }, + "Data": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcUserConfigData" + }, + { + "type": "null" + } + ] + }, + "Notify": { + "anyOf": [ + { + "$ref": "#/components/schemas/SrcUserConfigNotify" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "const": "SrcUserConfig", + "title": "Type", + "description": "配置类型", + "default": "SrcUserConfig" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcUserConfig" + }, + "SrcUserConfigData": { + "properties": { + "LastProxyDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lastproxydate" + }, + "ProxyTimes": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Proxytimes" + }, + "IfPassCheck": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifpasscheck" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcUserConfigData" + }, + "SrcUserConfigInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Status": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "Id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "Password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Password" + }, + "Mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "简洁", + "详细" + ] + }, + { + "type": "null" + } + ], + "title": "Mode" + }, + "Server": { + "anyOf": [ + { + "type": "string", + "enum": [ + "CN-Official", + "CN-Bilibili", + "VN-Official", + "OVERSEA-America", + "OVERSEA-Asia", + "OVERSEA-Europe", + "OVERSEA-TWHKMO" + ] + }, + { + "type": "null" + } + ], + "title": "Server" + }, + "RemainedDay": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": -1.0 + }, + { + "type": "null" + } + ], + "title": "Remainedday" + }, + "Notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + }, + "Tag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tag", + "readOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcUserConfigInfo" + }, + "SrcUserConfigNotify": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "IfSendStatistic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendstatistic" + }, + "IfSendMail": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifsendmail" + }, + "ToAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Toaddress" + }, + "IfServerChan": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Ifserverchan" + }, + "ServerChanKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Serverchankey" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcUserConfigNotify" + }, + "SrcUserConfigStage": { + "properties": { + "Channel": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Relic", + "Materials", + "Ornament" + ] + }, + { + "type": "null" + } + ], + "title": "Channel" + }, + "Relic": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Relic" + }, + "Materials": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Materials" + }, + "Ornament": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ornament" + }, + "ExtractReservedTrailblazePower": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Extractreservedtrailblazepower" + }, + "UseFuel": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Usefuel" + }, + "FuelReserve": { + "anyOf": [ + { + "type": "integer", + "maximum": 9999.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Fuelreserve" + }, + "EchoOfWar": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Echoofwar" + }, + "SimulatedUniverseWorld": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Simulateduniverseworld" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SrcUserConfigStage" + }, + "TaskCreateIn": { + "properties": { + "taskId": { + "type": "string", + "title": "Taskid", + "description": "目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID" + }, + "mode": { + "type": "string", + "enum": [ + "AutoProxy", + "ManualReview", + "ScriptConfig" + ], + "title": "Mode", + "description": "任务模式" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "taskId", + "mode" + ], + "title": "TaskCreateIn" + }, + "TaskCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "taskId": { + "type": "string", + "title": "Taskid", + "description": "新创建的任务ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "taskId" + ], + "title": "TaskCreateOut" + }, + "TimeSetCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/TimeSetRead", + "description": "资源数据" + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "TimeSetCreateOut", + "description": "时间集创建响应模型" + }, + "TimeSetDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/TimeSetRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "TimeSetDetailOut", + "description": "时间集详情响应模型" + }, + "TimeSetGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/TimeSetIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/TimeSetRead" + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "TimeSetGetOut", + "description": "时间集列表响应模型" + }, + "TimeSetIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "const": "TimeSet", + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "TimeSetIndexItem" + }, + "TimeSetRead": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/TimeSetReadInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "TimeSetRead", + "description": "时间集合读取/写入模型。" + }, + "TimeSetReadInfo": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "Days": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Days" + }, + "Time": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Time" + } + }, + "additionalProperties": false, + "type": "object", + "title": "TimeSetReadInfo" + }, + "ToolsConfigRead": { + "properties": { + "ArknightsPC": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolsConfigReadArknightsPC" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "ToolsConfigRead", + "description": "工具配置读取/写入模型。" + }, + "ToolsConfigReadArknightsPC": { + "properties": { + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "PauseKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pausekey" + }, + "SelectDeployedKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Selectdeployedkey" + }, + "UseSkillKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Useskillkey" + }, + "RetreatKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Retreatkey" + }, + "NextFrameKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Nextframekey" + }, + "AnotherQuitKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Anotherquitkey" + }, + "Status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status", + "readOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "ToolsConfigReadArknightsPC" + }, + "ToolsGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/ToolsConfigRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "ToolsGetOut", + "description": "工具配置响应模型" + }, + "UpdateCheckIn": { + "properties": { + "current_version": { + "type": "string", + "title": "Current Version", + "description": "当前前端版本号" + }, + "if_force": { + "type": "boolean", + "title": "If Force", + "description": "是否强制拉取更新信息", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "current_version" + ], + "title": "UpdateCheckIn" + }, + "UpdateCheckOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "if_need_update": { + "type": "boolean", + "title": "If Need Update", + "description": "是否需要更新前端" + }, + "latest_version": { + "type": "string", + "title": "Latest Version", + "description": "最新前端版本号" + }, + "update_info": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object", + "title": "Update Info", + "description": "版本更新信息字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "if_need_update", + "latest_version", + "update_info" + ], + "title": "UpdateCheckOut" + }, + "UserCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaUserConfig" + }, + { + "$ref": "#/components/schemas/SrcUserConfig" + }, + { + "$ref": "#/components/schemas/GeneralUserConfig" + }, + { + "$ref": "#/components/schemas/MaaEndUserConfig" + } + ], + "title": "Data", + "description": "资源数据", + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralUserConfig": "#/components/schemas/GeneralUserConfig", + "MaaEndUserConfig": "#/components/schemas/MaaEndUserConfig", + "MaaUserConfig": "#/components/schemas/MaaUserConfig", + "SrcUserConfig": "#/components/schemas/SrcUserConfig" + } + } + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "UserCreateOut", + "description": "用户创建响应模型" + }, + "UserDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaUserConfig" + }, + { + "$ref": "#/components/schemas/SrcUserConfig" + }, + { + "$ref": "#/components/schemas/GeneralUserConfig" + }, + { + "$ref": "#/components/schemas/MaaEndUserConfig" + } + ], + "title": "Data", + "description": "资源数据", + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralUserConfig": "#/components/schemas/GeneralUserConfig", + "MaaEndUserConfig": "#/components/schemas/MaaEndUserConfig", + "MaaUserConfig": "#/components/schemas/MaaUserConfig", + "SrcUserConfig": "#/components/schemas/SrcUserConfig" + } + } + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "UserDetailOut", + "description": "用户详情响应模型" + }, + "UserGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/UserIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaUserConfig" + }, + { + "$ref": "#/components/schemas/SrcUserConfig" + }, + { + "$ref": "#/components/schemas/GeneralUserConfig" + }, + { + "$ref": "#/components/schemas/MaaEndUserConfig" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralUserConfig": "#/components/schemas/GeneralUserConfig", + "MaaEndUserConfig": "#/components/schemas/MaaEndUserConfig", + "MaaUserConfig": "#/components/schemas/MaaUserConfig", + "SrcUserConfig": "#/components/schemas/SrcUserConfig" + } + } + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "UserGetOut", + "description": "用户列表响应模型" + }, + "UserIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "enum": [ + "MaaUserConfig", + "GeneralUserConfig", + "SrcUserConfig", + "MaaEndUserConfig" + ], + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "UserIndexItem" + }, + "UserPatchBody": { + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/MaaUserConfig" + }, + { + "$ref": "#/components/schemas/SrcUserConfig" + }, + { + "$ref": "#/components/schemas/GeneralUserConfig" + }, + { + "$ref": "#/components/schemas/MaaEndUserConfig" + } + ], + "title": "Data", + "description": "用户 Patch 数据", + "discriminator": { + "propertyName": "type", + "mapping": { + "GeneralUserConfig": "#/components/schemas/GeneralUserConfig", + "MaaEndUserConfig": "#/components/schemas/MaaEndUserConfig", + "MaaUserConfig": "#/components/schemas/MaaUserConfig", + "SrcUserConfig": "#/components/schemas/SrcUserConfig" + } + } + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "UserPatchBody" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "VersionOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "if_need_update": { + "type": "boolean", + "title": "If Need Update", + "description": "后端代码是否需要更新" + }, + "current_time": { + "type": "string", + "title": "Current Time", + "description": "后端代码当前时间戳" + }, + "current_hash": { + "type": "string", + "title": "Current Hash", + "description": "后端代码当前哈希值" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "if_need_update", + "current_time", + "current_hash" + ], + "title": "VersionOut" + }, + "WSClearHistoryIn": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "客户端名称,为空则清空所有" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WSClearHistoryIn", + "description": "清空消息历史请求" + }, + "WSClientAuthIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + }, + "token": { + "type": "string", + "title": "Token", + "description": "认证 Token" + }, + "auth_type": { + "type": "string", + "title": "Auth Type", + "description": "认证消息类型", + "default": "auth" + }, + "extra_data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra Data", + "description": "额外认证数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "token" + ], + "title": "WSClientAuthIn", + "description": "发送认证请求" + }, + "WSClientConnectIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "title": "WSClientConnectIn", + "description": "连接请求" + }, + "WSClientCreateIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称,用于标识" + }, + "url": { + "type": "string", + "title": "Url", + "description": "WebSocket 服务器地址,如 ws://localhost:5140/path" + }, + "ping_interval": { + "type": "number", + "title": "Ping Interval", + "description": "心跳发送间隔(秒)", + "default": 15.0 + }, + "ping_timeout": { + "type": "number", + "title": "Ping Timeout", + "description": "心跳超时时间(秒)", + "default": 30.0 + }, + "reconnect_interval": { + "type": "number", + "title": "Reconnect Interval", + "description": "重连间隔(秒)", + "default": 5.0 + }, + "max_reconnect_attempts": { + "type": "integer", + "title": "Max Reconnect Attempts", + "description": "最大重连次数,-1为无限", + "default": -1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "url" + ], + "title": "WSClientCreateIn", + "description": "创建 WebSocket 客户端请求" + }, + "WSClientCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "返回数据" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WSClientCreateOut", + "description": "创建客户端响应" + }, + "WSClientDisconnectIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "title": "WSClientDisconnectIn", + "description": "断开连接请求" + }, + "WSClientListOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "客户端列表" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WSClientListOut", + "description": "客户端列表响应" + }, + "WSClientRemoveIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "title": "WSClientRemoveIn", + "description": "删除客户端请求" + }, + "WSClientSendIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + }, + "message": { + "additionalProperties": true, + "type": "object", + "title": "Message", + "description": "要发送的 JSON 消息" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "message" + ], + "title": "WSClientSendIn", + "description": "发送消息请求" + }, + "WSClientSendJsonIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + }, + "msg_id": { + "type": "string", + "title": "Msg Id", + "description": "消息 ID", + "default": "Client" + }, + "msg_type": { + "type": "string", + "title": "Msg Type", + "description": "消息类型" + }, + "data": { + "additionalProperties": true, + "type": "object", + "title": "Data", + "description": "消息数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "msg_type" + ], + "title": "WSClientSendJsonIn", + "description": "发送自定义 JSON 消息请求" + }, + "WSClientStatusIn": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "客户端名称" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "title": "WSClientStatusIn", + "description": "获取客户端状态请求" + }, + "WSClientStatusOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "状态数据" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WSClientStatusOut", + "description": "客户端状态响应" + }, + "WSCommandsOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "命令列表" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WSCommandsOut", + "description": "可用命令列表响应" + }, + "WSMessageHistoryOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "消息历史" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WSMessageHistoryOut", + "description": "消息历史响应" + }, + "WebhookCreateOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/WebhookRead", + "description": "资源数据" + }, + "id": { + "type": "string", + "title": "Id", + "description": "新创建资源的唯一 ID" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "id" + ], + "title": "WebhookCreateOut", + "description": "Webhook 创建响应模型" + }, + "WebhookDetailOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/WebhookRead", + "description": "资源数据" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "WebhookDetailOut", + "description": "Webhook 详情响应模型" + }, + "WebhookGetOut": { + "properties": { + "code": { + "type": "integer", + "title": "Code", + "description": "状态码", + "default": 200 + }, + "status": { + "type": "string", + "title": "Status", + "description": "操作状态", + "default": "success" + }, + "message": { + "type": "string", + "title": "Message", + "description": "操作消息", + "default": "操作成功" + }, + "index": { + "items": { + "$ref": "#/components/schemas/WebhookIndexItem" + }, + "type": "array", + "title": "Index", + "description": "资源索引列表" + }, + "data": { + "additionalProperties": { + "$ref": "#/components/schemas/WebhookRead" + }, + "type": "object", + "title": "Data", + "description": "资源数据字典" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "index", + "data" + ], + "title": "WebhookGetOut", + "description": "Webhook 列表响应模型" + }, + "WebhookIndexItem": { + "properties": { + "uid": { + "type": "string", + "title": "Uid", + "description": "唯一标识符" + }, + "type": { + "type": "string", + "const": "Webhook", + "title": "Type", + "description": "配置类型" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "uid", + "type" + ], + "title": "WebhookIndexItem" + }, + "WebhookRead": { + "properties": { + "Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebhookReadInfo" + }, + { + "type": "null" + } + ] + }, + "Data": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebhookReadData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "WebhookRead", + "description": "Webhook 配置读取/写入模型。" + }, + "WebhookReadData": { + "properties": { + "Url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "Template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template" + }, + "Headers": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "Method": { + "anyOf": [ + { + "type": "string", + "enum": [ + "POST", + "GET" + ] + }, + { + "type": "null" + } + ], + "title": "Method" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WebhookReadData" + }, + "WebhookReadInfo": { + "properties": { + "Name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "Enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WebhookReadInfo" + } + } + } +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 83c2a49e..bc2288ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier . --write", - "openapi": "openapi --input http://127.0.0.1:36163/openapi.json --output ./src/api --client axios" + "openapi": "openapi-ts" }, "build": { "asar": true, @@ -81,6 +81,7 @@ }, "devDependencies": { "@eslint/js": "^9.37.0", + "@hey-api/openapi-ts": "^0.86.0", "@types/matter-js": "^0", "@types/node": "22.17.1", "@typescript-eslint/eslint-plugin": "^8.38.0", @@ -96,7 +97,6 @@ "eslint-plugin-prettier": "^5.5.3", "eslint-plugin-vue": "^10.4.0", "globals": "^16.4.0", - "openapi-typescript-codegen": "^0.29.0", "prettier": "^3.6.2", "typescript": "^5.9.2", "typescript-eslint": "^8.45.0", @@ -127,4 +127,4 @@ "rollup": "4.59.0" }, "packageManager": "yarn@4.9.1" -} \ No newline at end of file +} diff --git a/frontend/src/api/constants.ts b/frontend/src/api/constants.ts new file mode 100644 index 00000000..42d97ace --- /dev/null +++ b/frontend/src/api/constants.ts @@ -0,0 +1,55 @@ +import type { + GetStageIn, + HistorySearchIn, + PowerIn, + ScriptCreateIn, + TaskCreateIn, +} from './generated/types.gen' + +export type StageQueryType = GetStageIn['type'] +export type HistorySearchMode = HistorySearchIn['mode'] +export type ScriptCreateType = ScriptCreateIn['type'] +export type TaskCreateMode = TaskCreateIn['mode'] +export type PowerSignal = PowerIn['signal'] + +export const STAGE_QUERY_TYPE = { + USER: 'User', + TODAY: 'Today', + ALL: 'ALL', + MONDAY: 'Monday', + TUESDAY: 'Tuesday', + WEDNESDAY: 'Wednesday', + THURSDAY: 'Thursday', + FRIDAY: 'Friday', + SATURDAY: 'Saturday', + SUNDAY: 'Sunday', +} as const satisfies Record + +export const HISTORY_SEARCH_MODE = { + DAILY: 'DAILY', + WEEKLY: 'WEEKLY', + MONTHLY: 'MONTHLY', +} as const satisfies Record + +export const SCRIPT_CREATE_TYPE = { + MAA: 'MAA', + SRC: 'SRC', + GENERAL: 'General', + MAA_END: 'MaaEnd', +} as const satisfies Record + +export const TASK_CREATE_MODE = { + AUTO_PROXY: 'AutoProxy', + MANUAL_REVIEW: 'ManualReview', + SCRIPT_CONFIG: 'ScriptConfig', +} as const satisfies Record + +export const POWER_SIGNAL = { + NO_ACTION: 'NoAction', + SHUTDOWN: 'Shutdown', + SHUTDOWN_FORCE: 'ShutdownForce', + REBOOT: 'Reboot', + HIBERNATE: 'Hibernate', + SLEEP: 'Sleep', + KILL_SELF: 'KillSelf', +} as const satisfies Record diff --git a/frontend/src/api/core/ApiError.ts b/frontend/src/api/core/ApiError.ts deleted file mode 100644 index ec7b16af..00000000 --- a/frontend/src/api/core/ApiError.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: any; - public readonly request: ApiRequestOptions; - - constructor(request: ApiRequestOptions, response: ApiResult, message: string) { - super(message); - - this.name = 'ApiError'; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} diff --git a/frontend/src/api/core/ApiRequestOptions.ts b/frontend/src/api/core/ApiRequestOptions.ts deleted file mode 100644 index 93143c3c..00000000 --- a/frontend/src/api/core/ApiRequestOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiRequestOptions = { - readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; - readonly url: string; - readonly path?: Record; - readonly cookies?: Record; - readonly headers?: Record; - readonly query?: Record; - readonly formData?: Record; - readonly body?: any; - readonly mediaType?: string; - readonly responseHeader?: string; - readonly errors?: Record; -}; diff --git a/frontend/src/api/core/ApiResult.ts b/frontend/src/api/core/ApiResult.ts deleted file mode 100644 index ee1126e2..00000000 --- a/frontend/src/api/core/ApiResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiResult = { - readonly url: string; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly body: any; -}; diff --git a/frontend/src/api/core/CancelablePromise.ts b/frontend/src/api/core/CancelablePromise.ts deleted file mode 100644 index d70de929..00000000 --- a/frontend/src/api/core/CancelablePromise.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export class CancelError extends Error { - - constructor(message: string) { - super(message); - this.name = 'CancelError'; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise implements Promise { - #isResolved: boolean; - #isRejected: boolean; - #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; - readonly #promise: Promise; - #resolve?: (value: T | PromiseLike) => void; - #reject?: (reason?: any) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: any) => void, - onCancel: OnCancel - ) => void - ) { - this.#isResolved = false; - this.#isRejected = false; - this.#isCancelled = false; - this.#cancelHandlers = []; - this.#promise = new Promise((resolve, reject) => { - this.#resolve = resolve; - this.#reject = reject; - - const onResolve = (value: T | PromiseLike): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isResolved = true; - if (this.#resolve) this.#resolve(value); - }; - - const onReject = (reason?: any): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isRejected = true; - if (this.#reject) this.#reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, 'isResolved', { - get: (): boolean => this.#isResolved, - }); - - Object.defineProperty(onCancel, 'isRejected', { - get: (): boolean => this.#isRejected, - }); - - Object.defineProperty(onCancel, 'isCancelled', { - get: (): boolean => this.#isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise"; - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: any) => TResult2 | PromiseLike) | null - ): Promise { - return this.#promise.then(onFulfilled, onRejected); - } - - public catch( - onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise { - return this.#promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.#promise.finally(onFinally); - } - - public cancel(): void { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isCancelled = true; - if (this.#cancelHandlers.length) { - try { - for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn('Cancellation threw an error', error); - return; - } - } - this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); - } - - public get isCancelled(): boolean { - return this.#isCancelled; - } -} diff --git a/frontend/src/api/core/OpenAPI.ts b/frontend/src/api/core/OpenAPI.ts deleted file mode 100644 index a0a9ed4a..00000000 --- a/frontend/src/api/core/OpenAPI.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; - -type Resolver = (options: ApiRequestOptions) => Promise; -type Headers = Record; - -export type OpenAPIConfig = { - BASE: string; - VERSION: string; - WITH_CREDENTIALS: boolean; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - HEADERS?: Headers | Resolver | undefined; - ENCODE_PATH?: ((path: string) => string) | undefined; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: '', - VERSION: '1.0.0', - WITH_CREDENTIALS: false, - CREDENTIALS: 'include', - TOKEN: undefined, - USERNAME: undefined, - PASSWORD: undefined, - HEADERS: undefined, - ENCODE_PATH: undefined, -}; diff --git a/frontend/src/api/core/request.ts b/frontend/src/api/core/request.ts deleted file mode 100644 index 1dc6fef4..00000000 --- a/frontend/src/api/core/request.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import axios from 'axios'; -import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; -import FormData from 'form-data'; - -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isDefined = (value: T | null | undefined): value is Exclude => { - return value !== undefined && value !== null; -}; - -export const isString = (value: any): value is string => { - return typeof value === 'string'; -}; - -export const isStringWithValue = (value: any): value is string => { - return isString(value) && value !== ''; -}; - -export const isBlob = (value: any): value is Blob => { - return ( - typeof value === 'object' && - typeof value.type === 'string' && - typeof value.stream === 'function' && - typeof value.arrayBuffer === 'function' && - typeof value.constructor === 'function' && - typeof value.constructor.name === 'string' && - /^(Blob|File)$/.test(value.constructor.name) && - /^(Blob|File)$/.test(value[Symbol.toStringTag]) - ); -}; - -export const isFormData = (value: any): value is FormData => { - return value instanceof FormData; -}; - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } -}; - -export const getQueryString = (params: Record): string => { - const qs: string[] = []; - - const append = (key: string, value: any) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const process = (key: string, value: any) => { - if (isDefined(value)) { - if (Array.isArray(value)) { - value.forEach(v => { - process(key, v); - }); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => { - process(`${key}[${k}]`, v); - }); - } else { - append(key, value); - } - } - }; - - Object.entries(params).forEach(([key, value]) => { - process(key, value); - }); - - if (qs.length > 0) { - return `?${qs.join('&')}`; - } - - return ''; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = `${config.BASE}${path}`; - if (options.query) { - return `${url}${getQueryString(options.query)}`; - } - return url; -}; - -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: any) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([_, value]) => isDefined(value)) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver = (options: ApiRequestOptions) => Promise; - -export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { - if (typeof resolver === 'function') { - return (resolver as Resolver)(options); - } - return resolver; -}; - -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]); - - const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {} - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - ...formHeaders, - }) - .filter(([_, value]) => isDefined(value)) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } - - return headers; -}; - -export const getRequestBody = (options: ApiRequestOptions): any => { - if (options.body) { - return options.body; - } - return undefined; -}; - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: any, - formData: FormData | undefined, - headers: Record, - onCancel: OnCancel, - axiosClient: AxiosInstance -): Promise> => { - const source = axios.CancelToken.source(); - - const requestConfig: AxiosRequestConfig = { - url, - headers, - data: body ?? formData, - method: options.method, - withCredentials: config.WITH_CREDENTIALS, - withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false, - cancelToken: source.token, - }; - - onCancel(() => source.cancel('The user aborted a request.')); - - try { - return await axiosClient.request(requestConfig); - } catch (error) { - const axiosError = error as AxiosError; - if (axiosError.response) { - return axiosError.response; - } - throw error; - } -}; - -export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader]; - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = (response: AxiosResponse): any => { - if (response.status !== 204) { - return response.data; - } - return undefined; -}; - -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise - * @throws ApiError - */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options, formData); - - if (!onCancel.isCancelled) { - const response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; diff --git a/frontend/src/api/gateways/dispatch.ts b/frontend/src/api/gateways/dispatch.ts new file mode 100644 index 00000000..6e646991 --- /dev/null +++ b/frontend/src/api/gateways/dispatch.ts @@ -0,0 +1,30 @@ +import { + addTaskApiDispatchStartPost, + cancelPowerTaskApiDispatchCancelPowerPost, + getPowerApiDispatchGetPowerPost, + setPowerApiDispatchSetPowerPost, + stopTaskApiDispatchStopPost, +} from '../generated/sdk.gen' +import type { DispatchIn, PowerIn, TaskCreateIn } from '../generated/types.gen' + +export const dispatchApi = { + startTask(payload: TaskCreateIn) { + return addTaskApiDispatchStartPost({ body: payload }) + }, + + stopTask(payload: DispatchIn) { + return stopTaskApiDispatchStopPost({ body: payload }) + }, + + getPower() { + return getPowerApiDispatchGetPowerPost() + }, + + setPower(payload: PowerIn) { + return setPowerApiDispatchSetPowerPost({ body: payload }) + }, + + cancelPowerTask() { + return cancelPowerTaskApiDispatchCancelPowerPost() + }, +} diff --git a/frontend/src/api/gateways/emulator.ts b/frontend/src/api/gateways/emulator.ts new file mode 100644 index 00000000..c749348d --- /dev/null +++ b/frontend/src/api/gateways/emulator.ts @@ -0,0 +1,62 @@ +import { + createEmulatorApiEmulatorPost, + deleteEmulatorApiEmulatorEmulatorIdDelete, + detectEmulatorsApiEmulatorDetectedGet, + getEmulatorApiEmulatorEmulatorIdGet, + getEmulatorStatusApiEmulatorEmulatorIdStatusGet, + getEmulatorStatusesApiEmulatorStatusGet, + listEmulatorsApiEmulatorGet, + operateEmulatorApiEmulatorEmulatorIdActionsActionPost, + updateEmulatorApiEmulatorEmulatorIdPatch, +} from '../generated/sdk.gen' +import type { EmulatorActionBody, EmulatorRead } from '../generated/types.gen' + +export const emulatorApi = { + list() { + return listEmulatorsApiEmulatorGet() + }, + + create() { + return createEmulatorApiEmulatorPost() + }, + + get(emulatorId: string) { + return getEmulatorApiEmulatorEmulatorIdGet({ + path: { emulator_id: emulatorId }, + }) + }, + + update(emulatorId: string, payload: EmulatorRead) { + return updateEmulatorApiEmulatorEmulatorIdPatch({ + path: { emulator_id: emulatorId }, + body: payload, + }) + }, + + remove(emulatorId: string) { + return deleteEmulatorApiEmulatorEmulatorIdDelete({ + path: { emulator_id: emulatorId }, + }) + }, + + listDetected() { + return detectEmulatorsApiEmulatorDetectedGet() + }, + + getAllStatuses() { + return getEmulatorStatusesApiEmulatorStatusGet() + }, + + getStatus(emulatorId: string) { + return getEmulatorStatusApiEmulatorEmulatorIdStatusGet({ + path: { emulator_id: emulatorId }, + }) + }, + + operate(emulatorId: string, action: 'open' | 'close' | 'show', payload: EmulatorActionBody) { + return operateEmulatorApiEmulatorEmulatorIdActionsActionPost({ + path: { emulator_id: emulatorId, action }, + body: payload, + }) + }, +} diff --git a/frontend/src/api/gateways/history.ts b/frontend/src/api/gateways/history.ts new file mode 100644 index 00000000..d164ebda --- /dev/null +++ b/frontend/src/api/gateways/history.ts @@ -0,0 +1,15 @@ +import { + getHistoryDataApiHistoryDataPost, + searchHistoryApiHistorySearchPost, +} from '../generated/sdk.gen' +import type { HistoryDataGetIn, HistorySearchIn } from '../generated/types.gen' + +export const historyApi = { + search(payload: HistorySearchIn) { + return searchHistoryApiHistorySearchPost({ body: payload }) + }, + + getData(payload: HistoryDataGetIn) { + return getHistoryDataApiHistoryDataPost({ body: payload }) + }, +} diff --git a/frontend/src/api/gateways/info.ts b/frontend/src/api/gateways/info.ts new file mode 100644 index 00000000..955f980a --- /dev/null +++ b/frontend/src/api/gateways/info.ts @@ -0,0 +1,62 @@ +import { + confirmNoticeApiInfoNoticeConfirmPost, + getEmulatorComboxApiInfoComboxEmulatorPost, + getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost, + getGitVersionApiInfoVersionPost, + getNoticeInfoApiInfoNoticeGetPost, + getOverviewApiInfoGetOverviewPost, + getPlanComboxApiInfoComboxPlanPost, + getScriptComboxApiInfoComboxScriptPost, + getStageComboxApiInfoComboxStagePost, + getTaskComboxApiInfoComboxTaskPost, + getWebConfigApiInfoWebconfigPost, +} from '../generated/sdk.gen' +import type { EmulatorIdBody, GetStageIn } from '../generated/types.gen' + +export const infoApi = { + getVersion() { + return getGitVersionApiInfoVersionPost() + }, + + getStageOptions(payload: GetStageIn) { + return getStageComboxApiInfoComboxStagePost({ body: payload }) + }, + + getScriptOptions() { + return getScriptComboxApiInfoComboxScriptPost() + }, + + getTaskOptions() { + return getTaskComboxApiInfoComboxTaskPost() + }, + + getPlanOptions() { + return getPlanComboxApiInfoComboxPlanPost() + }, + + getEmulatorOptions() { + return getEmulatorComboxApiInfoComboxEmulatorPost() + }, + + getEmulatorDeviceOptions(payload: EmulatorIdBody) { + return getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost({ + body: payload, + }) + }, + + getNotice() { + return getNoticeInfoApiInfoNoticeGetPost() + }, + + confirmNotice() { + return confirmNoticeApiInfoNoticeConfirmPost() + }, + + getWebConfig() { + return getWebConfigApiInfoWebconfigPost() + }, + + getOverview() { + return getOverviewApiInfoGetOverviewPost() + }, +} diff --git a/frontend/src/api/gateways/ocr.ts b/frontend/src/api/gateways/ocr.ts new file mode 100644 index 00000000..51bb68e0 --- /dev/null +++ b/frontend/src/api/gateways/ocr.ts @@ -0,0 +1,48 @@ +import { + checkImageAllApiOcrCheckImageAllPost, + checkImageAnyApiOcrCheckImageAnyPost, + checkImageApiOcrCheckImagePost, + clickImageApiOcrClickImagePost, + clickTextApiOcrClickTextPost, + getScreenshotAdbApiOcrScreenshotAdbPost, + getScreenshotApiOcrScreenshotPost, +} from '../generated/sdk.gen' +import type { + AdbScreenshotIn, + CheckImageAllIn, + CheckImageAnyIn, + CheckImageIn, + ClickImageIn, + ClickTextIn, + OcrScreenshotIn, +} from '../generated/types.gen' + +export const ocrApi = { + getWindowScreenshot(payload: OcrScreenshotIn) { + return getScreenshotApiOcrScreenshotPost({ body: payload }) + }, + + getAdbScreenshot(payload: AdbScreenshotIn) { + return getScreenshotAdbApiOcrScreenshotAdbPost({ body: payload }) + }, + + checkImage(payload: CheckImageIn) { + return checkImageApiOcrCheckImagePost({ body: payload }) + }, + + checkAnyImage(payload: CheckImageAnyIn) { + return checkImageAnyApiOcrCheckImageAnyPost({ body: payload }) + }, + + checkAllImages(payload: CheckImageAllIn) { + return checkImageAllApiOcrCheckImageAllPost({ body: payload }) + }, + + clickImage(payload: ClickImageIn) { + return clickImageApiOcrClickImagePost({ body: payload }) + }, + + clickText(payload: ClickTextIn) { + return clickTextApiOcrClickTextPost({ body: payload }) + }, +} diff --git a/frontend/src/api/gateways/plan.ts b/frontend/src/api/gateways/plan.ts new file mode 100644 index 00000000..6c509fd1 --- /dev/null +++ b/frontend/src/api/gateways/plan.ts @@ -0,0 +1,38 @@ +import { + createPlanApiPlanPost, + deletePlanApiPlanPlanIdDelete, + getPlanApiPlanPlanIdGet, + listPlansApiPlanGet, + reorderPlanApiPlanOrderPatch, + updatePlanApiPlanPlanIdPatch, +} from '../generated/sdk.gen' +import type { IndexOrderPatch, PlanCreateIn, PlanUpdateBody } from '../generated/types.gen' + +export const planApi = { + list() { + return listPlansApiPlanGet() + }, + + create(payload: PlanCreateIn) { + return createPlanApiPlanPost({ body: payload }) + }, + + get(planId: string) { + return getPlanApiPlanPlanIdGet({ path: { plan_id: planId } }) + }, + + update(planId: string, payload: PlanUpdateBody) { + return updatePlanApiPlanPlanIdPatch({ + path: { plan_id: planId }, + body: payload, + }) + }, + + remove(planId: string) { + return deletePlanApiPlanPlanIdDelete({ path: { plan_id: planId } }) + }, + + reorder(payload: IndexOrderPatch) { + return reorderPlanApiPlanOrderPatch({ body: payload }) + }, +} diff --git a/frontend/src/api/gateways/queue.ts b/frontend/src/api/gateways/queue.ts new file mode 100644 index 00000000..ac63d6f0 --- /dev/null +++ b/frontend/src/api/gateways/queue.ts @@ -0,0 +1,122 @@ +import { + createQueueApiQueuePost, + createQueueItemApiQueueQueueIdItemsPost, + createTimeSetApiQueueQueueIdTimesPost, + deleteQueueApiQueueQueueIdDelete, + deleteQueueItemApiQueueQueueIdItemsQueueItemIdDelete, + deleteTimeSetApiQueueQueueIdTimesTimeSetIdDelete, + getQueueApiQueueQueueIdGet, + getQueueItemApiQueueQueueIdItemsQueueItemIdGet, + getTimeSetApiQueueQueueIdTimesTimeSetIdGet, + listQueueItemsApiQueueQueueIdItemsGet, + listQueuesApiQueueGet, + listTimeSetsApiQueueQueueIdTimesGet, + reorderQueueApiQueueOrderPatch, + reorderQueueItemsApiQueueQueueIdItemsOrderPatch, + reorderTimeSetsApiQueueQueueIdTimesOrderPatch, + updateQueueApiQueueQueueIdPatch, + updateQueueItemApiQueueQueueIdItemsQueueItemIdPatch, + updateTimeSetApiQueueQueueIdTimesTimeSetIdPatch, +} from '../generated/sdk.gen' +import type { IndexOrderPatch, QueueItemRead, QueueRead, TimeSetRead } from '../generated/types.gen' + +export const queueApi = { + list() { + return listQueuesApiQueueGet() + }, + + create() { + return createQueueApiQueuePost() + }, + + get(queueId: string) { + return getQueueApiQueueQueueIdGet({ path: { queue_id: queueId } }) + }, + + update(queueId: string, payload: QueueRead) { + return updateQueueApiQueueQueueIdPatch({ + path: { queue_id: queueId }, + body: payload, + }) + }, + + remove(queueId: string) { + return deleteQueueApiQueueQueueIdDelete({ path: { queue_id: queueId } }) + }, + + reorder(payload: IndexOrderPatch) { + return reorderQueueApiQueueOrderPatch({ body: payload }) + }, +} + +export const timeSetApi = { + list(queueId: string) { + return listTimeSetsApiQueueQueueIdTimesGet({ path: { queue_id: queueId } }) + }, + + create(queueId: string) { + return createTimeSetApiQueueQueueIdTimesPost({ path: { queue_id: queueId } }) + }, + + get(queueId: string, timeSetId: string) { + return getTimeSetApiQueueQueueIdTimesTimeSetIdGet({ + path: { queue_id: queueId, time_set_id: timeSetId }, + }) + }, + + update(queueId: string, timeSetId: string, payload: TimeSetRead) { + return updateTimeSetApiQueueQueueIdTimesTimeSetIdPatch({ + path: { queue_id: queueId, time_set_id: timeSetId }, + body: payload, + }) + }, + + remove(queueId: string, timeSetId: string) { + return deleteTimeSetApiQueueQueueIdTimesTimeSetIdDelete({ + path: { queue_id: queueId, time_set_id: timeSetId }, + }) + }, + + reorder(queueId: string, payload: IndexOrderPatch) { + return reorderTimeSetsApiQueueQueueIdTimesOrderPatch({ + path: { queue_id: queueId }, + body: payload, + }) + }, +} + +export const queueItemApi = { + list(queueId: string) { + return listQueueItemsApiQueueQueueIdItemsGet({ path: { queue_id: queueId } }) + }, + + create(queueId: string) { + return createQueueItemApiQueueQueueIdItemsPost({ path: { queue_id: queueId } }) + }, + + get(queueId: string, queueItemId: string) { + return getQueueItemApiQueueQueueIdItemsQueueItemIdGet({ + path: { queue_id: queueId, queue_item_id: queueItemId }, + }) + }, + + update(queueId: string, queueItemId: string, payload: QueueItemRead) { + return updateQueueItemApiQueueQueueIdItemsQueueItemIdPatch({ + path: { queue_id: queueId, queue_item_id: queueItemId }, + body: payload, + }) + }, + + remove(queueId: string, queueItemId: string) { + return deleteQueueItemApiQueueQueueIdItemsQueueItemIdDelete({ + path: { queue_id: queueId, queue_item_id: queueItemId }, + }) + }, + + reorder(queueId: string, payload: IndexOrderPatch) { + return reorderQueueItemsApiQueueQueueIdItemsOrderPatch({ + path: { queue_id: queueId }, + body: payload, + }) + }, +} diff --git a/frontend/src/api/gateways/script.ts b/frontend/src/api/gateways/script.ts new file mode 100644 index 00000000..9ba46f63 --- /dev/null +++ b/frontend/src/api/gateways/script.ts @@ -0,0 +1,147 @@ +import { + createScriptApiScriptsPost, + createUserApiScriptsScriptIdUsersPost, + deleteScriptApiScriptsScriptIdDelete, + deleteUserApiScriptsScriptIdUsersUserIdDelete, + exportScriptToFileApiScriptsScriptIdActionsExportFilePost, + getScriptApiScriptsScriptIdGet, + getUserApiScriptsScriptIdUsersUserIdGet, + getUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGet, + importInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePost, + importScriptFromFileApiScriptsScriptIdActionsImportFilePost, + importScriptFromWebApiScriptsImportWebPost, + importScriptFromWebApiScriptsScriptIdActionsImportWebPost, + listScriptsApiScriptsGet, + listUsersApiScriptsScriptIdUsersGet, + reorderScriptsApiScriptsOrderPatch, + reorderUsersApiScriptsScriptIdUsersOrderPatch, + updateScriptApiScriptsScriptIdPatch, + updateUserApiScriptsScriptIdUsersUserIdPatch, + uploadScriptToWebApiScriptsScriptIdActionsUploadWebPost, + uploadScriptToWebApiScriptsUploadWebPost, +} from '../generated/sdk.gen' +import type { + IndexOrderPatch, + InfrastructureImportBody, + ScriptCreateIn, + ScriptPatchBody, + ScriptUploadBody, + ScriptUrlBody, + UserPatchBody, +} from '../generated/types.gen' + +export const scriptApi = { + list() { + return listScriptsApiScriptsGet() + }, + + create(payload: ScriptCreateIn) { + return createScriptApiScriptsPost({ body: payload }) + }, + + get(scriptId: string) { + return getScriptApiScriptsScriptIdGet({ path: { script_id: scriptId } }) + }, + + update(scriptId: string, payload: ScriptPatchBody) { + return updateScriptApiScriptsScriptIdPatch({ + path: { script_id: scriptId }, + body: payload, + }) + }, + + remove(scriptId: string) { + return deleteScriptApiScriptsScriptIdDelete({ path: { script_id: scriptId } }) + }, + + reorder(payload: IndexOrderPatch) { + return reorderScriptsApiScriptsOrderPatch({ body: payload }) + }, + + importFromFile(scriptId: string, path: string) { + return importScriptFromFileApiScriptsScriptIdActionsImportFilePost({ + path: { script_id: scriptId }, + body: { path }, + }) + }, + + exportToFile(scriptId: string, path: string) { + return exportScriptToFileApiScriptsScriptIdActionsExportFilePost({ + path: { script_id: scriptId }, + body: { path }, + }) + }, + + importFromWeb(scriptId: string, body: ScriptUrlBody) { + return importScriptFromWebApiScriptsScriptIdActionsImportWebPost({ + path: { script_id: scriptId }, + body, + }) + }, + + importTemplateFromWeb(body: ScriptUrlBody) { + return importScriptFromWebApiScriptsImportWebPost({ body }) + }, + + uploadToWeb(scriptId: string, body: ScriptUploadBody) { + return uploadScriptToWebApiScriptsScriptIdActionsUploadWebPost({ + path: { script_id: scriptId }, + body, + }) + }, + + uploadTemplateToWeb(body: ScriptUploadBody) { + return uploadScriptToWebApiScriptsUploadWebPost({ body }) + }, +} + +export const userApi = { + list(scriptId: string) { + return listUsersApiScriptsScriptIdUsersGet({ + path: { script_id: scriptId }, + }) + }, + + create(scriptId: string) { + return createUserApiScriptsScriptIdUsersPost({ path: { script_id: scriptId } }) + }, + + get(scriptId: string, userId: string) { + return getUserApiScriptsScriptIdUsersUserIdGet({ + path: { script_id: scriptId, user_id: userId }, + }) + }, + + update(scriptId: string, userId: string, payload: UserPatchBody) { + return updateUserApiScriptsScriptIdUsersUserIdPatch({ + path: { script_id: scriptId, user_id: userId }, + body: payload, + }) + }, + + remove(scriptId: string, userId: string) { + return deleteUserApiScriptsScriptIdUsersUserIdDelete({ + path: { script_id: scriptId, user_id: userId }, + }) + }, + + reorder(scriptId: string, payload: IndexOrderPatch) { + return reorderUsersApiScriptsScriptIdUsersOrderPatch({ + path: { script_id: scriptId }, + body: payload, + }) + }, + + importInfrastructure(scriptId: string, userId: string, body: InfrastructureImportBody) { + return importInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePost({ + path: { script_id: scriptId, user_id: userId }, + body, + }) + }, + + getInfrastructureOptions(scriptId: string, userId: string) { + return getUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGet({ + path: { script_id: scriptId, user_id: userId }, + }) + }, +} diff --git a/frontend/src/api/gateways/setting.ts b/frontend/src/api/gateways/setting.ts new file mode 100644 index 00000000..f96efad2 --- /dev/null +++ b/frontend/src/api/gateways/setting.ts @@ -0,0 +1,108 @@ +import { + createUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPost, + createWebhookApiSettingWebhooksPost, + deleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDelete, + deleteWebhookApiSettingWebhooksWebhookIdDelete, + getSettingApiSettingGet, + getUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGet, + getWebhookApiSettingWebhooksWebhookIdGet, + listUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGet, + listWebhooksApiSettingWebhooksGet, + reorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatch, + reorderWebhooksApiSettingWebhooksOrderPatch, + testNotifyApiSettingActionsTestNotifyPost, + testWebhookApiSettingWebhooksTestPost, + updateSettingApiSettingPatch, + updateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatch, + updateWebhookApiSettingWebhooksWebhookIdPatch, +} from '../generated/sdk.gen' +import type { GlobalConfigRead, IndexOrderPatch, WebhookRead } from '../generated/types.gen' + +export const settingApi = { + get() { + return getSettingApiSettingGet() + }, + + update(payload: GlobalConfigRead) { + return updateSettingApiSettingPatch({ body: payload }) + }, + + testNotify() { + return testNotifyApiSettingActionsTestNotifyPost() + }, +} + +export const webhookApi = { + listGlobal() { + return listWebhooksApiSettingWebhooksGet() + }, + + createGlobal() { + return createWebhookApiSettingWebhooksPost() + }, + + getGlobal(webhookId: string) { + return getWebhookApiSettingWebhooksWebhookIdGet({ + path: { webhook_id: webhookId }, + }) + }, + + updateGlobal(webhookId: string, payload: WebhookRead) { + return updateWebhookApiSettingWebhooksWebhookIdPatch({ + path: { webhook_id: webhookId }, + body: payload, + }) + }, + + removeGlobal(webhookId: string) { + return deleteWebhookApiSettingWebhooksWebhookIdDelete({ + path: { webhook_id: webhookId }, + }) + }, + + reorderGlobal(payload: IndexOrderPatch) { + return reorderWebhooksApiSettingWebhooksOrderPatch({ body: payload }) + }, + + testGlobal(payload: WebhookRead) { + return testWebhookApiSettingWebhooksTestPost({ body: payload }) + }, + + listUser(scriptId: string, userId: string) { + return listUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGet({ + path: { script_id: scriptId, user_id: userId }, + }) + }, + + createUser(scriptId: string, userId: string) { + return createUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPost({ + path: { script_id: scriptId, user_id: userId }, + }) + }, + + getUser(scriptId: string, userId: string, webhookId: string) { + return getUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGet({ + path: { script_id: scriptId, user_id: userId, webhook_id: webhookId }, + }) + }, + + updateUser(scriptId: string, userId: string, webhookId: string, payload: WebhookRead) { + return updateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatch({ + path: { script_id: scriptId, user_id: userId, webhook_id: webhookId }, + body: payload, + }) + }, + + removeUser(scriptId: string, userId: string, webhookId: string) { + return deleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDelete({ + path: { script_id: scriptId, user_id: userId, webhook_id: webhookId }, + }) + }, + + reorderUser(scriptId: string, userId: string, payload: IndexOrderPatch) { + return reorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatch({ + path: { script_id: scriptId, user_id: userId }, + body: payload, + }) + }, +} diff --git a/frontend/src/api/gateways/tools.ts b/frontend/src/api/gateways/tools.ts new file mode 100644 index 00000000..cb152b49 --- /dev/null +++ b/frontend/src/api/gateways/tools.ts @@ -0,0 +1,12 @@ +import { getToolsApiToolsGet, updateToolsApiToolsPatch } from '../generated/sdk.gen' +import type { ToolsConfigRead } from '../generated/types.gen' + +export const toolsApi = { + get() { + return getToolsApiToolsGet() + }, + + update(payload: ToolsConfigRead) { + return updateToolsApiToolsPatch({ body: payload }) + }, +} diff --git a/frontend/src/api/gateways/update.ts b/frontend/src/api/gateways/update.ts new file mode 100644 index 00000000..84b5b70f --- /dev/null +++ b/frontend/src/api/gateways/update.ts @@ -0,0 +1,20 @@ +import { + checkUpdateApiUpdateCheckPost, + downloadUpdateApiUpdateDownloadPost, + installUpdateApiUpdateInstallPost, +} from '../generated/sdk.gen' +import type { UpdateCheckIn } from '../generated/types.gen' + +export const updateApi = { + check(payload: UpdateCheckIn) { + return checkUpdateApiUpdateCheckPost({ body: payload }) + }, + + download() { + return downloadUpdateApiUpdateDownloadPost() + }, + + install() { + return installUpdateApiUpdateInstallPost() + }, +} diff --git a/frontend/src/api/gateways/wsDebug.ts b/frontend/src/api/gateways/wsDebug.ts new file mode 100644 index 00000000..906f13d7 --- /dev/null +++ b/frontend/src/api/gateways/wsDebug.ts @@ -0,0 +1,64 @@ +import { + clearHistoryApiWsDebugHistoryClearPost, + connectClientApiWsDebugClientConnectPost, + createClientApiWsDebugClientCreatePost, + disconnectClientApiWsDebugClientDisconnectPost, + getHistoryApiWsDebugHistoryGet, + listClientsApiWsDebugClientListGet, + removeClientApiWsDebugClientRemovePost, + sendAuthApiWsDebugMessageAuthPost, + sendJsonMessageApiWsDebugMessageSendJsonPost, + sendMessageApiWsDebugMessageSendPost, +} from '../generated/sdk.gen' +import type { + WsClearHistoryIn, + WsClientAuthIn, + WsClientConnectIn, + WsClientCreateIn, + WsClientDisconnectIn, + WsClientRemoveIn, + WsClientSendIn, + WsClientSendJsonIn, +} from '../generated/types.gen' + +export const wsDebugApi = { + listClients() { + return listClientsApiWsDebugClientListGet() + }, + + createClient(payload: WsClientCreateIn) { + return createClientApiWsDebugClientCreatePost({ body: payload }) + }, + + connectClient(payload: WsClientConnectIn) { + return connectClientApiWsDebugClientConnectPost({ body: payload }) + }, + + disconnectClient(payload: WsClientDisconnectIn) { + return disconnectClientApiWsDebugClientDisconnectPost({ body: payload }) + }, + + removeClient(payload: WsClientRemoveIn) { + return removeClientApiWsDebugClientRemovePost({ body: payload }) + }, + + sendJson(payload: WsClientSendJsonIn) { + return sendJsonMessageApiWsDebugMessageSendJsonPost({ body: payload }) + }, + + sendMessage(payload: WsClientSendIn) { + return sendMessageApiWsDebugMessageSendPost({ body: payload }) + }, + + sendAuth(payload: WsClientAuthIn) { + return sendAuthApiWsDebugMessageAuthPost({ body: payload }) + }, + + clearHistory(payload: WsClearHistoryIn) { + return clearHistoryApiWsDebugHistoryClearPost({ body: payload }) + }, + + getHistory() { + return getHistoryApiWsDebugHistoryGet() + }, +} diff --git a/frontend/src/api/generated/client.gen.ts b/frontend/src/api/generated/client.gen.ts new file mode 100644 index 00000000..cab3c701 --- /dev/null +++ b/frontend/src/api/generated/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/frontend/src/api/generated/client/client.gen.ts b/frontend/src/api/generated/client/client.gen.ts new file mode 100644 index 00000000..a439d274 --- /dev/null +++ b/frontend/src/api/generated/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/frontend/src/api/generated/client/index.ts b/frontend/src/api/generated/client/index.ts new file mode 100644 index 00000000..cbf8dfee --- /dev/null +++ b/frontend/src/api/generated/client/index.ts @@ -0,0 +1,26 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/frontend/src/api/generated/client/types.gen.ts b/frontend/src/api/generated/client/types.gen.ts new file mode 100644 index 00000000..5c70971f --- /dev/null +++ b/frontend/src/api/generated/client/types.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'headers' | 'url' + > & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/frontend/src/api/generated/client/utils.gen.ts b/frontend/src/api/generated/client/utils.gen.ts new file mode 100644 index 00000000..4c48a9ee --- /dev/null +++ b/frontend/src/api/generated/client/utils.gen.ts @@ -0,0 +1,332 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/frontend/src/api/generated/core/auth.gen.ts b/frontend/src/api/generated/core/auth.gen.ts new file mode 100644 index 00000000..f8a73266 --- /dev/null +++ b/frontend/src/api/generated/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/frontend/src/api/generated/core/bodySerializer.gen.ts b/frontend/src/api/generated/core/bodySerializer.gen.ts new file mode 100644 index 00000000..552b50f7 --- /dev/null +++ b/frontend/src/api/generated/core/bodySerializer.gen.ts @@ -0,0 +1,100 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/frontend/src/api/generated/core/params.gen.ts b/frontend/src/api/generated/core/params.gen.ts new file mode 100644 index 00000000..602715c4 --- /dev/null +++ b/frontend/src/api/generated/core/params.gen.ts @@ -0,0 +1,176 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/frontend/src/api/generated/core/pathSerializer.gen.ts b/frontend/src/api/generated/core/pathSerializer.gen.ts new file mode 100644 index 00000000..8d999310 --- /dev/null +++ b/frontend/src/api/generated/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/frontend/src/api/generated/core/queryKeySerializer.gen.ts b/frontend/src/api/generated/core/queryKeySerializer.gen.ts new file mode 100644 index 00000000..d3bb6839 --- /dev/null +++ b/frontend/src/api/generated/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/frontend/src/api/generated/core/serverSentEvents.gen.ts b/frontend/src/api/generated/core/serverSentEvents.gen.ts new file mode 100644 index 00000000..f8fd78e2 --- /dev/null +++ b/frontend/src/api/generated/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/frontend/src/api/generated/core/types.gen.ts b/frontend/src/api/generated/core/types.gen.ts new file mode 100644 index 00000000..643c070c --- /dev/null +++ b/frontend/src/api/generated/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/frontend/src/api/generated/core/utils.gen.ts b/frontend/src/api/generated/core/utils.gen.ts new file mode 100644 index 00000000..0b5389d0 --- /dev/null +++ b/frontend/src/api/generated/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/frontend/src/api/generated/index.ts b/frontend/src/api/generated/index.ts new file mode 100644 index 00000000..c352c104 --- /dev/null +++ b/frontend/src/api/generated/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type * from './types.gen'; +export * from './sdk.gen'; diff --git a/frontend/src/api/generated/sdk.gen.ts b/frontend/src/api/generated/sdk.gen.ts new file mode 100644 index 00000000..4b50780c --- /dev/null +++ b/frontend/src/api/generated/sdk.gen.ts @@ -0,0 +1,1487 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { AddTaskApiDispatchStartPostData, AddTaskApiDispatchStartPostErrors, AddTaskApiDispatchStartPostResponses, CancelPowerTaskApiDispatchCancelPowerPostData, CancelPowerTaskApiDispatchCancelPowerPostResponses, CheckImageAllApiOcrCheckImageAllPostData, CheckImageAllApiOcrCheckImageAllPostErrors, CheckImageAllApiOcrCheckImageAllPostResponses, CheckImageAnyApiOcrCheckImageAnyPostData, CheckImageAnyApiOcrCheckImageAnyPostErrors, CheckImageAnyApiOcrCheckImageAnyPostResponses, CheckImageApiOcrCheckImagePostData, CheckImageApiOcrCheckImagePostErrors, CheckImageApiOcrCheckImagePostResponses, CheckUpdateApiUpdateCheckPostData, CheckUpdateApiUpdateCheckPostErrors, CheckUpdateApiUpdateCheckPostResponses, CheckUpdateRestApiUpdateCheckGetData, CheckUpdateRestApiUpdateCheckGetErrors, CheckUpdateRestApiUpdateCheckGetResponses, ClearHistoryApiWsDebugHistoryClearPostData, ClearHistoryApiWsDebugHistoryClearPostErrors, ClearHistoryApiWsDebugHistoryClearPostResponses, ClickImageApiOcrClickImagePostData, ClickImageApiOcrClickImagePostErrors, ClickImageApiOcrClickImagePostResponses, ClickTextApiOcrClickTextPostData, ClickTextApiOcrClickTextPostErrors, ClickTextApiOcrClickTextPostResponses, CloseApiCoreClosePostData, CloseApiCoreClosePostResponses, ConfirmNoticeApiInfoNoticeConfirmPostData, ConfirmNoticeApiInfoNoticeConfirmPostResponses, ConnectClientApiWsDebugClientConnectPostData, ConnectClientApiWsDebugClientConnectPostErrors, ConnectClientApiWsDebugClientConnectPostResponses, CreateClientApiWsDebugClientCreatePostData, CreateClientApiWsDebugClientCreatePostErrors, CreateClientApiWsDebugClientCreatePostResponses, CreateEmulatorApiEmulatorPostData, CreateEmulatorApiEmulatorPostResponses, CreatePlanApiPlanPostData, CreatePlanApiPlanPostErrors, CreatePlanApiPlanPostResponses, CreateQueueApiQueuePostData, CreateQueueApiQueuePostResponses, CreateQueueItemApiQueueQueueIdItemsPostData, CreateQueueItemApiQueueQueueIdItemsPostErrors, CreateQueueItemApiQueueQueueIdItemsPostResponses, CreateScriptApiScriptsPostData, CreateScriptApiScriptsPostErrors, CreateScriptApiScriptsPostResponses, CreateTimeSetApiQueueQueueIdTimesPostData, CreateTimeSetApiQueueQueueIdTimesPostErrors, CreateTimeSetApiQueueQueueIdTimesPostResponses, CreateUserApiScriptsScriptIdUsersPostData, CreateUserApiScriptsScriptIdUsersPostErrors, CreateUserApiScriptsScriptIdUsersPostResponses, CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostData, CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostErrors, CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostResponses, CreateWebhookApiSettingWebhooksPostData, CreateWebhookApiSettingWebhooksPostResponses, DeleteEmulatorApiEmulatorEmulatorIdDeleteData, DeleteEmulatorApiEmulatorEmulatorIdDeleteErrors, DeleteEmulatorApiEmulatorEmulatorIdDeleteResponses, DeletePlanApiPlanPlanIdDeleteData, DeletePlanApiPlanPlanIdDeleteErrors, DeletePlanApiPlanPlanIdDeleteResponses, DeleteQueueApiQueueQueueIdDeleteData, DeleteQueueApiQueueQueueIdDeleteErrors, DeleteQueueApiQueueQueueIdDeleteResponses, DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteData, DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteErrors, DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteResponses, DeleteScriptApiScriptsScriptIdDeleteData, DeleteScriptApiScriptsScriptIdDeleteErrors, DeleteScriptApiScriptsScriptIdDeleteResponses, DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteData, DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteErrors, DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteResponses, DeleteUserApiScriptsScriptIdUsersUserIdDeleteData, DeleteUserApiScriptsScriptIdUsersUserIdDeleteErrors, DeleteUserApiScriptsScriptIdUsersUserIdDeleteResponses, DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteData, DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteErrors, DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteResponses, DeleteWebhookApiSettingWebhooksWebhookIdDeleteData, DeleteWebhookApiSettingWebhooksWebhookIdDeleteErrors, DeleteWebhookApiSettingWebhooksWebhookIdDeleteResponses, DetectEmulatorsApiEmulatorDetectedGetData, DetectEmulatorsApiEmulatorDetectedGetResponses, DisconnectClientApiWsDebugClientDisconnectPostData, DisconnectClientApiWsDebugClientDisconnectPostErrors, DisconnectClientApiWsDebugClientDisconnectPostResponses, DownloadUpdateApiUpdateDownloadPostData, DownloadUpdateApiUpdateDownloadPostResponses, ExportScriptToFileApiScriptsScriptIdActionsExportFilePostData, ExportScriptToFileApiScriptsScriptIdActionsExportFilePostErrors, ExportScriptToFileApiScriptsScriptIdActionsExportFilePostResponses, GetClientStatusApiWsDebugClientStatusPostData, GetClientStatusApiWsDebugClientStatusPostErrors, GetClientStatusApiWsDebugClientStatusPostResponses, GetCommandsApiWsDebugCommandsGetData, GetCommandsApiWsDebugCommandsGetResponses, GetEmulatorApiEmulatorEmulatorIdGetData, GetEmulatorApiEmulatorEmulatorIdGetErrors, GetEmulatorApiEmulatorEmulatorIdGetResponses, GetEmulatorComboxApiInfoComboxEmulatorPostData, GetEmulatorComboxApiInfoComboxEmulatorPostResponses, GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostData, GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostErrors, GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostResponses, GetEmulatorStatusApiEmulatorEmulatorIdStatusGetData, GetEmulatorStatusApiEmulatorEmulatorIdStatusGetErrors, GetEmulatorStatusApiEmulatorEmulatorIdStatusGetResponses, GetEmulatorStatusesApiEmulatorStatusGetData, GetEmulatorStatusesApiEmulatorStatusGetResponses, GetGitVersionApiInfoVersionPostData, GetGitVersionApiInfoVersionPostResponses, GetHistoryApiWsDebugHistoryGetData, GetHistoryApiWsDebugHistoryGetErrors, GetHistoryApiWsDebugHistoryGetResponses, GetHistoryDataApiHistoryDataPostData, GetHistoryDataApiHistoryDataPostErrors, GetHistoryDataApiHistoryDataPostResponses, GetNoticeInfoApiInfoNoticeGetPostData, GetNoticeInfoApiInfoNoticeGetPostResponses, GetOverviewApiInfoGetOverviewPostData, GetOverviewApiInfoGetOverviewPostResponses, GetPlanApiPlanPlanIdGetData, GetPlanApiPlanPlanIdGetErrors, GetPlanApiPlanPlanIdGetResponses, GetPlanComboxApiInfoComboxPlanPostData, GetPlanComboxApiInfoComboxPlanPostResponses, GetPowerApiDispatchGetPowerPostData, GetPowerApiDispatchGetPowerPostResponses, GetQueueApiQueueQueueIdGetData, GetQueueApiQueueQueueIdGetErrors, GetQueueApiQueueQueueIdGetResponses, GetQueueItemApiQueueQueueIdItemsQueueItemIdGetData, GetQueueItemApiQueueQueueIdItemsQueueItemIdGetErrors, GetQueueItemApiQueueQueueIdItemsQueueItemIdGetResponses, GetScreenshotAdbApiOcrScreenshotAdbPostData, GetScreenshotAdbApiOcrScreenshotAdbPostErrors, GetScreenshotAdbApiOcrScreenshotAdbPostResponses, GetScreenshotApiOcrScreenshotPostData, GetScreenshotApiOcrScreenshotPostErrors, GetScreenshotApiOcrScreenshotPostResponses, GetScriptApiScriptsScriptIdGetData, GetScriptApiScriptsScriptIdGetErrors, GetScriptApiScriptsScriptIdGetResponses, GetScriptComboxApiInfoComboxScriptPostData, GetScriptComboxApiInfoComboxScriptPostResponses, GetSettingApiSettingGetData, GetSettingApiSettingGetResponses, GetStageComboxApiInfoComboxStagePostData, GetStageComboxApiInfoComboxStagePostErrors, GetStageComboxApiInfoComboxStagePostResponses, GetTaskComboxApiInfoComboxTaskPostData, GetTaskComboxApiInfoComboxTaskPostResponses, GetTimeSetApiQueueQueueIdTimesTimeSetIdGetData, GetTimeSetApiQueueQueueIdTimesTimeSetIdGetErrors, GetTimeSetApiQueueQueueIdTimesTimeSetIdGetResponses, GetToolsApiToolsGetData, GetToolsApiToolsGetResponses, GetUserApiScriptsScriptIdUsersUserIdGetData, GetUserApiScriptsScriptIdUsersUserIdGetErrors, GetUserApiScriptsScriptIdUsersUserIdGetResponses, GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetData, GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetErrors, GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetResponses, GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetData, GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetErrors, GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetResponses, GetWebConfigApiInfoWebconfigPostData, GetWebConfigApiInfoWebconfigPostResponses, GetWebhookApiSettingWebhooksWebhookIdGetData, GetWebhookApiSettingWebhooksWebhookIdGetErrors, GetWebhookApiSettingWebhooksWebhookIdGetResponses, ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostData, ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostErrors, ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostResponses, ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostData, ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostErrors, ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostResponses, ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostData, ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostErrors, ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostResponses, InstallUpdateApiUpdateInstallPostData, InstallUpdateApiUpdateInstallPostResponses, ListClientsApiWsDebugClientListGetData, ListClientsApiWsDebugClientListGetResponses, ListEmulatorsApiEmulatorGetData, ListEmulatorsApiEmulatorGetResponses, ListPlansApiPlanGetData, ListPlansApiPlanGetResponses, ListQueueItemsApiQueueQueueIdItemsGetData, ListQueueItemsApiQueueQueueIdItemsGetErrors, ListQueueItemsApiQueueQueueIdItemsGetResponses, ListQueuesApiQueueGetData, ListQueuesApiQueueGetResponses, ListScriptsApiScriptsGetData, ListScriptsApiScriptsGetResponses, ListTimeSetsApiQueueQueueIdTimesGetData, ListTimeSetsApiQueueQueueIdTimesGetErrors, ListTimeSetsApiQueueQueueIdTimesGetResponses, ListUsersApiScriptsScriptIdUsersGetData, ListUsersApiScriptsScriptIdUsersGetErrors, ListUsersApiScriptsScriptIdUsersGetResponses, ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetData, ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetErrors, ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetResponses, ListWebhooksApiSettingWebhooksGetData, ListWebhooksApiSettingWebhooksGetResponses, OperateEmulatorApiEmulatorEmulatorIdActionsActionPostData, OperateEmulatorApiEmulatorEmulatorIdActionsActionPostErrors, OperateEmulatorApiEmulatorEmulatorIdActionsActionPostResponses, RemoveClientApiWsDebugClientRemovePostData, RemoveClientApiWsDebugClientRemovePostErrors, RemoveClientApiWsDebugClientRemovePostResponses, ReorderEmulatorApiEmulatorOrderPatchData, ReorderEmulatorApiEmulatorOrderPatchErrors, ReorderEmulatorApiEmulatorOrderPatchResponses, ReorderPlanApiPlanOrderPatchData, ReorderPlanApiPlanOrderPatchErrors, ReorderPlanApiPlanOrderPatchResponses, ReorderQueueApiQueueOrderPatchData, ReorderQueueApiQueueOrderPatchErrors, ReorderQueueApiQueueOrderPatchResponses, ReorderQueueItemsApiQueueQueueIdItemsOrderPatchData, ReorderQueueItemsApiQueueQueueIdItemsOrderPatchErrors, ReorderQueueItemsApiQueueQueueIdItemsOrderPatchResponses, ReorderScriptsApiScriptsOrderPatchData, ReorderScriptsApiScriptsOrderPatchErrors, ReorderScriptsApiScriptsOrderPatchResponses, ReorderTimeSetsApiQueueQueueIdTimesOrderPatchData, ReorderTimeSetsApiQueueQueueIdTimesOrderPatchErrors, ReorderTimeSetsApiQueueQueueIdTimesOrderPatchResponses, ReorderUsersApiScriptsScriptIdUsersOrderPatchData, ReorderUsersApiScriptsScriptIdUsersOrderPatchErrors, ReorderUsersApiScriptsScriptIdUsersOrderPatchResponses, ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchData, ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchErrors, ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchResponses, ReorderWebhooksApiSettingWebhooksOrderPatchData, ReorderWebhooksApiSettingWebhooksOrderPatchErrors, ReorderWebhooksApiSettingWebhooksOrderPatchResponses, SearchHistoryApiHistorySearchPostData, SearchHistoryApiHistorySearchPostErrors, SearchHistoryApiHistorySearchPostResponses, SendAuthApiWsDebugMessageAuthPostData, SendAuthApiWsDebugMessageAuthPostErrors, SendAuthApiWsDebugMessageAuthPostResponses, SendJsonMessageApiWsDebugMessageSendJsonPostData, SendJsonMessageApiWsDebugMessageSendJsonPostErrors, SendJsonMessageApiWsDebugMessageSendJsonPostResponses, SendMessageApiWsDebugMessageSendPostData, SendMessageApiWsDebugMessageSendPostErrors, SendMessageApiWsDebugMessageSendPostResponses, SetPowerApiDispatchSetPowerPostData, SetPowerApiDispatchSetPowerPostErrors, SetPowerApiDispatchSetPowerPostResponses, StopTaskApiDispatchStopPostData, StopTaskApiDispatchStopPostErrors, StopTaskApiDispatchStopPostResponses, TestNotifyApiSettingActionsTestNotifyPostData, TestNotifyApiSettingActionsTestNotifyPostResponses, TestWebhookApiSettingWebhooksTestPostData, TestWebhookApiSettingWebhooksTestPostErrors, TestWebhookApiSettingWebhooksTestPostResponses, UpdateEmulatorApiEmulatorEmulatorIdPatchData, UpdateEmulatorApiEmulatorEmulatorIdPatchErrors, UpdateEmulatorApiEmulatorEmulatorIdPatchResponses, UpdatePlanApiPlanPlanIdPatchData, UpdatePlanApiPlanPlanIdPatchErrors, UpdatePlanApiPlanPlanIdPatchResponses, UpdateQueueApiQueueQueueIdPatchData, UpdateQueueApiQueueQueueIdPatchErrors, UpdateQueueApiQueueQueueIdPatchResponses, UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchData, UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchErrors, UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchResponses, UpdateScriptApiScriptsScriptIdPatchData, UpdateScriptApiScriptsScriptIdPatchErrors, UpdateScriptApiScriptsScriptIdPatchResponses, UpdateSettingApiSettingPatchData, UpdateSettingApiSettingPatchErrors, UpdateSettingApiSettingPatchResponses, UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchData, UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchErrors, UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchResponses, UpdateToolsApiToolsPatchData, UpdateToolsApiToolsPatchErrors, UpdateToolsApiToolsPatchResponses, UpdateUserApiScriptsScriptIdUsersUserIdPatchData, UpdateUserApiScriptsScriptIdUsersUserIdPatchErrors, UpdateUserApiScriptsScriptIdUsersUserIdPatchResponses, UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchData, UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchErrors, UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchResponses, UpdateWebhookApiSettingWebhooksWebhookIdPatchData, UpdateWebhookApiSettingWebhooksWebhookIdPatchErrors, UpdateWebhookApiSettingWebhooksWebhookIdPatchResponses, UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostData, UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostErrors, UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostResponses } from './types.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * 关闭后端程序 + * + * 关闭后端程序 + */ +export const closeApiCoreClosePost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/core/close', + ...options + }); +}; + +/** + * 获取后端git版本信息 + */ +export const getGitVersionApiInfoVersionPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/version', + ...options + }); +}; + +/** + * 获取关卡号下拉框信息 + */ +export const getStageComboxApiInfoComboxStagePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/info/combox/stage', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 获取脚本下拉框信息 + */ +export const getScriptComboxApiInfoComboxScriptPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/combox/script', + ...options + }); +}; + +/** + * 获取可选任务下拉框信息 + */ +export const getTaskComboxApiInfoComboxTaskPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/combox/task', + ...options + }); +}; + +/** + * 获取可选计划下拉框信息 + */ +export const getPlanComboxApiInfoComboxPlanPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/combox/plan', + ...options + }); +}; + +/** + * 获取可选模拟器下拉框信息 + */ +export const getEmulatorComboxApiInfoComboxEmulatorPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/combox/emulator', + ...options + }); +}; + +/** + * 获取可选模拟器多开实例下拉框信息 + */ +export const getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/info/combox/emulator/devices', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 获取通知信息 + */ +export const getNoticeInfoApiInfoNoticeGetPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/notice/get', + ...options + }); +}; + +/** + * 确认通知 + */ +export const confirmNoticeApiInfoNoticeConfirmPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/notice/confirm', + ...options + }); +}; + +/** + * 获取配置分享中心的配置信息 + */ +export const getWebConfigApiInfoWebconfigPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/webconfig', + ...options + }); +}; + +/** + * 信息总览 + */ +export const getOverviewApiInfoGetOverviewPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/info/get/overview', + ...options + }); +}; + +/** + * 查询全部脚本 + */ +export const listScriptsApiScriptsGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/scripts', + ...options + }); +}; + +/** + * 创建脚本 + */ +export const createScriptApiScriptsPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 重新排序脚本 + */ +export const reorderScriptsApiScriptsOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/scripts/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除脚本 + */ +export const deleteScriptApiScriptsScriptIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/scripts/{script_id}', + ...options + }); +}; + +/** + * 查询单个脚本 + */ +export const getScriptApiScriptsScriptIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/scripts/{script_id}', + ...options + }); +}; + +/** + * 更新脚本配置 + */ +export const updateScriptApiScriptsScriptIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/scripts/{script_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 从文件导入脚本配置 + */ +export const importScriptFromFileApiScriptsScriptIdActionsImportFilePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/actions/import-file', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 导出脚本配置到文件 + */ +export const exportScriptToFileApiScriptsScriptIdActionsExportFilePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/actions/export-file', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 从网络导入脚本配置 + */ +export const importScriptFromWebApiScriptsScriptIdActionsImportWebPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/actions/import-web', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 上传脚本配置到网络 + */ +export const uploadScriptToWebApiScriptsScriptIdActionsUploadWebPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/actions/upload-web', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询脚本下的全部用户 + */ +export const listUsersApiScriptsScriptIdUsersGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/scripts/{script_id}/users', + ...options + }); +}; + +/** + * 创建用户 + */ +export const createUserApiScriptsScriptIdUsersPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/users', + ...options + }); +}; + +/** + * 重新排序用户 + */ +export const reorderUsersApiScriptsScriptIdUsersOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/scripts/{script_id}/users/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除用户 + */ +export const deleteUserApiScriptsScriptIdUsersUserIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/scripts/{script_id}/users/{user_id}', + ...options + }); +}; + +/** + * 查询单个用户 + */ +export const getUserApiScriptsScriptIdUsersUserIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/scripts/{script_id}/users/{user_id}', + ...options + }); +}; + +/** + * 更新用户配置 + */ +export const updateUserApiScriptsScriptIdUsersUserIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/scripts/{script_id}/users/{user_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 导入基建配置文件 + */ +export const importInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/users/{user_id}/actions/import-infrastructure', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 用户自定义基建排班可选项 + */ +export const getUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/scripts/{script_id}/users/{user_id}/infrastructure-options', + ...options + }); +}; + +/** + * 查询用户下的全部 Webhook + */ +export const listUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/scripts/{script_id}/users/{user_id}/webhooks', + ...options + }); +}; + +/** + * 创建用户 Webhook + */ +export const createUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/scripts/{script_id}/users/{user_id}/webhooks', + ...options + }); +}; + +/** + * 重新排序用户 Webhook + */ +export const reorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除用户 Webhook + */ +export const deleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}', + ...options + }); +}; + +/** + * 查询单个用户 Webhook + */ +export const getUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}', + ...options + }); +}; + +/** + * 更新用户 Webhook + */ +export const updateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询全部计划表 + */ +export const listPlansApiPlanGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/plan', + ...options + }); +}; + +/** + * 创建计划表 + */ +export const createPlanApiPlanPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/plan', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除计划表 + */ +export const deletePlanApiPlanPlanIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/plan/{plan_id}', + ...options + }); +}; + +/** + * 查询单个计划表 + */ +export const getPlanApiPlanPlanIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/plan/{plan_id}', + ...options + }); +}; + +/** + * 更新计划表 + */ +export const updatePlanApiPlanPlanIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/plan/{plan_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 重新排序计划表 + */ +export const reorderPlanApiPlanOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/plan/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询全部模拟器配置 + */ +export const listEmulatorsApiEmulatorGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/emulator', + ...options + }); +}; + +/** + * 创建模拟器配置 + */ +export const createEmulatorApiEmulatorPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/emulator', + ...options + }); +}; + +/** + * 重新排序模拟器 + */ +export const reorderEmulatorApiEmulatorOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/emulator/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 搜索已安装的模拟器 + */ +export const detectEmulatorsApiEmulatorDetectedGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/emulator/detected', + ...options + }); +}; + +/** + * 查询全部模拟器状态 + */ +export const getEmulatorStatusesApiEmulatorStatusGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/emulator/status', + ...options + }); +}; + +/** + * 删除模拟器配置 + */ +export const deleteEmulatorApiEmulatorEmulatorIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/emulator/{emulator_id}', + ...options + }); +}; + +/** + * 查询单个模拟器配置 + */ +export const getEmulatorApiEmulatorEmulatorIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/emulator/{emulator_id}', + ...options + }); +}; + +/** + * 更新模拟器配置 + */ +export const updateEmulatorApiEmulatorEmulatorIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/emulator/{emulator_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询单个模拟器状态 + */ +export const getEmulatorStatusApiEmulatorEmulatorIdStatusGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/emulator/{emulator_id}/status', + ...options + }); +}; + +/** + * 执行模拟器动作 + */ +export const operateEmulatorApiEmulatorEmulatorIdActionsActionPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/emulator/{emulator_id}/actions/{action}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询全部调度队列 + */ +export const listQueuesApiQueueGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/queue', + ...options + }); +}; + +/** + * 创建调度队列 + */ +export const createQueueApiQueuePost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/queue', + ...options + }); +}; + +/** + * 重新排序调度队列 + */ +export const reorderQueueApiQueueOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/queue/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除调度队列 + */ +export const deleteQueueApiQueueQueueIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/queue/{queue_id}', + ...options + }); +}; + +/** + * 查询单个调度队列 + */ +export const getQueueApiQueueQueueIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/queue/{queue_id}', + ...options + }); +}; + +/** + * 更新调度队列 + */ +export const updateQueueApiQueueQueueIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/queue/{queue_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询队列下的全部定时项 + */ +export const listTimeSetsApiQueueQueueIdTimesGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/queue/{queue_id}/times', + ...options + }); +}; + +/** + * 创建定时项 + */ +export const createTimeSetApiQueueQueueIdTimesPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/queue/{queue_id}/times', + ...options + }); +}; + +/** + * 重新排序定时项 + */ +export const reorderTimeSetsApiQueueQueueIdTimesOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/queue/{queue_id}/times/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除定时项 + */ +export const deleteTimeSetApiQueueQueueIdTimesTimeSetIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/queue/{queue_id}/times/{time_set_id}', + ...options + }); +}; + +/** + * 查询单个定时项 + */ +export const getTimeSetApiQueueQueueIdTimesTimeSetIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/queue/{queue_id}/times/{time_set_id}', + ...options + }); +}; + +/** + * 更新定时项 + */ +export const updateTimeSetApiQueueQueueIdTimesTimeSetIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/queue/{queue_id}/times/{time_set_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询队列下的全部队列项 + */ +export const listQueueItemsApiQueueQueueIdItemsGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/queue/{queue_id}/items', + ...options + }); +}; + +/** + * 创建队列项 + */ +export const createQueueItemApiQueueQueueIdItemsPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/queue/{queue_id}/items', + ...options + }); +}; + +/** + * 重新排序队列项 + */ +export const reorderQueueItemsApiQueueQueueIdItemsOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/queue/{queue_id}/items/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除队列项 + */ +export const deleteQueueItemApiQueueQueueIdItemsQueueItemIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/queue/{queue_id}/items/{queue_item_id}', + ...options + }); +}; + +/** + * 查询单个队列项 + */ +export const getQueueItemApiQueueQueueIdItemsQueueItemIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/queue/{queue_id}/items/{queue_item_id}', + ...options + }); +}; + +/** + * 更新队列项 + */ +export const updateQueueItemApiQueueQueueIdItemsQueueItemIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/queue/{queue_id}/items/{queue_item_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 添加任务 + */ +export const addTaskApiDispatchStartPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/dispatch/start', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 中止任务 + */ +export const stopTaskApiDispatchStopPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/dispatch/stop', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 获取电源标志 + */ +export const getPowerApiDispatchGetPowerPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/dispatch/get/power', + ...options + }); +}; + +/** + * 设置电源标志 + */ +export const setPowerApiDispatchSetPowerPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/dispatch/set/power', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 取消电源任务 + */ +export const cancelPowerTaskApiDispatchCancelPowerPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/dispatch/cancel/power', + ...options + }); +}; + +/** + * 搜索历史记录总览信息 + */ +export const searchHistoryApiHistorySearchPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/history/search', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 从指定文件内获取历史记录数据 + */ +export const getHistoryDataApiHistoryDataPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/history/data', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询工具配置 + */ +export const getToolsApiToolsGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/tools', + ...options + }); +}; + +/** + * 更新工具配置 + */ +export const updateToolsApiToolsPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/tools', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 查询全局配置 + */ +export const getSettingApiSettingGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/setting', + ...options + }); +}; + +/** + * 更新全局配置 + */ +export const updateSettingApiSettingPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/setting', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 测试通知 + */ +export const testNotifyApiSettingActionsTestNotifyPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/setting/actions/test-notify', + ...options + }); +}; + +/** + * 查询全部全局 Webhook 配置 + */ +export const listWebhooksApiSettingWebhooksGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/setting/webhooks', + ...options + }); +}; + +/** + * 创建全局 Webhook 配置 + */ +export const createWebhookApiSettingWebhooksPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/setting/webhooks', + ...options + }); +}; + +/** + * 重新排序全局 Webhook + */ +export const reorderWebhooksApiSettingWebhooksOrderPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/setting/webhooks/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 测试指定 Webhook 配置 + */ +export const testWebhookApiSettingWebhooksTestPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/setting/webhooks/test', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除全局 Webhook 配置 + */ +export const deleteWebhookApiSettingWebhooksWebhookIdDelete = (options: Options) => { + return (options.client ?? client).delete({ + url: '/api/setting/webhooks/{webhook_id}', + ...options + }); +}; + +/** + * 查询单个全局 Webhook 配置 + */ +export const getWebhookApiSettingWebhooksWebhookIdGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/setting/webhooks/{webhook_id}', + ...options + }); +}; + +/** + * 更新全局 Webhook 配置 + */ +export const updateWebhookApiSettingWebhooksWebhookIdPatch = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/setting/webhooks/{webhook_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 按 REST 风格检查更新 + */ +export const checkUpdateRestApiUpdateCheckGet = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/update/check', + ...options + }); +}; + +/** + * 检查更新 + */ +export const checkUpdateApiUpdateCheckPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/update/check', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 下载更新 + */ +export const downloadUpdateApiUpdateDownloadPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/update/download', + ...options + }); +}; + +/** + * 安装更新 + */ +export const installUpdateApiUpdateInstallPost = (options?: Options) => { + return (options?.client ?? client).post({ + url: '/api/update/install', + ...options + }); +}; + +/** + * 获取窗口截图 + * + * 根据窗口标题获取截图,返回Base64编码的图像数据 + * + * Args: + * params: 截图参数 + * - window_title: 窗口标题关键字 + * - should_preprocess: 是否预处理图片区域(默认True) + * - aspect_ratio_width: 宽高比宽度(默认16) + * - aspect_ratio_height: 宽高比高度(默认9) + * - region: 自定义截图区域,格式为 (left, top, width, height) + * + * Returns: + * OCRScreenshotOut: 包含Base64编码的截图和区域信息 + */ +export const getScreenshotApiOcrScreenshotPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/screenshot', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 通过ADB获取设备截图 + * + * 通过 ADB 端口获取 Android 设备/模拟器截图,返回Base64编码的图像数据 + * + * 支持两种截图方法: + * 1. screencap PNG 方法(推荐):速度快,直接获取 PNG 图像 + * 2. screencap raw 方法:获取原始像素数据,适用于某些不支持 PNG 的设备 + * + * Args: + * params: ADB 截图参数 + * - adb_path: ADB 可执行文件的路径 + * - serial: 设备序列号,格式如 "127.0.0.1:5555" 或 "emulator-5554" + * - use_screencap: 是否使用 screencap PNG 方法(默认True) + * + * Returns: + * ADBScreenshotOut: 包含Base64编码的截图和设备信息 + */ +export const getScreenshotAdbApiOcrScreenshotAdbPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/screenshot/adb', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 检查是否存在指定图像 + * + * 截图并查找是否存在图片内的内容 + * + * Args: + * params: 检查图像参数 + * - window_title: 窗口标题关键字 + * - image_path: 要查找的图片路径 + * - interval: 截图间隔时间(秒),默认为 0 + * - retry_times: 重复截图次数,默认为 1 + * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 + * + * Returns: + * CheckImageOut: 包含查找结果和尝试次数 + */ +export const checkImageApiOcrCheckImagePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/check/image', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 检查是否存在任意一个指定图像 + * + * 截图并查找是否存在列表中任意一张图片的内容 + * + * Args: + * params: 检查图像参数 + * - window_title: 窗口标题关键字 + * - image_paths: 要查找的图片路径列表 + * - interval: 截图间隔时间(秒),默认为 0 + * - retry_times: 重复截图次数,默认为 1 + * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 + * + * Returns: + * CheckImageOut: 包含查找结果和尝试次数 + */ +export const checkImageAnyApiOcrCheckImageAnyPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/check/image/any', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 检查是否存在所有指定图像 + * + * 截图并查找是否存在列表中所有图片的内容 + * + * Args: + * params: 检查图像参数 + * - window_title: 窗口标题关键字 + * - image_paths: 要查找的图片路径列表 + * - interval: 截图间隔时间(秒),默认为 0 + * - retry_times: 重复截图次数,默认为 1 + * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 + * + * Returns: + * CheckImageOut: 包含查找结果和尝试次数 + */ +export const checkImageAllApiOcrCheckImageAllPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/check/image/all', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 点击指定图像位置 + * + * 截图、查找并点击与图像一致的位置 + * + * Args: + * params: 点击图像参数 + * - window_title: 窗口标题关键字 + * - image_path: 要查找并点击的图片路径 + * - interval: 截图间隔时间(秒),默认为 0 + * - retry_times: 重复截图次数,默认为 1 + * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 + * + * Returns: + * ClickOut: 包含点击结果和尝试次数 + */ +export const clickImageApiOcrClickImagePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/click/image', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 点击指定文字位置 + * + * 截图、OCR识别并点击与文字一致的位置 + * + * Args: + * params: 点击文字参数 + * - window_title: 窗口标题关键字 + * - text: 要查找并点击的文字内容 + * - interval: 截图间隔时间(秒),默认为 0 + * - retry_times: 重复截图次数,默认为 1 + * + * Returns: + * ClickOut: 包含点击结果和尝试次数 + */ +export const clickTextApiOcrClickTextPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ocr/click/text', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 创建 WebSocket 客户端 + * + * 创建一个新的 WebSocket 客户端实例 + * + * - **name**: 客户端唯一名称 + * - **url**: WebSocket 服务器地址 + * - **ping_interval**: 心跳发送间隔 + * - **ping_timeout**: 心跳超时时间 + * - **reconnect_interval**: 重连间隔 + * - **max_reconnect_attempts**: 最大重连次数 + */ +export const createClientApiWsDebugClientCreatePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/client/create', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 连接 WebSocket 客户端 + * + * 启动指定客户端的连接(非阻塞) + */ +export const connectClientApiWsDebugClientConnectPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/client/connect', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 断开 WebSocket 客户端 + * + * 断开指定客户端的连接 + */ +export const disconnectClientApiWsDebugClientDisconnectPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/client/disconnect', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 删除 WebSocket 客户端 + * + * 删除指定客户端(会自动断开连接) + * + * 注意:系统客户端(如 Koishi)不可删除 + */ +export const removeClientApiWsDebugClientRemovePost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/client/remove', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 获取客户端状态 + * + * 获取指定客户端的状态信息 + */ +export const getClientStatusApiWsDebugClientStatusPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/client/status', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 列出所有客户端 + * + * 获取所有已创建的 WebSocket 客户端列表及状态 + */ +export const listClientsApiWsDebugClientListGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/ws_debug/client/list', + ...options + }); +}; + +/** + * 发送原始消息 + * + * 发送原始 JSON 消息到指定客户端连接的服务器 + */ +export const sendMessageApiWsDebugMessageSendPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/message/send', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 发送格式化消息 + * + * 发送格式化的 JSON 消息(自动组装 id、type、data 结构) + */ +export const sendJsonMessageApiWsDebugMessageSendJsonPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/message/send_json', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 发送认证消息 + * + * 发送认证消息到服务器 + * + * - **name**: 客户端名称 + * - **token**: 认证 Token + * - **auth_type**: 认证消息类型,默认 "auth" + * - **extra_data**: 额外的认证数据 + */ +export const sendAuthApiWsDebugMessageAuthPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/message/auth', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 获取消息历史 + * + * 获取消息历史记录 + * + * - **name**: 客户端名称,为空则获取所有客户端的历史 + */ +export const getHistoryApiWsDebugHistoryGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/ws_debug/history', + ...options + }); +}; + +/** + * 清空消息历史 + * + * 清空消息历史记录 + * + * - **name**: 客户端名称,为空则清空所有 + */ +export const clearHistoryApiWsDebugHistoryClearPost = (options: Options) => { + return (options.client ?? client).post({ + url: '/api/ws_debug/history/clear', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * 获取可用 WS 命令 + * + * 获取所有已注册的 WebSocket 命令端点 + */ +export const getCommandsApiWsDebugCommandsGet = (options?: Options) => { + return (options?.client ?? client).get({ + url: '/api/ws_debug/commands', + ...options + }); +}; diff --git a/frontend/src/api/generated/types.gen.ts b/frontend/src/api/generated/types.gen.ts new file mode 100644 index 00000000..850060f3 --- /dev/null +++ b/frontend/src/api/generated/types.gen.ts @@ -0,0 +1,7887 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +/** + * ADBScreenshotIn + */ +export type AdbScreenshotIn = { + /** + * Adb Path + * + * ADB 可执行文件的路径 + */ + adb_path: string; + /** + * Serial + * + * 设备序列号,格式如 '127.0.0.1:5555' 或 'emulator-5554' + */ + serial: string; + /** + * Use Screencap + * + * 是否使用 screencap PNG 方法,False 时使用 screencap raw 方法 + */ + use_screencap?: boolean; +}; + +/** + * ADBScreenshotOut + */ +export type AdbScreenshotOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Image Base64 + * + * 截图的Base64编码(PNG格式) + */ + image_base64: string; + /** + * Image Width + * + * 截图宽度 + */ + image_width: number; + /** + * Image Height + * + * 截图高度 + */ + image_height: number; + /** + * Serial + * + * 设备序列号 + */ + serial: string; +}; + +/** + * CheckImageAllIn + */ +export type CheckImageAllIn = { + /** + * Window Title + * + * 窗口标题(用于查找窗口) + */ + window_title: string; + /** + * Interval + * + * 截图间隔时间(秒) + */ + interval?: number; + /** + * Retry Times + * + * 重复截图次数 + */ + retry_times?: number; + /** + * Threshold + * + * 图像匹配阈值,范围 0-1 + */ + threshold?: number; + /** + * Image Paths + * + * 要查找的图片路径列表 + */ + image_paths: Array; +}; + +/** + * CheckImageAnyIn + */ +export type CheckImageAnyIn = { + /** + * Window Title + * + * 窗口标题(用于查找窗口) + */ + window_title: string; + /** + * Interval + * + * 截图间隔时间(秒) + */ + interval?: number; + /** + * Retry Times + * + * 重复截图次数 + */ + retry_times?: number; + /** + * Threshold + * + * 图像匹配阈值,范围 0-1 + */ + threshold?: number; + /** + * Image Paths + * + * 要查找的图片路径列表 + */ + image_paths: Array; +}; + +/** + * CheckImageIn + */ +export type CheckImageIn = { + /** + * Window Title + * + * 窗口标题(用于查找窗口) + */ + window_title: string; + /** + * Interval + * + * 截图间隔时间(秒) + */ + interval?: number; + /** + * Retry Times + * + * 重复截图次数 + */ + retry_times?: number; + /** + * Threshold + * + * 图像匹配阈值,范围 0-1 + */ + threshold?: number; + /** + * Image Path + * + * 要查找的图片路径 + */ + image_path: string; +}; + +/** + * CheckImageOut + */ +export type CheckImageOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Found + * + * 是否找到图像 + */ + found: boolean; + /** + * Attempts + * + * 实际尝试次数 + */ + attempts: number; +}; + +/** + * ClickImageIn + */ +export type ClickImageIn = { + /** + * Window Title + * + * 窗口标题(用于查找窗口) + */ + window_title: string; + /** + * Interval + * + * 截图间隔时间(秒) + */ + interval?: number; + /** + * Retry Times + * + * 重复截图次数 + */ + retry_times?: number; + /** + * Threshold + * + * 图像匹配阈值,范围 0-1 + */ + threshold?: number; + /** + * Image Path + * + * 要查找并点击的图片路径 + */ + image_path: string; +}; + +/** + * ClickOut + */ +export type ClickOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Success + * + * 是否成功点击 + */ + success: boolean; + /** + * Attempts + * + * 实际尝试次数 + */ + attempts: number; +}; + +/** + * ClickTextIn + */ +export type ClickTextIn = { + /** + * Window Title + * + * 窗口标题(用于查找窗口) + */ + window_title: string; + /** + * Interval + * + * 截图间隔时间(秒) + */ + interval?: number; + /** + * Retry Times + * + * 重复截图次数 + */ + retry_times?: number; + /** + * Text + * + * 要查找并点击的文字内容 + */ + text: string; +}; + +/** + * ComboBoxItem + */ +export type ComboBoxItem = { + /** + * Label + * + * 展示值 + */ + label: string; + /** + * Value + * + * 实际值 + */ + value: string | null; +}; + +/** + * ComboBoxOut + */ +export type ComboBoxOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 下拉框选项 + */ + data: Array; +}; + +/** + * DeviceInfo + * + * API 层使用的设备信息模型。 + */ +export type DeviceInfo = { + /** + * Title + * + * 设备标题/名称 + */ + title: string; + /** + * Status + * + * 设备状态, 参考 DeviceStatus 枚举值 + */ + status: number; + /** + * Adb Address + * + * ADB连接地址 + */ + adb_address: string; +}; + +/** + * DispatchIn + */ +export type DispatchIn = { + /** + * Taskid + * + * 目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID + */ + taskId: string; +}; + +/** + * EmulatorActionBody + */ +export type EmulatorActionBody = { + /** + * Index + * + * 模拟器索引 + */ + index: string; +}; + +/** + * EmulatorConfigIndexItem + */ +export type EmulatorConfigIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'EmulatorConfig'; +}; + +/** + * EmulatorCreateOut + * + * 模拟器创建响应模型 + */ +export type EmulatorCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: EmulatorRead; + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * EmulatorDetailOut + * + * 模拟器详情响应模型 + */ +export type EmulatorDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: EmulatorRead; +}; + +/** + * EmulatorDeviceStatusOut + * + * 模拟器设备状态响应模型 + */ +export type EmulatorDeviceStatusOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: { + [key: string]: DeviceInfo; + }; +}; + +/** + * EmulatorGetOut + * + * 模拟器列表响应模型 + */ +export type EmulatorGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: EmulatorRead; + }; +}; + +/** + * EmulatorIdBody + */ +export type EmulatorIdBody = { + /** + * Emulatorid + * + * 模拟器 ID + */ + emulatorId: string; +}; + +/** + * EmulatorRead + * + * 模拟器配置读取/写入模型。 + */ +export type EmulatorRead = { + Info?: EmulatorReadInfo | null; +}; + +/** + * EmulatorReadInfo + */ +export type EmulatorReadInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Type + */ + Type?: 'general' | 'mumu' | 'ldplayer' | null; + /** + * Path + */ + Path?: string | null; + /** + * Bosskey + */ + BossKey?: string | null; + /** + * Maxwaittime + */ + MaxWaitTime?: number | null; +}; + +/** + * EmulatorSearchOut + */ +export type EmulatorSearchOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 搜索到的模拟器列表 + */ + data: Array; +}; + +/** + * EmulatorSearchResult + */ +export type EmulatorSearchResult = { + /** + * Type + * + * 模拟器类型 + */ + type: string; + /** + * Path + * + * 模拟器路径 + */ + path: string; + /** + * Name + * + * 模拟器名称 + */ + name: string; +}; + +/** + * EmulatorStatusOut + * + * 模拟器状态响应模型 + */ +export type EmulatorStatusOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: { + [key: string]: { + [key: string]: DeviceInfo; + }; + }; +}; + +/** + * GeneralConfig + */ +export type GeneralConfig = { + Info?: GeneralConfigInfo | null; + Script?: GeneralConfigScript | null; + Game?: GeneralConfigGame | null; + Run?: GeneralConfigRun | null; + /** + * Type + * + * 配置类型 + */ + type?: 'GeneralConfig'; +}; + +/** + * GeneralConfigGame + */ +export type GeneralConfigGame = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Type + */ + Type?: 'Emulator' | 'Client' | 'URL' | null; + /** + * Path + */ + Path?: string | null; + /** + * Url + */ + URL?: string | null; + /** + * Processname + */ + ProcessName?: string | null; + /** + * Arguments + */ + Arguments?: string | null; + /** + * Waittime + */ + WaitTime?: number | null; + /** + * Ifforceclose + */ + IfForceClose?: boolean | null; + /** + * Emulatorid + */ + EmulatorId?: string | null; + /** + * Emulatorindex + */ + EmulatorIndex?: string | null; +}; + +/** + * GeneralConfigInfo + */ +export type GeneralConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Rootpath + */ + RootPath?: string | null; +}; + +/** + * GeneralConfigRun + */ +export type GeneralConfigRun = { + /** + * Proxytimeslimit + */ + ProxyTimesLimit?: number | null; + /** + * Runtimeslimit + */ + RunTimesLimit?: number | null; + /** + * Runtimelimit + */ + RunTimeLimit?: number | null; +}; + +/** + * GeneralConfigScript + */ +export type GeneralConfigScript = { + /** + * Scriptpath + */ + ScriptPath?: string | null; + /** + * Arguments + */ + Arguments?: string | null; + /** + * Iftrackprocess + */ + IfTrackProcess?: boolean | null; + /** + * Trackprocessname + */ + TrackProcessName?: string | null; + /** + * Trackprocessexe + */ + TrackProcessExe?: string | null; + /** + * Trackprocesscmdline + */ + TrackProcessCmdline?: string | null; + /** + * Configpath + */ + ConfigPath?: string | null; + /** + * Configpathmode + */ + ConfigPathMode?: 'File' | 'Folder' | null; + /** + * Updateconfigmode + */ + UpdateConfigMode?: 'Never' | 'Success' | 'Failure' | 'Always' | null; + /** + * Logpath + */ + LogPath?: string | null; + /** + * Logpathformat + */ + LogPathFormat?: string | null; + /** + * Logtimestart + */ + LogTimeStart?: number | null; + /** + * Logtimeend + */ + LogTimeEnd?: number | null; + /** + * Logtimeformat + */ + LogTimeFormat?: string | null; + /** + * Successlog + */ + SuccessLog?: string | null; + /** + * Errorlog + */ + ErrorLog?: string | null; +}; + +/** + * GeneralUserConfig + */ +export type GeneralUserConfig = { + Info?: GeneralUserConfigInfo | null; + Data?: GeneralUserConfigData | null; + Notify?: GeneralUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'GeneralUserConfig'; +}; + +/** + * GeneralUserConfigData + */ +export type GeneralUserConfigData = { + /** + * Lastproxydate + */ + LastProxyDate?: string | null; + /** + * Proxytimes + */ + ProxyTimes?: number | null; +}; + +/** + * GeneralUserConfigInfo + */ +export type GeneralUserConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Ifscriptbeforetask + */ + IfScriptBeforeTask?: boolean | null; + /** + * Scriptbeforetask + */ + ScriptBeforeTask?: string | null; + /** + * Ifscriptaftertask + */ + IfScriptAfterTask?: boolean | null; + /** + * Scriptaftertask + */ + ScriptAfterTask?: string | null; + /** + * Notes + */ + Notes?: string | null; + /** + * Tag + */ + Tag?: string | null; +}; + +/** + * GeneralUserConfigNotify + */ +export type GeneralUserConfigNotify = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Ifsendstatistic + */ + IfSendStatistic?: boolean | null; + /** + * Ifsendmail + */ + IfSendMail?: boolean | null; + /** + * Toaddress + */ + ToAddress?: string | null; + /** + * Ifserverchan + */ + IfServerChan?: boolean | null; + /** + * Serverchankey + */ + ServerChanKey?: string | null; +}; + +/** + * GetStageIn + */ +export type GetStageIn = { + /** + * Type + * + * 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项 + */ + type: 'User' | 'Today' | 'ALL' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday'; +}; + +/** + * GlobalConfigRead + * + * 全局配置读取/写入模型。 + */ +export type GlobalConfigRead = { + Function?: GlobalConfigReadFunction | null; + Voice?: GlobalConfigReadVoice | null; + Start?: GlobalConfigReadStart | null; + UI?: GlobalConfigReadUi | null; + Notify?: GlobalConfigReadNotify | null; + Update?: GlobalConfigReadUpdate | null; +}; + +/** + * GlobalConfigReadFunction + */ +export type GlobalConfigReadFunction = { + /** + * Historyretentiontime + */ + HistoryRetentionTime?: 7 | 15 | 30 | 60 | 90 | 180 | 365 | 0 | null; + /** + * Ifallowsleep + */ + IfAllowSleep?: boolean | null; + /** + * Ifsilence + */ + IfSilence?: boolean | null; + /** + * Ifagreebilibili + */ + IfAgreeBilibili?: boolean | null; + /** + * Ifblockad + */ + IfBlockAd?: boolean | null; +}; + +/** + * GlobalConfigReadNotify + */ +export type GlobalConfigReadNotify = { + /** + * Sendtaskresulttime + */ + SendTaskResultTime?: '不推送' | '任何时刻' | '仅失败时' | null; + /** + * Ifsendstatistic + */ + IfSendStatistic?: boolean | null; + /** + * Ifsendsixstar + */ + IfSendSixStar?: boolean | null; + /** + * Ifpushplyer + */ + IfPushPlyer?: boolean | null; + /** + * Ifsendmail + */ + IfSendMail?: boolean | null; + /** + * Ifkoishisupport + */ + IfKoishiSupport?: boolean | null; + /** + * Koishiserveraddress + */ + KoishiServerAddress?: string | null; + /** + * Koishitoken + */ + KoishiToken?: string | null; + /** + * Smtpserveraddress + */ + SMTPServerAddress?: string | null; + /** + * Authorizationcode + */ + AuthorizationCode?: string | null; + /** + * Fromaddress + */ + FromAddress?: string | null; + /** + * Toaddress + */ + ToAddress?: string | null; + /** + * Ifserverchan + */ + IfServerChan?: boolean | null; + /** + * Serverchankey + */ + ServerChanKey?: string | null; +}; + +/** + * GlobalConfigReadStart + */ +export type GlobalConfigReadStart = { + /** + * Ifselfstart + */ + IfSelfStart?: boolean | null; + /** + * Ifminimizedirectly + */ + IfMinimizeDirectly?: boolean | null; +}; + +/** + * GlobalConfigReadUI + */ +export type GlobalConfigReadUi = { + /** + * Ifshowtray + */ + IfShowTray?: boolean | null; + /** + * Iftotray + */ + IfToTray?: boolean | null; +}; + +/** + * GlobalConfigReadUpdate + */ +export type GlobalConfigReadUpdate = { + /** + * Ifautoupdate + */ + IfAutoUpdate?: boolean | null; + /** + * Source + */ + Source?: 'GitHub' | 'MirrorChyan' | 'AutoSite' | null; + /** + * Channel + */ + Channel?: 'stable' | 'beta' | null; + /** + * Proxyaddress + */ + ProxyAddress?: string | null; + /** + * Mirrorchyancdk + */ + MirrorChyanCDK?: string | null; +}; + +/** + * GlobalConfigReadVoice + */ +export type GlobalConfigReadVoice = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Type + */ + Type?: 'simple' | 'noisy' | null; +}; + +/** + * HTTPValidationError + */ +export type HttpValidationError = { + /** + * Detail + */ + detail?: Array; +}; + +/** + * HistoryData + */ +export type HistoryData = { + /** + * Index + * + * 历史记录索引列表 + */ + index?: Array | null; + /** + * Recruit Statistics + * + * 公招统计数据, key为星级, value为对应的公招数量 + */ + recruit_statistics?: { + [key: string]: number; + } | null; + /** + * Drop Statistics + * + * 掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } } + */ + drop_statistics?: { + [key: string]: { + [key: string]: number; + }; + } | null; + /** + * Error Info + * + * 报错信息, key为时间戳, value为错误描述 + */ + error_info?: { + [key: string]: string; + } | null; + /** + * Sanity + * + * 当前理智值 + */ + sanity?: number | null; + /** + * Sanity Full At + * + * 理智回满时间, 格式通常为 YYYY-MM-DD HH:MM:SS + */ + sanity_full_at?: string | null; + /** + * Log Content + * + * 日志内容, 仅在提取单条历史记录数据时返回 + */ + log_content?: string | null; +}; + +/** + * HistoryDataGetIn + */ +export type HistoryDataGetIn = { + /** + * Jsonpath + * + * 需要提取数据的历史记录JSON文件 + */ + jsonPath: string; +}; + +/** + * HistoryDataGetOut + */ +export type HistoryDataGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 历史记录数据 + */ + data: HistoryData; +}; + +/** + * HistoryIndexItem + */ +export type HistoryIndexItem = { + /** + * Date + * + * 日期 + */ + date: string; + /** + * Status + * + * 状态 + */ + status: 'DONE' | 'ERROR'; + /** + * Jsonfile + * + * 对应JSON文件 + */ + jsonFile: string; +}; + +/** + * HistorySearchIn + */ +export type HistorySearchIn = { + /** + * Mode + * + * 合并模式 + */ + mode: 'DAILY' | 'WEEKLY' | 'MONTHLY'; + /** + * Start Date + * + * 开始日期, 格式YYYY-MM-DD + */ + start_date: string; + /** + * End Date + * + * 结束日期, 格式YYYY-MM-DD + */ + end_date: string; +}; + +/** + * HistorySearchOut + */ +export type HistorySearchOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } } + */ + data: { + [key: string]: { + [key: string]: HistoryData; + }; + }; +}; + +/** + * IndexOrderPatch + */ +export type IndexOrderPatch = { + /** + * Index List + * + * 按新顺序排列的资源 ID 列表 + */ + index_list: Array; +}; + +/** + * InfoOut + */ +export type InfoOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 收到的服务器数据 + */ + data: { + [key: string]: unknown; + }; +}; + +/** + * InfrastructureImportBody + */ +export type InfrastructureImportBody = { + /** + * Path + * + * JSON 文件路径, 用于导入自定义基建文件 + */ + path: string; +}; + +/** + * MaaConfig + */ +export type MaaConfig = { + Info?: MaaConfigInfo | null; + Emulator?: MaaConfigEmulator | null; + Run?: MaaConfigRun | null; + /** + * Type + * + * 配置类型 + */ + type?: 'MaaConfig'; +}; + +/** + * MaaConfigEmulator + */ +export type MaaConfigEmulator = { + /** + * Id + */ + Id?: string | null; + /** + * Index + */ + Index?: string | null; +}; + +/** + * MaaConfigInfo + */ +export type MaaConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Path + */ + Path?: string | null; +}; + +/** + * MaaConfigRun + */ +export type MaaConfigRun = { + /** + * Tasktransitionmethod + */ + TaskTransitionMethod?: 'NoAction' | 'ExitGame' | 'ExitEmulator' | null; + /** + * Proxytimeslimit + */ + ProxyTimesLimit?: number | null; + /** + * Runtimeslimit + */ + RunTimesLimit?: number | null; + /** + * Annihilationtimelimit + */ + AnnihilationTimeLimit?: number | null; + /** + * Routinetimelimit + */ + RoutineTimeLimit?: number | null; + /** + * Annihilationavoidwaste + */ + AnnihilationAvoidWaste?: boolean | null; +}; + +/** + * MaaEndConfig + */ +export type MaaEndConfig = { + Info?: MaaEndConfigInfo | null; + Run?: MaaEndConfigRun | null; + Game?: MaaEndConfigGame | null; + /** + * Type + * + * 配置类型 + */ + type?: 'MaaEndConfig'; +}; + +/** + * MaaEndConfigGame + */ +export type MaaEndConfigGame = { + /** + * Controllertype + */ + ControllerType?: 'Win32-Window' | 'Win32-Front' | 'Win32-Window-Background' | 'ADB' | null; + /** + * Path + */ + Path?: string | null; + /** + * Arguments + */ + Arguments?: string | null; + /** + * Waittime + */ + WaitTime?: number | null; + /** + * Emulatorid + */ + EmulatorId?: string | null; + /** + * Emulatorindex + */ + EmulatorIndex?: string | null; + /** + * Closeonfinish + */ + CloseOnFinish?: boolean | null; +}; + +/** + * MaaEndConfigInfo + */ +export type MaaEndConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Path + */ + Path?: string | null; +}; + +/** + * MaaEndConfigRun + */ +export type MaaEndConfigRun = { + /** + * Runtimelimit + */ + RunTimeLimit?: number | null; + /** + * Proxytimeslimit + */ + ProxyTimesLimit?: number | null; + /** + * Runtimeslimit + */ + RunTimesLimit?: number | null; +}; + +/** + * MaaEndUserConfig + */ +export type MaaEndUserConfig = { + Info?: MaaEndUserConfigInfo | null; + Task?: MaaEndUserConfigTask | null; + Data?: MaaEndUserConfigData | null; + Notify?: MaaEndUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'MaaEndUserConfig'; +}; + +/** + * MaaEndUserConfigData + */ +export type MaaEndUserConfigData = { + /** + * Lastproxydate + */ + LastProxyDate?: string | null; + /** + * Proxytimes + */ + ProxyTimes?: number | null; + /** + * Lastproxystatus + */ + LastProxyStatus?: '未知' | '成功' | '失败' | null; + /** + * Lastsklanddate + */ + LastSklandDate?: string | null; + /** + * Ifpasscheck + */ + IfPassCheck?: boolean | null; +}; + +/** + * MaaEndUserConfigInfo + */ +export type MaaEndUserConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Id + */ + Id?: string | null; + /** + * Password + */ + Password?: string | null; + /** + * Mode + */ + Mode?: '简洁' | '详细' | null; + /** + * Resource + */ + Resource?: '官服' | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Notes + */ + Notes?: string | null; + /** + * Ifskland + */ + IfSkland?: boolean | null; + /** + * Sklandtoken + */ + SklandToken?: string | null; + /** + * Tag + */ + Tag?: string | null; +}; + +/** + * MaaEndUserConfigNotify + */ +export type MaaEndUserConfigNotify = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Ifsendstatistic + */ + IfSendStatistic?: boolean | null; + /** + * Ifsendmail + */ + IfSendMail?: boolean | null; + /** + * Toaddress + */ + ToAddress?: string | null; + /** + * Ifserverchan + */ + IfServerChan?: boolean | null; + /** + * Serverchankey + */ + ServerChanKey?: string | null; +}; + +/** + * MaaEndUserConfigTask + */ +export type MaaEndUserConfigTask = { + /** + * Protocolspacetab + */ + ProtocolSpaceTab?: 'OperatorProgression' | 'WeaponProgression' | 'CrisisDrills' | null; + /** + * Operatorprogression + */ + OperatorProgression?: 'OperatorEXP' | 'Promotions' | 'T-Creds' | 'SkillUp' | null; + /** + * Weaponprogression + */ + WeaponProgression?: 'WeaponEXP' | 'WeaponTune' | null; + /** + * Crisisdrills + */ + CrisisDrills?: 'AdvancedProgression1' | 'AdvancedProgression2' | 'AdvancedProgression3' | 'AdvancedProgression4' | 'AdvancedProgression5' | null; + /** + * Rewardssetoption + */ + RewardsSetOption?: 'RewardsSetA' | 'RewardsSetB' | null; +}; + +/** + * MaaPlanDayPatch + */ +export type MaaPlanDayPatch = { + /** + * Medicine Numb + * + * 吃理智药 + */ + medicine_numb?: number | null; + /** + * Series Numb + * + * 连战次数 + */ + series_numb?: '0' | '6' | '5' | '4' | '3' | '2' | '1' | '-1' | null; + /** + * Stage + * + * 关卡选择 + */ + stage?: string | null; + /** + * Stage 1 + * + * 备选关卡 - 1 + */ + stage_1?: string | null; + /** + * Stage 2 + * + * 备选关卡 - 2 + */ + stage_2?: string | null; + /** + * Stage 3 + * + * 备选关卡 - 3 + */ + stage_3?: string | null; + /** + * Stage Remain + * + * 剩余理智关卡 + */ + stage_remain?: string | null; +}; + +/** + * MaaPlanDayRead + */ +export type MaaPlanDayRead = { + /** + * Medicinenumb + * + * 吃理智药 + */ + MedicineNumb?: number; + /** + * Seriesnumb + * + * 连战次数 + */ + SeriesNumb?: '0' | '6' | '5' | '4' | '3' | '2' | '1' | '-1'; + /** + * Stage + * + * 关卡选择 + */ + Stage?: string; + /** + * Stage 1 + * + * 备选关卡 - 1 + */ + Stage_1?: string; + /** + * Stage 2 + * + * 备选关卡 - 2 + */ + Stage_2?: string; + /** + * Stage 3 + * + * 备选关卡 - 3 + */ + Stage_3?: string; + /** + * Stage Remain + * + * 剩余理智关卡 + */ + Stage_Remain?: string; +}; + +/** + * MaaPlanInfoPatch + */ +export type MaaPlanInfoPatch = { + /** + * Name + * + * 计划表名称 + */ + name?: string | null; + /** + * Mode + * + * 计划表模式 + */ + mode?: 'ALL' | 'Weekly' | null; +}; + +/** + * MaaPlanInfoRead + */ +export type MaaPlanInfoRead = { + /** + * Name + * + * 计划表名称 + */ + Name?: string; + /** + * Mode + * + * 计划表模式 + */ + Mode?: 'ALL' | 'Weekly'; +}; + +/** + * MaaPlanPatch + */ +export type MaaPlanPatch = { + /** + * 基础信息 + */ + info?: MaaPlanInfoPatch | null; + /** + * 全局 + */ + all_days?: MaaPlanDayPatch | null; + /** + * 周一 + */ + monday?: MaaPlanDayPatch | null; + /** + * 周二 + */ + tuesday?: MaaPlanDayPatch | null; + /** + * 周三 + */ + wednesday?: MaaPlanDayPatch | null; + /** + * 周四 + */ + thursday?: MaaPlanDayPatch | null; + /** + * 周五 + */ + friday?: MaaPlanDayPatch | null; + /** + * 周六 + */ + saturday?: MaaPlanDayPatch | null; + /** + * 周日 + */ + sunday?: MaaPlanDayPatch | null; +}; + +/** + * MaaPlanRead + */ +export type MaaPlanRead = { + /** + * 基础信息 + */ + Info?: MaaPlanInfoRead; + /** + * 全局 + */ + ALL?: MaaPlanDayRead; + /** + * 周一 + */ + Monday?: MaaPlanDayRead; + /** + * 周二 + */ + Tuesday?: MaaPlanDayRead; + /** + * 周三 + */ + Wednesday?: MaaPlanDayRead; + /** + * 周四 + */ + Thursday?: MaaPlanDayRead; + /** + * 周五 + */ + Friday?: MaaPlanDayRead; + /** + * 周六 + */ + Saturday?: MaaPlanDayRead; + /** + * 周日 + */ + Sunday?: MaaPlanDayRead; +}; + +/** + * MaaUserConfig + */ +export type MaaUserConfig = { + Info?: MaaUserConfigInfo | null; + Data?: MaaUserConfigData | null; + Task?: MaaUserConfigTask | null; + Notify?: MaaUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'MaaUserConfig'; +}; + +/** + * MaaUserConfigData + */ +export type MaaUserConfigData = { + /** + * Lastproxydate + */ + LastProxyDate?: string | null; + /** + * Lastsklanddate + */ + LastSklandDate?: string | null; + /** + * Proxytimes + */ + ProxyTimes?: number | null; + /** + * Ifpasscheck + */ + IfPassCheck?: boolean | null; + /** + * Custominfrast + */ + CustomInfrast?: string | null; + /** + * Infrastindex + */ + InfrastIndex?: string | null; +}; + +/** + * MaaUserConfigInfo + */ +export type MaaUserConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Id + */ + Id?: string | null; + /** + * Password + */ + Password?: string | null; + /** + * Mode + */ + Mode?: '简洁' | '详细' | null; + /** + * Stagemode + */ + StageMode?: string | null; + /** + * Server + */ + Server?: 'Official' | 'Bilibili' | 'YoStarEN' | 'YoStarJP' | 'YoStarKR' | 'txwy' | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Annihilation + */ + Annihilation?: 'Close' | 'Annihilation' | 'Chernobog@Annihilation' | 'LungmenOutskirts@Annihilation' | 'LungmenDowntown@Annihilation' | null; + /** + * Infrastmode + */ + InfrastMode?: 'Normal' | 'Rotation' | 'Custom' | null; + /** + * Infrastname + */ + InfrastName?: string | null; + /** + * Infrastindex + */ + InfrastIndex?: string | null; + /** + * Notes + */ + Notes?: string | null; + /** + * Medicinenumb + */ + MedicineNumb?: number | null; + /** + * Seriesnumb + */ + SeriesNumb?: '0' | '6' | '5' | '4' | '3' | '2' | '1' | '-1' | null; + /** + * Stage + */ + Stage?: string | null; + /** + * Stage 1 + */ + Stage_1?: string | null; + /** + * Stage 2 + */ + Stage_2?: string | null; + /** + * Stage 3 + */ + Stage_3?: string | null; + /** + * Stage Remain + */ + Stage_Remain?: string | null; + /** + * Ifskland + */ + IfSkland?: boolean | null; + /** + * Sklandtoken + */ + SklandToken?: string | null; + /** + * Tag + */ + Tag?: string | null; +}; + +/** + * MaaUserConfigNotify + */ +export type MaaUserConfigNotify = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Ifsendstatistic + */ + IfSendStatistic?: boolean | null; + /** + * Ifsendsixstar + */ + IfSendSixStar?: boolean | null; + /** + * Ifsendmail + */ + IfSendMail?: boolean | null; + /** + * Toaddress + */ + ToAddress?: string | null; + /** + * Ifserverchan + */ + IfServerChan?: boolean | null; + /** + * Serverchankey + */ + ServerChanKey?: string | null; +}; + +/** + * MaaUserConfigTask + */ +export type MaaUserConfigTask = { + /** + * Ifstartup + */ + IfStartUp?: boolean | null; + /** + * Iffight + */ + IfFight?: boolean | null; + /** + * Ifinfrast + */ + IfInfrast?: boolean | null; + /** + * Ifrecruit + */ + IfRecruit?: boolean | null; + /** + * Ifmall + */ + IfMall?: boolean | null; + /** + * Ifaward + */ + IfAward?: boolean | null; + /** + * Ifroguelike + */ + IfRoguelike?: boolean | null; + /** + * Ifreclamation + */ + IfReclamation?: boolean | null; +}; + +/** + * NoticeOut + */ +export type NoticeOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * If Need Show + * + * 是否需要显示公告 + */ + if_need_show: boolean; + /** + * Data + * + * 公告信息, key为公告标题, value为公告内容 + */ + data: { + [key: string]: string; + }; +}; + +/** + * OCRScreenshotIn + */ +export type OcrScreenshotIn = { + /** + * Window Title + * + * 窗口标题(用于查找窗口) + */ + window_title: string; + /** + * Should Preprocess + * + * 是否预处理图片区域,True时排除边框和标题栏,False时使用完整窗口 + */ + should_preprocess?: boolean; + /** + * Aspect Ratio Width + * + * 宽高比宽度 + */ + aspect_ratio_width?: number; + /** + * Aspect Ratio Height + * + * 宽高比高度 + */ + aspect_ratio_height?: number; + /** + * Region + * + * 自定义截图区域 (left, top, width, height) + */ + region?: [ + number, + number, + number, + number + ] | null; +}; + +/** + * OCRScreenshotOut + */ +export type OcrScreenshotOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Image Base64 + * + * 截图的Base64编码(PNG格式) + */ + image_base64: string; + /** + * Region + * + * 实际使用的截图区域 (left, top, width, height) + */ + region: [ + number, + number, + number, + number + ]; + /** + * Image Width + * + * 截图宽度 + */ + image_width: number; + /** + * Image Height + * + * 截图高度 + */ + image_height: number; +}; + +/** + * OutBase + */ +export type OutBase = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; +}; + +/** + * PlanCreateIn + */ +export type PlanCreateIn = { + /** + * Type + * + * 计划类型 + */ + type: 'MaaPlan'; +}; + +/** + * PlanCreateOut + * + * 计划创建响应模型 + */ +export type PlanCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: MaaPlanRead; + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * PlanDetailOut + * + * 计划详情响应模型 + */ +export type PlanDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: MaaPlanRead; +}; + +/** + * PlanGetOut + * + * 计划列表响应模型 + */ +export type PlanGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: MaaPlanRead; + }; +}; + +/** + * PlanIndexItem + */ +export type PlanIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'MaaPlanConfig'; +}; + +/** + * PlanUpdateBody + */ +export type PlanUpdateBody = { + /** + * 计划更新数据 + */ + data: MaaPlanPatch; +}; + +/** + * PowerIn + */ +export type PowerIn = { + /** + * Signal + * + * 电源操作信号 + */ + signal: 'NoAction' | 'Shutdown' | 'ShutdownForce' | 'Reboot' | 'Hibernate' | 'Sleep' | 'KillSelf'; +}; + +/** + * PowerOut + */ +export type PowerOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Signal + * + * 电源操作信号 + */ + signal: 'NoAction' | 'Shutdown' | 'ShutdownForce' | 'Reboot' | 'Hibernate' | 'Sleep' | 'KillSelf'; +}; + +/** + * QueueCreateOut + * + * 队列创建响应模型 + */ +export type QueueCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: QueueRead; + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * QueueDetailOut + * + * 队列详情响应模型 + */ +export type QueueDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: QueueRead; +}; + +/** + * QueueGetOut + * + * 队列列表响应模型 + */ +export type QueueGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: QueueRead; + }; +}; + +/** + * QueueIndexItem + */ +export type QueueIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'QueueConfig'; +}; + +/** + * QueueItemCreateOut + * + * 队列项创建响应模型 + */ +export type QueueItemCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: QueueItemRead; + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * QueueItemDetailOut + * + * 队列项详情响应模型 + */ +export type QueueItemDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: QueueItemRead; +}; + +/** + * QueueItemGetOut + * + * 队列项列表响应模型 + */ +export type QueueItemGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: QueueItemRead; + }; +}; + +/** + * QueueItemIndexItem + */ +export type QueueItemIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'QueueItem'; +}; + +/** + * QueueItemRead + * + * 任务项读取/写入模型。 + */ +export type QueueItemRead = { + Info?: QueueItemReadInfo | null; +}; + +/** + * QueueItemReadInfo + */ +export type QueueItemReadInfo = { + /** + * Scriptid + */ + ScriptId?: string | null; +}; + +/** + * QueueRead + * + * 队列配置读取/写入模型。 + */ +export type QueueRead = { + Info?: QueueReadInfo | null; +}; + +/** + * QueueReadInfo + */ +export type QueueReadInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Timeenabled + */ + TimeEnabled?: boolean | null; + /** + * Startupenabled + */ + StartUpEnabled?: boolean | null; + /** + * Afteraccomplish + */ + AfterAccomplish?: 'NoAction' | 'Shutdown' | 'ShutdownForce' | 'Reboot' | 'Hibernate' | 'Sleep' | 'KillSelf' | null; +}; + +/** + * ScriptCreateIn + */ +export type ScriptCreateIn = { + /** + * Type + * + * 脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本 + */ + type: 'MAA' | 'SRC' | 'General' | 'MaaEnd'; + /** + * Copyfromid + * + * 直接从该脚本 ID 复制创建, 仅复制创建时使用 + */ + copyFromId?: string | null; +}; + +/** + * ScriptCreateOut + * + * 脚本创建响应模型 + */ +export type ScriptCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: ({ + type: 'MaaConfig'; + } & MaaConfig) | ({ + type: 'SrcConfig'; + } & SrcConfig) | ({ + type: 'GeneralConfig'; + } & GeneralConfig) | ({ + type: 'MaaEndConfig'; + } & MaaEndConfig); + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * ScriptDetailOut + * + * 脚本详情响应模型 + */ +export type ScriptDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: ({ + type: 'MaaConfig'; + } & MaaConfig) | ({ + type: 'SrcConfig'; + } & SrcConfig) | ({ + type: 'GeneralConfig'; + } & GeneralConfig) | ({ + type: 'MaaEndConfig'; + } & MaaEndConfig); +}; + +/** + * ScriptFileBody + */ +export type ScriptFileBody = { + /** + * Path + * + * 文件路径 + */ + path: string; +}; + +/** + * ScriptGetOut + * + * 脚本列表响应模型 + */ +export type ScriptGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: ({ + type: 'MaaConfig'; + } & MaaConfig) | ({ + type: 'SrcConfig'; + } & SrcConfig) | ({ + type: 'GeneralConfig'; + } & GeneralConfig) | ({ + type: 'MaaEndConfig'; + } & MaaEndConfig); + }; +}; + +/** + * ScriptIndexItem + */ +export type ScriptIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'MaaConfig' | 'GeneralConfig' | 'SrcConfig' | 'MaaEndConfig'; +}; + +/** + * ScriptPatchBody + */ +export type ScriptPatchBody = { + /** + * Data + * + * 脚本 Patch 数据 + */ + data: ({ + type: 'MaaConfig'; + } & MaaConfig) | ({ + type: 'SrcConfig'; + } & SrcConfig) | ({ + type: 'GeneralConfig'; + } & GeneralConfig) | ({ + type: 'MaaEndConfig'; + } & MaaEndConfig); +}; + +/** + * ScriptUploadBody + */ +export type ScriptUploadBody = { + /** + * Config Name + * + * 配置名称 + */ + config_name: string; + /** + * Author + * + * 作者 + */ + author: string; + /** + * Description + * + * 描述 + */ + description: string; +}; + +/** + * ScriptUrlBody + */ +export type ScriptUrlBody = { + /** + * Url + * + * 配置文件 URL + */ + url: string; +}; + +/** + * SettingGetOut + * + * 全局设置响应模型 + */ +export type SettingGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: GlobalConfigRead; +}; + +/** + * SrcConfig + */ +export type SrcConfig = { + Info?: SrcConfigInfo | null; + Emulator?: SrcConfigEmulator | null; + Run?: SrcConfigRun | null; + /** + * Type + * + * 配置类型 + */ + type?: 'SrcConfig'; +}; + +/** + * SrcConfigEmulator + */ +export type SrcConfigEmulator = { + /** + * Id + */ + Id?: string | null; + /** + * Index + */ + Index?: string | null; +}; + +/** + * SrcConfigInfo + */ +export type SrcConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Path + */ + Path?: string | null; +}; + +/** + * SrcConfigRun + */ +export type SrcConfigRun = { + /** + * Tasktransitionmethod + */ + TaskTransitionMethod?: 'ExitGame' | 'ExitEmulator' | null; + /** + * Proxytimeslimit + */ + ProxyTimesLimit?: number | null; + /** + * Runtimeslimit + */ + RunTimesLimit?: number | null; + /** + * Runtimelimit + */ + RunTimeLimit?: number | null; +}; + +/** + * SrcUserConfig + */ +export type SrcUserConfig = { + Info?: SrcUserConfigInfo | null; + Stage?: SrcUserConfigStage | null; + Data?: SrcUserConfigData | null; + Notify?: SrcUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'SrcUserConfig'; +}; + +/** + * SrcUserConfigData + */ +export type SrcUserConfigData = { + /** + * Lastproxydate + */ + LastProxyDate?: string | null; + /** + * Proxytimes + */ + ProxyTimes?: number | null; + /** + * Ifpasscheck + */ + IfPassCheck?: boolean | null; +}; + +/** + * SrcUserConfigInfo + */ +export type SrcUserConfigInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Id + */ + Id?: string | null; + /** + * Password + */ + Password?: string | null; + /** + * Mode + */ + Mode?: '简洁' | '详细' | null; + /** + * Server + */ + Server?: 'CN-Official' | 'CN-Bilibili' | 'VN-Official' | 'OVERSEA-America' | 'OVERSEA-Asia' | 'OVERSEA-Europe' | 'OVERSEA-TWHKMO' | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Notes + */ + Notes?: string | null; + /** + * Tag + */ + Tag?: string | null; +}; + +/** + * SrcUserConfigNotify + */ +export type SrcUserConfigNotify = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Ifsendstatistic + */ + IfSendStatistic?: boolean | null; + /** + * Ifsendmail + */ + IfSendMail?: boolean | null; + /** + * Toaddress + */ + ToAddress?: string | null; + /** + * Ifserverchan + */ + IfServerChan?: boolean | null; + /** + * Serverchankey + */ + ServerChanKey?: string | null; +}; + +/** + * SrcUserConfigStage + */ +export type SrcUserConfigStage = { + /** + * Channel + */ + Channel?: 'Relic' | 'Materials' | 'Ornament' | null; + /** + * Relic + */ + Relic?: string | null; + /** + * Materials + */ + Materials?: string | null; + /** + * Ornament + */ + Ornament?: string | null; + /** + * Extractreservedtrailblazepower + */ + ExtractReservedTrailblazePower?: boolean | null; + /** + * Usefuel + */ + UseFuel?: boolean | null; + /** + * Fuelreserve + */ + FuelReserve?: number | null; + /** + * Echoofwar + */ + EchoOfWar?: string | null; + /** + * Simulateduniverseworld + */ + SimulatedUniverseWorld?: string | null; +}; + +/** + * TaskCreateIn + */ +export type TaskCreateIn = { + /** + * Taskid + * + * 目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID + */ + taskId: string; + /** + * Mode + * + * 任务模式 + */ + mode: 'AutoProxy' | 'ManualReview' | 'ScriptConfig'; +}; + +/** + * TaskCreateOut + */ +export type TaskCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Taskid + * + * 新创建的任务ID + */ + taskId: string; +}; + +/** + * TimeSetCreateOut + * + * 时间集创建响应模型 + */ +export type TimeSetCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: TimeSetRead; + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * TimeSetDetailOut + * + * 时间集详情响应模型 + */ +export type TimeSetDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: TimeSetRead; +}; + +/** + * TimeSetGetOut + * + * 时间集列表响应模型 + */ +export type TimeSetGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: TimeSetRead; + }; +}; + +/** + * TimeSetIndexItem + */ +export type TimeSetIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'TimeSet'; +}; + +/** + * TimeSetRead + * + * 时间集合读取/写入模型。 + */ +export type TimeSetRead = { + Info?: TimeSetReadInfo | null; +}; + +/** + * TimeSetReadInfo + */ +export type TimeSetReadInfo = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Days + */ + Days?: Array | null; + /** + * Time + */ + Time?: string | null; +}; + +/** + * ToolsConfigRead + * + * 工具配置读取/写入模型。 + */ +export type ToolsConfigRead = { + ArknightsPC?: ToolsConfigReadArknightsPc | null; +}; + +/** + * ToolsConfigReadArknightsPC + */ +export type ToolsConfigReadArknightsPc = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Pausekey + */ + PauseKey?: string | null; + /** + * Selectdeployedkey + */ + SelectDeployedKey?: string | null; + /** + * Useskillkey + */ + UseSkillKey?: string | null; + /** + * Retreatkey + */ + RetreatKey?: string | null; + /** + * Nextframekey + */ + NextFrameKey?: string | null; + /** + * Anotherquitkey + */ + AnotherQuitKey?: string | null; + /** + * Status + */ + Status?: string | null; +}; + +/** + * ToolsGetOut + * + * 工具配置响应模型 + */ +export type ToolsGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: ToolsConfigRead; +}; + +/** + * UpdateCheckIn + */ +export type UpdateCheckIn = { + /** + * Current Version + * + * 当前前端版本号 + */ + current_version: string; + /** + * If Force + * + * 是否强制拉取更新信息 + */ + if_force?: boolean; +}; + +/** + * UpdateCheckOut + */ +export type UpdateCheckOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * If Need Update + * + * 是否需要更新前端 + */ + if_need_update: boolean; + /** + * Latest Version + * + * 最新前端版本号 + */ + latest_version: string; + /** + * Update Info + * + * 版本更新信息字典 + */ + update_info: { + [key: string]: Array; + }; +}; + +/** + * UserCreateOut + * + * 用户创建响应模型 + */ +export type UserCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: ({ + type: 'MaaUserConfig'; + } & MaaUserConfig) | ({ + type: 'SrcUserConfig'; + } & SrcUserConfig) | ({ + type: 'GeneralUserConfig'; + } & GeneralUserConfig) | ({ + type: 'MaaEndUserConfig'; + } & MaaEndUserConfig); + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * UserDetailOut + * + * 用户详情响应模型 + */ +export type UserDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: ({ + type: 'MaaUserConfig'; + } & MaaUserConfig) | ({ + type: 'SrcUserConfig'; + } & SrcUserConfig) | ({ + type: 'GeneralUserConfig'; + } & GeneralUserConfig) | ({ + type: 'MaaEndUserConfig'; + } & MaaEndUserConfig); +}; + +/** + * UserGetOut + * + * 用户列表响应模型 + */ +export type UserGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: ({ + type: 'MaaUserConfig'; + } & MaaUserConfig) | ({ + type: 'SrcUserConfig'; + } & SrcUserConfig) | ({ + type: 'GeneralUserConfig'; + } & GeneralUserConfig) | ({ + type: 'MaaEndUserConfig'; + } & MaaEndUserConfig); + }; +}; + +/** + * UserIndexItem + */ +export type UserIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'MaaUserConfig' | 'GeneralUserConfig' | 'SrcUserConfig' | 'MaaEndUserConfig'; +}; + +/** + * UserPatchBody + */ +export type UserPatchBody = { + /** + * Data + * + * 用户 Patch 数据 + */ + data: ({ + type: 'MaaUserConfig'; + } & MaaUserConfig) | ({ + type: 'SrcUserConfig'; + } & SrcUserConfig) | ({ + type: 'GeneralUserConfig'; + } & GeneralUserConfig) | ({ + type: 'MaaEndUserConfig'; + } & MaaEndUserConfig); +}; + +/** + * ValidationError + */ +export type ValidationError = { + /** + * Location + */ + loc: Array; + /** + * Message + */ + msg: string; + /** + * Error Type + */ + type: string; +}; + +/** + * VersionOut + */ +export type VersionOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * If Need Update + * + * 后端代码是否需要更新 + */ + if_need_update: boolean; + /** + * Current Time + * + * 后端代码当前时间戳 + */ + current_time: string; + /** + * Current Hash + * + * 后端代码当前哈希值 + */ + current_hash: string; +}; + +/** + * WSClearHistoryIn + * + * 清空消息历史请求 + */ +export type WsClearHistoryIn = { + /** + * Name + * + * 客户端名称,为空则清空所有 + */ + name?: string | null; +}; + +/** + * WSClientAuthIn + * + * 发送认证请求 + */ +export type WsClientAuthIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; + /** + * Token + * + * 认证 Token + */ + token: string; + /** + * Auth Type + * + * 认证消息类型 + */ + auth_type?: string; + /** + * Extra Data + * + * 额外认证数据 + */ + extra_data?: { + [key: string]: unknown; + } | null; +}; + +/** + * WSClientConnectIn + * + * 连接请求 + */ +export type WsClientConnectIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; +}; + +/** + * WSClientCreateIn + * + * 创建 WebSocket 客户端请求 + */ +export type WsClientCreateIn = { + /** + * Name + * + * 客户端名称,用于标识 + */ + name: string; + /** + * Url + * + * WebSocket 服务器地址,如 ws://localhost:5140/path + */ + url: string; + /** + * Ping Interval + * + * 心跳发送间隔(秒) + */ + ping_interval?: number; + /** + * Ping Timeout + * + * 心跳超时时间(秒) + */ + ping_timeout?: number; + /** + * Reconnect Interval + * + * 重连间隔(秒) + */ + reconnect_interval?: number; + /** + * Max Reconnect Attempts + * + * 最大重连次数,-1为无限 + */ + max_reconnect_attempts?: number; +}; + +/** + * WSClientCreateOut + * + * 创建客户端响应 + */ +export type WsClientCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 返回数据 + */ + data?: { + [key: string]: unknown; + } | null; +}; + +/** + * WSClientDisconnectIn + * + * 断开连接请求 + */ +export type WsClientDisconnectIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; +}; + +/** + * WSClientListOut + * + * 客户端列表响应 + */ +export type WsClientListOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 客户端列表 + */ + data?: { + [key: string]: unknown; + } | null; +}; + +/** + * WSClientRemoveIn + * + * 删除客户端请求 + */ +export type WsClientRemoveIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; +}; + +/** + * WSClientSendIn + * + * 发送消息请求 + */ +export type WsClientSendIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; + /** + * Message + * + * 要发送的 JSON 消息 + */ + message: { + [key: string]: unknown; + }; +}; + +/** + * WSClientSendJsonIn + * + * 发送自定义 JSON 消息请求 + */ +export type WsClientSendJsonIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; + /** + * Msg Id + * + * 消息 ID + */ + msg_id?: string; + /** + * Msg Type + * + * 消息类型 + */ + msg_type: string; + /** + * Data + * + * 消息数据 + */ + data?: { + [key: string]: unknown; + }; +}; + +/** + * WSClientStatusIn + * + * 获取客户端状态请求 + */ +export type WsClientStatusIn = { + /** + * Name + * + * 客户端名称 + */ + name: string; +}; + +/** + * WSClientStatusOut + * + * 客户端状态响应 + */ +export type WsClientStatusOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 状态数据 + */ + data?: { + [key: string]: unknown; + } | null; +}; + +/** + * WSCommandsOut + * + * 可用命令列表响应 + */ +export type WsCommandsOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 命令列表 + */ + data?: { + [key: string]: unknown; + } | null; +}; + +/** + * WSMessageHistoryOut + * + * 消息历史响应 + */ +export type WsMessageHistoryOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 消息历史 + */ + data?: { + [key: string]: unknown; + } | null; +}; + +/** + * WebhookCreateOut + * + * Webhook 创建响应模型 + */ +export type WebhookCreateOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: WebhookRead; + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * WebhookDetailOut + * + * Webhook 详情响应模型 + */ +export type WebhookDetailOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: WebhookRead; +}; + +/** + * WebhookGetOut + * + * Webhook 列表响应模型 + */ +export type WebhookGetOut = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: WebhookRead; + }; +}; + +/** + * WebhookIndexItem + */ +export type WebhookIndexItem = { + /** + * Uid + * + * 唯一标识符 + */ + uid: string; + /** + * Type + * + * 配置类型 + */ + type: 'Webhook'; +}; + +/** + * WebhookRead + * + * Webhook 配置读取/写入模型。 + */ +export type WebhookRead = { + Info?: WebhookReadInfo | null; + Data?: WebhookReadData | null; +}; + +/** + * WebhookReadData + */ +export type WebhookReadData = { + /** + * Url + */ + Url?: string | null; + /** + * Template + */ + Template?: string | null; + /** + * Headers + */ + Headers?: string | null; + /** + * Method + */ + Method?: 'POST' | 'GET' | null; +}; + +/** + * WebhookReadInfo + */ +export type WebhookReadInfo = { + /** + * Name + */ + Name?: string | null; + /** + * Enabled + */ + Enabled?: boolean | null; +}; + +/** + * GeneralUserConfig + */ +export type GeneralUserConfigWritable = { + Info?: GeneralUserConfigInfoWritable | null; + Data?: GeneralUserConfigData | null; + Notify?: GeneralUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'GeneralUserConfig'; +}; + +/** + * GeneralUserConfigInfo + */ +export type GeneralUserConfigInfoWritable = { + /** + * Name + */ + Name?: string | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Ifscriptbeforetask + */ + IfScriptBeforeTask?: boolean | null; + /** + * Scriptbeforetask + */ + ScriptBeforeTask?: string | null; + /** + * Ifscriptaftertask + */ + IfScriptAfterTask?: boolean | null; + /** + * Scriptaftertask + */ + ScriptAfterTask?: string | null; + /** + * Notes + */ + Notes?: string | null; +}; + +/** + * MaaEndUserConfig + */ +export type MaaEndUserConfigWritable = { + Info?: MaaEndUserConfigInfoWritable | null; + Task?: MaaEndUserConfigTask | null; + Data?: MaaEndUserConfigData | null; + Notify?: MaaEndUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'MaaEndUserConfig'; +}; + +/** + * MaaEndUserConfigInfo + */ +export type MaaEndUserConfigInfoWritable = { + /** + * Name + */ + Name?: string | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Id + */ + Id?: string | null; + /** + * Password + */ + Password?: string | null; + /** + * Mode + */ + Mode?: '简洁' | '详细' | null; + /** + * Resource + */ + Resource?: '官服' | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Notes + */ + Notes?: string | null; + /** + * Ifskland + */ + IfSkland?: boolean | null; + /** + * Sklandtoken + */ + SklandToken?: string | null; +}; + +/** + * MaaUserConfig + */ +export type MaaUserConfigWritable = { + Info?: MaaUserConfigInfoWritable | null; + Data?: MaaUserConfigData | null; + Task?: MaaUserConfigTask | null; + Notify?: MaaUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'MaaUserConfig'; +}; + +/** + * MaaUserConfigInfo + */ +export type MaaUserConfigInfoWritable = { + /** + * Name + */ + Name?: string | null; + /** + * Id + */ + Id?: string | null; + /** + * Password + */ + Password?: string | null; + /** + * Mode + */ + Mode?: '简洁' | '详细' | null; + /** + * Stagemode + */ + StageMode?: string | null; + /** + * Server + */ + Server?: 'Official' | 'Bilibili' | 'YoStarEN' | 'YoStarJP' | 'YoStarKR' | 'txwy' | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Annihilation + */ + Annihilation?: 'Close' | 'Annihilation' | 'Chernobog@Annihilation' | 'LungmenOutskirts@Annihilation' | 'LungmenDowntown@Annihilation' | null; + /** + * Infrastmode + */ + InfrastMode?: 'Normal' | 'Rotation' | 'Custom' | null; + /** + * Notes + */ + Notes?: string | null; + /** + * Medicinenumb + */ + MedicineNumb?: number | null; + /** + * Seriesnumb + */ + SeriesNumb?: '0' | '6' | '5' | '4' | '3' | '2' | '1' | '-1' | null; + /** + * Stage + */ + Stage?: string | null; + /** + * Stage 1 + */ + Stage_1?: string | null; + /** + * Stage 2 + */ + Stage_2?: string | null; + /** + * Stage 3 + */ + Stage_3?: string | null; + /** + * Stage Remain + */ + Stage_Remain?: string | null; + /** + * Ifskland + */ + IfSkland?: boolean | null; + /** + * Sklandtoken + */ + SklandToken?: string | null; +}; + +/** + * SrcUserConfig + */ +export type SrcUserConfigWritable = { + Info?: SrcUserConfigInfoWritable | null; + Stage?: SrcUserConfigStage | null; + Data?: SrcUserConfigData | null; + Notify?: SrcUserConfigNotify | null; + /** + * Type + * + * 配置类型 + */ + type?: 'SrcUserConfig'; +}; + +/** + * SrcUserConfigInfo + */ +export type SrcUserConfigInfoWritable = { + /** + * Name + */ + Name?: string | null; + /** + * Status + */ + Status?: boolean | null; + /** + * Id + */ + Id?: string | null; + /** + * Password + */ + Password?: string | null; + /** + * Mode + */ + Mode?: '简洁' | '详细' | null; + /** + * Server + */ + Server?: 'CN-Official' | 'CN-Bilibili' | 'VN-Official' | 'OVERSEA-America' | 'OVERSEA-Asia' | 'OVERSEA-Europe' | 'OVERSEA-TWHKMO' | null; + /** + * Remainedday + */ + RemainedDay?: number | null; + /** + * Notes + */ + Notes?: string | null; +}; + +/** + * ToolsConfigRead + * + * 工具配置读取/写入模型。 + */ +export type ToolsConfigReadWritable = { + ArknightsPC?: ToolsConfigReadArknightsPcWritable | null; +}; + +/** + * ToolsConfigReadArknightsPC + */ +export type ToolsConfigReadArknightsPcWritable = { + /** + * Enabled + */ + Enabled?: boolean | null; + /** + * Pausekey + */ + PauseKey?: string | null; + /** + * Selectdeployedkey + */ + SelectDeployedKey?: string | null; + /** + * Useskillkey + */ + UseSkillKey?: string | null; + /** + * Retreatkey + */ + RetreatKey?: string | null; + /** + * Nextframekey + */ + NextFrameKey?: string | null; + /** + * Anotherquitkey + */ + AnotherQuitKey?: string | null; +}; + +/** + * ToolsGetOut + * + * 工具配置响应模型 + */ +export type ToolsGetOutWritable = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * 资源数据 + */ + data: ToolsConfigReadWritable; +}; + +/** + * UserCreateOut + * + * 用户创建响应模型 + */ +export type UserCreateOutWritable = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: ({ + type: 'MaaUserConfigWritable'; + } & MaaUserConfigWritable) | ({ + type: 'SrcUserConfigWritable'; + } & SrcUserConfigWritable) | ({ + type: 'GeneralUserConfigWritable'; + } & GeneralUserConfigWritable) | ({ + type: 'MaaEndUserConfigWritable'; + } & MaaEndUserConfigWritable); + /** + * Id + * + * 新创建资源的唯一 ID + */ + id: string; +}; + +/** + * UserDetailOut + * + * 用户详情响应模型 + */ +export type UserDetailOutWritable = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Data + * + * 资源数据 + */ + data: ({ + type: 'MaaUserConfigWritable'; + } & MaaUserConfigWritable) | ({ + type: 'SrcUserConfigWritable'; + } & SrcUserConfigWritable) | ({ + type: 'GeneralUserConfigWritable'; + } & GeneralUserConfigWritable) | ({ + type: 'MaaEndUserConfigWritable'; + } & MaaEndUserConfigWritable); +}; + +/** + * UserGetOut + * + * 用户列表响应模型 + */ +export type UserGetOutWritable = { + /** + * Code + * + * 状态码 + */ + code?: number; + /** + * Status + * + * 操作状态 + */ + status?: string; + /** + * Message + * + * 操作消息 + */ + message?: string; + /** + * Index + * + * 资源索引列表 + */ + index: Array; + /** + * Data + * + * 资源数据字典 + */ + data: { + [key: string]: ({ + type: 'MaaUserConfigWritable'; + } & MaaUserConfigWritable) | ({ + type: 'SrcUserConfigWritable'; + } & SrcUserConfigWritable) | ({ + type: 'GeneralUserConfigWritable'; + } & GeneralUserConfigWritable) | ({ + type: 'MaaEndUserConfigWritable'; + } & MaaEndUserConfigWritable); + }; +}; + +/** + * UserPatchBody + */ +export type UserPatchBodyWritable = { + /** + * Data + * + * 用户 Patch 数据 + */ + data: ({ + type: 'MaaUserConfigWritable'; + } & MaaUserConfigWritable) | ({ + type: 'SrcUserConfigWritable'; + } & SrcUserConfigWritable) | ({ + type: 'GeneralUserConfigWritable'; + } & GeneralUserConfigWritable) | ({ + type: 'MaaEndUserConfigWritable'; + } & MaaEndUserConfigWritable); +}; + +export type CloseApiCoreClosePostData = { + body?: never; + path?: never; + query?: never; + url: '/api/core/close'; +}; + +export type CloseApiCoreClosePostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type CloseApiCoreClosePostResponse = CloseApiCoreClosePostResponses[keyof CloseApiCoreClosePostResponses]; + +export type GetGitVersionApiInfoVersionPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/version'; +}; + +export type GetGitVersionApiInfoVersionPostResponses = { + /** + * Successful Response + */ + 200: VersionOut; +}; + +export type GetGitVersionApiInfoVersionPostResponse = GetGitVersionApiInfoVersionPostResponses[keyof GetGitVersionApiInfoVersionPostResponses]; + +export type GetStageComboxApiInfoComboxStagePostData = { + /** + * 关卡号类型 + */ + body: GetStageIn; + path?: never; + query?: never; + url: '/api/info/combox/stage'; +}; + +export type GetStageComboxApiInfoComboxStagePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetStageComboxApiInfoComboxStagePostError = GetStageComboxApiInfoComboxStagePostErrors[keyof GetStageComboxApiInfoComboxStagePostErrors]; + +export type GetStageComboxApiInfoComboxStagePostResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetStageComboxApiInfoComboxStagePostResponse = GetStageComboxApiInfoComboxStagePostResponses[keyof GetStageComboxApiInfoComboxStagePostResponses]; + +export type GetScriptComboxApiInfoComboxScriptPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/combox/script'; +}; + +export type GetScriptComboxApiInfoComboxScriptPostResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetScriptComboxApiInfoComboxScriptPostResponse = GetScriptComboxApiInfoComboxScriptPostResponses[keyof GetScriptComboxApiInfoComboxScriptPostResponses]; + +export type GetTaskComboxApiInfoComboxTaskPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/combox/task'; +}; + +export type GetTaskComboxApiInfoComboxTaskPostResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetTaskComboxApiInfoComboxTaskPostResponse = GetTaskComboxApiInfoComboxTaskPostResponses[keyof GetTaskComboxApiInfoComboxTaskPostResponses]; + +export type GetPlanComboxApiInfoComboxPlanPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/combox/plan'; +}; + +export type GetPlanComboxApiInfoComboxPlanPostResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetPlanComboxApiInfoComboxPlanPostResponse = GetPlanComboxApiInfoComboxPlanPostResponses[keyof GetPlanComboxApiInfoComboxPlanPostResponses]; + +export type GetEmulatorComboxApiInfoComboxEmulatorPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/combox/emulator'; +}; + +export type GetEmulatorComboxApiInfoComboxEmulatorPostResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetEmulatorComboxApiInfoComboxEmulatorPostResponse = GetEmulatorComboxApiInfoComboxEmulatorPostResponses[keyof GetEmulatorComboxApiInfoComboxEmulatorPostResponses]; + +export type GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostData = { + body: EmulatorIdBody; + path?: never; + query?: never; + url: '/api/info/combox/emulator/devices'; +}; + +export type GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostError = GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostErrors[keyof GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostErrors]; + +export type GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostResponse = GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostResponses[keyof GetEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPostResponses]; + +export type GetNoticeInfoApiInfoNoticeGetPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/notice/get'; +}; + +export type GetNoticeInfoApiInfoNoticeGetPostResponses = { + /** + * Successful Response + */ + 200: NoticeOut; +}; + +export type GetNoticeInfoApiInfoNoticeGetPostResponse = GetNoticeInfoApiInfoNoticeGetPostResponses[keyof GetNoticeInfoApiInfoNoticeGetPostResponses]; + +export type ConfirmNoticeApiInfoNoticeConfirmPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/notice/confirm'; +}; + +export type ConfirmNoticeApiInfoNoticeConfirmPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ConfirmNoticeApiInfoNoticeConfirmPostResponse = ConfirmNoticeApiInfoNoticeConfirmPostResponses[keyof ConfirmNoticeApiInfoNoticeConfirmPostResponses]; + +export type GetWebConfigApiInfoWebconfigPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/webconfig'; +}; + +export type GetWebConfigApiInfoWebconfigPostResponses = { + /** + * Successful Response + */ + 200: InfoOut; +}; + +export type GetWebConfigApiInfoWebconfigPostResponse = GetWebConfigApiInfoWebconfigPostResponses[keyof GetWebConfigApiInfoWebconfigPostResponses]; + +export type GetOverviewApiInfoGetOverviewPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/info/get/overview'; +}; + +export type GetOverviewApiInfoGetOverviewPostResponses = { + /** + * Successful Response + */ + 200: InfoOut; +}; + +export type GetOverviewApiInfoGetOverviewPostResponse = GetOverviewApiInfoGetOverviewPostResponses[keyof GetOverviewApiInfoGetOverviewPostResponses]; + +export type ListScriptsApiScriptsGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/scripts'; +}; + +export type ListScriptsApiScriptsGetResponses = { + /** + * Successful Response + */ + 200: ScriptGetOut; +}; + +export type ListScriptsApiScriptsGetResponse = ListScriptsApiScriptsGetResponses[keyof ListScriptsApiScriptsGetResponses]; + +export type CreateScriptApiScriptsPostData = { + body: ScriptCreateIn; + path?: never; + query?: never; + url: '/api/scripts'; +}; + +export type CreateScriptApiScriptsPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateScriptApiScriptsPostError = CreateScriptApiScriptsPostErrors[keyof CreateScriptApiScriptsPostErrors]; + +export type CreateScriptApiScriptsPostResponses = { + /** + * Successful Response + */ + 200: ScriptCreateOut; +}; + +export type CreateScriptApiScriptsPostResponse = CreateScriptApiScriptsPostResponses[keyof CreateScriptApiScriptsPostResponses]; + +export type ReorderScriptsApiScriptsOrderPatchData = { + body: IndexOrderPatch; + path?: never; + query?: never; + url: '/api/scripts/order'; +}; + +export type ReorderScriptsApiScriptsOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderScriptsApiScriptsOrderPatchError = ReorderScriptsApiScriptsOrderPatchErrors[keyof ReorderScriptsApiScriptsOrderPatchErrors]; + +export type ReorderScriptsApiScriptsOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderScriptsApiScriptsOrderPatchResponse = ReorderScriptsApiScriptsOrderPatchResponses[keyof ReorderScriptsApiScriptsOrderPatchResponses]; + +export type DeleteScriptApiScriptsScriptIdDeleteData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}'; +}; + +export type DeleteScriptApiScriptsScriptIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteScriptApiScriptsScriptIdDeleteError = DeleteScriptApiScriptsScriptIdDeleteErrors[keyof DeleteScriptApiScriptsScriptIdDeleteErrors]; + +export type DeleteScriptApiScriptsScriptIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteScriptApiScriptsScriptIdDeleteResponse = DeleteScriptApiScriptsScriptIdDeleteResponses[keyof DeleteScriptApiScriptsScriptIdDeleteResponses]; + +export type GetScriptApiScriptsScriptIdGetData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}'; +}; + +export type GetScriptApiScriptsScriptIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetScriptApiScriptsScriptIdGetError = GetScriptApiScriptsScriptIdGetErrors[keyof GetScriptApiScriptsScriptIdGetErrors]; + +export type GetScriptApiScriptsScriptIdGetResponses = { + /** + * Successful Response + */ + 200: ScriptDetailOut; +}; + +export type GetScriptApiScriptsScriptIdGetResponse = GetScriptApiScriptsScriptIdGetResponses[keyof GetScriptApiScriptsScriptIdGetResponses]; + +export type UpdateScriptApiScriptsScriptIdPatchData = { + body: ScriptPatchBody; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}'; +}; + +export type UpdateScriptApiScriptsScriptIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateScriptApiScriptsScriptIdPatchError = UpdateScriptApiScriptsScriptIdPatchErrors[keyof UpdateScriptApiScriptsScriptIdPatchErrors]; + +export type UpdateScriptApiScriptsScriptIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateScriptApiScriptsScriptIdPatchResponse = UpdateScriptApiScriptsScriptIdPatchResponses[keyof UpdateScriptApiScriptsScriptIdPatchResponses]; + +export type ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostData = { + body: ScriptFileBody; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/actions/import-file'; +}; + +export type ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostError = ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostErrors[keyof ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostErrors]; + +export type ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostResponse = ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostResponses[keyof ImportScriptFromFileApiScriptsScriptIdActionsImportFilePostResponses]; + +export type ExportScriptToFileApiScriptsScriptIdActionsExportFilePostData = { + body: ScriptFileBody; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/actions/export-file'; +}; + +export type ExportScriptToFileApiScriptsScriptIdActionsExportFilePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ExportScriptToFileApiScriptsScriptIdActionsExportFilePostError = ExportScriptToFileApiScriptsScriptIdActionsExportFilePostErrors[keyof ExportScriptToFileApiScriptsScriptIdActionsExportFilePostErrors]; + +export type ExportScriptToFileApiScriptsScriptIdActionsExportFilePostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ExportScriptToFileApiScriptsScriptIdActionsExportFilePostResponse = ExportScriptToFileApiScriptsScriptIdActionsExportFilePostResponses[keyof ExportScriptToFileApiScriptsScriptIdActionsExportFilePostResponses]; + +export type ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostData = { + body: ScriptUrlBody; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/actions/import-web'; +}; + +export type ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostError = ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostErrors[keyof ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostErrors]; + +export type ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostResponse = ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostResponses[keyof ImportScriptFromWebApiScriptsScriptIdActionsImportWebPostResponses]; + +export type UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostData = { + body: ScriptUploadBody; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/actions/upload-web'; +}; + +export type UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostError = UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostErrors[keyof UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostErrors]; + +export type UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostResponse = UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostResponses[keyof UploadScriptToWebApiScriptsScriptIdActionsUploadWebPostResponses]; + +export type ListUsersApiScriptsScriptIdUsersGetData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users'; +}; + +export type ListUsersApiScriptsScriptIdUsersGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ListUsersApiScriptsScriptIdUsersGetError = ListUsersApiScriptsScriptIdUsersGetErrors[keyof ListUsersApiScriptsScriptIdUsersGetErrors]; + +export type ListUsersApiScriptsScriptIdUsersGetResponses = { + /** + * Successful Response + */ + 200: UserGetOut; +}; + +export type ListUsersApiScriptsScriptIdUsersGetResponse = ListUsersApiScriptsScriptIdUsersGetResponses[keyof ListUsersApiScriptsScriptIdUsersGetResponses]; + +export type CreateUserApiScriptsScriptIdUsersPostData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users'; +}; + +export type CreateUserApiScriptsScriptIdUsersPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateUserApiScriptsScriptIdUsersPostError = CreateUserApiScriptsScriptIdUsersPostErrors[keyof CreateUserApiScriptsScriptIdUsersPostErrors]; + +export type CreateUserApiScriptsScriptIdUsersPostResponses = { + /** + * Successful Response + */ + 200: UserCreateOut; +}; + +export type CreateUserApiScriptsScriptIdUsersPostResponse = CreateUserApiScriptsScriptIdUsersPostResponses[keyof CreateUserApiScriptsScriptIdUsersPostResponses]; + +export type ReorderUsersApiScriptsScriptIdUsersOrderPatchData = { + body: IndexOrderPatch; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/order'; +}; + +export type ReorderUsersApiScriptsScriptIdUsersOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderUsersApiScriptsScriptIdUsersOrderPatchError = ReorderUsersApiScriptsScriptIdUsersOrderPatchErrors[keyof ReorderUsersApiScriptsScriptIdUsersOrderPatchErrors]; + +export type ReorderUsersApiScriptsScriptIdUsersOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderUsersApiScriptsScriptIdUsersOrderPatchResponse = ReorderUsersApiScriptsScriptIdUsersOrderPatchResponses[keyof ReorderUsersApiScriptsScriptIdUsersOrderPatchResponses]; + +export type DeleteUserApiScriptsScriptIdUsersUserIdDeleteData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}'; +}; + +export type DeleteUserApiScriptsScriptIdUsersUserIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteUserApiScriptsScriptIdUsersUserIdDeleteError = DeleteUserApiScriptsScriptIdUsersUserIdDeleteErrors[keyof DeleteUserApiScriptsScriptIdUsersUserIdDeleteErrors]; + +export type DeleteUserApiScriptsScriptIdUsersUserIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteUserApiScriptsScriptIdUsersUserIdDeleteResponse = DeleteUserApiScriptsScriptIdUsersUserIdDeleteResponses[keyof DeleteUserApiScriptsScriptIdUsersUserIdDeleteResponses]; + +export type GetUserApiScriptsScriptIdUsersUserIdGetData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}'; +}; + +export type GetUserApiScriptsScriptIdUsersUserIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetUserApiScriptsScriptIdUsersUserIdGetError = GetUserApiScriptsScriptIdUsersUserIdGetErrors[keyof GetUserApiScriptsScriptIdUsersUserIdGetErrors]; + +export type GetUserApiScriptsScriptIdUsersUserIdGetResponses = { + /** + * Successful Response + */ + 200: UserDetailOut; +}; + +export type GetUserApiScriptsScriptIdUsersUserIdGetResponse = GetUserApiScriptsScriptIdUsersUserIdGetResponses[keyof GetUserApiScriptsScriptIdUsersUserIdGetResponses]; + +export type UpdateUserApiScriptsScriptIdUsersUserIdPatchData = { + body: UserPatchBodyWritable; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}'; +}; + +export type UpdateUserApiScriptsScriptIdUsersUserIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateUserApiScriptsScriptIdUsersUserIdPatchError = UpdateUserApiScriptsScriptIdUsersUserIdPatchErrors[keyof UpdateUserApiScriptsScriptIdUsersUserIdPatchErrors]; + +export type UpdateUserApiScriptsScriptIdUsersUserIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateUserApiScriptsScriptIdUsersUserIdPatchResponse = UpdateUserApiScriptsScriptIdUsersUserIdPatchResponses[keyof UpdateUserApiScriptsScriptIdUsersUserIdPatchResponses]; + +export type ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostData = { + body: InfrastructureImportBody; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/actions/import-infrastructure'; +}; + +export type ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostError = ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostErrors[keyof ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostErrors]; + +export type ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostResponse = ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostResponses[keyof ImportInfrastructureApiScriptsScriptIdUsersUserIdActionsImportInfrastructurePostResponses]; + +export type GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/infrastructure-options'; +}; + +export type GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetError = GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetErrors[keyof GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetErrors]; + +export type GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetResponses = { + /** + * Successful Response + */ + 200: ComboBoxOut; +}; + +export type GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetResponse = GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetResponses[keyof GetUserInfrastructureOptionsApiScriptsScriptIdUsersUserIdInfrastructureOptionsGetResponses]; + +export type ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/webhooks'; +}; + +export type ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetError = ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetErrors[keyof ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetErrors]; + +export type ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetResponses = { + /** + * Successful Response + */ + 200: WebhookGetOut; +}; + +export type ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetResponse = ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetResponses[keyof ListUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksGetResponses]; + +export type CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/webhooks'; +}; + +export type CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostError = CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostErrors[keyof CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostErrors]; + +export type CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostResponses = { + /** + * Successful Response + */ + 200: WebhookCreateOut; +}; + +export type CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostResponse = CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostResponses[keyof CreateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksPostResponses]; + +export type ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchData = { + body: IndexOrderPatch; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/order'; +}; + +export type ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchError = ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchErrors[keyof ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchErrors]; + +export type ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchResponse = ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchResponses[keyof ReorderUserWebhooksApiScriptsScriptIdUsersUserIdWebhooksOrderPatchResponses]; + +export type DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + /** + * Webhook Id + * + * Webhook ID + */ + webhook_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}'; +}; + +export type DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteError = DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteErrors[keyof DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteErrors]; + +export type DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteResponse = DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteResponses[keyof DeleteUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdDeleteResponses]; + +export type GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetData = { + body?: never; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + /** + * Webhook Id + * + * Webhook ID + */ + webhook_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}'; +}; + +export type GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetError = GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetErrors[keyof GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetErrors]; + +export type GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetResponses = { + /** + * Successful Response + */ + 200: WebhookDetailOut; +}; + +export type GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetResponse = GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetResponses[keyof GetUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdGetResponses]; + +export type UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchData = { + body: WebhookRead; + path: { + /** + * Script Id + * + * 脚本 ID + */ + script_id: string; + /** + * User Id + * + * 用户 ID + */ + user_id: string; + /** + * Webhook Id + * + * Webhook ID + */ + webhook_id: string; + }; + query?: never; + url: '/api/scripts/{script_id}/users/{user_id}/webhooks/{webhook_id}'; +}; + +export type UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchError = UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchErrors[keyof UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchErrors]; + +export type UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchResponse = UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchResponses[keyof UpdateUserWebhookApiScriptsScriptIdUsersUserIdWebhooksWebhookIdPatchResponses]; + +export type ListPlansApiPlanGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/plan'; +}; + +export type ListPlansApiPlanGetResponses = { + /** + * Successful Response + */ + 200: PlanGetOut; +}; + +export type ListPlansApiPlanGetResponse = ListPlansApiPlanGetResponses[keyof ListPlansApiPlanGetResponses]; + +export type CreatePlanApiPlanPostData = { + body: PlanCreateIn; + path?: never; + query?: never; + url: '/api/plan'; +}; + +export type CreatePlanApiPlanPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreatePlanApiPlanPostError = CreatePlanApiPlanPostErrors[keyof CreatePlanApiPlanPostErrors]; + +export type CreatePlanApiPlanPostResponses = { + /** + * Successful Response + */ + 200: PlanCreateOut; +}; + +export type CreatePlanApiPlanPostResponse = CreatePlanApiPlanPostResponses[keyof CreatePlanApiPlanPostResponses]; + +export type DeletePlanApiPlanPlanIdDeleteData = { + body?: never; + path: { + /** + * Plan Id + * + * 计划 ID + */ + plan_id: string; + }; + query?: never; + url: '/api/plan/{plan_id}'; +}; + +export type DeletePlanApiPlanPlanIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeletePlanApiPlanPlanIdDeleteError = DeletePlanApiPlanPlanIdDeleteErrors[keyof DeletePlanApiPlanPlanIdDeleteErrors]; + +export type DeletePlanApiPlanPlanIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeletePlanApiPlanPlanIdDeleteResponse = DeletePlanApiPlanPlanIdDeleteResponses[keyof DeletePlanApiPlanPlanIdDeleteResponses]; + +export type GetPlanApiPlanPlanIdGetData = { + body?: never; + path: { + /** + * Plan Id + * + * 计划 ID + */ + plan_id: string; + }; + query?: never; + url: '/api/plan/{plan_id}'; +}; + +export type GetPlanApiPlanPlanIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetPlanApiPlanPlanIdGetError = GetPlanApiPlanPlanIdGetErrors[keyof GetPlanApiPlanPlanIdGetErrors]; + +export type GetPlanApiPlanPlanIdGetResponses = { + /** + * Successful Response + */ + 200: PlanDetailOut; +}; + +export type GetPlanApiPlanPlanIdGetResponse = GetPlanApiPlanPlanIdGetResponses[keyof GetPlanApiPlanPlanIdGetResponses]; + +export type UpdatePlanApiPlanPlanIdPatchData = { + body: PlanUpdateBody; + path: { + /** + * Plan Id + * + * 计划 ID + */ + plan_id: string; + }; + query?: never; + url: '/api/plan/{plan_id}'; +}; + +export type UpdatePlanApiPlanPlanIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdatePlanApiPlanPlanIdPatchError = UpdatePlanApiPlanPlanIdPatchErrors[keyof UpdatePlanApiPlanPlanIdPatchErrors]; + +export type UpdatePlanApiPlanPlanIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdatePlanApiPlanPlanIdPatchResponse = UpdatePlanApiPlanPlanIdPatchResponses[keyof UpdatePlanApiPlanPlanIdPatchResponses]; + +export type ReorderPlanApiPlanOrderPatchData = { + body: IndexOrderPatch; + path?: never; + query?: never; + url: '/api/plan/order'; +}; + +export type ReorderPlanApiPlanOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderPlanApiPlanOrderPatchError = ReorderPlanApiPlanOrderPatchErrors[keyof ReorderPlanApiPlanOrderPatchErrors]; + +export type ReorderPlanApiPlanOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderPlanApiPlanOrderPatchResponse = ReorderPlanApiPlanOrderPatchResponses[keyof ReorderPlanApiPlanOrderPatchResponses]; + +export type ListEmulatorsApiEmulatorGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/emulator'; +}; + +export type ListEmulatorsApiEmulatorGetResponses = { + /** + * Successful Response + */ + 200: EmulatorGetOut; +}; + +export type ListEmulatorsApiEmulatorGetResponse = ListEmulatorsApiEmulatorGetResponses[keyof ListEmulatorsApiEmulatorGetResponses]; + +export type CreateEmulatorApiEmulatorPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/emulator'; +}; + +export type CreateEmulatorApiEmulatorPostResponses = { + /** + * Successful Response + */ + 200: EmulatorCreateOut; +}; + +export type CreateEmulatorApiEmulatorPostResponse = CreateEmulatorApiEmulatorPostResponses[keyof CreateEmulatorApiEmulatorPostResponses]; + +export type ReorderEmulatorApiEmulatorOrderPatchData = { + body: IndexOrderPatch; + path?: never; + query?: never; + url: '/api/emulator/order'; +}; + +export type ReorderEmulatorApiEmulatorOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderEmulatorApiEmulatorOrderPatchError = ReorderEmulatorApiEmulatorOrderPatchErrors[keyof ReorderEmulatorApiEmulatorOrderPatchErrors]; + +export type ReorderEmulatorApiEmulatorOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderEmulatorApiEmulatorOrderPatchResponse = ReorderEmulatorApiEmulatorOrderPatchResponses[keyof ReorderEmulatorApiEmulatorOrderPatchResponses]; + +export type DetectEmulatorsApiEmulatorDetectedGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/emulator/detected'; +}; + +export type DetectEmulatorsApiEmulatorDetectedGetResponses = { + /** + * Successful Response + */ + 200: EmulatorSearchOut; +}; + +export type DetectEmulatorsApiEmulatorDetectedGetResponse = DetectEmulatorsApiEmulatorDetectedGetResponses[keyof DetectEmulatorsApiEmulatorDetectedGetResponses]; + +export type GetEmulatorStatusesApiEmulatorStatusGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/emulator/status'; +}; + +export type GetEmulatorStatusesApiEmulatorStatusGetResponses = { + /** + * Successful Response + */ + 200: EmulatorStatusOut; +}; + +export type GetEmulatorStatusesApiEmulatorStatusGetResponse = GetEmulatorStatusesApiEmulatorStatusGetResponses[keyof GetEmulatorStatusesApiEmulatorStatusGetResponses]; + +export type DeleteEmulatorApiEmulatorEmulatorIdDeleteData = { + body?: never; + path: { + /** + * Emulator Id + * + * 模拟器 ID + */ + emulator_id: string; + }; + query?: never; + url: '/api/emulator/{emulator_id}'; +}; + +export type DeleteEmulatorApiEmulatorEmulatorIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteEmulatorApiEmulatorEmulatorIdDeleteError = DeleteEmulatorApiEmulatorEmulatorIdDeleteErrors[keyof DeleteEmulatorApiEmulatorEmulatorIdDeleteErrors]; + +export type DeleteEmulatorApiEmulatorEmulatorIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteEmulatorApiEmulatorEmulatorIdDeleteResponse = DeleteEmulatorApiEmulatorEmulatorIdDeleteResponses[keyof DeleteEmulatorApiEmulatorEmulatorIdDeleteResponses]; + +export type GetEmulatorApiEmulatorEmulatorIdGetData = { + body?: never; + path: { + /** + * Emulator Id + * + * 模拟器 ID + */ + emulator_id: string; + }; + query?: never; + url: '/api/emulator/{emulator_id}'; +}; + +export type GetEmulatorApiEmulatorEmulatorIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetEmulatorApiEmulatorEmulatorIdGetError = GetEmulatorApiEmulatorEmulatorIdGetErrors[keyof GetEmulatorApiEmulatorEmulatorIdGetErrors]; + +export type GetEmulatorApiEmulatorEmulatorIdGetResponses = { + /** + * Successful Response + */ + 200: EmulatorDetailOut; +}; + +export type GetEmulatorApiEmulatorEmulatorIdGetResponse = GetEmulatorApiEmulatorEmulatorIdGetResponses[keyof GetEmulatorApiEmulatorEmulatorIdGetResponses]; + +export type UpdateEmulatorApiEmulatorEmulatorIdPatchData = { + body: EmulatorRead; + path: { + /** + * Emulator Id + * + * 模拟器 ID + */ + emulator_id: string; + }; + query?: never; + url: '/api/emulator/{emulator_id}'; +}; + +export type UpdateEmulatorApiEmulatorEmulatorIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateEmulatorApiEmulatorEmulatorIdPatchError = UpdateEmulatorApiEmulatorEmulatorIdPatchErrors[keyof UpdateEmulatorApiEmulatorEmulatorIdPatchErrors]; + +export type UpdateEmulatorApiEmulatorEmulatorIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateEmulatorApiEmulatorEmulatorIdPatchResponse = UpdateEmulatorApiEmulatorEmulatorIdPatchResponses[keyof UpdateEmulatorApiEmulatorEmulatorIdPatchResponses]; + +export type GetEmulatorStatusApiEmulatorEmulatorIdStatusGetData = { + body?: never; + path: { + /** + * Emulator Id + * + * 模拟器 ID + */ + emulator_id: string; + }; + query?: never; + url: '/api/emulator/{emulator_id}/status'; +}; + +export type GetEmulatorStatusApiEmulatorEmulatorIdStatusGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetEmulatorStatusApiEmulatorEmulatorIdStatusGetError = GetEmulatorStatusApiEmulatorEmulatorIdStatusGetErrors[keyof GetEmulatorStatusApiEmulatorEmulatorIdStatusGetErrors]; + +export type GetEmulatorStatusApiEmulatorEmulatorIdStatusGetResponses = { + /** + * Successful Response + */ + 200: EmulatorDeviceStatusOut; +}; + +export type GetEmulatorStatusApiEmulatorEmulatorIdStatusGetResponse = GetEmulatorStatusApiEmulatorEmulatorIdStatusGetResponses[keyof GetEmulatorStatusApiEmulatorEmulatorIdStatusGetResponses]; + +export type OperateEmulatorApiEmulatorEmulatorIdActionsActionPostData = { + body: EmulatorActionBody; + path: { + /** + * Emulator Id + * + * 模拟器 ID + */ + emulator_id: string; + /** + * Action + * + * 模拟器动作 + */ + action: 'open' | 'close' | 'show'; + }; + query?: never; + url: '/api/emulator/{emulator_id}/actions/{action}'; +}; + +export type OperateEmulatorApiEmulatorEmulatorIdActionsActionPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type OperateEmulatorApiEmulatorEmulatorIdActionsActionPostError = OperateEmulatorApiEmulatorEmulatorIdActionsActionPostErrors[keyof OperateEmulatorApiEmulatorEmulatorIdActionsActionPostErrors]; + +export type OperateEmulatorApiEmulatorEmulatorIdActionsActionPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type OperateEmulatorApiEmulatorEmulatorIdActionsActionPostResponse = OperateEmulatorApiEmulatorEmulatorIdActionsActionPostResponses[keyof OperateEmulatorApiEmulatorEmulatorIdActionsActionPostResponses]; + +export type ListQueuesApiQueueGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/queue'; +}; + +export type ListQueuesApiQueueGetResponses = { + /** + * Successful Response + */ + 200: QueueGetOut; +}; + +export type ListQueuesApiQueueGetResponse = ListQueuesApiQueueGetResponses[keyof ListQueuesApiQueueGetResponses]; + +export type CreateQueueApiQueuePostData = { + body?: never; + path?: never; + query?: never; + url: '/api/queue'; +}; + +export type CreateQueueApiQueuePostResponses = { + /** + * Successful Response + */ + 200: QueueCreateOut; +}; + +export type CreateQueueApiQueuePostResponse = CreateQueueApiQueuePostResponses[keyof CreateQueueApiQueuePostResponses]; + +export type ReorderQueueApiQueueOrderPatchData = { + body: IndexOrderPatch; + path?: never; + query?: never; + url: '/api/queue/order'; +}; + +export type ReorderQueueApiQueueOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderQueueApiQueueOrderPatchError = ReorderQueueApiQueueOrderPatchErrors[keyof ReorderQueueApiQueueOrderPatchErrors]; + +export type ReorderQueueApiQueueOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderQueueApiQueueOrderPatchResponse = ReorderQueueApiQueueOrderPatchResponses[keyof ReorderQueueApiQueueOrderPatchResponses]; + +export type DeleteQueueApiQueueQueueIdDeleteData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}'; +}; + +export type DeleteQueueApiQueueQueueIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteQueueApiQueueQueueIdDeleteError = DeleteQueueApiQueueQueueIdDeleteErrors[keyof DeleteQueueApiQueueQueueIdDeleteErrors]; + +export type DeleteQueueApiQueueQueueIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteQueueApiQueueQueueIdDeleteResponse = DeleteQueueApiQueueQueueIdDeleteResponses[keyof DeleteQueueApiQueueQueueIdDeleteResponses]; + +export type GetQueueApiQueueQueueIdGetData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}'; +}; + +export type GetQueueApiQueueQueueIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetQueueApiQueueQueueIdGetError = GetQueueApiQueueQueueIdGetErrors[keyof GetQueueApiQueueQueueIdGetErrors]; + +export type GetQueueApiQueueQueueIdGetResponses = { + /** + * Successful Response + */ + 200: QueueDetailOut; +}; + +export type GetQueueApiQueueQueueIdGetResponse = GetQueueApiQueueQueueIdGetResponses[keyof GetQueueApiQueueQueueIdGetResponses]; + +export type UpdateQueueApiQueueQueueIdPatchData = { + body: QueueRead; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}'; +}; + +export type UpdateQueueApiQueueQueueIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateQueueApiQueueQueueIdPatchError = UpdateQueueApiQueueQueueIdPatchErrors[keyof UpdateQueueApiQueueQueueIdPatchErrors]; + +export type UpdateQueueApiQueueQueueIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateQueueApiQueueQueueIdPatchResponse = UpdateQueueApiQueueQueueIdPatchResponses[keyof UpdateQueueApiQueueQueueIdPatchResponses]; + +export type ListTimeSetsApiQueueQueueIdTimesGetData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/times'; +}; + +export type ListTimeSetsApiQueueQueueIdTimesGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ListTimeSetsApiQueueQueueIdTimesGetError = ListTimeSetsApiQueueQueueIdTimesGetErrors[keyof ListTimeSetsApiQueueQueueIdTimesGetErrors]; + +export type ListTimeSetsApiQueueQueueIdTimesGetResponses = { + /** + * Successful Response + */ + 200: TimeSetGetOut; +}; + +export type ListTimeSetsApiQueueQueueIdTimesGetResponse = ListTimeSetsApiQueueQueueIdTimesGetResponses[keyof ListTimeSetsApiQueueQueueIdTimesGetResponses]; + +export type CreateTimeSetApiQueueQueueIdTimesPostData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/times'; +}; + +export type CreateTimeSetApiQueueQueueIdTimesPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateTimeSetApiQueueQueueIdTimesPostError = CreateTimeSetApiQueueQueueIdTimesPostErrors[keyof CreateTimeSetApiQueueQueueIdTimesPostErrors]; + +export type CreateTimeSetApiQueueQueueIdTimesPostResponses = { + /** + * Successful Response + */ + 200: TimeSetCreateOut; +}; + +export type CreateTimeSetApiQueueQueueIdTimesPostResponse = CreateTimeSetApiQueueQueueIdTimesPostResponses[keyof CreateTimeSetApiQueueQueueIdTimesPostResponses]; + +export type ReorderTimeSetsApiQueueQueueIdTimesOrderPatchData = { + body: IndexOrderPatch; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/times/order'; +}; + +export type ReorderTimeSetsApiQueueQueueIdTimesOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderTimeSetsApiQueueQueueIdTimesOrderPatchError = ReorderTimeSetsApiQueueQueueIdTimesOrderPatchErrors[keyof ReorderTimeSetsApiQueueQueueIdTimesOrderPatchErrors]; + +export type ReorderTimeSetsApiQueueQueueIdTimesOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderTimeSetsApiQueueQueueIdTimesOrderPatchResponse = ReorderTimeSetsApiQueueQueueIdTimesOrderPatchResponses[keyof ReorderTimeSetsApiQueueQueueIdTimesOrderPatchResponses]; + +export type DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + /** + * Time Set Id + * + * 时间设置 ID + */ + time_set_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/times/{time_set_id}'; +}; + +export type DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteError = DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteErrors[keyof DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteErrors]; + +export type DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteResponse = DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteResponses[keyof DeleteTimeSetApiQueueQueueIdTimesTimeSetIdDeleteResponses]; + +export type GetTimeSetApiQueueQueueIdTimesTimeSetIdGetData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + /** + * Time Set Id + * + * 时间设置 ID + */ + time_set_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/times/{time_set_id}'; +}; + +export type GetTimeSetApiQueueQueueIdTimesTimeSetIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetTimeSetApiQueueQueueIdTimesTimeSetIdGetError = GetTimeSetApiQueueQueueIdTimesTimeSetIdGetErrors[keyof GetTimeSetApiQueueQueueIdTimesTimeSetIdGetErrors]; + +export type GetTimeSetApiQueueQueueIdTimesTimeSetIdGetResponses = { + /** + * Successful Response + */ + 200: TimeSetDetailOut; +}; + +export type GetTimeSetApiQueueQueueIdTimesTimeSetIdGetResponse = GetTimeSetApiQueueQueueIdTimesTimeSetIdGetResponses[keyof GetTimeSetApiQueueQueueIdTimesTimeSetIdGetResponses]; + +export type UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchData = { + body: TimeSetRead; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + /** + * Time Set Id + * + * 时间设置 ID + */ + time_set_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/times/{time_set_id}'; +}; + +export type UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchError = UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchErrors[keyof UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchErrors]; + +export type UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchResponse = UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchResponses[keyof UpdateTimeSetApiQueueQueueIdTimesTimeSetIdPatchResponses]; + +export type ListQueueItemsApiQueueQueueIdItemsGetData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/items'; +}; + +export type ListQueueItemsApiQueueQueueIdItemsGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ListQueueItemsApiQueueQueueIdItemsGetError = ListQueueItemsApiQueueQueueIdItemsGetErrors[keyof ListQueueItemsApiQueueQueueIdItemsGetErrors]; + +export type ListQueueItemsApiQueueQueueIdItemsGetResponses = { + /** + * Successful Response + */ + 200: QueueItemGetOut; +}; + +export type ListQueueItemsApiQueueQueueIdItemsGetResponse = ListQueueItemsApiQueueQueueIdItemsGetResponses[keyof ListQueueItemsApiQueueQueueIdItemsGetResponses]; + +export type CreateQueueItemApiQueueQueueIdItemsPostData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/items'; +}; + +export type CreateQueueItemApiQueueQueueIdItemsPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateQueueItemApiQueueQueueIdItemsPostError = CreateQueueItemApiQueueQueueIdItemsPostErrors[keyof CreateQueueItemApiQueueQueueIdItemsPostErrors]; + +export type CreateQueueItemApiQueueQueueIdItemsPostResponses = { + /** + * Successful Response + */ + 200: QueueItemCreateOut; +}; + +export type CreateQueueItemApiQueueQueueIdItemsPostResponse = CreateQueueItemApiQueueQueueIdItemsPostResponses[keyof CreateQueueItemApiQueueQueueIdItemsPostResponses]; + +export type ReorderQueueItemsApiQueueQueueIdItemsOrderPatchData = { + body: IndexOrderPatch; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/items/order'; +}; + +export type ReorderQueueItemsApiQueueQueueIdItemsOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderQueueItemsApiQueueQueueIdItemsOrderPatchError = ReorderQueueItemsApiQueueQueueIdItemsOrderPatchErrors[keyof ReorderQueueItemsApiQueueQueueIdItemsOrderPatchErrors]; + +export type ReorderQueueItemsApiQueueQueueIdItemsOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderQueueItemsApiQueueQueueIdItemsOrderPatchResponse = ReorderQueueItemsApiQueueQueueIdItemsOrderPatchResponses[keyof ReorderQueueItemsApiQueueQueueIdItemsOrderPatchResponses]; + +export type DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + /** + * Queue Item Id + * + * 队列项 ID + */ + queue_item_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/items/{queue_item_id}'; +}; + +export type DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteError = DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteErrors[keyof DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteErrors]; + +export type DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteResponse = DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteResponses[keyof DeleteQueueItemApiQueueQueueIdItemsQueueItemIdDeleteResponses]; + +export type GetQueueItemApiQueueQueueIdItemsQueueItemIdGetData = { + body?: never; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + /** + * Queue Item Id + * + * 队列项 ID + */ + queue_item_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/items/{queue_item_id}'; +}; + +export type GetQueueItemApiQueueQueueIdItemsQueueItemIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetQueueItemApiQueueQueueIdItemsQueueItemIdGetError = GetQueueItemApiQueueQueueIdItemsQueueItemIdGetErrors[keyof GetQueueItemApiQueueQueueIdItemsQueueItemIdGetErrors]; + +export type GetQueueItemApiQueueQueueIdItemsQueueItemIdGetResponses = { + /** + * Successful Response + */ + 200: QueueItemDetailOut; +}; + +export type GetQueueItemApiQueueQueueIdItemsQueueItemIdGetResponse = GetQueueItemApiQueueQueueIdItemsQueueItemIdGetResponses[keyof GetQueueItemApiQueueQueueIdItemsQueueItemIdGetResponses]; + +export type UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchData = { + body: QueueItemRead; + path: { + /** + * Queue Id + * + * 队列 ID + */ + queue_id: string; + /** + * Queue Item Id + * + * 队列项 ID + */ + queue_item_id: string; + }; + query?: never; + url: '/api/queue/{queue_id}/items/{queue_item_id}'; +}; + +export type UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchError = UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchErrors[keyof UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchErrors]; + +export type UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchResponse = UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchResponses[keyof UpdateQueueItemApiQueueQueueIdItemsQueueItemIdPatchResponses]; + +export type AddTaskApiDispatchStartPostData = { + body: TaskCreateIn; + path?: never; + query?: never; + url: '/api/dispatch/start'; +}; + +export type AddTaskApiDispatchStartPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AddTaskApiDispatchStartPostError = AddTaskApiDispatchStartPostErrors[keyof AddTaskApiDispatchStartPostErrors]; + +export type AddTaskApiDispatchStartPostResponses = { + /** + * Successful Response + */ + 200: TaskCreateOut; +}; + +export type AddTaskApiDispatchStartPostResponse = AddTaskApiDispatchStartPostResponses[keyof AddTaskApiDispatchStartPostResponses]; + +export type StopTaskApiDispatchStopPostData = { + body: DispatchIn; + path?: never; + query?: never; + url: '/api/dispatch/stop'; +}; + +export type StopTaskApiDispatchStopPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type StopTaskApiDispatchStopPostError = StopTaskApiDispatchStopPostErrors[keyof StopTaskApiDispatchStopPostErrors]; + +export type StopTaskApiDispatchStopPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type StopTaskApiDispatchStopPostResponse = StopTaskApiDispatchStopPostResponses[keyof StopTaskApiDispatchStopPostResponses]; + +export type GetPowerApiDispatchGetPowerPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/dispatch/get/power'; +}; + +export type GetPowerApiDispatchGetPowerPostResponses = { + /** + * Successful Response + */ + 200: PowerOut; +}; + +export type GetPowerApiDispatchGetPowerPostResponse = GetPowerApiDispatchGetPowerPostResponses[keyof GetPowerApiDispatchGetPowerPostResponses]; + +export type SetPowerApiDispatchSetPowerPostData = { + body: PowerIn; + path?: never; + query?: never; + url: '/api/dispatch/set/power'; +}; + +export type SetPowerApiDispatchSetPowerPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SetPowerApiDispatchSetPowerPostError = SetPowerApiDispatchSetPowerPostErrors[keyof SetPowerApiDispatchSetPowerPostErrors]; + +export type SetPowerApiDispatchSetPowerPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type SetPowerApiDispatchSetPowerPostResponse = SetPowerApiDispatchSetPowerPostResponses[keyof SetPowerApiDispatchSetPowerPostResponses]; + +export type CancelPowerTaskApiDispatchCancelPowerPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/dispatch/cancel/power'; +}; + +export type CancelPowerTaskApiDispatchCancelPowerPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type CancelPowerTaskApiDispatchCancelPowerPostResponse = CancelPowerTaskApiDispatchCancelPowerPostResponses[keyof CancelPowerTaskApiDispatchCancelPowerPostResponses]; + +export type SearchHistoryApiHistorySearchPostData = { + body: HistorySearchIn; + path?: never; + query?: never; + url: '/api/history/search'; +}; + +export type SearchHistoryApiHistorySearchPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SearchHistoryApiHistorySearchPostError = SearchHistoryApiHistorySearchPostErrors[keyof SearchHistoryApiHistorySearchPostErrors]; + +export type SearchHistoryApiHistorySearchPostResponses = { + /** + * Successful Response + */ + 200: HistorySearchOut; +}; + +export type SearchHistoryApiHistorySearchPostResponse = SearchHistoryApiHistorySearchPostResponses[keyof SearchHistoryApiHistorySearchPostResponses]; + +export type GetHistoryDataApiHistoryDataPostData = { + body: HistoryDataGetIn; + path?: never; + query?: never; + url: '/api/history/data'; +}; + +export type GetHistoryDataApiHistoryDataPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetHistoryDataApiHistoryDataPostError = GetHistoryDataApiHistoryDataPostErrors[keyof GetHistoryDataApiHistoryDataPostErrors]; + +export type GetHistoryDataApiHistoryDataPostResponses = { + /** + * Successful Response + */ + 200: HistoryDataGetOut; +}; + +export type GetHistoryDataApiHistoryDataPostResponse = GetHistoryDataApiHistoryDataPostResponses[keyof GetHistoryDataApiHistoryDataPostResponses]; + +export type GetToolsApiToolsGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/tools'; +}; + +export type GetToolsApiToolsGetResponses = { + /** + * Successful Response + */ + 200: ToolsGetOut; +}; + +export type GetToolsApiToolsGetResponse = GetToolsApiToolsGetResponses[keyof GetToolsApiToolsGetResponses]; + +export type UpdateToolsApiToolsPatchData = { + body: ToolsConfigReadWritable; + path?: never; + query?: never; + url: '/api/tools'; +}; + +export type UpdateToolsApiToolsPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateToolsApiToolsPatchError = UpdateToolsApiToolsPatchErrors[keyof UpdateToolsApiToolsPatchErrors]; + +export type UpdateToolsApiToolsPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateToolsApiToolsPatchResponse = UpdateToolsApiToolsPatchResponses[keyof UpdateToolsApiToolsPatchResponses]; + +export type GetSettingApiSettingGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/setting'; +}; + +export type GetSettingApiSettingGetResponses = { + /** + * Successful Response + */ + 200: SettingGetOut; +}; + +export type GetSettingApiSettingGetResponse = GetSettingApiSettingGetResponses[keyof GetSettingApiSettingGetResponses]; + +export type UpdateSettingApiSettingPatchData = { + body: GlobalConfigRead; + path?: never; + query?: never; + url: '/api/setting'; +}; + +export type UpdateSettingApiSettingPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateSettingApiSettingPatchError = UpdateSettingApiSettingPatchErrors[keyof UpdateSettingApiSettingPatchErrors]; + +export type UpdateSettingApiSettingPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateSettingApiSettingPatchResponse = UpdateSettingApiSettingPatchResponses[keyof UpdateSettingApiSettingPatchResponses]; + +export type TestNotifyApiSettingActionsTestNotifyPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/setting/actions/test-notify'; +}; + +export type TestNotifyApiSettingActionsTestNotifyPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type TestNotifyApiSettingActionsTestNotifyPostResponse = TestNotifyApiSettingActionsTestNotifyPostResponses[keyof TestNotifyApiSettingActionsTestNotifyPostResponses]; + +export type ListWebhooksApiSettingWebhooksGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/setting/webhooks'; +}; + +export type ListWebhooksApiSettingWebhooksGetResponses = { + /** + * Successful Response + */ + 200: WebhookGetOut; +}; + +export type ListWebhooksApiSettingWebhooksGetResponse = ListWebhooksApiSettingWebhooksGetResponses[keyof ListWebhooksApiSettingWebhooksGetResponses]; + +export type CreateWebhookApiSettingWebhooksPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/setting/webhooks'; +}; + +export type CreateWebhookApiSettingWebhooksPostResponses = { + /** + * Successful Response + */ + 200: WebhookCreateOut; +}; + +export type CreateWebhookApiSettingWebhooksPostResponse = CreateWebhookApiSettingWebhooksPostResponses[keyof CreateWebhookApiSettingWebhooksPostResponses]; + +export type ReorderWebhooksApiSettingWebhooksOrderPatchData = { + body: IndexOrderPatch; + path?: never; + query?: never; + url: '/api/setting/webhooks/order'; +}; + +export type ReorderWebhooksApiSettingWebhooksOrderPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReorderWebhooksApiSettingWebhooksOrderPatchError = ReorderWebhooksApiSettingWebhooksOrderPatchErrors[keyof ReorderWebhooksApiSettingWebhooksOrderPatchErrors]; + +export type ReorderWebhooksApiSettingWebhooksOrderPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type ReorderWebhooksApiSettingWebhooksOrderPatchResponse = ReorderWebhooksApiSettingWebhooksOrderPatchResponses[keyof ReorderWebhooksApiSettingWebhooksOrderPatchResponses]; + +export type TestWebhookApiSettingWebhooksTestPostData = { + body: WebhookRead; + path?: never; + query?: never; + url: '/api/setting/webhooks/test'; +}; + +export type TestWebhookApiSettingWebhooksTestPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type TestWebhookApiSettingWebhooksTestPostError = TestWebhookApiSettingWebhooksTestPostErrors[keyof TestWebhookApiSettingWebhooksTestPostErrors]; + +export type TestWebhookApiSettingWebhooksTestPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type TestWebhookApiSettingWebhooksTestPostResponse = TestWebhookApiSettingWebhooksTestPostResponses[keyof TestWebhookApiSettingWebhooksTestPostResponses]; + +export type DeleteWebhookApiSettingWebhooksWebhookIdDeleteData = { + body?: never; + path: { + /** + * Webhook Id + * + * Webhook ID + */ + webhook_id: string; + }; + query?: never; + url: '/api/setting/webhooks/{webhook_id}'; +}; + +export type DeleteWebhookApiSettingWebhooksWebhookIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteWebhookApiSettingWebhooksWebhookIdDeleteError = DeleteWebhookApiSettingWebhooksWebhookIdDeleteErrors[keyof DeleteWebhookApiSettingWebhooksWebhookIdDeleteErrors]; + +export type DeleteWebhookApiSettingWebhooksWebhookIdDeleteResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DeleteWebhookApiSettingWebhooksWebhookIdDeleteResponse = DeleteWebhookApiSettingWebhooksWebhookIdDeleteResponses[keyof DeleteWebhookApiSettingWebhooksWebhookIdDeleteResponses]; + +export type GetWebhookApiSettingWebhooksWebhookIdGetData = { + body?: never; + path: { + /** + * Webhook Id + * + * Webhook ID + */ + webhook_id: string; + }; + query?: never; + url: '/api/setting/webhooks/{webhook_id}'; +}; + +export type GetWebhookApiSettingWebhooksWebhookIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetWebhookApiSettingWebhooksWebhookIdGetError = GetWebhookApiSettingWebhooksWebhookIdGetErrors[keyof GetWebhookApiSettingWebhooksWebhookIdGetErrors]; + +export type GetWebhookApiSettingWebhooksWebhookIdGetResponses = { + /** + * Successful Response + */ + 200: WebhookDetailOut; +}; + +export type GetWebhookApiSettingWebhooksWebhookIdGetResponse = GetWebhookApiSettingWebhooksWebhookIdGetResponses[keyof GetWebhookApiSettingWebhooksWebhookIdGetResponses]; + +export type UpdateWebhookApiSettingWebhooksWebhookIdPatchData = { + body: WebhookRead; + path: { + /** + * Webhook Id + * + * Webhook ID + */ + webhook_id: string; + }; + query?: never; + url: '/api/setting/webhooks/{webhook_id}'; +}; + +export type UpdateWebhookApiSettingWebhooksWebhookIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateWebhookApiSettingWebhooksWebhookIdPatchError = UpdateWebhookApiSettingWebhooksWebhookIdPatchErrors[keyof UpdateWebhookApiSettingWebhooksWebhookIdPatchErrors]; + +export type UpdateWebhookApiSettingWebhooksWebhookIdPatchResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type UpdateWebhookApiSettingWebhooksWebhookIdPatchResponse = UpdateWebhookApiSettingWebhooksWebhookIdPatchResponses[keyof UpdateWebhookApiSettingWebhooksWebhookIdPatchResponses]; + +export type CheckUpdateRestApiUpdateCheckGetData = { + body?: never; + path?: never; + query: { + /** + * Current Version + */ + current_version: string; + /** + * If Force + */ + if_force?: boolean; + }; + url: '/api/update/check'; +}; + +export type CheckUpdateRestApiUpdateCheckGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CheckUpdateRestApiUpdateCheckGetError = CheckUpdateRestApiUpdateCheckGetErrors[keyof CheckUpdateRestApiUpdateCheckGetErrors]; + +export type CheckUpdateRestApiUpdateCheckGetResponses = { + /** + * Successful Response + */ + 200: UpdateCheckOut; +}; + +export type CheckUpdateRestApiUpdateCheckGetResponse = CheckUpdateRestApiUpdateCheckGetResponses[keyof CheckUpdateRestApiUpdateCheckGetResponses]; + +export type CheckUpdateApiUpdateCheckPostData = { + body: UpdateCheckIn; + path?: never; + query?: never; + url: '/api/update/check'; +}; + +export type CheckUpdateApiUpdateCheckPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CheckUpdateApiUpdateCheckPostError = CheckUpdateApiUpdateCheckPostErrors[keyof CheckUpdateApiUpdateCheckPostErrors]; + +export type CheckUpdateApiUpdateCheckPostResponses = { + /** + * Successful Response + */ + 200: UpdateCheckOut; +}; + +export type CheckUpdateApiUpdateCheckPostResponse = CheckUpdateApiUpdateCheckPostResponses[keyof CheckUpdateApiUpdateCheckPostResponses]; + +export type DownloadUpdateApiUpdateDownloadPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/update/download'; +}; + +export type DownloadUpdateApiUpdateDownloadPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type DownloadUpdateApiUpdateDownloadPostResponse = DownloadUpdateApiUpdateDownloadPostResponses[keyof DownloadUpdateApiUpdateDownloadPostResponses]; + +export type InstallUpdateApiUpdateInstallPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/update/install'; +}; + +export type InstallUpdateApiUpdateInstallPostResponses = { + /** + * Successful Response + */ + 200: OutBase; +}; + +export type InstallUpdateApiUpdateInstallPostResponse = InstallUpdateApiUpdateInstallPostResponses[keyof InstallUpdateApiUpdateInstallPostResponses]; + +export type GetScreenshotApiOcrScreenshotPostData = { + body: OcrScreenshotIn; + path?: never; + query?: never; + url: '/api/ocr/screenshot'; +}; + +export type GetScreenshotApiOcrScreenshotPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetScreenshotApiOcrScreenshotPostError = GetScreenshotApiOcrScreenshotPostErrors[keyof GetScreenshotApiOcrScreenshotPostErrors]; + +export type GetScreenshotApiOcrScreenshotPostResponses = { + /** + * Successful Response + */ + 200: OcrScreenshotOut; +}; + +export type GetScreenshotApiOcrScreenshotPostResponse = GetScreenshotApiOcrScreenshotPostResponses[keyof GetScreenshotApiOcrScreenshotPostResponses]; + +export type GetScreenshotAdbApiOcrScreenshotAdbPostData = { + body: AdbScreenshotIn; + path?: never; + query?: never; + url: '/api/ocr/screenshot/adb'; +}; + +export type GetScreenshotAdbApiOcrScreenshotAdbPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetScreenshotAdbApiOcrScreenshotAdbPostError = GetScreenshotAdbApiOcrScreenshotAdbPostErrors[keyof GetScreenshotAdbApiOcrScreenshotAdbPostErrors]; + +export type GetScreenshotAdbApiOcrScreenshotAdbPostResponses = { + /** + * Successful Response + */ + 200: AdbScreenshotOut; +}; + +export type GetScreenshotAdbApiOcrScreenshotAdbPostResponse = GetScreenshotAdbApiOcrScreenshotAdbPostResponses[keyof GetScreenshotAdbApiOcrScreenshotAdbPostResponses]; + +export type CheckImageApiOcrCheckImagePostData = { + body: CheckImageIn; + path?: never; + query?: never; + url: '/api/ocr/check/image'; +}; + +export type CheckImageApiOcrCheckImagePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CheckImageApiOcrCheckImagePostError = CheckImageApiOcrCheckImagePostErrors[keyof CheckImageApiOcrCheckImagePostErrors]; + +export type CheckImageApiOcrCheckImagePostResponses = { + /** + * Successful Response + */ + 200: CheckImageOut; +}; + +export type CheckImageApiOcrCheckImagePostResponse = CheckImageApiOcrCheckImagePostResponses[keyof CheckImageApiOcrCheckImagePostResponses]; + +export type CheckImageAnyApiOcrCheckImageAnyPostData = { + body: CheckImageAnyIn; + path?: never; + query?: never; + url: '/api/ocr/check/image/any'; +}; + +export type CheckImageAnyApiOcrCheckImageAnyPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CheckImageAnyApiOcrCheckImageAnyPostError = CheckImageAnyApiOcrCheckImageAnyPostErrors[keyof CheckImageAnyApiOcrCheckImageAnyPostErrors]; + +export type CheckImageAnyApiOcrCheckImageAnyPostResponses = { + /** + * Successful Response + */ + 200: CheckImageOut; +}; + +export type CheckImageAnyApiOcrCheckImageAnyPostResponse = CheckImageAnyApiOcrCheckImageAnyPostResponses[keyof CheckImageAnyApiOcrCheckImageAnyPostResponses]; + +export type CheckImageAllApiOcrCheckImageAllPostData = { + body: CheckImageAllIn; + path?: never; + query?: never; + url: '/api/ocr/check/image/all'; +}; + +export type CheckImageAllApiOcrCheckImageAllPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CheckImageAllApiOcrCheckImageAllPostError = CheckImageAllApiOcrCheckImageAllPostErrors[keyof CheckImageAllApiOcrCheckImageAllPostErrors]; + +export type CheckImageAllApiOcrCheckImageAllPostResponses = { + /** + * Successful Response + */ + 200: CheckImageOut; +}; + +export type CheckImageAllApiOcrCheckImageAllPostResponse = CheckImageAllApiOcrCheckImageAllPostResponses[keyof CheckImageAllApiOcrCheckImageAllPostResponses]; + +export type ClickImageApiOcrClickImagePostData = { + body: ClickImageIn; + path?: never; + query?: never; + url: '/api/ocr/click/image'; +}; + +export type ClickImageApiOcrClickImagePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ClickImageApiOcrClickImagePostError = ClickImageApiOcrClickImagePostErrors[keyof ClickImageApiOcrClickImagePostErrors]; + +export type ClickImageApiOcrClickImagePostResponses = { + /** + * Successful Response + */ + 200: ClickOut; +}; + +export type ClickImageApiOcrClickImagePostResponse = ClickImageApiOcrClickImagePostResponses[keyof ClickImageApiOcrClickImagePostResponses]; + +export type ClickTextApiOcrClickTextPostData = { + body: ClickTextIn; + path?: never; + query?: never; + url: '/api/ocr/click/text'; +}; + +export type ClickTextApiOcrClickTextPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ClickTextApiOcrClickTextPostError = ClickTextApiOcrClickTextPostErrors[keyof ClickTextApiOcrClickTextPostErrors]; + +export type ClickTextApiOcrClickTextPostResponses = { + /** + * Successful Response + */ + 200: ClickOut; +}; + +export type ClickTextApiOcrClickTextPostResponse = ClickTextApiOcrClickTextPostResponses[keyof ClickTextApiOcrClickTextPostResponses]; + +export type CreateClientApiWsDebugClientCreatePostData = { + body: WsClientCreateIn; + path?: never; + query?: never; + url: '/api/ws_debug/client/create'; +}; + +export type CreateClientApiWsDebugClientCreatePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateClientApiWsDebugClientCreatePostError = CreateClientApiWsDebugClientCreatePostErrors[keyof CreateClientApiWsDebugClientCreatePostErrors]; + +export type CreateClientApiWsDebugClientCreatePostResponses = { + /** + * Successful Response + */ + 200: WsClientCreateOut; +}; + +export type CreateClientApiWsDebugClientCreatePostResponse = CreateClientApiWsDebugClientCreatePostResponses[keyof CreateClientApiWsDebugClientCreatePostResponses]; + +export type ConnectClientApiWsDebugClientConnectPostData = { + body: WsClientConnectIn; + path?: never; + query?: never; + url: '/api/ws_debug/client/connect'; +}; + +export type ConnectClientApiWsDebugClientConnectPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ConnectClientApiWsDebugClientConnectPostError = ConnectClientApiWsDebugClientConnectPostErrors[keyof ConnectClientApiWsDebugClientConnectPostErrors]; + +export type ConnectClientApiWsDebugClientConnectPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type ConnectClientApiWsDebugClientConnectPostResponse = ConnectClientApiWsDebugClientConnectPostResponses[keyof ConnectClientApiWsDebugClientConnectPostResponses]; + +export type DisconnectClientApiWsDebugClientDisconnectPostData = { + body: WsClientDisconnectIn; + path?: never; + query?: never; + url: '/api/ws_debug/client/disconnect'; +}; + +export type DisconnectClientApiWsDebugClientDisconnectPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DisconnectClientApiWsDebugClientDisconnectPostError = DisconnectClientApiWsDebugClientDisconnectPostErrors[keyof DisconnectClientApiWsDebugClientDisconnectPostErrors]; + +export type DisconnectClientApiWsDebugClientDisconnectPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type DisconnectClientApiWsDebugClientDisconnectPostResponse = DisconnectClientApiWsDebugClientDisconnectPostResponses[keyof DisconnectClientApiWsDebugClientDisconnectPostResponses]; + +export type RemoveClientApiWsDebugClientRemovePostData = { + body: WsClientRemoveIn; + path?: never; + query?: never; + url: '/api/ws_debug/client/remove'; +}; + +export type RemoveClientApiWsDebugClientRemovePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RemoveClientApiWsDebugClientRemovePostError = RemoveClientApiWsDebugClientRemovePostErrors[keyof RemoveClientApiWsDebugClientRemovePostErrors]; + +export type RemoveClientApiWsDebugClientRemovePostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type RemoveClientApiWsDebugClientRemovePostResponse = RemoveClientApiWsDebugClientRemovePostResponses[keyof RemoveClientApiWsDebugClientRemovePostResponses]; + +export type GetClientStatusApiWsDebugClientStatusPostData = { + body: WsClientStatusIn; + path?: never; + query?: never; + url: '/api/ws_debug/client/status'; +}; + +export type GetClientStatusApiWsDebugClientStatusPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetClientStatusApiWsDebugClientStatusPostError = GetClientStatusApiWsDebugClientStatusPostErrors[keyof GetClientStatusApiWsDebugClientStatusPostErrors]; + +export type GetClientStatusApiWsDebugClientStatusPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type GetClientStatusApiWsDebugClientStatusPostResponse = GetClientStatusApiWsDebugClientStatusPostResponses[keyof GetClientStatusApiWsDebugClientStatusPostResponses]; + +export type ListClientsApiWsDebugClientListGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/ws_debug/client/list'; +}; + +export type ListClientsApiWsDebugClientListGetResponses = { + /** + * Successful Response + */ + 200: WsClientListOut; +}; + +export type ListClientsApiWsDebugClientListGetResponse = ListClientsApiWsDebugClientListGetResponses[keyof ListClientsApiWsDebugClientListGetResponses]; + +export type SendMessageApiWsDebugMessageSendPostData = { + body: WsClientSendIn; + path?: never; + query?: never; + url: '/api/ws_debug/message/send'; +}; + +export type SendMessageApiWsDebugMessageSendPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SendMessageApiWsDebugMessageSendPostError = SendMessageApiWsDebugMessageSendPostErrors[keyof SendMessageApiWsDebugMessageSendPostErrors]; + +export type SendMessageApiWsDebugMessageSendPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type SendMessageApiWsDebugMessageSendPostResponse = SendMessageApiWsDebugMessageSendPostResponses[keyof SendMessageApiWsDebugMessageSendPostResponses]; + +export type SendJsonMessageApiWsDebugMessageSendJsonPostData = { + body: WsClientSendJsonIn; + path?: never; + query?: never; + url: '/api/ws_debug/message/send_json'; +}; + +export type SendJsonMessageApiWsDebugMessageSendJsonPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SendJsonMessageApiWsDebugMessageSendJsonPostError = SendJsonMessageApiWsDebugMessageSendJsonPostErrors[keyof SendJsonMessageApiWsDebugMessageSendJsonPostErrors]; + +export type SendJsonMessageApiWsDebugMessageSendJsonPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type SendJsonMessageApiWsDebugMessageSendJsonPostResponse = SendJsonMessageApiWsDebugMessageSendJsonPostResponses[keyof SendJsonMessageApiWsDebugMessageSendJsonPostResponses]; + +export type SendAuthApiWsDebugMessageAuthPostData = { + body: WsClientAuthIn; + path?: never; + query?: never; + url: '/api/ws_debug/message/auth'; +}; + +export type SendAuthApiWsDebugMessageAuthPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SendAuthApiWsDebugMessageAuthPostError = SendAuthApiWsDebugMessageAuthPostErrors[keyof SendAuthApiWsDebugMessageAuthPostErrors]; + +export type SendAuthApiWsDebugMessageAuthPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type SendAuthApiWsDebugMessageAuthPostResponse = SendAuthApiWsDebugMessageAuthPostResponses[keyof SendAuthApiWsDebugMessageAuthPostResponses]; + +export type GetHistoryApiWsDebugHistoryGetData = { + body?: never; + path?: never; + query?: { + /** + * Name + */ + name?: string | null; + }; + url: '/api/ws_debug/history'; +}; + +export type GetHistoryApiWsDebugHistoryGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetHistoryApiWsDebugHistoryGetError = GetHistoryApiWsDebugHistoryGetErrors[keyof GetHistoryApiWsDebugHistoryGetErrors]; + +export type GetHistoryApiWsDebugHistoryGetResponses = { + /** + * Successful Response + */ + 200: WsMessageHistoryOut; +}; + +export type GetHistoryApiWsDebugHistoryGetResponse = GetHistoryApiWsDebugHistoryGetResponses[keyof GetHistoryApiWsDebugHistoryGetResponses]; + +export type ClearHistoryApiWsDebugHistoryClearPostData = { + body: WsClearHistoryIn; + path?: never; + query?: never; + url: '/api/ws_debug/history/clear'; +}; + +export type ClearHistoryApiWsDebugHistoryClearPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ClearHistoryApiWsDebugHistoryClearPostError = ClearHistoryApiWsDebugHistoryClearPostErrors[keyof ClearHistoryApiWsDebugHistoryClearPostErrors]; + +export type ClearHistoryApiWsDebugHistoryClearPostResponses = { + /** + * Successful Response + */ + 200: WsClientStatusOut; +}; + +export type ClearHistoryApiWsDebugHistoryClearPostResponse = ClearHistoryApiWsDebugHistoryClearPostResponses[keyof ClearHistoryApiWsDebugHistoryClearPostResponses]; + +export type GetCommandsApiWsDebugCommandsGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/ws_debug/commands'; +}; + +export type GetCommandsApiWsDebugCommandsGetResponses = { + /** + * Successful Response + */ + 200: WsCommandsOut; +}; + +export type GetCommandsApiWsDebugCommandsGetResponse = GetCommandsApiWsDebugCommandsGetResponses[keyof GetCommandsApiWsDebugCommandsGetResponses]; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 6a808ac2..3aee3987 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,198 +1,134 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; -export type { OpenAPIConfig } from './core/OpenAPI'; +export { apiClient, apiRuntime } from './runtime' +export { + HISTORY_SEARCH_MODE, + POWER_SIGNAL, + SCRIPT_CREATE_TYPE, + STAGE_QUERY_TYPE, + TASK_CREATE_MODE, +} from './constants' +export type { + HistorySearchMode, + PowerSignal, + ScriptCreateType, + StageQueryType, + TaskCreateMode, +} from './constants' -export type { ADBScreenshotIn } from './models/ADBScreenshotIn'; -export type { ADBScreenshotOut } from './models/ADBScreenshotOut'; -export type { CheckImageAllIn } from './models/CheckImageAllIn'; -export type { CheckImageAnyIn } from './models/CheckImageAnyIn'; -export type { CheckImageIn } from './models/CheckImageIn'; -export type { CheckImageOut } from './models/CheckImageOut'; -export type { ClickImageIn } from './models/ClickImageIn'; -export type { ClickOut } from './models/ClickOut'; -export type { ClickTextIn } from './models/ClickTextIn'; -export type { ComboBoxItem } from './models/ComboBoxItem'; -export type { ComboBoxOut } from './models/ComboBoxOut'; -export type { DeviceInfo } from './models/DeviceInfo'; -export type { DispatchIn } from './models/DispatchIn'; -export type { EmulatorConfig } from './models/EmulatorConfig'; -export type { EmulatorConfig_Info } from './models/EmulatorConfig_Info'; -export type { EmulatorConfigIndexItem } from './models/EmulatorConfigIndexItem'; -export type { EmulatorCreateOut } from './models/EmulatorCreateOut'; -export type { EmulatorDeleteIn } from './models/EmulatorDeleteIn'; -export type { EmulatorGetIn } from './models/EmulatorGetIn'; -export type { EmulatorGetOut } from './models/EmulatorGetOut'; -export { EmulatorOperateIn } from './models/EmulatorOperateIn'; -export type { EmulatorReorderIn } from './models/EmulatorReorderIn'; -export type { EmulatorSearchOut } from './models/EmulatorSearchOut'; -export type { EmulatorSearchResult } from './models/EmulatorSearchResult'; -export type { EmulatorStatusOut } from './models/EmulatorStatusOut'; -export type { EmulatorUpdateIn } from './models/EmulatorUpdateIn'; -export type { GeneralConfig } from './models/GeneralConfig'; -export type { GeneralConfig_Game } from './models/GeneralConfig_Game'; -export type { GeneralConfig_Info } from './models/GeneralConfig_Info'; -export type { GeneralConfig_Run } from './models/GeneralConfig_Run'; -export type { GeneralConfig_Script } from './models/GeneralConfig_Script'; -export type { GeneralUserConfig } from './models/GeneralUserConfig'; -export type { GeneralUserConfig_Data } from './models/GeneralUserConfig_Data'; -export type { GeneralUserConfig_Info } from './models/GeneralUserConfig_Info'; -export type { GeneralUserConfig_Notify } from './models/GeneralUserConfig_Notify'; -export { GetStageIn } from './models/GetStageIn'; -export type { GlobalConfig } from './models/GlobalConfig'; -export type { GlobalConfig_Function } from './models/GlobalConfig_Function'; -export type { GlobalConfig_Notify } from './models/GlobalConfig_Notify'; -export type { GlobalConfig_Start } from './models/GlobalConfig_Start'; -export type { GlobalConfig_UI } from './models/GlobalConfig_UI'; -export type { GlobalConfig_Update } from './models/GlobalConfig_Update'; -export type { GlobalConfig_Voice } from './models/GlobalConfig_Voice'; -export type { HistoryData } from './models/HistoryData'; -export type { HistoryDataGetIn } from './models/HistoryDataGetIn'; -export type { HistoryDataGetOut } from './models/HistoryDataGetOut'; -export { HistoryIndexItem } from './models/HistoryIndexItem'; -export { HistorySearchIn } from './models/HistorySearchIn'; -export type { HistorySearchOut } from './models/HistorySearchOut'; -export type { HTTPValidationError } from './models/HTTPValidationError'; -export type { InfoOut } from './models/InfoOut'; -export type { MaaConfig } from './models/MaaConfig'; -export type { MaaConfig_Emulator } from './models/MaaConfig_Emulator'; -export type { MaaConfig_Info } from './models/MaaConfig_Info'; -export type { MaaConfig_Run } from './models/MaaConfig_Run'; -export type { MaaEndConfig } from './models/MaaEndConfig'; -export type { MaaEndConfig_Game } from './models/MaaEndConfig_Game'; -export type { MaaEndConfig_Info } from './models/MaaEndConfig_Info'; -export type { MaaEndConfig_Run } from './models/MaaEndConfig_Run'; -export type { MaaEndUserConfig } from './models/MaaEndUserConfig'; -export type { MaaEndUserConfig_Info } from './models/MaaEndUserConfig_Info'; -export type { MaaEndUserConfig_Notify } from './models/MaaEndUserConfig_Notify'; -export type { MaaEndUserConfig_Task } from './models/MaaEndUserConfig_Task'; -export type { MaaPlanConfig } from './models/MaaPlanConfig'; -export type { MaaPlanConfig_Info } from './models/MaaPlanConfig_Info'; -export type { MaaPlanConfig_Item } from './models/MaaPlanConfig_Item'; -export type { MaaUserConfig } from './models/MaaUserConfig'; -export type { MaaUserConfig_Data } from './models/MaaUserConfig_Data'; -export type { MaaUserConfig_Info } from './models/MaaUserConfig_Info'; -export type { MaaUserConfig_Notify } from './models/MaaUserConfig_Notify'; -export type { MaaUserConfig_Task } from './models/MaaUserConfig_Task'; -export type { NoticeOut } from './models/NoticeOut'; -export type { OCRScreenshotIn } from './models/OCRScreenshotIn'; -export type { OCRScreenshotOut } from './models/OCRScreenshotOut'; -export type { OutBase } from './models/OutBase'; -export type { PlanCreateIn } from './models/PlanCreateIn'; -export type { PlanCreateOut } from './models/PlanCreateOut'; -export type { PlanDeleteIn } from './models/PlanDeleteIn'; -export type { PlanGetIn } from './models/PlanGetIn'; -export type { PlanGetOut } from './models/PlanGetOut'; -export type { PlanIndexItem } from './models/PlanIndexItem'; -export type { PlanReorderIn } from './models/PlanReorderIn'; -export type { PlanUpdateIn } from './models/PlanUpdateIn'; -export { PowerIn } from './models/PowerIn'; -export { PowerOut } from './models/PowerOut'; -export type { QueueConfig } from './models/QueueConfig'; -export type { QueueConfig_Info } from './models/QueueConfig_Info'; -export type { QueueCreateOut } from './models/QueueCreateOut'; -export type { QueueDeleteIn } from './models/QueueDeleteIn'; -export type { QueueGetIn } from './models/QueueGetIn'; -export type { QueueGetOut } from './models/QueueGetOut'; -export type { QueueIndexItem } from './models/QueueIndexItem'; -export type { QueueItem } from './models/QueueItem'; -export type { QueueItem_Info } from './models/QueueItem_Info'; -export type { QueueItemCreateOut } from './models/QueueItemCreateOut'; -export type { QueueItemDeleteIn } from './models/QueueItemDeleteIn'; -export type { QueueItemGetIn } from './models/QueueItemGetIn'; -export type { QueueItemGetOut } from './models/QueueItemGetOut'; -export type { QueueItemIndexItem } from './models/QueueItemIndexItem'; -export type { QueueItemReorderIn } from './models/QueueItemReorderIn'; -export type { QueueItemUpdateIn } from './models/QueueItemUpdateIn'; -export type { QueueReorderIn } from './models/QueueReorderIn'; -export type { QueueSetInBase } from './models/QueueSetInBase'; -export type { QueueUpdateIn } from './models/QueueUpdateIn'; -export { ScriptCreateIn } from './models/ScriptCreateIn'; -export type { ScriptCreateOut } from './models/ScriptCreateOut'; -export type { ScriptDeleteIn } from './models/ScriptDeleteIn'; -export type { ScriptFileIn } from './models/ScriptFileIn'; -export type { ScriptGetIn } from './models/ScriptGetIn'; -export type { ScriptGetOut } from './models/ScriptGetOut'; -export { ScriptIndexItem } from './models/ScriptIndexItem'; -export type { ScriptReorderIn } from './models/ScriptReorderIn'; -export type { ScriptUpdateIn } from './models/ScriptUpdateIn'; -export type { ScriptUploadIn } from './models/ScriptUploadIn'; -export type { ScriptUrlIn } from './models/ScriptUrlIn'; -export type { SettingGetOut } from './models/SettingGetOut'; -export type { SettingUpdateIn } from './models/SettingUpdateIn'; -export type { SrcConfig } from './models/SrcConfig'; -export type { SrcConfig_Emulator } from './models/SrcConfig_Emulator'; -export type { SrcConfig_Info } from './models/SrcConfig_Info'; -export type { SrcConfig_Run } from './models/SrcConfig_Run'; -export type { SrcUserConfig } from './models/SrcUserConfig'; -export type { SrcUserConfig_Data } from './models/SrcUserConfig_Data'; -export type { SrcUserConfig_Info } from './models/SrcUserConfig_Info'; -export type { SrcUserConfig_Notify } from './models/SrcUserConfig_Notify'; -export type { SrcUserConfig_Stage } from './models/SrcUserConfig_Stage'; -export { TaskCreateIn } from './models/TaskCreateIn'; -export type { TaskCreateOut } from './models/TaskCreateOut'; -export type { TimeSet } from './models/TimeSet'; -export type { TimeSet_Info } from './models/TimeSet_Info'; -export type { TimeSetCreateOut } from './models/TimeSetCreateOut'; -export type { TimeSetDeleteIn } from './models/TimeSetDeleteIn'; -export type { TimeSetGetIn } from './models/TimeSetGetIn'; -export type { TimeSetGetOut } from './models/TimeSetGetOut'; -export type { TimeSetIndexItem } from './models/TimeSetIndexItem'; -export type { TimeSetReorderIn } from './models/TimeSetReorderIn'; -export type { TimeSetUpdateIn } from './models/TimeSetUpdateIn'; -export type { ToolsConfig } from './models/ToolsConfig'; -export type { ToolsConfig_ArknightsPC } from './models/ToolsConfig_ArknightsPC'; -export type { ToolsGetOut } from './models/ToolsGetOut'; -export type { ToolsUpdateIn } from './models/ToolsUpdateIn'; -export type { UpdateCheckIn } from './models/UpdateCheckIn'; -export type { UpdateCheckOut } from './models/UpdateCheckOut'; -export type { UserCreateOut } from './models/UserCreateOut'; -export type { UserDeleteIn } from './models/UserDeleteIn'; -export type { UserGetIn } from './models/UserGetIn'; -export type { UserGetOut } from './models/UserGetOut'; -export type { UserInBase } from './models/UserInBase'; -export { UserIndexItem } from './models/UserIndexItem'; -export type { UserReorderIn } from './models/UserReorderIn'; -export type { UserSetIn } from './models/UserSetIn'; -export type { UserUpdateIn } from './models/UserUpdateIn'; -export type { ValidationError } from './models/ValidationError'; -export type { VersionOut } from './models/VersionOut'; -export type { Webhook } from './models/Webhook'; -export type { Webhook_Data } from './models/Webhook_Data'; -export type { Webhook_Info } from './models/Webhook_Info'; -export type { WebhookCreateOut } from './models/WebhookCreateOut'; -export type { WebhookDeleteIn } from './models/WebhookDeleteIn'; -export type { WebhookGetIn } from './models/WebhookGetIn'; -export type { WebhookGetOut } from './models/WebhookGetOut'; -export type { WebhookInBase } from './models/WebhookInBase'; -export type { WebhookIndexItem } from './models/WebhookIndexItem'; -export type { WebhookReorderIn } from './models/WebhookReorderIn'; -export type { WebhookTestIn } from './models/WebhookTestIn'; -export type { WebhookUpdateIn } from './models/WebhookUpdateIn'; -export type { WSClearHistoryIn } from './models/WSClearHistoryIn'; -export type { WSClientAuthIn } from './models/WSClientAuthIn'; -export type { WSClientConnectIn } from './models/WSClientConnectIn'; -export type { WSClientCreateIn } from './models/WSClientCreateIn'; -export type { WSClientCreateOut } from './models/WSClientCreateOut'; -export type { WSClientDisconnectIn } from './models/WSClientDisconnectIn'; -export type { WSClientListOut } from './models/WSClientListOut'; -export type { WSClientRemoveIn } from './models/WSClientRemoveIn'; -export type { WSClientSendIn } from './models/WSClientSendIn'; -export type { WSClientSendJsonIn } from './models/WSClientSendJsonIn'; -export type { WSClientStatusIn } from './models/WSClientStatusIn'; -export type { WSClientStatusOut } from './models/WSClientStatusOut'; -export type { WSCommandsOut } from './models/WSCommandsOut'; -export type { WSMessageHistoryOut } from './models/WSMessageHistoryOut'; +export { dispatchApi } from './gateways/dispatch' +export { emulatorApi } from './gateways/emulator' +export { historyApi } from './gateways/history' +export { infoApi } from './gateways/info' +export { ocrApi } from './gateways/ocr' +export { planApi } from './gateways/plan' +export { queueApi, queueItemApi, timeSetApi } from './gateways/queue' +export { scriptApi, userApi } from './gateways/script' +export { settingApi, webhookApi } from './gateways/setting' +export { toolsApi } from './gateways/tools' +export { updateApi } from './gateways/update' +export { wsDebugApi } from './gateways/wsDebug' -export { Service } from './services/Service'; -export { ActionService } from './services/ActionService'; -export { AddService } from './services/AddService'; -export { DeleteService } from './services/DeleteService'; -export { GetService } from './services/GetService'; -export { OcrService } from './services/OcrService'; -export { UpdateService } from './services/UpdateService'; -export { WebSocketService } from './services/WebSocketService'; +export type { + AdbScreenshotIn, + AdbScreenshotOut, + ComboBoxItem, + DispatchIn, + EmulatorActionBody, + EmulatorConfigIndexItem, + EmulatorDetailOut, + EmulatorGetOut, + EmulatorRead, + EmulatorSearchOut, + EmulatorSearchResult, + GeneralConfig, + GeneralUserConfig, + GetStageIn, + GlobalConfigRead, + HistoryData, + HistoryDataGetIn, + HistoryDataGetOut, + HistoryIndexItem, + HistorySearchIn, + HttpValidationError, + IndexOrderPatch, + InfoOut, + InfrastructureImportBody, + MaaConfig, + MaaEndConfig, + MaaEndUserConfig, + MaaPlanDayPatch, + MaaPlanDayRead, + MaaPlanInfoPatch, + MaaPlanInfoRead, + MaaPlanPatch, + MaaPlanRead, + MaaUserConfig, + NoticeOut, + OcrScreenshotIn, + OcrScreenshotOut, + OutBase, + PlanCreateIn, + PlanCreateOut, + PlanDetailOut, + PlanGetOut, + PlanIndexItem, + PlanUpdateBody, + PowerIn, + PowerOut, + QueueCreateOut, + QueueDetailOut, + QueueGetOut, + QueueIndexItem, + QueueItemCreateOut, + QueueItemDetailOut, + QueueItemGetOut, + QueueItemIndexItem, + QueueItemRead, + QueueRead, + ScriptCreateIn, + ScriptCreateOut, + ScriptDetailOut, + ScriptFileBody, + ScriptGetOut, + ScriptIndexItem, + ScriptPatchBody, + ScriptUploadBody, + ScriptUrlBody, + SrcConfig, + SrcUserConfig, + TaskCreateIn, + TimeSetCreateOut, + TimeSetDetailOut, + TimeSetGetOut, + TimeSetIndexItem, + TimeSetRead, + ToolsConfigRead, + ToolsConfigReadArknightsPc, + UpdateCheckIn, + UpdateCheckOut, + UserCreateOut, + UserDetailOut, + UserGetOut, + UserIndexItem, + UserPatchBody, + ValidationError, + VersionOut, + WebhookCreateOut, + WebhookDetailOut, + WebhookGetOut, + WebhookIndexItem, + WebhookRead, + WsClearHistoryIn, + WsClientAuthIn, + WsClientConnectIn, + WsClientCreateIn, + WsClientCreateOut, + WsClientDisconnectIn, + WsClientListOut, + WsClientRemoveIn, + WsClientSendIn, + WsClientSendJsonIn, + WsClientStatusIn, + WsClientStatusOut, + WsCommandsOut, + WsMessageHistoryOut, +} from './generated' diff --git a/frontend/src/api/models/ADBScreenshotIn.ts b/frontend/src/api/models/ADBScreenshotIn.ts deleted file mode 100644 index 96e0e138..00000000 --- a/frontend/src/api/models/ADBScreenshotIn.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ADBScreenshotIn = { - /** - * ADB 可执行文件的路径 - */ - adb_path: string; - /** - * 设备序列号,格式如 '127.0.0.1:5555' 或 'emulator-5554' - */ - serial: string; - /** - * 是否使用 screencap PNG 方法,False 时使用 screencap raw 方法 - */ - use_screencap?: boolean; -}; - diff --git a/frontend/src/api/models/ADBScreenshotOut.ts b/frontend/src/api/models/ADBScreenshotOut.ts deleted file mode 100644 index 922c55dd..00000000 --- a/frontend/src/api/models/ADBScreenshotOut.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ADBScreenshotOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 截图的Base64编码(PNG格式) - */ - image_base64: string; - /** - * 截图宽度 - */ - image_width: number; - /** - * 截图高度 - */ - image_height: number; - /** - * 设备序列号 - */ - serial: string; -}; - diff --git a/frontend/src/api/models/CheckImageAllIn.ts b/frontend/src/api/models/CheckImageAllIn.ts deleted file mode 100644 index 8e11117e..00000000 --- a/frontend/src/api/models/CheckImageAllIn.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type CheckImageAllIn = { - /** - * 窗口标题(用于查找窗口) - */ - window_title: string; - /** - * 要查找的图片路径列表 - */ - image_paths: Array; - /** - * 截图间隔时间(秒) - */ - interval?: number; - /** - * 重复截图次数 - */ - retry_times?: number; - /** - * 图像匹配阈值,范围 0-1 - */ - threshold?: number; -}; - diff --git a/frontend/src/api/models/CheckImageAnyIn.ts b/frontend/src/api/models/CheckImageAnyIn.ts deleted file mode 100644 index a890400a..00000000 --- a/frontend/src/api/models/CheckImageAnyIn.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type CheckImageAnyIn = { - /** - * 窗口标题(用于查找窗口) - */ - window_title: string; - /** - * 要查找的图片路径列表 - */ - image_paths: Array; - /** - * 截图间隔时间(秒) - */ - interval?: number; - /** - * 重复截图次数 - */ - retry_times?: number; - /** - * 图像匹配阈值,范围 0-1 - */ - threshold?: number; -}; - diff --git a/frontend/src/api/models/CheckImageIn.ts b/frontend/src/api/models/CheckImageIn.ts deleted file mode 100644 index 3f6c21c9..00000000 --- a/frontend/src/api/models/CheckImageIn.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type CheckImageIn = { - /** - * 窗口标题(用于查找窗口) - */ - window_title: string; - /** - * 要查找的图片路径 - */ - image_path: string; - /** - * 截图间隔时间(秒) - */ - interval?: number; - /** - * 重复截图次数 - */ - retry_times?: number; - /** - * 图像匹配阈值,范围 0-1 - */ - threshold?: number; -}; - diff --git a/frontend/src/api/models/CheckImageOut.ts b/frontend/src/api/models/CheckImageOut.ts deleted file mode 100644 index 0ea87fb1..00000000 --- a/frontend/src/api/models/CheckImageOut.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type CheckImageOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 是否找到图像 - */ - found: boolean; - /** - * 实际尝试次数 - */ - attempts: number; -}; - diff --git a/frontend/src/api/models/ClickImageIn.ts b/frontend/src/api/models/ClickImageIn.ts deleted file mode 100644 index 71958e71..00000000 --- a/frontend/src/api/models/ClickImageIn.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ClickImageIn = { - /** - * 窗口标题(用于查找窗口) - */ - window_title: string; - /** - * 要查找并点击的图片路径 - */ - image_path: string; - /** - * 截图间隔时间(秒) - */ - interval?: number; - /** - * 重复截图次数 - */ - retry_times?: number; - /** - * 图像匹配阈值,范围 0-1 - */ - threshold?: number; -}; - diff --git a/frontend/src/api/models/ClickOut.ts b/frontend/src/api/models/ClickOut.ts deleted file mode 100644 index b1e09891..00000000 --- a/frontend/src/api/models/ClickOut.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ClickOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 是否成功点击 - */ - success: boolean; - /** - * 实际尝试次数 - */ - attempts: number; -}; - diff --git a/frontend/src/api/models/ClickTextIn.ts b/frontend/src/api/models/ClickTextIn.ts deleted file mode 100644 index dfab5e7c..00000000 --- a/frontend/src/api/models/ClickTextIn.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ClickTextIn = { - /** - * 窗口标题(用于查找窗口) - */ - window_title: string; - /** - * 要查找并点击的文字内容 - */ - text: string; - /** - * 截图间隔时间(秒) - */ - interval?: number; - /** - * 重复截图次数 - */ - retry_times?: number; -}; - diff --git a/frontend/src/api/models/ComboBoxItem.ts b/frontend/src/api/models/ComboBoxItem.ts deleted file mode 100644 index 67dd70f0..00000000 --- a/frontend/src/api/models/ComboBoxItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ComboBoxItem = { - /** - * 展示值 - */ - label: string; - /** - * 实际值 - */ - value: (string | null); -}; - diff --git a/frontend/src/api/models/ComboBoxOut.ts b/frontend/src/api/models/ComboBoxOut.ts deleted file mode 100644 index 753e7164..00000000 --- a/frontend/src/api/models/ComboBoxOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ComboBoxItem } from './ComboBoxItem'; -export type ComboBoxOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 下拉框选项 - */ - data: Array; -}; - diff --git a/frontend/src/api/models/DeviceInfo.ts b/frontend/src/api/models/DeviceInfo.ts deleted file mode 100644 index 9b27dc1a..00000000 --- a/frontend/src/api/models/DeviceInfo.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 设备信息 - */ -export type DeviceInfo = { - /** - * 设备标题/名称 - */ - title: string; - /** - * 设备状态, 参考DeviceStatus枚举值 - */ - status: number; - /** - * ADB连接地址 - */ - adb_address: string; -}; - diff --git a/frontend/src/api/models/DispatchIn.ts b/frontend/src/api/models/DispatchIn.ts deleted file mode 100644 index e8332cf8..00000000 --- a/frontend/src/api/models/DispatchIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type DispatchIn = { - /** - * 目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID - */ - taskId: string; -}; - diff --git a/frontend/src/api/models/EmulatorConfig.ts b/frontend/src/api/models/EmulatorConfig.ts deleted file mode 100644 index 95de07f0..00000000 --- a/frontend/src/api/models/EmulatorConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorConfig_Info } from './EmulatorConfig_Info'; -export type EmulatorConfig = { - /** - * 模拟器基础信息 - */ - Info?: (EmulatorConfig_Info | null); -}; - diff --git a/frontend/src/api/models/EmulatorConfigIndexItem.ts b/frontend/src/api/models/EmulatorConfigIndexItem.ts deleted file mode 100644 index 8a3643c3..00000000 --- a/frontend/src/api/models/EmulatorConfigIndexItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorConfigIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: string; -}; - diff --git a/frontend/src/api/models/EmulatorConfig_Info.ts b/frontend/src/api/models/EmulatorConfig_Info.ts deleted file mode 100644 index 80b8397a..00000000 --- a/frontend/src/api/models/EmulatorConfig_Info.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorConfig_Info = { - /** - * 模拟器名称 - */ - Name?: (string | null); - /** - * 模拟器类型 - */ - Type?: ('general' | 'mumu' | 'ldplayer' | null); - /** - * 模拟器路径 - */ - Path?: (string | null); - /** - * 老板键快捷键配置 - */ - BossKey?: (string | null); - /** - * 最大等待时间(秒) - */ - MaxWaitTime?: (number | null); -}; - diff --git a/frontend/src/api/models/EmulatorCreateOut.ts b/frontend/src/api/models/EmulatorCreateOut.ts deleted file mode 100644 index c48745cc..00000000 --- a/frontend/src/api/models/EmulatorCreateOut.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorConfig } from './EmulatorConfig'; -export type EmulatorCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的模拟器 ID - */ - emulatorId: string; - /** - * 模拟器配置数据 - */ - data: EmulatorConfig; -}; - diff --git a/frontend/src/api/models/EmulatorDeleteIn.ts b/frontend/src/api/models/EmulatorDeleteIn.ts deleted file mode 100644 index d6ded6f3..00000000 --- a/frontend/src/api/models/EmulatorDeleteIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorDeleteIn = { - /** - * 模拟器 ID - */ - emulatorId: string; -}; - diff --git a/frontend/src/api/models/EmulatorGetIn.ts b/frontend/src/api/models/EmulatorGetIn.ts deleted file mode 100644 index 13a6eb69..00000000 --- a/frontend/src/api/models/EmulatorGetIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorGetIn = { - /** - * 模拟器ID, 未携带时表示获取所有模拟器数据 - */ - emulatorId?: (string | null); -}; - diff --git a/frontend/src/api/models/EmulatorGetOut.ts b/frontend/src/api/models/EmulatorGetOut.ts deleted file mode 100644 index b2b939fe..00000000 --- a/frontend/src/api/models/EmulatorGetOut.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorConfig } from './EmulatorConfig'; -import type { EmulatorConfigIndexItem } from './EmulatorConfigIndexItem'; -export type EmulatorGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 模拟器索引列表 - */ - index: Array; - /** - * 模拟器数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/EmulatorOperateIn.ts b/frontend/src/api/models/EmulatorOperateIn.ts deleted file mode 100644 index 91174a44..00000000 --- a/frontend/src/api/models/EmulatorOperateIn.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorOperateIn = { - /** - * 模拟器 ID - */ - emulatorId: string; - /** - * 操作类型 - */ - operate: EmulatorOperateIn.operate; - /** - * 模拟器索引 - */ - index: string; -}; -export namespace EmulatorOperateIn { - /** - * 操作类型 - */ - export enum operate { - OPEN = 'open', - CLOSE = 'close', - SHOW = 'show', - } -} - diff --git a/frontend/src/api/models/EmulatorReorderIn.ts b/frontend/src/api/models/EmulatorReorderIn.ts deleted file mode 100644 index 14b71bd5..00000000 --- a/frontend/src/api/models/EmulatorReorderIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorReorderIn = { - /** - * 模拟器 ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/EmulatorSearchOut.ts b/frontend/src/api/models/EmulatorSearchOut.ts deleted file mode 100644 index 24f3b3e3..00000000 --- a/frontend/src/api/models/EmulatorSearchOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorSearchResult } from './EmulatorSearchResult'; -export type EmulatorSearchOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 搜索到的模拟器列表 - */ - emulators?: Array; -}; - diff --git a/frontend/src/api/models/EmulatorSearchResult.ts b/frontend/src/api/models/EmulatorSearchResult.ts deleted file mode 100644 index 43923597..00000000 --- a/frontend/src/api/models/EmulatorSearchResult.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type EmulatorSearchResult = { - /** - * 模拟器类型 - */ - type: string; - /** - * 模拟器路径 - */ - path: string; - /** - * 模拟器名称 - */ - name: string; -}; - diff --git a/frontend/src/api/models/EmulatorStatusOut.ts b/frontend/src/api/models/EmulatorStatusOut.ts deleted file mode 100644 index 2fb1810c..00000000 --- a/frontend/src/api/models/EmulatorStatusOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { DeviceInfo } from './DeviceInfo'; -export type EmulatorStatusOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 模拟器状态信息, 外层key为模拟器ID, 内层key为设备索引, value为设备信息 - */ - data: Record>; -}; - diff --git a/frontend/src/api/models/EmulatorUpdateIn.ts b/frontend/src/api/models/EmulatorUpdateIn.ts deleted file mode 100644 index 1c023ff5..00000000 --- a/frontend/src/api/models/EmulatorUpdateIn.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorConfig } from './EmulatorConfig'; -export type EmulatorUpdateIn = { - /** - * 模拟器 ID - */ - emulatorId: string; - /** - * 模拟器更新数据 - */ - data: EmulatorConfig; -}; - diff --git a/frontend/src/api/models/GeneralConfig.ts b/frontend/src/api/models/GeneralConfig.ts deleted file mode 100644 index 1637ffa6..00000000 --- a/frontend/src/api/models/GeneralConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralConfig_Game } from './GeneralConfig_Game'; -import type { GeneralConfig_Info } from './GeneralConfig_Info'; -import type { GeneralConfig_Run } from './GeneralConfig_Run'; -import type { GeneralConfig_Script } from './GeneralConfig_Script'; -export type GeneralConfig = { - /** - * 脚本基础信息 - */ - Info?: (GeneralConfig_Info | null); - /** - * 脚本配置 - */ - Script?: (GeneralConfig_Script | null); - /** - * 游戏配置 - */ - Game?: (GeneralConfig_Game | null); - /** - * 运行配置 - */ - Run?: (GeneralConfig_Run | null); -}; - diff --git a/frontend/src/api/models/GeneralConfig_Game.ts b/frontend/src/api/models/GeneralConfig_Game.ts deleted file mode 100644 index 4555c042..00000000 --- a/frontend/src/api/models/GeneralConfig_Game.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralConfig_Game = { - /** - * 游戏/模拟器相关功能是否启用 - */ - Enabled?: (boolean | null); - /** - * 类型: 模拟器, PC端, URL协议 - */ - Type?: ('Emulator' | 'Client' | 'URL' | null); - /** - * 游戏/模拟器程序路径 - */ - Path?: (string | null); - /** - * 自定义协议URL - */ - URL?: (string | null); - /** - * 游戏进程名称 - */ - ProcessName?: (string | null); - /** - * 游戏/模拟器启动参数 - */ - Arguments?: (string | null); - /** - * 游戏/模拟器等待启动时间 - */ - WaitTime?: (number | null); - /** - * 是否强制关闭游戏/模拟器进程 - */ - IfForceClose?: (boolean | null); - /** - * 模拟器ID - */ - EmulatorId?: (string | null); - /** - * 模拟器多开实例索引 - */ - EmulatorIndex?: (string | null); -}; - diff --git a/frontend/src/api/models/GeneralConfig_Info.ts b/frontend/src/api/models/GeneralConfig_Info.ts deleted file mode 100644 index 866a89cf..00000000 --- a/frontend/src/api/models/GeneralConfig_Info.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralConfig_Info = { - /** - * 脚本名称 - */ - Name?: (string | null); - /** - * 脚本根目录 - */ - RootPath?: (string | null); -}; - diff --git a/frontend/src/api/models/GeneralConfig_Run.ts b/frontend/src/api/models/GeneralConfig_Run.ts deleted file mode 100644 index 3de79aa2..00000000 --- a/frontend/src/api/models/GeneralConfig_Run.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralConfig_Run = { - /** - * 每日代理次数限制 - */ - ProxyTimesLimit?: (number | null); - /** - * 重试次数限制 - */ - RunTimesLimit?: (number | null); - /** - * 日志超时限制 - */ - RunTimeLimit?: (number | null); -}; - diff --git a/frontend/src/api/models/GeneralConfig_Script.ts b/frontend/src/api/models/GeneralConfig_Script.ts deleted file mode 100644 index 342e741e..00000000 --- a/frontend/src/api/models/GeneralConfig_Script.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralConfig_Script = { - /** - * 脚本可执行文件路径 - */ - ScriptPath?: (string | null); - /** - * 脚本启动附加命令参数 - */ - Arguments?: (string | null); - /** - * 是否追踪脚本子进程 - */ - IfTrackProcess?: (boolean | null); - /** - * 追踪进程名称 - */ - TrackProcessName?: (string | null); - /** - * 追踪进程文件路径 - */ - TrackProcessExe?: (string | null); - /** - * 追踪进程启动命令行参数 - */ - TrackProcessCmdline?: (string | null); - /** - * 配置文件路径 - */ - ConfigPath?: (string | null); - /** - * 配置文件类型: 单个文件, 文件夹 - */ - ConfigPathMode?: ('File' | 'Folder' | null); - /** - * 更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时 - */ - UpdateConfigMode?: ('Never' | 'Success' | 'Failure' | 'Always' | null); - /** - * 日志文件路径 - */ - LogPath?: (string | null); - /** - * 日志文件名格式 - */ - LogPathFormat?: (string | null); - /** - * 日志时间戳开始位置 - */ - LogTimeStart?: (number | null); - /** - * 日志时间戳结束位置 - */ - LogTimeEnd?: (number | null); - /** - * 日志时间戳格式 - */ - LogTimeFormat?: (string | null); - /** - * 成功时日志 - */ - SuccessLog?: (string | null); - /** - * 错误时日志 - */ - ErrorLog?: (string | null); -}; - diff --git a/frontend/src/api/models/GeneralUserConfig.ts b/frontend/src/api/models/GeneralUserConfig.ts deleted file mode 100644 index 3538a952..00000000 --- a/frontend/src/api/models/GeneralUserConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralUserConfig_Data } from './GeneralUserConfig_Data'; -import type { GeneralUserConfig_Info } from './GeneralUserConfig_Info'; -import type { GeneralUserConfig_Notify } from './GeneralUserConfig_Notify'; -export type GeneralUserConfig = { - /** - * 用户信息 - */ - Info?: (GeneralUserConfig_Info | null); - /** - * 用户数据 - */ - Data?: (GeneralUserConfig_Data | null); - /** - * 单独通知 - */ - Notify?: (GeneralUserConfig_Notify | null); -}; - diff --git a/frontend/src/api/models/GeneralUserConfig_Data.ts b/frontend/src/api/models/GeneralUserConfig_Data.ts deleted file mode 100644 index 85e0acf3..00000000 --- a/frontend/src/api/models/GeneralUserConfig_Data.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralUserConfig_Data = { - /** - * 上次代理日期 - */ - LastProxyDate?: (string | null); - /** - * 代理次数 - */ - ProxyTimes?: (number | null); -}; - diff --git a/frontend/src/api/models/GeneralUserConfig_Info.ts b/frontend/src/api/models/GeneralUserConfig_Info.ts deleted file mode 100644 index fc49bc5d..00000000 --- a/frontend/src/api/models/GeneralUserConfig_Info.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralUserConfig_Info = { - /** - * 用户名 - */ - Name?: (string | null); - /** - * 用户状态 - */ - Status?: (boolean | null); - /** - * 剩余天数 - */ - RemainedDay?: (number | null); - /** - * 是否在任务前执行脚本 - */ - IfScriptBeforeTask?: (boolean | null); - /** - * 任务前脚本路径 - */ - ScriptBeforeTask?: (string | null); - /** - * 是否在任务后执行脚本 - */ - IfScriptAfterTask?: (boolean | null); - /** - * 任务后脚本路径 - */ - ScriptAfterTask?: (string | null); - /** - * 备注 - */ - Notes?: (string | null); - /** - * 用户标签列表(JSON字符串,TagItem的dict列表) - */ - Tag?: (string | null); -}; - diff --git a/frontend/src/api/models/GeneralUserConfig_Notify.ts b/frontend/src/api/models/GeneralUserConfig_Notify.ts deleted file mode 100644 index b3e401aa..00000000 --- a/frontend/src/api/models/GeneralUserConfig_Notify.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GeneralUserConfig_Notify = { - /** - * 是否启用通知 - */ - Enabled?: (boolean | null); - /** - * 是否发送统计信息 - */ - IfSendStatistic?: (boolean | null); - /** - * 是否发送邮件通知 - */ - IfSendMail?: (boolean | null); - /** - * 邮件接收地址 - */ - ToAddress?: (string | null); - /** - * 是否使用Server酱推送 - */ - IfServerChan?: (boolean | null); - /** - * ServerChanKey - */ - ServerChanKey?: (string | null); -}; - diff --git a/frontend/src/api/models/GetStageIn.ts b/frontend/src/api/models/GetStageIn.ts deleted file mode 100644 index 50a0f047..00000000 --- a/frontend/src/api/models/GetStageIn.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GetStageIn = { - /** - * 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项 - */ - type: GetStageIn.type; -}; -export namespace GetStageIn { - /** - * 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项 - */ - export enum type { - USER = 'User', - TODAY = 'Today', - ALL = 'ALL', - MONDAY = 'Monday', - TUESDAY = 'Tuesday', - WEDNESDAY = 'Wednesday', - THURSDAY = 'Thursday', - FRIDAY = 'Friday', - SATURDAY = 'Saturday', - SUNDAY = 'Sunday', - } -} - diff --git a/frontend/src/api/models/GlobalConfig.ts b/frontend/src/api/models/GlobalConfig.ts deleted file mode 100644 index 548f6003..00000000 --- a/frontend/src/api/models/GlobalConfig.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GlobalConfig_Function } from './GlobalConfig_Function'; -import type { GlobalConfig_Notify } from './GlobalConfig_Notify'; -import type { GlobalConfig_Start } from './GlobalConfig_Start'; -import type { GlobalConfig_UI } from './GlobalConfig_UI'; -import type { GlobalConfig_Update } from './GlobalConfig_Update'; -import type { GlobalConfig_Voice } from './GlobalConfig_Voice'; -export type GlobalConfig = { - /** - * 功能相关配置 - */ - Function?: (GlobalConfig_Function | null); - /** - * 语音相关配置 - */ - Voice?: (GlobalConfig_Voice | null); - /** - * 启动相关配置 - */ - Start?: (GlobalConfig_Start | null); - /** - * 界面相关配置 - */ - UI?: (GlobalConfig_UI | null); - /** - * 通知相关配置 - */ - Notify?: (GlobalConfig_Notify | null); - /** - * 更新相关配置 - */ - Update?: (GlobalConfig_Update | null); -}; - diff --git a/frontend/src/api/models/GlobalConfig_Function.ts b/frontend/src/api/models/GlobalConfig_Function.ts deleted file mode 100644 index 33dcfef8..00000000 --- a/frontend/src/api/models/GlobalConfig_Function.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GlobalConfig_Function = { - /** - * 历史记录保留时间, 0表示永久保存 - */ - HistoryRetentionTime?: (7 | 15 | 30 | 60 | 90 | 180 | 365 | 0 | null); - /** - * 允许休眠 - */ - IfAllowSleep?: (boolean | null); - /** - * 静默模式 - */ - IfSilence?: (boolean | null); - /** - * 同意哔哩哔哩用户协议 - */ - IfAgreeBilibili?: (boolean | null); - /** - * 屏蔽模拟器广告 - */ - IfBlockAd?: (boolean | null); -}; - diff --git a/frontend/src/api/models/GlobalConfig_Notify.ts b/frontend/src/api/models/GlobalConfig_Notify.ts deleted file mode 100644 index ed604a16..00000000 --- a/frontend/src/api/models/GlobalConfig_Notify.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GlobalConfig_Notify = { - /** - * 任务结果推送时机 - */ - SendTaskResultTime?: ('不推送' | '任何时刻' | '仅失败时' | null); - /** - * 是否发送统计信息 - */ - IfSendStatistic?: (boolean | null); - /** - * 是否发送公招六星通知 - */ - IfSendSixStar?: (boolean | null); - /** - * 是否推送系统通知 - */ - IfPushPlyer?: (boolean | null); - /** - * 是否发送邮件通知 - */ - IfSendMail?: (boolean | null); - /** - * 是否启用Koishi支持 - */ - IfKoishiSupport?: (boolean | null); - /** - * Koishi服务器地址 - */ - KoishiServerAddress?: (string | null); - /** - * Koishi Token - */ - KoishiToken?: (string | null); - /** - * SMTP服务器地址 - */ - SMTPServerAddress?: (string | null); - /** - * SMTP授权码 - */ - AuthorizationCode?: (string | null); - /** - * 邮件发送地址 - */ - FromAddress?: (string | null); - /** - * 邮件接收地址 - */ - ToAddress?: (string | null); - /** - * 是否使用ServerChan推送 - */ - IfServerChan?: (boolean | null); - /** - * ServerChan推送密钥 - */ - ServerChanKey?: (string | null); -}; - diff --git a/frontend/src/api/models/GlobalConfig_Start.ts b/frontend/src/api/models/GlobalConfig_Start.ts deleted file mode 100644 index 8c0a0ef6..00000000 --- a/frontend/src/api/models/GlobalConfig_Start.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GlobalConfig_Start = { - /** - * 是否在系统启动时自动运行 - */ - IfSelfStart?: (boolean | null); - /** - * 启动时是否直接最小化到托盘而不显示主窗口 - */ - IfMinimizeDirectly?: (boolean | null); -}; - diff --git a/frontend/src/api/models/GlobalConfig_UI.ts b/frontend/src/api/models/GlobalConfig_UI.ts deleted file mode 100644 index 05b32c42..00000000 --- a/frontend/src/api/models/GlobalConfig_UI.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GlobalConfig_UI = { - /** - * 是否常态显示托盘图标 - */ - IfShowTray?: (boolean | null); - /** - * 是否最小化到托盘 - */ - IfToTray?: (boolean | null); -}; - diff --git a/frontend/src/api/models/GlobalConfig_Update.ts b/frontend/src/api/models/GlobalConfig_Update.ts deleted file mode 100644 index bbf6b833..00000000 --- a/frontend/src/api/models/GlobalConfig_Update.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GlobalConfig_Update = { - /** - * 是否自动更新 - */ - IfAutoUpdate?: (boolean | null); - /** - * 更新源: GitHub源, Mirror酱源, 自建源 - */ - Source?: ('GitHub' | 'MirrorChyan' | 'AutoSite' | null); - /** - * 更新渠道: 稳定版, 测试版 - */ - Channel?: ('stable' | 'beta' | null); - /** - * 网络代理地址 - */ - ProxyAddress?: (string | null); - /** - * Mirror酱CDK - */ - MirrorChyanCDK?: (string | null); -}; - diff --git a/frontend/src/api/models/GlobalConfig_Voice.ts b/frontend/src/api/models/GlobalConfig_Voice.ts deleted file mode 100644 index 10bf3e3c..00000000 --- a/frontend/src/api/models/GlobalConfig_Voice.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type GlobalConfig_Voice = { - /** - * 语音功能是否启用 - */ - Enabled?: (boolean | null); - /** - * 语音类型, simple为简洁, noisy为聒噪 - */ - Type?: ('simple' | 'noisy' | null); -}; - diff --git a/frontend/src/api/models/HTTPValidationError.ts b/frontend/src/api/models/HTTPValidationError.ts deleted file mode 100644 index f9b1a79e..00000000 --- a/frontend/src/api/models/HTTPValidationError.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ValidationError } from './ValidationError'; -export type HTTPValidationError = { - detail?: Array; -}; - diff --git a/frontend/src/api/models/HistoryData.ts b/frontend/src/api/models/HistoryData.ts deleted file mode 100644 index 7c6430ce..00000000 --- a/frontend/src/api/models/HistoryData.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { HistoryIndexItem } from './HistoryIndexItem'; -export type HistoryData = { - /** - * 历史记录索引列表 - */ - index?: (Array | null); - /** - * 公招统计数据, key为星级, value为对应的公招数量 - */ - recruit_statistics?: (Record | null); - /** - * 掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } } - */ - drop_statistics?: (Record> | null); - /** - * 报错信息, key为时间戳, value为错误描述 - */ - error_info?: (Record | null); - /** - * 日志内容, 仅在提取单条历史记录数据时返回 - */ - log_content?: (string | null); -}; - diff --git a/frontend/src/api/models/HistoryDataGetIn.ts b/frontend/src/api/models/HistoryDataGetIn.ts deleted file mode 100644 index e5869089..00000000 --- a/frontend/src/api/models/HistoryDataGetIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type HistoryDataGetIn = { - /** - * 需要提取数据的历史记录JSON文件 - */ - jsonPath: string; -}; - diff --git a/frontend/src/api/models/HistoryDataGetOut.ts b/frontend/src/api/models/HistoryDataGetOut.ts deleted file mode 100644 index 35ade257..00000000 --- a/frontend/src/api/models/HistoryDataGetOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { HistoryData } from './HistoryData'; -export type HistoryDataGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 历史记录数据 - */ - data: HistoryData; -}; - diff --git a/frontend/src/api/models/HistoryIndexItem.ts b/frontend/src/api/models/HistoryIndexItem.ts deleted file mode 100644 index 9946a5e4..00000000 --- a/frontend/src/api/models/HistoryIndexItem.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type HistoryIndexItem = { - /** - * 日期 - */ - date: string; - /** - * 状态 - */ - status: HistoryIndexItem.status; - /** - * 对应JSON文件 - */ - jsonFile: string; -}; -export namespace HistoryIndexItem { - /** - * 状态 - */ - export enum status { - DONE = 'DONE', - ERROR = 'ERROR', - } -} - diff --git a/frontend/src/api/models/HistorySearchIn.ts b/frontend/src/api/models/HistorySearchIn.ts deleted file mode 100644 index e0df648c..00000000 --- a/frontend/src/api/models/HistorySearchIn.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type HistorySearchIn = { - /** - * 合并模式 - */ - mode: HistorySearchIn.mode; - /** - * 开始日期, 格式YYYY-MM-DD - */ - start_date: string; - /** - * 结束日期, 格式YYYY-MM-DD - */ - end_date: string; -}; -export namespace HistorySearchIn { - /** - * 合并模式 - */ - export enum mode { - DAILY = 'DAILY', - WEEKLY = 'WEEKLY', - MONTHLY = 'MONTHLY', - } -} - diff --git a/frontend/src/api/models/HistorySearchOut.ts b/frontend/src/api/models/HistorySearchOut.ts deleted file mode 100644 index 4665af5f..00000000 --- a/frontend/src/api/models/HistorySearchOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { HistoryData } from './HistoryData'; -export type HistorySearchOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } } - */ - data: Record>; -}; - diff --git a/frontend/src/api/models/InfoOut.ts b/frontend/src/api/models/InfoOut.ts deleted file mode 100644 index ddaa5bf0..00000000 --- a/frontend/src/api/models/InfoOut.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type InfoOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 收到的服务器数据 - */ - data: Record; -}; - diff --git a/frontend/src/api/models/MaaConfig.ts b/frontend/src/api/models/MaaConfig.ts deleted file mode 100644 index 8dc37d6d..00000000 --- a/frontend/src/api/models/MaaConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaConfig_Emulator } from './MaaConfig_Emulator'; -import type { MaaConfig_Info } from './MaaConfig_Info'; -import type { MaaConfig_Run } from './MaaConfig_Run'; -export type MaaConfig = { - /** - * 脚本基础信息 - */ - Info?: (MaaConfig_Info | null); - /** - * 模拟器配置 - */ - Emulator?: (MaaConfig_Emulator | null); - /** - * 脚本运行配置 - */ - Run?: (MaaConfig_Run | null); -}; - diff --git a/frontend/src/api/models/MaaConfig_Emulator.ts b/frontend/src/api/models/MaaConfig_Emulator.ts deleted file mode 100644 index 96c1bd23..00000000 --- a/frontend/src/api/models/MaaConfig_Emulator.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaConfig_Emulator = { - /** - * 模拟器ID - */ - Id?: (string | null); - /** - * 模拟器多开实例索引 - */ - Index?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaConfig_Info.ts b/frontend/src/api/models/MaaConfig_Info.ts deleted file mode 100644 index 534eb5ae..00000000 --- a/frontend/src/api/models/MaaConfig_Info.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaConfig_Info = { - /** - * 脚本名称 - */ - Name?: (string | null); - /** - * 脚本路径 - */ - Path?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaConfig_Run.ts b/frontend/src/api/models/MaaConfig_Run.ts deleted file mode 100644 index 1f7e9b64..00000000 --- a/frontend/src/api/models/MaaConfig_Run.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaConfig_Run = { - /** - * 简洁任务间切换方式 - */ - TaskTransitionMethod?: ('NoAction' | 'ExitGame' | 'ExitEmulator' | null); - /** - * 每日代理次数限制 - */ - ProxyTimesLimit?: (number | null); - /** - * 重试次数限制 - */ - RunTimesLimit?: (number | null); - /** - * 剿灭超时限制 - */ - AnnihilationTimeLimit?: (number | null); - /** - * 日常超时限制 - */ - RoutineTimeLimit?: (number | null); - /** - * 剿灭避免无代理卡浪费理智 - */ - AnnihilationAvoidWaste?: (boolean | null); -}; - diff --git a/frontend/src/api/models/MaaEndConfig.ts b/frontend/src/api/models/MaaEndConfig.ts deleted file mode 100644 index 8e157fe9..00000000 --- a/frontend/src/api/models/MaaEndConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaEndConfig_Game } from './MaaEndConfig_Game'; -import type { MaaEndConfig_Info } from './MaaEndConfig_Info'; -import type { MaaEndConfig_Run } from './MaaEndConfig_Run'; -export type MaaEndConfig = { - /** - * 脚本信息 - */ - Info?: (MaaEndConfig_Info | null); - /** - * 运行配置 - */ - Run?: (MaaEndConfig_Run | null); - /** - * 游戏配置 - */ - Game?: (MaaEndConfig_Game | null); -}; - diff --git a/frontend/src/api/models/MaaEndConfig_Game.ts b/frontend/src/api/models/MaaEndConfig_Game.ts deleted file mode 100644 index 620701d2..00000000 --- a/frontend/src/api/models/MaaEndConfig_Game.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaEndConfig_Game = { - /** - * 控制器类型 - */ - ControllerType?: ('Win32-Window' | 'Win32-Window-Background' | 'Win32-Front' | 'ADB' | null); - /** - * 终末地客户端路径 - */ - Path?: (string | null); - /** - * 游戏启动参数 - */ - Arguments?: (string | null); - /** - * 游戏等待时间 - */ - WaitTime?: (number | null); - /** - * 模拟器ID - */ - EmulatorId?: (string | null); - /** - * 模拟器索引 - */ - EmulatorIndex?: (string | null); - /** - * 结束后关闭游戏 - */ - CloseOnFinish?: (boolean | null); -}; - diff --git a/frontend/src/api/models/MaaEndConfig_Info.ts b/frontend/src/api/models/MaaEndConfig_Info.ts deleted file mode 100644 index 0415cfe7..00000000 --- a/frontend/src/api/models/MaaEndConfig_Info.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaEndConfig_Info = { - /** - * 脚本名称 - */ - Name?: (string | null); - /** - * 脚本路径 - */ - Path?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaEndConfig_Run.ts b/frontend/src/api/models/MaaEndConfig_Run.ts deleted file mode 100644 index 94beebbb..00000000 --- a/frontend/src/api/models/MaaEndConfig_Run.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaEndConfig_Run = { - /** - * 运行时间限制(分钟) - */ - RunTimeLimit?: (number | null); - /** - * 每日代理次数限制 - */ - ProxyTimesLimit?: (number | null); - /** - * 重试次数限制 - */ - RunTimesLimit?: (number | null); -}; - diff --git a/frontend/src/api/models/MaaEndUserConfig.ts b/frontend/src/api/models/MaaEndUserConfig.ts deleted file mode 100644 index 9c854ca6..00000000 --- a/frontend/src/api/models/MaaEndUserConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaEndUserConfig_Info } from './MaaEndUserConfig_Info'; -import type { MaaEndUserConfig_Notify } from './MaaEndUserConfig_Notify'; -import type { MaaEndUserConfig_Task } from './MaaEndUserConfig_Task'; -export type MaaEndUserConfig = { - /** - * 用户信息 - */ - Info?: (MaaEndUserConfig_Info | null); - /** - * 任务配置 - */ - Task?: (MaaEndUserConfig_Task | null); - /** - * 通知配置 - */ - Notify?: (MaaEndUserConfig_Notify | null); -}; - diff --git a/frontend/src/api/models/MaaEndUserConfig_Info.ts b/frontend/src/api/models/MaaEndUserConfig_Info.ts deleted file mode 100644 index a0eb9f38..00000000 --- a/frontend/src/api/models/MaaEndUserConfig_Info.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaEndUserConfig_Info = { - /** - * 用户名 - */ - Name?: (string | null); - /** - * 用户状态 - */ - Status?: (boolean | null); - /** - * 用户ID - */ - Id?: (string | null); - /** - * 密码 - */ - Password?: (string | null); - /** - * 配置模式 - */ - Mode?: ('简洁' | '详细' | null); - /** - * 资源名称 - */ - Resource?: (string | null); - /** - * 剩余天数 - */ - RemainedDay?: (number | null); - /** - * 备注 - */ - Notes?: (string | null); - /** - * 用户标签信息 - */ - Tag?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaEndUserConfig_Notify.ts b/frontend/src/api/models/MaaEndUserConfig_Notify.ts deleted file mode 100644 index 8635f1e8..00000000 --- a/frontend/src/api/models/MaaEndUserConfig_Notify.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaEndUserConfig_Notify = { - /** - * 是否启用通知 - */ - Enabled?: (boolean | null); - /** - * 是否发送统计信息 - */ - IfSendStatistic?: (boolean | null); - /** - * 是否发送邮件 - */ - IfSendMail?: (boolean | null); - /** - * 收件地址 - */ - ToAddress?: (string | null); - /** - * 是否启用Server酱 - */ - IfServerChan?: (boolean | null); - /** - * Server酱密钥 - */ - ServerChanKey?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaEndUserConfig_Task.ts b/frontend/src/api/models/MaaEndUserConfig_Task.ts deleted file mode 100644 index 49a31905..00000000 --- a/frontend/src/api/models/MaaEndUserConfig_Task.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaEndUserConfig_Task = { - /** - * 协议空间选项卡 - */ - ProtocolSpaceTab?: ('OperatorProgression' | 'WeaponProgression' | 'CrisisDrills' | null); - /** - * 干员养成任务 - */ - OperatorProgression?: ('OperatorEXP' | 'Promotions' | 'T-Creds' | 'SkillUp' | null); - /** - * 武器养成任务 - */ - WeaponProgression?: ('WeaponEXP' | 'WeaponTune' | null); - /** - * 危境预演任务 - */ - CrisisDrills?: ('AdvancedProgression1' | 'AdvancedProgression2' | 'AdvancedProgression3' | 'AdvancedProgression4' | 'AdvancedProgression5' | null); - /** - * 奖励套组选项 - */ - RewardsSetOption?: ('RewardsSetA' | 'RewardsSetB' | null); -}; - diff --git a/frontend/src/api/models/MaaPlanConfig.ts b/frontend/src/api/models/MaaPlanConfig.ts deleted file mode 100644 index 80227cd6..00000000 --- a/frontend/src/api/models/MaaPlanConfig.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaPlanConfig_Info } from './MaaPlanConfig_Info'; -import type { MaaPlanConfig_Item } from './MaaPlanConfig_Item'; -export type MaaPlanConfig = { - /** - * 基础信息 - */ - Info?: (MaaPlanConfig_Info | null); - /** - * 全局 - */ - ALL?: (MaaPlanConfig_Item | null); - /** - * 周一 - */ - Monday?: (MaaPlanConfig_Item | null); - /** - * 周二 - */ - Tuesday?: (MaaPlanConfig_Item | null); - /** - * 周三 - */ - Wednesday?: (MaaPlanConfig_Item | null); - /** - * 周四 - */ - Thursday?: (MaaPlanConfig_Item | null); - /** - * 周五 - */ - Friday?: (MaaPlanConfig_Item | null); - /** - * 周六 - */ - Saturday?: (MaaPlanConfig_Item | null); - /** - * 周日 - */ - Sunday?: (MaaPlanConfig_Item | null); -}; - diff --git a/frontend/src/api/models/MaaPlanConfig_Info.ts b/frontend/src/api/models/MaaPlanConfig_Info.ts deleted file mode 100644 index cea0432b..00000000 --- a/frontend/src/api/models/MaaPlanConfig_Info.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaPlanConfig_Info = { - /** - * 计划表名称 - */ - Name?: (string | null); - /** - * 计划表模式 - */ - Mode?: ('ALL' | 'Weekly' | null); -}; - diff --git a/frontend/src/api/models/MaaPlanConfig_Item.ts b/frontend/src/api/models/MaaPlanConfig_Item.ts deleted file mode 100644 index 367f15ee..00000000 --- a/frontend/src/api/models/MaaPlanConfig_Item.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaPlanConfig_Item = { - /** - * 吃理智药 - */ - MedicineNumb?: (number | null); - /** - * 连战次数 - */ - SeriesNumb?: ('0' | '6' | '5' | '4' | '3' | '2' | '1' | '-1' | null); - /** - * 关卡选择 - */ - Stage?: (string | null); - /** - * 备选关卡 - 1 - */ - Stage_1?: (string | null); - /** - * 备选关卡 - 2 - */ - Stage_2?: (string | null); - /** - * 备选关卡 - 3 - */ - Stage_3?: (string | null); - /** - * 剩余理智关卡 - */ - Stage_Remain?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaUserConfig.ts b/frontend/src/api/models/MaaUserConfig.ts deleted file mode 100644 index 16e248c1..00000000 --- a/frontend/src/api/models/MaaUserConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaUserConfig_Data } from './MaaUserConfig_Data'; -import type { MaaUserConfig_Info } from './MaaUserConfig_Info'; -import type { MaaUserConfig_Notify } from './MaaUserConfig_Notify'; -import type { MaaUserConfig_Task } from './MaaUserConfig_Task'; -export type MaaUserConfig = { - /** - * 基础信息 - */ - Info?: (MaaUserConfig_Info | null); - /** - * 用户数据 - */ - Data?: (MaaUserConfig_Data | null); - /** - * 任务列表 - */ - Task?: (MaaUserConfig_Task | null); - /** - * 单独通知 - */ - Notify?: (MaaUserConfig_Notify | null); -}; - diff --git a/frontend/src/api/models/MaaUserConfig_Data.ts b/frontend/src/api/models/MaaUserConfig_Data.ts deleted file mode 100644 index 2af50423..00000000 --- a/frontend/src/api/models/MaaUserConfig_Data.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaUserConfig_Data = { - /** - * 是否通过人工排查 - */ - IfPassCheck?: (boolean | null); -}; - diff --git a/frontend/src/api/models/MaaUserConfig_Info.ts b/frontend/src/api/models/MaaUserConfig_Info.ts deleted file mode 100644 index 75bde533..00000000 --- a/frontend/src/api/models/MaaUserConfig_Info.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaUserConfig_Info = { - /** - * 用户名 - */ - Name?: (string | null); - /** - * 用户ID - */ - Id?: (string | null); - /** - * 用户配置模式 - */ - Mode?: ('简洁' | '详细' | null); - /** - * 关卡配置模式 - */ - StageMode?: (string | null); - /** - * 服务器 - */ - Server?: ('Official' | 'Bilibili' | 'YoStarEN' | 'YoStarJP' | 'YoStarKR' | 'txwy' | null); - /** - * 用户状态 - */ - Status?: (boolean | null); - /** - * 剩余天数 - */ - RemainedDay?: (number | null); - /** - * 剿灭模式 - */ - Annihilation?: ('Close' | 'Annihilation' | 'Chernobog@Annihilation' | 'LungmenOutskirts@Annihilation' | 'LungmenDowntown@Annihilation' | null); - /** - * 基建模式 - */ - InfrastMode?: ('Normal' | 'Rotation' | 'Custom' | null); - /** - * 基建方案名称 - */ - InfrastName?: (string | null); - /** - * 基建方案索引 - */ - InfrastIndex?: (string | null); - /** - * 密码 - */ - Password?: (string | null); - /** - * 备注 - */ - Notes?: (string | null); - /** - * 吃理智药数量 - */ - MedicineNumb?: (number | null); - /** - * 连战次数 - */ - SeriesNumb?: ('0' | '6' | '5' | '4' | '3' | '2' | '1' | '-1' | null); - /** - * 关卡选择 - */ - Stage?: (string | null); - /** - * 备选关卡 - 1 - */ - Stage_1?: (string | null); - /** - * 备选关卡 - 2 - */ - Stage_2?: (string | null); - /** - * 备选关卡 - 3 - */ - Stage_3?: (string | null); - /** - * 剩余理智关卡 - */ - Stage_Remain?: (string | null); - /** - * 是否启用森空岛签到 - */ - IfSkland?: (boolean | null); - /** - * SklandToken - */ - SklandToken?: (string | null); - /** - * 状态标签列表 - */ - Tag?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaUserConfig_Notify.ts b/frontend/src/api/models/MaaUserConfig_Notify.ts deleted file mode 100644 index 48ea52a0..00000000 --- a/frontend/src/api/models/MaaUserConfig_Notify.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaUserConfig_Notify = { - /** - * 是否启用通知 - */ - Enabled?: (boolean | null); - /** - * 是否发送统计信息 - */ - IfSendStatistic?: (boolean | null); - /** - * 是否发送高资喜报 - */ - IfSendSixStar?: (boolean | null); - /** - * 是否发送邮件通知 - */ - IfSendMail?: (boolean | null); - /** - * 邮件接收地址 - */ - ToAddress?: (string | null); - /** - * 是否使用Server酱推送 - */ - IfServerChan?: (boolean | null); - /** - * ServerChanKey - */ - ServerChanKey?: (string | null); -}; - diff --git a/frontend/src/api/models/MaaUserConfig_Task.ts b/frontend/src/api/models/MaaUserConfig_Task.ts deleted file mode 100644 index 08b7b9dc..00000000 --- a/frontend/src/api/models/MaaUserConfig_Task.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type MaaUserConfig_Task = { - /** - * 开始唤醒 - */ - IfStartUp?: (boolean | null); - /** - * 自动公招 - */ - IfRecruit?: (boolean | null); - /** - * 基建换班 - */ - IfInfrast?: (boolean | null); - /** - * 理智作战 - */ - IfFight?: (boolean | null); - /** - * 信用收支 - */ - IfMall?: (boolean | null); - /** - * 领取奖励 - */ - IfAward?: (boolean | null); - /** - * 自动肉鸽 - */ - IfRoguelike?: (boolean | null); - /** - * 生息演算 - */ - IfReclamation?: (boolean | null); -}; - diff --git a/frontend/src/api/models/NoticeOut.ts b/frontend/src/api/models/NoticeOut.ts deleted file mode 100644 index 5dd6e8f0..00000000 --- a/frontend/src/api/models/NoticeOut.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type NoticeOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 是否需要显示公告 - */ - if_need_show: boolean; - /** - * 公告信息, key为公告标题, value为公告内容 - */ - data: Record; -}; - diff --git a/frontend/src/api/models/OCRScreenshotIn.ts b/frontend/src/api/models/OCRScreenshotIn.ts deleted file mode 100644 index 6a97d5ec..00000000 --- a/frontend/src/api/models/OCRScreenshotIn.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type OCRScreenshotIn = { - /** - * 窗口标题(用于查找窗口) - */ - window_title: string; - /** - * 是否预处理图片区域,True时排除边框和标题栏,False时使用完整窗口 - */ - should_preprocess?: boolean; - /** - * 宽高比宽度 - */ - aspect_ratio_width?: number; - /** - * 宽高比高度 - */ - aspect_ratio_height?: number; - /** - * 自定义截图区域 (left, top, width, height) - */ - region?: (any[] | null); -}; - diff --git a/frontend/src/api/models/OCRScreenshotOut.ts b/frontend/src/api/models/OCRScreenshotOut.ts deleted file mode 100644 index 15920932..00000000 --- a/frontend/src/api/models/OCRScreenshotOut.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type OCRScreenshotOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 截图的Base64编码(PNG格式) - */ - image_base64: string; - /** - * 实际使用的截图区域 (left, top, width, height) - */ - region: any[]; - /** - * 截图宽度 - */ - image_width: number; - /** - * 截图高度 - */ - image_height: number; -}; - diff --git a/frontend/src/api/models/OutBase.ts b/frontend/src/api/models/OutBase.ts deleted file mode 100644 index 7fd4772c..00000000 --- a/frontend/src/api/models/OutBase.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type OutBase = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; -}; - diff --git a/frontend/src/api/models/PlanCreateIn.ts b/frontend/src/api/models/PlanCreateIn.ts deleted file mode 100644 index 4ef99957..00000000 --- a/frontend/src/api/models/PlanCreateIn.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PlanCreateIn = { - type: string; -}; - diff --git a/frontend/src/api/models/PlanCreateOut.ts b/frontend/src/api/models/PlanCreateOut.ts deleted file mode 100644 index 5cb909d9..00000000 --- a/frontend/src/api/models/PlanCreateOut.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaPlanConfig } from './MaaPlanConfig'; -export type PlanCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的计划ID - */ - planId: string; - /** - * 计划配置数据 - */ - data: MaaPlanConfig; -}; - diff --git a/frontend/src/api/models/PlanDeleteIn.ts b/frontend/src/api/models/PlanDeleteIn.ts deleted file mode 100644 index 75fc54c1..00000000 --- a/frontend/src/api/models/PlanDeleteIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PlanDeleteIn = { - /** - * 计划ID - */ - planId: string; -}; - diff --git a/frontend/src/api/models/PlanGetIn.ts b/frontend/src/api/models/PlanGetIn.ts deleted file mode 100644 index 5560368d..00000000 --- a/frontend/src/api/models/PlanGetIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PlanGetIn = { - /** - * 计划ID, 未携带时表示获取所有计划数据 - */ - planId?: (string | null); -}; - diff --git a/frontend/src/api/models/PlanGetOut.ts b/frontend/src/api/models/PlanGetOut.ts deleted file mode 100644 index affc9c77..00000000 --- a/frontend/src/api/models/PlanGetOut.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaPlanConfig } from './MaaPlanConfig'; -import type { PlanIndexItem } from './PlanIndexItem'; -export type PlanGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 计划索引列表 - */ - index: Array; - /** - * 计划列表或单个计划数据 - */ - data: Record; -}; - diff --git a/frontend/src/api/models/PlanIndexItem.ts b/frontend/src/api/models/PlanIndexItem.ts deleted file mode 100644 index 8580db8d..00000000 --- a/frontend/src/api/models/PlanIndexItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PlanIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: string; -}; - diff --git a/frontend/src/api/models/PlanReorderIn.ts b/frontend/src/api/models/PlanReorderIn.ts deleted file mode 100644 index ea5ec844..00000000 --- a/frontend/src/api/models/PlanReorderIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PlanReorderIn = { - /** - * 计划ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/PlanUpdateIn.ts b/frontend/src/api/models/PlanUpdateIn.ts deleted file mode 100644 index a4e5e627..00000000 --- a/frontend/src/api/models/PlanUpdateIn.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { MaaPlanConfig } from './MaaPlanConfig'; -export type PlanUpdateIn = { - /** - * 计划ID - */ - planId: string; - /** - * 计划更新数据 - */ - data: MaaPlanConfig; -}; - diff --git a/frontend/src/api/models/PowerIn.ts b/frontend/src/api/models/PowerIn.ts deleted file mode 100644 index 3a281000..00000000 --- a/frontend/src/api/models/PowerIn.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PowerIn = { - /** - * 电源操作信号 - */ - signal: PowerIn.signal; -}; -export namespace PowerIn { - /** - * 电源操作信号 - */ - export enum signal { - NO_ACTION = 'NoAction', - SHUTDOWN = 'Shutdown', - SHUTDOWN_FORCE = 'ShutdownForce', - REBOOT = 'Reboot', - HIBERNATE = 'Hibernate', - SLEEP = 'Sleep', - KILL_SELF = 'KillSelf', - } -} - diff --git a/frontend/src/api/models/PowerOut.ts b/frontend/src/api/models/PowerOut.ts deleted file mode 100644 index 1d7a7def..00000000 --- a/frontend/src/api/models/PowerOut.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type PowerOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 电源操作信号 - */ - signal: PowerOut.signal; -}; -export namespace PowerOut { - /** - * 电源操作信号 - */ - export enum signal { - NO_ACTION = 'NoAction', - SHUTDOWN = 'Shutdown', - SHUTDOWN_FORCE = 'ShutdownForce', - REBOOT = 'Reboot', - HIBERNATE = 'Hibernate', - SLEEP = 'Sleep', - KILL_SELF = 'KillSelf', - } -} - diff --git a/frontend/src/api/models/QueueConfig.ts b/frontend/src/api/models/QueueConfig.ts deleted file mode 100644 index 66467b91..00000000 --- a/frontend/src/api/models/QueueConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueConfig_Info } from './QueueConfig_Info'; -export type QueueConfig = { - /** - * 队列信息 - */ - Info?: (QueueConfig_Info | null); -}; - diff --git a/frontend/src/api/models/QueueConfig_Info.ts b/frontend/src/api/models/QueueConfig_Info.ts deleted file mode 100644 index 7e362a07..00000000 --- a/frontend/src/api/models/QueueConfig_Info.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueConfig_Info = { - /** - * 队列名称 - */ - Name?: (string | null); - /** - * 是否启用定时 - */ - TimeEnabled?: (boolean | null); - /** - * 是否启动时运行 - */ - StartUpEnabled?: (boolean | null); - /** - * 完成后操作 - */ - AfterAccomplish?: ('NoAction' | 'Shutdown' | 'ShutdownForce' | 'Reboot' | 'Hibernate' | 'Sleep' | 'KillSelf' | null); -}; - diff --git a/frontend/src/api/models/QueueCreateOut.ts b/frontend/src/api/models/QueueCreateOut.ts deleted file mode 100644 index da6bb394..00000000 --- a/frontend/src/api/models/QueueCreateOut.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueConfig } from './QueueConfig'; -export type QueueCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的队列ID - */ - queueId: string; - /** - * 队列配置数据 - */ - data: QueueConfig; -}; - diff --git a/frontend/src/api/models/QueueDeleteIn.ts b/frontend/src/api/models/QueueDeleteIn.ts deleted file mode 100644 index 7bc4b303..00000000 --- a/frontend/src/api/models/QueueDeleteIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueDeleteIn = { - /** - * 队列ID - */ - queueId: string; -}; - diff --git a/frontend/src/api/models/QueueGetIn.ts b/frontend/src/api/models/QueueGetIn.ts deleted file mode 100644 index d1692604..00000000 --- a/frontend/src/api/models/QueueGetIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueGetIn = { - /** - * 队列ID, 未携带时表示获取所有队列数据 - */ - queueId?: (string | null); -}; - diff --git a/frontend/src/api/models/QueueGetOut.ts b/frontend/src/api/models/QueueGetOut.ts deleted file mode 100644 index 636b9920..00000000 --- a/frontend/src/api/models/QueueGetOut.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueConfig } from './QueueConfig'; -import type { QueueIndexItem } from './QueueIndexItem'; -export type QueueGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 队列索引列表 - */ - index: Array; - /** - * 队列数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/QueueIndexItem.ts b/frontend/src/api/models/QueueIndexItem.ts deleted file mode 100644 index 61ca6796..00000000 --- a/frontend/src/api/models/QueueIndexItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: string; -}; - diff --git a/frontend/src/api/models/QueueItem.ts b/frontend/src/api/models/QueueItem.ts deleted file mode 100644 index 58fe39b0..00000000 --- a/frontend/src/api/models/QueueItem.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueItem_Info } from './QueueItem_Info'; -export type QueueItem = { - /** - * 队列项 - */ - Info?: (QueueItem_Info | null); -}; - diff --git a/frontend/src/api/models/QueueItemCreateOut.ts b/frontend/src/api/models/QueueItemCreateOut.ts deleted file mode 100644 index 983d675b..00000000 --- a/frontend/src/api/models/QueueItemCreateOut.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueItem } from './QueueItem'; -export type QueueItemCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的队列项ID - */ - queueItemId: string; - /** - * 队列项配置数据 - */ - data: QueueItem; -}; - diff --git a/frontend/src/api/models/QueueItemDeleteIn.ts b/frontend/src/api/models/QueueItemDeleteIn.ts deleted file mode 100644 index c7a454b3..00000000 --- a/frontend/src/api/models/QueueItemDeleteIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueItemDeleteIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 队列项ID - */ - queueItemId: string; -}; - diff --git a/frontend/src/api/models/QueueItemGetIn.ts b/frontend/src/api/models/QueueItemGetIn.ts deleted file mode 100644 index 25b99c82..00000000 --- a/frontend/src/api/models/QueueItemGetIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueItemGetIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 队列项ID, 未携带时表示获取所有队列项数据 - */ - queueItemId?: (string | null); -}; - diff --git a/frontend/src/api/models/QueueItemGetOut.ts b/frontend/src/api/models/QueueItemGetOut.ts deleted file mode 100644 index 6ea96d97..00000000 --- a/frontend/src/api/models/QueueItemGetOut.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueItem } from './QueueItem'; -import type { QueueItemIndexItem } from './QueueItemIndexItem'; -export type QueueItemGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 队列项索引列表 - */ - index: Array; - /** - * 队列项数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/QueueItemIndexItem.ts b/frontend/src/api/models/QueueItemIndexItem.ts deleted file mode 100644 index 63c2368c..00000000 --- a/frontend/src/api/models/QueueItemIndexItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueItemIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: string; -}; - diff --git a/frontend/src/api/models/QueueItemReorderIn.ts b/frontend/src/api/models/QueueItemReorderIn.ts deleted file mode 100644 index b4f79b8c..00000000 --- a/frontend/src/api/models/QueueItemReorderIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueItemReorderIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 队列项ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/QueueItemUpdateIn.ts b/frontend/src/api/models/QueueItemUpdateIn.ts deleted file mode 100644 index d5da2a5c..00000000 --- a/frontend/src/api/models/QueueItemUpdateIn.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueItem } from './QueueItem'; -export type QueueItemUpdateIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 队列项ID - */ - queueItemId: string; - /** - * 队列项更新数据 - */ - data: QueueItem; -}; - diff --git a/frontend/src/api/models/QueueItem_Info.ts b/frontend/src/api/models/QueueItem_Info.ts deleted file mode 100644 index 0e86ee63..00000000 --- a/frontend/src/api/models/QueueItem_Info.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueItem_Info = { - /** - * 任务所对应的脚本ID, 为None时表示未选择 - */ - ScriptId?: (string | null); -}; - diff --git a/frontend/src/api/models/QueueReorderIn.ts b/frontend/src/api/models/QueueReorderIn.ts deleted file mode 100644 index 9768fce0..00000000 --- a/frontend/src/api/models/QueueReorderIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueReorderIn = { - /** - * 按新顺序排列的调度队列UID列表 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/QueueSetInBase.ts b/frontend/src/api/models/QueueSetInBase.ts deleted file mode 100644 index d431c60b..00000000 --- a/frontend/src/api/models/QueueSetInBase.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type QueueSetInBase = { - /** - * 所属队列ID - */ - queueId: string; -}; - diff --git a/frontend/src/api/models/QueueUpdateIn.ts b/frontend/src/api/models/QueueUpdateIn.ts deleted file mode 100644 index 53bf019b..00000000 --- a/frontend/src/api/models/QueueUpdateIn.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { QueueConfig } from './QueueConfig'; -export type QueueUpdateIn = { - /** - * 队列ID - */ - queueId: string; - /** - * 队列更新数据 - */ - data: QueueConfig; -}; - diff --git a/frontend/src/api/models/ScriptCreateIn.ts b/frontend/src/api/models/ScriptCreateIn.ts deleted file mode 100644 index 45e66e7a..00000000 --- a/frontend/src/api/models/ScriptCreateIn.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptCreateIn = { - /** - * 脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本 - */ - type: ScriptCreateIn.type; - /** - * 直接从该脚本ID复制创建, 仅在复制创建时使用 - */ - scriptId?: (string | null); -}; -export namespace ScriptCreateIn { - /** - * 脚本类型: MAA脚本, 通用脚本, SRC脚本, MaaEnd脚本 - */ - export enum type { - MAA = 'MAA', - SRC = 'SRC', - GENERAL = 'General', - MAA_END = 'MaaEnd', - } -} - diff --git a/frontend/src/api/models/ScriptCreateOut.ts b/frontend/src/api/models/ScriptCreateOut.ts deleted file mode 100644 index 845b9ac0..00000000 --- a/frontend/src/api/models/ScriptCreateOut.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralConfig } from './GeneralConfig'; -import type { MaaConfig } from './MaaConfig'; -import type { MaaEndConfig } from './MaaEndConfig'; -import type { SrcConfig } from './SrcConfig'; -export type ScriptCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的脚本ID - */ - scriptId: string; - /** - * 脚本配置数据 - */ - data: (MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig); -}; - diff --git a/frontend/src/api/models/ScriptDeleteIn.ts b/frontend/src/api/models/ScriptDeleteIn.ts deleted file mode 100644 index 21b3e4bb..00000000 --- a/frontend/src/api/models/ScriptDeleteIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptDeleteIn = { - /** - * 脚本ID - */ - scriptId: string; -}; - diff --git a/frontend/src/api/models/ScriptFileIn.ts b/frontend/src/api/models/ScriptFileIn.ts deleted file mode 100644 index 296ad4c0..00000000 --- a/frontend/src/api/models/ScriptFileIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptFileIn = { - /** - * 脚本ID - */ - scriptId: string; - /** - * 配置文件路径 - */ - jsonFile: string; -}; - diff --git a/frontend/src/api/models/ScriptGetIn.ts b/frontend/src/api/models/ScriptGetIn.ts deleted file mode 100644 index 55e1f8cd..00000000 --- a/frontend/src/api/models/ScriptGetIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptGetIn = { - /** - * 脚本ID, 未携带时表示获取所有脚本数据 - */ - scriptId?: (string | null); -}; - diff --git a/frontend/src/api/models/ScriptGetOut.ts b/frontend/src/api/models/ScriptGetOut.ts deleted file mode 100644 index 8f9bbcb2..00000000 --- a/frontend/src/api/models/ScriptGetOut.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralConfig } from './GeneralConfig'; -import type { MaaConfig } from './MaaConfig'; -import type { MaaEndConfig } from './MaaEndConfig'; -import type { ScriptIndexItem } from './ScriptIndexItem'; -import type { SrcConfig } from './SrcConfig'; -export type ScriptGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 脚本索引列表 - */ - index: Array; - /** - * 脚本数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/ScriptIndexItem.ts b/frontend/src/api/models/ScriptIndexItem.ts deleted file mode 100644 index 967f5827..00000000 --- a/frontend/src/api/models/ScriptIndexItem.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: ScriptIndexItem.type; -}; -export namespace ScriptIndexItem { - /** - * 配置类型 - */ - export enum type { - MAA_CONFIG = 'MaaConfig', - GENERAL_CONFIG = 'GeneralConfig', - SRC_CONFIG = 'SrcConfig', - MAA_END_CONFIG = 'MaaEndConfig', - } -} - diff --git a/frontend/src/api/models/ScriptReorderIn.ts b/frontend/src/api/models/ScriptReorderIn.ts deleted file mode 100644 index 92769b84..00000000 --- a/frontend/src/api/models/ScriptReorderIn.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptReorderIn = { - /** - * 脚本ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/ScriptUpdateIn.ts b/frontend/src/api/models/ScriptUpdateIn.ts deleted file mode 100644 index 08a82c05..00000000 --- a/frontend/src/api/models/ScriptUpdateIn.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralConfig } from './GeneralConfig'; -import type { MaaConfig } from './MaaConfig'; -import type { MaaEndConfig } from './MaaEndConfig'; -import type { SrcConfig } from './SrcConfig'; -export type ScriptUpdateIn = { - /** - * 脚本ID - */ - scriptId: string; - /** - * 脚本更新数据 - */ - data: (MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig); -}; - diff --git a/frontend/src/api/models/ScriptUploadIn.ts b/frontend/src/api/models/ScriptUploadIn.ts deleted file mode 100644 index eb28bdb4..00000000 --- a/frontend/src/api/models/ScriptUploadIn.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptUploadIn = { - /** - * 脚本ID - */ - scriptId: string; - /** - * 配置名称 - */ - config_name: string; - /** - * 作者 - */ - author: string; - /** - * 描述 - */ - description: string; -}; - diff --git a/frontend/src/api/models/ScriptUrlIn.ts b/frontend/src/api/models/ScriptUrlIn.ts deleted file mode 100644 index 2d98450d..00000000 --- a/frontend/src/api/models/ScriptUrlIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ScriptUrlIn = { - /** - * 脚本ID - */ - scriptId: string; - /** - * 配置文件URL - */ - url: string; -}; - diff --git a/frontend/src/api/models/SettingGetOut.ts b/frontend/src/api/models/SettingGetOut.ts deleted file mode 100644 index e909ef69..00000000 --- a/frontend/src/api/models/SettingGetOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GlobalConfig } from './GlobalConfig'; -export type SettingGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 全局设置数据 - */ - data: GlobalConfig; -}; - diff --git a/frontend/src/api/models/SettingUpdateIn.ts b/frontend/src/api/models/SettingUpdateIn.ts deleted file mode 100644 index 51e89914..00000000 --- a/frontend/src/api/models/SettingUpdateIn.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GlobalConfig } from './GlobalConfig'; -export type SettingUpdateIn = { - /** - * 全局设置需要更新的数据 - */ - data: GlobalConfig; -}; - diff --git a/frontend/src/api/models/SrcConfig.ts b/frontend/src/api/models/SrcConfig.ts deleted file mode 100644 index 5d42b3fc..00000000 --- a/frontend/src/api/models/SrcConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { SrcConfig_Emulator } from './SrcConfig_Emulator'; -import type { SrcConfig_Info } from './SrcConfig_Info'; -import type { SrcConfig_Run } from './SrcConfig_Run'; -export type SrcConfig = { - /** - * 脚本基础信息 - */ - Info?: (SrcConfig_Info | null); - /** - * 模拟器配置 - */ - Emulator?: (SrcConfig_Emulator | null); - /** - * 脚本运行配置 - */ - Run?: (SrcConfig_Run | null); -}; - diff --git a/frontend/src/api/models/SrcConfig_Emulator.ts b/frontend/src/api/models/SrcConfig_Emulator.ts deleted file mode 100644 index af4f488f..00000000 --- a/frontend/src/api/models/SrcConfig_Emulator.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcConfig_Emulator = { - /** - * 模拟器ID - */ - Id?: (string | null); - /** - * 模拟器索引 - */ - Index?: (string | null); -}; - diff --git a/frontend/src/api/models/SrcConfig_Info.ts b/frontend/src/api/models/SrcConfig_Info.ts deleted file mode 100644 index 19eac40a..00000000 --- a/frontend/src/api/models/SrcConfig_Info.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcConfig_Info = { - /** - * SRC脚本名称 - */ - Name?: (string | null); - /** - * SRC路径 - */ - Path?: (string | null); -}; - diff --git a/frontend/src/api/models/SrcConfig_Run.ts b/frontend/src/api/models/SrcConfig_Run.ts deleted file mode 100644 index a3909fe0..00000000 --- a/frontend/src/api/models/SrcConfig_Run.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcConfig_Run = { - /** - * 任务切换方式 - */ - TaskTransitionMethod?: ('ExitGame' | 'ExitEmulator' | null); - /** - * 代理次数限制 - */ - ProxyTimesLimit?: (number | null); - /** - * 运行次数限制 - */ - RunTimesLimit?: (number | null); - /** - * 运行时间限制(分钟) - */ - RunTimeLimit?: (number | null); -}; - diff --git a/frontend/src/api/models/SrcUserConfig.ts b/frontend/src/api/models/SrcUserConfig.ts deleted file mode 100644 index 4ea9d435..00000000 --- a/frontend/src/api/models/SrcUserConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { SrcUserConfig_Data } from './SrcUserConfig_Data'; -import type { SrcUserConfig_Info } from './SrcUserConfig_Info'; -import type { SrcUserConfig_Notify } from './SrcUserConfig_Notify'; -import type { SrcUserConfig_Stage } from './SrcUserConfig_Stage'; -export type SrcUserConfig = { - /** - * 基础信息 - */ - Info?: (SrcUserConfig_Info | null); - /** - * 关卡配置 - */ - Stage?: (SrcUserConfig_Stage | null); - /** - * 用户数据 - */ - Data?: (SrcUserConfig_Data | null); - /** - * 单独通知 - */ - Notify?: (SrcUserConfig_Notify | null); -}; - diff --git a/frontend/src/api/models/SrcUserConfig_Data.ts b/frontend/src/api/models/SrcUserConfig_Data.ts deleted file mode 100644 index b5b9f0c1..00000000 --- a/frontend/src/api/models/SrcUserConfig_Data.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcUserConfig_Data = { - /** - * 上次代理日期 - */ - LastProxyDate?: (string | null); - /** - * 代理次数 - */ - ProxyTimes?: (number | null); - /** - * 是否通过检查 - */ - IfPassCheck?: (boolean | null); -}; - diff --git a/frontend/src/api/models/SrcUserConfig_Info.ts b/frontend/src/api/models/SrcUserConfig_Info.ts deleted file mode 100644 index fc030eef..00000000 --- a/frontend/src/api/models/SrcUserConfig_Info.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcUserConfig_Info = { - /** - * 用户名称 - */ - Name?: (string | null); - /** - * 是否启用 - */ - Status?: (boolean | null); - /** - * 用户ID - */ - Id?: (string | null); - /** - * 密码 - */ - Password?: (string | null); - /** - * 脚本模式 - */ - Mode?: ('简洁' | '详细' | null); - /** - * 游戏服务器 - */ - Server?: ('CN-Official' | 'CN-Bilibili' | 'VN-Official' | 'OVERSEA-America' | 'OVERSEA-Asia' | 'OVERSEA-Europe' | 'OVERSEA-TWHKMO' | null); - /** - * 剩余天数 - */ - RemainedDay?: (number | null); - /** - * 备注 - */ - Notes?: (string | null); - /** - * 用户标签信息 - */ - Tag?: (string | null); -}; - diff --git a/frontend/src/api/models/SrcUserConfig_Notify.ts b/frontend/src/api/models/SrcUserConfig_Notify.ts deleted file mode 100644 index 0fe0c146..00000000 --- a/frontend/src/api/models/SrcUserConfig_Notify.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcUserConfig_Notify = { - /** - * 是否启用通知 - */ - Enabled?: (boolean | null); - /** - * 是否发送统计信息 - */ - IfSendStatistic?: (boolean | null); - /** - * 是否发送邮件 - */ - IfSendMail?: (boolean | null); - /** - * 收件地址 - */ - ToAddress?: (string | null); - /** - * 是否启用Server酱 - */ - IfServerChan?: (boolean | null); - /** - * Server酱密钥 - */ - ServerChanKey?: (string | null); -}; - diff --git a/frontend/src/api/models/SrcUserConfig_Stage.ts b/frontend/src/api/models/SrcUserConfig_Stage.ts deleted file mode 100644 index 5fbf28f3..00000000 --- a/frontend/src/api/models/SrcUserConfig_Stage.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type SrcUserConfig_Stage = { - /** - * 关卡通道 - */ - Channel?: ('Relic' | 'Materials' | 'Ornament' | null); - /** - * 遗器关卡 - */ - Relic?: ('-' | 'Cavern_of_Corrosion_Path_of_Possession' | 'Cavern_of_Corrosion_Path_of_Hidden_Salvation' | 'Cavern_of_Corrosion_Path_of_Thundersurge' | 'Cavern_of_Corrosion_Path_of_Aria' | 'Cavern_of_Corrosion_Path_of_Uncertainty' | 'Cavern_of_Corrosion_Path_of_Cavalier' | 'Cavern_of_Corrosion_Path_of_DreamdiveCavern_of_Corrosion_Path_of_Darkness' | 'Cavern_of_Corrosion_Path_of_Elixir_Seekers' | 'Cavern_of_Corrosion_Path_of_Conflagration' | 'Cavern_of_Corrosion_Path_of_Holy_Hymn' | 'Cavern_of_Corrosion_Path_of_Providence' | 'Cavern_of_Corrosion_Path_of_Drifting' | 'Cavern_of_Corrosion_Path_of_Jabbing_Punch' | 'Cavern_of_Corrosion_Path_of_Gelid_Wind' | null); - /** - * 材料关卡 - */ - Materials?: ('-' | 'Calyx_Golden_Memories_Planarcadia' | 'Calyx_Golden_Aether_Planarcadia' | 'Calyx_Golden_Treasures_Planarcadia' | 'Calyx_Golden_Memories_Amphoreus' | 'Calyx_Golden_Aether_Amphoreus' | 'Calyx_Golden_Treasures_Amphoreus' | 'Calyx_Golden_Memories_Penacony' | 'Calyx_Golden_Aether_Penacony' | 'Calyx_Golden_Treasures_Penacony' | 'Calyx_Golden_Memories_The_Xianzhou_Luofu' | 'Calyx_Golden_Aether_The_Xianzhou_Luofu' | 'Calyx_Golden_Treasures_The_Xianzhou_Luofu' | 'Calyx_Golden_Memories_Jarilo_VI' | 'Calyx_Golden_Aether_Jarilo_VI' | 'Calyx_Golden_Treasures_Jarilo_VI' | 'Calyx_Crimson_Destruction_Herta_StorageZone' | 'Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape' | 'Calyx_Crimson_Preservation_Herta_SupplyZone' | 'Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark' | 'Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains' | 'Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue' | 'Calyx_Crimson_The_Hunt_Amphoreus_MemortisShoreRuinsofTime' | 'Calyx_Crimson_Abundance_Jarilo_BackwaterPass' | 'Calyx_Crimson_Abundance_Luofu_FyxestrollGarden' | 'Calyx_Crimson_Erudition_Jarilo_RivetTown' | 'Calyx_Crimson_Erudition_Penacony_PenaconyGrandTheater' | 'Calyx_Crimson_Harmony_Jarilo_RobotSettlement' | 'Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape' | 'Calyx_Crimson_Nihility_Jarilo_GreatMine' | 'Calyx_Crimson_Nihility_Luofu_AlchemyCommission' | 'Calyx_Crimson_Remembrance_Amphoreus_StrifeRuinsCastrumKremnos' | 'Calyx_Crimson_Elation_Planarcadia_WorldEndTavern' | 'Stagnant_Shadow_Quanta' | 'Stagnant_Shadow_Gust' | 'Stagnant_Shadow_Fulmination' | 'Stagnant_Shadow_Blaze' | 'Stagnant_Shadow_Spike' | 'Stagnant_Shadow_Rime' | 'Stagnant_Shadow_Mirage' | 'Stagnant_Shadow_Icicle' | 'Stagnant_Shadow_Doom' | 'Stagnant_Shadow_Puppetry' | 'Stagnant_Shadow_Abomination' | 'Stagnant_Shadow_Scorch' | 'Stagnant_Shadow_Celestial' | 'Stagnant_Shadow_Perdition' | 'Stagnant_Shadow_Nectar' | 'Stagnant_Shadow_Roast' | 'Stagnant_Shadow_Ire' | 'Stagnant_Shadow_Duty' | 'Stagnant_Shadow_Timbre' | 'Stagnant_Shadow_Mechwolf' | 'Stagnant_Shadow_Gloam' | 'Stagnant_Shadow_Sloggyre' | 'Stagnant_Shadow_Gelidmoon' | 'Stagnant_Shadow_Deepsheaf' | 'Stagnant_Shadow_Cinders' | 'Stagnant_Shadow_Sirens' | 'Stagnant_Shadow_Ashes' | 'Stagnant_Shadow_Soundburst' | null); - /** - * 饰品关卡 - */ - Ornament?: ('-' | 'Divergent_Universe_Within_the_West_Wind' | 'Divergent_Universe_Moonlit_Blood' | 'Divergent_Universe_Unceasing_Strife' | 'Divergent_Universe_Famished_Worker' | 'Divergent_Universe_Eternal_Comedy' | 'Divergent_Universe_To_Sweet_Dreams' | 'Divergent_Universe_Pouring_Blades' | 'Divergent_Universe_Fruit_of_Evil' | 'Divergent_Universe_Permafrost' | 'Divergent_Universe_Gentle_Words' | 'Divergent_Universe_Smelted_Heart' | 'Divergent_Universe_Untoppled_Walls' | null); - /** - * 使用储备开拓力 - */ - ExtractReservedTrailblazePower?: (boolean | null); - /** - * 使用燃料 - */ - UseFuel?: (boolean | null); - /** - * 保留的燃料数量 - */ - FuelReserve?: (number | null); - /** - * 历战余响关卡 - */ - EchoOfWar?: (string | null); - /** - * 模拟宇宙关卡 - */ - SimulatedUniverseWorld?: (string | null); -}; - diff --git a/frontend/src/api/models/TaskCreateIn.ts b/frontend/src/api/models/TaskCreateIn.ts deleted file mode 100644 index c068a5de..00000000 --- a/frontend/src/api/models/TaskCreateIn.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TaskCreateIn = { - /** - * 目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID - */ - taskId: string; - /** - * 任务模式 - */ - mode: TaskCreateIn.mode; -}; -export namespace TaskCreateIn { - /** - * 任务模式 - */ - export enum mode { - AUTO_PROXY = 'AutoProxy', - MANUAL_REVIEW = 'ManualReview', - SCRIPT_CONFIG = 'ScriptConfig', - } -} - diff --git a/frontend/src/api/models/TaskCreateOut.ts b/frontend/src/api/models/TaskCreateOut.ts deleted file mode 100644 index a1cbd6b0..00000000 --- a/frontend/src/api/models/TaskCreateOut.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TaskCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的任务ID - */ - taskId: string; -}; - diff --git a/frontend/src/api/models/TimeSet.ts b/frontend/src/api/models/TimeSet.ts deleted file mode 100644 index 517c7233..00000000 --- a/frontend/src/api/models/TimeSet.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TimeSet_Info } from './TimeSet_Info'; -export type TimeSet = { - /** - * 时间项 - */ - Info?: (TimeSet_Info | null); -}; - diff --git a/frontend/src/api/models/TimeSetCreateOut.ts b/frontend/src/api/models/TimeSetCreateOut.ts deleted file mode 100644 index bfd4e293..00000000 --- a/frontend/src/api/models/TimeSetCreateOut.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TimeSet } from './TimeSet'; -export type TimeSetCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的时间设置ID - */ - timeSetId: string; - /** - * 时间设置配置数据 - */ - data: TimeSet; -}; - diff --git a/frontend/src/api/models/TimeSetDeleteIn.ts b/frontend/src/api/models/TimeSetDeleteIn.ts deleted file mode 100644 index 380f5504..00000000 --- a/frontend/src/api/models/TimeSetDeleteIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TimeSetDeleteIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 时间设置ID - */ - timeSetId: string; -}; - diff --git a/frontend/src/api/models/TimeSetGetIn.ts b/frontend/src/api/models/TimeSetGetIn.ts deleted file mode 100644 index 8ecbf41b..00000000 --- a/frontend/src/api/models/TimeSetGetIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TimeSetGetIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 时间设置ID, 未携带时表示获取所有时间设置数据 - */ - timeSetId?: (string | null); -}; - diff --git a/frontend/src/api/models/TimeSetGetOut.ts b/frontend/src/api/models/TimeSetGetOut.ts deleted file mode 100644 index 80bc4c09..00000000 --- a/frontend/src/api/models/TimeSetGetOut.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TimeSet } from './TimeSet'; -import type { TimeSetIndexItem } from './TimeSetIndexItem'; -export type TimeSetGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 时间设置索引列表 - */ - index: Array; - /** - * 时间设置数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/TimeSetIndexItem.ts b/frontend/src/api/models/TimeSetIndexItem.ts deleted file mode 100644 index 474dbeaa..00000000 --- a/frontend/src/api/models/TimeSetIndexItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TimeSetIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: string; -}; - diff --git a/frontend/src/api/models/TimeSetReorderIn.ts b/frontend/src/api/models/TimeSetReorderIn.ts deleted file mode 100644 index 3164dc86..00000000 --- a/frontend/src/api/models/TimeSetReorderIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TimeSetReorderIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 时间设置ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/TimeSetUpdateIn.ts b/frontend/src/api/models/TimeSetUpdateIn.ts deleted file mode 100644 index 0589edcf..00000000 --- a/frontend/src/api/models/TimeSetUpdateIn.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TimeSet } from './TimeSet'; -export type TimeSetUpdateIn = { - /** - * 所属队列ID - */ - queueId: string; - /** - * 时间设置ID - */ - timeSetId: string; - /** - * 时间设置更新数据 - */ - data: TimeSet; -}; - diff --git a/frontend/src/api/models/TimeSet_Info.ts b/frontend/src/api/models/TimeSet_Info.ts deleted file mode 100644 index 40591e6c..00000000 --- a/frontend/src/api/models/TimeSet_Info.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TimeSet_Info = { - /** - * 是否启用 - */ - Enabled?: (boolean | null); - /** - * 执行周期, 可多选 - */ - Days?: (Array<'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday'> | null); - /** - * 时间设置, 格式为HH:MM - */ - Time?: (string | null); -}; - diff --git a/frontend/src/api/models/ToolsConfig.ts b/frontend/src/api/models/ToolsConfig.ts deleted file mode 100644 index cd478ad4..00000000 --- a/frontend/src/api/models/ToolsConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ToolsConfig_ArknightsPC } from './ToolsConfig_ArknightsPC'; -export type ToolsConfig = { - /** - * 明日方舟PC工具配置 - */ - ArknightsPC?: (ToolsConfig_ArknightsPC | null); -}; - diff --git a/frontend/src/api/models/ToolsConfig_ArknightsPC.ts b/frontend/src/api/models/ToolsConfig_ArknightsPC.ts deleted file mode 100644 index f12aeb9c..00000000 --- a/frontend/src/api/models/ToolsConfig_ArknightsPC.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ToolsConfig_ArknightsPC = { - /** - * 是否启用 ArknightsPC 工具 - */ - Enabled?: (boolean | null); - /** - * 暂停键位 - */ - PauseKey?: (string | null); - /** - * 选中已部署干员键位 - */ - SelectDeployedKey?: (string | null); - /** - * 释放技能键位 - */ - UseSkillKey?: (string | null); - /** - * 撤退键位 - */ - RetreatKey?: (string | null); - /** - * 下一帧键位 - */ - NextFrameKey?: (string | null); - /** - * 自定义退出、暂停键位 - */ - AnotherQuitKey?: (string | null); - /** - * 工具状态 Tag - */ - Status?: (string | null); -}; - diff --git a/frontend/src/api/models/ToolsGetOut.ts b/frontend/src/api/models/ToolsGetOut.ts deleted file mode 100644 index f8985624..00000000 --- a/frontend/src/api/models/ToolsGetOut.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ToolsConfig } from './ToolsConfig'; -export type ToolsGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 工具配置数据 - */ - data: ToolsConfig; -}; - diff --git a/frontend/src/api/models/ToolsUpdateIn.ts b/frontend/src/api/models/ToolsUpdateIn.ts deleted file mode 100644 index 6692e015..00000000 --- a/frontend/src/api/models/ToolsUpdateIn.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ToolsConfig } from './ToolsConfig'; -export type ToolsUpdateIn = { - /** - * 工具配置需要更新的数据 - */ - data: ToolsConfig; -}; - diff --git a/frontend/src/api/models/UpdateCheckIn.ts b/frontend/src/api/models/UpdateCheckIn.ts deleted file mode 100644 index 4d665110..00000000 --- a/frontend/src/api/models/UpdateCheckIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UpdateCheckIn = { - /** - * 当前前端版本号 - */ - current_version: string; - /** - * 是否强制拉取更新信息 - */ - if_force?: boolean; -}; - diff --git a/frontend/src/api/models/UpdateCheckOut.ts b/frontend/src/api/models/UpdateCheckOut.ts deleted file mode 100644 index 52264d3c..00000000 --- a/frontend/src/api/models/UpdateCheckOut.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UpdateCheckOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 是否需要更新前端 - */ - if_need_update: boolean; - /** - * 最新前端版本号 - */ - latest_version: string; - /** - * 版本更新信息字典 - */ - update_info: Record>; -}; - diff --git a/frontend/src/api/models/UserCreateOut.ts b/frontend/src/api/models/UserCreateOut.ts deleted file mode 100644 index 1a7acbb5..00000000 --- a/frontend/src/api/models/UserCreateOut.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralUserConfig } from './GeneralUserConfig'; -import type { MaaEndUserConfig } from './MaaEndUserConfig'; -import type { MaaUserConfig } from './MaaUserConfig'; -import type { SrcUserConfig } from './SrcUserConfig'; -export type UserCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的用户ID - */ - userId: string; - /** - * 用户配置数据 - */ - data: (MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig); -}; - diff --git a/frontend/src/api/models/UserDeleteIn.ts b/frontend/src/api/models/UserDeleteIn.ts deleted file mode 100644 index 7a9f8476..00000000 --- a/frontend/src/api/models/UserDeleteIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserDeleteIn = { - /** - * 所属脚本ID - */ - scriptId: string; - /** - * 用户ID - */ - userId: string; -}; - diff --git a/frontend/src/api/models/UserGetIn.ts b/frontend/src/api/models/UserGetIn.ts deleted file mode 100644 index 0c7b2c6e..00000000 --- a/frontend/src/api/models/UserGetIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserGetIn = { - /** - * 所属脚本ID - */ - scriptId: string; - /** - * 用户ID, 未携带时表示获取所有用户数据 - */ - userId?: (string | null); -}; - diff --git a/frontend/src/api/models/UserGetOut.ts b/frontend/src/api/models/UserGetOut.ts deleted file mode 100644 index 6ad68c50..00000000 --- a/frontend/src/api/models/UserGetOut.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralUserConfig } from './GeneralUserConfig'; -import type { MaaEndUserConfig } from './MaaEndUserConfig'; -import type { MaaUserConfig } from './MaaUserConfig'; -import type { SrcUserConfig } from './SrcUserConfig'; -import type { UserIndexItem } from './UserIndexItem'; -export type UserGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 用户索引列表 - */ - index: Array; - /** - * 用户数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/UserInBase.ts b/frontend/src/api/models/UserInBase.ts deleted file mode 100644 index 1d31b2c7..00000000 --- a/frontend/src/api/models/UserInBase.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserInBase = { - /** - * 所属脚本ID - */ - scriptId: string; -}; - diff --git a/frontend/src/api/models/UserIndexItem.ts b/frontend/src/api/models/UserIndexItem.ts deleted file mode 100644 index da1b7670..00000000 --- a/frontend/src/api/models/UserIndexItem.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: UserIndexItem.type; -}; -export namespace UserIndexItem { - /** - * 配置类型 - */ - export enum type { - MAA_USER_CONFIG = 'MaaUserConfig', - GENERAL_USER_CONFIG = 'GeneralUserConfig', - SRC_USER_CONFIG = 'SrcUserConfig', - MAA_END_USER_CONFIG = 'MaaEndUserConfig', - } -} - diff --git a/frontend/src/api/models/UserReorderIn.ts b/frontend/src/api/models/UserReorderIn.ts deleted file mode 100644 index a573388f..00000000 --- a/frontend/src/api/models/UserReorderIn.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserReorderIn = { - /** - * 所属脚本ID - */ - scriptId: string; - /** - * 用户ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/UserSetIn.ts b/frontend/src/api/models/UserSetIn.ts deleted file mode 100644 index 81f1f99d..00000000 --- a/frontend/src/api/models/UserSetIn.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserSetIn = { - /** - * 所属脚本ID - */ - scriptId: string; - /** - * 用户ID - */ - userId: string; - /** - * JSON文件路径, 用于导入自定义基建文件 - */ - jsonFile: string; -}; - diff --git a/frontend/src/api/models/UserUpdateIn.ts b/frontend/src/api/models/UserUpdateIn.ts deleted file mode 100644 index 102e4a71..00000000 --- a/frontend/src/api/models/UserUpdateIn.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { GeneralUserConfig } from './GeneralUserConfig'; -import type { MaaEndUserConfig } from './MaaEndUserConfig'; -import type { MaaUserConfig } from './MaaUserConfig'; -import type { SrcUserConfig } from './SrcUserConfig'; -export type UserUpdateIn = { - /** - * 所属脚本ID - */ - scriptId: string; - /** - * 用户ID - */ - userId: string; - /** - * 用户更新数据 - */ - data: (MaaUserConfig | SrcUserConfig | GeneralUserConfig | MaaEndUserConfig); -}; - diff --git a/frontend/src/api/models/ValidationError.ts b/frontend/src/api/models/ValidationError.ts deleted file mode 100644 index aaf1c921..00000000 --- a/frontend/src/api/models/ValidationError.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ValidationError = { - loc: Array<(string | number)>; - msg: string; - type: string; -}; - diff --git a/frontend/src/api/models/VersionOut.ts b/frontend/src/api/models/VersionOut.ts deleted file mode 100644 index 6add6e38..00000000 --- a/frontend/src/api/models/VersionOut.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type VersionOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 后端代码是否需要更新 - */ - if_need_update: boolean; - /** - * 后端代码当前时间戳 - */ - current_time: string; - /** - * 后端代码当前哈希值 - */ - current_hash: string; -}; - diff --git a/frontend/src/api/models/WSClearHistoryIn.ts b/frontend/src/api/models/WSClearHistoryIn.ts deleted file mode 100644 index fdf046c5..00000000 --- a/frontend/src/api/models/WSClearHistoryIn.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 清空消息历史请求 - */ -export type WSClearHistoryIn = { - /** - * 客户端名称,为空则清空所有 - */ - name?: (string | null); -}; - diff --git a/frontend/src/api/models/WSClientAuthIn.ts b/frontend/src/api/models/WSClientAuthIn.ts deleted file mode 100644 index d4d56906..00000000 --- a/frontend/src/api/models/WSClientAuthIn.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 发送认证请求 - */ -export type WSClientAuthIn = { - /** - * 客户端名称 - */ - name: string; - /** - * 认证 Token - */ - token: string; - /** - * 认证消息类型 - */ - auth_type?: string; - /** - * 额外认证数据 - */ - extra_data?: (Record | null); -}; - diff --git a/frontend/src/api/models/WSClientConnectIn.ts b/frontend/src/api/models/WSClientConnectIn.ts deleted file mode 100644 index b3fadee3..00000000 --- a/frontend/src/api/models/WSClientConnectIn.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 连接请求 - */ -export type WSClientConnectIn = { - /** - * 客户端名称 - */ - name: string; -}; - diff --git a/frontend/src/api/models/WSClientCreateIn.ts b/frontend/src/api/models/WSClientCreateIn.ts deleted file mode 100644 index 2e7da367..00000000 --- a/frontend/src/api/models/WSClientCreateIn.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 创建 WebSocket 客户端请求 - */ -export type WSClientCreateIn = { - /** - * 客户端名称,用于标识 - */ - name: string; - /** - * WebSocket 服务器地址,如 ws://localhost:5140/path - */ - url: string; - /** - * 心跳发送间隔(秒) - */ - ping_interval?: number; - /** - * 心跳超时时间(秒) - */ - ping_timeout?: number; - /** - * 重连间隔(秒) - */ - reconnect_interval?: number; - /** - * 最大重连次数,-1为无限 - */ - max_reconnect_attempts?: number; -}; - diff --git a/frontend/src/api/models/WSClientCreateOut.ts b/frontend/src/api/models/WSClientCreateOut.ts deleted file mode 100644 index 0edb76be..00000000 --- a/frontend/src/api/models/WSClientCreateOut.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 创建客户端响应 - */ -export type WSClientCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 返回数据 - */ - data?: (Record | null); -}; - diff --git a/frontend/src/api/models/WSClientDisconnectIn.ts b/frontend/src/api/models/WSClientDisconnectIn.ts deleted file mode 100644 index 5e3c4025..00000000 --- a/frontend/src/api/models/WSClientDisconnectIn.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 断开连接请求 - */ -export type WSClientDisconnectIn = { - /** - * 客户端名称 - */ - name: string; -}; - diff --git a/frontend/src/api/models/WSClientListOut.ts b/frontend/src/api/models/WSClientListOut.ts deleted file mode 100644 index d463755d..00000000 --- a/frontend/src/api/models/WSClientListOut.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 客户端列表响应 - */ -export type WSClientListOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 客户端列表 - */ - data?: (Record | null); -}; - diff --git a/frontend/src/api/models/WSClientRemoveIn.ts b/frontend/src/api/models/WSClientRemoveIn.ts deleted file mode 100644 index 02b40e36..00000000 --- a/frontend/src/api/models/WSClientRemoveIn.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 删除客户端请求 - */ -export type WSClientRemoveIn = { - /** - * 客户端名称 - */ - name: string; -}; - diff --git a/frontend/src/api/models/WSClientSendIn.ts b/frontend/src/api/models/WSClientSendIn.ts deleted file mode 100644 index 8f7bd8b6..00000000 --- a/frontend/src/api/models/WSClientSendIn.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 发送消息请求 - */ -export type WSClientSendIn = { - /** - * 客户端名称 - */ - name: string; - /** - * 要发送的 JSON 消息 - */ - message: Record; -}; - diff --git a/frontend/src/api/models/WSClientSendJsonIn.ts b/frontend/src/api/models/WSClientSendJsonIn.ts deleted file mode 100644 index 1427c162..00000000 --- a/frontend/src/api/models/WSClientSendJsonIn.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 发送自定义 JSON 消息请求 - */ -export type WSClientSendJsonIn = { - /** - * 客户端名称 - */ - name: string; - /** - * 消息 ID - */ - msg_id?: string; - /** - * 消息类型 - */ - msg_type: string; - /** - * 消息数据 - */ - data?: Record; -}; - diff --git a/frontend/src/api/models/WSClientStatusIn.ts b/frontend/src/api/models/WSClientStatusIn.ts deleted file mode 100644 index 2e356e1d..00000000 --- a/frontend/src/api/models/WSClientStatusIn.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 获取客户端状态请求 - */ -export type WSClientStatusIn = { - /** - * 客户端名称 - */ - name: string; -}; - diff --git a/frontend/src/api/models/WSClientStatusOut.ts b/frontend/src/api/models/WSClientStatusOut.ts deleted file mode 100644 index 3738fa9d..00000000 --- a/frontend/src/api/models/WSClientStatusOut.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 客户端状态响应 - */ -export type WSClientStatusOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 状态数据 - */ - data?: (Record | null); -}; - diff --git a/frontend/src/api/models/WSCommandsOut.ts b/frontend/src/api/models/WSCommandsOut.ts deleted file mode 100644 index 99ff64c6..00000000 --- a/frontend/src/api/models/WSCommandsOut.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 可用命令列表响应 - */ -export type WSCommandsOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 命令列表 - */ - data?: (Record | null); -}; - diff --git a/frontend/src/api/models/WSMessageHistoryOut.ts b/frontend/src/api/models/WSMessageHistoryOut.ts deleted file mode 100644 index c20e6267..00000000 --- a/frontend/src/api/models/WSMessageHistoryOut.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 消息历史响应 - */ -export type WSMessageHistoryOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 消息历史 - */ - data?: (Record | null); -}; - diff --git a/frontend/src/api/models/Webhook.ts b/frontend/src/api/models/Webhook.ts deleted file mode 100644 index 825405c7..00000000 --- a/frontend/src/api/models/Webhook.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { Webhook_Data } from './Webhook_Data'; -import type { Webhook_Info } from './Webhook_Info'; -export type Webhook = { - /** - * Webhook基础信息 - */ - Info?: (Webhook_Info | null); - /** - * Webhook配置数据 - */ - Data?: (Webhook_Data | null); -}; - diff --git a/frontend/src/api/models/WebhookCreateOut.ts b/frontend/src/api/models/WebhookCreateOut.ts deleted file mode 100644 index 69b0ad9b..00000000 --- a/frontend/src/api/models/WebhookCreateOut.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { Webhook } from './Webhook'; -export type WebhookCreateOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * 新创建的Webhook ID - */ - webhookId: string; - /** - * Webhook配置数据 - */ - data: Webhook; -}; - diff --git a/frontend/src/api/models/WebhookDeleteIn.ts b/frontend/src/api/models/WebhookDeleteIn.ts deleted file mode 100644 index 44be4953..00000000 --- a/frontend/src/api/models/WebhookDeleteIn.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type WebhookDeleteIn = { - /** - * 所属脚本ID, 获取全局设置的Webhook数据时无需携带 - */ - scriptId?: (string | null); - /** - * 所属用户ID, 获取全局设置的Webhook数据时无需携带 - */ - userId?: (string | null); - /** - * Webhook ID - */ - webhookId: string; -}; - diff --git a/frontend/src/api/models/WebhookGetIn.ts b/frontend/src/api/models/WebhookGetIn.ts deleted file mode 100644 index 68675c33..00000000 --- a/frontend/src/api/models/WebhookGetIn.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type WebhookGetIn = { - /** - * 所属脚本ID, 获取全局设置的Webhook数据时无需携带 - */ - scriptId?: (string | null); - /** - * 所属用户ID, 获取全局设置的Webhook数据时无需携带 - */ - userId?: (string | null); - /** - * Webhook ID, 未携带时表示获取所有Webhook数据 - */ - webhookId?: (string | null); -}; - diff --git a/frontend/src/api/models/WebhookGetOut.ts b/frontend/src/api/models/WebhookGetOut.ts deleted file mode 100644 index 3503b1b8..00000000 --- a/frontend/src/api/models/WebhookGetOut.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { Webhook } from './Webhook'; -import type { WebhookIndexItem } from './WebhookIndexItem'; -export type WebhookGetOut = { - /** - * 状态码 - */ - code?: number; - /** - * 操作状态 - */ - status?: string; - /** - * 操作消息 - */ - message?: string; - /** - * Webhook索引列表 - */ - index: Array; - /** - * Webhook数据字典, key来自于index列表的uid - */ - data: Record; -}; - diff --git a/frontend/src/api/models/WebhookInBase.ts b/frontend/src/api/models/WebhookInBase.ts deleted file mode 100644 index 1ab90eaf..00000000 --- a/frontend/src/api/models/WebhookInBase.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type WebhookInBase = { - /** - * 所属脚本ID, 获取全局设置的Webhook数据时无需携带 - */ - scriptId?: (string | null); - /** - * 所属用户ID, 获取全局设置的Webhook数据时无需携带 - */ - userId?: (string | null); -}; - diff --git a/frontend/src/api/models/WebhookIndexItem.ts b/frontend/src/api/models/WebhookIndexItem.ts deleted file mode 100644 index 7bce9724..00000000 --- a/frontend/src/api/models/WebhookIndexItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type WebhookIndexItem = { - /** - * 唯一标识符 - */ - uid: string; - /** - * 配置类型 - */ - type: string; -}; - diff --git a/frontend/src/api/models/WebhookReorderIn.ts b/frontend/src/api/models/WebhookReorderIn.ts deleted file mode 100644 index 59c7f885..00000000 --- a/frontend/src/api/models/WebhookReorderIn.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type WebhookReorderIn = { - /** - * 所属脚本ID, 获取全局设置的Webhook数据时无需携带 - */ - scriptId?: (string | null); - /** - * 所属用户ID, 获取全局设置的Webhook数据时无需携带 - */ - userId?: (string | null); - /** - * Webhook ID列表, 按新顺序排列 - */ - indexList: Array; -}; - diff --git a/frontend/src/api/models/WebhookTestIn.ts b/frontend/src/api/models/WebhookTestIn.ts deleted file mode 100644 index 2a5978e8..00000000 --- a/frontend/src/api/models/WebhookTestIn.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { Webhook } from './Webhook'; -export type WebhookTestIn = { - /** - * 所属脚本ID, 获取全局设置的Webhook数据时无需携带 - */ - scriptId?: (string | null); - /** - * 所属用户ID, 获取全局设置的Webhook数据时无需携带 - */ - userId?: (string | null); - /** - * Webhook配置数据 - */ - data: Webhook; -}; - diff --git a/frontend/src/api/models/WebhookUpdateIn.ts b/frontend/src/api/models/WebhookUpdateIn.ts deleted file mode 100644 index 17e9a233..00000000 --- a/frontend/src/api/models/WebhookUpdateIn.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { Webhook } from './Webhook'; -export type WebhookUpdateIn = { - /** - * 所属脚本ID, 获取全局设置的Webhook数据时无需携带 - */ - scriptId?: (string | null); - /** - * 所属用户ID, 获取全局设置的Webhook数据时无需携带 - */ - userId?: (string | null); - /** - * Webhook ID - */ - webhookId: string; - /** - * Webhook更新数据 - */ - data: Webhook; -}; - diff --git a/frontend/src/api/models/Webhook_Data.ts b/frontend/src/api/models/Webhook_Data.ts deleted file mode 100644 index bb42dd64..00000000 --- a/frontend/src/api/models/Webhook_Data.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type Webhook_Data = { - /** - * Webhook URL - */ - Url?: (string | null); - /** - * 消息模板 - */ - Template?: (string | null); - /** - * 自定义请求头 - */ - Headers?: (string | null); - /** - * 请求方法 - */ - Method?: ('POST' | 'GET' | null); -}; - diff --git a/frontend/src/api/models/Webhook_Info.ts b/frontend/src/api/models/Webhook_Info.ts deleted file mode 100644 index 90fe03ba..00000000 --- a/frontend/src/api/models/Webhook_Info.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type Webhook_Info = { - /** - * Webhook名称 - */ - Name?: (string | null); - /** - * 是否启用 - */ - Enabled?: (boolean | null); -}; - diff --git a/frontend/src/api/runtime.ts b/frontend/src/api/runtime.ts new file mode 100644 index 00000000..d0201960 --- /dev/null +++ b/frontend/src/api/runtime.ts @@ -0,0 +1,17 @@ +import { client } from './generated/client.gen' + +client.setConfig({ + responseStyle: 'data', + throwOnError: true, +}) + +export const apiClient = client + +export const apiRuntime = { + get baseUrl(): string { + return apiClient.getConfig().baseUrl ?? '' + }, + set baseUrl(value: string) { + apiClient.setConfig({ baseUrl: value }) + }, +} diff --git a/frontend/src/api/services/ActionService.ts b/frontend/src/api/services/ActionService.ts deleted file mode 100644 index a08efc11..00000000 --- a/frontend/src/api/services/ActionService.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ClickImageIn } from '../models/ClickImageIn'; -import type { ClickOut } from '../models/ClickOut'; -import type { ClickTextIn } from '../models/ClickTextIn'; -import type { DispatchIn } from '../models/DispatchIn'; -import type { EmulatorOperateIn } from '../models/EmulatorOperateIn'; -import type { OutBase } from '../models/OutBase'; -import type { PowerIn } from '../models/PowerIn'; -import type { ScriptFileIn } from '../models/ScriptFileIn'; -import type { ScriptUploadIn } from '../models/ScriptUploadIn'; -import type { TaskCreateIn } from '../models/TaskCreateIn'; -import type { TaskCreateOut } from '../models/TaskCreateOut'; -import type { WebhookTestIn } from '../models/WebhookTestIn'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class ActionService { - /** - * 确认通知 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static confirmNoticeApiInfoNoticeConfirmPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/notice/confirm', - }); - } - /** - * 导出脚本配置到文件 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static exportScriptToFileApiScriptsExportFilePost( - requestBody: ScriptFileIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/export/file', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 上传脚本配置到网络 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static uploadScriptToWebApiScriptsUploadWebPost( - requestBody: ScriptUploadIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/Upload/web', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 操作模拟器 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static operationEmulatorApiEmulatorOperatePost( - requestBody: EmulatorOperateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/operate', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加任务 - * @param requestBody - * @returns TaskCreateOut Successful Response - * @throws ApiError - */ - public static addTaskApiDispatchStartPost( - requestBody: TaskCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/start', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 中止任务 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static stopTaskApiDispatchStopPost( - requestBody: DispatchIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/stop', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 设置电源标志 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static setPowerApiDispatchSetPowerPost( - requestBody: PowerIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/set/power', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 取消电源任务 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static cancelPowerTaskApiDispatchCancelPowerPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/cancel/power', - }); - } - /** - * 测试通知 - * 测试通知 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static testNotifyApiSettingTestNotifyPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/test_notify', - }); - } - /** - * 测试Webhook配置 - * 测试自定义Webhook - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static testWebhookApiSettingWebhookTestPost( - requestBody: WebhookTestIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/test', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 下载更新 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static downloadUpdateApiUpdateDownloadPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/update/download', - }); - } - /** - * 安装更新 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static installUpdateApiUpdateInstallPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/update/install', - }); - } - /** - * 点击指定图像位置 - * 截图、查找并点击与图像一致的位置 - * - * Args: - * params: 点击图像参数 - * - window_title: 窗口标题关键字 - * - image_path: 要查找并点击的图片路径 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * ClickOut: 包含点击结果和尝试次数 - * @param requestBody - * @returns ClickOut Successful Response - * @throws ApiError - */ - public static clickImageApiOcrClickImagePost( - requestBody: ClickImageIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/click/image', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 点击指定文字位置 - * 截图、OCR识别并点击与文字一致的位置 - * - * Args: - * params: 点击文字参数 - * - window_title: 窗口标题关键字 - * - text: 要查找并点击的文字内容 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - * Returns: - * ClickOut: 包含点击结果和尝试次数 - * @param requestBody - * @returns ClickOut Successful Response - * @throws ApiError - */ - public static clickTextApiOcrClickTextPost( - requestBody: ClickTextIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/click/text', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/frontend/src/api/services/AddService.ts b/frontend/src/api/services/AddService.ts deleted file mode 100644 index cdd5c2f8..00000000 --- a/frontend/src/api/services/AddService.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorCreateOut } from '../models/EmulatorCreateOut'; -import type { PlanCreateIn } from '../models/PlanCreateIn'; -import type { PlanCreateOut } from '../models/PlanCreateOut'; -import type { QueueCreateOut } from '../models/QueueCreateOut'; -import type { QueueItemCreateOut } from '../models/QueueItemCreateOut'; -import type { QueueSetInBase } from '../models/QueueSetInBase'; -import type { ScriptCreateIn } from '../models/ScriptCreateIn'; -import type { ScriptCreateOut } from '../models/ScriptCreateOut'; -import type { TimeSetCreateOut } from '../models/TimeSetCreateOut'; -import type { UserCreateOut } from '../models/UserCreateOut'; -import type { UserInBase } from '../models/UserInBase'; -import type { WebhookCreateOut } from '../models/WebhookCreateOut'; -import type { WebhookInBase } from '../models/WebhookInBase'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class AddService { - /** - * 添加脚本 - * @param requestBody - * @returns ScriptCreateOut Successful Response - * @throws ApiError - */ - public static addScriptApiScriptsAddPost( - requestBody: ScriptCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加用户 - * @param requestBody - * @returns UserCreateOut Successful Response - * @throws ApiError - */ - public static addUserApiScriptsUserAddPost( - requestBody: UserInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加webhook项 - * @param requestBody - * @returns WebhookCreateOut Successful Response - * @throws ApiError - */ - public static addWebhookApiScriptsWebhookAddPost( - requestBody: WebhookInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加计划表 - * @param requestBody - * @returns PlanCreateOut Successful Response - * @throws ApiError - */ - public static addPlanApiPlanAddPost( - requestBody: PlanCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加模拟器项 - * @returns EmulatorCreateOut Successful Response - * @throws ApiError - */ - public static addEmulatorApiEmulatorAddPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/add', - }); - } - /** - * 添加调度队列 - * @returns QueueCreateOut Successful Response - * @throws ApiError - */ - public static addQueueApiQueueAddPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/add', - }); - } - /** - * 添加定时项 - * @param requestBody - * @returns TimeSetCreateOut Successful Response - * @throws ApiError - */ - public static addTimeSetApiQueueTimeAddPost( - requestBody: QueueSetInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加队列项 - * @param requestBody - * @returns QueueItemCreateOut Successful Response - * @throws ApiError - */ - public static addItemApiQueueItemAddPost( - requestBody: QueueSetInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加webhook项 - * @returns WebhookCreateOut Successful Response - * @throws ApiError - */ - public static addWebhookApiSettingWebhookAddPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/add', - }); - } -} diff --git a/frontend/src/api/services/DeleteService.ts b/frontend/src/api/services/DeleteService.ts deleted file mode 100644 index 73d0be06..00000000 --- a/frontend/src/api/services/DeleteService.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorDeleteIn } from '../models/EmulatorDeleteIn'; -import type { OutBase } from '../models/OutBase'; -import type { PlanDeleteIn } from '../models/PlanDeleteIn'; -import type { QueueDeleteIn } from '../models/QueueDeleteIn'; -import type { QueueItemDeleteIn } from '../models/QueueItemDeleteIn'; -import type { ScriptDeleteIn } from '../models/ScriptDeleteIn'; -import type { TimeSetDeleteIn } from '../models/TimeSetDeleteIn'; -import type { UserDeleteIn } from '../models/UserDeleteIn'; -import type { WebhookDeleteIn } from '../models/WebhookDeleteIn'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class DeleteService { - /** - * 删除脚本 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteScriptApiScriptsDeletePost( - requestBody: ScriptDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除用户 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteUserApiScriptsUserDeletePost( - requestBody: UserDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteWebhookApiScriptsWebhookDeletePost( - requestBody: WebhookDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除计划表 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deletePlanApiPlanDeletePost( - requestBody: PlanDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除模拟器项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteEmulatorApiEmulatorDeletePost( - requestBody: EmulatorDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除调度队列 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteQueueApiQueueDeletePost( - requestBody: QueueDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除定时项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteTimeSetApiQueueTimeDeletePost( - requestBody: TimeSetDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除队列项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteItemApiQueueItemDeletePost( - requestBody: QueueItemDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteWebhookApiSettingWebhookDeletePost( - requestBody: WebhookDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/frontend/src/api/services/GetService.ts b/frontend/src/api/services/GetService.ts deleted file mode 100644 index d70f8093..00000000 --- a/frontend/src/api/services/GetService.ts +++ /dev/null @@ -1,647 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ADBScreenshotIn } from '../models/ADBScreenshotIn'; -import type { ADBScreenshotOut } from '../models/ADBScreenshotOut'; -import type { CheckImageAllIn } from '../models/CheckImageAllIn'; -import type { CheckImageAnyIn } from '../models/CheckImageAnyIn'; -import type { CheckImageIn } from '../models/CheckImageIn'; -import type { CheckImageOut } from '../models/CheckImageOut'; -import type { ComboBoxOut } from '../models/ComboBoxOut'; -import type { EmulatorDeleteIn } from '../models/EmulatorDeleteIn'; -import type { EmulatorGetIn } from '../models/EmulatorGetIn'; -import type { EmulatorGetOut } from '../models/EmulatorGetOut'; -import type { EmulatorSearchOut } from '../models/EmulatorSearchOut'; -import type { EmulatorStatusOut } from '../models/EmulatorStatusOut'; -import type { GetStageIn } from '../models/GetStageIn'; -import type { HistoryDataGetIn } from '../models/HistoryDataGetIn'; -import type { HistoryDataGetOut } from '../models/HistoryDataGetOut'; -import type { HistorySearchIn } from '../models/HistorySearchIn'; -import type { HistorySearchOut } from '../models/HistorySearchOut'; -import type { InfoOut } from '../models/InfoOut'; -import type { NoticeOut } from '../models/NoticeOut'; -import type { OCRScreenshotIn } from '../models/OCRScreenshotIn'; -import type { OCRScreenshotOut } from '../models/OCRScreenshotOut'; -import type { PlanGetIn } from '../models/PlanGetIn'; -import type { PlanGetOut } from '../models/PlanGetOut'; -import type { PowerOut } from '../models/PowerOut'; -import type { QueueGetIn } from '../models/QueueGetIn'; -import type { QueueGetOut } from '../models/QueueGetOut'; -import type { QueueItemGetIn } from '../models/QueueItemGetIn'; -import type { QueueItemGetOut } from '../models/QueueItemGetOut'; -import type { ScriptGetIn } from '../models/ScriptGetIn'; -import type { ScriptGetOut } from '../models/ScriptGetOut'; -import type { SettingGetOut } from '../models/SettingGetOut'; -import type { TimeSetGetIn } from '../models/TimeSetGetIn'; -import type { TimeSetGetOut } from '../models/TimeSetGetOut'; -import type { ToolsGetOut } from '../models/ToolsGetOut'; -import type { UpdateCheckIn } from '../models/UpdateCheckIn'; -import type { UpdateCheckOut } from '../models/UpdateCheckOut'; -import type { UserDeleteIn } from '../models/UserDeleteIn'; -import type { UserGetIn } from '../models/UserGetIn'; -import type { UserGetOut } from '../models/UserGetOut'; -import type { VersionOut } from '../models/VersionOut'; -import type { WebhookGetIn } from '../models/WebhookGetIn'; -import type { WebhookGetOut } from '../models/WebhookGetOut'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class GetService { - /** - * 获取后端git版本信息 - * @returns VersionOut Successful Response - * @throws ApiError - */ - public static getGitVersionApiInfoVersionPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/version', - }); - } - /** - * 获取关卡号下拉框信息 - * @param requestBody - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getStageComboxApiInfoComboxStagePost( - requestBody: GetStageIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/stage', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取脚本下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getScriptComboxApiInfoComboxScriptPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/script', - }); - } - /** - * 获取可选任务下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getTaskComboxApiInfoComboxTaskPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/task', - }); - } - /** - * 获取可选计划下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getPlanComboxApiInfoComboxPlanPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/plan', - }); - } - /** - * 获取可选模拟器下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getEmulatorComboxApiInfoComboxEmulatorPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/emulator', - }); - } - /** - * 获取可选模拟器多开实例下拉框信息 - * @param requestBody - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost( - requestBody: EmulatorDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/emulator/devices', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取通知信息 - * @returns NoticeOut Successful Response - * @throws ApiError - */ - public static getNoticeInfoApiInfoNoticeGetPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/notice/get', - }); - } - /** - * 获取配置分享中心的配置信息 - * @returns InfoOut Successful Response - * @throws ApiError - */ - public static getWebConfigApiInfoWebconfigPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/webconfig', - }); - } - /** - * 信息总览 - * @returns InfoOut Successful Response - * @throws ApiError - */ - public static getOverviewApiInfoGetOverviewPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/get/overview', - }); - } - /** - * 查询脚本配置信息 - * @param requestBody - * @returns ScriptGetOut Successful Response - * @throws ApiError - */ - public static getScriptApiScriptsGetPost( - requestBody: ScriptGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询用户 - * @param requestBody - * @returns UserGetOut Successful Response - * @throws ApiError - */ - public static getUserApiScriptsUserGetPost( - requestBody: UserGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 用户自定义基建排班可选项 - * @param requestBody - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getUserComboxInfrastructureApiScriptsUserComboxInfrastructurePost( - requestBody: UserDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/combox/infrastructure', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询 webhook 配置 - * @param requestBody - * @returns WebhookGetOut Successful Response - * @throws ApiError - */ - public static getWebhookApiScriptsWebhookGetPost( - requestBody: WebhookGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询计划表 - * @param requestBody - * @returns PlanGetOut Successful Response - * @throws ApiError - */ - public static getPlanApiPlanGetPost( - requestBody: PlanGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询模拟器配置 - * @param requestBody - * @returns EmulatorGetOut Successful Response - * @throws ApiError - */ - public static getEmulatorApiEmulatorGetPost( - requestBody: EmulatorGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询模拟器状态 - * @param requestBody - * @returns EmulatorStatusOut Successful Response - * @throws ApiError - */ - public static getStatusApiEmulatorStatusPost( - requestBody: EmulatorGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/status', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 搜索已安装的模拟器 - * 自动搜索系统中已安装的模拟器 - * @returns EmulatorSearchOut Successful Response - * @throws ApiError - */ - public static searchEmulatorsApiEmulatorEmulatorSearchPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/emulator/search', - }); - } - /** - * 查询调度队列配置信息 - * @param requestBody - * @returns QueueGetOut Successful Response - * @throws ApiError - */ - public static getQueuesApiQueueGetPost( - requestBody: QueueGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询定时项 - * @param requestBody - * @returns TimeSetGetOut Successful Response - * @throws ApiError - */ - public static getTimeSetApiQueueTimeGetPost( - requestBody: TimeSetGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询队列项 - * @param requestBody - * @returns QueueItemGetOut Successful Response - * @throws ApiError - */ - public static getItemApiQueueItemGetPost( - requestBody: QueueItemGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取电源标志 - * @returns PowerOut Successful Response - * @throws ApiError - */ - public static getPowerApiDispatchGetPowerPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/get/power', - }); - } - /** - * 搜索历史记录总览信息 - * @param requestBody - * @returns HistorySearchOut Successful Response - * @throws ApiError - */ - public static searchHistoryApiHistorySearchPost( - requestBody: HistorySearchIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/history/search', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 从指定文件内获取历史记录数据 - * @param requestBody - * @returns HistoryDataGetOut Successful Response - * @throws ApiError - */ - public static getHistoryDataApiHistoryDataPost( - requestBody: HistoryDataGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/history/data', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询工具配置 - * 查询工具配置 - * @returns ToolsGetOut Successful Response - * @throws ApiError - */ - public static getToolsApiToolsGetPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/tools/get', - }); - } - /** - * 查询配置 - * 查询配置 - * @returns SettingGetOut Successful Response - * @throws ApiError - */ - public static getScriptsApiSettingGetPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/get', - }); - } - /** - * 查询 webhook 配置 - * @param requestBody - * @returns WebhookGetOut Successful Response - * @throws ApiError - */ - public static getWebhookApiSettingWebhookGetPost( - requestBody: WebhookGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查更新 - * @param requestBody - * @returns UpdateCheckOut Successful Response - * @throws ApiError - */ - public static checkUpdateApiUpdateCheckPost( - requestBody: UpdateCheckIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/update/check', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取窗口截图 - * 根据窗口标题获取截图,返回Base64编码的图像数据 - * - * Args: - * params: 截图参数 - * - window_title: 窗口标题关键字 - * - should_preprocess: 是否预处理图片区域(默认True) - * - aspect_ratio_width: 宽高比宽度(默认16) - * - aspect_ratio_height: 宽高比高度(默认9) - * - region: 自定义截图区域,格式为 (left, top, width, height) - * - * Returns: - * OCRScreenshotOut: 包含Base64编码的截图和区域信息 - * @param requestBody - * @returns OCRScreenshotOut Successful Response - * @throws ApiError - */ - public static getScreenshotApiOcrScreenshotPost( - requestBody: OCRScreenshotIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/screenshot', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 通过ADB获取设备截图 - * 通过 ADB 端口获取 Android 设备/模拟器截图,返回Base64编码的图像数据 - * - * 支持两种截图方法: - * 1. screencap PNG 方法(推荐):速度快,直接获取 PNG 图像 - * 2. screencap raw 方法:获取原始像素数据,适用于某些不支持 PNG 的设备 - * - * Args: - * params: ADB 截图参数 - * - adb_path: ADB 可执行文件的路径 - * - serial: 设备序列号,格式如 "127.0.0.1:5555" 或 "emulator-5554" - * - use_screencap: 是否使用 screencap PNG 方法(默认True) - * - * Returns: - * ADBScreenshotOut: 包含Base64编码的截图和设备信息 - * @param requestBody - * @returns ADBScreenshotOut Successful Response - * @throws ApiError - */ - public static getScreenshotAdbApiOcrScreenshotAdbPost( - requestBody: ADBScreenshotIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/screenshot/adb', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查是否存在指定图像 - * 截图并查找是否存在图片内的内容 - * - * Args: - * params: 检查图像参数 - * - window_title: 窗口标题关键字 - * - image_path: 要查找的图片路径 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * CheckImageOut: 包含查找结果和尝试次数 - * @param requestBody - * @returns CheckImageOut Successful Response - * @throws ApiError - */ - public static checkImageApiOcrCheckImagePost( - requestBody: CheckImageIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/check/image', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查是否存在任意一个指定图像 - * 截图并查找是否存在列表中任意一张图片的内容 - * - * Args: - * params: 检查图像参数 - * - window_title: 窗口标题关键字 - * - image_paths: 要查找的图片路径列表 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * CheckImageOut: 包含查找结果和尝试次数 - * @param requestBody - * @returns CheckImageOut Successful Response - * @throws ApiError - */ - public static checkImageAnyApiOcrCheckImageAnyPost( - requestBody: CheckImageAnyIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/check/image/any', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查是否存在所有指定图像 - * 截图并查找是否存在列表中所有图片的内容 - * - * Args: - * params: 检查图像参数 - * - window_title: 窗口标题关键字 - * - image_paths: 要查找的图片路径列表 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * CheckImageOut: 包含查找结果和尝试次数 - * @param requestBody - * @returns CheckImageOut Successful Response - * @throws ApiError - */ - public static checkImageAllApiOcrCheckImageAllPost( - requestBody: CheckImageAllIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/check/image/all', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/frontend/src/api/services/OcrService.ts b/frontend/src/api/services/OcrService.ts deleted file mode 100644 index 55f43a0c..00000000 --- a/frontend/src/api/services/OcrService.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ADBScreenshotIn } from '../models/ADBScreenshotIn'; -import type { ADBScreenshotOut } from '../models/ADBScreenshotOut'; -import type { CheckImageAllIn } from '../models/CheckImageAllIn'; -import type { CheckImageAnyIn } from '../models/CheckImageAnyIn'; -import type { CheckImageIn } from '../models/CheckImageIn'; -import type { CheckImageOut } from '../models/CheckImageOut'; -import type { ClickImageIn } from '../models/ClickImageIn'; -import type { ClickOut } from '../models/ClickOut'; -import type { ClickTextIn } from '../models/ClickTextIn'; -import type { OCRScreenshotIn } from '../models/OCRScreenshotIn'; -import type { OCRScreenshotOut } from '../models/OCRScreenshotOut'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class OcrService { - /** - * 获取窗口截图 - * 根据窗口标题获取截图,返回Base64编码的图像数据 - * - * Args: - * params: 截图参数 - * - window_title: 窗口标题关键字 - * - should_preprocess: 是否预处理图片区域(默认True) - * - aspect_ratio_width: 宽高比宽度(默认16) - * - aspect_ratio_height: 宽高比高度(默认9) - * - region: 自定义截图区域,格式为 (left, top, width, height) - * - * Returns: - * OCRScreenshotOut: 包含Base64编码的截图和区域信息 - * @param requestBody - * @returns OCRScreenshotOut Successful Response - * @throws ApiError - */ - public static getScreenshotApiOcrScreenshotPost( - requestBody: OCRScreenshotIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/screenshot', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 通过ADB获取设备截图 - * 通过 ADB 端口获取 Android 设备/模拟器截图,返回Base64编码的图像数据 - * - * 支持两种截图方法: - * 1. screencap PNG 方法(推荐):速度快,直接获取 PNG 图像 - * 2. screencap raw 方法:获取原始像素数据,适用于某些不支持 PNG 的设备 - * - * Args: - * params: ADB 截图参数 - * - adb_path: ADB 可执行文件的路径 - * - serial: 设备序列号,格式如 "127.0.0.1:5555" 或 "emulator-5554" - * - use_screencap: 是否使用 screencap PNG 方法(默认True) - * - * Returns: - * ADBScreenshotOut: 包含Base64编码的截图和设备信息 - * @param requestBody - * @returns ADBScreenshotOut Successful Response - * @throws ApiError - */ - public static getScreenshotAdbApiOcrScreenshotAdbPost( - requestBody: ADBScreenshotIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/screenshot/adb', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查是否存在指定图像 - * 截图并查找是否存在图片内的内容 - * - * Args: - * params: 检查图像参数 - * - window_title: 窗口标题关键字 - * - image_path: 要查找的图片路径 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * CheckImageOut: 包含查找结果和尝试次数 - * @param requestBody - * @returns CheckImageOut Successful Response - * @throws ApiError - */ - public static checkImageApiOcrCheckImagePost( - requestBody: CheckImageIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/check/image', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查是否存在任意一个指定图像 - * 截图并查找是否存在列表中任意一张图片的内容 - * - * Args: - * params: 检查图像参数 - * - window_title: 窗口标题关键字 - * - image_paths: 要查找的图片路径列表 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * CheckImageOut: 包含查找结果和尝试次数 - * @param requestBody - * @returns CheckImageOut Successful Response - * @throws ApiError - */ - public static checkImageAnyApiOcrCheckImageAnyPost( - requestBody: CheckImageAnyIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/check/image/any', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查是否存在所有指定图像 - * 截图并查找是否存在列表中所有图片的内容 - * - * Args: - * params: 检查图像参数 - * - window_title: 窗口标题关键字 - * - image_paths: 要查找的图片路径列表 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * CheckImageOut: 包含查找结果和尝试次数 - * @param requestBody - * @returns CheckImageOut Successful Response - * @throws ApiError - */ - public static checkImageAllApiOcrCheckImageAllPost( - requestBody: CheckImageAllIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/check/image/all', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 点击指定图像位置 - * 截图、查找并点击与图像一致的位置 - * - * Args: - * params: 点击图像参数 - * - window_title: 窗口标题关键字 - * - image_path: 要查找并点击的图片路径 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - threshold: 图像匹配阈值,范围 0-1,默认 0.8 - * - * Returns: - * ClickOut: 包含点击结果和尝试次数 - * @param requestBody - * @returns ClickOut Successful Response - * @throws ApiError - */ - public static clickImageApiOcrClickImagePost( - requestBody: ClickImageIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/click/image', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 点击指定文字位置 - * 截图、OCR识别并点击与文字一致的位置 - * - * Args: - * params: 点击文字参数 - * - window_title: 窗口标题关键字 - * - text: 要查找并点击的文字内容 - * - interval: 截图间隔时间(秒),默认为 0 - * - retry_times: 重复截图次数,默认为 1 - * - * Returns: - * ClickOut: 包含点击结果和尝试次数 - * @param requestBody - * @returns ClickOut Successful Response - * @throws ApiError - */ - public static clickTextApiOcrClickTextPost( - requestBody: ClickTextIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ocr/click/text', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/frontend/src/api/services/Service.ts b/frontend/src/api/services/Service.ts deleted file mode 100644 index 7507e6fc..00000000 --- a/frontend/src/api/services/Service.ts +++ /dev/null @@ -1,1489 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ComboBoxOut } from '../models/ComboBoxOut'; -import type { DispatchIn } from '../models/DispatchIn'; -import type { EmulatorCreateOut } from '../models/EmulatorCreateOut'; -import type { EmulatorDeleteIn } from '../models/EmulatorDeleteIn'; -import type { EmulatorGetIn } from '../models/EmulatorGetIn'; -import type { EmulatorGetOut } from '../models/EmulatorGetOut'; -import type { EmulatorOperateIn } from '../models/EmulatorOperateIn'; -import type { EmulatorReorderIn } from '../models/EmulatorReorderIn'; -import type { EmulatorSearchOut } from '../models/EmulatorSearchOut'; -import type { EmulatorStatusOut } from '../models/EmulatorStatusOut'; -import type { EmulatorUpdateIn } from '../models/EmulatorUpdateIn'; -import type { GetStageIn } from '../models/GetStageIn'; -import type { HistoryDataGetIn } from '../models/HistoryDataGetIn'; -import type { HistoryDataGetOut } from '../models/HistoryDataGetOut'; -import type { HistorySearchIn } from '../models/HistorySearchIn'; -import type { HistorySearchOut } from '../models/HistorySearchOut'; -import type { InfoOut } from '../models/InfoOut'; -import type { NoticeOut } from '../models/NoticeOut'; -import type { OutBase } from '../models/OutBase'; -import type { PlanCreateIn } from '../models/PlanCreateIn'; -import type { PlanCreateOut } from '../models/PlanCreateOut'; -import type { PlanDeleteIn } from '../models/PlanDeleteIn'; -import type { PlanGetIn } from '../models/PlanGetIn'; -import type { PlanGetOut } from '../models/PlanGetOut'; -import type { PlanReorderIn } from '../models/PlanReorderIn'; -import type { PlanUpdateIn } from '../models/PlanUpdateIn'; -import type { PowerIn } from '../models/PowerIn'; -import type { PowerOut } from '../models/PowerOut'; -import type { QueueCreateOut } from '../models/QueueCreateOut'; -import type { QueueDeleteIn } from '../models/QueueDeleteIn'; -import type { QueueGetIn } from '../models/QueueGetIn'; -import type { QueueGetOut } from '../models/QueueGetOut'; -import type { QueueItemCreateOut } from '../models/QueueItemCreateOut'; -import type { QueueItemDeleteIn } from '../models/QueueItemDeleteIn'; -import type { QueueItemGetIn } from '../models/QueueItemGetIn'; -import type { QueueItemGetOut } from '../models/QueueItemGetOut'; -import type { QueueItemReorderIn } from '../models/QueueItemReorderIn'; -import type { QueueItemUpdateIn } from '../models/QueueItemUpdateIn'; -import type { QueueReorderIn } from '../models/QueueReorderIn'; -import type { QueueSetInBase } from '../models/QueueSetInBase'; -import type { QueueUpdateIn } from '../models/QueueUpdateIn'; -import type { ScriptCreateIn } from '../models/ScriptCreateIn'; -import type { ScriptCreateOut } from '../models/ScriptCreateOut'; -import type { ScriptDeleteIn } from '../models/ScriptDeleteIn'; -import type { ScriptFileIn } from '../models/ScriptFileIn'; -import type { ScriptGetIn } from '../models/ScriptGetIn'; -import type { ScriptGetOut } from '../models/ScriptGetOut'; -import type { ScriptReorderIn } from '../models/ScriptReorderIn'; -import type { ScriptUpdateIn } from '../models/ScriptUpdateIn'; -import type { ScriptUploadIn } from '../models/ScriptUploadIn'; -import type { ScriptUrlIn } from '../models/ScriptUrlIn'; -import type { SettingGetOut } from '../models/SettingGetOut'; -import type { SettingUpdateIn } from '../models/SettingUpdateIn'; -import type { TaskCreateIn } from '../models/TaskCreateIn'; -import type { TaskCreateOut } from '../models/TaskCreateOut'; -import type { TimeSetCreateOut } from '../models/TimeSetCreateOut'; -import type { TimeSetDeleteIn } from '../models/TimeSetDeleteIn'; -import type { TimeSetGetIn } from '../models/TimeSetGetIn'; -import type { TimeSetGetOut } from '../models/TimeSetGetOut'; -import type { TimeSetReorderIn } from '../models/TimeSetReorderIn'; -import type { TimeSetUpdateIn } from '../models/TimeSetUpdateIn'; -import type { ToolsGetOut } from '../models/ToolsGetOut'; -import type { ToolsUpdateIn } from '../models/ToolsUpdateIn'; -import type { UpdateCheckIn } from '../models/UpdateCheckIn'; -import type { UpdateCheckOut } from '../models/UpdateCheckOut'; -import type { UserCreateOut } from '../models/UserCreateOut'; -import type { UserDeleteIn } from '../models/UserDeleteIn'; -import type { UserGetIn } from '../models/UserGetIn'; -import type { UserGetOut } from '../models/UserGetOut'; -import type { UserInBase } from '../models/UserInBase'; -import type { UserReorderIn } from '../models/UserReorderIn'; -import type { UserSetIn } from '../models/UserSetIn'; -import type { UserUpdateIn } from '../models/UserUpdateIn'; -import type { VersionOut } from '../models/VersionOut'; -import type { WebhookCreateOut } from '../models/WebhookCreateOut'; -import type { WebhookDeleteIn } from '../models/WebhookDeleteIn'; -import type { WebhookGetIn } from '../models/WebhookGetIn'; -import type { WebhookGetOut } from '../models/WebhookGetOut'; -import type { WebhookInBase } from '../models/WebhookInBase'; -import type { WebhookReorderIn } from '../models/WebhookReorderIn'; -import type { WebhookTestIn } from '../models/WebhookTestIn'; -import type { WebhookUpdateIn } from '../models/WebhookUpdateIn'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class Service { - /** - * 关闭后端程序 - * 关闭后端程序 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static closeApiCoreClosePost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/core/close', - }); - } - /** - * 获取后端git版本信息 - * @returns VersionOut Successful Response - * @throws ApiError - */ - public static getGitVersionApiInfoVersionPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/version', - }); - } - /** - * 获取关卡号下拉框信息 - * @param requestBody - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getStageComboxApiInfoComboxStagePost( - requestBody: GetStageIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/stage', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取脚本下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getScriptComboxApiInfoComboxScriptPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/script', - }); - } - /** - * 获取可选任务下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getTaskComboxApiInfoComboxTaskPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/task', - }); - } - /** - * 获取可选计划下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getPlanComboxApiInfoComboxPlanPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/plan', - }); - } - /** - * 获取可选模拟器下拉框信息 - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getEmulatorComboxApiInfoComboxEmulatorPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/emulator', - }); - } - /** - * 获取可选模拟器多开实例下拉框信息 - * @param requestBody - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost( - requestBody: EmulatorDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/combox/emulator/devices', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取通知信息 - * @returns NoticeOut Successful Response - * @throws ApiError - */ - public static getNoticeInfoApiInfoNoticeGetPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/notice/get', - }); - } - /** - * 确认通知 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static confirmNoticeApiInfoNoticeConfirmPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/notice/confirm', - }); - } - /** - * 获取配置分享中心的配置信息 - * @returns InfoOut Successful Response - * @throws ApiError - */ - public static getWebConfigApiInfoWebconfigPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/webconfig', - }); - } - /** - * 信息总览 - * @returns InfoOut Successful Response - * @throws ApiError - */ - public static getOverviewApiInfoGetOverviewPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/info/get/overview', - }); - } - /** - * 添加脚本 - * @param requestBody - * @returns ScriptCreateOut Successful Response - * @throws ApiError - */ - public static addScriptApiScriptsAddPost( - requestBody: ScriptCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询脚本配置信息 - * @param requestBody - * @returns ScriptGetOut Successful Response - * @throws ApiError - */ - public static getScriptApiScriptsGetPost( - requestBody: ScriptGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新脚本配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateScriptApiScriptsUpdatePost( - requestBody: ScriptUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除脚本 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteScriptApiScriptsDeletePost( - requestBody: ScriptDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序脚本 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderScriptApiScriptsOrderPost( - requestBody: ScriptReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 从文件加载脚本配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static importScriptFromFileApiScriptsImportFilePost( - requestBody: ScriptFileIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/import/file', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 导出脚本配置到文件 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static exportScriptToFileApiScriptsExportFilePost( - requestBody: ScriptFileIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/export/file', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 从网络加载脚本配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static importScriptFromWebApiScriptsImportWebPost( - requestBody: ScriptUrlIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/import/web', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 上传脚本配置到网络 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static uploadScriptToWebApiScriptsUploadWebPost( - requestBody: ScriptUploadIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/Upload/web', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询用户 - * @param requestBody - * @returns UserGetOut Successful Response - * @throws ApiError - */ - public static getUserApiScriptsUserGetPost( - requestBody: UserGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加用户 - * @param requestBody - * @returns UserCreateOut Successful Response - * @throws ApiError - */ - public static addUserApiScriptsUserAddPost( - requestBody: UserInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新用户配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateUserApiScriptsUserUpdatePost( - requestBody: UserUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除用户 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteUserApiScriptsUserDeletePost( - requestBody: UserDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序用户 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderUserApiScriptsUserOrderPost( - requestBody: UserReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 导入基建配置文件 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static importInfrastructureApiScriptsUserInfrastructurePost( - requestBody: UserSetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/infrastructure', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 用户自定义基建排班可选项 - * @param requestBody - * @returns ComboBoxOut Successful Response - * @throws ApiError - */ - public static getUserComboxInfrastructureApiScriptsUserComboxInfrastructurePost( - requestBody: UserDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/combox/infrastructure', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询 webhook 配置 - * @param requestBody - * @returns WebhookGetOut Successful Response - * @throws ApiError - */ - public static getWebhookApiScriptsWebhookGetPost( - requestBody: WebhookGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加webhook项 - * @param requestBody - * @returns WebhookCreateOut Successful Response - * @throws ApiError - */ - public static addWebhookApiScriptsWebhookAddPost( - requestBody: WebhookInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateWebhookApiScriptsWebhookUpdatePost( - requestBody: WebhookUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteWebhookApiScriptsWebhookDeletePost( - requestBody: WebhookDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderWebhookApiScriptsWebhookOrderPost( - requestBody: WebhookReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加计划表 - * @param requestBody - * @returns PlanCreateOut Successful Response - * @throws ApiError - */ - public static addPlanApiPlanAddPost( - requestBody: PlanCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询计划表 - * @param requestBody - * @returns PlanGetOut Successful Response - * @throws ApiError - */ - public static getPlanApiPlanGetPost( - requestBody: PlanGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新计划表配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updatePlanApiPlanUpdatePost( - requestBody: PlanUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除计划表 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deletePlanApiPlanDeletePost( - requestBody: PlanDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序计划表 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderPlanApiPlanOrderPost( - requestBody: PlanReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询模拟器配置 - * @param requestBody - * @returns EmulatorGetOut Successful Response - * @throws ApiError - */ - public static getEmulatorApiEmulatorGetPost( - requestBody: EmulatorGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加模拟器项 - * @returns EmulatorCreateOut Successful Response - * @throws ApiError - */ - public static addEmulatorApiEmulatorAddPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/add', - }); - } - /** - * 更新模拟器项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateEmulatorApiEmulatorUpdatePost( - requestBody: EmulatorUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除模拟器项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteEmulatorApiEmulatorDeletePost( - requestBody: EmulatorDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序模拟器项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderEmulatorApiEmulatorOrderPost( - requestBody: EmulatorReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 操作模拟器 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static operationEmulatorApiEmulatorOperatePost( - requestBody: EmulatorOperateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/operate', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询模拟器状态 - * @param requestBody - * @returns EmulatorStatusOut Successful Response - * @throws ApiError - */ - public static getStatusApiEmulatorStatusPost( - requestBody: EmulatorGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/status', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 搜索已安装的模拟器 - * 自动搜索系统中已安装的模拟器 - * @returns EmulatorSearchOut Successful Response - * @throws ApiError - */ - public static searchEmulatorsApiEmulatorEmulatorSearchPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/emulator/search', - }); - } - /** - * 添加调度队列 - * @returns QueueCreateOut Successful Response - * @throws ApiError - */ - public static addQueueApiQueueAddPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/add', - }); - } - /** - * 查询调度队列配置信息 - * @param requestBody - * @returns QueueGetOut Successful Response - * @throws ApiError - */ - public static getQueuesApiQueueGetPost( - requestBody: QueueGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新调度队列配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateQueueApiQueueUpdatePost( - requestBody: QueueUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除调度队列 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteQueueApiQueueDeletePost( - requestBody: QueueDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderQueueApiQueueOrderPost( - requestBody: QueueReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询定时项 - * @param requestBody - * @returns TimeSetGetOut Successful Response - * @throws ApiError - */ - public static getTimeSetApiQueueTimeGetPost( - requestBody: TimeSetGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加定时项 - * @param requestBody - * @returns TimeSetCreateOut Successful Response - * @throws ApiError - */ - public static addTimeSetApiQueueTimeAddPost( - requestBody: QueueSetInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新定时项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateTimeSetApiQueueTimeUpdatePost( - requestBody: TimeSetUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除定时项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteTimeSetApiQueueTimeDeletePost( - requestBody: TimeSetDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序定时项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderTimeSetApiQueueTimeOrderPost( - requestBody: TimeSetReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询队列项 - * @param requestBody - * @returns QueueItemGetOut Successful Response - * @throws ApiError - */ - public static getItemApiQueueItemGetPost( - requestBody: QueueItemGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加队列项 - * @param requestBody - * @returns QueueItemCreateOut Successful Response - * @throws ApiError - */ - public static addItemApiQueueItemAddPost( - requestBody: QueueSetInBase, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/add', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新队列项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateItemApiQueueItemUpdatePost( - requestBody: QueueItemUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除队列项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteItemApiQueueItemDeletePost( - requestBody: QueueItemDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序队列项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderItemApiQueueItemOrderPost( - requestBody: QueueItemReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加任务 - * @param requestBody - * @returns TaskCreateOut Successful Response - * @throws ApiError - */ - public static addTaskApiDispatchStartPost( - requestBody: TaskCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/start', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 中止任务 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static stopTaskApiDispatchStopPost( - requestBody: DispatchIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/stop', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取电源标志 - * @returns PowerOut Successful Response - * @throws ApiError - */ - public static getPowerApiDispatchGetPowerPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/get/power', - }); - } - /** - * 设置电源标志 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static setPowerApiDispatchSetPowerPost( - requestBody: PowerIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/set/power', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 取消电源任务 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static cancelPowerTaskApiDispatchCancelPowerPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/dispatch/cancel/power', - }); - } - /** - * 搜索历史记录总览信息 - * @param requestBody - * @returns HistorySearchOut Successful Response - * @throws ApiError - */ - public static searchHistoryApiHistorySearchPost( - requestBody: HistorySearchIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/history/search', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 从指定文件内获取历史记录数据 - * @param requestBody - * @returns HistoryDataGetOut Successful Response - * @throws ApiError - */ - public static getHistoryDataApiHistoryDataPost( - requestBody: HistoryDataGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/history/data', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询工具配置 - * 查询工具配置 - * @returns ToolsGetOut Successful Response - * @throws ApiError - */ - public static getToolsApiToolsGetPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/tools/get', - }); - } - /** - * 更新工具配置 - * 更新工具配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateToolsApiToolsUpdatePost( - requestBody: ToolsUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/tools/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 查询配置 - * 查询配置 - * @returns SettingGetOut Successful Response - * @throws ApiError - */ - public static getScriptsApiSettingGetPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/get', - }); - } - /** - * 更新配置 - * 更新配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateScriptApiSettingUpdatePost( - requestBody: SettingUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 测试通知 - * 测试通知 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static testNotifyApiSettingTestNotifyPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/test_notify', - }); - } - /** - * 查询 webhook 配置 - * @param requestBody - * @returns WebhookGetOut Successful Response - * @throws ApiError - */ - public static getWebhookApiSettingWebhookGetPost( - requestBody: WebhookGetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/get', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 添加webhook项 - * @returns WebhookCreateOut Successful Response - * @throws ApiError - */ - public static addWebhookApiSettingWebhookAddPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/add', - }); - } - /** - * 更新webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateWebhookApiSettingWebhookUpdatePost( - requestBody: WebhookUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static deleteWebhookApiSettingWebhookDeletePost( - requestBody: WebhookDeleteIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/delete', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderWebhookApiSettingWebhookOrderPost( - requestBody: WebhookReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 测试Webhook配置 - * 测试自定义Webhook - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static testWebhookApiSettingWebhookTestPost( - requestBody: WebhookTestIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/test', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 检查更新 - * @param requestBody - * @returns UpdateCheckOut Successful Response - * @throws ApiError - */ - public static checkUpdateApiUpdateCheckPost( - requestBody: UpdateCheckIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/update/check', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 下载更新 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static downloadUpdateApiUpdateDownloadPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/update/download', - }); - } - /** - * 安装更新 - * @returns OutBase Successful Response - * @throws ApiError - */ - public static installUpdateApiUpdateInstallPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/update/install', - }); - } -} diff --git a/frontend/src/api/services/UpdateService.ts b/frontend/src/api/services/UpdateService.ts deleted file mode 100644 index 493bfb80..00000000 --- a/frontend/src/api/services/UpdateService.ts +++ /dev/null @@ -1,470 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EmulatorReorderIn } from '../models/EmulatorReorderIn'; -import type { EmulatorUpdateIn } from '../models/EmulatorUpdateIn'; -import type { OutBase } from '../models/OutBase'; -import type { PlanReorderIn } from '../models/PlanReorderIn'; -import type { PlanUpdateIn } from '../models/PlanUpdateIn'; -import type { QueueItemReorderIn } from '../models/QueueItemReorderIn'; -import type { QueueItemUpdateIn } from '../models/QueueItemUpdateIn'; -import type { QueueReorderIn } from '../models/QueueReorderIn'; -import type { QueueUpdateIn } from '../models/QueueUpdateIn'; -import type { ScriptFileIn } from '../models/ScriptFileIn'; -import type { ScriptReorderIn } from '../models/ScriptReorderIn'; -import type { ScriptUpdateIn } from '../models/ScriptUpdateIn'; -import type { ScriptUrlIn } from '../models/ScriptUrlIn'; -import type { SettingUpdateIn } from '../models/SettingUpdateIn'; -import type { TimeSetReorderIn } from '../models/TimeSetReorderIn'; -import type { TimeSetUpdateIn } from '../models/TimeSetUpdateIn'; -import type { ToolsUpdateIn } from '../models/ToolsUpdateIn'; -import type { UserReorderIn } from '../models/UserReorderIn'; -import type { UserSetIn } from '../models/UserSetIn'; -import type { UserUpdateIn } from '../models/UserUpdateIn'; -import type { WebhookReorderIn } from '../models/WebhookReorderIn'; -import type { WebhookUpdateIn } from '../models/WebhookUpdateIn'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class UpdateService { - /** - * 更新脚本配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateScriptApiScriptsUpdatePost( - requestBody: ScriptUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序脚本 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderScriptApiScriptsOrderPost( - requestBody: ScriptReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 从文件加载脚本配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static importScriptFromFileApiScriptsImportFilePost( - requestBody: ScriptFileIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/import/file', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 从网络加载脚本配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static importScriptFromWebApiScriptsImportWebPost( - requestBody: ScriptUrlIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/import/web', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新用户配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateUserApiScriptsUserUpdatePost( - requestBody: UserUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序用户 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderUserApiScriptsUserOrderPost( - requestBody: UserReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 导入基建配置文件 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static importInfrastructureApiScriptsUserInfrastructurePost( - requestBody: UserSetIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/user/infrastructure', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateWebhookApiScriptsWebhookUpdatePost( - requestBody: WebhookUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderWebhookApiScriptsWebhookOrderPost( - requestBody: WebhookReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/scripts/webhook/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新计划表配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updatePlanApiPlanUpdatePost( - requestBody: PlanUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序计划表 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderPlanApiPlanOrderPost( - requestBody: PlanReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/plan/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新模拟器项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateEmulatorApiEmulatorUpdatePost( - requestBody: EmulatorUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序模拟器项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderEmulatorApiEmulatorOrderPost( - requestBody: EmulatorReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/emulator/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新调度队列配置信息 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateQueueApiQueueUpdatePost( - requestBody: QueueUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderQueueApiQueueOrderPost( - requestBody: QueueReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新定时项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateTimeSetApiQueueTimeUpdatePost( - requestBody: TimeSetUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序定时项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderTimeSetApiQueueTimeOrderPost( - requestBody: TimeSetReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/time/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新队列项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateItemApiQueueItemUpdatePost( - requestBody: QueueItemUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序队列项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderItemApiQueueItemOrderPost( - requestBody: QueueItemReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/queue/item/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新工具配置 - * 更新工具配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateToolsApiToolsUpdatePost( - requestBody: ToolsUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/tools/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新配置 - * 更新配置 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateScriptApiSettingUpdatePost( - requestBody: SettingUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 更新webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static updateWebhookApiSettingWebhookUpdatePost( - requestBody: WebhookUpdateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/update', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 重新排序webhook项 - * @param requestBody - * @returns OutBase Successful Response - * @throws ApiError - */ - public static reorderWebhookApiSettingWebhookOrderPost( - requestBody: WebhookReorderIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/setting/webhook/order', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/frontend/src/api/services/WebSocketService.ts b/frontend/src/api/services/WebSocketService.ts deleted file mode 100644 index a9e1704d..00000000 --- a/frontend/src/api/services/WebSocketService.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { WSClearHistoryIn } from '../models/WSClearHistoryIn'; -import type { WSClientAuthIn } from '../models/WSClientAuthIn'; -import type { WSClientConnectIn } from '../models/WSClientConnectIn'; -import type { WSClientCreateIn } from '../models/WSClientCreateIn'; -import type { WSClientCreateOut } from '../models/WSClientCreateOut'; -import type { WSClientDisconnectIn } from '../models/WSClientDisconnectIn'; -import type { WSClientListOut } from '../models/WSClientListOut'; -import type { WSClientRemoveIn } from '../models/WSClientRemoveIn'; -import type { WSClientSendIn } from '../models/WSClientSendIn'; -import type { WSClientSendJsonIn } from '../models/WSClientSendJsonIn'; -import type { WSClientStatusIn } from '../models/WSClientStatusIn'; -import type { WSClientStatusOut } from '../models/WSClientStatusOut'; -import type { WSCommandsOut } from '../models/WSCommandsOut'; -import type { WSMessageHistoryOut } from '../models/WSMessageHistoryOut'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class WebSocketService { - /** - * 创建 WebSocket 客户端 - * 创建一个新的 WebSocket 客户端实例 - * - * - **name**: 客户端唯一名称 - * - **url**: WebSocket 服务器地址 - * - **ping_interval**: 心跳发送间隔 - * - **ping_timeout**: 心跳超时时间 - * - **reconnect_interval**: 重连间隔 - * - **max_reconnect_attempts**: 最大重连次数 - * @param requestBody - * @returns WSClientCreateOut Successful Response - * @throws ApiError - */ - public static createClientApiWsDebugClientCreatePost( - requestBody: WSClientCreateIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/client/create', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 连接 WebSocket 客户端 - * 启动指定客户端的连接(非阻塞) - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static connectClientApiWsDebugClientConnectPost( - requestBody: WSClientConnectIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/client/connect', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 断开 WebSocket 客户端 - * 断开指定客户端的连接 - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static disconnectClientApiWsDebugClientDisconnectPost( - requestBody: WSClientDisconnectIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/client/disconnect', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 删除 WebSocket 客户端 - * 删除指定客户端(会自动断开连接) - * - * 注意:系统客户端(如 Koishi)不可删除 - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static removeClientApiWsDebugClientRemovePost( - requestBody: WSClientRemoveIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/client/remove', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取客户端状态 - * 获取指定客户端的状态信息 - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static getClientStatusApiWsDebugClientStatusPost( - requestBody: WSClientStatusIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/client/status', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 列出所有客户端 - * 获取所有已创建的 WebSocket 客户端列表及状态 - * @returns WSClientListOut Successful Response - * @throws ApiError - */ - public static listClientsApiWsDebugClientListGet(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/ws_debug/client/list', - }); - } - /** - * 发送原始消息 - * 发送原始 JSON 消息到指定客户端连接的服务器 - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static sendMessageApiWsDebugMessageSendPost( - requestBody: WSClientSendIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/message/send', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 发送格式化消息 - * 发送格式化的 JSON 消息(自动组装 id、type、data 结构) - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static sendJsonMessageApiWsDebugMessageSendJsonPost( - requestBody: WSClientSendJsonIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/message/send_json', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 发送认证消息 - * 发送认证消息到服务器 - * - * - **name**: 客户端名称 - * - **token**: 认证 Token - * - **auth_type**: 认证消息类型,默认 "auth" - * - **extra_data**: 额外的认证数据 - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static sendAuthApiWsDebugMessageAuthPost( - requestBody: WSClientAuthIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/message/auth', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取消息历史 - * 获取消息历史记录 - * - * - **name**: 客户端名称,为空则获取所有客户端的历史 - * @param name - * @returns WSMessageHistoryOut Successful Response - * @throws ApiError - */ - public static getHistoryApiWsDebugHistoryGet( - name?: (string | null), - ): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/ws_debug/history', - query: { - 'name': name, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 清空消息历史 - * 清空消息历史记录 - * - * - **name**: 客户端名称,为空则清空所有 - * @param requestBody - * @returns WSClientStatusOut Successful Response - * @throws ApiError - */ - public static clearHistoryApiWsDebugHistoryClearPost( - requestBody: WSClearHistoryIn, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/ws_debug/history/clear', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 获取可用 WS 命令 - * 获取所有已注册的 WebSocket 命令端点 - * @returns WSCommandsOut Successful Response - * @throws ApiError - */ - public static getCommandsApiWsDebugCommandsGet(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/ws_debug/commands', - }); - } -} diff --git a/frontend/src/components/GlobalPowerCountdown.vue b/frontend/src/components/GlobalPowerCountdown.vue index fc21f51a..3f7653ba 100644 --- a/frontend/src/components/GlobalPowerCountdown.vue +++ b/frontend/src/components/GlobalPowerCountdown.vue @@ -1,7 +1,17 @@ - - +
示例:{{ logTimeFormatPreview }}
- 提示:%f 同时支持 3 位毫秒(如 123)和 6 位微秒(如 123456),会按日志中的位数自动识别。 + 提示:%f 同时支持 3 位毫秒(如 123)和 6 位微秒(如 + 123456),会按日志中的位数自动识别。
@@ -354,15 +438,21 @@ - + @@ -375,8 +465,13 @@ - + @@ -398,8 +493,11 @@ - + @@ -415,7 +513,11 @@ - + 模拟器 PC客户端 URL协议(如Starward) @@ -434,8 +536,13 @@ - + - - + + {{ item.label }} @@ -476,8 +592,12 @@ - + @@ -485,10 +605,13 @@ - + @blur="handleChange('Game', 'EmulatorIndex', generalConfig.Game.EmulatorIndex)" + /> - - + + {{ item.label }} @@ -527,8 +665,13 @@ - + @@ -541,9 +684,15 @@ - + @@ -556,8 +705,11 @@ - + @@ -571,15 +723,22 @@ - + @@ -592,16 +751,24 @@ - + @@ -614,9 +781,15 @@ - + @@ -629,9 +802,15 @@ - + @@ -641,29 +820,61 @@ - - + + - + - + - + @@ -678,8 +889,7 @@ import type { FormInstance } from 'ant-design-vue' import { message } from 'ant-design-vue' import type { GeneralScriptConfig, ScriptType } from '../../../types/script.ts' import { useScriptApi } from '../../../composables/useScriptApi.ts' -import { Service, type ComboBoxItem } from '../../../api' -import type { ScriptUploadIn } from '../../../api' +import { infoApi, scriptApi, type ComboBoxItem, type ScriptUploadBody } from '@/api' import { ArrowLeftOutlined, CloudUploadOutlined, @@ -1342,7 +1552,7 @@ const handleCancel = () => { const loadEmulatorOptions = async () => { emulatorLoading.value = true try { - const response = await Service.getEmulatorComboxApiInfoComboxEmulatorPost() + const response = await infoApi.getEmulatorOptions() if (response && response.code === 200) { emulatorOptions.value = response.data || [] } else { @@ -1362,7 +1572,7 @@ const loadEmulatorDeviceOptions = async (emulatorId: string) => { emulatorDeviceLoading.value = true try { - const response = await Service.getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost({ + const response = await infoApi.getEmulatorDeviceOptions({ emulatorId: emulatorId, }) if (response && response.code === 200) { @@ -1525,10 +1735,7 @@ const selectRootPath = async () => { if (generalConfig.Script.LogPath && generalConfig.Script.LogPath !== '.') { scriptPathUpdates.LogPath = generalConfig.Script.LogPath } - if ( - generalConfig.Script.TrackProcessExe && - generalConfig.Script.TrackProcessExe !== '.' - ) { + if (generalConfig.Script.TrackProcessExe && generalConfig.Script.TrackProcessExe !== '.') { scriptPathUpdates.TrackProcessExe = generalConfig.Script.TrackProcessExe } @@ -1766,7 +1973,7 @@ const handleUpload = async () => { uploadLoading.value = true // 构建上传数据 - const uploadData: ScriptUploadIn = { + const uploadData: ScriptUploadBody = { scriptId: scriptId, config_name: uploadForm.config_name, author: uploadForm.author, @@ -1774,7 +1981,7 @@ const handleUpload = async () => { } // 调用上传API - await Service.uploadScriptToWebApiScriptsUploadWebPost(uploadData) + await scriptApi.uploadTemplateToWeb(uploadData) message.success('脚本配置上传成功,等待审核通过后即可向所有用户展示~') uploadModalVisible.value = false @@ -2213,7 +2420,9 @@ const handleUpload = async () => { .format-preview-value { color: var(--ant-color-text); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; } .format-preview-tip { diff --git a/frontend/src/views/EditView/Script/MAAScriptEdit.vue b/frontend/src/views/EditView/Script/MAAScriptEdit.vue index bb8ef9c5..602c523b 100644 --- a/frontend/src/views/EditView/Script/MAAScriptEdit.vue +++ b/frontend/src/views/EditView/Script/MAAScriptEdit.vue @@ -1,4 +1,4 @@ - - + - - + + {{ item.label }} @@ -104,7 +123,12 @@ - + - - + + {{ item.label }} @@ -144,8 +187,11 @@ - + 重启模拟器 重启明日方舟 直接切换账号 @@ -155,31 +201,43 @@ - + - + @@ -197,9 +255,21 @@ - + @@ -212,9 +282,15 @@ - + @@ -227,14 +303,19 @@ - + - @@ -247,7 +328,7 @@ import type { FormInstance } from 'ant-design-vue' import { message } from 'ant-design-vue' import type { MAAScriptConfig, ScriptType } from '../../../types/script.ts' import { useScriptApi } from '../../../composables/useScriptApi.ts' -import { Service, type ComboBoxItem } from '../../../api' +import { infoApi, type ComboBoxItem } from '@/api' import { ArrowLeftOutlined, FolderOpenOutlined, @@ -421,7 +502,7 @@ const handleCancel = () => { const loadEmulatorOptions = async () => { emulatorLoading.value = true try { - const response = await Service.getEmulatorComboxApiInfoComboxEmulatorPost() + const response = await infoApi.getEmulatorOptions() if (response && response.code === 200) { emulatorOptions.value = response.data || [] } else { @@ -441,8 +522,8 @@ const loadEmulatorDeviceOptions = async (emulatorId: string) => { emulatorDeviceLoading.value = true try { - const response = await Service.getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost({ - emulatorId: emulatorId + const response = await infoApi.getEmulatorDeviceOptions({ + emulatorId: emulatorId, }) if (response && response.code === 200) { emulatorDeviceOptions.value = response.data || [] @@ -469,8 +550,8 @@ const handleEmulatorSelectChange = async (emulatorId: string) => { const updateData = { Emulator: { Id: emulatorId, - Index: '' - } + Index: '', + }, } const success = await updateScript(scriptId, updateData) if (success) { diff --git a/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue b/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue index 4258931e..eefa75ea 100644 --- a/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue +++ b/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue @@ -1,4 +1,4 @@ - - + - + @@ -105,8 +119,12 @@ - + @@ -123,8 +141,13 @@ - + - + @@ -158,8 +186,14 @@ - + @@ -175,9 +209,18 @@ - - + + {{ item.label }} @@ -189,18 +232,38 @@ 模拟器实例 + :title=" + emulatorDeviceOptions.length === 0 && !emulatorDeviceLoading + ? '不支持自动扫描实例的模拟器,请手动输入实例信息' + : '选择模拟器的具体实例' + " + > - - - + + + {{ item.label }} @@ -219,14 +282,21 @@ - + @blur="handleChange('Run', 'ProxyTimesLimit', maaEndConfig.Run.ProxyTimesLimit)" + /> @@ -239,8 +309,14 @@ - + @@ -253,8 +329,14 @@ - + @@ -269,8 +351,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import type { FormInstance } from 'ant-design-vue' import { message } from 'ant-design-vue' -import type { ComboBoxItem } from '@/api' -import { Service } from '@/api' +import { infoApi, type ComboBoxItem } from '@/api' import type { MaaEndScriptConfig, ScriptType } from '@/types/script' import { useScriptApi } from '@/composables/useScriptApi' import { @@ -377,7 +458,7 @@ const refreshScript = async () => { const loadEmulatorOptions = async () => { emulatorLoading.value = true try { - const response = await Service.getEmulatorComboxApiInfoComboxEmulatorPost() + const response = await infoApi.getEmulatorOptions() if (response.code === 200) { emulatorOptions.value = response.data || [] } @@ -391,7 +472,7 @@ const loadEmulatorDeviceOptions = async (emulatorId: string) => { emulatorDeviceLoading.value = true try { - const response = await Service.getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost({ + const response = await infoApi.getEmulatorDeviceOptions({ emulatorId, }) if (response.code === 200) { @@ -437,16 +518,16 @@ const handleControllerTypeChange = async (value: MaaEndScriptConfig['Game']['Con const gamePayload = value === 'ADB' ? { - ControllerType: value, - Path: '', - Arguments: '', - WaitTime: 15, - } + ControllerType: value, + Path: '', + Arguments: '', + WaitTime: 15, + } : { - ControllerType: value, - EmulatorId: '', - EmulatorIndex: '', - } + ControllerType: value, + EmulatorId: '', + EmulatorIndex: '', + } if (value !== 'ADB') { emulatorDeviceOptions.value = [] diff --git a/frontend/src/views/EditView/Script/SRCScriptEdit.vue b/frontend/src/views/EditView/Script/SRCScriptEdit.vue index 3d764f85..273d2b1e 100644 --- a/frontend/src/views/EditView/Script/SRCScriptEdit.vue +++ b/frontend/src/views/EditView/Script/SRCScriptEdit.vue @@ -1,4 +1,4 @@ - - + - - + + {{ item.label }} @@ -104,7 +123,12 @@ - + - - + + {{ item.label }} @@ -144,8 +187,11 @@ - + 重启模拟器 重启游戏 @@ -154,16 +200,24 @@ - + @@ -176,9 +230,15 @@ - + @@ -193,14 +253,19 @@ - + - @@ -213,7 +278,7 @@ import type { FormInstance } from 'ant-design-vue' import { message } from 'ant-design-vue' import type { SRCScriptConfig, ScriptType } from '../../../types/script.ts' import { useScriptApi } from '../../../composables/useScriptApi.ts' -import { Service, type ComboBoxItem } from '../../../api' +import { infoApi, type ComboBoxItem } from '@/api' import { ArrowLeftOutlined, FolderOpenOutlined, @@ -381,7 +446,7 @@ const handleCancel = () => { const loadEmulatorOptions = async () => { emulatorLoading.value = true try { - const response = await Service.getEmulatorComboxApiInfoComboxEmulatorPost() + const response = await infoApi.getEmulatorOptions() if (response && response.code === 200) { emulatorOptions.value = response.data || [] } else { @@ -401,8 +466,8 @@ const loadEmulatorDeviceOptions = async (emulatorId: string) => { emulatorDeviceLoading.value = true try { - const response = await Service.getEmulatorDevicesComboxApiInfoComboxEmulatorDevicesPost({ - emulatorId: emulatorId + const response = await infoApi.getEmulatorDeviceOptions({ + emulatorId: emulatorId, }) if (response && response.code === 200) { emulatorDeviceOptions.value = response.data || [] @@ -429,8 +494,8 @@ const handleEmulatorSelectChange = async (emulatorId: string) => { const updateData = { Emulator: { Id: emulatorId, - Index: '' - } + Index: '', + }, } const success = await updateScript(scriptId, updateData) if (success) { diff --git a/frontend/src/views/EditView/User/GeneralUserEdit.vue b/frontend/src/views/EditView/User/GeneralUserEdit.vue index c9a4db71..3a0c4423 100644 --- a/frontend/src/views/EditView/User/GeneralUserEdit.vue +++ b/frontend/src/views/EditView/User/GeneralUserEdit.vue @@ -1,4 +1,4 @@ - - + @@ -112,9 +137,16 @@ - + @@ -131,8 +163,14 @@ - + @@ -152,15 +190,31 @@ - + - - + + @@ -181,15 +235,31 @@ - + - - + + @@ -211,8 +281,11 @@ 启用通知 - + 启用后将发送任务通知 @@ -223,9 +296,11 @@ 通知内容 - 统计信息 + @change="handleFieldSave('Notify.IfSendStatistic', formData.Notify.IfSendStatistic)" + >统计信息 @@ -233,34 +308,55 @@ - 邮件通知 + 邮件通知 - + - Server酱 + Server酱 - +
- +
@@ -283,8 +379,7 @@ import type { FormInstance, Rule } from 'ant-design-vue/es/form' import { useUserApi } from '@/composables/useUserApi.ts' import { useScriptApi } from '@/composables/useScriptApi.ts' import { useWebSocket } from '@/composables/useWebSocket.ts' -import { Service } from '@/api' -import { TaskCreateIn } from '@/api/models/TaskCreateIn.ts' +import { TASK_CREATE_MODE, dispatchApi } from '@/api' import WebhookManager from '@/components/WebhookManager.vue' const logger = window.electronAPI.getLogger('通用用户编辑') @@ -539,7 +634,6 @@ const loadUserData = async () => { } const handleGeneralConfig = async () => { - try { generalConfigLoading.value = true @@ -560,9 +654,9 @@ const handleGeneralConfig = async () => { } // 调用后端启动任务接口,传入 userId 作为 taskId 与设置模式 - const response = await Service.addTaskApiDispatchStartPost({ + const response = await dispatchApi.startTask({ taskId: userId, - mode: TaskCreateIn.mode.SCRIPT_CONFIG, + mode: TASK_CREATE_MODE.SCRIPT_CONFIG, }) logger.debug(`通用配置 start 接口返回: ${response}`) @@ -597,7 +691,11 @@ const handleGeneralConfig = async () => { } // 处理任务结束消息(Signal类型且包含Accomplish字段) - if (wsMessage.type === 'Signal' && wsMessage.data && wsMessage.data.Accomplish !== undefined) { + if ( + wsMessage.type === 'Signal' && + wsMessage.data && + wsMessage.data.Accomplish !== undefined + ) { logger.info(`用户 ${formData.userName} 通用配置任务已结束`) // 根据结果显示不同消息 const result = wsMessage.data.Accomplish @@ -628,12 +726,14 @@ const handleGeneralConfig = async () => { async () => { if (generalSubscriptionId.value && generalWebsocketId.value) { // 超时后自动保存配置 - message.warning(`用户 ${formData.userName} 的配置会话已超时(30分钟),正在自动保存配置...`) + message.warning( + `用户 ${formData.userName} 的配置会话已超时(30分钟),正在自动保存配置...` + ) logger.warn('配置会话已超时,自动执行保存操作') try { const websocketId = generalWebsocketId.value - const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId }) + const response = await dispatchApi.stopTask({ taskId: websocketId }) if (response && response.code === 200) { if (generalSubscriptionId.value) { @@ -681,7 +781,7 @@ const handleSaveGeneralConfig = async () => { return } - const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId }) + const response = await dispatchApi.stopTask({ taskId: websocketId }) if (response && response.code === 200) { if (generalSubscriptionId.value) { unsubscribe(generalSubscriptionId.value) diff --git a/frontend/src/views/EditView/User/MAAUserEdit.vue b/frontend/src/views/EditView/User/MAAUserEdit.vue index 1b695792..f39f142b 100644 --- a/frontend/src/views/EditView/User/MAAUserEdit.vue +++ b/frontend/src/views/EditView/User/MAAUserEdit.vue @@ -1,4 +1,4 @@ -