diff --git a/module/game/cloud.py b/module/game/cloud.py index 39368bf7b..d74625460 100644 --- a/module/game/cloud.py +++ b/module/game/cloud.py @@ -43,6 +43,24 @@ class CloudGameController(GameControllerBase): BROWSER_TAG = "--march-7th-assistant-sr-cloud-game" # 自定义浏览器参数作为标识,用于识别哪些浏览器进程属于三月七小助手 BROWSER_INSTALL_PATH = os.path.join(os.getcwd(), "3rdparty", "WebBrowser") # 浏览器安装路径 INTEGRATED_BROWSER_VERSION = "140.0.7339.207" # 浏览器版本 + DISABLE_POINTER_LOCK_SCRIPT = """ + (() => { + const blocked = function () { + return Promise.reject(new DOMException( + 'Pointer Lock is disabled in background mode.', + 'NotAllowedError' + )); + }; + Object.defineProperty(Element.prototype, 'requestPointerLock', { + configurable: true, + writable: true, + value: blocked, + }); + if (document.pointerLockElement && document.exitPointerLock) { + document.exitPointerLock(); + } + })(); + """ @staticmethod def _get_platform_dir() -> str: @@ -165,6 +183,20 @@ def _confirm_viewport_resolution(self) -> None: "mobile": False }) + def _configure_pointer_lock(self, headless: bool) -> None: + """无窗口运行时禁止网页锁定系统鼠标指针。""" + if not headless or not self.driver: + return + + try: + self.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { + "source": self.DISABLE_POINTER_LOCK_SCRIPT, + "runImmediately": True, + }) + self.log_debug("无窗口模式已禁用 Pointer Lock") + except Exception as e: + self.log_warning(f"无窗口模式禁用 Pointer Lock 失败: {e}") + def _prepare_browser_and_driver(self, browser_type: str, integrated: bool) -> tuple[str, str]: self.user_profile_path = os.path.join(self.BROWSER_INSTALL_PATH, "UserProfile", self.cfg.browser_type.capitalize()) # 判断环境变量 MARCH7TH_BROWSER_PATH 和 MARCH7TH_DRIVER_PATH,同时存在时优先使用 @@ -340,6 +372,7 @@ def _connect_or_create_browser(self, headless=False) -> None: try: options.debugger_address = f"127.0.0.1:{reconnect_port}" self.driver = webdriver_type(service=service, options=options) + self._configure_pointer_lock(headless) self.log_info("已连接到现有浏览器") return except Exception: @@ -402,6 +435,7 @@ def _connect_or_create_browser(self, headless=False) -> None: self.log_error(f"浏览器启动失败: {e}") raise RuntimeError("浏览器启动失败") + self._configure_pointer_lock(headless) if not self.cfg.cloud_game_fullscreen_enable: self.driver.set_window_size(1920, 1120) if first_run or not self.cfg.browser_persistent_enable: diff --git a/tests/test_module/test_cloud_game.py b/tests/test_module/test_cloud_game.py new file mode 100644 index 000000000..bb50bfe57 --- /dev/null +++ b/tests/test_module/test_cloud_game.py @@ -0,0 +1,51 @@ +import unittest + +from module.game.cloud import CloudGameController + + +class FakeDriver: + def __init__(self): + self.commands = [] + + def execute_cdp_cmd(self, command, params): + self.commands.append((command, params)) + + +class FakeLogger: + def __init__(self): + self.warnings = [] + + def debug(self, _message): + pass + + def warning(self, message): + self.warnings.append(message) + + +class TestCloudGamePointerLock(unittest.TestCase): + def create_controller(self): + controller = object.__new__(CloudGameController) + controller.driver = FakeDriver() + controller.logger = FakeLogger() + return controller + + def test_pointer_lock_is_denied_in_headless_mode(self): + controller = self.create_controller() + + controller._configure_pointer_lock(headless=True) + + self.assertEqual( + controller.driver.commands[0][0], + "Page.addScriptToEvaluateOnNewDocument", + ) + params = controller.driver.commands[0][1] + self.assertTrue(params["runImmediately"]) + self.assertIn("requestPointerLock", params["source"]) + self.assertIn("document.exitPointerLock", params["source"]) + + def test_pointer_lock_is_unchanged_in_visible_mode(self): + controller = self.create_controller() + + controller._configure_pointer_lock(headless=False) + + self.assertEqual(controller.driver.commands, [])