diff --git a/app/api/scripts.py b/app/api/scripts.py index 0aad13afe..8d2bd2d0b 100644 --- a/app/api/scripts.py +++ b/app/api/scripts.py @@ -289,6 +289,34 @@ async def import_script_config_file( return OutBase(message="脚本配置文件已导入") +@router.post( + "/maaend/options", + tags=["Get"], + summary="获取 MaaEnd 动态选项", + response_model=MaaEndOptionsOut, + status_code=200, +) +async def get_maaend_options(options: ScriptDeleteIn = Body(...)) -> MaaEndOptionsOut: + try: + data = await Config.get_maaend_options(options.scriptId) + return MaaEndOptionsOut( + controllers=[ComboBoxItem(**item) for item in data["controllers"]], + controllerTypes=data["controllerTypes"], + essenceLocations=[ + ComboBoxItem(**item) for item in data["essenceLocations"] + ], + ) + except Exception as e: + return MaaEndOptionsOut( + code=500, + status="error", + message=f"{type(e).__name__}: {str(e)}", + controllers=[], + controllerTypes={}, + essenceLocations=[], + ) + + @router.post( "/user/get", tags=["Get"], diff --git a/app/core/config.py b/app/core/config.py index db333dcc8..152578d89 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -679,6 +679,22 @@ async def get_script(self, script_id: str | None) -> tuple[list, dict]: index = data.pop("instances", []) return list(index), data + async def get_maaend_options( + self, script_id: str + ) -> dict[str, list[dict[str, str]]]: + """读取指定 MaaEnd 安装目录中的动态选项。""" + + script_config = self.ScriptConfig[uuid.UUID(script_id)] + if not isinstance(script_config, MaaEndConfig): + raise TypeError("脚本配置类型错误, 不是 MaaEnd 类型") + root_path = str(script_config.get("Info", "Path") or "").strip() + if not root_path: + raise ValueError("MaaEnd 路径未配置") + + from app.task.MaaEnd.ScriptConfig import load_maaend_options + + return await asyncio.to_thread(load_maaend_options, Path(root_path)) + async def update_script( self, script_id: str, data: Dict[str, Dict[str, Any]] ) -> None: diff --git a/app/models/config.py b/app/models/config.py index 58102db1f..6b789ea08 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -32,7 +32,6 @@ MATERIALS_MAP, RESOURCE_STAGE_INFO, MAA_STAGE_KEY, - MAAEND_AUTO_ESSENCE_LOCATION_OPTIONS, MAAEND_PROTOCOL_SPACE_TASK_OPTIONS, MAAEND_SANITY_TASK_DEFAULTS, MAAEND_SANITY_TASK_DETAIL_LABELS, @@ -115,7 +114,7 @@ def init_maaend_task_config(config) -> None: "Task", "AutoEssenceSpecifiedLocation", MAAEND_SANITY_TASK_DEFAULTS["AutoEssenceSpecifiedLocation"], - OptionsValidator(list(MAAEND_AUTO_ESSENCE_LOCATION_OPTIONS)), + StringValidator(), ) for task_name in MAAEND_TASKS: @@ -980,9 +979,14 @@ def getTags(self) -> str: if sanity_task_type == "Essence" else task_config[sanity_task_type] ) + detail_label = ( + detail_key + if sanity_task_type == "Essence" + else MAAEND_SANITY_TASK_DETAIL_LABELS[detail_key] + ) tags.append( { - "text": f"详细任务:{MAAEND_SANITY_TASK_DETAIL_LABELS[detail_key]}", + "text": f"详细任务:{detail_label}", "color": "blue", } ) @@ -1052,13 +1056,12 @@ def __init__(self) -> None: self.Game_ControllerType = ConfigItem( "Game", "ControllerType", - "Win32-Front", - OptionsValidator( - [ - "Win32-Front", - "ADB", - ] - ), + "", + StringValidator(), + ) + ## 控制器协议类型 + self.Game_ControllerProtocol = ConfigItem( + "Game", "ControllerProtocol", "", StringValidator() ) ## 终末地游戏路径 self.Game_Path = ConfigItem("Game", "Path", "", FileValidator()) diff --git a/app/models/schema.py b/app/models/schema.py index 30940cdf8..7a01174e6 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -76,6 +76,12 @@ class ComboBoxOut(OutBase): data: List[ComboBoxItem] = Field(..., description="下拉框选项") +class MaaEndOptionsOut(OutBase): + controllers: List[ComboBoxItem] = Field(..., description="MaaEnd 控制器选项") + controllerTypes: dict[str, str] = Field(..., description="控制器协议类型映射") + essenceLocations: List[ComboBoxItem] = Field(..., description="MaaEnd 基质刷取地点选项") + + class GetStageIn(BaseModel): type: Literal[ "User", @@ -892,17 +898,9 @@ class MaaEndUserConfig_Task(BaseModel): RewardsSetOption: Optional[Literal["RewardsSetA", "RewardsSetB"]] = Field( default=None, description="奖励组选项" ) - AutoEssenceSpecifiedLocation: Optional[ - Literal[ - "VFTheHub", - "VFOriginiumSciencePark", - "VFOriginLodespring", - "VFPowerPlateau", - "WLWulingCity", - "WLQingboStockade", - "WLMarkerStone", - ] - ] = Field(default=None, description="基质刷取指定地点") + AutoEssenceSpecifiedLocation: Optional[str] = Field( + default=None, description="基质刷取指定地点" + ) IfSanity: Optional[bool] = Field(default=None, description="理智任务") IfAutoUseSpMedication: Optional[bool] = Field( default=None, description="应急理智加强剂" @@ -974,9 +972,8 @@ class MaaEndConfig_Run(BaseModel): class MaaEndConfig_Game(BaseModel): - ControllerType: Optional[Literal["Win32-Front", "ADB"]] = Field( - default=None, description="控制器类型" - ) + ControllerType: Optional[str] = Field(default=None, description="控制器类型") + ControllerProtocol: Optional[str] = Field(default=None, description="控制器协议类型") Path: Optional[str] = Field(default=None, description="终末地客户端路径") Arguments: Optional[str] = Field(default=None, description="游戏启动参数") WaitTime: Optional[int] = Field(default=None, ge=60, description="游戏等待时间") diff --git a/app/task/MaaEnd/AutoProxy.py b/app/task/MaaEnd/AutoProxy.py index 7b9138b02..ba00217ad 100644 --- a/app/task/MaaEnd/AutoProxy.py +++ b/app/task/MaaEnd/AutoProxy.py @@ -22,7 +22,6 @@ import re import uuid import json -import json5 import shutil import asyncio from pathlib import Path @@ -38,6 +37,7 @@ from app.tools import skland_sign_in from app.utils.constants import UTC4, UTC8, MAAEND_SANITY_TASK_FIELDS, MAAEND_TASKS from .tools import login, push_notification, replace_account_switch_task +from .resource_loader import load_maaend_task_i18n from app.task.general.tools import execute_script_task logger = get_logger("MaaEnd 自动代理") @@ -240,7 +240,7 @@ async def main_task(self): controller_type = self.script_config.get("Game", "ControllerType") try: if self.emulator_manager is None: - if controller_type != "ADB" and is_process_running("Endfield.exe"): + if is_process_running("Endfield.exe"): logger.info( "检测到终末地客户端进程已在运行,跳过由 MAS 重复启动游戏" ) @@ -407,8 +407,11 @@ async def main_task(self): "脚本后任务", ) - if "游戏分辨率设置错误" in self.cur_user_log.status: - logger.info("检测到游戏分辨率设置错误,跳过后续重试") + if ( + "游戏分辨率设置错误" in self.cur_user_log.status + or "颜色识别失败" in self.cur_user_log.status + ): + logger.info("检测到游戏画面参数错误,跳过后续重试") break async def handle_pre_maaend_error( @@ -559,26 +562,12 @@ async def set_maaend(self, device_info: DeviceInfo | None) -> None: settings = maaend_set["settings"] if settings["language"] == "system": settings["language"] = "zh-CN" - maaend_i18n_raw = json.loads( - ( - self.maaend_root_path - / f"locales/interface/{settings['language'].lower().replace('-', '_')}.json" - ).read_text(encoding="utf-8") + maaend_i18n = await asyncio.to_thread( + load_maaend_task_i18n, + self.maaend_root_path, + str(settings["language"]), ) - maaend_i18n: dict[str, str] = {} - for task_definition_file in self.maaend_root_path.glob("tasks/*.json"): - task_definition = json5.loads( # type: ignore - task_definition_file.read_text(encoding="utf-8") - )["task"][0] - if task_definition["label"].startswith("$"): - locale_text = maaend_i18n_raw.get(task_definition["label"].lstrip("$")) - if locale_text is None: - raise RuntimeError("MaaEnd 文件不完整,卸载后重新安装MaaEnd") - maaend_i18n[task_definition["name"]] = locale_text - else: - maaend_i18n[task_definition["name"]] = task_definition["label"] - if_quick_config = self.cur_user_config.get("Info", "IfQuickConfig") def get_task_book_name(task: dict[str, object]) -> str: @@ -741,7 +730,7 @@ def get_task_book_name(task: dict[str, object]) -> str: and target_task_name == "AutoEssence" ): task.setdefault("optionValues", {}) - task["optionValues"]["AutoEssenceSpecifiedLocation"] = { + task["optionValues"]["AutoEssenceChooseLocation"] = { "type": "select", "caseName": sanity_task_config["AutoEssenceSpecifiedLocation"], } @@ -816,6 +805,8 @@ async def check_log(self, log_content: list[str], latest_time: datetime) -> None self.cur_user_log.status = "MaaEnd 任务启动失败" elif "resolution check failed" in log or "分辨率不符合要求" in log: self.cur_user_log.status = "游戏分辨率设置错误,请重设分辨率比例为16:9" + elif "识别颜色失败" in log or "Color identification failed" in log: + self.cur_user_log.status = "MaaEnd 颜色识别失败,请关闭滤镜或 HDR" elif "任务失败: AccountSwitch" in log: self.cur_user_log.status = "MaaEnd 账号切换失败" elif ( diff --git a/app/task/MaaEnd/ManualReview.py b/app/task/MaaEnd/ManualReview.py index f5f9e18dc..be49512f5 100644 --- a/app/task/MaaEnd/ManualReview.py +++ b/app/task/MaaEnd/ManualReview.py @@ -300,7 +300,7 @@ async def _run_maaend_account_switch(self, account_id: str) -> None: controller_type=str( self.script_config.get("Game", "ControllerType") ), - template_set=local_config, + fallback_set=local_config, ) maaend_instance = maaend_set["instances"][0] maaend_instance["tasks"] = [] diff --git a/app/task/MaaEnd/ScriptConfig.py b/app/task/MaaEnd/ScriptConfig.py index 30fef286f..23b9f1c35 100644 --- a/app/task/MaaEnd/ScriptConfig.py +++ b/app/task/MaaEnd/ScriptConfig.py @@ -32,6 +32,7 @@ from app.models.emulator import DeviceBase from app.services import System from app.utils import get_logger, ProcessManager +from .resource_loader import load_maaend_options logger = get_logger("MaaEnd 脚本设置") @@ -39,7 +40,7 @@ def normalize_maaend_config( maaend_set: dict[str, Any], controller_type: str, - template_set: dict[str, Any] | None = None, + fallback_set: dict[str, Any] | None = None, ) -> dict[str, Any]: """将 MaaEnd 配置收束为 AUTO-MAS 单实例配置""" @@ -61,8 +62,8 @@ def select_instance(source_set: dict[str, Any]) -> dict[str, Any] | None: return None selected_instance = select_instance(maaend_set) - if selected_instance is None and template_set is not None: - selected_instance = select_instance(template_set) + if selected_instance is None and fallback_set is not None: + selected_instance = select_instance(fallback_set) if selected_instance is None: raise ValueError("MaaEnd 配置文件中未找到可用实例") @@ -131,16 +132,12 @@ async def set_maaend(self): if (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) - else: - maaend_template_path = ( - Path.cwd() / "res/templates/MaaEnd/config/mxu-MaaEnd.json" + elif self.maaend_set_path.exists(): + shutil.copytree( + self.maaend_set_path, + self.config_file_path, + dirs_exist_ok=True, ) - if maaend_template_path.exists(): - shutil.rmtree(self.maaend_set_path, ignore_errors=True) - self.maaend_set_path.mkdir(parents=True, exist_ok=True) - shutil.copy2( - maaend_template_path, self.maaend_set_path / "mxu-MaaEnd.json" - ) maaend_set_path = self.maaend_set_path / "mxu-MaaEnd.json" if not maaend_set_path.exists(): @@ -149,18 +146,9 @@ async def set_maaend(self): ) maaend_set = json.loads(maaend_set_path.read_text(encoding="utf-8")) - maaend_template_path = ( - Path.cwd() / "res/templates/MaaEnd/config/mxu-MaaEnd.json" - ) - template_config = ( - json.loads(maaend_template_path.read_text(encoding="utf-8")) - if maaend_template_path.exists() - else None - ) maaend_set = normalize_maaend_config( maaend_set, self.script_config.get("Game", "ControllerType"), - template_config, ) maaend_set_path.write_text( diff --git a/app/task/MaaEnd/manager.py b/app/task/MaaEnd/manager.py index d721dcc22..454089354 100644 --- a/app/task/MaaEnd/manager.py +++ b/app/task/MaaEnd/manager.py @@ -57,6 +57,7 @@ def __init__(self, script_info: ScriptItem): self.task_info = script_info.task_info self.script_info = script_info self.check_result = "-" + self.controller_protocol = "" async def check(self) -> str: if self.task_info.mode not in METHOD_BOOK: @@ -70,16 +71,19 @@ async def check(self) -> str: if not (Path(script_config.get("Info", "Path")) / "MaaEnd.exe").exists(): return "MaaEnd.exe文件不存在, 请检查MaaEnd路径设置!" - if (script_config.get("Game", "ControllerType") == "ADB") and ( + self.controller_protocol = script_config.get("Game", "ControllerProtocol") + + if self.controller_protocol == "Adb" and ( script_config.get("Game", "EmulatorId") == "-" or script_config.get("Game", "EmulatorIndex") in ["", "-"] ): return "未完成模拟器配置, 请检查脚本配置中的模拟器设置!" - elif ( - script_config.get("Game", "ControllerType").startswith("Win32") - and not Path(script_config.get("Game", "Path")).exists() - ): + elif self.controller_protocol == "Win32" and not Path( + script_config.get("Game", "Path") + ).exists(): return "未完成游戏配置, 请检查脚本配置中的游戏设置!" + elif self.controller_protocol not in ("Adb", "Win32"): + return "MaaEnd 控制器协议未配置, 请重新选择控制器!" if self.task_info.mode == "AutoProxy" and not ( Path( @@ -106,7 +110,7 @@ async def prepare(self): self.temp_path = Path.cwd() / f"data/{self.script_info.script_id}/Temp" # 初始化模拟器管理器 - if self.script_config.get("Game", "ControllerType") == "ADB": + if self.controller_protocol == "Adb": self.emulator_manager = await EmulatorManager.get_emulator_instance( self.script_config.get("Game", "EmulatorId") ) diff --git a/app/task/MaaEnd/resource_loader.py b/app/task/MaaEnd/resource_loader.py new file mode 100644 index 000000000..37d6bc960 --- /dev/null +++ b/app/task/MaaEnd/resource_loader.py @@ -0,0 +1,146 @@ +# 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 json +from _thread import LockType +from pathlib import Path +from threading import Lock +from typing import Any + +import json5 + + +FileSignature = tuple[tuple[str, int, int], ...] +_options_cache: dict[Path, tuple[FileSignature, tuple[Path, ...], dict[str, Any]]] = {} +_task_i18n_cache: dict[tuple[Path, str], tuple[FileSignature, dict[str, str]]] = {} +_root_locks: dict[Path, LockType] = {} +_locks_guard = Lock() + + +def _signature(paths: tuple[Path, ...]) -> FileSignature: + signature = [] + for path in paths: + stat = path.stat() + signature.append((str(path), stat.st_mtime_ns, stat.st_size)) + return tuple(signature) + + +def _root_lock(root_path: Path) -> LockType: + with _locks_guard: + return _root_locks.setdefault(root_path, Lock()) + + +def load_maaend_options(root_path: Path) -> dict[str, Any]: + """加载并缓存 MaaEnd 控制器与基质刷取选项。""" + + root_path = root_path.resolve() + with _root_lock(root_path): + cached = _options_cache.get(root_path) + if cached is not None: + signature, paths, data = cached + try: + if _signature(paths) == signature: + return data + except OSError: + pass + + config_path = root_path / "config/mxu-MaaEnd.json" + interface_path = root_path / "interface.json" + config = json5.loads(config_path.read_text(encoding="utf-8")) + interface = json5.loads(interface_path.read_text(encoding="utf-8")) + language = str(config["settings"]["language"]) + language = ( + "zh_cn" + if language.lower() == "system" + else language.lower().replace("-", "_") + ) + locale_path = ( + interface_path.parent / interface["languages"][language] + ).resolve() + locale = json5.loads(locale_path.read_text(encoding="utf-8")) + + def options(cases: list[dict[str, str]]) -> list[dict[str, str]]: + return [ + { + "label": locale.get(case["label"][1:], case["name"]) + if (case.get("label") or "").startswith("$") + else case.get("label") or case["name"], + "value": case["name"], + } + for case in cases + ] + + task_path = next( + ( + (interface_path.parent / path).resolve() + for path in interface["import"] + if Path(path).stem == "AutoEssence" + ), + None, + ) + if task_path is None: + raise ValueError( + f"MaaEnd Interface 未导入 AutoEssence 任务: {interface_path}" + ) + + task = json5.loads(task_path.read_text(encoding="utf-8")) + data = { + "controllers": options(interface["controller"]), + "controllerTypes": { + case["name"]: case["type"] for case in interface["controller"] + }, + "essenceLocations": options( + task["option"]["AutoEssenceChooseLocation"]["cases"] + ), + } + paths = (config_path, interface_path, locale_path, task_path) + _options_cache[root_path] = (_signature(paths), paths, data) + return data + + +def load_maaend_task_i18n(root_path: Path, language: str) -> dict[str, str]: + """加载并缓存 MaaEnd 任务名称的本地化映射。""" + + root_path = root_path.resolve() + language = "zh-CN" if language.lower() == "system" else language + language = language.lower().replace("-", "_") + cache_key = (root_path, language) + + with _root_lock(root_path): + locale_path = root_path / f"locales/interface/{language}.json" + task_paths = tuple(sorted(root_path.glob("tasks/*.json"))) + paths = (locale_path, *task_paths) + signature = _signature(paths) + cached = _task_i18n_cache.get(cache_key) + if cached is not None and cached[0] == signature: + return cached[1] + + locale = json.loads(locale_path.read_text(encoding="utf-8")) + data: dict[str, str] = {} + for task_path in task_paths: + task = json5.loads(task_path.read_text(encoding="utf-8"))["task"][0] + label = task["label"] + if label.startswith("$"): + label = locale.get(label.lstrip("$")) + if label is None: + raise RuntimeError("MaaEnd 文件不完整,卸载后重新安装MaaEnd") + data[task["name"]] = label + + _task_i18n_cache[cache_key] = (signature, data) + return data diff --git a/app/utils/constants.py b/app/utils/constants.py index 62c5e20b8..ec2feaf9a 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -241,13 +241,6 @@ "AdvancedProgression3": "高阶培养 III - 快子遴捡晶格", "AdvancedProgression4": "高阶培养 IV - 象限拟合液", "AdvancedProgression5": "高阶培养 V - 三相纳米片", - "VFTheHub": "枢纽区", - "VFOriginiumSciencePark": "源石研究园", - "VFOriginLodespring": "矿脉源区", - "VFPowerPlateau": "供能高地", - "WLWulingCity": "武陵城区", - "WLQingboStockade": "清波寨", - "WLMarkerStone": "首墩", } """MaaEnd理智任务详细选项展示文案""" @@ -272,17 +265,6 @@ } """MaaEnd协议空间任务选项列表""" -MAAEND_AUTO_ESSENCE_LOCATION_OPTIONS = ( - "VFTheHub", - "VFOriginiumSciencePark", - "VFOriginLodespring", - "VFPowerPlateau", - "WLWulingCity", - "WLQingboStockade", - "WLMarkerStone", -) -"""MaaEnd基质刷取地点选项列表""" - MAAEND_STAGE_WITH_AB = set(["OperatorEXP", "Promotions", "SkillUp", "WeaponTune"]) """MAAEnd任务包含AB关的关卡列表""" @@ -347,7 +329,7 @@ "WeaponProgression": "WeaponEXP", "CrisisDrills": "AdvancedProgression1", "RewardsSetOption": "RewardsSetA", - "AutoEssenceSpecifiedLocation": "VFTheHub", + "AutoEssenceSpecifiedLocation": "", } """MaaEnd理智任务字段默认值""" diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index de5dcbe4b..19d156eaf 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -21,6 +21,7 @@ import { registerFileHandlers } from './ipc/fileHandlers' import { registerOkwwPathDiscoveryHandlers } from './ipc/okwwPathDiscoveryHandlers' import { getLogger, initializeLogger } from './services/logger' +import { createMaaEndIssueReport } from './services/maaEndIssueReportService' import AdmZip = require('adm-zip') // 初始化日志系统(必须在创建 logger 之前) @@ -836,6 +837,30 @@ ipcMain.handle('log:export', async () => { } }) +ipcMain.handle('maaend:exportIssueReport', async () => { + try { + if (!mainWindow) return { success: false, error: '窗口未初始化' } + + const result = await dialog.showSaveDialog(mainWindow, { + title: '导出 MaaEnd 问题包', + defaultPath: `MaaEnd-logs-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.zip`, + filters: [{ name: 'ZIP文件', extensions: ['zip'] }], + }) + + if (result.canceled || !result.filePath) { + return { success: false, error: '用户取消' } + } + + return createMaaEndIssueReport(getAppRoot(), result.filePath) + } catch (error) { + logger.error('导出 MaaEnd 问题包失败:', error) + return { + success: false, + error: error instanceof Error ? error.message : String(error), + } + } +}) + ipcMain.handle('log:getContent', async (_event, lines?: number, fileName?: string) => { try { const appRoot = getAppRoot() diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 6c3898845..dc114ac97 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -74,6 +74,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // 日志文件操作 exportLogs: () => ipcRenderer.invoke('log:export'), + exportMaaEndIssueReport: () => ipcRenderer.invoke('maaend:exportIssueReport'), getLogs: (lines?: number, fileName?: string) => ipcRenderer.invoke('log:getContent', lines, fileName), openLogWindow: () => ipcRenderer.invoke('log:openWindow'), diff --git a/frontend/electron/services/maaEndIssueReportService.ts b/frontend/electron/services/maaEndIssueReportService.ts new file mode 100644 index 000000000..1217c8f26 --- /dev/null +++ b/frontend/electron/services/maaEndIssueReportService.ts @@ -0,0 +1,505 @@ +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import AdmZip = require('adm-zip') + +import { getLogger } from './logger' + +const logger = getLogger('MaaEnd问题包') + +const MAX_ENTRY_BYTES = 25 * 1024 * 1024 +const MAX_ARCHIVE_BYTES = 95 * 1024 * 1024 +const TEXT_EXTENSIONS = new Set([ + '.cfg', + '.csv', + '.ini', + '.json', + '.jsonc', + '.log', + '.md', + '.out', + '.txt', + '.xml', + '.yaml', + '.yml', +]) +const SENSITIVE_KEY_PATTERN = + /(?:password|passwd|token|cookie|secret|authorization|credential|api[_-]?key|stoken|ltoken|serverchan|path)/i +const SENSITIVE_BEARER_PATTERN = + /((?:["']?[\w-]*(?:password|passwd|token|cookie|secret|authorization|credential|api[_-]?key|stoken|ltoken|serverchan|path)[\w-]*["']?\s*[:=]\s*["']?(?:Bearer|Basic)\s+))[^"'\s,;&}\]]+/gi +const SENSITIVE_ASSIGNMENT_PATTERN = + /((?:["']?[\w-]*(?:password|passwd|token|cookie|secret|authorization|credential|api[_-]?key|stoken|ltoken|serverchan|path)[\w-]*["']?\s*[:=]\s*["']?))(?!Bearer\b|Basic\b)[^"'\s,;&}\]]+/gi + +interface MaaEndInstallation { + label: string + rootPath: string + version?: string +} + +interface ReportEntry { + path: string + sourceSize: number + storedSize: number + status: 'included' | 'truncated' | 'skipped' + reason?: string +} + +interface CollectorState { + zip: AdmZip + entries: ReportEntry[] + archiveBytes: number +} + +interface MaaEndConfigRecord { + instances?: Array<{ uid?: string; type?: string }> + [key: string]: unknown +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isTextFile(filePath: string): boolean { + return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase()) +} + +function sanitizeText(text: string): string { + let sanitized = text.replace(SENSITIVE_BEARER_PATTERN, '$1***') + sanitized = sanitized.replace(SENSITIVE_ASSIGNMENT_PATTERN, '$1***') + const homePath = os.homedir() + if (homePath) { + sanitized = sanitized.split(homePath).join('') + } + return sanitized +} + +function sanitizeJsonValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(item => sanitizeJsonValue(item)) + } + + if (typeof value === 'string') { + return sanitizeText(value) + } + + if (!isRecord(value)) { + return value + } + + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [ + key, + SENSITIVE_KEY_PATTERN.test(key) ? '***' : sanitizeJsonValue(item), + ]) + ) +} + +function readJson(filePath: string): unknown { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '')) + } catch { + return undefined + } +} + +function readVersionFromInterface(rootPath: string): string | undefined { + const interfacePath = path.join(rootPath, 'interface.json') + if (!fs.existsSync(interfacePath)) { + return undefined + } + + const data = readJson(interfacePath) + if (isRecord(data) && typeof data.version === 'string') { + return data.version + } + + try { + const text = fs.readFileSync(interfacePath, 'utf-8') + return text.match(/"version"\s*:\s*"([^"]+)"/)?.[1] + } catch { + return undefined + } +} + +function resolveDataRoots(appRoot: string): string[] { + const roots = [path.resolve(appRoot)] + const parentRoot = path.resolve(appRoot, '..') + if ( + parentRoot !== roots[0] && + (fs.existsSync(path.join(parentRoot, 'main.py')) || fs.existsSync(path.join(parentRoot, 'app'))) + ) { + roots.push(parentRoot) + } + return roots +} + +function discoverMaaEndInstallations(dataRoots: string[]): MaaEndInstallation[] { + const installations: MaaEndInstallation[] = [] + const seenPaths = new Set() + + for (const dataRoot of dataRoots) { + const config = readJson(path.join(dataRoot, 'config', 'ScriptConfig.json')) + if (!isRecord(config)) { + continue + } + + const records = config as MaaEndConfigRecord + for (const instance of records.instances || []) { + if (instance?.type !== 'MaaEndConfig' || !instance.uid) { + continue + } + + const scriptConfig = records[instance.uid] + const info = + isRecord(scriptConfig) && isRecord(scriptConfig.Info) ? scriptConfig.Info : undefined + const rootPath = info && typeof info.Path === 'string' ? info.Path.trim() : '' + if (!rootPath) { + continue + } + + const normalizedPath = path.resolve(rootPath) + const pathKey = process.platform === 'win32' ? normalizedPath.toLowerCase() : normalizedPath + if (seenPaths.has(pathKey)) { + continue + } + + seenPaths.add(pathKey) + installations.push({ + label: `maaend-${installations.length + 1}`, + rootPath: normalizedPath, + version: readVersionFromInterface(normalizedPath), + }) + } + } + + return installations +} + +function addEntry( + state: CollectorState, + archivePath: string, + sourceSize: number, + content: Buffer, + status: ReportEntry['status'], + reason?: string +): void { + state.zip.addFile(archivePath, content) + state.archiveBytes += content.byteLength + state.entries.push({ + path: archivePath, + sourceSize, + storedSize: content.byteLength, + status, + reason, + }) +} + +function addSkippedEntry( + state: CollectorState, + archivePath: string, + sourceSize: number, + reason: string +): void { + state.entries.push({ + path: archivePath, + sourceSize, + storedSize: 0, + status: 'skipped', + reason, + }) +} + +function readDiagnosticContent(filePath: string): Buffer { + const rawText = fs.readFileSync(filePath, 'utf-8') + if (path.extname(filePath).toLowerCase() === '.json') { + const json = readJson(filePath) + if (json !== undefined) { + return Buffer.from(`${JSON.stringify(sanitizeJsonValue(json), null, 2)}\n`, 'utf-8') + } + } + return Buffer.from(sanitizeText(rawText), 'utf-8') +} + +function addDiagnosticFile(state: CollectorState, sourcePath: string, archivePath: string): void { + let stat: fs.Stats + try { + stat = fs.statSync(sourcePath) + } catch (error) { + logger.debug(`读取诊断文件失败: ${sourcePath}, ${String(error)}`) + return + } + + if (!stat.isFile()) { + return + } + + const remainingBytes = MAX_ARCHIVE_BYTES - state.archiveBytes + if (remainingBytes <= 0) { + addSkippedEntry(state, archivePath, stat.size, '问题包已达到总大小限制') + return + } + + if (isTextFile(sourcePath)) { + try { + const content = readDiagnosticContent(sourcePath) + if (content.byteLength <= MAX_ENTRY_BYTES && content.byteLength <= remainingBytes) { + addEntry(state, archivePath, stat.size, content, 'included') + return + } + + const storedSize = Math.min(MAX_ENTRY_BYTES, remainingBytes) + if (storedSize <= 0) { + addSkippedEntry(state, archivePath, stat.size, '问题包已达到总大小限制') + return + } + + const tail = content.subarray(content.byteLength - storedSize) + addEntry( + state, + `${archivePath}.tail`, + stat.size, + Buffer.concat([Buffer.from('[文件过大,仅保留文件末尾内容。]\n', 'utf-8'), tail]).subarray( + 0, + storedSize + ), + 'truncated', + `原始文件超过 ${MAX_ENTRY_BYTES} 字节` + ) + return + } catch (error) { + addSkippedEntry(state, archivePath, stat.size, `读取文本文件失败: ${String(error)}`) + return + } + } + + if (stat.size > MAX_ENTRY_BYTES || stat.size > remainingBytes) { + addSkippedEntry(state, archivePath, stat.size, '二进制文件超过问题包大小限制') + return + } + + try { + addEntry(state, archivePath, stat.size, fs.readFileSync(sourcePath), 'included') + } catch (error) { + addSkippedEntry(state, archivePath, stat.size, `读取二进制文件失败: ${String(error)}`) + } +} + +function addDirectory(state: CollectorState, sourceDir: string, archiveDir: string): boolean { + if (!fs.existsSync(sourceDir)) { + return false + } + + let foundFile = false + let entries: fs.Dirent[] + try { + entries = fs + .readdirSync(sourceDir, { withFileTypes: true }) + .sort((left, right) => left.name.localeCompare(right.name)) + } catch (error) { + logger.debug(`读取诊断目录失败: ${sourceDir}, ${String(error)}`) + return false + } + + for (const entry of entries) { + if (entry.isSymbolicLink()) { + continue + } + + const sourcePath = path.join(sourceDir, entry.name) + const archivePath = path.posix.join(archiveDir, entry.name) + if (entry.isDirectory()) { + foundFile = addDirectory(state, sourcePath, archivePath) || foundFile + } else if (entry.isFile()) { + addDiagnosticFile(state, sourcePath, archivePath) + foundFile = true + } + } + + return foundFile +} + +function addSanitizedJsonFile( + state: CollectorState, + sourcePath: string, + archivePath: string +): boolean { + if (!fs.existsSync(sourcePath)) { + return false + } + + try { + const json = readJson(sourcePath) + if (json === undefined) { + addDiagnosticFile(state, sourcePath, archivePath) + } else { + const content = Buffer.from(`${JSON.stringify(sanitizeJsonValue(json), null, 2)}\n`, 'utf-8') + const sourceSize = fs.statSync(sourcePath).size + const remainingBytes = MAX_ARCHIVE_BYTES - state.archiveBytes + if (content.byteLength > MAX_ENTRY_BYTES || content.byteLength > remainingBytes) { + addSkippedEntry(state, archivePath, sourceSize, '脱敏配置超过问题包大小限制') + } else { + addEntry(state, archivePath, sourceSize, content, 'included') + } + } + return true + } catch (error) { + logger.debug(`脱敏配置失败: ${sourcePath}, ${String(error)}`) + return false + } +} + +function readAutoMasVersion(dataRoots: string[]): string | undefined { + for (const dataRoot of dataRoots) { + const versionData = readJson(path.join(dataRoot, 'res', 'version.json')) + if (isRecord(versionData) && typeof versionData.version === 'string') { + return versionData.version + } + } + return undefined +} + +function buildIssueTemplate(archiveName: string, autoMasVersion?: string): string { + return `# MaaEnd Issue 信息 + +## 问题描述及复现步骤 + +预期行为: + +实际行为: + +复现步骤: +1. +2. +3. + +## 日志文件 + +已生成:\`${archiveName}\`。 +压缩包中的 \`logs/\`、\`maaend/\` 和 \`metadata/\` 目录由 AUTO-MAS 自动收集。 +请将这个 ZIP 原文件发送到 AUTO-MAS 官方 QQ 群(群号:957750551),不要解压或修改。 + +## 软件画面截图 + +请将出现问题时完整的 MaaEnd 软件画面截图一并发送到 MAS 群。 + +## 游戏画面截图 + +请将出现问题时的游戏画面截图一并发送到 MAS 群。 + +## 版本信息截图 + +请将 MaaEnd「设置 - 调试 - 版本信息」截图一并发送到 MAS 群。 +压缩包中的 \`metadata/collection-manifest.json\` 同时记录了可复制粘贴的版本信息。 + +## 其他信息 + +- AUTO-MAS 版本:${autoMasVersion || '未知'} +- 请确认提交前已经更新到最新版本的 MaaEnd。 +` +} + +export interface MaaEndIssueReportResult { + success: boolean + message?: string + zipPath?: string + error?: string +} + +export function createMaaEndIssueReport(appRoot: string, zipPath: string): MaaEndIssueReportResult { + const zip = new AdmZip() + const state: CollectorState = { zip, entries: [], archiveBytes: 0 } + const generatedAt = new Date().toISOString() + const dataRoots = resolveDataRoots(appRoot) + const installations = discoverMaaEndInstallations(dataRoots) + const autoMasVersion = readAutoMasVersion(dataRoots) + const installationManifest: Array> = [] + + dataRoots.forEach((dataRoot, index) => { + addDirectory( + state, + path.join(dataRoot, 'debug'), + index === 0 ? 'logs/auto-mas' : 'logs/auto-mas/backend' + ) + }) + + const runtimeDebugDir = path.join(path.dirname(process.execPath), 'debug') + const knownDebugDirs = new Set(dataRoots.map(dataRoot => path.resolve(dataRoot, 'debug'))) + if (!knownDebugDirs.has(path.resolve(runtimeDebugDir))) { + addDirectory(state, runtimeDebugDir, 'logs/frontend-runtime') + } + + for (const installation of installations) { + const debugIncluded = addDirectory( + state, + path.join(installation.rootPath, 'debug'), + `maaend/${installation.label}/debug` + ) + const onErrorIncluded = addDirectory( + state, + path.join(installation.rootPath, 'on_error'), + `maaend/${installation.label}/on_error` + ) + const configIncluded = addSanitizedJsonFile( + state, + path.join(installation.rootPath, 'config', 'mxu-MaaEnd.json'), + `maaend/${installation.label}/config/mxu-MaaEnd.json` + ) + + installationManifest.push({ + id: installation.label, + version: installation.version || '未知', + debugIncluded, + onErrorIncluded, + configIncluded, + }) + } + + const metadata = { + formatVersion: 1, + generatedAt, + autoMasVersion: autoMasVersion || '未知', + system: { + platform: process.platform, + platformRelease: os.release(), + architecture: process.arch, + nodeVersion: process.version, + }, + maaend: installationManifest, + notes: [ + '配置中的路径、账号密码、Token、Cookie、Secret 等字段会脱敏;日志文本会按常见键值格式脱敏并隐藏当前用户目录。', + '单个文件最大保留 25 MiB,问题包总大小最大保留 95 MiB。超限文本文件仅保留末尾内容。', + '当前流程请先将问题包原文件发送到 AUTO-MAS 官方 QQ 群;软件截图、游戏截图和版本信息截图可在群内补充。', + ], + entries: state.entries, + } + + state.zip.addFile( + 'metadata/system-info.json', + Buffer.from(`${JSON.stringify(metadata.system, null, 2)}\n`, 'utf-8') + ) + state.zip.addFile( + 'issue-template.md', + Buffer.from(buildIssueTemplate(path.basename(zipPath), autoMasVersion), 'utf-8') + ) + state.zip.addFile( + 'metadata/collection-manifest.json', + Buffer.from(`${JSON.stringify(metadata, null, 2)}\n`, 'utf-8') + ) + + try { + fs.mkdirSync(path.dirname(zipPath), { recursive: true }) + zip.writeZip(zipPath) + logger.info(`MaaEnd 问题包已导出: ${zipPath}`) + return { + success: true, + message: `MaaEnd 问题包导出成功,已收集 ${state.entries.filter(entry => entry.status !== 'skipped').length} 个文件`, + zipPath, + } + } catch (error) { + logger.error(`MaaEnd 问题包导出失败: ${String(error)}`) + return { + success: false, + error: error instanceof Error ? error.message : String(error), + } + } +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 0840e6107..c788ef952 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -105,6 +105,7 @@ export type { MaaEndConfig } from './models/MaaEndConfig'; export type { MaaEndConfig_Game } from './models/MaaEndConfig_Game'; export type { MaaEndConfig_Info } from './models/MaaEndConfig_Info'; export type { MaaEndConfig_Run } from './models/MaaEndConfig_Run'; +export type { MaaEndOptionsOut } from './models/MaaEndOptionsOut'; export type { MaaEndUserConfig } from './models/MaaEndUserConfig'; export type { MaaEndUserConfig_Data } from './models/MaaEndUserConfig_Data'; export type { MaaEndUserConfig_Info } from './models/MaaEndUserConfig_Info'; diff --git a/frontend/src/api/models/MaaEndConfig_Game.ts b/frontend/src/api/models/MaaEndConfig_Game.ts index cf135a147..ea36435bc 100644 --- a/frontend/src/api/models/MaaEndConfig_Game.ts +++ b/frontend/src/api/models/MaaEndConfig_Game.ts @@ -6,7 +6,7 @@ export type MaaEndConfig_Game = { /** * 控制器类型 */ - ControllerType?: ('Win32-Front' | 'ADB' | null); + ControllerType?: (string | null); /** * 终末地客户端路径 */ diff --git a/frontend/src/api/models/MaaEndOptionsOut.ts b/frontend/src/api/models/MaaEndOptionsOut.ts new file mode 100644 index 000000000..df6d05bd3 --- /dev/null +++ b/frontend/src/api/models/MaaEndOptionsOut.ts @@ -0,0 +1,27 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ComboBoxItem } from './ComboBoxItem'; +export type MaaEndOptionsOut = { + /** + * 状态码 + */ + code?: number; + /** + * 操作状态 + */ + status?: string; + /** + * 操作消息 + */ + message?: string; + /** + * MaaEnd 控制器选项 + */ + controllers: Array; + /** + * MaaEnd 基质刷取地点选项 + */ + essenceLocations: Array; +}; diff --git a/frontend/src/api/models/MaaEndUserConfig_Task.ts b/frontend/src/api/models/MaaEndUserConfig_Task.ts index 7d5969ba5..018281b69 100644 --- a/frontend/src/api/models/MaaEndUserConfig_Task.ts +++ b/frontend/src/api/models/MaaEndUserConfig_Task.ts @@ -26,7 +26,7 @@ export type MaaEndUserConfig_Task = { /** * 基质刷取指定地点 */ - AutoEssenceSpecifiedLocation?: ('VFTheHub' | 'VFOriginiumSciencePark' | 'VFOriginLodespring' | 'VFPowerPlateau' | 'WLWulingCity' | 'WLQingboStockade' | 'WLMarkerStone' | null); + AutoEssenceSpecifiedLocation?: (string | null); /** * 理智任务 */ diff --git a/frontend/src/api/services/GetService.ts b/frontend/src/api/services/GetService.ts index e9fb7f365..828f029e4 100644 --- a/frontend/src/api/services/GetService.ts +++ b/frontend/src/api/services/GetService.ts @@ -20,6 +20,7 @@ import type { HistoryDataGetOut } from '../models/HistoryDataGetOut'; import type { HistorySearchIn } from '../models/HistorySearchIn'; import type { HistorySearchOut } from '../models/HistorySearchOut'; import type { InfoOut } from '../models/InfoOut'; +import type { MaaEndOptionsOut } from '../models/MaaEndOptionsOut'; import type { NoticeOut } from '../models/NoticeOut'; import type { OCRScreenshotIn } from '../models/OCRScreenshotIn'; import type { OCRScreenshotOut } from '../models/OCRScreenshotOut'; @@ -60,6 +61,25 @@ export class GetService { url: '/api/info/version', }); } + /** + * 获取 MaaEnd 动态选项 + * @param requestBody + * @returns MaaEndOptionsOut Successful Response + * @throws ApiError + */ + public static getMaaendOptionsApiScriptsMaaendOptionsPost( + requestBody: ScriptDeleteIn, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/maaend/options', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * 获取关卡号下拉框信息 * @param requestBody diff --git a/frontend/src/api/services/Service.ts b/frontend/src/api/services/Service.ts index 634f3011c..ef80043b8 100644 --- a/frontend/src/api/services/Service.ts +++ b/frontend/src/api/services/Service.ts @@ -29,6 +29,7 @@ import type { HistorySearchIn } from '../models/HistorySearchIn'; import type { HistorySearchOut } from '../models/HistorySearchOut'; import type { HSRStageOptionsOut } from '../models/HSRStageOptionsOut'; import type { InfoOut } from '../models/InfoOut'; +import type { MaaEndOptionsOut } from '../models/MaaEndOptionsOut'; import type { NoticeOut } from '../models/NoticeOut'; import type { OutBase } from '../models/OutBase'; import type { PlanCreateIn } from '../models/PlanCreateIn'; @@ -116,6 +117,25 @@ export class Service { url: '/api/core/close', }); } + /** + * 获取 MaaEnd 动态选项 + * @param requestBody + * @returns MaaEndOptionsOut Successful Response + * @throws ApiError + */ + public static getMaaendOptionsApiScriptsMaaendOptionsPost( + requestBody: ScriptDeleteIn, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/maaend/options', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * 获取后端git版本信息 * @returns VersionOut Successful Response diff --git a/frontend/src/composables/useMaaEndIssueReport.ts b/frontend/src/composables/useMaaEndIssueReport.ts new file mode 100644 index 000000000..9202d7ec7 --- /dev/null +++ b/frontend/src/composables/useMaaEndIssueReport.ts @@ -0,0 +1,48 @@ +import { message } from 'ant-design-vue' +import { ref } from 'vue' + +import { showMaaEndIssueReportGuide } from '@/utils/maaEndIssueReport' + +interface ReportLogger { + info: (message: string) => void | Promise + error: (message: string) => void | Promise +} + +export function useMaaEndIssueReport(logger: ReportLogger) { + const exporting = ref(false) + + const exportMaaEndIssueReport = async () => { + exporting.value = true + try { + const result = await window.electronAPI?.exportMaaEndIssueReport?.() + + if (!result) { + message.error('导出功能未响应,请检查程序') + logger.error('导出 MaaEnd 问题包失败: 未收到响应') + return + } + + if (result.success) { + message.success(result.message || 'MaaEnd 问题包导出成功') + logger.info(`MaaEnd 问题包导出成功: ${result.zipPath || '路径未知'}`) + if (result.zipPath) { + await window.electronAPI?.showItemInFolder?.(result.zipPath) + } + showMaaEndIssueReportGuide(result.zipPath) + return + } + + const errorMsg = result.error || 'MaaEnd 问题包导出失败' + logger.error(`导出 MaaEnd 问题包失败: ${errorMsg}`) + message.error(errorMsg) + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + logger.error(`导出 MaaEnd 问题包失败: ${errorMsg}`) + message.error(`导出问题包异常: ${errorMsg}`) + } finally { + exporting.value = false + } + } + + return { exporting, exportMaaEndIssueReport } +} diff --git a/frontend/src/composables/useScriptApi.ts b/frontend/src/composables/useScriptApi.ts index cb54a55d2..0705657a1 100644 --- a/frontend/src/composables/useScriptApi.ts +++ b/frontend/src/composables/useScriptApi.ts @@ -10,6 +10,7 @@ import { type SrcConfig, type HSRConfig, type HSRStageOptionsData, + type MaaEndOptionsOut, ScriptCreateIn, type ScriptReorderIn, HsrService, @@ -695,7 +696,7 @@ export function useScriptApi() { AutoEssenceSpecifiedLocation: maaEndUserData.Task?.AutoEssenceSpecifiedLocation != null ? maaEndUserData.Task.AutoEssenceSpecifiedLocation - : 'VFTheHub', + : '', IfSanity: maaEndUserData.Task?.IfSanity != null ? maaEndUserData.Task.IfSanity @@ -1231,6 +1232,19 @@ export function useScriptApi() { } } + const getMaaEndOptions = async (scriptId: string): Promise => { + try { + const response = await Service.getMaaendOptionsApiScriptsMaaendOptionsPost({ scriptId }) + if (response?.code !== 200) throw new Error(response?.message || '接口返回异常') + return response + } catch (err) { + const errorMsg = err instanceof Error ? err.message : '获取 MaaEnd 动态选项失败' + error.value = errorMsg + logger.error(`获取 MaaEnd 动态选项失败: ${errorMsg}`) + return null + } + } + // 删除脚本 const deleteScript = async (scriptId: string): Promise => { loading.value = true @@ -1337,6 +1351,7 @@ export function useScriptApi() { getScriptsWithUsers, getScript, getHsrStageOptions, + getMaaEndOptions, deleteScript, updateScript, reorderScript, diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index 48fa65a8b..cb1708bb5 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -102,6 +102,12 @@ export interface ElectronAPI { zipPath?: string error?: string }> + exportMaaEndIssueReport: () => Promise<{ + success: boolean + message?: string + zipPath?: string + error?: string + }> getLogs: (lines?: number, fileName?: string) => Promise // 获取模块化日志器(使用主进程配置) diff --git a/frontend/src/types/script.ts b/frontend/src/types/script.ts index a0c079e40..8ab22205b 100644 --- a/frontend/src/types/script.ts +++ b/frontend/src/types/script.ts @@ -139,7 +139,8 @@ export interface MaaEndScriptConfig { AccountSwitchMethod: 'MAS' | 'MAAEND' } Game: { - ControllerType: 'Win32-Front' | 'ADB' | null + ControllerType: string | null + ControllerProtocol: string | null Path: string Arguments: string WaitTime: number diff --git a/frontend/src/utils/maaEndIssueReport.ts b/frontend/src/utils/maaEndIssueReport.ts new file mode 100644 index 000000000..02d9ff7ca --- /dev/null +++ b/frontend/src/utils/maaEndIssueReport.ts @@ -0,0 +1,19 @@ +import { Modal } from 'ant-design-vue' + +import { MAS_QQ_GROUP_URL, openExternalUrl } from './openExternal' + +const getZipFileName = (zipPath?: string): string => { + if (!zipPath) return 'MaaEnd-logs-*.zip' + return zipPath.split(/[\\/]/).pop() || 'MaaEnd-logs-*.zip' +} + +export function showMaaEndIssueReportGuide(zipPath?: string): void { + const fileName = getZipFileName(zipPath) + + Modal.info({ + title: '请将问题包发送到 MAS 群', + content: `问题包「${fileName}」已生成。请将 ZIP 原文件直接发送到 AUTO-MAS 官方 QQ 群(群号:957750551),不要解压、修改或只复制其中的日志内容。`, + okText: '打开 MAS 群', + onOk: () => openExternalUrl(MAS_QQ_GROUP_URL), + }) +} diff --git a/frontend/src/utils/maaEndProtocolSpace.ts b/frontend/src/utils/maaEndProtocolSpace.ts index e96f00bda..13d1b10f2 100644 --- a/frontend/src/utils/maaEndProtocolSpace.ts +++ b/frontend/src/utils/maaEndProtocolSpace.ts @@ -23,17 +23,7 @@ export const REWARD_OPTIONS = [ export type RewardSetOption = (typeof REWARD_OPTIONS)[number]['value'] -export const AUTO_ESSENCE_LOCATION_OPTIONS = [ - { label: '枢纽区', value: 'VFTheHub' }, - { label: '源石研究园', value: 'VFOriginiumSciencePark' }, - { label: '矿脉源区', value: 'VFOriginLodespring' }, - { label: '供能高地', value: 'VFPowerPlateau' }, - { label: '武陵城区', value: 'WLWulingCity' }, - { label: '清波寨', value: 'WLQingboStockade' }, - { label: '首墩', value: 'WLMarkerStone' }, -] as const - -export type AutoEssenceLocation = (typeof AUTO_ESSENCE_LOCATION_OPTIONS)[number]['value'] +export type AutoEssenceLocation = string export const PROTOCOL_SPACE_TASK_OPTIONS_MAP = { OperatorProgression: [ @@ -163,10 +153,6 @@ export const PROTOCOL_SPACE_TASK_LABEL_MAP = Object.fromEntries( .map(option => [option.value, option.label]) ) as Record -export const AUTO_ESSENCE_LOCATION_LABEL_MAP = Object.fromEntries( - AUTO_ESSENCE_LOCATION_OPTIONS.map(option => [option.value, option.label]) -) as Record - export const PROTOCOL_SPACE_TASK_TITLE_MAP: Record = { OperatorProgression: '干员养成任务', WeaponProgression: '武器养成任务', @@ -189,7 +175,7 @@ export const createDefaultMaaEndSanityConfig = (): MaaEndSanityConfig => ({ WeaponProgression: 'WeaponEXP', CrisisDrills: 'AdvancedProgression1', RewardsSetOption: 'RewardsSetA', - AutoEssenceSpecifiedLocation: 'VFTheHub', + AutoEssenceSpecifiedLocation: '', }) export const getProtocolSpaceTaskField = (tab: ProtocolSpaceTab): CurrentTaskField => @@ -219,7 +205,7 @@ export const isProtocolSpaceRewardEnabled = (config: MaaEndSanityConfig): boolea export const getSanityTaskDisplayValue = (rawConfig?: Partial | null) => { const config = normalizeMaaEndSanityConfig(rawConfig) if (config.SanityTaskType === 'Essence') { - return AUTO_ESSENCE_LOCATION_LABEL_MAP[config.AutoEssenceSpecifiedLocation] + return config.AutoEssenceSpecifiedLocation } return PROTOCOL_SPACE_TASK_LABEL_MAP[getCurrentProtocolTaskValue(config)] } @@ -235,9 +221,6 @@ export const normalizeMaaEndSanityConfig = ( if (!SANITY_TASK_TYPE_LABEL_MAP[config.SanityTaskType]) { config.SanityTaskType = 'OperatorProgression' } - if (!AUTO_ESSENCE_LOCATION_LABEL_MAP[config.AutoEssenceSpecifiedLocation]) { - config.AutoEssenceSpecifiedLocation = 'VFTheHub' - } if (!REWARD_LABEL_MAP[config.RewardsSetOption]) { config.RewardsSetOption = 'RewardsSetA' } diff --git a/frontend/src/utils/openExternal.ts b/frontend/src/utils/openExternal.ts index 27eb21c73..4a92d4c13 100644 --- a/frontend/src/utils/openExternal.ts +++ b/frontend/src/utils/openExternal.ts @@ -1,3 +1,5 @@ +export const MAS_QQ_GROUP_URL = 'https://qm.qq.com/q/bd9fISNoME' + /** * 在系统默认浏览器中打开URL * @param url 要打开的URL diff --git a/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue b/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue index 2e83e262d..65131ab22 100644 --- a/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue +++ b/frontend/src/views/EditView/Script/MaaEndScriptEdit.vue @@ -40,7 +40,18 @@ - + + + + 导出问题包 + MaaEnd专项还在积极测试中,如有问题请加入 + +
@@ -167,6 +186,8 @@ v-model:value="maaEndConfig.Game.ControllerType" size="large" :options="controllerOptions" + :loading="maaEndOptionsLoading" + :disabled="maaEndOptionsLoading || isSaving" @change="handleControllerTypeChange" /> @@ -260,7 +281,7 @@ - +