diff --git a/app/models/config.py b/app/models/config.py index a27a9c6af..800386ff3 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -32,6 +32,11 @@ from app.utils.constants import ( CYCLE_EMPTY_TIME, MAA_STAGE_KEY, + MAAEND_AUTO_COLLECT_MODES, + MAAEND_AUTO_COLLECT_ROUTE_OPTIONS, + MAAEND_AUTO_COLLECT_TASK, + MAAEND_DELIVERY_COMMISSION_SOURCES, + MAAEND_DELIVERY_TASK, MAAEND_PROTOCOL_SPACE_TASK_OPTIONS, MAAEND_SANITY_TASK_DEFAULTS, MAAEND_SANITY_TASK_DETAIL_LABELS, @@ -126,6 +131,54 @@ def init_maaend_task_config(config) -> None: StringValidator(), ) + ## 抢委托送货最低接取价格(万) + config.Task_SeizeDeliveryJobsReward = ConfigItem( + "Task", "SeizeDeliveryJobsReward", 15.9, RangeValidator(0, 9999) + ) + ## 抢委托送货委托接收点 + config.Task_SeizeDeliveryJobsCommissionSource = ConfigItem( + "Task", + "SeizeDeliveryJobsCommissionSource", + "Unlimited", + OptionsValidator(list(MAAEND_DELIVERY_COMMISSION_SOURCES)), + ) + ## 独立送货任务 + setattr( + config, + f"Task_If{MAAEND_DELIVERY_TASK}", + ConfigItem("Task", f"If{MAAEND_DELIVERY_TASK}", True, BoolValidator()), + ) + + ## 独立自动采集任务 + config.Task_IfAutoCollect = ConfigItem( + "Task", f"If{MAAEND_AUTO_COLLECT_TASK}", True, BoolValidator() + ) + ## 自动采集路线安排:分散为三日轮换,集中为每三日执行一次 + config.Task_AutoCollectMode = ConfigItem( + "Task", + "AutoCollectMode", + "Distributed", + OptionsValidator(list(MAAEND_AUTO_COLLECT_MODES)), + ) + ## 自动采集区域资源路线 + config.Task_AutoCollectRoutes = ConfigItem( + "Task", + "AutoCollectRoutes", + list(MAAEND_AUTO_COLLECT_ROUTE_OPTIONS["AutoCollectRoutes"]), + MultipleOptionsValidator( + list(MAAEND_AUTO_COLLECT_ROUTE_OPTIONS["AutoCollectRoutes"]) + ), + ) + ## 自动采集通用资源路线 + config.Task_AutoCollectCommonRoutes = ConfigItem( + "Task", + "AutoCollectCommonRoutes", + list(MAAEND_AUTO_COLLECT_ROUTE_OPTIONS["AutoCollectCommonRoutes"]), + MultipleOptionsValidator( + list(MAAEND_AUTO_COLLECT_ROUTE_OPTIONS["AutoCollectCommonRoutes"]) + ), + ) + for task_name in MAAEND_TASKS: setattr( config, @@ -973,6 +1026,18 @@ def __init__(self) -> None: super().__init__() +class MaaEndConfigModeValidator(OptionsValidator): + """兼容旧版来源名称,统一为脚本/用户/直控。""" + + LEGACY_MODE_MAP = {"简洁": "脚本", "详细": "用户", "自定义": "用户"} + + def __init__(self) -> None: + super().__init__(["脚本", "用户", "直控"]) + + def correct(self, value: Any) -> Any: + return self.LEGACY_MODE_MAP.get(value, super().correct(value)) + + class MaaEndUserConfig(ConfigBase): """MaaEnd用户配置""" @@ -991,9 +1056,7 @@ def __init__(self) -> None: ## 密码 self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator()) ## 配置文件来源 - self.Info_Mode = ConfigItem( - "Info", "Mode", "脚本", ScriptUserModeValidator() - ) + self.Info_Mode = ConfigItem("Info", "Mode", "脚本", MaaEndConfigModeValidator()) ## 是否启用快速配置 self.Info_IfQuickConfig = ConfigItem( "Info", "IfQuickConfig", True, BoolValidator() @@ -1094,7 +1157,6 @@ async def load(self, data: dict): ): info_data["Mode"] = "脚本" info_data.pop("IfQuickConfig", None) - task_data = data.get("Task") if isinstance(task_data, dict): _normalize_maaend_sanity_task_type(task_data) diff --git a/app/models/schema.py b/app/models/schema.py index 2f323b7e9..802972a0c 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -1225,8 +1225,9 @@ class MaaEndUserConfig_Info(BaseModel): Status: Optional[bool] = Field(default=None, description="用户状态") Id: Optional[str] = Field(default=None, description="用户ID") Password: Optional[str] = Field(default=None, description="密码") - Mode: Optional[Literal["脚本", "用户"]] = Field( - default=None, description="配置来源(脚本/用户)" + Mode: Optional[Literal["脚本", "用户", "直控"]] = Field( + default=None, + description="配置来源(脚本共享、用户独立、脚本直控)", ) IfQuickConfig: Optional[bool] = Field(default=None, description="是否启用快速配置") SanityMode: Optional[str] = Field(default=None, description="理智任务配置模式") @@ -1244,6 +1245,35 @@ class MaaEndUserConfig_Info(BaseModel): Tag: Optional[str] = Field(default=None, description="用户标签信息") +MaaEndAutoCollectRoute = Literal[ + "Route1", + "Route2", + "Route3", + "Route4", + "Route5", + "Route6", + "Route7", + "Route8", + "Route9", + "Route10", + "Route11", + "Route12", + "Route13", + "Route14", + "Route15", +] +MaaEndAutoCollectCommonRoute = Literal[ + "CommonRoute1", + "CommonRoute2", + "CommonRoute3", + "CommonRoute4", + "CommonRoute5", + "CommonRoute6", + "CommonRoute7", + "CommonRoute8", +] + + class MaaEndUserConfig_Task(BaseModel): SanityTaskType: Optional[ Literal["OperatorProgression", "WeaponProgression", "CrisisDrills", "Essence"] @@ -1280,13 +1310,28 @@ class MaaEndUserConfig_Task(BaseModel): IfAutoStockStaple: Optional[bool] = Field(default=None, description="购买稳定物资") IfVisitFriends: Optional[bool] = Field(default=None, description="拜访好友") IfCreditShoppingN2: Optional[bool] = Field(default=None, description="信用点购物") - IfSeizeEntrustTask: Optional[bool] = Field(default=None, description="抢委托") + SeizeDeliveryJobsReward: Optional[float] = Field( + default=None, ge=0, description="抢委托送货最低接取价格(万)" + ) + SeizeDeliveryJobsCommissionSource: Optional[ + Literal["Unlimited", "WulingCity", "TestArea"] + ] = Field(default=None, description="抢委托送货委托接收点") + IfSeizeDeliveryJobs: Optional[bool] = Field(default=None, description="抢委托送货") IfAutoEcoFarm: Optional[bool] = Field(default=None, description="生态农场") IfAutoSell: Optional[bool] = Field(default=None, description="售卖弹性物资") IfEnvironmentMonitoring: Optional[bool] = Field( default=None, description="环境监测" ) IfAutoCollect: Optional[bool] = Field(default=None, description="自动采集") + AutoCollectMode: Optional[Literal["Distributed", "Concentrated"]] = Field( + default=None, description="自动采集路线安排:分散或集中" + ) + AutoCollectRoutes: Optional[list[MaaEndAutoCollectRoute]] = Field( + default=None, description="自动采集区域资源路线" + ) + AutoCollectCommonRoutes: Optional[list[MaaEndAutoCollectCommonRoute]] = Field( + default=None, description="自动采集通用资源路线" + ) IfTrialOfSwordmancy: Optional[bool] = Field(default=None, description="选剑演武") IfDailyRewards: Optional[bool] = Field(default=None, description="日常奖励领取") IfResourceRecycleStation: Optional[bool] = Field( diff --git a/app/task/MaaEnd/AutoProxy.py b/app/task/MaaEnd/AutoProxy.py index ca0af73d6..fd1733e53 100644 --- a/app/task/MaaEnd/AutoProxy.py +++ b/app/task/MaaEnd/AutoProxy.py @@ -36,17 +36,81 @@ from app.services import Notify, System from app.task.general.tools import execute_script_task from app.utils import LogMonitor, ProcessManager, get_logger, is_process_running -from app.utils.constants import MAAEND_TASKS, UTC4 +from app.utils.constants import ( + MAAEND_AUTO_COLLECT_MODES, + MAAEND_AUTO_COLLECT_ROUTE_OPTIONS, + MAAEND_AUTO_COLLECT_SCHEDULE_OPTIONS, + MAAEND_AUTO_COLLECT_TASK, + MAAEND_DELIVERY_TASK, + MAAEND_RUN_MOOD_BOOK, + MAAEND_TASKS, + UTC4, +) from app.utils.io import read_file, write_file from .resource_loader import ( load_maaend_interface_i18n, load_maaend_task_i18n, ) +from .ScriptConfig import maaend_config_mode, maaend_mas_config_dir from .tools import login, push_notification, replace_account_switch_task logger = get_logger("MaaEnd 自动代理") +_MAAEND_ACCOUNT_SWITCH_TASK = "AccountSwitch" + + +def _task_enabled_for_mode(task_name: object, enabled: object, mode: str) -> bool: + """按送货/日常/自动采集阶段筛选 MaaEnd 任务。""" + + name = str(task_name) + if mode == "Delivery": + return bool(enabled) and name in { + MAAEND_DELIVERY_TASK, + _MAAEND_ACCOUNT_SWITCH_TASK, + } + if mode == "AutoCollect": + return bool(enabled) and name in { + MAAEND_AUTO_COLLECT_TASK, + _MAAEND_ACCOUNT_SWITCH_TASK, + } + if mode == "Routine": + return bool(enabled) and name not in { + MAAEND_DELIVERY_TASK, + MAAEND_AUTO_COLLECT_TASK, + } + raise ValueError(f"未知 MaaEnd 运行模式: {mode}") + + +def _select_auto_collect_routes( + mode: str, + routes: list[str], + common_routes: list[str], + now: datetime, +) -> dict[str, list[str]]: + """按东四区日期选择本轮需要执行的自动采集路线。""" + + cycle_index = now.date().toordinal() % 3 + + def select_routes(option_name: str, selected: list[str]) -> list[str]: + selected_set = set(selected) + return [ + route + for index, route in enumerate(MAAEND_AUTO_COLLECT_ROUTE_OPTIONS[option_name]) + if route in selected_set + and ( + mode == "Concentrated" and cycle_index == 0 + or mode == "Distributed" and index % 3 == cycle_index + ) + ] + + return { + "AutoCollectRoutes": select_routes("AutoCollectRoutes", routes), + "AutoCollectCommonRoutes": select_routes( + "AutoCollectCommonRoutes", common_routes + ), + } + class AutoProxyTask(TaskExecuteBase): """MaaEnd 自动代理模式""" @@ -75,6 +139,16 @@ def __init__( self.account_switch_task_name = "" self.color_match_failed_message: str | None = None self.retryable = True + self.mode = "Routine" + self.run_book: dict[str, bool] = {mode: False for mode in MAAEND_RUN_MOOD_BOOK} + self.task_dict: dict[str, dict[str, bool]] | None = None + self.unique_task: dict[str, str] = {} + self.maaend_config_file: Path | None = None + self.account_switch_mode: str | None = None + self.auto_collect_run_at: datetime | None = None + self.auto_collect_routes: dict[str, list[str]] = { + option_name: [] for option_name in MAAEND_AUTO_COLLECT_ROUTE_OPTIONS + } async def check(self) -> str: @@ -100,15 +174,21 @@ async def check(self) -> str: "请检查账号ID或改用 MAS 自建账号切换" ) - config_user_id = ( - "Default" - if self.cur_user_config.get("Info", "Mode") == "脚本" - else self.cur_user_uid - ) - config_file = ( - Path.cwd() - / f"data/{self.script_info.script_id}/{config_user_id}/ConfigFile/mxu-MaaEnd.json" - ) + config_mode = maaend_config_mode(self.cur_user_config.get("Info", "Mode")) + if config_mode == "直控": + config_file = ( + Path.cwd() / f"data/{self.script_info.script_id}/Temp/mxu-MaaEnd.json" + ) + else: + config_file = ( + maaend_mas_config_dir( + self.script_info.script_id, + str(self.cur_user_uid), + config_mode, + ) + / "mxu-MaaEnd.json" + ) + self.maaend_config_file = config_file if not config_file.exists(): self.cur_user_item.status = "异常" return "未找到 MaaEnd 配置文件, 请先完成「MaaEnd 配置」步骤" @@ -134,7 +214,102 @@ async def prepare(self): (1, 23), "%Y-%m-%d %H:%M:%S.%f", self.check_log ) - self.run_book = False + if self.cur_user_config.get("Info", "IfQuickConfig"): + auto_collect_mode = self.cur_user_config.get("Task", "AutoCollectMode") + if auto_collect_mode not in MAAEND_AUTO_COLLECT_MODES: + auto_collect_mode = MAAEND_AUTO_COLLECT_MODES[0] + self.auto_collect_run_at = datetime.now(tz=UTC4) + self.auto_collect_routes = _select_auto_collect_routes( + auto_collect_mode, + list(self.cur_user_config.get("Task", "AutoCollectRoutes") or []), + list( + self.cur_user_config.get("Task", "AutoCollectCommonRoutes") or [] + ), + self.auto_collect_run_at, + ) + + mode_enabled = { + mode: self._mode_has_enabled_tasks(mode) for mode in MAAEND_RUN_MOOD_BOOK + } + self.account_switch_mode = next( + (mode for mode, enabled in mode_enabled.items() if enabled), + None, + ) + self.run_book = {mode: not enabled for mode, enabled in mode_enabled.items()} + + def _source_maaend_tasks(self) -> list[dict[str, object]] | None: + """读取当前用户所选 MaaEnd 实例的任务列表。""" + + if self.maaend_config_file is None: + return None + try: + source_config = read_file(self.maaend_config_file) + except (OSError, TypeError, ValueError): + return None + if not isinstance(source_config, dict): + return None + + instances = source_config.get("instances") + if not isinstance(instances, list) or not instances: + return None + + selected_instance = None + for instance in instances: + if not isinstance(instance, dict): + continue + if instance.get("id") == "automas" or instance.get("name") == "AUTO-MAS": + selected_instance = instance + break + if instance.get("id") == source_config.get("lastActiveInstanceId"): + selected_instance = instance + break + if selected_instance is None: + selected_instance = instances[0] + tasks = selected_instance.get("tasks") + if not isinstance(tasks, list): + return None + return [task for task in tasks if isinstance(task, dict)] + + def _mode_has_enabled_tasks(self, mode: str) -> bool: + """判断某阶段是否有任务,空阶段直接视为完成。""" + + if self.cur_user_config.get("Info", "IfQuickConfig"): + if mode == "Delivery": + return bool(self.cur_user_config.get("Task", "IfSeizeDeliveryJobs")) + if mode == "AutoCollect": + return bool( + self.cur_user_config.get("Task", "IfAutoCollect") + and any(self.auto_collect_routes.values()) + ) + return any( + bool(self.cur_user_config.get("Task", f"If{task_name}")) + for task_name in MAAEND_TASKS + ) + + tasks = self._source_maaend_tasks() + if tasks is None: + # 配置读取失败时交给 MaaEnd 执行并由日志判定,避免误跳过用户任务。 + return True + if mode == "Delivery": + return any( + str(task.get("taskName", "")) == MAAEND_DELIVERY_TASK + and bool(task.get("enabled", False)) + for task in tasks + ) + if mode == "AutoCollect": + return any( + str(task.get("taskName", "")) == MAAEND_AUTO_COLLECT_TASK + and bool(task.get("enabled", False)) + for task in tasks + ) + return any( + _task_enabled_for_mode( + task.get("taskName"), task.get("enabled", False), mode + ) + for task in tasks + if not str(task.get("taskName", "")).startswith("__MXU_") + and str(task.get("taskName", "")) != _MAAEND_ACCOUNT_SWITCH_TASK + ) async def main_task(self): """自动代理模式主逻辑""" @@ -162,19 +337,33 @@ async def main_task(self): logger.info(f"开始代理用户 {self.cur_user_uid}") self.cur_user_item.status = "运行" - self.task_dict: dict[str, dict[str, bool]] | None = None - self.unique_task: dict[str, str] = {} - run_times_limit = self.script_config.get("Run", "RunTimesLimit") maaend_update_retry_used = False i = 0 - while i < run_times_limit: - if self.run_book: - break + mode_order = list(MAAEND_RUN_MOOD_BOOK) + mode_index = 0 + while mode_index < len(mode_order): + self.mode = mode_order[mode_index] + if self.run_book[self.mode]: + mode_index += 1 + i = 0 + maaend_update_retry_used = False + self.task_dict = None + continue + if i >= run_times_limit: + logger.warning( + f"用户: {self.cur_user_uid} - {MAAEND_RUN_MOOD_BOOK[self.mode]}阶段重试次数已耗尽" + ) + mode_index += 1 + i = 0 + maaend_update_retry_used = False + self.task_dict = None + continue i += 1 self.retryable = True logger.info( - f"用户 {self.cur_user_item.name} - 尝试次数: {i}/{run_times_limit}" + f"用户 {self.cur_user_item.name} - {MAAEND_RUN_MOOD_BOOK[self.mode]}" + f"阶段尝试次数: {i}/{run_times_limit}" ) self.log_start_time = datetime.now() self.cur_user_item.log_record[self.log_start_time] = self.cur_user_log = ( @@ -314,26 +503,27 @@ async def main_task(self): await self.maaend_log_monitor.stop() if self.cur_user_log.status == "Success!": - self.run_book = True + self.run_book[self.mode] = True self.script_info.log = ( - "检测到 MaaEnd 完成代理任务\n正在等待相关程序结束" + f"检测到 MaaEnd 完成{MAAEND_RUN_MOOD_BOOK[self.mode]}任务\n" + "正在等待相关程序结束" ) # 中止相关程序 await self.maaend_process_manager.kill() await System.kill_process(self.maaend_exe_path) - # 执行任务后脚本 - if self.cur_user_config.get("Info", "IfScriptAfterTask"): - await execute_script_task( - Path(self.cur_user_config.get("Info", "ScriptAfterTask")), - "脚本后任务", - ) + mode_index += 1 + i = 0 + maaend_update_retry_used = False + self.task_dict = None else: if self.cur_user_log.status == "MaaEnd 正在更新": - logger.info("MaaEnd 更新流程已退出,准备自动重试当前用户") - self.script_info.log = "MaaEnd 更新完成,正在自动重试当前用户" + logger.info( + f"MaaEnd 更新流程已退出,准备自动重试{MAAEND_RUN_MOOD_BOOK[self.mode]}阶段" + ) + self.script_info.log = "MaaEnd 更新完成,正在自动重试当前阶段" # MaaEnd 更新后只重启脚本本体,保留 Endfield 进程减少重试成本。 await self.maaend_process_manager.kill() @@ -346,7 +536,8 @@ async def main_task(self): continue logger.warning("MaaEnd 更新后已自动重试一次,跳过后续重试") - break + i = run_times_limit + continue logger.warning( f"用户: {self.cur_user_uid} - 代理任务异常: {self.cur_user_log.status}" @@ -363,16 +554,15 @@ async def main_task(self): 3, ) - # 执行任务后脚本 - if self.cur_user_config.get("Info", "IfScriptAfterTask"): - await execute_script_task( - Path(self.cur_user_config.get("Info", "ScriptAfterTask")), - "脚本后任务", - ) - if not self.retryable: logger.info("检测到游戏画面参数错误,跳过后续重试") - break + i = run_times_limit + + if self.cur_user_config.get("Info", "IfScriptAfterTask"): + await execute_script_task( + Path(self.cur_user_config.get("Info", "ScriptAfterTask")), + "脚本后任务", + ) async def handle_pre_maaend_error( self, error_message: str, e: Exception | None = None @@ -441,14 +631,15 @@ async def set_maaend(self, device_info: DeviceInfo | None) -> None: if (self.maaend_set_path / "mxu-MaaEnd.json").exists(): maaend_local_config = read_file(self.maaend_set_path / "mxu-MaaEnd.json") - config_user_id = ( - "Default" - if self.cur_user_config.get("Info", "Mode") == "脚本" - else self.cur_user_uid - ) + config_mode = maaend_config_mode(self.cur_user_config.get("Info", "Mode")) maaend_config_path = ( - Path.cwd() - / f"data/{self.script_info.script_id}/{config_user_id}/ConfigFile" + Path.cwd() / f"data/{self.script_info.script_id}/Temp" + if config_mode == "直控" + else maaend_mas_config_dir( + self.script_info.script_id, + str(self.cur_user_uid), + config_mode, + ) ) maaend_config_file = maaend_config_path / "mxu-MaaEnd.json" if not maaend_config_file.exists(): @@ -571,25 +762,44 @@ def get_task_book_name(task: dict[str, object]) -> str: ) for task in maaend_tasks: - if task["taskName"].startswith("__MXU_"): + task_name_value = str(task.get("taskName")) + if task_name_value.startswith("__MXU_"): continue - task_enabled = task["enabled"] + task_enabled = bool(task.get("enabled", False)) if if_quick_config: - if task["taskName"] in ("ProtocolSpace", "AutoEssence"): + if task_name_value in ("ProtocolSpace", "AutoEssence"): if sanity_managed: task_enabled = ( sanity_switch_enabled - and task["taskName"] == target_task_name + and task_name_value == target_task_name and not sanity_configured ) if task_enabled: sanity_configured = True - elif task["taskName"] in MAAEND_TASKS: + elif task_name_value == MAAEND_DELIVERY_TASK: + task_enabled = self.cur_user_config.get( + "Task", f"If{MAAEND_DELIVERY_TASK}" + ) + elif task_name_value == MAAEND_AUTO_COLLECT_TASK: + task_enabled = bool( + self.cur_user_config.get("Task", "IfAutoCollect") + and any(self.auto_collect_routes.values()) + ) + elif task_name_value in MAAEND_TASKS: task_enabled = self.cur_user_config.get( - "Task", f"If{task['taskName']}" + "Task", f"If{task_name_value}" ) + task_enabled = _task_enabled_for_mode( + task_name_value, task_enabled, self.mode + ) + if ( + task_name_value == _MAAEND_ACCOUNT_SWITCH_TASK + and self.mode != self.account_switch_mode + ): + task_enabled = False + task_name = get_task_book_name(task) if task_name not in self.task_dict: self.task_dict[task_name] = {} @@ -609,19 +819,47 @@ def get_task_book_name(task: dict[str, object]) -> str: # 按本轮任务表写回 MaaEnd 运行配置 for task in maaend_tasks: - if task["taskName"].startswith("__MXU_"): + task_name_value = str(task.get("taskName")) + if task_name_value.startswith("__MXU_"): continue task_name = get_task_book_name(task) if task_name in self.task_dict and task["id"] in self.task_dict[task_name]: task["enabled"] = self.task_dict[task_name][task["id"]] - if not task["enabled"]: + if not task.get("enabled", False): continue - if ( + if if_quick_config and task_name_value == MAAEND_DELIVERY_TASK: + task.setdefault("optionValues", {}) + task["optionValues"]["SeizeDeliveryJobsReward"] = { + "type": "input", + "values": { + "Reward": str( + self.cur_user_config.get("Task", "SeizeDeliveryJobsReward") + ) + }, + } + task["optionValues"]["SeizeDeliveryJobsCommissionSource"] = { + "type": "select", + "caseName": self.cur_user_config.get( + "Task", "SeizeDeliveryJobsCommissionSource" + ), + } + elif if_quick_config and task_name_value == MAAEND_AUTO_COLLECT_TASK: + task.setdefault("optionValues", {}) + task["optionValues"]["AutoCollectSchedule"] = { + "type": "checkbox", + "caseNames": list(MAAEND_AUTO_COLLECT_SCHEDULE_OPTIONS), + } + for option_name, route_names in self.auto_collect_routes.items(): + task["optionValues"][option_name] = { + "type": "checkbox", + "caseNames": route_names, + } + elif ( if_quick_config - and task["taskName"] == target_task_name + and task_name_value == target_task_name and target_task_name == "ProtocolSpace" ): task.setdefault("optionValues", {}) @@ -691,7 +929,7 @@ def get_task_book_name(task: dict[str, object]) -> str: } elif ( if_quick_config - and task["taskName"] == target_task_name + and task_name_value == target_task_name and target_task_name == "AutoEssence" ): task.setdefault("optionValues", {}) @@ -729,6 +967,11 @@ async def monitor_maaend_update_download(self) -> None: self.script_info.log = "检测到 MaaEnd 正在更新,正在等待更新进程退出" if_maaend_updating = True + if if_maaend_updating and not await self.maaend_process_manager.is_running(): + logger.info("MaaEnd 更新进程已退出,后台检测释放日志锁") + self.wait_event.set() + return + await asyncio.sleep(5) async def check_log( @@ -836,7 +1079,7 @@ async def final_task(self): await self.maaend_log_monitor.stop() if ( self.script_info.current_index == len(self.script_info.user_list) - 1 - and self.run_book + and all(self.run_book.values()) and not self.script_config.get("Game", "CloseOnFinish") ): try: @@ -889,10 +1132,12 @@ async def final_task(self): statistics["start_time"] = self.user_start_time.strftime("%Y-%m-%d %H:%M:%S") statistics["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") statistics["user_result"] = ( - "代理任务全部完成" if self.run_book else self.cur_user_item.result + "代理任务全部完成" + if all(self.run_book.values()) + else self.cur_user_item.result ) - success_symbol = "√" if self.run_book else "X" + success_symbol = "√" if all(self.run_book.values()) else "X" if user_logs_list: try: @@ -912,7 +1157,7 @@ async def final_task(self): ), ) - if self.run_book: + if all(self.run_book.values()): if ( self.cur_user_config.get("Data", "ProxyTimes") == 0 and self.cur_user_config.get("Info", "RemainedDay") != -1 diff --git a/app/task/MaaEnd/ScriptConfig.py b/app/task/MaaEnd/ScriptConfig.py index 2ea4f42cf..8eec07867 100644 --- a/app/task/MaaEnd/ScriptConfig.py +++ b/app/task/MaaEnd/ScriptConfig.py @@ -21,6 +21,7 @@ import asyncio import shutil +import uuid from pathlib import Path from typing import Any @@ -37,6 +38,23 @@ logger = get_logger("MaaEnd 脚本设置") +def maaend_config_mode(raw: object) -> str: + """读取 MaaEnd 配置来源,并兼容旧版名称。""" + + mode = str(raw or "脚本") + return {"简洁": "脚本", "详细": "用户", "自定义": "用户"}.get(mode, mode) + + +def maaend_mas_config_dir(script_id: str, user_id: str, mode: str) -> Path: + """返回脚本共享或用户独立配置目录。""" + + mode = maaend_config_mode(mode) + if mode == "直控": + raise ValueError("直控配置不使用 MAS 配置目录") + owner = "Default" if mode == "脚本" else user_id + return Path.cwd() / "data" / script_id / owner / "ConfigFile" + + def normalize_maaend_config( maaend_set: dict[str, Any], controller_type: str, @@ -99,7 +117,24 @@ def __init__( self.task_info = script_info.task_info self.script_info = script_info self.script_config = script_config + self.user_config = user_config self.cur_user_item = self.script_info.user_list[self.script_info.current_index] + target_user_id = self.cur_user_item.user_id + self.config_mode = "脚本" + if target_user_id != "Default": + self.config_mode = maaend_config_mode( + self.user_config[uuid.UUID(target_user_id)].get("Info", "Mode") + ) + self.use_mas_config = self.config_mode != "直控" + self.config_file_path = ( + maaend_mas_config_dir( + self.script_info.script_id, + target_user_id, + self.config_mode, + ) + if self.use_mas_config + else None + ) async def prepare(self): @@ -109,10 +144,6 @@ async def prepare(self): self.maaend_root_path = Path(self.script_config.get("Info", "Path")) self.maaend_set_path = self.maaend_root_path / "config" self.maaend_exe_path = self.maaend_root_path / "MaaEnd.exe" - self.config_file_path = ( - Path.cwd() - / f"data/{self.script_info.script_id}/{self.cur_user_item.user_id}/ConfigFile" - ) async def main_task(self): @@ -132,10 +163,18 @@ async def set_maaend(self): await self.maaend_process_manager.kill() await System.kill_process(self.maaend_exe_path) - if (self.config_file_path / "mxu-MaaEnd.json").exists(): + if ( + self.use_mas_config + and self.config_file_path + and (self.config_file_path / "mxu-MaaEnd.json").exists() + ): shutil.rmtree(self.maaend_set_path, ignore_errors=True) shutil.copytree(self.config_file_path, self.maaend_set_path) - elif self.maaend_set_path.exists(): + elif ( + self.use_mas_config + and self.config_file_path + and self.maaend_set_path.exists() + ): shutil.copytree( self.maaend_set_path, self.config_file_path, @@ -148,31 +187,36 @@ async def set_maaend(self): "未找到 MaaEnd 配置文件, 请检查 MaaEnd 路径设置或先启动 MaaEnd 完成配置文件生成" ) - maaend_set = read_file(maaend_set_path) - maaend_set = normalize_maaend_config( - maaend_set, - self.script_config.get("Game", "ControllerType"), - ) - - write_file(maaend_set_path, maaend_set) - logger.success( - f"MaaEnd 运行参数配置完成: 设置脚本 {self.cur_user_item.user_id}" - ) + if self.use_mas_config: + maaend_set = read_file(maaend_set_path) + maaend_set = normalize_maaend_config( + maaend_set, + self.script_config.get("Game", "ControllerType"), + ) + write_file(maaend_set_path, maaend_set) + logger.success(f"MaaEnd 运行参数配置完成: {self.config_mode}配置") async def final_task(self): await self.maaend_process_manager.kill() await System.kill_process(self.maaend_exe_path) - shutil.rmtree(self.config_file_path, ignore_errors=True) - self.config_file_path.mkdir(parents=True, exist_ok=True) - shutil.copytree(self.maaend_set_path, self.config_file_path, dirs_exist_ok=True) - config_path = self.config_file_path / "mxu-MaaEnd.json" - maaend_set = read_file(config_path) - maaend_set = normalize_maaend_config( - maaend_set, self.script_config.get("Game", "ControllerType") - ) - write_file(config_path, maaend_set) + if self.use_mas_config and self.config_file_path: + shutil.rmtree(self.config_file_path, ignore_errors=True) + self.config_file_path.mkdir(parents=True, exist_ok=True) + shutil.copytree( + self.maaend_set_path, self.config_file_path, dirs_exist_ok=True + ) + config_path = self.config_file_path / "mxu-MaaEnd.json" + maaend_set = read_file(config_path) + maaend_set = normalize_maaend_config( + maaend_set, self.script_config.get("Game", "ControllerType") + ) + write_file(config_path, maaend_set) + logger.success(f"MaaEnd 配置已保存到: {self.config_file_path}") + else: + logger.success("MaaEnd 直控配置已由脚本原生 GUI 保存") + self.cur_user_item.status = "完成" async def on_crash(self, e: Exception): self.cur_user_item.status = "异常" diff --git a/app/task/MaaEnd/manager.py b/app/task/MaaEnd/manager.py index ac8b6324d..98ca111ae 100644 --- a/app/task/MaaEnd/manager.py +++ b/app/task/MaaEnd/manager.py @@ -21,6 +21,7 @@ import shutil import uuid +from contextlib import suppress from datetime import datetime from pathlib import Path @@ -39,7 +40,7 @@ from .AutoProxy import AutoProxyTask from .resource_loader import load_maaend_controller_protocol -from .ScriptConfig import ScriptConfigTask +from .ScriptConfig import ScriptConfigTask, maaend_config_mode from .tools import push_notification logger = get_logger("MaaEnd 调度器") @@ -63,6 +64,11 @@ def __init__(self, script_info: ScriptItem): self.script_info = script_info self.check_result = "-" self.controller_protocol = "" + self.user_config: MultipleConfig[MaaEndUserConfig] | None = None + self.maaend_config_dir: Path | None = None + self.temp_path: Path | None = None + self.had_original_script_config = False + self.script_config_mode = "脚本" async def check(self) -> str: if self.task_info.mode not in METHOD_BOOK: @@ -133,15 +139,19 @@ async def prepare(self): shutil.rmtree(self.temp_path, ignore_errors=True) self.temp_path.mkdir(parents=True, exist_ok=True) if self.maaend_config_dir.exists(): + self.had_original_script_config = True shutil.copytree(self.maaend_config_dir, self.temp_path, dirs_exist_ok=True) # 构建用户列表 if self.task_info.mode == "ScriptConfig": + target_user_id = self.task_info.user_id or "Default" self.script_info.user_list = [ - UserItem( - user_id=self.task_info.user_id or "Default", name="", status="等待" - ) + UserItem(user_id=target_user_id, name="", status="等待") ] + if target_user_id != "Default": + self.script_config_mode = maaend_config_mode( + self.user_config[uuid.UUID(target_user_id)].get("Info", "Mode") + ) else: self.script_info.user_list = [ UserItem( @@ -156,6 +166,41 @@ async def prepare(self): f"用户列表加载完成, 已筛选用户数: {len(self.script_info.user_list)}" ) + async def _restore_script_config_from_temp(self) -> None: + """恢复任务开始前的 MaaEnd working 配置。""" + + if ( + not self.temp_path + or not self.temp_path.exists() + or not self.maaend_config_dir + ): + return + if not self.had_original_script_config: + shutil.rmtree(self.maaend_config_dir, ignore_errors=True) + return + + temporary_path = self.maaend_config_dir.with_name( + self.maaend_config_dir.name + ".tmp" + ) + shutil.rmtree(temporary_path, ignore_errors=True) + shutil.copytree(self.temp_path, temporary_path, dirs_exist_ok=True) + shutil.rmtree(self.maaend_config_dir, ignore_errors=True) + temporary_path.rename(self.maaend_config_dir) + + def _cleanup_script_config_temp(self) -> None: + if self.temp_path: + shutil.rmtree(self.temp_path, ignore_errors=True) + + def _keep_script_config_changes(self) -> bool: + """直控配置会话成功时保留 MaaEnd GUI 的写回。""" + + return ( + self.task_info.mode == "ScriptConfig" + and self.script_config_mode == "直控" + and bool(self.script_info.user_list) + and self.script_info.user_list[0].status == "完成" + ) + async def main_task(self): self.check_result = await self.check() @@ -175,13 +220,25 @@ async def main_task(self): raise RuntimeError("脚本配置类型错误, 不是 MaaEnd 脚本类型") for self.script_info.current_index in range(len(self.script_info.user_list)): + current_user = self.script_info.user_list[self.script_info.current_index] + if self.task_info.mode != "ScriptConfig": + current_config = self.user_config[uuid.UUID(current_user.user_id)] + config_mode = maaend_config_mode(current_config.get("Info", "Mode")) + logger.info(f"用户 {current_user.user_id} 配置来源: {config_mode}") + if config_mode == "直控": + await self._restore_script_config_from_temp() + task = METHOD_BOOK[self.task_info.mode]( self.script_info, self.script_config, self.user_config, self.emulator_manager, ) - await self.spawn(task) + try: + await self.spawn(task) + finally: + if self.task_info.mode != "ScriptConfig": + await self._restore_script_config_from_temp() async def final_task(self): @@ -190,6 +247,12 @@ async def final_task(self): return logger.info("MaaEnd 主任务已结束, 开始执行后续操作") + if self._keep_script_config_changes(): + logger.info("直控配置会话成功,保留 MaaEnd 原生配置") + else: + await self._restore_script_config_from_temp() + self._cleanup_script_config_temp() + await Config.ScriptConfig[uuid.UUID(self.script_info.script_id)].unlock() logger.success(f"已解锁脚本配置 {self.script_info.script_id}") @@ -250,17 +313,28 @@ async def final_task(self): ), ) - # 还原配置 - if (self.temp_path).exists(): - shutil.rmtree(self.maaend_config_dir, ignore_errors=True) - shutil.copytree(self.temp_path, self.maaend_config_dir, dirs_exist_ok=True) - shutil.rmtree(self.temp_path, ignore_errors=True) - - self.script_info.status = "完成" + if any(user.status == "异常" for user in self.script_info.user_list): + self.script_info.status = "异常" + else: + self.script_info.status = "完成" async def on_crash(self, e: Exception): self.script_info.status = "异常" logger.opt(exception=True).warning(f"MaaEnd任务出现异常: {e}") + with suppress(Exception): + await self._restore_script_config_from_temp() + self._cleanup_script_config_temp() + + script_config = Config.ScriptConfig[uuid.UUID(self.script_info.script_id)] + if script_config.is_locked: + with suppress(Exception): + await script_config.unlock() + + if self.task_info.mode in ("AutoProxy",) and self.user_config: + with suppress(Exception): + await script_config.UserData.load(await self.user_config.toDict()) + await Config.ScriptConfig.save() + await Publisher.send( id=self.task_info.task_id, type=protocol.TASK_NOTICE, diff --git a/app/utils/constants.py b/app/utils/constants.py index a43c58be6..92a1fbd24 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -64,6 +64,47 @@ } """MAA运行模式对应的超时配置项:绿票商店只买一次商店,复用日常时限""" +MAAEND_RUN_MOOD_BOOK = { + "Delivery": "送货", + "Routine": "日常", + "AutoCollect": "自动采集", +} +"""MaaEnd 自动代理运行模式映射表""" + +MAAEND_DELIVERY_TASK = "SeizeDeliveryJobs" +"""MaaEnd 送货阶段使用的任务名称""" + +MAAEND_AUTO_COLLECT_TASK = "AutoCollect" +"""MaaEnd 自动采集阶段使用的任务名称""" + +MAAEND_DELIVERY_COMMISSION_SOURCES = ("Unlimited", "WulingCity", "TestArea") +"""MaaEnd 抢委托送货的委托接收点选项""" + +MAAEND_AUTO_COLLECT_MODES = ("Distributed", "Concentrated") +"""MaaEnd 自动采集的三日周期模式""" + +MAAEND_AUTO_COLLECT_SCHEDULE_OPTIONS = tuple( + f"AutoCollectSchedule{weekday}" + for weekday in ( + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ) +) +"""MaaEnd 自动采集任务的星期计划选项""" + +MAAEND_AUTO_COLLECT_ROUTE_OPTIONS = { + "AutoCollectRoutes": tuple(f"Route{index}" for index in range(1, 16)), + "AutoCollectCommonRoutes": tuple( + f"CommonRoute{index}" for index in range(1, 9) + ), +} +"""MaaEnd 自动采集两类路线选项""" + MAA_TASKS = [ "StartUp", "DepotMaintain", @@ -330,7 +371,6 @@ "tasks": ( ("VisitFriends", "拜访好友"), ("CreditShoppingN2", "信用点购物"), - ("SeizeEntrustTask", "抢委托"), ), }, "Frontend": { @@ -339,7 +379,6 @@ ("AutoEcoFarm", "生态农场"), ("AutoSell", "售卖弹性物资"), ("EnvironmentMonitoring", "环境监测"), - ("AutoCollect", "自动采集"), ("TrialOfSwordmancy", "选剑演武"), ), }, @@ -362,7 +401,7 @@ for group in MAAEND_TASK_GROUPS.values() for task_name, _ in group["tasks"] ) -"""MaaEnd托管任务列表""" +"""MaaEnd快速控制任务列表(不含独立送货和自动采集阶段)""" MAAEND_SANITY_TASK_DEFAULTS = { "SanityTaskType": "OperatorProgression", diff --git a/frontend/src/components/ExtraScriptSection.vue b/frontend/src/components/ExtraScriptSection.vue index 7ef950650..7f8b7b83e 100644 --- a/frontend/src/components/ExtraScriptSection.vue +++ b/frontend/src/components/ExtraScriptSection.vue @@ -1,6 +1,6 @@