diff --git a/app/api/scripts.py b/app/api/scripts.py index 63c0c5a37..3f6a9385d 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -28,6 +28,7 @@ from fastapi import APIRouter, Body from app.core import Config +from app.models.config import OkNteConfig as RuntimeOkNteConfig from app.models.schema import * router = APIRouter(prefix="/api/scripts", tags=["脚本管理"]) @@ -48,6 +49,36 @@ def _okww_config_file_path(config_dir: Path, filename: str) -> Path: return config_dir / filename +def _oknte_script_config(script_id: str) -> tuple[uuid.UUID, RuntimeOkNteConfig]: + script_uid = uuid.UUID(script_id) + script_config = Config.ScriptConfig[script_uid] + if not isinstance(script_config, RuntimeOkNteConfig): + raise ValueError("脚本配置类型错误, 不是 OK-NTE 类型") + return script_uid, script_config + + +def _oknte_legacy_mas_config_dir(script_id: str) -> Path: + script_uid, _ = _oknte_script_config(script_id) + return Path.cwd() / "data" / str(script_uid) / "Default" / "ConfigFile" + + +def _oknte_mas_config_dir(script_id: str, user_id: str) -> Path: + script_uid, _ = _oknte_script_config(script_id) + user_uid = uuid.UUID(user_id) + return Path.cwd() / "data" / str(script_uid) / str(user_uid) / "ConfigFile" + + +def _oknte_config_file_path(config_dir: Path, filename: str) -> Path: + file_path = Path(filename) + if ( + file_path.name != filename + or file_path.is_absolute() + or ".." in file_path.parts + ): + raise ValueError("配置文件名非法") + return config_dir / filename + + SCRIPT_BOOK = { "MaaConfig": MaaConfig, "SrcConfig": SrcConfig, @@ -55,6 +86,7 @@ def _okww_config_file_path(config_dir: Path, filename: str) -> Path: "M9AConfig": M9AConfig, "GeneralConfig": GeneralConfig, "OkwwConfig": OkwwConfig, + "OkNteConfig": OkNteConfig, "HSRConfig": HSRConfig, } USER_BOOK = { @@ -64,6 +96,7 @@ def _okww_config_file_path(config_dir: Path, filename: str) -> Path: "M9AConfig": M9AUserConfig, "GeneralConfig": GeneralUserConfig, "OkwwConfig": OkwwUserConfig, + "OkNteConfig": OkNteUserConfig, "HSRConfig": HSRUserConfig, } @@ -878,3 +911,212 @@ async def batch_update_okww_configs( "status": "error", "message": f"{type(e).__name__}: {str(e)}", } + + +@router.post( + "/oknte/configs/list", + tags=["OKNTE"], + summary="获取 OK-NTE 配置文件列表及 schema", + status_code=200, +) +async def get_oknte_configs_list(script_id: str, user_id: str): + """ + 获取 OK-NTE 配置文件列表及 schema 定义。 + 读写用户配置目录(data/{script_id}/{user_id}/ConfigFile/), + 若为空则自动从 ok-nte configs 目录初始化默认配置。 + + Args: + script_id: OK-NTE 脚本 ID + user_id: 用户 ID + + Returns: + dict: 包含配置文件列表和 schema 的响应 + """ + try: + import json + import shutil + from app.task.OkNte.config_schema import ( + get_all_config_info, build_fields_for_config, load_oknte_option_labels, + ) + + _, script_config = _oknte_script_config(script_id) + + # 从 ok-nte 安装目录加载翻译 → option_labels + root_path = script_config.get("Info", "RootPath") + option_labels = load_oknte_option_labels(root_path) if root_path else {} + + # 用户配置目录;旧版 Default 目录仅作为升级后的初始化来源。 + mas_config_dir = _oknte_mas_config_dir(script_id, user_id) + + # ok-nte 源配置目录(用于自动初始化) + legacy_config_dir = _oknte_legacy_mas_config_dir(script_id) + oknte_configs_dir = ( + legacy_config_dir + if legacy_config_dir.is_dir() and any(legacy_config_dir.iterdir()) + else None + ) + if oknte_configs_dir is None: + raw_config_path = script_config.get("Script", "ConfigPath") + oknte_configs_dir = Path(raw_config_path) if raw_config_path else None + if not oknte_configs_dir or not oknte_configs_dir.exists(): + if root_path: + root = Path(root_path) + packaged_dir = root / "data" / "apps" / "ok-nte" / "working" / "configs" + source_dir = root / "configs" + oknte_configs_dir = packaged_dir if packaged_dir.is_dir() else source_dir + + # 自动初始化:用户目录为空时从旧版共享目录或 ok-nte configs 复制默认配置 + need_init = not mas_config_dir.exists() or not any(mas_config_dir.iterdir()) + if need_init and oknte_configs_dir and oknte_configs_dir.is_dir(): + mas_config_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree(oknte_configs_dir, mas_config_dir, dirs_exist_ok=True) + + configs_info = get_all_config_info() + + # 读取 per-user JSON 配置,通过 build_fields_for_config 构建字段列表 + result = [] + for info in configs_info: + filename = info["filename"] + filepath = _oknte_config_file_path(mas_config_dir, filename) + current_data: dict[str, Any] = {} + if filepath.exists(): + try: + current_data = json.loads(filepath.read_text(encoding="utf-8")) + except Exception: + pass + + fields = build_fields_for_config(filename, current_data, option_labels) + + result.append({ + **info, + "fields": fields, + "currentData": current_data, + }) + + return { + "code": 200, + "status": "success", + "message": f"共 {len(result)} 个配置文件", + "data": result, + "optionLabels": option_labels, + "configPath": str(mas_config_dir) if mas_config_dir else None, + } + except Exception as e: + return { + "code": 500, + "status": "error", + "message": f"{type(e).__name__}: {str(e)}", + "data": [], + } + + +@router.post( + "/oknte/configs/update", + tags=["OKNTE"], + summary="更新 OK-NTE 配置文件", + status_code=200, +) +async def update_oknte_config( + script_id: str = Body(...), + user_id: str = Body(...), + filename: str = Body(...), + data: dict = Body(...), +): + """ + 更新 OK-NTE 配置文件 + + Args: + script_id: OK-NTE 脚本 ID + user_id: 用户 ID + filename: 配置文件名(如 DailyTask.json) + data: 要更新的配置数据 + + Returns: + dict: 操作结果 + """ + try: + import json + + # 写入用户配置目录 + mas_config_dir = _oknte_mas_config_dir(script_id, user_id) + mas_config_dir.mkdir(parents=True, exist_ok=True) + + filepath = _oknte_config_file_path(mas_config_dir, filename) + + existing_data = {} + if filepath.exists(): + with open(filepath, "r", encoding="utf-8") as f: + existing_data = json.load(f) + + existing_data.update(data) + + with open(filepath, "w", encoding="utf-8") as f: + json.dump(existing_data, f, ensure_ascii=False, indent=4) + + return { + "code": 200, + "status": "success", + "message": f"配置文件 {filename} 已更新", + "data": existing_data, + } + except Exception as e: + return { + "code": 500, + "status": "error", + "message": f"{type(e).__name__}: {str(e)}", + } + + +@router.post( + "/oknte/configs/batch-update", + tags=["OKNTE"], + summary="批量更新 OK-NTE 配置文件", + status_code=200, +) +async def batch_update_oknte_configs( + script_id: str = Body(...), + user_id: str = Body(...), + configs: dict = Body(...), +): + """ + 批量更新 OK-NTE 配置文件 + + Args: + script_id: OK-NTE 脚本 ID + user_id: 用户 ID + configs: { filename: data } 格式的配置数据 + + Returns: + dict: 操作结果 + """ + try: + import json + + # 写入用户配置目录 + mas_config_dir = _oknte_mas_config_dir(script_id, user_id) + mas_config_dir.mkdir(parents=True, exist_ok=True) + + updated_files = [] + for filename, data in configs.items(): + filepath = _oknte_config_file_path(mas_config_dir, filename) + existing_data = {} + if filepath.exists(): + with open(filepath, "r", encoding="utf-8") as f: + existing_data = json.load(f) + existing_data.update(data) + with open(filepath, "w", encoding="utf-8") as f: + json.dump(existing_data, f, ensure_ascii=False, indent=4) + updated_files.append(filename) + + return { + "code": 200, + "status": "success", + "message": f"已更新 {len(updated_files)} 个配置文件", + "data": updated_files, + } + except Exception as e: + return { + "code": 500, + "status": "error", + "message": f"{type(e).__name__}: {str(e)}", + } diff --git a/app/core/config.py b/app/core/config.py index de6c67336..2e2606f96 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -45,6 +45,7 @@ M9AConfig, MaaEndConfig, OkwwConfig, + OkNteConfig, HSRConfig, HSRUserConfig, MaaPlanConfig, @@ -56,6 +57,7 @@ MaaEndUserConfig, GeneralUserConfig, OkwwUserConfig, + OkNteUserConfig, GlobalConfig, CLASS_BOOK, Webhook, @@ -532,11 +534,18 @@ def _get_git_info(): async def add_script( self, - script: Literal["MAA", "SRC", "General", "MaaEnd", "M9A", "Okww", "HSR"], + script: Literal["MAA", "SRC", "General", "MaaEnd", "M9A", "Okww", "OkNte", "HSR"], script_id: str | None = None, ) -> tuple[ uuid.UUID, - MaaConfig | SrcConfig | GeneralConfig | MaaEndConfig | M9AConfig | OkwwConfig | HSRConfig, + MaaConfig + | SrcConfig + | GeneralConfig + | MaaEndConfig + | M9AConfig + | OkwwConfig + | OkNteConfig + | HSRConfig, ]: """添加脚本配置""" @@ -822,6 +831,7 @@ async def add_user(self, script_id: str) -> tuple[ | MaaEndUserConfig | M9AUserConfig | OkwwUserConfig + | OkNteUserConfig | HSRUserConfig, ]: """添加用户配置""" @@ -839,6 +849,8 @@ async def add_user(self, script_id: str) -> tuple[ uid, config = await script_config.UserData.add(GeneralUserConfig) elif isinstance(script_config, OkwwConfig): uid, config = await script_config.UserData.add(OkwwUserConfig) + elif isinstance(script_config, OkNteConfig): + uid, config = await script_config.UserData.add(OkNteUserConfig) elif isinstance(script_config, MaaEndConfig): uid, config = await script_config.UserData.add(MaaEndUserConfig) elif isinstance(script_config, M9AConfig): diff --git a/app/core/task_manager.py b/app/core/task_manager.py index 4b4ee51b6..37637a536 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -32,6 +32,7 @@ MaaEndConfig, M9AConfig, OkwwConfig, + OkNteConfig, HSRConfig, ) from app.services import System @@ -44,6 +45,7 @@ MaaEndManager, M9AManager, OkwwManager, + OkNteManager, HSRManager, ) from app.utils.constants import POWER_SIGN_MAP @@ -176,6 +178,8 @@ async def main_task(self): task_item = GeneralManager(script_item) elif isinstance(Config.ScriptConfig[current_script_uid], OkwwConfig): task_item = OkwwManager(script_item) + elif isinstance(Config.ScriptConfig[current_script_uid], OkNteConfig): + task_item = OkNteManager(script_item) elif isinstance(Config.ScriptConfig[current_script_uid], MaaEndConfig): task_item = MaaEndManager(script_item) elif isinstance(Config.ScriptConfig[current_script_uid], M9AConfig): diff --git a/app/models/config.py b/app/models/config.py index f75458656..f5c19e956 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -2311,6 +2311,142 @@ def getTags(self) -> str: return json.dumps(tags, ensure_ascii=False) +class OkNteUserConfig(ConfigBase): + """OK-NTE 用户配置(ok-script 线)""" + + OKNTE_TASK_BOOK: dict[int, str] = { + 1: "启动游戏", + 2: "日常任务", + 3: "一咖舍", + 4: "钓鱼", + 5: "异象界域", + 6: "音游", + 7: "业主选拔", + 8: "粉爪大劫案", + 9: "暗域任务", + 10: "呗果智能体", + 11: "诊断", + } + + def __init__(self) -> None: + + ## Info ------------------------------------------------------------ + self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) + self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) + self.Info_Id = ConfigItem("Info", "Id", "") + self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) + self.Info_Resource = ConfigItem( + "Info", "Resource", "官服", OptionsValidator(["官服"]) + ) + self.Info_RemainedDay = ConfigItem( + "Info", "RemainedDay", -1, RangeValidator(-1, 9999) + ) + self.Info_Mode = ConfigItem( + "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) + ) + self.Info_IfScriptBeforeTask = ConfigItem( + "Info", "IfScriptBeforeTask", False, BoolValidator() + ) + self.Info_ScriptBeforeTask = ConfigItem( + "Info", "ScriptBeforeTask", "", FileValidator() + ) + self.Info_IfScriptAfterTask = ConfigItem( + "Info", "IfScriptAfterTask", False, BoolValidator() + ) + self.Info_ScriptAfterTask = ConfigItem( + "Info", "ScriptAfterTask", "", FileValidator() + ) + self.Info_Notes = ConfigItem("Info", "Notes", "无") + self.Info_Tag = ConfigItem( + "Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags) + ) + + ## Task ------------------------------------------------------------ + # ok-nte.exe -t N -e;上游 DailyTask 是 -t 2 + self.Task_TaskIndex = ConfigItem( + "Task", "TaskIndex", 2, RangeValidator(1, 11) + ) + self.Task_ExitOnFinish = ConfigItem("Task", "ExitOnFinish", True, BoolValidator()) + + ## 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_LastTaskIndex = ConfigItem( + "Data", "LastTaskIndex", 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", "") + self.Notify_IfServerChan = ConfigItem( + "Notify", "IfServerChan", False, BoolValidator() + ) + self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") + self.Notify_CustomWebhooks = MultipleConfig([Webhook]) + + super().__init__() + + def getTags(self) -> str: + tags = [] + + last_status = self.get("Data", "LastProxyStatus") + tags.append({"text": f"上次:{last_status}", "color": "green"}) + + last_task_index = int(self.get("Data", "LastTaskIndex") or 0) + task_label = self.OKNTE_TASK_BOOK.get(last_task_index, "未知") + tags.append({"text": f"任务:{task_label}", "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): """通用配置""" @@ -2530,6 +2666,102 @@ def __init__(self) -> None: super().__init__() +class OkNteConfig(ConfigBase): + """OK-NTE 配置(ok-script 线)""" + + def __init__(self) -> None: + + ## Info ------------------------------------------------------------ + self.Info_Name = ConfigItem("Info", "Name", "新 OK-NTE 脚本") + self.Info_RootPath = ConfigItem( + "Info", "RootPath", "", FileValidator() + ) + + ## Script ---------------------------------------------------------- + self.Script_ScriptPath = ConfigItem( + "Script", "ScriptPath", "", FileValidator() + ) + # OkNte 运行参数建议由用户配置(-t / -e 由用户配置 Task 决定),但仍保留高级参数入口 + self.Script_Arguments = ConfigItem( + "Script", "Arguments", "", AdvancedArgumentValidator() + ) + self.Script_IfTrackProcess = ConfigItem( + "Script", "IfTrackProcess", True, 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", "", FileValidator() + ) + self.Script_ConfigPathMode = ConfigItem( + "Script", "ConfigPathMode", "Folder", OptionsValidator(["File", "Folder"]) + ) + self.Script_UpdateConfigMode = ConfigItem( + "Script", + "UpdateConfigMode", + "Always", + OptionsValidator(["Never", "Success", "Failure", "Always"]), + ) + self.Script_LogPath = ConfigItem( + "Script", "LogPath", "", FileValidator() + ) + self.Script_LogPathFormat = ConfigItem("Script", "LogPathFormat", "") + self.Script_LogTimeStart = ConfigItem( + "Script", "LogTimeStart", 1, RangeValidator(1, 9999) + ) + self.Script_LogTimeEnd = ConfigItem( + "Script", "LogTimeEnd", 23, RangeValidator(1, 9999) + ) + self.Script_LogTimeFormat = ConfigItem( + "Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S,%f" + ) + self.Script_SuccessLog = ConfigItem( + "Script", "SuccessLog", "Successfully Executed Task|任务执行完成" + ) + self.Script_ErrorLog = ConfigItem( + "Script", + "ErrorLog", + "connected:False|Resolution Error|Timed out waiting for game process|" + "Timed out waiting for launcher process", + ) + + ## Game ------------------------------------------------------------ + self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator()) + self.Game_LaunchBeforeTask = ConfigItem( + "Game", "LaunchBeforeTask", False, BoolValidator() + ) + self.Game_Type = ConfigItem( + "Game", "Type", "Client", OptionsValidator(["Client", "URL"]) + ) + self.Game_Path = ConfigItem("Game", "Path", "", FileValidator()) + 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", 60, RangeValidator(0, 9999)) + self.Game_IfForceClose = ConfigItem("Game", "IfForceClose", True, BoolValidator()) + self.Game_CloseOnFinish = ConfigItem( + "Game", "CloseOnFinish", True, BoolValidator() + ) + + ## Run ------------------------------------------------------------- + self.Run_ProxyTimesLimit = ConfigItem( + "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999) + ) + self.Run_RunTimesLimit = ConfigItem( + "Run", "RunTimesLimit", 1, RangeValidator(1, 9999) + ) + self.Run_RunTimeLimit = ConfigItem( + "Run", "RunTimeLimit", 120, RangeValidator(1, 9999) + ) + + self.UserData = MultipleConfig([OkNteUserConfig]) + + super().__init__() + + class ToolsConfig(ConfigBase): """工具配置""" @@ -2796,7 +3028,16 @@ def __init__(self): self.PlanConfig = MultipleConfig([MaaPlanConfig]) ## 脚本配置列表 self.ScriptConfig = MultipleConfig( - [MaaConfig, MaaEndConfig, SrcConfig, M9AConfig, GeneralConfig, OkwwConfig, HSRConfig] + [ + MaaConfig, + MaaEndConfig, + SrcConfig, + M9AConfig, + GeneralConfig, + OkwwConfig, + OkNteConfig, + HSRConfig, + ] ) ## 队列配置列表 self.QueueConfig = MultipleConfig([QueueConfig]) @@ -2883,6 +3124,7 @@ def getStage(self) -> str: "M9A": M9AConfig, "General": GeneralConfig, "Okww": OkwwConfig, + "OkNte": OkNteConfig, "HSR": HSRConfig, } """配置类映射表""" diff --git a/app/models/schema.py b/app/models/schema.py index 92a12db3f..3f936aaed 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -329,6 +329,7 @@ class ScriptIndexItem(BaseModel): "MaaConfig", "GeneralConfig", "OkwwConfig", + "OkNteConfig", "SrcConfig", "MaaEndConfig", "M9AConfig", @@ -344,6 +345,7 @@ class UserIndexItem(BaseModel): "MaaUserConfig", "GeneralUserConfig", "OkwwUserConfig", + "OkNteUserConfig", "SrcUserConfig", "MaaEndUserConfig", "M9AUserConfig", @@ -552,6 +554,44 @@ class OkwwUserConfig(BaseModel): Notify: Optional[OkwwUserConfig_Notify] = Field(default=None, description="单独通知") +class OkNteUserConfig_Task(BaseModel): + TaskIndex: Optional[int] = Field(default=None, description="启动后执行第 N 个任务(-t N,从 1 开始)") + ExitOnFinish: Optional[bool] = Field(default=None, description="任务结束后退出(-e)") + + +class OkNteUserConfig_Info(GeneralUserConfig_Info): + """OK-NTE 用户信息(复用通用字段)""" + + Id: Optional[str] = Field(default=None, description="账号") + Password: Optional[str] = Field(default=None, description="密码") + Mode: Optional[Literal["简洁", "详细"]] = Field( + default=None, description="用户配置模式(简洁/详细)" + ) + Resource: Optional[Literal["官服"]] = Field(default=None, description="游戏资源") + + +class OkNteUserConfig_Data(GeneralUserConfig_Data): + """OK-NTE 用户数据(复用通用字段)""" + + LastProxyStatus: Optional[str] = Field( + default=None, description="上次代理状态(未知/成功/失败)" + ) + LastTaskIndex: Optional[int] = Field( + default=None, description="上次运行的 ok-nte 任务序号(-t N)" + ) + + +class OkNteUserConfig_Notify(GeneralUserConfig_Notify): + """OK-NTE 用户通知(复用通用字段)""" + + +class OkNteUserConfig(BaseModel): + Info: Optional[OkNteUserConfig_Info] = Field(default=None, description="用户信息") + Task: Optional[OkNteUserConfig_Task] = Field(default=None, description="任务配置") + Data: Optional[OkNteUserConfig_Data] = Field(default=None, description="用户数据") + Notify: Optional[OkNteUserConfig_Notify] = Field(default=None, description="单独通知") + + class GeneralConfig_Info(BaseModel): Name: Optional[str] = Field(default=None, description="脚本名称") RootPath: Optional[str] = Field(default=None, description="脚本根目录") @@ -678,6 +718,50 @@ class OkwwConfig(BaseModel): Run: Optional[OkwwConfig_Run] = Field(default=None, description="运行配置") +class OkNteConfig_Info(GeneralConfig_Info): + """OK-NTE 脚本基础信息(复用通用字段)""" + + +class OkNteConfig_Script(GeneralConfig_Script): + """OK-NTE 脚本配置(复用通用字段)""" + + +class OkNteConfig_Game(BaseModel): + """OK-NTE 游戏配置""" + + Enabled: Optional[bool] = Field( + default=None, description="游戏相关功能是否启用" + ) + Type: Optional[Literal["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="是否强制关闭游戏进程" + ) + LaunchBeforeTask: Optional[bool] = Field( + default=None, description="任务开始前是否由 MAS 启动游戏" + ) + CloseOnFinish: Optional[bool] = Field( + default=None, description="任务结束后是否关闭游戏" + ) + + +class OkNteConfig_Run(GeneralConfig_Run): + """OK-NTE 运行配置(复用通用字段)""" + + +class OkNteConfig(BaseModel): + Info: Optional[OkNteConfig_Info] = Field(default=None, description="脚本基础信息") + Script: Optional[OkNteConfig_Script] = Field(default=None, description="脚本配置") + Game: Optional[OkNteConfig_Game] = Field(default=None, description="游戏配置") + Run: Optional[OkNteConfig_Run] = Field(default=None, description="运行配置") + + class MaaEndUserConfig_Info(BaseModel): Name: Optional[str] = Field(default=None, description="用户名") Status: Optional[bool] = Field(default=None, description="用户状态") @@ -1371,8 +1455,8 @@ class HistoryData(BaseModel): class ScriptCreateIn(BaseModel): - type: Literal["MAA", "SRC", "General", "Okww", "MaaEnd", "M9A", "HSR"] = Field( - ..., description="脚本类型: MAA脚本, 通用脚本, OK-WW脚本, SRC脚本, MaaEnd脚本, M9A脚本, HSR脚本" + type: Literal["MAA", "SRC", "General", "Okww", "OkNte", "MaaEnd", "M9A", "HSR"] = Field( + ..., description="脚本类型: MAA脚本, 通用脚本, OK-WW脚本, OK-NTE脚本, SRC脚本, MaaEnd脚本, M9A脚本, HSR脚本" ) scriptId: str | None = Field( default=None, description="直接从该脚本ID复制创建, 仅在复制创建时使用" @@ -1381,7 +1465,16 @@ class ScriptCreateIn(BaseModel): class ScriptCreateOut(OutBase): scriptId: str = Field(..., description="新创建的脚本ID") - data: Union[MaaConfig, SrcConfig, GeneralConfig, OkwwConfig, MaaEndConfig, M9AConfig, HSRConfig] = Field( + data: Union[ + MaaConfig, + SrcConfig, + GeneralConfig, + OkwwConfig, + OkNteConfig, + MaaEndConfig, + M9AConfig, + HSRConfig, + ] = Field( ..., description="脚本配置数据" ) @@ -1395,7 +1488,17 @@ class ScriptGetIn(BaseModel): class ScriptGetOut(OutBase): index: List[ScriptIndexItem] = Field(..., description="脚本索引列表") data: Dict[ - str, Union[MaaConfig, SrcConfig, GeneralConfig, OkwwConfig, MaaEndConfig, M9AConfig, HSRConfig] + str, + Union[ + MaaConfig, + SrcConfig, + GeneralConfig, + OkwwConfig, + OkNteConfig, + MaaEndConfig, + M9AConfig, + HSRConfig, + ], ] = Field( ..., description="脚本数据字典, key来自于index列表的uid" ) @@ -1403,7 +1506,16 @@ class ScriptGetOut(OutBase): class ScriptUpdateIn(BaseModel): scriptId: str = Field(..., description="脚本ID") - data: Union[MaaConfig, SrcConfig, GeneralConfig, OkwwConfig, MaaEndConfig, M9AConfig, HSRConfig] = Field( + data: Union[ + MaaConfig, + SrcConfig, + GeneralConfig, + OkwwConfig, + OkNteConfig, + MaaEndConfig, + M9AConfig, + HSRConfig, + ] = Field( ..., description="脚本更新数据" ) @@ -1458,6 +1570,7 @@ class UserGetOut(OutBase): SrcUserConfig, GeneralUserConfig, OkwwUserConfig, + OkNteUserConfig, MaaEndUserConfig, M9AUserConfig, HSRUserConfig, @@ -1472,6 +1585,7 @@ class UserCreateOut(OutBase): SrcUserConfig, GeneralUserConfig, OkwwUserConfig, + OkNteUserConfig, MaaEndUserConfig, M9AUserConfig, HSRUserConfig, @@ -1487,6 +1601,7 @@ class UserUpdateIn(UserInBase): SrcUserConfig, GeneralUserConfig, OkwwUserConfig, + OkNteUserConfig, MaaEndUserConfig, M9AUserConfig, HSRUserConfig, diff --git a/app/task/OkNte/AutoProxy.py b/app/task/OkNte/AutoProxy.py new file mode 100644 index 000000000..5e06cdc2f --- /dev/null +++ b/app/task/OkNte/AutoProxy.py @@ -0,0 +1,763 @@ +# 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 . + +import asyncio +import re +import shlex +import shutil +import uuid +from contextlib import suppress +from datetime import datetime, timedelta +from pathlib import Path + +from app.core import Config +from app.models.task import TaskExecuteBase, ScriptItem, UserItem, LogRecord +from app.models.ConfigBase import MultipleConfig +from app.models.config import OkNteConfig, OkNteUserConfig +from app.services import Notify, System +from app.utils import get_logger, ProcessManager, ProcessInfo, is_process_running +from app.utils.LogMonitor import LogMonitor +from app.utils.constants import UTC4 +from app.task.general.tools import execute_script_task + +logger = get_logger("OK-NTE 自动代理") + +# 异环 PC 客户端进程名固定,MAS 接管启动前据此避免重复拉起 +_NTE_CLIENT_PROCESS = "HTGame.exe" + + +def _yes_no(value: bool) -> str: + return "是" if value else "否" + +# 对齐 MaaEnd:专项内置致命日志片段(非用户 Success/Error 配置);`Script.ErrorLog` 仅追加补充子串 +_OKNTE_BUILTIN_FATAL: tuple[tuple[str, str], ...] = ( + ("connected:False", "OK-NTE 未连接游戏客户端"), + ("Resolution Error", "OK-NTE 游戏分辨率不符合要求"), + ("Timed out waiting for game process", "OK-NTE 等待游戏进程超时"), + ("Timed out waiting for launcher process", "OK-NTE 等待启动器进程超时"), +) + +# prepare 中 ErrorLog 经清洗后为空时回退(与 OkNteConfig 默认串一致) +_DEFAULT_OKNTE_ERROR_LOG = ( + "connected:False|Resolution Error|Timed out waiting for game process|" + "Timed out waiting for launcher process" +) + +_OKNTE_DAILY_TASK_INDEX = 2 +_OKNTE_DAILY_REQUIRED_SUCCESS = "完成每日活跃度" +_OKNTE_DAILY_SUCCESS_RE = re.compile( + r"DailyTask:info_set success\s*(?P\[[^\r\n]*\])" +) + + +def _split_args(raw: object) -> list[str]: + value = str(raw or "").strip() + return shlex.split(value, posix=False) if value else [] + +def _sanitize_oknte_error_log_tokens(tokens: list[str]) -> list[str]: + return [t for raw in tokens if (t := raw.strip())] + + +def _oknte_log_indicates_success(log: str, success_log: list[str]) -> bool: + if ( + "Successfully Executed Task" in log + or "任务执行完成" in log + or "task completed" in log.lower() + ): + return True + return any(k in log for k in success_log if k) + + +def _oknte_daily_task_success_error(log: str) -> str | None: + daily_success_matches = _OKNTE_DAILY_SUCCESS_RE.findall(log) + if not daily_success_matches: + return "OK-NTE 日常任务未确认完成每日活跃度" + + if _OKNTE_DAILY_REQUIRED_SUCCESS not in daily_success_matches[-1]: + return "OK-NTE 日常任务未完成每日活跃度" + return None + + +class AutoProxyTask(TaskExecuteBase): + """OK-NTE 自动代理:拼 `-t N -e` 启动参数并监控日志""" + + def __init__( + self, + script_info: ScriptItem, + script_config: OkNteConfig, + user_config: MultipleConfig[OkNteUserConfig], + game_manager: ProcessManager | None, + ): + super().__init__() + if script_info.task_info is None: + raise RuntimeError("ScriptItem 未绑定到 TaskItem") + + self.task_info = script_info.task_info + self.script_info = script_info + self.script_config = script_config + self.user_config = user_config + self.game_manager = game_manager + + self.cur_user_item: UserItem = self.script_info.user_list[self.script_info.current_index] + self.cur_user_uid = uuid.UUID(self.cur_user_item.user_id) + self.cur_user_config: OkNteUserConfig = self.user_config[self.cur_user_uid] + self.curdate = "" + self.user_run_result_persisted = False + + async def _reset_daily_proxy_count(self) -> None: + self.curdate = datetime.now(tz=UTC4).strftime("%Y-%m-%d") + if self.cur_user_config.get("Data", "LastProxyDate") != self.curdate: + await self.cur_user_config.set("Data", "LastProxyDate", self.curdate) + await self.cur_user_config.set("Data", "ProxyTimes", 0) + + async def check(self) -> str: + if not Path(self.script_config.get("Info", "RootPath")).is_dir(): + return "请设置 OK-NTE 脚本路径" + if not Path(self.script_config.get("Script", "ScriptPath")).is_file(): + return "请设置 OK-NTE 脚本路径" + + await self._reset_daily_proxy_count() + if ( + self.script_config.get("Run", "ProxyTimesLimit") != 0 + and self.cur_user_config.get("Data", "ProxyTimes") + >= self.script_config.get("Run", "ProxyTimesLimit") + ): + self.cur_user_item.status = "跳过" + return "今日代理次数已达上限, 跳过该用户" + if self.cur_user_config.get("Info", "RemainedDay") == 0: + self.cur_user_item.status = "跳过" + return "用户剩余天数为 0, 跳过该用户" + + if ( + self.script_config.get("Game", "Enabled") + and self.script_config.get("Game", "Type") == "Client" + and not Path(self.script_config.get("Game", "Path")).is_file() + ): + return "请设置异环游戏路径" + if ( + self.script_config.get("Game", "Enabled") + and self.script_config.get("Game", "Type") == "URL" + and self.script_config.get("Game", "LaunchBeforeTask") + ): + if not str(self.script_config.get("Game", "URL") or "").strip(): + return "请设置异环游戏 URL" + if not str(self.script_config.get("Game", "ProcessName") or "").strip(): + return "请设置异环游戏进程名称" + return "Pass" + + async def prepare(self): + self.oknte_process_manager = ProcessManager() + self.wait_event = asyncio.Event() + + self.user_start_time = datetime.now() + self.log_start_time = datetime.now() + + self.script_root_path = Path(self.script_config.get("Info", "RootPath")) + self.script_exe_path = Path(self.script_config.get("Script", "ScriptPath")) + self.script_target_process_info: ProcessInfo | None = None + if self.script_config.get("Script", "IfTrackProcess"): + track_name = self.script_config.get("Script", "TrackProcessName") or "pythonw.exe" + track_exe = self.script_config.get("Script", "TrackProcessExe") or "" + if not track_exe: + track_exe = str( + self.script_root_path / "data/apps/ok-nte/python/pythonw.exe" + ) + track_cmdline = ( + shlex.split( + self.script_config.get("Script", "TrackProcessCmdline"), posix=False + ) + or None + ) + self.script_target_process_info = ProcessInfo( + name=track_name or None, + exe=track_exe or None, + cmdline=track_cmdline, + ) + + self.script_log_path = Path(self.script_config.get("Script", "LogPath")) + self.log_format = self.script_config.get("Script", "LogPathFormat") or "" + if self.log_format: + with suppress(ValueError): + datetime.strptime(self.script_log_path.stem, self.log_format) + self.log_format = f"{self.log_format}{self.script_log_path.suffix}" + else: + self.log_format = self.script_log_path.name + + self.log_time_range = ( + self.script_config.get("Script", "LogTimeStart") - 1, + self.script_config.get("Script", "LogTimeEnd"), + ) + self.log_time_format = self.script_config.get("Script", "LogTimeFormat") + self.log_monitor = LogMonitor( + self.log_time_range, + self.log_time_format, + self.check_log, + ) + self.success_log = [ + _.strip() + for _ in str(self.script_config.get("Script", "SuccessLog")).split("|") + if _.strip() + ] + raw_error_tokens = [ + _.strip() + for _ in str(self.script_config.get("Script", "ErrorLog")).split("|") + if _.strip() + ] + self.error_log = _sanitize_oknte_error_log_tokens(raw_error_tokens) + if not self.error_log: + self.error_log = [ + _.strip() + for _ in _DEFAULT_OKNTE_ERROR_LOG.split("|") + if _.strip() + ] + logger.warning( + "OK-NTE ErrorLog 去掉过宽容词后为空,已回退为内置默认失败关键词" + ) + + # 当前用户配置 + + self.task_index = int(self.cur_user_config.get("Task", "TaskIndex")) + self.exit_on_finish = bool(self.cur_user_config.get("Task", "ExitOnFinish")) + + extra_args = _split_args(self.script_config.get("Script", "Arguments")) + + self.oknte_args = ["-t", str(self.task_index)] + if self.exit_on_finish: + self.oknte_args.append("-e") + self.oknte_args.extend(extra_args) + + # 游戏配置(对齐通用脚本逻辑) + self.game_path = Path(self.script_config.get("Game", "Path")) + self.game_url = self.script_config.get("Game", "URL") + self.game_process_name = self.script_config.get("Game", "ProcessName") + self.script_config_path = Path(self.script_config.get("Script", "ConfigPath")) + + self.run_book = False + + def _oknte_legacy_mas_config_dir(self) -> Path: + return Path.cwd() / "data" / self.script_info.script_id / "Default" / "ConfigFile" + + def _oknte_mas_config_dir(self) -> Path: + return Path.cwd() / "data" / self.script_info.script_id / str(self.cur_user_uid) / "ConfigFile" + + def _oknte_source_config_dir(self, mas_config_dir: Path) -> Path | None: + candidates = [ + self._oknte_legacy_mas_config_dir(), + self.script_config_path, + self.script_root_path / "data" / "apps" / "ok-nte" / "working" / "configs", + self.script_root_path / "configs", + ] + for config_dir in candidates: + if not config_dir.is_dir(): + continue + with suppress(OSError): + if config_dir.resolve() == mas_config_dir.resolve(): + continue + return config_dir + return None + + def _ensure_oknte_mas_config_dir(self) -> Path: + mas_config_dir = self._oknte_mas_config_dir() + if mas_config_dir.exists() and any(mas_config_dir.iterdir()): + return mas_config_dir + + mas_config_dir.mkdir(parents=True, exist_ok=True) + if self.script_config.get("Script", "ConfigPathMode") == "File": + if not self.script_config_path.is_file(): + raise FileNotFoundError("OK-NTE 配置文件未初始化,请先设置有效配置路径") + shutil.copy( + self.script_config_path, + mas_config_dir / self.script_config_path.name, + ) + return mas_config_dir + + source_config_dir = self._oknte_source_config_dir(mas_config_dir) + if source_config_dir is None: + raise FileNotFoundError("OK-NTE 配置目录未初始化,请先设置有效配置路径") + + shutil.copytree(source_config_dir, mas_config_dir, dirs_exist_ok=True) + return mas_config_dir + + async def set_oknte(self) -> None: + """将 MAS 侧 OK-NTE 任务配置下发到脚本 working 目录(对齐 General.set_general)。""" + + logger.info("开始配置 OK-NTE 运行参数: 自动代理") + await System.kill_process(self.script_exe_path) + + mas_config_dir = self._ensure_oknte_mas_config_dir() + if self.script_config.get("Script", "ConfigPathMode") == "Folder": + tmp_dst = self.script_config_path.with_name( + self.script_config_path.name + ".tmp" + ) + shutil.rmtree(tmp_dst, ignore_errors=True) + shutil.copytree(mas_config_dir, tmp_dst, dirs_exist_ok=True) + shutil.rmtree(self.script_config_path, ignore_errors=True) + tmp_dst.rename(self.script_config_path) + elif self.script_config.get("Script", "ConfigPathMode") == "File": + shutil.copy( + mas_config_dir / self.script_config_path.name, + self.script_config_path, + ) + logger.info(f"OK-NTE 运行参数配置完成: 自动代理") + + async def update_config(self) -> None: + """将脚本侧配置回写 MAS ConfigFile(对齐 General.update_config)。""" + + mas_config_dir = self._oknte_mas_config_dir() + mas_config_dir.mkdir(parents=True, exist_ok=True) + if self.script_config.get("Script", "ConfigPathMode") == "Folder": + shutil.copytree( + self.script_config_path, mas_config_dir, dirs_exist_ok=True + ) + elif self.script_config.get("Script", "ConfigPathMode") == "File": + shutil.copy( + self.script_config_path, + mas_config_dir / self.script_config_path.name, + ) + logger.success("OK-NTE 配置文件已更新") + + def _game_config_summary_lines(self) -> list[str]: + """游戏配置摘要行(调度台展示用)。""" + + game_args = str(self.script_config.get("Game", "Arguments") or "").strip() + return [ + f"[游戏配置] 用户: {self.cur_user_item.name}", + f" 启用游戏配置: {_yes_no(bool(self.script_config.get('Game', 'Enabled')))}", + f" 任务前启动游戏: {_yes_no(bool(self.script_config.get('Game', 'LaunchBeforeTask')))}", + f" 任务后关闭游戏: {_yes_no(bool(self.script_config.get('Game', 'CloseOnFinish')))}", + f" 启动参数: {game_args or '(无)'}", + ] + + async def _push_dispatch_log(self, line: str) -> None: + """向调度台追加流程日志(赋值 script_info.log 会触发 WebSocket 推送)。""" + + prev = self.script_info.log + self.script_info.log = f"{prev}\n{line}" if prev else line + await asyncio.sleep(0) + + async def _log_game_config_summary(self) -> None: + """在调度台开头输出当前脚本的游戏相关配置,便于用户确认与问题排查。""" + + self.script_info.log = "\n".join(self._game_config_summary_lines()) + await asyncio.sleep(0) + + async def _mas_launch_game_before_task(self) -> None: + """MAS 接管启动游戏,并将各步骤写入调度台日志。""" + + game_type = self.script_config.get("Game", "Type") + await self._push_dispatch_log("正在准备由 MAS 启动游戏...") + + if isinstance(self.game_manager, ProcessManager) and game_type == "Client": + await self._push_dispatch_log( + f"正在检查异环客户端进程 ({_NTE_CLIENT_PROCESS})..." + ) + if is_process_running(_NTE_CLIENT_PROCESS): + logger.info( + "检测到异环客户端进程已在运行,跳过由 MAS 重复启动游戏" + ) + await self._push_dispatch_log("检测到客户端已在运行,跳过启动") + return + + await self._push_dispatch_log("未检测到运行中的客户端,正在拉起游戏...") + await self.game_manager.open_process( + self.game_path, + *_split_args(self.script_config.get("Game", "Arguments")), + ) + wait_time = int(self.script_config.get("Game", "WaitTime")) + await self._push_dispatch_log( + f"正在等待游戏完成启动({wait_time}s)..." + ) + await asyncio.sleep(wait_time) + await self._push_dispatch_log("游戏启动完成") + return + + if isinstance(self.game_manager, ProcessManager) and game_type == "URL": + await self._push_dispatch_log("正在通过 URL 协议启动游戏...") + game_url = str(self.game_url or "").strip() + game_process_name = str(self.game_process_name or "").strip() + if not game_url: + raise RuntimeError("请设置异环游戏 URL") + if not game_process_name: + raise RuntimeError("请设置异环游戏进程名称") + if is_process_running(game_process_name): + logger.info(f"检测到异环客户端进程已在运行,跳过启动: {game_process_name}") + await self._push_dispatch_log("检测到客户端已在运行,跳过启动") + return + await self.game_manager.open_protocol( + game_url, + ProcessInfo(name=game_process_name), + ) + await asyncio.sleep(2) + await self._push_dispatch_log("游戏启动指令已发送") + return + + async def main_task(self): + await self.prepare() + await self._reset_daily_proxy_count() + + self.cur_user_item.status = "运行" + + run_limit = int(self.script_config.get("Run", "RunTimesLimit")) + for i in range(run_limit): + if self.run_book: + break + logger.info( + f"用户 {self.cur_user_item.name} - 尝试次数: {i + 1}/{run_limit}" + ) + self.cur_user_item.status = "运行" + self.log_start_time = datetime.now() + self.cur_user_item.log_record[self.log_start_time] = LogRecord() + self.cur_user_log = self.cur_user_item.log_record[self.log_start_time] + + if self.cur_user_config.get("Info", "IfScriptBeforeTask"): + await execute_script_task( + Path(self.cur_user_config.get("Info", "ScriptBeforeTask")), + "脚本前任务", + ) + + await self._log_game_config_summary() + + # 总开关开启且勾选「任务前启动」时由 MAS 拉起游戏 + if ( + self.script_config.get("Game", "Enabled") + and self.script_config.get("Game", "LaunchBeforeTask") + and self.game_manager is not None + ): + try: + await self._mas_launch_game_before_task() + except Exception as e: + await self._push_dispatch_log(f"游戏启动失败: {e}") + self.cur_user_log.status = f"游戏启动失败: {e}" + self.cur_user_log.content = [f"游戏启动失败: {e}"] + await Config.send_websocket_message( + id=self.task_info.task_id, + type="Info", + data={"Error": f"游戏启动失败: {e}"}, + ) + await self.kill_managed_process( + kill_game=self._mas_should_close_game_on_retry() + ) + try: + await Notify.push_plyer( + "OK-NTE 自动代理出现异常!", + f"用户 {self.cur_user_item.name} 游戏启动失败", + f"{self.cur_user_item.name}的自动代理出现异常", + 3, + ) + except Exception: + pass + if i + 1 < run_limit: + await self._push_dispatch_log( + f"游戏启动失败,将在稍后重试 ({i + 1}/{run_limit})" + ) + await asyncio.sleep(10) + else: + self.cur_user_item.status = "异常" + continue + + await self.set_oknte() + await self._push_dispatch_log( + f"启动 OK-NTE: -t {self.task_index}" + + (" -e" if self.exit_on_finish else "") + ) + logger.info( + f"启动 OK-NTE 进程: {self.script_exe_path} {' '.join(self.oknte_args)}" + ) + + await self.oknte_process_manager.open_process( + self.script_exe_path, + *self.oknte_args, + target_process=self.script_target_process_info, + ) + + # 启动日志监控(文件日志) + await asyncio.sleep(1) + await self.log_monitor.start_monitor_file( + self._resolve_log_path(), self.log_start_time + ) + + self.wait_event.clear() + await self.wait_event.wait() + await self.log_monitor.stop() + + if self.cur_user_log.status == "Success!": + self.run_book = True + self.script_info.log = ( + "检测到 OK-NTE 已完成任务\n正在等待相关进程结束" + ) + # 对齐 MaaEnd:成功时先只结束 OK-NTE;是否关游戏由 Game.CloseOnFinish 在 final_task 决定 + await self._kill_oknte_process() + if self.script_config.get("Script", "UpdateConfigMode") in ( + "Success", + "Always", + ): + await self.update_config() + if self.cur_user_config.get("Info", "IfScriptAfterTask"): + await execute_script_task( + Path(self.cur_user_config.get("Info", "ScriptAfterTask")), + "脚本后任务", + ) + await asyncio.sleep(3) + break + + logger.error( + f"用户 {self.cur_user_item.name} - OK-NTE 代理异常: {self.cur_user_log.status}" + ) + self.script_info.log = ( + f"{self.cur_user_log.status}\n正在中止相关程序" + ) + await self.kill_managed_process( + kill_game=self._mas_should_close_game_on_retry() + ) + try: + await Notify.push_plyer( + "OK-NTE 自动代理出现异常!", + f"用户 {self.cur_user_item.name} 的自动代理出现一次异常", + f"{self.cur_user_item.name}的自动代理出现异常", + 3, + ) + except Exception: + pass + if self.script_config.get("Script", "UpdateConfigMode") in ( + "Failure", + "Always", + ): + await self.update_config() + if self.cur_user_config.get("Info", "IfScriptAfterTask"): + await execute_script_task( + Path(self.cur_user_config.get("Info", "ScriptAfterTask")), + "脚本后任务", + ) + if i + 1 < run_limit: + self.script_info.log += ( + f"\n将在稍后重试 ({i + 1}/{run_limit})" + ) + await asyncio.sleep(10) + + def _game_management_enabled(self) -> bool: + return bool(self.script_config.get("Game", "Enabled")) + + def _mas_should_close_game_after_success(self) -> bool: + return self._game_management_enabled() and bool( + self.script_config.get("Game", "CloseOnFinish") + ) + + def _mas_should_close_game_on_retry(self) -> bool: + """失败/重试/启动游戏失败:总开关开启且任一生周期子项启用时结束游戏""" + return self._game_management_enabled() and bool( + self.script_config.get("Game", "LaunchBeforeTask") + or self.script_config.get("Game", "CloseOnFinish") + ) + + def _resolve_log_path(self) -> Path: + # 若用户给了带日期模板的日志路径,则按启动时间格式化文件名 + if self.log_format and self.script_log_path.name != self.log_format: + try: + filename = self.log_start_time.strftime(self.log_format) + return self.script_log_path.with_name(filename) + except Exception: + return self.script_log_path + return self.script_log_path + + async def check_log(self, log_content: list[str], latest_time: datetime) -> None: + """与 MaaEnd 类似:内置致命片段优先,再读配置补充;成功;进程结束;超时;否则为运行中。 + + `Script.ErrorLog` / `SuccessLog` 仅在 AutoProxy.prepare → 本回调中使用,全仓无第二处运行时判据, + 避免「配置一套、代码另一套」的分裂;内置项保证未改配置时也有基线行为。 + """ + log = "".join(log_content) + self.cur_user_log.content = log_content + self.script_info.log = log[-4000:] if len(log) > 4000 else log + + log_status = "OK-NTE 正常运行中" + user_item_status: str | None = None + + for needle, msg in _OKNTE_BUILTIN_FATAL: + if needle in log: + log_status = msg + user_item_status = "异常" + break + else: + for k in self.error_log: + if k and k in log: + log_status = f"OK-NTE:{k}" + user_item_status = "异常" + break + else: + if _oknte_log_indicates_success(log, self.success_log): + daily_task_error = ( + _oknte_daily_task_success_error(log) + if self.task_index == _OKNTE_DAILY_TASK_INDEX + else None + ) + if daily_task_error: + log_status = daily_task_error + user_item_status = "异常" + else: + log_status = "Success!" + user_item_status = "完成" + elif not await self.oknte_process_manager.is_running(): + log_status = "OK-NTE 在完成任务前退出" + user_item_status = "异常" + elif datetime.now() - latest_time > timedelta( + minutes=self.script_config.get("Run", "RunTimeLimit") + ): + log_status = "OK-NTE 运行超时" + user_item_status = "异常" + + self.cur_user_log.status = log_status + if user_item_status is not None: + self.cur_user_item.status = user_item_status + + logger.debug(f"OK-NTE 日志分析结果: {self.cur_user_log.status}") + if self.cur_user_log.status != "OK-NTE 正常运行中": + logger.info(f"OK-NTE 任务结果: {self.cur_user_log.status}, 日志锁已释放") + self.wait_event.set() + + async def final_task(self): + # 结束时先清理进程与监控 + with suppress(Exception): + await self.log_monitor.stop() + if self.run_book and not self._mas_should_close_game_after_success(): + await self._kill_oknte_process() + else: + kill_game = ( + self._mas_should_close_game_after_success() + if self.run_book + else self._mas_should_close_game_on_retry() + ) + await self.kill_managed_process(kill_game=kill_game) + + # 写入历史记录(对齐 General/SRC/MaaEnd 行为) + 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() + / f"history/{dt.strftime('%Y-%m-%d')}/{self.cur_user_item.name}/{dt.strftime('%H-%M-%S')}.log" + ) + + if log_item.status == "OK-NTE 正常运行中": + log_item.status = "任务被用户手动中止" + + if len(log_item.content) == 0: + log_item.content = ["未捕获到任何日志内容"] + log_item.status = "未捕获到日志" + + await Config.save_general_log(log_path, log_item.content, log_item.status) + + await self._persist_user_run_result() + + async def _persist_user_run_result(self) -> None: + if self.user_run_result_persisted: + return + self.user_run_result_persisted = True + + await self.cur_user_config.set("Data", "LastTaskIndex", getattr(self, "task_index", 0)) + if self.run_book: + if ( + self.cur_user_config.get("Data", "ProxyTimes") == 0 + and self.cur_user_config.get("Info", "RemainedDay") != -1 + ): + await self.cur_user_config.set( + "Info", + "RemainedDay", + self.cur_user_config.get("Info", "RemainedDay") - 1, + ) + await self.cur_user_config.set( + "Data", + "ProxyTimes", + self.cur_user_config.get("Data", "ProxyTimes") + 1, + ) + await self.cur_user_config.set("Data", "LastProxyStatus", "成功") + if self.cur_user_item.status != "异常": + self.cur_user_item.status = "完成" + logger.success(f"用户 {self.cur_user_uid} 的 OK-NTE 自动代理任务已完成") + else: + await self.cur_user_config.set("Data", "LastProxyStatus", "失败") + if self.cur_user_item.status != "完成": + self.cur_user_item.status = "异常" + + async def on_crash(self, e: Exception): + self.cur_user_item.status = "异常" + if hasattr(self, "cur_user_log"): + self.cur_user_log.status = f"OK-NTE 运行异常: {e}" + logger.exception(f"OK-NTE 自动代理任务出现异常: {e}") + if hasattr(self, "wait_event"): + self.wait_event.set() + await Config.send_websocket_message( + id=self.task_info.task_id, + type="Info", + data={"Error": f"OK-NTE 自动代理任务出现异常: {e}"}, + ) + await self.kill_managed_process( + kill_game=self._mas_should_close_game_on_retry() + ) + await self._persist_user_run_result() + + # 推送通知(复用 Notify) + try: + if ( + hasattr(self, "cur_user_log") + and self.cur_user_log.status + and self.cur_user_log.status != "Success!" + ): + await Notify.push_plyer( + "OK-NTE 运行异常", + f"用户 {self.cur_user_item.name}:{self.cur_user_log.status}", + "异常", + 3, + ) + except Exception: + pass + + async def _kill_oknte_process(self) -> None: + try: + await self.oknte_process_manager.kill() + except Exception as e: + logger.exception(f"通过进程管理器中止 OK-NTE 进程失败: {e}") + try: + await System.kill_process(self.script_exe_path) + except Exception as e: + logger.exception(f"中止 OK-NTE 主进程失败: {e}") + track_exe = str(self.script_config.get("Script", "TrackProcessExe") or "").strip() + if not track_exe: + track_exe = str(self.script_root_path / "data/apps/ok-nte/python/pythonw.exe") + if track_exe: + try: + await System.kill_process(Path(track_exe)) + except Exception as e: + logger.exception(f"中止 OK-NTE 追踪进程失败: {e}") + + async def _kill_game_process(self) -> None: + """结束游戏:不依赖 LaunchBeforeTask(可自行开游戏,由 CloseOnFinish/失败重试触发)""" + game_type = self.script_config.get("Game", "Type") + try: + if isinstance(self.game_manager, ProcessManager): + await self.game_manager.kill() + if game_type == "Client": + gp = self.game_path + if gp.is_file(): + await System.kill_process(gp) + except Exception as e: + logger.exception(f"关闭游戏进程失败: {e}") + + async def kill_managed_process(self, *, kill_game: bool = True) -> None: + """中止 OK-NTE;kill_game 为真时结束游戏(失败重试恒为真;成功收尾看 CloseOnFinish)""" + await self._kill_oknte_process() + if kill_game: + await self._kill_game_process() diff --git a/app/task/OkNte/ScriptConfig.py b/app/task/OkNte/ScriptConfig.py new file mode 100644 index 000000000..f62b3d8fe --- /dev/null +++ b/app/task/OkNte/ScriptConfig.py @@ -0,0 +1,184 @@ +# 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 . + +import asyncio +import shutil +from contextlib import suppress +from pathlib import Path + +from app.core import Config +from app.models.ConfigBase import MultipleConfig +from app.models.config import OkNteConfig, OkNteUserConfig +from app.models.task import ScriptItem, TaskExecuteBase +from app.services import System +from app.utils import ProcessManager, get_logger + +logger = get_logger("OK-NTE 脚本设置") + + +class ScriptConfigTask(TaskExecuteBase): + """OK-NTE GUI 配置会话""" + + def __init__( + self, + script_info: ScriptItem, + script_config: OkNteConfig, + user_config: MultipleConfig[OkNteUserConfig], + game_manager: ProcessManager | None, + ): + super().__init__() + + if script_info.task_info is None: + raise RuntimeError("ScriptItem 未绑定到 TaskItem") + + self.task_info = script_info.task_info + self.script_info = script_info + self.script_config = script_config + self.user_config = user_config + self.game_manager = game_manager + self.cur_user_item = self.script_info.user_list[self.script_info.current_index] + self.oknte_process_manager: ProcessManager = ProcessManager() + self.wait_event: asyncio.Event = asyncio.Event() + self.script_root_path: Path = Path(self.script_config.get("Info", "RootPath")) + self.script_exe_path: Path = Path( + self.script_config.get("Script", "ScriptPath") + ) + self.script_config_path: Path = Path( + self.script_config.get("Script", "ConfigPath") + ) + self.mas_config_dir: Path = ( + Path.cwd() + / "data" + / self.script_info.script_id + / self.cur_user_item.user_id + / "ConfigFile" + ) + + async def check(self) -> str: + return "Pass" + + async def prepare(self) -> None: + self.oknte_process_manager = ProcessManager() + self.wait_event = asyncio.Event() + + async def main_task(self) -> None: + await self.prepare() + await self.set_oknte() + + logger.info(f"启动 OK-NTE GUI 配置进程: {self.script_exe_path}") + self.cur_user_item.status = "运行" + self.wait_event.clear() + await self.oknte_process_manager.open_process(self.script_exe_path) + await self.wait_event.wait() + + async def set_oknte(self) -> None: + """将 AUTO-MAS 侧用户配置下发到 OK-NTE GUI 读取目录。""" + + logger.info(f"开始配置 OK-NTE GUI: 设置脚本 {self.cur_user_item.user_id}") + await self._kill_oknte_process() + + if not self.mas_config_dir.exists() or not any(self.mas_config_dir.iterdir()): + logger.info("未找到用户级 OK-NTE 配置,使用脚本当前配置启动 GUI") + return + + if self.script_config.get("Script", "ConfigPathMode") == "Folder": + tmp_dst = self.script_config_path.with_name( + self.script_config_path.name + ".tmp" + ) + shutil.rmtree(tmp_dst, ignore_errors=True) + shutil.copytree(self.mas_config_dir, tmp_dst, dirs_exist_ok=True) + shutil.rmtree(self.script_config_path, ignore_errors=True) + tmp_dst.rename(self.script_config_path) + elif self.script_config.get("Script", "ConfigPathMode") == "File": + src_file = self.mas_config_dir / self.script_config_path.name + if src_file.exists(): + self.script_config_path.parent.mkdir(parents=True, exist_ok=True) + tmp_file = self.script_config_path.with_name( + self.script_config_path.name + ".tmp" + ) + shutil.copy(src_file, tmp_file) + tmp_file.replace(self.script_config_path) + + logger.success(f"OK-NTE GUI 配置已加载: {self.cur_user_item.user_id}") + + async def final_task(self) -> None: + await self._kill_oknte_process() + + self.mas_config_dir.parent.mkdir(parents=True, exist_ok=True) + if self.script_config.get("Script", "ConfigPathMode") == "Folder": + if not self.script_config_path.exists(): + raise FileNotFoundError( + "未找到 OK-NTE 配置目录,请在 GUI 中保存后再点击保存配置" + ) + + tmp_dst = self.mas_config_dir.with_name(self.mas_config_dir.name + ".tmp") + shutil.rmtree(tmp_dst, ignore_errors=True) + shutil.copytree(self.script_config_path, tmp_dst, dirs_exist_ok=True) + shutil.rmtree(self.mas_config_dir, ignore_errors=True) + tmp_dst.rename(self.mas_config_dir) + logger.success(f"OK-NTE 配置已保存到: {self.mas_config_dir}") + elif self.script_config.get("Script", "ConfigPathMode") == "File": + if not self.script_config_path.exists(): + raise FileNotFoundError( + "未找到 OK-NTE 配置文件,请在 GUI 中保存后再点击保存配置" + ) + + self.mas_config_dir.mkdir(parents=True, exist_ok=True) + tmp_file = self.mas_config_dir / (self.script_config_path.name + ".tmp") + shutil.copy(self.script_config_path, tmp_file) + tmp_file.replace(self.mas_config_dir / self.script_config_path.name) + logger.success( + f"OK-NTE 配置已保存到: {self.mas_config_dir / self.script_config_path.name}" + ) + + self.cur_user_item.status = "完成" + + async def on_crash(self, e: Exception) -> None: + self.cur_user_item.status = "异常" + logger.exception(f"OK-NTE GUI 配置任务出现异常: {e}") + with suppress(Exception): + await self._kill_oknte_process() + await Config.send_websocket_message( + id=self.task_info.task_id, + type="Info", + data={"Error": f"OK-NTE GUI 配置任务出现异常: {e}"}, + ) + + async def _kill_oknte_process(self) -> None: + try: + await self.oknte_process_manager.kill() + except Exception as e: + logger.exception(f"通过进程管理器中止 OK-NTE GUI 进程失败: {e}") + + try: + await System.kill_process(self.script_exe_path) + except Exception as e: + logger.exception(f"中止 OK-NTE 主进程失败: {e}") + + track_exe = str( + self.script_config.get("Script", "TrackProcessExe") or "" + ).strip() + if not track_exe: + track_exe = str( + self.script_root_path / "data/apps/ok-nte/python/pythonw.exe" + ) + if track_exe: + try: + await System.kill_process(Path(track_exe)) + except Exception as e: + logger.exception(f"中止 OK-NTE 追踪进程失败: {e}") diff --git a/app/task/OkNte/__init__.py b/app/task/OkNte/__init__.py new file mode 100644 index 000000000..79d7c64c0 --- /dev/null +++ b/app/task/OkNte/__init__.py @@ -0,0 +1,21 @@ +# 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 . + +from .manager import OkNteManager + +__all__ = ["OkNteManager"] diff --git a/app/task/OkNte/config_schema.py b/app/task/OkNte/config_schema.py new file mode 100644 index 000000000..3cf451016 --- /dev/null +++ b/app/task/OkNte/config_schema.py @@ -0,0 +1,401 @@ +# 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 . + +""" +OK-NTE 配置文件 Schema 定义 + +半自动模式: +- 字段名 / 类型从 JSON 配置文件值自动推断 +- 中文标签从 OK-NTE 安装目录的 .po / .mo / .ts 自动加载 +- 仅下拉 / 多选的可选项列表需在此手工维护 +""" + +from __future__ import annotations +from typing import Any +from pathlib import Path +import re +import struct +from xml.etree import ElementTree + + +# ─── OK-NTE 翻译文件自动加载 ───────────────────────────────────────────────── + +_PO_ENTRY_RE = re.compile( + r'^msgid\s+"((?:[^"\\]|\\.)*)"\s*\nmsgstr\s+"((?:[^"\\]|\\.)*)"', + re.MULTILINE, +) + + +def _parse_po_file(po_path: Path) -> dict[str, str]: + """解析 .po 翻译文件,返回 {msgid: msgstr} 映射。""" + labels: dict[str, str] = {} + try: + text = po_path.read_text(encoding="utf-8") + for match in _PO_ENTRY_RE.finditer(text): + msgid = match.group(1) + msgstr = match.group(2) + if msgid and msgstr: + labels[msgid] = msgstr + except Exception: + pass + return labels + + +def _parse_mo_file(mo_path: Path) -> dict[str, str]: + """解析 .mo 编译翻译文件,返回 {msgid: msgstr} 映射。""" + labels: dict[str, str] = {} + try: + data = mo_path.read_bytes() + if len(data) < 20: + return labels + + magic, _rev, n_strings, orig_off, trans_off = struct.unpack_from( + "II" + + def read_strings(table_offset: int) -> list[str]: + strings: list[str] = [] + for i in range(n_strings): + length, offset = struct.unpack_from( + fmt, data, table_offset + i * 8 + ) + if length > 0: + s = data[offset : offset + length] + strings.append(s.decode("utf-8", errors="replace")) + else: + strings.append("") + return strings + + orig_strings = read_strings(orig_off) + trans_strings = read_strings(trans_off) + + for orig, trans in zip(orig_strings, trans_strings): + if orig and trans: + labels[orig] = trans + except Exception: + pass + return labels + + +def _parse_ts_file(ts_path: Path) -> dict[str, str]: + """解析 Qt .ts 翻译文件(ok-script 框架级翻译)。""" + labels: dict[str, str] = {} + try: + root = ElementTree.parse(str(ts_path)).getroot() + for message in root.iter("message"): + source = message.find("source") + translation = message.find("translation") + if ( + source is not None + and translation is not None + and source.text + and translation.text + and translation.attrib.get("type") != "unfinished" + ): + labels[source.text] = translation.text + except Exception: + pass + return labels + + +def load_oknte_option_labels(root_path: Path | str) -> dict[str, str]: + """从 OK-NTE 安装目录自动加载选项的英文→中文翻译映射。 + + 搜索优先级:ok.mo > ok.po,同时补充 ok-script 框架的 zh_CN.ts。 + """ + root = Path(root_path) + labels: dict[str, str] = {} + + i18n_candidates = [ + root / "i18n", + root / "_internal" / "i18n", + root / "data" / "apps" / "ok-nte" / "repo" / "i18n", + root / "data" / "apps" / "ok-nte" / "working" / "i18n", + ] + + for i18n_dir in i18n_candidates: + mo_file = i18n_dir / "zh_CN" / "LC_MESSAGES" / "ok.mo" + if mo_file.is_file(): + loaded = _parse_mo_file(mo_file) + if loaded: + labels.update(loaded) + break + + po_file = i18n_dir / "zh_CN" / "LC_MESSAGES" / "ok.po" + if po_file.is_file(): + loaded = _parse_po_file(po_file) + if loaded: + labels.update(loaded) + break + + ts_candidates = [ + root / "ok" / "gui" / "i18n" / "zh_CN.ts", + root / "_internal" / "ok" / "gui" / "i18n" / "zh_CN.ts", + root / "data" / "apps" / "ok-nte" / "repo" / "ok" / "gui" / "i18n" / "zh_CN.ts", + ] + for ts_file in ts_candidates: + if ts_file.is_file(): + loaded = _parse_ts_file(ts_file) + if loaded: + labels.update(loaded) + break + + return labels + + +# ─── 通用兜底标签(JSON 中不出现但前端需要) ───────────────────────────── + +_FALLBACK_LABELS: dict[str, str] = { + "Yes": "是", + "No": "否", + "Auto": "自动", + "None": "无", +} + + +# ─── 手工维护:下拉 / 多选的可选项列表 ─────────────────────────────────── +# +# OK-NTE 打包后源码不可读,JSON 配置文件只存当前值不存侯选列表, +# 因此下拉 / 多选字段的选项必须在这里声明。 +# 布尔、整数、文本字段无需声明——自动从 JSON 值推断类型。 + +SELECT_OPTIONS: dict[str, dict[str, list[str]]] = { + # ── 任务配置 ── + "DailyTask.json": { + "任务类型": ["经验与甲硬币", "异能升级材料", "弧盘突破材料", "空幕"], + "具体奖励目标": ["角色经验", "弧盘经验", "甲硬币"], + "一咖舍任务": ["不执行", "领取/补货一咖舍", "运行一咖舍自动化"], + }, + "AnomalyTask.json": { + "任务类型": ["经验与甲硬币", "异能升级材料", "弧盘突破材料", "空幕"], + "具体奖励目标": ["角色经验", "弧盘经验", "甲硬币"], + }, + "CoffeeTask.json": { + "补货时长": ["auto", "2小时", "4小时", "8小时", "24小时"], + "商品位数量": ["auto", "1", "2", "3", "4", "5"], + "价格表": ["auto", "disabled"], + }, + "FishingTask.json": { + "控条模式": ["长按", "点按"], + }, + "AutoHeistTask.json": { + "路径": [ + "路径1(路线参考自B站UP: 早柚大魔王丶)", + "路径2(在路径1基础上优化了大厅到办公层的路线)", + ], + "战斗角色": ["1", "2", "3", "4"], + "跑图角色": ["1", "2", "3", "4"], + "避战角色": ["1", "2", "3", "4"], + "避战方法": ["长按shift", "长按攻击"], + }, + # ── 全局配置 ── + "Basic Options.json": { + "Use DirectML": ["Auto", "Yes", "No"], + "Start/Stop": ["None", "F9", "F10", "F11", "F12"], + "Blur Algorithm": ["Blur", "Inpaint"], + }, +} + + +def _get_select_options(filename: str, field_name: str) -> list[str] | None: + """获取指定字段的下拉 / 多选选项列表。""" + return SELECT_OPTIONS.get(filename, {}).get(field_name) + + +# ─── 文件注册表 ─────────────────────────────────────────────────────────── + +CONFIG_GROUPS = { + "任务配置": [ + "LauncherTask.json", + "DailyTask.json", + "CoffeeTask.json", + "FishingTask.json", + "AnomalyTask.json", + "RhythmTask.json", + "OwnerSelectionTask.json", + "AutoHeistTask.json", + "DarkTask.json", + "DiagnosisTask.json", + ], + "触发配置": [ + "AutoCombatTask.json", + "AutoLoginTask.json", + "FastTravelTask.json", + "HeistTask.json", + "SkipDialogTask.json", + "SoundTriggerTask.json", + ], + "全局配置": [ + "Game Hotkey Config.json", + "Monthly Card Config.json", + "Sound Trigger Config.json", + "Basic Options.json", + ], +} + +CONFIG_DISPLAY_NAMES: dict[str, str] = { + "LauncherTask.json": "启动游戏", + "DailyTask.json": "日常任务", + "CoffeeTask.json": "一咖舍自动化", + "FishingTask.json": "自动钓鱼", + "AnomalyTask.json": "异象界域", + "RhythmTask.json": "自动音游", + "OwnerSelectionTask.json": "业主选拔", + "AutoHeistTask.json": "自动粉爪大劫案", + "DarkTask.json": "暗域任务", + "DiagnosisTask.json": "诊断", + "AutoCombatTask.json": "自动战斗触发", + "AutoLoginTask.json": "自动登录触发", + "FastTravelTask.json": "快速传送触发", + "HeistTask.json": "粉爪大劫案触发", + "SkipDialogTask.json": "跳过对话触发", + "SoundTriggerTask.json": "声音触发", + "Game Hotkey Config.json": "游戏快捷键", + "Monthly Card Config.json": "小月卡设置", + "Sound Trigger Config.json": "声音触发设置", + "Basic Options.json": "基本设置", +} + +TASK_INDEX_MAP: dict[str, int] = { + "LauncherTask.json": 1, + "DailyTask.json": 2, + "CoffeeTask.json": 3, + "FishingTask.json": 4, + "AnomalyTask.json": 5, + "RhythmTask.json": 6, + "OwnerSelectionTask.json": 7, + "AutoHeistTask.json": 8, + "DarkTask.json": 9, + "DiagnosisTask.json": 11, +} + + +# ─── JSON 字段自动发现 ──────────────────────────────────────────────────── + +def _infer_field_type(value: Any) -> str: + """从 JSON 值推断前端字段类型。""" + if isinstance(value, bool): + return "bool" + if isinstance(value, int): + return "int" + if isinstance(value, float): + return "float" + if isinstance(value, list): + return "list" + return "string" + + +def _translate(key: str, labels: dict[str, str]) -> str: + """查找翻译:OK-NTE 标签 > 兜底标签 > 原始 key。""" + if key in labels: + return labels[key] + if key in _FALLBACK_LABELS: + return _FALLBACK_LABELS[key] + return key + + +def build_fields_for_config( + filename: str, + json_data: dict[str, Any], + option_labels: dict[str, str], +) -> list[dict[str, Any]]: + """从 JSON 数据 + 选项映射 + 翻译标签构建前端字段列表。 + + 逻辑: + 1. 遍历 JSON 中的字段 → 根据值推断类型 + 2. 若字段在 SELECT_OPTIONS 中有定义 → 设为 select / list 并附选项 + 3. 若字段在翻译中有映射 → 用翻译作为 label + 4. SELECT_OPTIONS 中定义但 JSON 中没有的字段 → 也加入(新字段,值为 None) + """ + seen: set[str] = set() + + def _is_internal(name: str) -> bool: + """OK-NTE 框架内部字段(_enabled 等),不暴露给 MAS 用户编辑。""" + return name.startswith("_") + + def make_field(name: str, raw_value: Any) -> dict[str, Any]: + seen.add(name) + opts = _get_select_options(filename, name) + + if opts is not None: + # 下拉或多选 + field_type = "list" if isinstance(raw_value, list) else "select" + return { + "name": name, + "type": field_type, + "label": _translate(name, option_labels), + "description": "", + "value": raw_value, + "options": opts, + "min": None, + "max": None, + "step": None, + } + + # 普通字段:从 JSON 值推断类型 + field_type = _infer_field_type(raw_value) + return { + "name": name, + "type": field_type, + "label": _translate(name, option_labels), + "description": "", + "value": raw_value, + "options": None, + "min": None, + "max": None, + "step": None, + } + + fields = [ + make_field(k, v) + for k, v in json_data.items() + if not _is_internal(k) # 屏蔽 _enabled 等 OK-NTE 框架内部字段 + ] + + # 补充:SELECT_OPTIONS 中有定义但 JSON 中没有的字段(OK-NTE 新增配置项) + known_options = SELECT_OPTIONS.get(filename, {}) + for name in known_options: + if name not in seen and not _is_internal(name): + fields.append(make_field(name, None)) + + return fields + + +# ─── API 辅助函数 ───────────────────────────────────────────────────────── + +def get_all_config_info() -> list[dict[str, Any]]: + """获取所有配置文件的元信息(用于前端列表展示)。""" + result = [] + for group_name, filenames in CONFIG_GROUPS.items(): + for filename in filenames: + # 字段数量 = JSON 中已有的 + SELECT_OPTIONS 中新增的 + field_count = len(SELECT_OPTIONS.get(filename, {})) + result.append({ + "filename": filename, + "displayName": CONFIG_DISPLAY_NAMES.get(filename, filename), + "group": group_name, + "taskIndex": TASK_INDEX_MAP.get(filename), + "fieldCount": max(field_count, 1), # 至少 1,避免显示 0 + }) + return result diff --git a/app/task/OkNte/manager.py b/app/task/OkNte/manager.py new file mode 100644 index 000000000..c0cc9e9d3 --- /dev/null +++ b/app/task/OkNte/manager.py @@ -0,0 +1,288 @@ +# 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 . + +import uuid +import shutil +from contextlib import suppress + +from pathlib import Path + +from app.core import Config +from app.models.task import TaskExecuteBase, ScriptItem, UserItem +from app.models.config import OkNteConfig, OkNteUserConfig +from app.models.ConfigBase import MultipleConfig +from app.utils import get_logger, ProcessManager + +from .AutoProxy import AutoProxyTask +from .ScriptConfig import ScriptConfigTask + +logger = get_logger("OK-NTE 调度器") + +METHOD_BOOK: dict[str, type[AutoProxyTask | ScriptConfigTask]] = { + "AutoProxy": AutoProxyTask, + "ScriptConfig": ScriptConfigTask, +} + + +class OkNteManager(TaskExecuteBase): + """OK-NTE 控制器(ok-script 线)""" + + def __init__(self, script_info: ScriptItem): + super().__init__() + + if script_info.task_info is None: + raise RuntimeError("ScriptItem 未绑定到 TaskItem") + + self.task_info = script_info.task_info + self.script_info = script_info + self.check_result = "-" + self.temp_path: Path | None = None + self.script_config_path: Path | None = None + self.script_config: OkNteConfig | None = None + self.user_config: MultipleConfig[OkNteUserConfig] | None = None + self.game_manager: ProcessManager | None = None + self.had_original_script_config = False + self.crashed = False + + async def check(self) -> str: + if self.task_info.mode not in METHOD_BOOK: + return "不支持的任务模式, 请检查任务配置!" + + script_uid = uuid.UUID(self.script_info.script_id) + script_config = Config.ScriptConfig[script_uid] + if not isinstance(script_config, OkNteConfig): + return "脚本配置类型错误, 不是 OK-NTE 类型" + + if self.task_info.mode == "ScriptConfig": + if not Path(script_config.get("Info", "RootPath")).is_dir(): + return "请设置 OK-NTE 脚本路径" + if not Path(script_config.get("Script", "ScriptPath")).is_file(): + return "请设置 OK-NTE 脚本路径" + if not str(script_config.get("Script", "ConfigPath") or "").strip(): + return "请设置 OK-NTE 配置路径" + if self.task_info.user_id and self.task_info.user_id != "Default": + try: + user_uid = uuid.UUID(self.task_info.user_id) + except ValueError: + return "OK-NTE 用户不存在,请刷新后重试" + if user_uid not in script_config.UserData: + return "OK-NTE 用户不存在,请刷新后重试" + + # AutoProxy 模式只做用户列表可用性校验;逐用户配置文件检查放到 AutoProxyTask.check() + if self.task_info.mode == "AutoProxy": + if (not self.script_info.user_list) or ( + self.script_info.user_list + and self.script_info.user_list[0].name == "暂未加载" + ): + self.script_info.user_list = [ + UserItem(user_id=str(uid), name=config.get("Info", "Name"), status="等待") + for uid, config in Config.ScriptConfig[script_uid].UserData.items() + if config.get("Info", "Status") + and config.get("Info", "RemainedDay") != 0 + ] + if not self.script_info.user_list: + return "当前没有可执行的用户,请先添加并启用用户" + + return "Pass" + + async def prepare(self): + script_uid = uuid.UUID(self.script_info.script_id) + await Config.ScriptConfig[script_uid].lock() + script_config = Config.ScriptConfig[script_uid] + if not isinstance(script_config, OkNteConfig): + raise TypeError("脚本配置类型错误") + + self.script_config = script_config + # 任务期使用独立副本,避免在 ScriptConfig 已锁时写 UserData(对齐 General) + self.user_config = MultipleConfig([OkNteUserConfig]) + await self.user_config.load(await self.script_config.UserData.toDict()) + logger.success(f"{self.script_info.script_id} 已锁定,OK-NTE 用户配置已提取") + + if self.task_info.mode == "ScriptConfig": + target_user_id = self.task_info.user_id or "Default" + target_user_name = "" + with suppress(ValueError): + target_user_uid = uuid.UUID(target_user_id) + if target_user_uid in self.user_config: + target_user_name = self.user_config[target_user_uid].get( + "Info", "Name" + ) + self.script_info.user_list = [ + UserItem(user_id=target_user_id, name=target_user_name, status="等待") + ] + else: + # 构建用户列表:遍历脚本用户,筛选启用且剩余天数不为 0 的 + self.script_info.user_list = [ + UserItem( + user_id=str(uid), name=config.get("Info", "Name"), status="等待" + ) + for uid, config in self.user_config.items() + if config.get("Info", "Status") + and config.get("Info", "RemainedDay") != 0 + ] + + # Enabled=游戏管理总开关;LaunchBeforeTask/CloseOnFinish=启动与收尾子项(可单独开启) + self.game_manager = None + if self.script_config.get("Game", "Enabled") and ( + self.script_config.get("Game", "LaunchBeforeTask") + or self.script_config.get("Game", "CloseOnFinish") + ): + self.game_manager = ProcessManager() + + if self.task_info.mode in ("AutoProxy", "ScriptConfig"): + self.script_config_path = Path( + self.script_config.get("Script", "ConfigPath") + ) + self.temp_path = Path.cwd() / f"data/{self.script_info.script_id}/Temp" + shutil.rmtree(self.temp_path, ignore_errors=True) + self.temp_path.mkdir(parents=True, exist_ok=True) + if self.script_config_path.exists(): + self.had_original_script_config = True + if self.script_config.get("Script", "ConfigPathMode") == "Folder": + shutil.copytree( + self.script_config_path, self.temp_path, dirs_exist_ok=True + ) + elif self.script_config.get("Script", "ConfigPathMode") == "File": + shutil.copy(self.script_config_path, self.temp_path / "config.temp") + + async def _restore_script_config_from_temp(self) -> None: + if not ( + self.task_info.mode in ("AutoProxy", "ScriptConfig") + and self.temp_path + and self.temp_path.exists() + and self.script_config_path + and self.script_config + ): + return + if self.script_config.get("Script", "ConfigPathMode") == "Folder": + if not self.had_original_script_config: + logger.info(f"清理任务期写入的 OK-NTE 脚本配置目录: {self.script_config_path}") + shutil.rmtree(self.script_config_path, ignore_errors=True) + else: + logger.info(f"复原 OK-NTE 脚本配置文件: {self.temp_path}") + tmp_dst = self.script_config_path.with_name( + self.script_config_path.name + ".tmp" + ) + shutil.rmtree(tmp_dst, ignore_errors=True) + shutil.copytree(self.temp_path, tmp_dst, dirs_exist_ok=True) + shutil.rmtree(self.script_config_path, ignore_errors=True) + tmp_dst.rename(self.script_config_path) + elif self.script_config.get("Script", "ConfigPathMode") == "File": + if (self.temp_path / "config.temp").exists(): + logger.info(f"复原 OK-NTE 脚本配置文件: {self.temp_path / 'config.temp'}") + shutil.copy(self.temp_path / "config.temp", self.script_config_path) + elif not self.had_original_script_config: + logger.info(f"清理任务期写入的 OK-NTE 脚本配置文件: {self.script_config_path}") + with suppress(FileNotFoundError): + self.script_config_path.unlink() + shutil.rmtree(self.temp_path, ignore_errors=True) + + async def main_task(self): + self.check_result = await self.check() + if self.check_result != "Pass": + self.script_info.status = "异常" + await Config.send_websocket_message( + id=self.task_info.task_id, type="Info", data={"Error": self.check_result} + ) + return + + await self.prepare() + if self.script_config is None or self.user_config is None: + raise RuntimeError("OK-NTE 用户配置未完成初始化") + + method_cls = METHOD_BOOK[self.task_info.mode] + for self.script_info.current_index in range(len(self.script_info.user_list)): + method = method_cls( + script_info=self.script_info, + script_config=self.script_config, # type: ignore[arg-type] + user_config=self.user_config, # type: ignore[arg-type] + game_manager=self.game_manager, + ) + + sub_check = await method.check() + if sub_check != "Pass": + self.check_result = sub_check + current_user = self.script_info.user_list[self.script_info.current_index] + if current_user.status == "等待": + current_user.status = "异常" + await Config.send_websocket_message( + id=self.task_info.task_id, type="Info", data={"Error": sub_check} + ) + continue + + await self.spawn(method) + + async def final_task(self): + script_uid = uuid.UUID(self.script_info.script_id) + script_cfg = Config.ScriptConfig[script_uid] + + try: + await self._restore_script_config_from_temp() + + # 先解锁,再写回 UserData(load() 在锁定状态下会抛异常) + if script_cfg.is_locked: + await script_cfg.unlock() + + if self.task_info.mode == "AutoProxy" and self.user_config is not None: + await script_cfg.UserData.load(await self.user_config.toDict()) + + if self.crashed: + self.script_info.status = "异常" + return + + if self.check_result != "Pass" and not any( + user.status == "完成" for user in self.script_info.user_list + ): + self.script_info.status = "异常" + return + + if any(user.status == "异常" for user in self.script_info.user_list): + self.script_info.status = "异常" + else: + self.script_info.status = "完成" + finally: + if script_cfg.is_locked: + with suppress(Exception): + await script_cfg.unlock() + + async def on_crash(self, e: Exception): + self.crashed = True + self.script_info.status = "异常" + logger.exception(f"OK-NTE任务出现异常: {e}") + script_uid = uuid.UUID(self.script_info.script_id) + + await self._restore_script_config_from_temp() + + # 先解锁,再写回 UserData(load() 在锁定状态下会抛异常) + script_cfg = Config.ScriptConfig[script_uid] + if script_cfg.is_locked: + with suppress(Exception): + await script_cfg.unlock() + + try: + if self.task_info.mode == "AutoProxy" and self.user_config is not None: + await script_cfg.UserData.load( + await self.user_config.toDict() + ) + except Exception: + logger.exception("on_crash 写回 UserConfig 失败,放弃本次状态变更") + await Config.send_websocket_message( + id=self.task_info.task_id, + type="Info", + data={"Error": f"OK-NTE任务出现异常: {e}"}, + ) diff --git a/app/task/__init__.py b/app/task/__init__.py index ae0414f29..f68e5ef07 100644 --- a/app/task/__init__.py +++ b/app/task/__init__.py @@ -27,6 +27,7 @@ from .M9A import M9AManager from .general import GeneralManager from .Okww import OkwwManager +from .OkNte import OkNteManager from .HSR import HSRManager __all__ = [ @@ -36,5 +37,6 @@ "GeneralManager", "MaaEndManager", "OkwwManager", + "OkNteManager", "HSRManager", ] diff --git a/app/utils/constants.py b/app/utils/constants.py index e89ce498d..dea82f087 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -41,6 +41,7 @@ "MaaEndConfig": "MaaEnd", "GeneralConfig": "通用", "OkwwConfig": "ok-ww", + "OkNteConfig": "OK-NTE", "M9AConfig": "M9A", "M9AUserConfig": "M9A", "HSRConfig": "HSR", diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 330bdb528..181a77d46 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -11,7 +11,9 @@ export type { AbyssSnapshotImportItem } from './models/AbyssSnapshotImportItem'; export type { AbyssSnapshotImportOut } from './models/AbyssSnapshotImportOut'; export type { ADBScreenshotIn } from './models/ADBScreenshotIn'; export type { ADBScreenshotOut } from './models/ADBScreenshotOut'; +export type { Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post } from './models/Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post'; export type { Body_batch_update_okww_configs_api_scripts_okww_configs_batch_update_post } from './models/Body_batch_update_okww_configs_api_scripts_okww_configs_batch_update_post'; +export type { Body_update_oknte_config_api_scripts_oknte_configs_update_post } from './models/Body_update_oknte_config_api_scripts_oknte_configs_update_post'; export type { Body_update_okww_config_api_scripts_okww_configs_update_post } from './models/Body_update_okww_config_api_scripts_okww_configs_update_post'; export type { CheckImageAllIn } from './models/CheckImageAllIn'; export type { CheckImageAnyIn } from './models/CheckImageAnyIn'; @@ -114,6 +116,16 @@ 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 { OkNteConfig } from './models/OkNteConfig'; +export type { OkNteConfig_Game } from './models/OkNteConfig_Game'; +export type { OkNteConfig_Info } from './models/OkNteConfig_Info'; +export type { OkNteConfig_Run } from './models/OkNteConfig_Run'; +export type { OkNteConfig_Script } from './models/OkNteConfig_Script'; +export type { OkNteUserConfig } from './models/OkNteUserConfig'; +export type { OkNteUserConfig_Data } from './models/OkNteUserConfig_Data'; +export type { OkNteUserConfig_Info } from './models/OkNteUserConfig_Info'; +export type { OkNteUserConfig_Notify } from './models/OkNteUserConfig_Notify'; +export type { OkNteUserConfig_Task } from './models/OkNteUserConfig_Task'; export type { OkwwConfig } from './models/OkwwConfig'; export type { OkwwConfig_Game } from './models/OkwwConfig_Game'; export type { OkwwConfig_Info } from './models/OkwwConfig_Info'; @@ -241,6 +253,7 @@ export { GetService } from './services/GetService'; export { HsrService } from './services/HsrService'; export { M9AService } from './services/M9AService'; export { OcrService } from './services/OcrService'; +export { OknteService } from './services/OknteService'; export { OkwwService } from './services/OkwwService'; export { UpdateService } from './services/UpdateService'; export { WebSocketService } from './services/WebSocketService'; diff --git a/frontend/src/api/models/AbyssSnapshotImportItem.ts b/frontend/src/api/models/AbyssSnapshotImportItem.ts index 32cf5011b..7457a1fb0 100644 --- a/frontend/src/api/models/AbyssSnapshotImportItem.ts +++ b/frontend/src/api/models/AbyssSnapshotImportItem.ts @@ -27,3 +27,4 @@ export type AbyssSnapshotImportItem = { */ error?: (string | null); }; + diff --git a/frontend/src/api/models/AbyssSnapshotImportOut.ts b/frontend/src/api/models/AbyssSnapshotImportOut.ts index a86d72204..d5d2030f1 100644 --- a/frontend/src/api/models/AbyssSnapshotImportOut.ts +++ b/frontend/src/api/models/AbyssSnapshotImportOut.ts @@ -33,3 +33,4 @@ export type AbyssSnapshotImportOut = { */ updatedUserData: HSRUserConfig; }; + diff --git a/frontend/src/api/models/Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post.ts b/frontend/src/api/models/Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post.ts new file mode 100644 index 000000000..fbeb1ab83 --- /dev/null +++ b/frontend/src/api/models/Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post = { + script_id: string; + user_id: string; + configs: Record; +}; + diff --git a/frontend/src/api/models/Body_update_oknte_config_api_scripts_oknte_configs_update_post.ts b/frontend/src/api/models/Body_update_oknte_config_api_scripts_oknte_configs_update_post.ts new file mode 100644 index 000000000..384441600 --- /dev/null +++ b/frontend/src/api/models/Body_update_oknte_config_api_scripts_oknte_configs_update_post.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Body_update_oknte_config_api_scripts_oknte_configs_update_post = { + script_id: string; + user_id: string; + filename: string; + data: Record; +}; + diff --git a/frontend/src/api/models/HSRConfig.ts b/frontend/src/api/models/HSRConfig.ts index f0bee647a..205c57e7d 100644 --- a/frontend/src/api/models/HSRConfig.ts +++ b/frontend/src/api/models/HSRConfig.ts @@ -24,3 +24,4 @@ export type HSRConfig = { */ TaskMapping?: (HSRConfig_TaskMapping | null); }; + diff --git a/frontend/src/api/models/HSRConfig_Game.ts b/frontend/src/api/models/HSRConfig_Game.ts index 845679ad7..30806a920 100644 --- a/frontend/src/api/models/HSRConfig_Game.ts +++ b/frontend/src/api/models/HSRConfig_Game.ts @@ -16,3 +16,4 @@ export type HSRConfig_Game = { */ WaitTime?: (number | null); }; + diff --git a/frontend/src/api/models/HSRConfig_Info.ts b/frontend/src/api/models/HSRConfig_Info.ts index e542f3f95..d0e12b8b1 100644 --- a/frontend/src/api/models/HSRConfig_Info.ts +++ b/frontend/src/api/models/HSRConfig_Info.ts @@ -16,3 +16,4 @@ export type HSRConfig_Info = { */ SRAPath?: (string | null); }; + diff --git a/frontend/src/api/models/HSRConfig_Run.ts b/frontend/src/api/models/HSRConfig_Run.ts index 7e40cd238..bafc52708 100644 --- a/frontend/src/api/models/HSRConfig_Run.ts +++ b/frontend/src/api/models/HSRConfig_Run.ts @@ -24,3 +24,4 @@ export type HSRConfig_Run = { */ LowPerformanceMode?: (boolean | null); }; + diff --git a/frontend/src/api/models/HSRConfig_TaskMapping.ts b/frontend/src/api/models/HSRConfig_TaskMapping.ts index 9b51b5f09..ca2763af2 100644 --- a/frontend/src/api/models/HSRConfig_TaskMapping.ts +++ b/frontend/src/api/models/HSRConfig_TaskMapping.ts @@ -20,3 +20,4 @@ export type HSRConfig_TaskMapping = { */ CurrencyWars?: ('M7A' | 'SRA' | null); }; + diff --git a/frontend/src/api/models/HSRDynamicStageCategory.ts b/frontend/src/api/models/HSRDynamicStageCategory.ts index cb893c2cc..7fc88c230 100644 --- a/frontend/src/api/models/HSRDynamicStageCategory.ts +++ b/frontend/src/api/models/HSRDynamicStageCategory.ts @@ -25,3 +25,4 @@ export type HSRDynamicStageCategory = { */ options?: Array; }; + diff --git a/frontend/src/api/models/HSRDynamicStageM7A.ts b/frontend/src/api/models/HSRDynamicStageM7A.ts index 9fbf73e23..ee4094230 100644 --- a/frontend/src/api/models/HSRDynamicStageM7A.ts +++ b/frontend/src/api/models/HSRDynamicStageM7A.ts @@ -12,3 +12,4 @@ export type HSRDynamicStageM7A = { */ instanceName?: (string | null); }; + diff --git a/frontend/src/api/models/HSRDynamicStageOption.ts b/frontend/src/api/models/HSRDynamicStageOption.ts index 8bfcc94e8..a40d78541 100644 --- a/frontend/src/api/models/HSRDynamicStageOption.ts +++ b/frontend/src/api/models/HSRDynamicStageOption.ts @@ -42,3 +42,4 @@ export type HSRDynamicStageOption = { */ sra?: (HSRDynamicStageSRA | null); }; + diff --git a/frontend/src/api/models/HSRDynamicStageSRA.ts b/frontend/src/api/models/HSRDynamicStageSRA.ts index 9469089d0..b520122e8 100644 --- a/frontend/src/api/models/HSRDynamicStageSRA.ts +++ b/frontend/src/api/models/HSRDynamicStageSRA.ts @@ -12,3 +12,4 @@ export type HSRDynamicStageSRA = { */ level?: (number | null); }; + diff --git a/frontend/src/api/models/HSRStageOptionsData.ts b/frontend/src/api/models/HSRStageOptionsData.ts index 15993a6d2..26b336acf 100644 --- a/frontend/src/api/models/HSRStageOptionsData.ts +++ b/frontend/src/api/models/HSRStageOptionsData.ts @@ -26,3 +26,4 @@ export namespace HSRStageOptionsData { SRA = 'SRA', } } + diff --git a/frontend/src/api/models/HSRStageOptionsOut.ts b/frontend/src/api/models/HSRStageOptionsOut.ts index 01a48251c..90c6fb003 100644 --- a/frontend/src/api/models/HSRStageOptionsOut.ts +++ b/frontend/src/api/models/HSRStageOptionsOut.ts @@ -21,3 +21,4 @@ export type HSRStageOptionsOut = { */ data?: (HSRStageOptionsData | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig.ts b/frontend/src/api/models/HSRUserConfig.ts index a1335bcfb..694b37792 100644 --- a/frontend/src/api/models/HSRUserConfig.ts +++ b/frontend/src/api/models/HSRUserConfig.ts @@ -39,3 +39,4 @@ export type HSRUserConfig = { */ Abyss?: (HSRUserConfig_Abyss | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_Abyss.ts b/frontend/src/api/models/HSRUserConfig_Abyss.ts index 778e3f005..41f4b4a08 100644 --- a/frontend/src/api/models/HSRUserConfig_Abyss.ts +++ b/frontend/src/api/models/HSRUserConfig_Abyss.ts @@ -11,3 +11,4 @@ export type HSRUserConfig_Abyss = { */ Snapshots?: (string | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_Data.ts b/frontend/src/api/models/HSRUserConfig_Data.ts index 8202cdcc4..270322d36 100644 --- a/frontend/src/api/models/HSRUserConfig_Data.ts +++ b/frontend/src/api/models/HSRUserConfig_Data.ts @@ -52,3 +52,4 @@ export type HSRUserConfig_Data = { */ AbyssLastCompletionDate?: (string | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_Info.ts b/frontend/src/api/models/HSRUserConfig_Info.ts index 757555d90..0abbf8020 100644 --- a/frontend/src/api/models/HSRUserConfig_Info.ts +++ b/frontend/src/api/models/HSRUserConfig_Info.ts @@ -36,3 +36,4 @@ export type HSRUserConfig_Info = { */ Tag?: (string | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_Notify.ts b/frontend/src/api/models/HSRUserConfig_Notify.ts index ee6b6eb80..6638d238a 100644 --- a/frontend/src/api/models/HSRUserConfig_Notify.ts +++ b/frontend/src/api/models/HSRUserConfig_Notify.ts @@ -28,3 +28,4 @@ export type HSRUserConfig_Notify = { */ ServerChanKey?: (string | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_Stage.ts b/frontend/src/api/models/HSRUserConfig_Stage.ts index 9763660e3..c05f3d770 100644 --- a/frontend/src/api/models/HSRUserConfig_Stage.ts +++ b/frontend/src/api/models/HSRUserConfig_Stage.ts @@ -16,3 +16,4 @@ export type HSRUserConfig_Stage = { */ ScriptEchoOfWar?: (string | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_TaskOpt.ts b/frontend/src/api/models/HSRUserConfig_TaskOpt.ts index 83450b40f..0b2d4c565 100644 --- a/frontend/src/api/models/HSRUserConfig_TaskOpt.ts +++ b/frontend/src/api/models/HSRUserConfig_TaskOpt.ts @@ -8,3 +8,4 @@ export type HSRUserConfig_TaskOpt = { */ EchoOfWarWeekday?: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday' | null); }; + diff --git a/frontend/src/api/models/HSRUserConfig_TaskSwitch.ts b/frontend/src/api/models/HSRUserConfig_TaskSwitch.ts index 1dcb7c105..e70794566 100644 --- a/frontend/src/api/models/HSRUserConfig_TaskSwitch.ts +++ b/frontend/src/api/models/HSRUserConfig_TaskSwitch.ts @@ -24,3 +24,4 @@ export type HSRUserConfig_TaskSwitch = { */ ForgottenHall?: (boolean | null); }; + diff --git a/frontend/src/api/models/OkNteConfig.ts b/frontend/src/api/models/OkNteConfig.ts new file mode 100644 index 000000000..a924200d7 --- /dev/null +++ b/frontend/src/api/models/OkNteConfig.ts @@ -0,0 +1,27 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { OkNteConfig_Game } from './OkNteConfig_Game'; +import type { OkNteConfig_Info } from './OkNteConfig_Info'; +import type { OkNteConfig_Run } from './OkNteConfig_Run'; +import type { OkNteConfig_Script } from './OkNteConfig_Script'; +export type OkNteConfig = { + /** + * 脚本基础信息 + */ + Info?: (OkNteConfig_Info | null); + /** + * 脚本配置 + */ + Script?: (OkNteConfig_Script | null); + /** + * 游戏配置 + */ + Game?: (OkNteConfig_Game | null); + /** + * 运行配置 + */ + Run?: (OkNteConfig_Run | null); +}; + diff --git a/frontend/src/api/models/OkNteConfig_Game.ts b/frontend/src/api/models/OkNteConfig_Game.ts new file mode 100644 index 000000000..b7409c9f2 --- /dev/null +++ b/frontend/src/api/models/OkNteConfig_Game.ts @@ -0,0 +1,58 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 游戏配置(复用通用字段) + */ +export type OkNteConfig_Game = { + /** + * 游戏/模拟器相关功能是否启用 + */ + Enabled?: (boolean | null); + /** + * 类型: PC端, URL协议 + */ + Type?: ('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); + /** + * 任务开始前是否由 MAS 启动游戏 + */ + LaunchBeforeTask?: (boolean | null); + /** + * 任务结束后是否关闭游戏 + */ + CloseOnFinish?: (boolean | null); +}; + diff --git a/frontend/src/api/models/OkNteConfig_Info.ts b/frontend/src/api/models/OkNteConfig_Info.ts new file mode 100644 index 000000000..3894a1459 --- /dev/null +++ b/frontend/src/api/models/OkNteConfig_Info.ts @@ -0,0 +1,18 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 脚本基础信息(复用通用字段) + */ +export type OkNteConfig_Info = { + /** + * 脚本名称 + */ + Name?: (string | null); + /** + * 脚本根目录 + */ + RootPath?: (string | null); +}; + diff --git a/frontend/src/api/models/OkNteConfig_Run.ts b/frontend/src/api/models/OkNteConfig_Run.ts new file mode 100644 index 000000000..c5fb96e2c --- /dev/null +++ b/frontend/src/api/models/OkNteConfig_Run.ts @@ -0,0 +1,22 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 运行配置(复用通用字段) + */ +export type OkNteConfig_Run = { + /** + * 每日代理次数限制 + */ + ProxyTimesLimit?: (number | null); + /** + * 重试次数限制 + */ + RunTimesLimit?: (number | null); + /** + * 日志超时限制 + */ + RunTimeLimit?: (number | null); +}; + diff --git a/frontend/src/api/models/OkNteConfig_Script.ts b/frontend/src/api/models/OkNteConfig_Script.ts new file mode 100644 index 000000000..ec5ab966d --- /dev/null +++ b/frontend/src/api/models/OkNteConfig_Script.ts @@ -0,0 +1,74 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 脚本配置(复用通用字段) + */ +export type OkNteConfig_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/OkNteUserConfig.ts b/frontend/src/api/models/OkNteUserConfig.ts new file mode 100644 index 000000000..d29b066b7 --- /dev/null +++ b/frontend/src/api/models/OkNteUserConfig.ts @@ -0,0 +1,27 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { OkNteUserConfig_Data } from './OkNteUserConfig_Data'; +import type { OkNteUserConfig_Info } from './OkNteUserConfig_Info'; +import type { OkNteUserConfig_Notify } from './OkNteUserConfig_Notify'; +import type { OkNteUserConfig_Task } from './OkNteUserConfig_Task'; +export type OkNteUserConfig = { + /** + * 用户信息 + */ + Info?: (OkNteUserConfig_Info | null); + /** + * 任务配置 + */ + Task?: (OkNteUserConfig_Task | null); + /** + * 用户数据 + */ + Data?: (OkNteUserConfig_Data | null); + /** + * 单独通知 + */ + Notify?: (OkNteUserConfig_Notify | null); +}; + diff --git a/frontend/src/api/models/OkNteUserConfig_Data.ts b/frontend/src/api/models/OkNteUserConfig_Data.ts new file mode 100644 index 000000000..a896c02a1 --- /dev/null +++ b/frontend/src/api/models/OkNteUserConfig_Data.ts @@ -0,0 +1,26 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 用户数据(复用通用字段) + */ +export type OkNteUserConfig_Data = { + /** + * 上次代理日期 + */ + LastProxyDate?: (string | null); + /** + * 代理次数 + */ + ProxyTimes?: (number | null); + /** + * 上次代理状态(未知/成功/失败) + */ + LastProxyStatus?: (string | null); + /** + * 上次运行的 ok-nte 任务序号(-t N) + */ + LastTaskIndex?: (number | null); +}; + diff --git a/frontend/src/api/models/OkNteUserConfig_Info.ts b/frontend/src/api/models/OkNteUserConfig_Info.ts new file mode 100644 index 000000000..0d3dd1d79 --- /dev/null +++ b/frontend/src/api/models/OkNteUserConfig_Info.ts @@ -0,0 +1,62 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 用户信息(复用通用字段) + */ +export type OkNteUserConfig_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); + /** + * 账号 + */ + Id?: (string | null); + /** + * 密码 + */ + Password?: (string | null); + /** + * 用户配置模式(简洁/详细) + */ + Mode?: ('简洁' | '详细' | null); + /** + * 游戏资源 + */ + Resource?: (string | null); +}; + diff --git a/frontend/src/api/models/OkNteUserConfig_Notify.ts b/frontend/src/api/models/OkNteUserConfig_Notify.ts new file mode 100644 index 000000000..774edb52e --- /dev/null +++ b/frontend/src/api/models/OkNteUserConfig_Notify.ts @@ -0,0 +1,34 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * OK-NTE 用户通知(复用通用字段) + */ +export type OkNteUserConfig_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/OkNteUserConfig_Task.ts b/frontend/src/api/models/OkNteUserConfig_Task.ts new file mode 100644 index 000000000..aec25c3fc --- /dev/null +++ b/frontend/src/api/models/OkNteUserConfig_Task.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type OkNteUserConfig_Task = { + /** + * 启动后执行第 N 个任务(-t N,从 1 开始) + */ + TaskIndex?: (number | null); + /** + * 任务结束后退出(-e) + */ + ExitOnFinish?: (boolean | null); +}; + diff --git a/frontend/src/api/models/ScriptCreateIn.ts b/frontend/src/api/models/ScriptCreateIn.ts index 725eb9f32..9a812c274 100644 --- a/frontend/src/api/models/ScriptCreateIn.ts +++ b/frontend/src/api/models/ScriptCreateIn.ts @@ -4,7 +4,7 @@ /* eslint-disable */ export type ScriptCreateIn = { /** - * 脚本类型: MAA脚本, 通用脚本, OK-WW脚本, SRC脚本, MaaEnd脚本, M9A脚本, HSR脚本 + * 脚本类型: MAA脚本, 通用脚本, OK-WW脚本, OK-NTE脚本, SRC脚本, MaaEnd脚本, M9A脚本, HSR脚本 */ type: ScriptCreateIn.type; /** @@ -14,13 +14,14 @@ export type ScriptCreateIn = { }; export namespace ScriptCreateIn { /** - * 脚本类型: MAA脚本, 通用脚本, OK-WW脚本, SRC脚本, MaaEnd脚本, M9A脚本, HSR脚本 + * 脚本类型: MAA脚本, 通用脚本, OK-WW脚本, OK-NTE脚本, SRC脚本, MaaEnd脚本, M9A脚本, HSR脚本 */ export enum type { MAA = 'MAA', SRC = 'SRC', GENERAL = 'General', OKWW = 'Okww', + OK_NTE = 'OkNte', MAA_END = 'MaaEnd', M9A = 'M9A', HSR = 'HSR', diff --git a/frontend/src/api/models/ScriptCreateOut.ts b/frontend/src/api/models/ScriptCreateOut.ts index 513ece2e0..c46ff0808 100644 --- a/frontend/src/api/models/ScriptCreateOut.ts +++ b/frontend/src/api/models/ScriptCreateOut.ts @@ -7,6 +7,7 @@ import type { HSRConfig } from './HSRConfig'; import type { M9AConfig } from './M9AConfig'; import type { MaaConfig } from './MaaConfig'; import type { MaaEndConfig } from './MaaEndConfig'; +import type { OkNteConfig } from './OkNteConfig'; import type { OkwwConfig } from './OkwwConfig'; import type { SrcConfig } from './SrcConfig'; export type ScriptCreateOut = { @@ -29,6 +30,6 @@ export type ScriptCreateOut = { /** * 脚本配置数据 */ - data: (MaaConfig | SrcConfig | GeneralConfig | OkwwConfig | MaaEndConfig | M9AConfig | HSRConfig); + data: (MaaConfig | SrcConfig | GeneralConfig | OkwwConfig | OkNteConfig | MaaEndConfig | M9AConfig | HSRConfig); }; diff --git a/frontend/src/api/models/ScriptGetOut.ts b/frontend/src/api/models/ScriptGetOut.ts index 2feac2c8e..4882adbf1 100644 --- a/frontend/src/api/models/ScriptGetOut.ts +++ b/frontend/src/api/models/ScriptGetOut.ts @@ -7,6 +7,7 @@ import type { HSRConfig } from './HSRConfig'; import type { M9AConfig } from './M9AConfig'; import type { MaaConfig } from './MaaConfig'; import type { MaaEndConfig } from './MaaEndConfig'; +import type { OkNteConfig } from './OkNteConfig'; import type { OkwwConfig } from './OkwwConfig'; import type { ScriptIndexItem } from './ScriptIndexItem'; import type { SrcConfig } from './SrcConfig'; @@ -30,6 +31,6 @@ export type ScriptGetOut = { /** * 脚本数据字典, key来自于index列表的uid */ - data: Record; + data: Record; }; diff --git a/frontend/src/api/models/ScriptIndexItem.ts b/frontend/src/api/models/ScriptIndexItem.ts index a6a479ad4..c26697a72 100644 --- a/frontend/src/api/models/ScriptIndexItem.ts +++ b/frontend/src/api/models/ScriptIndexItem.ts @@ -20,6 +20,7 @@ export namespace ScriptIndexItem { MAA_CONFIG = 'MaaConfig', GENERAL_CONFIG = 'GeneralConfig', OKWW_CONFIG = 'OkwwConfig', + OK_NTE_CONFIG = 'OkNteConfig', SRC_CONFIG = 'SrcConfig', MAA_END_CONFIG = 'MaaEndConfig', M9ACONFIG = 'M9AConfig', diff --git a/frontend/src/api/models/ScriptUpdateIn.ts b/frontend/src/api/models/ScriptUpdateIn.ts index a2165431a..f9725c4a3 100644 --- a/frontend/src/api/models/ScriptUpdateIn.ts +++ b/frontend/src/api/models/ScriptUpdateIn.ts @@ -7,6 +7,7 @@ import type { HSRConfig } from './HSRConfig'; import type { M9AConfig } from './M9AConfig'; import type { MaaConfig } from './MaaConfig'; import type { MaaEndConfig } from './MaaEndConfig'; +import type { OkNteConfig } from './OkNteConfig'; import type { OkwwConfig } from './OkwwConfig'; import type { SrcConfig } from './SrcConfig'; export type ScriptUpdateIn = { @@ -17,6 +18,6 @@ export type ScriptUpdateIn = { /** * 脚本更新数据 */ - data: (MaaConfig | SrcConfig | GeneralConfig | OkwwConfig | MaaEndConfig | M9AConfig | HSRConfig); + data: (MaaConfig | SrcConfig | GeneralConfig | OkwwConfig | OkNteConfig | MaaEndConfig | M9AConfig | HSRConfig); }; diff --git a/frontend/src/api/models/UserCreateOut.ts b/frontend/src/api/models/UserCreateOut.ts index 1f1475b6a..0abd2ed5c 100644 --- a/frontend/src/api/models/UserCreateOut.ts +++ b/frontend/src/api/models/UserCreateOut.ts @@ -7,6 +7,7 @@ import type { HSRUserConfig } from './HSRUserConfig'; import type { M9AUserConfig } from './M9AUserConfig'; import type { MaaEndUserConfig } from './MaaEndUserConfig'; import type { MaaUserConfig } from './MaaUserConfig'; +import type { OkNteUserConfig } from './OkNteUserConfig'; import type { OkwwUserConfig } from './OkwwUserConfig'; import type { SrcUserConfig } from './SrcUserConfig'; export type UserCreateOut = { @@ -29,6 +30,6 @@ export type UserCreateOut = { /** * 用户配置数据 */ - data: (MaaUserConfig | SrcUserConfig | GeneralUserConfig | OkwwUserConfig | MaaEndUserConfig | M9AUserConfig | HSRUserConfig); + data: (MaaUserConfig | SrcUserConfig | GeneralUserConfig | OkwwUserConfig | OkNteUserConfig | MaaEndUserConfig | M9AUserConfig | HSRUserConfig); }; diff --git a/frontend/src/api/models/UserGetOut.ts b/frontend/src/api/models/UserGetOut.ts index 3ba9d3904..20586583c 100644 --- a/frontend/src/api/models/UserGetOut.ts +++ b/frontend/src/api/models/UserGetOut.ts @@ -7,6 +7,7 @@ import type { HSRUserConfig } from './HSRUserConfig'; import type { M9AUserConfig } from './M9AUserConfig'; import type { MaaEndUserConfig } from './MaaEndUserConfig'; import type { MaaUserConfig } from './MaaUserConfig'; +import type { OkNteUserConfig } from './OkNteUserConfig'; import type { OkwwUserConfig } from './OkwwUserConfig'; import type { SrcUserConfig } from './SrcUserConfig'; import type { UserIndexItem } from './UserIndexItem'; @@ -30,6 +31,6 @@ export type UserGetOut = { /** * 用户数据字典, key来自于index列表的uid */ - data: Record; + data: Record; }; diff --git a/frontend/src/api/models/UserImportAbyssSnapshotIn.ts b/frontend/src/api/models/UserImportAbyssSnapshotIn.ts index 4bb1d38cd..6e327fc7a 100644 --- a/frontend/src/api/models/UserImportAbyssSnapshotIn.ts +++ b/frontend/src/api/models/UserImportAbyssSnapshotIn.ts @@ -15,3 +15,4 @@ export type UserImportAbyssSnapshotIn = { */ userId: string; }; + diff --git a/frontend/src/api/models/UserIndexItem.ts b/frontend/src/api/models/UserIndexItem.ts index a6feb20db..c84745615 100644 --- a/frontend/src/api/models/UserIndexItem.ts +++ b/frontend/src/api/models/UserIndexItem.ts @@ -20,6 +20,7 @@ export namespace UserIndexItem { MAA_USER_CONFIG = 'MaaUserConfig', GENERAL_USER_CONFIG = 'GeneralUserConfig', OKWW_USER_CONFIG = 'OkwwUserConfig', + OK_NTE_USER_CONFIG = 'OkNteUserConfig', SRC_USER_CONFIG = 'SrcUserConfig', MAA_END_USER_CONFIG = 'MaaEndUserConfig', M9AUSER_CONFIG = 'M9AUserConfig', diff --git a/frontend/src/api/models/UserUpdateIn.ts b/frontend/src/api/models/UserUpdateIn.ts index 29144e795..07a530ab9 100644 --- a/frontend/src/api/models/UserUpdateIn.ts +++ b/frontend/src/api/models/UserUpdateIn.ts @@ -7,6 +7,7 @@ import type { HSRUserConfig } from './HSRUserConfig'; import type { M9AUserConfig } from './M9AUserConfig'; import type { MaaEndUserConfig } from './MaaEndUserConfig'; import type { MaaUserConfig } from './MaaUserConfig'; +import type { OkNteUserConfig } from './OkNteUserConfig'; import type { OkwwUserConfig } from './OkwwUserConfig'; import type { SrcUserConfig } from './SrcUserConfig'; export type UserUpdateIn = { @@ -21,6 +22,6 @@ export type UserUpdateIn = { /** * 用户更新数据 */ - data: (MaaUserConfig | SrcUserConfig | GeneralUserConfig | OkwwUserConfig | MaaEndUserConfig | M9AUserConfig | HSRUserConfig); + data: (MaaUserConfig | SrcUserConfig | GeneralUserConfig | OkwwUserConfig | OkNteUserConfig | MaaEndUserConfig | M9AUserConfig | HSRUserConfig); }; diff --git a/frontend/src/api/services/OknteService.ts b/frontend/src/api/services/OknteService.ts new file mode 100644 index 000000000..7fe810e39 --- /dev/null +++ b/frontend/src/api/services/OknteService.ts @@ -0,0 +1,101 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post } from '../models/Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post'; +import type { Body_update_oknte_config_api_scripts_oknte_configs_update_post } from '../models/Body_update_oknte_config_api_scripts_oknte_configs_update_post'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class OknteService { + /** + * 获取 OK-NTE 配置文件列表及 schema + * 获取 OK-NTE 配置文件列表及 schema 定义。 + * 读写用户配置目录(data/{script_id}/{user_id}/ConfigFile/), + * 若为空则自动从 ok-nte configs 目录初始化默认配置。 + * + * Args: + * script_id: OK-NTE 脚本 ID + * user_id: 用户 ID + * + * Returns: + * dict: 包含配置文件列表和 schema 的响应 + * @param scriptId + * @param userId + * @returns any Successful Response + * @throws ApiError + */ + public static getOknteConfigsListApiScriptsOknteConfigsListPost( + scriptId: string, + userId: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/oknte/configs/list', + query: { + 'script_id': scriptId, + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * 更新 OK-NTE 配置文件 + * 更新 OK-NTE 配置文件 + * + * Args: + * script_id: OK-NTE 脚本 ID + * user_id: 用户 ID + * filename: 配置文件名(如 DailyTask.json) + * data: 要更新的配置数据 + * + * Returns: + * dict: 操作结果 + * @param requestBody + * @returns any Successful Response + * @throws ApiError + */ + public static updateOknteConfigApiScriptsOknteConfigsUpdatePost( + requestBody: Body_update_oknte_config_api_scripts_oknte_configs_update_post, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/oknte/configs/update', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * 批量更新 OK-NTE 配置文件 + * 批量更新 OK-NTE 配置文件 + * + * Args: + * script_id: OK-NTE 脚本 ID + * user_id: 用户 ID + * configs: { filename: data } 格式的配置数据 + * + * Returns: + * dict: 操作结果 + * @param requestBody + * @returns any Successful Response + * @throws ApiError + */ + public static batchUpdateOknteConfigsApiScriptsOknteConfigsBatchUpdatePost( + requestBody: Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/oknte/configs/batch-update', + 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 index ce3158516..b15a3160a 100644 --- a/frontend/src/api/services/Service.ts +++ b/frontend/src/api/services/Service.ts @@ -3,7 +3,9 @@ /* tslint:disable */ /* eslint-disable */ import type { AbyssSnapshotImportOut } from '../models/AbyssSnapshotImportOut'; +import type { Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post } from '../models/Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post'; import type { Body_batch_update_okww_configs_api_scripts_okww_configs_batch_update_post } from '../models/Body_batch_update_okww_configs_api_scripts_okww_configs_batch_update_post'; +import type { Body_update_oknte_config_api_scripts_oknte_configs_update_post } from '../models/Body_update_oknte_config_api_scripts_oknte_configs_update_post'; import type { Body_update_okww_config_api_scripts_okww_configs_update_post } from '../models/Body_update_okww_config_api_scripts_okww_configs_update_post'; import type { ComboBoxOut } from '../models/ComboBoxOut'; import type { DispatchIn } from '../models/DispatchIn'; @@ -825,6 +827,96 @@ export class Service { }, }); } + /** + * 获取 OK-NTE 配置文件列表及 schema + * 获取 OK-NTE 配置文件列表及 schema 定义。 + * 读写用户配置目录(data/{script_id}/{user_id}/ConfigFile/), + * 若为空则自动从 ok-nte configs 目录初始化默认配置。 + * + * Args: + * script_id: OK-NTE 脚本 ID + * user_id: 用户 ID + * + * Returns: + * dict: 包含配置文件列表和 schema 的响应 + * @param scriptId + * @param userId + * @returns any Successful Response + * @throws ApiError + */ + public static getOknteConfigsListApiScriptsOknteConfigsListPost( + scriptId: string, + userId: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/oknte/configs/list', + query: { + 'script_id': scriptId, + 'user_id': userId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * 更新 OK-NTE 配置文件 + * 更新 OK-NTE 配置文件 + * + * Args: + * script_id: OK-NTE 脚本 ID + * user_id: 用户 ID + * filename: 配置文件名(如 DailyTask.json) + * data: 要更新的配置数据 + * + * Returns: + * dict: 操作结果 + * @param requestBody + * @returns any Successful Response + * @throws ApiError + */ + public static updateOknteConfigApiScriptsOknteConfigsUpdatePost( + requestBody: Body_update_oknte_config_api_scripts_oknte_configs_update_post, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/oknte/configs/update', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * 批量更新 OK-NTE 配置文件 + * 批量更新 OK-NTE 配置文件 + * + * Args: + * script_id: OK-NTE 脚本 ID + * user_id: 用户 ID + * configs: { filename: data } 格式的配置数据 + * + * Returns: + * dict: 操作结果 + * @param requestBody + * @returns any Successful Response + * @throws ApiError + */ + public static batchUpdateOknteConfigsApiScriptsOknteConfigsBatchUpdatePost( + requestBody: Body_batch_update_oknte_configs_api_scripts_oknte_configs_batch_update_post, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/oknte/configs/batch-update', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * 添加计划表 * @param requestBody diff --git a/frontend/src/assets/ok-nte.ico b/frontend/src/assets/ok-nte.ico new file mode 100644 index 000000000..b30510d39 Binary files /dev/null and b/frontend/src/assets/ok-nte.ico differ diff --git a/frontend/src/components/ScriptTable.vue b/frontend/src/components/ScriptTable.vue index 228bf82f1..4aa725c50 100644 --- a/frontend/src/components/ScriptTable.vue +++ b/frontend/src/components/ScriptTable.vue @@ -20,6 +20,7 @@ class="script-logo" /> + @@ -35,9 +36,11 @@ ? 'cyan' : script.type === 'Okww' ? 'blue' - : script.type === 'HSR' - ? 'purple' - : 'green' + : script.type === 'OkNte' + ? 'blue' + : script.type === 'HSR' + ? 'purple' + : 'green' " class="script-type"> {{ getScriptTypeLabel(script.type) }} @@ -189,8 +192,8 @@ {{ tag.text }} - - + + {{ tag.text }} @@ -456,6 +459,7 @@ const handleToggleUserStatus = (user: User) => { const getScriptTypeLabel = (type: Script['type']) => { if (type === 'Okww') return 'ok-ww' + if (type === 'OkNte') return 'ok-nte' return type } diff --git a/frontend/src/composables/useScriptApi.ts b/frontend/src/composables/useScriptApi.ts index b78f51a1e..7e8f2d471 100644 --- a/frontend/src/composables/useScriptApi.ts +++ b/frontend/src/composables/useScriptApi.ts @@ -6,6 +6,7 @@ import { type MaaEndConfig, type M9AConfig, type OkwwConfig, + type OkNteConfig, type SrcConfig, type HSRConfig, type HSRStageOptionsData, @@ -23,6 +24,7 @@ type ScriptListConfig = | MaaConfig | GeneralConfig | OkwwConfig + | OkNteConfig | SrcConfig | MaaEndConfig | M9AConfig @@ -36,6 +38,7 @@ const SCRIPT_CREATE_TYPE_BY_SCRIPT_TYPE: Record MaaEnd: ScriptCreateIn.type.MAA_END, M9A: ScriptCreateIn.type.M9A, Okww: ScriptCreateIn.type.OKWW, + OkNte: ScriptCreateIn.type.OK_NTE, HSR: ScriptCreateIn.type.HSR, General: ScriptCreateIn.type.GENERAL, } @@ -44,6 +47,7 @@ const SCRIPT_TYPE_BY_CONFIG_TYPE: Record = { MaaConfig: 'MAA', SrcConfig: 'SRC', OkwwConfig: 'Okww', + OkNteConfig: 'OkNte', MaaEndConfig: 'MaaEnd', M9AConfig: 'M9A', HSRConfig: 'HSR', @@ -896,7 +900,10 @@ export function useScriptApi() { : false, }, } - } else if (userIndex.type === 'OkwwUserConfig' && userData) { + } else if ( + (userIndex.type === 'OkwwUserConfig' || userIndex.type === 'OkNteUserConfig') && + userData + ) { const okwwUserData = userData as any return { id: userIndex.uid, @@ -947,7 +954,9 @@ export function useScriptApi() { TaskIndex: okwwUserData.Task?.TaskIndex !== undefined ? okwwUserData.Task.TaskIndex - : 1, + : userIndex.type === 'OkNteUserConfig' + ? 2 + : 1, ExitOnFinish: okwwUserData.Task?.ExitOnFinish !== undefined ? okwwUserData.Task.ExitOnFinish diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e656725b2..a961ea7de 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -74,6 +74,12 @@ const routes = [ component: () => import('../views/EditView/Script/OkwwScriptEdit.vue'), meta: { title: '编辑ok-ww脚本' }, }, + { + path: '/scripts/:id/edit/oknte', + name: 'OkNteScriptEdit', + component: () => import('../views/EditView/Script/OkNteScriptEdit.vue'), + meta: { title: '编辑ok-nte脚本' }, + }, { path: '/scripts/:scriptId/users/add/maa', name: 'MAAUserAdd', @@ -158,6 +164,18 @@ const routes = [ component: () => import('../views/EditView/User/OkwwUserEdit.vue'), meta: { title: '编辑ok-ww用户' }, }, + { + path: '/scripts/:scriptId/users/add/oknte', + name: 'OkNteUserAdd', + component: () => import('../views/EditView/User/OkNteUserEdit.vue'), + meta: { title: '添加ok-nte用户' }, + }, + { + path: '/scripts/:scriptId/users/:userId/edit/oknte', + name: 'OkNteUserEdit', + component: () => import('../views/EditView/User/OkNteUserEdit.vue'), + meta: { title: '编辑ok-nte用户' }, + }, { path: '/plans', name: 'Plans', diff --git a/frontend/src/types/script.ts b/frontend/src/types/script.ts index f8680edb2..04c355cf0 100644 --- a/frontend/src/types/script.ts +++ b/frontend/src/types/script.ts @@ -6,6 +6,7 @@ import type { MaaConfig, GeneralConfig, OkwwConfig, + OkNteConfig, SrcConfig, MaaEndConfig, M9AConfig, @@ -18,9 +19,10 @@ import type { SanityTaskType, } from '@/utils/maaEndProtocolSpace' -export type ScriptType = 'MAA' | 'General' | 'Okww' | 'SRC' | 'MaaEnd' | 'M9A' | 'HSR' +export type ScriptType = 'MAA' | 'General' | 'Okww' | 'OkNte' | 'SRC' | 'MaaEnd' | 'M9A' | 'HSR' export type OkwwScriptConfig = OkwwConfig +export type OkNteScriptConfig = OkNteConfig // MAA脚本配置 export interface MAAScriptConfig { Info: { @@ -201,7 +203,15 @@ export interface Script { id: string type: ScriptType name: string - config: MaaConfig | GeneralConfig | OkwwConfig | SrcConfig | MaaEndConfig | M9AConfig | HSRConfig + config: + | MaaConfig + | GeneralConfig + | OkwwConfig + | OkNteConfig + | SrcConfig + | MaaEndConfig + | M9AConfig + | HSRConfig users: User[] } @@ -293,6 +303,7 @@ export interface AddScriptResponse { | MAAScriptConfig | GeneralScriptConfig | OkwwScriptConfig + | OkNteScriptConfig | SRCScriptConfig | MaaEndScriptConfig | M9AScriptConfig @@ -306,6 +317,7 @@ export interface ScriptIndexItem { | 'MaaConfig' | 'GeneralConfig' | 'OkwwConfig' + | 'OkNteConfig' | 'SrcConfig' | 'MaaEndConfig' | 'M9AConfig' @@ -323,6 +335,7 @@ export interface GetScriptsResponse { | MAAScriptConfig | GeneralScriptConfig | OkwwScriptConfig + | OkNteScriptConfig | SRCScriptConfig | MaaEndScriptConfig | M9AScriptConfig @@ -335,7 +348,15 @@ export interface ScriptDetail { uid: string type: ScriptType name: string - config: MaaConfig | GeneralConfig | OkwwConfig | SrcConfig | MaaEndConfig | M9AConfig | HSRConfig + config: + | MaaConfig + | GeneralConfig + | OkwwConfig + | OkNteConfig + | SrcConfig + | MaaEndConfig + | M9AConfig + | HSRConfig users?: User[] createTime?: string } diff --git a/frontend/src/views/EditView/Script/OkNteScriptEdit.vue b/frontend/src/views/EditView/Script/OkNteScriptEdit.vue new file mode 100644 index 000000000..2f0ee3ef6 --- /dev/null +++ b/frontend/src/views/EditView/Script/OkNteScriptEdit.vue @@ -0,0 +1,721 @@ + + + + + + 脚本管理 + + + + + 编辑脚本 + + + + + + + + + + + 返回 + + + + + + + + OK-NTE + + + + + + 基本信息 + + + + + + + 脚本名称 + + + + + + + + + + + + + OK-NTE 路径 + + + + + + + + + + + + 选择目录 + + + + + + + + + + 游戏配置 + + + + + + + + 启用游戏配置 + + + + + + 是 + 否 + + + + + + + + + 任务前启动游戏 + + + + + + 是 + 否 + + + + + + + + + 任务后关闭游戏 + + + + + + 是 + 否 + + + + + + + + + + + 游戏根目录 + 选择包含 Neverness To Everness 的任意目录,自动定位 HTGame.exe + + + + + + + + + 选择目录 + + + + + + + + + 启动参数 + + + + + + + + + + + + + 启动等待时间 + + + + + + + + + + + + + + 运行配置 + + + + + + + 单日代理次数上限 + + + + + + + + + + + + + 重试次数限制 + + + + + + + + + + + + + 代理超时限制(分钟) + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/EditView/User/OkNteUserEdit.vue b/frontend/src/views/EditView/User/OkNteUserEdit.vue new file mode 100644 index 000000000..f0e55fe5d --- /dev/null +++ b/frontend/src/views/EditView/User/OkNteUserEdit.vue @@ -0,0 +1,869 @@ + + + + + + + 脚本管理 + + + + {{ scriptName }} + + + + {{ isEdit ? '编辑用户' : '添加用户' }} + + + + + + + + + + 配置 OK-NTE + + + + + + 正在配置 + + + + + + 返回 + + + + + + + + + + + 正在进行 OK-NTE 配置 + + 当前正在进行该用户的 OK-NTE GUI 配置,请在 OK-NTE 界面完成相关设置。 + + 配置完成后,请点击“保存配置”按钮来结束配置会话。 + + + + 保存配置 + + + + + + + + + + + + 基本信息 + + + + + + + + 用户名 + + + + + + + + + + + + + 启用状态 + + + + + + + 是 + 否 + + + + + + + + + + + 账号 + + + + + + + + + + + + + 密码 + + + + + + + + + + + + + + + + 游戏资源 + + + + + + + + + + + + + 剩余天数 + + + + + + + + + + + + + + 备注 + + + + + + + + + + + + 任务配置 + + + + + + + + 启动任务(-t N) + + + + + + + + {{ item.label }} + + + + + + + + + 当前启动参数 + + + + + + + + + + + + + + + + + + + + + + + 通知配置 + + + + 启用通知 + + + + + + + + + 通知内容 + + + + 统计信息 + + + + + + + + 邮件通知 + + + + + + + + + + + Server酱 + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/OkNteUserEdit/OkNteConfigEditor.vue b/frontend/src/views/OkNteUserEdit/OkNteConfigEditor.vue new file mode 100644 index 000000000..ae0cf5dff --- /dev/null +++ b/frontend/src/views/OkNteUserEdit/OkNteConfigEditor.vue @@ -0,0 +1,544 @@ + + + + OK-NTE 配置编辑 + 有未保存的更改 + 已保存 + + + + + + + + + {{ groupName }} + + + + {{ config.displayName }} + + -t {{ config.taskIndex }} + + + + + + + + + + + + + + {{ selectedConfig.displayName }} + {{ selectedConfig.filename }} + + + + + + {{ field.description }} + + + + setFieldValue(selectedConfig.filename, field.name, val) + " + /> + + + setFieldValue(selectedConfig.filename, field.name, val)" + > + + {{ getOptionLabel(opt) }} + + + + + setFieldValue(selectedConfig.filename, field.name, val) + " + > + + {{ getOptionLabel(opt) }} + + + + + { + if (val !== null) setFieldValue(selectedConfig.filename, field.name, val) + } + " + /> + + + { + if (val !== null) setFieldValue(selectedConfig.filename, field.name, val) + } + " + /> + + + + setFieldValue( + selectedConfig.filename, + field.name, + (e.target as HTMLInputElement).value + ) + " + /> + + + + setFieldValue( + selectedConfig.filename, + field.name, + (e.target as HTMLInputElement).value + ) + " + /> + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/Scripts.vue b/frontend/src/views/Scripts.vue index f4d6908bd..46807ee66 100644 --- a/frontend/src/views/Scripts.vue +++ b/frontend/src/views/Scripts.vue @@ -229,6 +229,12 @@ alt="ok-ww" class="type-icon" /> + {{ script.type === 'MAA' ? 'MAA脚本' @@ -254,7 +264,11 @@ ? 'M9A脚本' : script.type === 'Okww' ? 'ok-ww脚本' - : '通用脚本' + : script.type === 'OkNte' + ? 'ok-nte脚本' + : script.type === 'HSR' + ? 'HSR脚本' + : '通用脚本' }} @@ -341,6 +355,17 @@ + + + + + + + ok-nte脚本 + 异环 OK-NTE 自动化脚本,支持 -t/-e 任务启动 + + + @@ -563,6 +588,19 @@ const showMaaEndConfigMask = ref(false) // 控制MaaEnd配置遮罩层的显示 const currentConfigScript = ref
+ 当前正在进行该用户的 OK-NTE GUI 配置,请在 OK-NTE 界面完成相关设置。 + + 配置完成后,请点击“保存配置”按钮来结束配置会话。 +