From 267415071f658d2820da59c03fcb1da97c720bbe Mon Sep 17 00:00:00 2001 From: AthenaHibou <13084046220@163.com> Date: Tue, 1 Sep 2026 16:09:12 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(okww):=20=E8=B4=A6=E5=8F=B7=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=AD=89=E5=BE=85=E6=B8=B8=E6=88=8F=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E4=B8=8E=E7=99=BB=E5=BD=95=E6=80=81=E5=B0=B1=E7=BB=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/task/Okww/tools/account_switch.py | 97 +++++++++++++++++++++------ res/version.json | 3 +- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/app/task/Okww/tools/account_switch.py b/app/task/Okww/tools/account_switch.py index d36c3448a..66867eb12 100644 --- a/app/task/Okww/tools/account_switch.py +++ b/app/task/Okww/tools/account_switch.py @@ -78,6 +78,17 @@ def _write_diagnostic(text: str) -> None: _WUWA_CLASS = "UnrealWindow" _WUWA_PROCESS = "Client-Win64-Shipping.exe" +# 游戏窗口就绪宽限期:进程拉起到 UnrealWindow 窗口可见通常存在启动延迟,且设备 +# 性能越差窗口创建/亮相越慢。账号切换紧随脚本配置的定长 WaitTime 之后立即执行, +# 须在宽限期内轮询等待窗口就绪,否则慢设备会误报「未找到鸣潮游戏窗口」。 +_GAME_WINDOW_WAIT_SECONDS = 60.0 +# 窗口轮询间隔。 +_WINDOW_POLL_INTERVAL = 1.0 +# 启动界面稳定等待:窗口已出现但尚未进入可执行登录态(仍停在警告/加载过渡帧)时, +# 在宽限期内轮询等待「登录页」或「已登录主菜单」出现;超时说明可能已直接进入游戏 +# 主场景,交由返回登录流程兜底,避免在过渡帧上盲目按 ESC。 +_SWITCH_STABILIZE_SECONDS = 45.0 + # 截图基准分辨率(16:9),OCR 与点击均在此坐标空间计算后再映射回真实窗口 _FRAME_WIDTH = 1920 _FRAME_HEIGHT = 1080 @@ -132,33 +143,49 @@ def _window_area(hwnd: int) -> int: return 0 -def _find_game_hwnd() -> int: +def _find_game_hwnd(*, wait: bool = True) -> int: """按窗口类 + 所属进程名定位鸣潮主窗口。 用 ``EnumWindows + psutil.Process(pid).name()`` 而非 process_iter 的 name 属性, 规避提权进程 name 读取被拒导致的漏判;同进程存在多个窗口时取面积最大者。 + + Args: + wait: 为 True 时在宽限期 ``_GAME_WINDOW_WAIT_SECONDS`` 内轮询等待窗口就绪, + 以吸收进程拉起后窗口延迟亮相的启动阶段;为 False 时单次枚举立即返回。 + + Raises: + RuntimeError: 宽限期结束仍未定位到鸣潮游戏窗口。 """ - candidates: list[int] = [] + deadline = time.monotonic() + _GAME_WINDOW_WAIT_SECONDS + while True: + candidates: list[int] = [] - def _enum(hwnd: int, _lparam: int) -> bool: - try: - if not win32gui.IsWindowVisible(hwnd): - return True - if win32gui.GetClassName(hwnd) != _WUWA_CLASS: + def _enum(hwnd: int, _lparam: int) -> bool: + try: + if not win32gui.IsWindowVisible(hwnd): + return True + if win32gui.GetClassName(hwnd) != _WUWA_CLASS: + return True + except Exception: return True - except Exception: + _, pid = win32process.GetWindowThreadProcessId(hwnd) + if pid and _process_name(pid) == _WUWA_PROCESS: + candidates.append(hwnd) return True - _, pid = win32process.GetWindowThreadProcessId(hwnd) - if pid and _process_name(pid) == _WUWA_PROCESS: - candidates.append(hwnd) - return True - win32gui.EnumWindows(_enum, 0) - if not candidates: - raise RuntimeError( - f"未找到鸣潮游戏窗口(进程 {_WUWA_PROCESS},窗口类 {_WUWA_CLASS})" + win32gui.EnumWindows(_enum, 0) + if candidates: + return max(candidates, key=_window_area) + if not wait or time.monotonic() >= deadline: + break + logger.info( + "鸣潮游戏进程已启动但窗口暂未就绪," + f"{_WINDOW_POLL_INTERVAL:g} 秒后重试..." ) - return max(candidates, key=_window_area) + time.sleep(_WINDOW_POLL_INTERVAL) + raise RuntimeError( + f"未找到鸣潮游戏窗口(进程 {_WUWA_PROCESS},窗口类 {_WUWA_CLASS})" + ) # ── 截图 / 交互(前台 pyautogui + DPI 适配)───────────────────────────── @@ -389,8 +416,39 @@ def _logout_from_menu(hwnd: int, on_log: Callable[[str], None]) -> None: raise RuntimeError("未找到「确认登出」入口,请人工确认主菜单登出按钮位置") +def _wait_for_actionable_state( + hwnd: int, on_log: Callable[[str], None] +) -> None: + """等待进入可执行的登录态(登录页或已登录主菜单)。 + + 游戏窗口刚出现时可能仍停在启动过渡帧(警告弹窗、加载条、自动登录等),此时登录页 + 与主菜单两态都不命中。若立即按 ESC 走返回登录,可能落在不匹配的画面上。故在宽限期 + ``_SWITCH_STABILIZE_SECONDS`` 内轮询等待两态之一出现: + + - 命中任意态 → 返回,由调用方按对应态执行(登录页→选择登录,主菜单→登出); + - 超时仍未命中 → 可能已直接进入游戏主场景(无法用登录/菜单特征识别),交由调用方 + 走「ESC→终端→返回登录」兜底。 + + 进度日志按约 5 条/轮询节流,避免每秒向调度台刷屏。 + """ + deadline = time.monotonic() + _SWITCH_STABILIZE_SECONDS + iter_count = 0 + while time.monotonic() < deadline: + if _on_login_page(hwnd) or _on_logged_in_menu(hwnd): + return + if iter_count % 5 == 0: + on_log("窗口已出现但尚未进入登录态,等待启动过渡帧稳定...") + iter_count += 1 + time.sleep(1) + on_log( + f"等待登录界面稳定超时({_SWITCH_STABILIZE_SECONDS:g}s),按未知状态走返回登录流程" + ) + + def _switch_to_login(hwnd: int, on_log: Callable[[str], None]) -> None: - """回到登录界面;已处于登录页直接返回,主菜单走登出,其余走游戏内返回。""" + """回到登录界面;已处于登录页直接返回,主菜单走登出,其余(含游戏主场景)走游戏内返回。""" + _wait_for_actionable_state(hwnd, on_log) + if _on_login_page(hwnd): on_log("已处于登录界面,跳过返回登录") return @@ -590,7 +648,8 @@ def _on_log(msg: str) -> None: _select_and_login(hwnd, suffix, _on_log) except Exception: try: - _save_error_screenshot(_find_game_hwnd()) + # wait=False:主流程已等待过窗口,此处单次枚举即可,避免失败后再空等宽限期。 + _save_error_screenshot(_find_game_hwnd(wait=False)) except Exception: pass raise diff --git a/res/version.json b/res/version.json index 404d04a24..71f5ada12 100644 --- a/res/version.json +++ b/res/version.json @@ -19,7 +19,8 @@ "OK-WW与OK-NTE专项 多用户切换时等待上一用户的游戏进程完全退出,避免误复用正在退出的旧游戏窗口导致任务被中止 by [@AthenaHibou](https://github.com/AthenaHibou)", "OK-NTE专项 修复任务报告剩余体力计算不准确的问题:体力不足以刷满设定目标时误显「剩余体力: 0」,改为按实际刷本消耗(异象界域双倍/单倍次数、异象追猎实际消耗)精确计算剩余体力 by [@AthenaHibou](https://github.com/AthenaHibou)", "前端类型治理 清理脚本与 Electron 基础边界中的 any,并修复 BetterGI 设置会话无法接收完成和错误事件的问题", - "HSR专项 修复 MAS 管控任务的文本与 JSON 配置项在输入过程中被重渲染清空、导致只有数字项能填写的问题(如货币战争的首领/投资重开条件、策略文件、开拓者名称) by [@qiyinxi](https://github.com/qiyinxi)" + "HSR专项 修复 MAS 管控任务的文本与 JSON 配置项在输入过程中被重渲染清空、导致只有数字项能填写的问题(如货币战争的首领/投资重开条件、策略文件、开拓者名称) by [@qiyinxi](https://github.com/qiyinxi)", + "OK-WW专项 修复账号切换在游戏启动阶段定位不到窗口、或在过渡帧上就直接操作而失败的问题,改为分两段在宽限期内轮询等待:先等窗口就绪,再等界面进入可执行的登录态,兼容低性能设备的启动延迟" ] }, "v5.5.0-beta.2": { From ae3b8054d28ef8525f8438d3769be0548bdf5817 Mon Sep 17 00:00:00 2001 From: AthenaHibou <13084046220@163.com> Date: Tue, 1 Sep 2026 17:11:24 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(okww):=20=E8=B4=A6=E5=8F=B7=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E4=BB=A5=E8=BF=9B=E5=B1=95=E7=BB=AD=E5=BB=B6=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E6=B8=B8=E6=88=8F=E5=86=85=E6=9B=B4=E6=96=B0/?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=B0=B1=E7=BB=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/task/Okww/tools/account_switch.py | 55 +++++++++++++++++++-------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/app/task/Okww/tools/account_switch.py b/app/task/Okww/tools/account_switch.py index 66867eb12..2973742e5 100644 --- a/app/task/Okww/tools/account_switch.py +++ b/app/task/Okww/tools/account_switch.py @@ -84,10 +84,15 @@ def _write_diagnostic(text: str) -> None: _GAME_WINDOW_WAIT_SECONDS = 60.0 # 窗口轮询间隔。 _WINDOW_POLL_INTERVAL = 1.0 -# 启动界面稳定等待:窗口已出现但尚未进入可执行登录态(仍停在警告/加载过渡帧)时, -# 在宽限期内轮询等待「登录页」或「已登录主菜单」出现;超时说明可能已直接进入游戏 -# 主场景,交由返回登录流程兜底,避免在过渡帧上盲目按 ESC。 -_SWITCH_STABILIZE_SECONDS = 45.0 +# 启动界面稳定等待:窗口已出现但尚未进入可执行登录态(仍停在警告/加载/游戏内更新 +# 过渡帧)时,在硬上限内轮询等待「登录页」或「已登录主菜单」出现。鸣潮更新由 MAS +# 启动前预更新,游戏内更新概率较低,但仍以「进展续延」应答长加载:界面仍在变化时 +# 持续顺延,长时间无进展才判失败并交由返回登录流程兜底。 +_IN_GAME_UPDATE_TIMEOUT = 1800.0 +# 长时间无进展判定:界面静止达到该时长仍未进入登录态则视为卡死,提前交还兜底流程。 +_IN_GAME_STALL_SECONDS = 60.0 +# 游戏内更新/加载等待的轮询间隔(比窗口等待更宽松,降低长等待期 OCR 负载)。 +_IN_GAME_POLL_INTERVAL = 3.0 # 截图基准分辨率(16:9),OCR 与点击均在此坐标空间计算后再映射回真实窗口 _FRAME_WIDTH = 1920 @@ -416,32 +421,52 @@ def _logout_from_menu(hwnd: int, on_log: Callable[[str], None]) -> None: raise RuntimeError("未找到「确认登出」入口,请人工确认主菜单登出按钮位置") +def _frame_signature(frame: np.ndarray) -> int: + """对下采样的帧做哈希,用于判断界面是否仍在变化(有更新/加载进展)。""" + small = frame[::40, ::40] + return hash(small.tobytes()) + + def _wait_for_actionable_state( hwnd: int, on_log: Callable[[str], None] ) -> None: """等待进入可执行的登录态(登录页或已登录主菜单)。 - 游戏窗口刚出现时可能仍停在启动过渡帧(警告弹窗、加载条、自动登录等),此时登录页 - 与主菜单两态都不命中。若立即按 ESC 走返回登录,可能落在不匹配的画面上。故在宽限期 - ``_SWITCH_STABILIZE_SECONDS`` 内轮询等待两态之一出现: + 游戏窗口刚出现时可能仍停在启动过渡帧(警告弹窗、加载条、游戏内更新、自动登录等), + 此时登录页与主菜单两态都不命中。若立即按 ESC 走返回登录,可能落在不匹配的画面上。 + 故采用「进展续延」语义等待两态之一出现: - 命中任意态 → 返回,由调用方按对应态执行(登录页→选择登录,主菜单→登出); - - 超时仍未命中 → 可能已直接进入游戏主场景(无法用登录/菜单特征识别),交由调用方 - 走「ESC→终端→返回登录」兜底。 + - 界面仍在变化(有更新/加载进展)→ 持续顺延,硬上限 ``_IN_GAME_UPDATE_TIMEOUT``; + - 界面持续 ``_IN_GAME_STALL_SECONDS`` 无任何进展且仍非登录态 → 判卡死,交还调用方 + 走「ESC→终端→返回登录」兜底(覆盖已直接进入游戏主场景的情形)。 - 进度日志按约 5 条/轮询节流,避免每秒向调度台刷屏。 + 进度日志按约 5 条/轮询节流,避免向调度台频繁刷屏。 """ - deadline = time.monotonic() + _SWITCH_STABILIZE_SECONDS + deadline = time.monotonic() + _IN_GAME_UPDATE_TIMEOUT + last_progress = time.monotonic() + last_sig: int | None = None iter_count = 0 while time.monotonic() < deadline: - if _on_login_page(hwnd) or _on_logged_in_menu(hwnd): + frame = _capture_window(hwnd, activate=False) + if ( + _find_text(ocr_image(frame, _LOGIN_ROI), _LOGIN_PAGE_TEXTS) is not None + or _find_text(ocr_image(frame), _LOGGED_IN_MENU_TEXTS) is not None + ): return + sig = _frame_signature(frame) + if sig != last_sig: + last_sig = sig + last_progress = time.monotonic() + if time.monotonic() - last_progress >= _IN_GAME_STALL_SECONDS: + break if iter_count % 5 == 0: - on_log("窗口已出现但尚未进入登录态,等待启动过渡帧稳定...") + on_log("鸣潮仍在启动/游戏内更新或加载过渡帧中,等待进入登录页或已登录主菜单...") iter_count += 1 - time.sleep(1) + time.sleep(_IN_GAME_POLL_INTERVAL) on_log( - f"等待登录界面稳定超时({_SWITCH_STABILIZE_SECONDS:g}s),按未知状态走返回登录流程" + "等待进入可执行登录态超时或长时间无进展" + f"({_IN_GAME_UPDATE_TIMEOUT:g}s),按未知状态走返回登录流程" ) From d3c114e74ad75438c66d39712a29836c536f2bd1 Mon Sep 17 00:00:00 2001 From: AthenaHibou <13084046220@163.com> Date: Tue, 1 Sep 2026 18:52:34 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(okww):=20=E8=B4=A6=E5=8F=B7=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=A8=B3=E5=AE=9A=E5=8C=96=E7=AD=89=E5=BE=85=E6=9C=9F?= =?UTF-8?q?=E9=97=B4=E8=A1=A5=E9=BD=90=E8=AF=8A=E6=96=AD=20OCR=20=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/task/Okww/tools/account_switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/task/Okww/tools/account_switch.py b/app/task/Okww/tools/account_switch.py index 2973742e5..2af961f69 100644 --- a/app/task/Okww/tools/account_switch.py +++ b/app/task/Okww/tools/account_switch.py @@ -449,9 +449,11 @@ def _wait_for_actionable_state( iter_count = 0 while time.monotonic() < deadline: frame = _capture_window(hwnd, activate=False) + items_full = ocr_image(frame) + _dump_ocr_items(items_full) if ( _find_text(ocr_image(frame, _LOGIN_ROI), _LOGIN_PAGE_TEXTS) is not None - or _find_text(ocr_image(frame), _LOGGED_IN_MENU_TEXTS) is not None + or _find_text(items_full, _LOGGED_IN_MENU_TEXTS) is not None ): return sig = _frame_signature(frame) From 8fd6ae13e85fc127bc1abc8c213ab829182e267a Mon Sep 17 00:00:00 2001 From: AthenaHibou <13084046220@163.com> Date: Tue, 1 Sep 2026 18:54:46 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(okww):=20=E9=95=BF=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E6=9C=9F=E8=AF=8A=E6=96=AD=20OCR=20=E8=90=BD=E7=9B=98=E8=8A=82?= =?UTF-8?q?=E6=B5=81=EF=BC=8C=E9=81=BF=E5=85=8D=E6=9B=B4=E6=96=B0=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E6=97=B6=E8=AF=8A=E6=96=AD=E6=96=87=E4=BB=B6=E8=86=A8?= =?UTF-8?q?=E8=83=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/task/Okww/tools/account_switch.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/task/Okww/tools/account_switch.py b/app/task/Okww/tools/account_switch.py index 2af961f69..d06213022 100644 --- a/app/task/Okww/tools/account_switch.py +++ b/app/task/Okww/tools/account_switch.py @@ -93,6 +93,9 @@ def _write_diagnostic(text: str) -> None: _IN_GAME_STALL_SECONDS = 60.0 # 游戏内更新/加载等待的轮询间隔(比窗口等待更宽松,降低长等待期 OCR 负载)。 _IN_GAME_POLL_INTERVAL = 3.0 +# 长等待期诊断 OCR 的落盘节流:更新可能耗时数十分钟,若每个轮询都全量写诊断文件, +# 单次切换会累积数千行;改为每 N 次轮询(≈ N*3s)写一次,既能保留过渡帧采样又不膨胀。 +_DIAGNOSTIC_DUMP_EVERY_POLLS = 10 # 截图基准分辨率(16:9),OCR 与点击均在此坐标空间计算后再映射回真实窗口 _FRAME_WIDTH = 1920 @@ -450,7 +453,8 @@ def _wait_for_actionable_state( while time.monotonic() < deadline: frame = _capture_window(hwnd, activate=False) items_full = ocr_image(frame) - _dump_ocr_items(items_full) + if iter_count % _DIAGNOSTIC_DUMP_EVERY_POLLS == 0: + _dump_ocr_items(items_full) if ( _find_text(ocr_image(frame, _LOGIN_ROI), _LOGIN_PAGE_TEXTS) is not None or _find_text(items_full, _LOGGED_IN_MENU_TEXTS) is not None