diff --git a/app/models/config/script.py b/app/models/config/script.py index c95f24f22e..fe9f864989 100644 --- a/app/models/config/script.py +++ b/app/models/config/script.py @@ -3057,6 +3057,14 @@ class TaskFields(ConfigGroup): description="每日任务后运行的附加任务", ) + IfCustomArguments: bool = Field( + default_factory=lambda: False, description="启用自定义启动参数" + ) + CustomArguments: str = Field( + default_factory=lambda: "-t DailyTask", + description="自定义启动参数,开启后原样传递给 ok-ww", + ) + FarmNightmareNestForDailyEcho: bool = Field( default_factory=lambda: True, description="使用梦魇巢穴完成日常声骸" ) @@ -3096,6 +3104,8 @@ def getTags(self) -> str: task_label = self.OKWW_TASK_BOOK.get(last_task_index, "未知") if self.Data.LastTaskName == "MultiAccountConfigTask": task_label = "原生多账号配置" + elif self.Data.LastTaskName == "CustomTask": + task_label = "自定义任务" tags.append({"text": f"任务:{task_label}", "color": "orange"}) tags.append(_tag_remained_days(self)) tags.append(_tag_notes(self)) diff --git a/app/task/Okww/AutoProxy.py b/app/task/Okww/AutoProxy.py index cccb95f7f5..56990c2157 100644 --- a/app/task/Okww/AutoProxy.py +++ b/app/task/Okww/AutoProxy.py @@ -23,8 +23,6 @@ from datetime import datetime from pathlib import Path -from packaging.version import InvalidVersion, Version - from app.config import ConfigCollection from app.core import Notify, Publisher from app.core.history import aggregate_data @@ -60,6 +58,8 @@ from . import push_log as _push_log from .tools import async_switch_account +from .tools.arguments import build_custom_arguments +from .tools.arguments import daily_task_argument as _daily_task_argument from .tools.backup_archive import ( archive_mas_runtime_backup, mas_config_dir, @@ -139,24 +139,6 @@ _OKWW_LOG_TIME_FORMAT = "%Y-%m-%d %H:%M:%S,%f" -def _daily_task_argument(script_root: Path) -> str: - """按已安装版本选择上游支持的日常任务参数。""" - app_config = json.loads( - (script_root / _OKWW_REL_APP_JSON).read_text(encoding="utf-8-sig") - ) - if not isinstance(app_config, dict): - raise ValueError("OK-WW 启动器配置格式错误,请先在脚本启动器中完成安装或更新") - raw_version = app_config.get("current_version") - try: - version = Version(str(raw_version)) - except InvalidVersion as exc: - raise ValueError( - "无法识别 OK-WW 当前版本,请先在脚本启动器中完成安装或更新" - ) from exc - # 上游 f09ac771(ok-script 2.0.7b1);首个含此变更的预发布 tag。 - return "DailyTask" if version >= Version("3.6.7b2") else "1" - - def _okww_config_base(raw: object) -> str: """读取 OK-WW 配置来源(base 三态),并兼容旧版字面量。""" @@ -271,6 +253,10 @@ async def check(self) -> str: return "请设置ok-ww脚本路径" if not (root / _OKWW_REL_APP_JSON).is_file(): return "请设置ok-ww脚本路径" + try: + self._build_run_arguments() + except ValueError as error: + return f"OK-WW 启动参数错误:{error}" # 启动检查早于每日重置,旧日期的次数不参与今日上限判断。 curdate = datetime.now(tz=_utils_constants.UTC4).strftime("%Y-%m-%d") proxy_times = ( @@ -444,13 +430,16 @@ async def prepare(self): self.task_name = "DailyTask" self.task_index = 1 if self.task_name == "DailyTask" else 0 - self.okww_args = ["-t", _daily_task_argument(self.script_root_path), "-e"] + self.okww_args = self._build_run_arguments() + if self.cur_user_config.Task.IfCustomArguments: + self.task_name = "CustomTask" + self.task_index = 0 self.script_config_path = self.script_root_path / _OKWW_REL_CONFIG_DIR async def _start_okww_with_current_arguments(self) -> None: """启动时刷新参数,自更新跨过兼容边界后用新参数重启。""" - daily = self.okww_args[1] in ("1", "DailyTask") + daily = self.task_name == "DailyTask" while True: if daily: self.okww_args = [ @@ -481,6 +470,11 @@ async def _start_okww_with_current_arguments(self) -> None: def _resolve_log_file_path(self) -> Path: return self.script_log_path + def _build_run_arguments(self) -> list[str]: + if self.cur_user_config.Task.IfCustomArguments: + return build_custom_arguments(self.cur_user_config.Task.CustomArguments) + return ["-t", _daily_task_argument(self.script_config.Info.RootPath), "-e"] + def _apply_mas_overrides(self) -> None: """overlay 覆盖段:把 MAS 侧运行值覆盖到当前来源配置之上。 @@ -858,7 +852,7 @@ async def main_task(self): # 开关在游戏配置区,依赖 Game.Enabled(未启用游戏配置则不生效); # 用户未填写手机号时跳过不切换。 if ( - self.task_name == "DailyTask" + self.task_name in ("DailyTask", "CustomTask") and self.script_config.Game.AccountSwitchMethod == "MAS" and self.script_config.Game.Enabled and (self.game_manager is not None) @@ -1052,6 +1046,7 @@ async def check_log( success_logs = ( _OKWW_SUCCESS_LOG[:1] if self.task_name == "MultiAccountConfigTask" + or self.cur_user_config.Task.IfCustomArguments else _OKWW_SUCCESS_LOG ) if any(needle in log for needle in success_logs): diff --git a/app/task/Okww/BatchProxy.py b/app/task/Okww/BatchProxy.py index d31f0da4d0..3e9598dfb3 100644 --- a/app/task/Okww/BatchProxy.py +++ b/app/task/Okww/BatchProxy.py @@ -38,11 +38,7 @@ def __init__( self, script_info, script_config, user_config, game_manager, *, participants ): super().__init__(script_info, script_config, user_config, game_manager) - self.cur_user_item = UserItem( - user_id=self.cur_user_item.user_id, - name=self.cur_user_item.name if len(participants) == 1 else "OK-WW 批次", - status="等待", - ) + # 准入检查始终保留真实账号节点;运行节点在 prepare 中另建。 self.participants = participants self.succeeded = set() self.pending = set() @@ -54,6 +50,11 @@ def __init__( self.account_failures = {} self.attempted_accounts = set() + def _build_run_arguments(self) -> list[str]: + if self.cur_user_config.Task.IfCustomArguments: + return super()._build_run_arguments() + return ["-t", "MultiAccountConfigTask", "-e"] + async def check(self): result = await super().check() if result != "Pass": @@ -72,6 +73,14 @@ async def check(self): async def prepare(self): await super().prepare() + # 内部轮次日志与真实账号的汇总记录隔离,单账号批次同样需要隔离。 + self.cur_user_item = UserItem( + user_id=self.cur_user_item.user_id, + name=self.cur_user_item.name + if len(self.participants) == 1 + else "OK-WW 批次", + status="等待", + ) today = datetime.now(tz=_utils_constants.UTC4) for user in self.participants: config = self.user_config[uuid.UUID(user.user_id)] @@ -80,7 +89,7 @@ async def prepare(self): config.Data.ProxyTimes = 0 await config.commit() self.task_name = "MultiAccountConfigTask" - self.okww_args = ["-t", self.task_name, "-e"] + self.okww_args = self._build_run_arguments() self.task_index = 0 # 原始账号历史独立于节点推送开关,关闭节点时也需保留日志。 if self._log_source is None: diff --git a/app/task/Okww/api.py b/app/task/Okww/api.py index e276b6f7a3..cf105e94c0 100644 --- a/app/task/Okww/api.py +++ b/app/task/Okww/api.py @@ -18,6 +18,7 @@ from fastapi import APIRouter from pydantic import ( + BaseModel, Field, ) @@ -37,6 +38,37 @@ class OkwwClientPathOut(OutBase): logger = get_logger("托管管理 API") +class OkwwArgumentsIn(BaseModel): + scriptId: str + custom: bool = False + arguments: str = "" + + +class OkwwArgumentsOut(OutBase): + arguments: list[str] = Field(default_factory=list) + native_switch: bool = False + + +@router.post("/okww/startup-arguments", tags=["Get"], response_model=OkwwArgumentsOut) +async def preview_okww_arguments_api(data: OkwwArgumentsIn) -> OkwwArgumentsOut: + """按实际启动规则预览参数,不启动脚本。""" + from .api_service import resolve_startup_arguments + + try: + arguments, native = resolve_startup_arguments( + data.scriptId, custom=data.custom, arguments=data.arguments + ) + return OkwwArgumentsOut( + code=200, + status="success", + message="", + arguments=arguments, + native_switch=native, + ) + except (ValueError, KeyError, TypeError, OSError) as error: + return OkwwArgumentsOut(code=400, status="error", message=str(error)) + + @router.get( "/okww/client-path", tags=["Get"], diff --git a/app/task/Okww/api_service.py b/app/task/Okww/api_service.py index 336a9af24b..7f8b4ba232 100644 --- a/app/task/Okww/api_service.py +++ b/app/task/Okww/api_service.py @@ -33,3 +33,21 @@ def resolve_client_path(script_id: str) -> Path: launcher_path ) return client_path + + +def resolve_startup_arguments( + script_id: str, *, custom: bool, arguments: str +) -> tuple[list[str], bool]: + from .tools.arguments import build_custom_arguments, daily_task_argument + + config = Config.ScriptConfig[uuid.UUID(script_id)] + if not isinstance(config, RuntimeOkwwConfig): + raise TypeError("托管配置类型错误, 不是 OK-WW 类型") + native = config.Game.AccountSwitchMethod in ("OKWW", "OKWW_SINGLE") + if custom: + return build_custom_arguments(arguments), native + if native: + return ["-t", "MultiAccountConfigTask", "-e"], True + if config.Info.RootPath is None: + raise ValueError("请设置 ok-ww 脚本路径") + return ["-t", daily_task_argument(config.Info.RootPath), "-e"], False diff --git a/app/task/Okww/manager.py b/app/task/Okww/manager.py index 030398b2f0..919b187f79 100644 --- a/app/task/Okww/manager.py +++ b/app/task/Okww/manager.py @@ -289,20 +289,28 @@ async def main_task(self): if self.script_config.Game.AccountSwitchMethod == "OKWW": participants = [] + argument_groups = [] + previous_arguments = None for index, user in enumerate(self.script_info.user_list): await self.script_info.set_current_index(index) if await self.check_user_before_run( user, self.user_config[uuid.UUID(user.user_id)] ): - candidate = AutoProxyTask( + candidate = BatchProxyTask( self.script_info, self.script_config, self.user_config, self.game_manager, + participants=[user], ) result = await candidate.check() if result == "Pass": participants.append(user) + arguments = candidate._build_run_arguments() + if not argument_groups or arguments != previous_arguments: + argument_groups.append([]) + argument_groups[-1].append(user) + previous_arguments = arguments else: await self.notify_user_check_failure(user, result) if participants: @@ -321,12 +329,23 @@ async def main_task(self): for user in participants: await self.notify_user_check_failure(user, result) else: - try: - for user in participants: - self.record_user_run_start(user) - await self.spawn(method) - finally: - await self._restore_script_config_from_temp() + for group in argument_groups: + await self.script_info.set_current_index( + self.script_info.user_list.index(group[0]) + ) + method = BatchProxyTask( + self.script_info, + self.script_config, + self.user_config, + self.game_manager, + participants=group, + ) + try: + for user in group: + self.record_user_run_start(user) + await self.spawn(method) + finally: + await self._restore_script_config_from_temp() return for _runtime_user_index in range(len(self.script_info.user_list)): diff --git a/app/task/Okww/tools/arguments.py b/app/task/Okww/tools/arguments.py new file mode 100644 index 0000000000..fedbb0cfd8 --- /dev/null +++ b/app/task/Okww/tools/arguments.py @@ -0,0 +1,31 @@ +"""OK-WW 普通模式启动参数。""" + +import json +from pathlib import Path + +from packaging.version import InvalidVersion, Version + +from app.task.proxy_helpers import split_args + + +def build_custom_arguments(arguments: str) -> list[str]: + """按原样拆分用户参数,不补充或替换选项。""" + return split_args(arguments) + + +def daily_task_argument(script_root: Path) -> str: + """按已安装版本选择上游支持的日常任务参数。""" + app_config = json.loads( + (script_root / "data/apps/ok-ww/app.json").read_text(encoding="utf-8-sig") + ) + if not isinstance(app_config, dict): + raise ValueError("OK-WW 启动器配置格式错误,请先在脚本启动器中完成安装或更新") + raw_version = app_config.get("current_version") + try: + version = Version(str(raw_version)) + except InvalidVersion as exc: + raise ValueError( + "无法识别 OK-WW 当前版本,请先在脚本启动器中完成安装或更新" + ) from exc + # 上游 f09ac771(ok-script 2.0.7b1);首个含此变更的预发布 tag。 + return "DailyTask" if version >= Version("3.6.7b2") else "1" diff --git a/app/task/Okww/tools/backup_archive.py b/app/task/Okww/tools/backup_archive.py index 0adf0d047f..4c29d235c9 100644 --- a/app/task/Okww/tools/backup_archive.py +++ b/app/task/Okww/tools/backup_archive.py @@ -130,6 +130,8 @@ def mas_config_dir(script_id: str, owner: str) -> Path: 恢复时以当前值为准,回填旧值会静默翻转 shared/independent 来源。""" _OVERLAY_TASK_KEYS = ( + "IfCustomArguments", + "CustomArguments", "WhichToFarm", "WhichTacetSuppressionToFarm", "WhichForgeryChallengeToFarm", @@ -496,6 +498,8 @@ def restore_native_backup(config_path: Path, ts: str) -> None: """DailyTask.json 文件字段名 → MAS 账号配置 Task 键名(同一概念的两种 owner)""" _OVERLAY_FIELD_LABELS = { + "IfCustomArguments": "自定义启动参数", + "CustomArguments": "启动参数", "Id": "账号", "Base": "配置文件来源", **{overlay: _FIELD_LABELS[file] for file, overlay in _FILE_TO_OVERLAY_KEY.items()}, diff --git a/changelog.d/feat-okww-custom-arguments.feat.md b/changelog.d/feat-okww-custom-arguments.feat.md new file mode 100644 index 0000000000..8763d4cb37 --- /dev/null +++ b/changelog.d/feat-okww-custom-arguments.feat.md @@ -0,0 +1,2 @@ +project: okww +支持为账号自定义启动参数 diff --git a/frontend/e2e/okww-arguments.pw.ts b/frontend/e2e/okww-arguments.pw.ts new file mode 100644 index 0000000000..c040a61cc6 --- /dev/null +++ b/frontend/e2e/okww-arguments.pw.ts @@ -0,0 +1,123 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { expect, test } from './fixtures' + +const backend = `http://127.0.0.1:${process.env.AUTO_MAS_E2E_BACKEND_PORT ?? 36174}` + +test.describe('@okww-arguments', () => { + for (const mode of ['MAS', 'OKWW_SINGLE', 'OKWW']) { + test(`confirms, persists and reloads custom arguments in ${mode}`, async ({ app, page }) => { + const post = async (endpoint: string, data: unknown) => { + const response = await page.request.post(`${backend}${endpoint}`, { data }) + expect(response.ok()).toBe(true) + const body = await response.json() + expect(body.code, body.message).toBe(200) + return body + } + const { scriptId } = await post('/api/scripts/add', { type: 'Okww' }) + const install = path.join(path.dirname(process.env.AUTO_MAS_E2E_DATA_ROOT!), `okww-${mode}`) + const appDir = path.join(install, 'data/apps/ok-ww') + await fs.mkdir(path.join(appDir, 'working/configs'), { recursive: true }) + await fs.writeFile(path.join(install, 'ok-ww.exe'), '') + await fs.writeFile(path.join(appDir, 'app.json'), '{"current_version":"v3.8.0-beta.1"}') + await fs.writeFile(path.join(appDir, 'working/configs/DailyTask.json'), '{}') + await post('/api/scripts/update', { + scriptId, + data: { Info: { RootPath: install }, Game: { AccountSwitchMethod: mode } }, + }) + const { userId } = await post('/api/scripts/user/add', { scriptId }) + await app.goto(`/scripts/${scriptId}/users/${userId}/edit/okww`) + const field = page.locator('.ant-form-item').filter({ + has: page.getByRole('group', { name: 'Launch arguments', exact: true }), + }) + const input = field.getByRole('textbox') + const toggle = field.getByRole('switch', { name: 'Custom arguments', exact: true }) + const original = `-t ${mode === 'MAS' ? 'DailyTask' : 'MultiAccountConfigTask'} -e` + await expect(input).toHaveValue(original) + await expect(input).toHaveAttribute('readonly') + await expect(toggle).toBeEnabled() + + await toggle.click() + const dialog = page.getByRole('dialog') + await expect(dialog).toContainText('Changing launch arguments may prevent ok-ww') + await dialog.getByRole('button', { name: 'Cancel', exact: true }).click() + await expect(toggle).not.toBeChecked() + await expect(input).toHaveValue(original) + + await page.route( + '**/api/scripts/user/update', + route => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ code: 400, status: 'error', message: 'E2E save rejected' }), + }), + { times: 1 } + ) + await toggle.click() + const rejected = page.waitForResponse(response => + response.url().endsWith('/api/scripts/user/update') + ) + await dialog.getByRole('button', { name: 'OK', exact: true }).click() + expect((await (await rejected).json()).code).toBe(400) + await expect(dialog).not.toBeVisible() + await expect(toggle).not.toBeChecked() + await expect(input).toHaveValue(original) + await expect(toggle).toBeEnabled() + + await toggle.click() + const enabled = page.waitForResponse( + response => + response.url().endsWith('/api/scripts/user/update') && + response.request().method() === 'POST' + ) + await dialog.getByRole('button', { name: 'OK', exact: true }).click() + expect((await (await enabled).json()).code).toBe(200) + await expect(dialog).not.toBeVisible() + await expect(toggle).toBeChecked() + await expect(input).toHaveValue(original) + await expect(input).not.toHaveAttribute('readonly') + + await input.fill('"unfinished') + await expect(field).toContainText('No closing quotation') + const custom = '-t "Other Task" -h --' + await input.fill(custom) + const saved = page.waitForResponse( + response => + response.url().endsWith('/api/scripts/user/update') && + response.request().method() === 'POST' + ) + await input.blur() + expect((await (await saved).json()).code).toBe(200) + await page.reload() + await expect(toggle).toBeChecked() + await expect(input).toHaveValue(custom) + const preview = await post('/api/scripts/okww/startup-arguments', { + scriptId, + custom: true, + arguments: custom, + }) + expect(preview.arguments).toEqual(['-t', 'Other Task', '-h', '--']) + await expect(toggle).toBeEnabled() + await field.scrollIntoViewIfNeeded() + await app.evidence(`okww-custom-arguments-${mode}`) + + const disabled = page.waitForResponse( + response => + response.url().endsWith('/api/scripts/user/update') && + response.request().method() === 'POST' + ) + await toggle.click() + expect((await (await disabled).json()).code).toBe(200) + await expect(input).toHaveValue(original) + await expect(input).toHaveAttribute('readonly') + await expect(toggle).toBeEnabled() + await toggle.click() + await expect(dialog).toBeVisible() + await app.goto('/scripts') + await expect(dialog).not.toBeVisible() + const stored = await post('/api/scripts/user/get', { scriptId, userId }) + expect(stored.data[userId].Task.IfCustomArguments).toBe(false) + }) + } +}) diff --git a/frontend/electron/services/analyticsCoverage.md b/frontend/electron/services/analyticsCoverage.md index 2b05035846..2bbbc26074 100644 --- a/frontend/electron/services/analyticsCoverage.md +++ b/frontend/electron/services/analyticsCoverage.md @@ -209,3 +209,5 @@ started 未送达时不发送对应 finished。运行 UUID 只留在本机消息 OkNte 日常计划的快捷表格与详细编辑共享当天字段覆写。快捷保存复用计划编辑事件,详细编辑按保存接口结果记录;不采集日常参数或计划标识。 - Okww 原生多账号任务:任务选择保存复用 `useUserApi.updateUser` 的 `config_write_finished`;批次仍按 MAS 用户归属结果,运行结局沿用 `taskAnalytics.ts` 的现有观察边界,不拆分或采集上游账号、任务配置及日志。 + +- OK-WW 自定义启动参数的账号保存复用 `useUserApi.updateUser` 的 `config_write_finished`;运行复用既有任务启动和终态事件,不采集参数内容或任务名称。 diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1dff4b7bbb..7f3a8e40f3 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -974,6 +974,8 @@ export type { OkNteUserConfig_NotifyInput } from './models/OkNteUserConfig_Notif export type { OkNteUserConfig_Task } from './models/OkNteUserConfig_Task'; export type { OkNteUserConfig_TaskInput } from './models/OkNteUserConfig_TaskInput'; export type { OkNteUserConfigInput } from './models/OkNteUserConfigInput'; +export type { OkwwArgumentsIn } from './models/OkwwArgumentsIn'; +export type { OkwwArgumentsOut } from './models/OkwwArgumentsOut'; export type { OkwwClientPathOut } from './models/OkwwClientPathOut'; export type { OkwwConfig } from './models/OkwwConfig'; export type { OkwwConfig_Game } from './models/OkwwConfig_Game'; diff --git a/frontend/src/api/models/OkwwArgumentsIn.ts b/frontend/src/api/models/OkwwArgumentsIn.ts new file mode 100644 index 0000000000..879912e02a --- /dev/null +++ b/frontend/src/api/models/OkwwArgumentsIn.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type OkwwArgumentsIn = { + scriptId: string; + custom?: boolean; + arguments?: string; +}; + diff --git a/frontend/src/api/models/OkwwArgumentsOut.ts b/frontend/src/api/models/OkwwArgumentsOut.ts new file mode 100644 index 0000000000..78d1bf3fa7 --- /dev/null +++ b/frontend/src/api/models/OkwwArgumentsOut.ts @@ -0,0 +1,21 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type OkwwArgumentsOut = { + /** + * 状态码 + */ + code?: number; + /** + * 操作状态 + */ + status?: string; + /** + * 操作消息 + */ + message?: string; + arguments?: Array; + native_switch?: boolean; +}; + diff --git a/frontend/src/api/models/OkwwUserConfig_Task.ts b/frontend/src/api/models/OkwwUserConfig_Task.ts index 32db030450..5b68a49e42 100644 --- a/frontend/src/api/models/OkwwUserConfig_Task.ts +++ b/frontend/src/api/models/OkwwUserConfig_Task.ts @@ -7,6 +7,14 @@ export type OkwwUserConfig_Task = { * 每日任务后运行的附加任务 */ AdditionalTasks?: (Array<'Check Weekly Garden' | 'Auto Farm all Nightmare Nest' | 'Merge Echo If discarded > 1000' | 'Teleport and Farm 4C Echo'> | null); + /** + * 启用自定义启动参数 + */ + IfCustomArguments?: (boolean | null); + /** + * 自定义启动参数,开启后原样传递给 ok-ww + */ + CustomArguments?: (string | null); /** * 使用梦魇巢穴完成日常声骸 */ diff --git a/frontend/src/api/models/OkwwUserConfig_TaskInput.ts b/frontend/src/api/models/OkwwUserConfig_TaskInput.ts index 2709cfbb55..bad07b7b48 100644 --- a/frontend/src/api/models/OkwwUserConfig_TaskInput.ts +++ b/frontend/src/api/models/OkwwUserConfig_TaskInput.ts @@ -7,6 +7,14 @@ export type OkwwUserConfig_TaskInput = { * 每日任务后运行的附加任务 */ AdditionalTasks?: (Array<'Check Weekly Garden' | 'Auto Farm all Nightmare Nest' | 'Merge Echo If discarded > 1000' | 'Teleport and Farm 4C Echo'> | null); + /** + * 启用自定义启动参数 + */ + IfCustomArguments?: (boolean | null); + /** + * 自定义启动参数,开启后原样传递给 ok-ww + */ + CustomArguments?: (string | null); /** * 使用梦魇巢穴完成日常声骸 */ diff --git a/frontend/src/api/services/GetService.ts b/frontend/src/api/services/GetService.ts index 29d2dd8b59..bf4b309434 100644 --- a/frontend/src/api/services/GetService.ts +++ b/frontend/src/api/services/GetService.ts @@ -63,6 +63,8 @@ import type { NotifyChannelsOut } from '../models/NotifyChannelsOut'; import type { ObsReplayCheckOut } from '../models/ObsReplayCheckOut'; import type { OCRScreenshotIn } from '../models/OCRScreenshotIn'; import type { OCRScreenshotOut } from '../models/OCRScreenshotOut'; +import type { OkwwArgumentsIn } from '../models/OkwwArgumentsIn'; +import type { OkwwArgumentsOut } from '../models/OkwwArgumentsOut'; import type { OkwwClientPathOut } from '../models/OkwwClientPathOut'; import type { PlanComboxIn } from '../models/PlanComboxIn'; import type { PlanGetIn } from '../models/PlanGetIn'; @@ -545,6 +547,26 @@ export class GetService { }, }); } + /** + * Preview Okww Arguments Api + * 按实际启动规则预览参数,不启动脚本。 + * @param requestBody + * @returns OkwwArgumentsOut Successful Response + * @throws ApiError + */ + public static previewOkwwArgumentsApiApiScriptsOkwwStartupArgumentsPost( + requestBody: OkwwArgumentsIn, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/okww/startup-arguments', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * 解码鸣潮启动器,返回客户端 exe 路径 * 解码鸣潮启动器记录,返回客户端 exe 完整路径(仅直启模式的前端展示用)。 diff --git a/frontend/src/api/services/Service.ts b/frontend/src/api/services/Service.ts index 4f1c63c9f4..6c3b0bd9ce 100644 --- a/frontend/src/api/services/Service.ts +++ b/frontend/src/api/services/Service.ts @@ -118,6 +118,8 @@ import type { NotifyChannelsOut } from '../models/NotifyChannelsOut'; import type { ObsReplayCheckOut } from '../models/ObsReplayCheckOut'; import type { ObsReplaySaveOut } from '../models/ObsReplaySaveOut'; import type { OkNtePlanConfigsOut } from '../models/OkNtePlanConfigsOut'; +import type { OkwwArgumentsIn } from '../models/OkwwArgumentsIn'; +import type { OkwwArgumentsOut } from '../models/OkwwArgumentsOut'; import type { OkwwClientPathOut } from '../models/OkwwClientPathOut'; import type { OutBase } from '../models/OutBase'; import type { PatternDebugIn } from '../models/PatternDebugIn'; @@ -1214,6 +1216,26 @@ export class Service { }, }); } + /** + * Preview Okww Arguments Api + * 按实际启动规则预览参数,不启动脚本。 + * @param requestBody + * @returns OkwwArgumentsOut Successful Response + * @throws ApiError + */ + public static previewOkwwArgumentsApiApiScriptsOkwwStartupArgumentsPost( + requestBody: OkwwArgumentsIn, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/scripts/okww/startup-arguments', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * 解码鸣潮启动器,返回客户端 exe 路径 * 解码鸣潮启动器记录,返回客户端 exe 完整路径(仅直启模式的前端展示用)。 diff --git a/frontend/src/composables/useOkwwArgumentsApi.ts b/frontend/src/composables/useOkwwArgumentsApi.ts new file mode 100644 index 0000000000..4de3815fbf --- /dev/null +++ b/frontend/src/composables/useOkwwArgumentsApi.ts @@ -0,0 +1,14 @@ +import { Service } from '@/api' + +export function useOkwwArgumentsApi() { + const previewArguments = async (scriptId: string, custom: boolean, argumentsText: string) => { + const response = await Service.previewOkwwArgumentsApiApiScriptsOkwwStartupArgumentsPost({ + scriptId, + custom, + arguments: argumentsText, + }) + if (response.code !== 200) throw new Error(response.message || '无法读取启动参数') + return response + } + return { previewArguments } +} diff --git a/frontend/src/views/EditView/User/OkwwUserEdit.vue b/frontend/src/views/EditView/User/OkwwUserEdit.vue index 16c699de8f..121120603f 100644 --- a/frontend/src/views/EditView/User/OkwwUserEdit.vue +++ b/frontend/src/views/EditView/User/OkwwUserEdit.vue @@ -188,6 +188,8 @@ import OkwwTaskConfigSection from './components/OkwwTaskConfigSection.vue' +import { getDefaultOkwwTaskConfig } from './components/okwwTaskConfig' import UserAccountHeading from '@/components/UserAccountHeading.vue' import accountIcon from '@/assets/ok-ww.ico' import UserBasicInfoFields from '@/components/UserBasicInfoFields.vue' @@ -395,14 +398,7 @@ const getDefaultUserData = (): Omit => ({ Notes: '', Tag: '', }, - Task: { - WhichToFarm: 'Tacet Suppression', - WhichTacetSuppressionToFarm: 1, - WhichForgeryChallengeToFarm: 1, - MaterialSelection: 'Shell Credit', - FarmNightmareNestForDailyEcho: true, - AdditionalTasks: ['Check Weekly Garden'], - }, + Task: getDefaultOkwwTaskConfig(), Notify: { Enabled: false, PushLogMode: '汇总', @@ -493,20 +489,10 @@ const handleQuickConfigChange = async (value: boolean) => { } } -const saveTaskConfig = async () => { - if (isInitializing.value || !userId.value) return - await enqueue(() => - updateUser(scriptId, userId.value, { - Task: { - WhichToFarm: formData.Task.WhichToFarm, - WhichTacetSuppressionToFarm: formData.Task.WhichTacetSuppressionToFarm, - WhichForgeryChallengeToFarm: formData.Task.WhichForgeryChallengeToFarm, - MaterialSelection: formData.Task.MaterialSelection, - FarmNightmareNestForDailyEcho: formData.Task.FarmNightmareNestForDailyEcho, - AdditionalTasks: formData.Task.AdditionalTasks, - }, - }) - ) +const saveTaskConfig = async (): Promise => { + if (isInitializing.value || !userId.value) return false + const Task = { ...formData.Task, AdditionalTasks: [...formData.Task.AdditionalTasks] } + return await enqueue(() => updateUser(scriptId, userId.value, { Task })) } const handleOkwwConfig = async () => { diff --git a/frontend/src/views/EditView/User/components/OkwwArgumentsConfig.test.ts b/frontend/src/views/EditView/User/components/OkwwArgumentsConfig.test.ts new file mode 100644 index 0000000000..affb1f51e2 --- /dev/null +++ b/frontend/src/views/EditView/User/components/OkwwArgumentsConfig.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' +import { createSSRApp, h } from 'vue' +import { renderToString } from '@vue/server-renderer' +import { createI18n } from 'vue-i18n' +import Antd, { Form } from 'ant-design-vue' +import OkwwArgumentsConfig from './OkwwArgumentsConfig.vue' +import { getDefaultOkwwTaskConfig } from './okwwTaskConfig' + +async function renderArguments(custom: boolean, disabled = false) { + const task = getDefaultOkwwTaskConfig() + task.IfCustomArguments = custom + task.CustomArguments = '-t OtherTask -h' + const app = createSSRApp({ + render: () => + h(Form, { disabled }, () => + h(OkwwArgumentsConfig, { modelValue: task, scriptId: 'test', save: async () => true }) + ), + }) + app.use(Antd) + app.use(createI18n({ legacy: false, locale: 'en', missingWarn: false, fallbackWarn: false })) + return renderToString(app) +} + +describe('OK-WW custom arguments', () => { + it('keeps default arguments readonly while custom mode is off', async () => { + const html = await renderArguments(false) + expect(html).not.toContain('-t DailyTask -e / -t 1 -e') + expect(html).toContain('readonly') + expect(html).not.toContain('OtherTask') + }) + + it('allows editing without rewriting the entered arguments', async () => { + const html = await renderArguments(true) + expect(html).toContain('value="-t OtherTask -h"') + expect(html).not.toContain('readonly') + }) + + it('inherits the account configuration lock from its form', async () => { + const html = await renderArguments(true, true) + expect(html).toMatch(/]*disabled/) + expect(html).toMatch(/]*disabled/) + }) +}) diff --git a/frontend/src/views/EditView/User/components/OkwwArgumentsConfig.vue b/frontend/src/views/EditView/User/components/OkwwArgumentsConfig.vue new file mode 100644 index 0000000000..1a5b5f933d --- /dev/null +++ b/frontend/src/views/EditView/User/components/OkwwArgumentsConfig.vue @@ -0,0 +1,99 @@ + + + diff --git a/frontend/src/views/EditView/User/components/OkwwTaskConfigSection.vue b/frontend/src/views/EditView/User/components/OkwwTaskConfigSection.vue index ff859e18a1..532b2b1825 100644 --- a/frontend/src/views/EditView/User/components/OkwwTaskConfigSection.vue +++ b/frontend/src/views/EditView/User/components/OkwwTaskConfigSection.vue @@ -4,16 +4,12 @@
- - - - + @@ -84,19 +80,18 @@