From c9a111be73586bdf6fc44536f088e4db6ed86d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 18 Jun 2026 17:22:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=20CDP=20=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cdp_browser.py | 95 +++++++++++++++++++++++++++++++++++++++ tools/cdp_browser.py | 25 ++++++++--- 2 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 tests/test_cdp_browser.py diff --git a/tests/test_cdp_browser.py b/tests/test_cdp_browser.py new file mode 100644 index 0000000..66a4b3f --- /dev/null +++ b/tests/test_cdp_browser.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +from unittest.mock import AsyncMock, MagicMock + +import pytest + +import config +from tools.cdp_browser import CDPBrowserManager + + +@pytest.mark.asyncio +async def test_existing_browser_connects_directly_to_devtools_browser(monkeypatch): + monkeypatch.setattr(config, "CDP_CONNECT_EXISTING", True) + monkeypatch.setattr(config, "BROWSER_LAUNCH_TIMEOUT", 60) + + manager = CDPBrowserManager() + manager.debug_port = 9222 + manager._get_browser_websocket_url = AsyncMock( # type: ignore[method-assign] + side_effect=AssertionError("existing browser mode must not call /json/version") + ) + + browser = MagicMock() + browser.is_connected.return_value = True + browser.contexts = [] + + playwright = MagicMock() + playwright.chromium.connect_over_cdp = AsyncMock(return_value=browser) + + await manager._connect_via_cdp(playwright) + + playwright.chromium.connect_over_cdp.assert_awaited_once_with( + "ws://localhost:9222/devtools/browser", + timeout=60000, + ) + + +@pytest.mark.asyncio +async def test_existing_browser_falls_back_to_discovered_websocket_url(monkeypatch): + monkeypatch.setattr(config, "CDP_CONNECT_EXISTING", True) + monkeypatch.setattr(config, "BROWSER_LAUNCH_TIMEOUT", 60) + + manager = CDPBrowserManager() + manager.debug_port = 9222 + manager._get_browser_websocket_url = AsyncMock( # type: ignore[method-assign] + return_value="ws://localhost:9222/devtools/browser/generated-id" + ) + + browser = MagicMock() + browser.is_connected.return_value = True + browser.contexts = [] + + playwright = MagicMock() + playwright.chromium.connect_over_cdp = AsyncMock( + side_effect=[RuntimeError("direct websocket failed"), browser] + ) + + await manager._connect_via_cdp(playwright) + + manager._get_browser_websocket_url.assert_awaited_once_with(9222) + assert playwright.chromium.connect_over_cdp.await_args_list[0].args == ( + "ws://localhost:9222/devtools/browser", + ) + assert playwright.chromium.connect_over_cdp.await_args_list[0].kwargs == { + "timeout": 60000, + } + assert playwright.chromium.connect_over_cdp.await_args_list[1].args == ( + "ws://localhost:9222/devtools/browser/generated-id", + ) + assert playwright.chromium.connect_over_cdp.await_args_list[1].kwargs == { + "timeout": 60000, + } + + +@pytest.mark.asyncio +async def test_launched_browser_uses_discovered_websocket_url(monkeypatch): + monkeypatch.setattr(config, "CDP_CONNECT_EXISTING", False) + + manager = CDPBrowserManager() + manager.debug_port = 9223 + manager._get_browser_websocket_url = AsyncMock( # type: ignore[method-assign] + return_value="ws://localhost:9223/devtools/browser/generated-id" + ) + + browser = MagicMock() + browser.is_connected.return_value = True + browser.contexts = [] + + playwright = MagicMock() + playwright.chromium.connect_over_cdp = AsyncMock(return_value=browser) + + await manager._connect_via_cdp(playwright) + + manager._get_browser_websocket_url.assert_awaited_once_with(9223) + playwright.chromium.connect_over_cdp.assert_awaited_once_with( + "ws://localhost:9223/devtools/browser/generated-id" + ) diff --git a/tools/cdp_browser.py b/tools/cdp_browser.py index c6d76b9..bc5a76c 100644 --- a/tools/cdp_browser.py +++ b/tools/cdp_browser.py @@ -316,16 +316,29 @@ class CDPBrowserManager: """ try: if config.CDP_CONNECT_EXISTING: - # Existing Chrome exposes the browser websocket URL from /json/version. - # Newer Chrome versions return a UUID path instead of plain /devtools/browser. - ws_url = await self._get_browser_websocket_url(self.debug_port) + # Existing browser remote debugging in Chrome 136+ does not expose + # /json/version. Connect directly and wait for user confirmation. + ws_url = f"ws://localhost:{self.debug_port}/devtools/browser" utils.logger.info(f"[CDPBrowserManager] Connecting to existing browser via CDP: {ws_url}") utils.logger.info( "[CDPBrowserManager] Please check your browser for a confirmation dialog and accept it" ) - self.browser = await playwright.chromium.connect_over_cdp( - ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 - ) + try: + self.browser = await playwright.chromium.connect_over_cdp( + ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 + ) + except Exception as direct_error: + utils.logger.warning( + "[CDPBrowserManager] Direct existing-browser CDP connection failed: " + f"{direct_error}. Trying /json/version discovery..." + ) + ws_url = await self._get_browser_websocket_url(self.debug_port) + utils.logger.info( + f"[CDPBrowserManager] Connecting to existing browser via discovered CDP: {ws_url}" + ) + self.browser = await playwright.chromium.connect_over_cdp( + ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 + ) else: # For launched browser, get WebSocket URL first ws_url = await self._get_browser_websocket_url(self.debug_port)