pi-lean-dimension 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
BrowserBridge — base class for Python browser automation backends.
|
|
3
3
|
|
|
4
4
|
Provides JSON-RPC command routing, session lifecycle, element caching,
|
|
5
|
-
and a ``run()`` main loop. Subclasses override ``create_browser_session()
|
|
6
|
-
and
|
|
7
|
-
(e.g. Chromium via Playwright, Camoufox, etc.).
|
|
5
|
+
and a ``run()`` main loop. Subclasses override ``create_browser_session()``,
|
|
6
|
+
``create_browser_context()``, and the ``do_*`` operation methods to implement
|
|
7
|
+
specific browser backends (e.g. Chromium via Playwright, Camoufox, etc.).
|
|
8
8
|
|
|
9
9
|
Protocol
|
|
10
10
|
--------
|
|
@@ -21,11 +21,28 @@ Lifecycle
|
|
|
21
21
|
4. Subsequent commands use the existing session.
|
|
22
22
|
5. ``browser.cleanup`` closes the session for a taskId.
|
|
23
23
|
6. ``browser.shutdown`` (sent via cleanupAll) terminates the process.
|
|
24
|
+
|
|
25
|
+
Subclass contract (all ``@abstractmethod``)
|
|
26
|
+
--------------------------------------------
|
|
27
|
+
* ``create_browser_session(task_id, config)`` — build a per-task session dict.
|
|
28
|
+
* ``create_browser_context(config)`` — build an isolated BrowserContext.
|
|
29
|
+
* ``do_navigate / do_snapshot / do_click / do_type / do_scroll / do_go_back
|
|
30
|
+
/ do_press / do_screenshot / do_get_console_messages / do_clear_console
|
|
31
|
+
/ do_evaluate / do_get_cookies / do_add_cookies / do_clear_cookies
|
|
32
|
+
/ do_get_storage_state`` — the 15 operation methods. Each returns a
|
|
33
|
+
dict whose shape is documented at the dispatch site (``_h_*`` handlers).
|
|
34
|
+
``do_cleanup`` has a concrete default (calls ``close_browser_session``)
|
|
35
|
+
and need not be overridden.
|
|
36
|
+
|
|
37
|
+
Named profiles are fully handled by the TypeScript side via
|
|
38
|
+
``core/shared/storage-state.ts`` (disk persistence). The Python
|
|
39
|
+
bridge receives ``storageState`` in the navigate request and applies
|
|
40
|
+
it when creating a new BrowserContext — it does NOT track shared
|
|
41
|
+
contexts across tasks.
|
|
24
42
|
"""
|
|
25
43
|
|
|
26
|
-
import inspect
|
|
27
|
-
import sys
|
|
28
44
|
import traceback
|
|
45
|
+
from abc import ABC, abstractmethod
|
|
29
46
|
from typing import Any, Optional
|
|
30
47
|
|
|
31
48
|
from .transport import (
|
|
@@ -35,10 +52,8 @@ from .transport import (
|
|
|
35
52
|
make_error_response,
|
|
36
53
|
make_parse_error,
|
|
37
54
|
make_invalid_request,
|
|
38
|
-
make_internal_error,
|
|
39
55
|
make_application_error,
|
|
40
56
|
InvalidRequestError,
|
|
41
|
-
APPLICATION_ERROR,
|
|
42
57
|
METHOD_NOT_FOUND,
|
|
43
58
|
INVALID_PARAMS,
|
|
44
59
|
SESSION_ERROR,
|
|
@@ -51,23 +66,11 @@ DEFAULT_NAVIGATION_TIMEOUT_MS: int = 30_000
|
|
|
51
66
|
DEFAULT_INTERACTION_TIMEOUT_MS: int = 10_000
|
|
52
67
|
|
|
53
68
|
|
|
54
|
-
class BrowserBridge:
|
|
69
|
+
class BrowserBridge(ABC):
|
|
55
70
|
"""Base class for Python browser automation bridges.
|
|
56
71
|
|
|
57
|
-
Subclass this and override
|
|
58
|
-
|
|
59
|
-
* ``create_browser_session(task_id, config)`` — mandatory
|
|
60
|
-
* ``create_browser_context(config)`` — mandatory (creates isolated context per task)
|
|
61
|
-
* Any operation methods you want to customise (optional)
|
|
62
|
-
* ``close_browser_session(task_id)`` — optional (default unregisters)
|
|
63
|
-
|
|
64
|
-
Call ``run()`` to start the JSON-RPC command loop.
|
|
65
|
-
|
|
66
|
-
Named profiles are fully handled by the TypeScript side via
|
|
67
|
-
``core/shared/storage-state.ts`` (disk persistence). The Python
|
|
68
|
-
bridge receives ``storageState`` in the navigate request and applies
|
|
69
|
-
it when creating a new BrowserContext — it does NOT track shared
|
|
70
|
-
contexts across tasks.
|
|
72
|
+
Subclass this and override the abstract methods listed in the module
|
|
73
|
+
docstring. Call ``run()`` to start the JSON-RPC command loop.
|
|
71
74
|
"""
|
|
72
75
|
|
|
73
76
|
# ── Session storage ─────────────────────────────────────────
|
|
@@ -83,25 +86,40 @@ class BrowserBridge:
|
|
|
83
86
|
#: Whether the bridge is still running.
|
|
84
87
|
_running: bool
|
|
85
88
|
|
|
89
|
+
#: Plugin configuration dict forwarded from the TypeScript adapter via
|
|
90
|
+
#: the ``browser.init`` RPC. Defaults to ``{}`` for bridges that never
|
|
91
|
+
#: receive an init call (e.g. older adapters, or the shipped
|
|
92
|
+
#: ``chromium-py``/``firefox-py`` when run standalone). Subclasses read
|
|
93
|
+
#: engine-specific options from ``self.plugin_config.get("launch", {})``.
|
|
94
|
+
_plugin_config: dict[str, Any]
|
|
95
|
+
|
|
86
96
|
def __init__(self) -> None:
|
|
87
97
|
self.sessions = {}
|
|
88
98
|
self.element_caches = {}
|
|
89
99
|
self._running = False
|
|
100
|
+
self._plugin_config = {}
|
|
101
|
+
|
|
102
|
+
# ── Plugin config (forwarded via browser.init) ───────────────
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def plugin_config(self) -> dict[str, Any]:
|
|
106
|
+
"""Return the plugin config dict forwarded from the TypeScript adapter.
|
|
107
|
+
|
|
108
|
+
Populated by the ``browser.init`` RPC handler. Always returns a
|
|
109
|
+
dict (empty when no init was received) so subclasses can safely
|
|
110
|
+
call ``self.plugin_config.get("launch", {})``.
|
|
111
|
+
"""
|
|
112
|
+
return self._plugin_config
|
|
90
113
|
|
|
91
114
|
# ── Subclass hooks ──────────────────────────────────────────
|
|
92
115
|
|
|
116
|
+
@abstractmethod
|
|
93
117
|
def create_browser_session(self, task_id: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
94
118
|
"""Create a new browser session for the given task.
|
|
95
119
|
|
|
96
120
|
Must return a dict that will be stored in ``self.sessions[task_id]``.
|
|
97
121
|
The dict is backend-specific (e.g. containing a Playwright page/context).
|
|
98
|
-
|
|
99
|
-
Raises:
|
|
100
|
-
RuntimeError: if a session cannot be created.
|
|
101
122
|
"""
|
|
102
|
-
raise NotImplementedError(
|
|
103
|
-
f"{type(self).__name__} must implement create_browser_session()"
|
|
104
|
-
)
|
|
105
123
|
|
|
106
124
|
def close_browser_session(self, task_id: str) -> None:
|
|
107
125
|
"""Close and clean up the session for the given task.
|
|
@@ -112,6 +130,7 @@ class BrowserBridge:
|
|
|
112
130
|
self.sessions.pop(task_id, None)
|
|
113
131
|
self.element_caches.pop(task_id, None)
|
|
114
132
|
|
|
133
|
+
@abstractmethod
|
|
115
134
|
def create_browser_context(self, config: dict[str, Any]) -> Any:
|
|
116
135
|
"""Create a new isolated BrowserContext for a task session.
|
|
117
136
|
|
|
@@ -119,20 +138,7 @@ class BrowserBridge:
|
|
|
119
138
|
tasks. Named profiles are handled by the TypeScript side via
|
|
120
139
|
``core/shared/storage-state.ts`` — the ``config`` may contain
|
|
121
140
|
``storageState`` to restore cookies and localStorage.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
config: Configuration dict (may contain ``storageState``,
|
|
125
|
-
``viewport``, ``userAgent``, etc.)
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
A backend-specific BrowserContext object.
|
|
129
|
-
|
|
130
|
-
Raises:
|
|
131
|
-
NotImplementedError: if the subclass doesn't implement this.
|
|
132
141
|
"""
|
|
133
|
-
raise NotImplementedError(
|
|
134
|
-
f"{type(self).__name__} must implement create_browser_context()"
|
|
135
|
-
)
|
|
136
142
|
|
|
137
143
|
# ── Session helpers ─────────────────────────────────────────
|
|
138
144
|
|
|
@@ -159,17 +165,6 @@ class BrowserBridge:
|
|
|
159
165
|
self.sessions[task_id] = new_session
|
|
160
166
|
return new_session
|
|
161
167
|
|
|
162
|
-
# ── Page session setup hook ──────────────────────────────────
|
|
163
|
-
|
|
164
|
-
def _setup_page_session(self, page: Any) -> dict[str, Any]:
|
|
165
|
-
"""Set up event handlers (console capture, dialog dismissal) on a new page.
|
|
166
|
-
|
|
167
|
-
Base implementation returns a minimal session dict. Subclasses
|
|
168
|
-
(e.g. ChromiumPyBridge) override this to attach console-message
|
|
169
|
-
accumulators and dialog handlers.
|
|
170
|
-
"""
|
|
171
|
-
return {"page": page}
|
|
172
|
-
|
|
173
168
|
def get_element_cache(self, task_id: str) -> Optional[AriaParseResult]:
|
|
174
169
|
"""Get the cached element parse result for a task, or None."""
|
|
175
170
|
return self.element_caches.get(task_id)
|
|
@@ -179,7 +174,13 @@ class BrowserBridge:
|
|
|
179
174
|
self.element_caches[task_id] = result
|
|
180
175
|
|
|
181
176
|
# ── Operation stubs (override in subclasses) ────────────────
|
|
177
|
+
#
|
|
178
|
+
# The 15 ``do_*`` methods below are the operation contract. Each
|
|
179
|
+
# returns a dict whose shape is enforced at the dispatch site
|
|
180
|
+
# (``_h_*`` handlers in ``_DISPATCH``). ``do_cleanup`` has a concrete
|
|
181
|
+
# default and is not abstract.
|
|
182
182
|
|
|
183
|
+
@abstractmethod
|
|
183
184
|
def do_navigate(
|
|
184
185
|
self,
|
|
185
186
|
task_id: str,
|
|
@@ -189,160 +190,63 @@ class BrowserBridge:
|
|
|
189
190
|
profileName: Optional[str] = None,
|
|
190
191
|
profileMode: Optional[str] = None,
|
|
191
192
|
) -> dict[str, Any]:
|
|
192
|
-
"""Navigate the browser to a URL.
|
|
193
|
-
|
|
194
|
-
Subclasses interpret the extra params:
|
|
195
|
-
|
|
196
|
-
- ``storageState``: Playwright storage state for session restoration.
|
|
197
|
-
- ``profileName``: Named profile name (e.g. "work", "shopping").
|
|
198
|
-
- ``profileMode``: ``"none"`` / ``"session"`` / ``"named"``.
|
|
199
|
-
All modes create task-isolated BrowserContexts; named profiles
|
|
200
|
-
are handled by the TypeScript side via ``storageState``
|
|
201
|
-
(``core/shared/storage-state.ts``, disk persistence).
|
|
202
|
-
|
|
203
|
-
Must return a dict with keys:
|
|
204
|
-
success (bool), url (str), title (str),
|
|
205
|
-
snapshot (str), elementCount (int)
|
|
206
|
-
Optionally: botDetected (bool), profileName (str)
|
|
207
|
-
"""
|
|
208
|
-
raise NotImplementedError(
|
|
209
|
-
f"{type(self).__name__} must implement do_navigate()"
|
|
210
|
-
)
|
|
193
|
+
"""Navigate the browser to a URL."""
|
|
211
194
|
|
|
195
|
+
@abstractmethod
|
|
212
196
|
def do_snapshot(self, task_id: str) -> dict[str, Any]:
|
|
213
|
-
"""Take an accessibility snapshot of the current page.
|
|
214
|
-
|
|
215
|
-
Must return a dict with keys:
|
|
216
|
-
success (bool), snapshot (str), elementCount (int)
|
|
217
|
-
"""
|
|
218
|
-
raise NotImplementedError(
|
|
219
|
-
f"{type(self).__name__} must implement do_snapshot()"
|
|
220
|
-
)
|
|
197
|
+
"""Take an accessibility snapshot of the current page."""
|
|
221
198
|
|
|
199
|
+
@abstractmethod
|
|
222
200
|
def do_click(self, task_id: str, ref: str) -> dict[str, Any]:
|
|
223
|
-
"""Click an element by @e ref.
|
|
224
|
-
|
|
225
|
-
Must return a dict with keys:
|
|
226
|
-
success (bool)
|
|
227
|
-
Optionally: snapshot (str), elementCount (int), newUrl (str), newTitle (str)
|
|
228
|
-
"""
|
|
229
|
-
raise NotImplementedError(
|
|
230
|
-
f"{type(self).__name__} must implement do_click()"
|
|
231
|
-
)
|
|
201
|
+
"""Click an element by @e ref."""
|
|
232
202
|
|
|
203
|
+
@abstractmethod
|
|
233
204
|
def do_type(self, task_id: str, ref: str, text: str) -> dict[str, Any]:
|
|
234
|
-
"""Type text into an element by @e ref.
|
|
235
|
-
|
|
236
|
-
Must return a dict with keys:
|
|
237
|
-
success (bool)
|
|
238
|
-
Optionally: snapshot (str), elementCount (int), newUrl (str), newTitle (str)
|
|
239
|
-
"""
|
|
240
|
-
raise NotImplementedError(
|
|
241
|
-
f"{type(self).__name__} must implement do_type()"
|
|
242
|
-
)
|
|
205
|
+
"""Type text into an element by @e ref."""
|
|
243
206
|
|
|
207
|
+
@abstractmethod
|
|
244
208
|
def do_scroll(self, task_id: str, direction: str) -> dict[str, Any]:
|
|
245
|
-
"""Scroll the page up or down.
|
|
246
|
-
|
|
247
|
-
Must return a dict with keys:
|
|
248
|
-
success (bool)
|
|
249
|
-
Optionally: snapshot (str), elementCount (int), newUrl (str), newTitle (str)
|
|
250
|
-
"""
|
|
251
|
-
raise NotImplementedError(
|
|
252
|
-
f"{type(self).__name__} must implement do_scroll()"
|
|
253
|
-
)
|
|
209
|
+
"""Scroll the page up or down."""
|
|
254
210
|
|
|
211
|
+
@abstractmethod
|
|
255
212
|
def do_go_back(self, task_id: str) -> dict[str, Any]:
|
|
256
|
-
"""Navigate back in history.
|
|
257
|
-
|
|
258
|
-
Must return a dict with keys:
|
|
259
|
-
success (bool)
|
|
260
|
-
Optionally: snapshot (str), elementCount (int), newUrl (str), newTitle (str)
|
|
261
|
-
"""
|
|
262
|
-
raise NotImplementedError(
|
|
263
|
-
f"{type(self).__name__} must implement do_go_back()"
|
|
264
|
-
)
|
|
213
|
+
"""Navigate back in history."""
|
|
265
214
|
|
|
215
|
+
@abstractmethod
|
|
266
216
|
def do_press(self, task_id: str, key: str) -> dict[str, Any]:
|
|
267
|
-
"""Press a keyboard key on the current page (or focused element).
|
|
217
|
+
"""Press a keyboard key on the current page (or focused element)."""
|
|
268
218
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"""
|
|
273
|
-
raise NotImplementedError(
|
|
274
|
-
f"{type(self).__name__} must implement do_press()"
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
def do_screenshot(
|
|
278
|
-
self,
|
|
279
|
-
task_id: str,
|
|
280
|
-
full_page: bool = False,
|
|
281
|
-
) -> dict[str, Any]:
|
|
282
|
-
"""Take a screenshot of the current page.
|
|
283
|
-
|
|
284
|
-
Must return a dict with keys:
|
|
285
|
-
success (bool), dataUri (str) — JPEG base64 data URI
|
|
286
|
-
"""
|
|
287
|
-
raise NotImplementedError(
|
|
288
|
-
f"{type(self).__name__} must implement do_screenshot()"
|
|
289
|
-
)
|
|
219
|
+
@abstractmethod
|
|
220
|
+
def do_screenshot(self, task_id: str, full_page: bool = False) -> dict[str, Any]:
|
|
221
|
+
"""Take a screenshot of the current page (JPEG base64 data URI)."""
|
|
290
222
|
|
|
223
|
+
@abstractmethod
|
|
291
224
|
def do_get_console_messages(self, task_id: str) -> dict[str, Any]:
|
|
292
|
-
"""Get captured console messages.
|
|
293
|
-
|
|
294
|
-
Must return a dict with keys:
|
|
295
|
-
success (bool), messages (list) — each with type, text
|
|
296
|
-
"""
|
|
297
|
-
raise NotImplementedError(
|
|
298
|
-
f"{type(self).__name__} must implement do_get_console_messages()"
|
|
299
|
-
)
|
|
225
|
+
"""Get captured console messages."""
|
|
300
226
|
|
|
227
|
+
@abstractmethod
|
|
301
228
|
def do_clear_console(self, task_id: str) -> dict[str, Any]:
|
|
302
|
-
"""Clear captured console messages.
|
|
229
|
+
"""Clear captured console messages."""
|
|
303
230
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
def do_evaluate(self, task_id: str, expression: str) -> dict[str, Any]:
|
|
312
|
-
"""Evaluate JavaScript in the page.
|
|
313
|
-
|
|
314
|
-
Must return a dict with keys:
|
|
315
|
-
success (bool)
|
|
316
|
-
Optionally: result (any)
|
|
317
|
-
"""
|
|
318
|
-
raise NotImplementedError(
|
|
319
|
-
f"{type(self).__name__} must implement do_evaluate()"
|
|
320
|
-
)
|
|
231
|
+
@abstractmethod
|
|
232
|
+
def do_evaluate(
|
|
233
|
+
self, task_id: str, expression: str, *, read_only: bool = False
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
"""Evaluate JavaScript in the page."""
|
|
321
236
|
|
|
237
|
+
@abstractmethod
|
|
322
238
|
def do_get_cookies(
|
|
323
239
|
self, task_id: str, urls: Optional[list[str]] = None
|
|
324
240
|
) -> dict[str, Any]:
|
|
325
|
-
"""Get all cookies, optionally filtered by URL.
|
|
326
|
-
|
|
327
|
-
Must return a dict with keys:
|
|
328
|
-
success (bool), cookies (list) — each with name, value, domain, ...
|
|
329
|
-
"""
|
|
330
|
-
raise NotImplementedError(
|
|
331
|
-
f"{type(self).__name__} must implement do_get_cookies()"
|
|
332
|
-
)
|
|
241
|
+
"""Get all cookies, optionally filtered by URL."""
|
|
333
242
|
|
|
243
|
+
@abstractmethod
|
|
334
244
|
def do_add_cookies(
|
|
335
245
|
self, task_id: str, cookies: list[dict[str, Any]]
|
|
336
246
|
) -> dict[str, Any]:
|
|
337
|
-
"""Add cookies to the browser context.
|
|
338
|
-
|
|
339
|
-
Must return a dict with keys:
|
|
340
|
-
success (bool)
|
|
341
|
-
"""
|
|
342
|
-
raise NotImplementedError(
|
|
343
|
-
f"{type(self).__name__} must implement do_add_cookies()"
|
|
344
|
-
)
|
|
247
|
+
"""Add cookies to the browser context."""
|
|
345
248
|
|
|
249
|
+
@abstractmethod
|
|
346
250
|
def do_clear_cookies(
|
|
347
251
|
self,
|
|
348
252
|
task_id: str,
|
|
@@ -350,38 +254,176 @@ class BrowserBridge:
|
|
|
350
254
|
domain: Optional[str] = None,
|
|
351
255
|
path: Optional[str] = None,
|
|
352
256
|
) -> dict[str, Any]:
|
|
353
|
-
"""Clear cookies, optionally filtered by name/domain/path.
|
|
354
|
-
|
|
355
|
-
Must return a dict with keys:
|
|
356
|
-
success (bool)
|
|
357
|
-
"""
|
|
358
|
-
raise NotImplementedError(
|
|
359
|
-
f"{type(self).__name__} must implement do_clear_cookies()"
|
|
360
|
-
)
|
|
257
|
+
"""Clear cookies, optionally filtered by name/domain/path."""
|
|
361
258
|
|
|
259
|
+
@abstractmethod
|
|
362
260
|
def do_get_storage_state(self, task_id: str) -> dict[str, Any]:
|
|
363
|
-
"""Get full storage state (cookies + localStorage + IndexedDB).
|
|
364
|
-
|
|
365
|
-
Must return a dict with keys:
|
|
366
|
-
success (bool), cookies (list), origins (list)
|
|
367
|
-
"""
|
|
368
|
-
raise NotImplementedError(
|
|
369
|
-
f"{type(self).__name__} must implement do_get_storage_state()"
|
|
370
|
-
)
|
|
261
|
+
"""Get full storage state (cookies + localStorage + IndexedDB)."""
|
|
371
262
|
|
|
372
263
|
def do_cleanup(self, task_id: str) -> dict[str, Any]:
|
|
373
264
|
"""Clean up resources for a specific task.
|
|
374
265
|
|
|
375
266
|
Profile persistence is handled by the TypeScript side
|
|
376
267
|
(``python-adapter.ts`` auto-saves storage state before calling
|
|
377
|
-
cleanup), so this
|
|
378
|
-
|
|
379
|
-
Must return a dict with keys:
|
|
380
|
-
success (bool)
|
|
268
|
+
cleanup), so this default always calls ``close_browser_session()``.
|
|
381
269
|
"""
|
|
382
270
|
self.close_browser_session(task_id)
|
|
383
271
|
return {"success": True}
|
|
384
272
|
|
|
273
|
+
# ── Command handlers ────────────────────────────────────────
|
|
274
|
+
#
|
|
275
|
+
# One ``_h_*`` method per JSON-RPC method. Each extracts params and
|
|
276
|
+
# calls the matching ``do_*`` method. ``_DISPATCH`` (class attribute,
|
|
277
|
+
# built at the bottom of the class body) maps method name → handler.
|
|
278
|
+
|
|
279
|
+
def _h_ping(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
280
|
+
return make_success_response(cmd_id, "pong")
|
|
281
|
+
|
|
282
|
+
def _h_init(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
283
|
+
# Forward plugin config from the TypeScript adapter. Sent exactly
|
|
284
|
+
# once after the ping handshake, before any other RPC.
|
|
285
|
+
self._plugin_config = params.get("config") or {}
|
|
286
|
+
return make_success_response(cmd_id, {"ok": True})
|
|
287
|
+
|
|
288
|
+
def _h_shutdown(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
289
|
+
self._running = False
|
|
290
|
+
return make_success_response(cmd_id, "shutting_down")
|
|
291
|
+
|
|
292
|
+
def _h_navigate(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
293
|
+
url = self._require_param(params, "url", str)
|
|
294
|
+
task_id = self._require_param(params, "taskId", str)
|
|
295
|
+
timeout_ms = params.get("timeoutMs", DEFAULT_NAVIGATION_TIMEOUT_MS)
|
|
296
|
+
result = self.do_navigate(
|
|
297
|
+
task_id, url, timeout_ms,
|
|
298
|
+
storageState=params.get("storageState"),
|
|
299
|
+
profileName=params.get("profileName"),
|
|
300
|
+
profileMode=params.get("profileMode"),
|
|
301
|
+
)
|
|
302
|
+
return make_success_response(cmd_id, result)
|
|
303
|
+
|
|
304
|
+
def _h_snapshot(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
305
|
+
task_id = self._require_param(params, "taskId", str)
|
|
306
|
+
return make_success_response(cmd_id, self.do_snapshot(task_id))
|
|
307
|
+
|
|
308
|
+
def _h_click(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
309
|
+
task_id = self._require_param(params, "taskId", str)
|
|
310
|
+
ref = self._require_param(params, "ref", str)
|
|
311
|
+
return make_success_response(cmd_id, self.do_click(task_id, ref))
|
|
312
|
+
|
|
313
|
+
def _h_type(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
314
|
+
task_id = self._require_param(params, "taskId", str)
|
|
315
|
+
ref = self._require_param(params, "ref", str)
|
|
316
|
+
text = self._require_param(params, "text", str)
|
|
317
|
+
return make_success_response(cmd_id, self.do_type(task_id, ref, text))
|
|
318
|
+
|
|
319
|
+
def _h_scroll(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
320
|
+
task_id = self._require_param(params, "taskId", str)
|
|
321
|
+
direction = self._require_param(params, "direction", str)
|
|
322
|
+
if direction not in ("up", "down"):
|
|
323
|
+
return make_error_response(
|
|
324
|
+
cmd_id, INVALID_PARAMS,
|
|
325
|
+
'direction must be "up" or "down"',
|
|
326
|
+
)
|
|
327
|
+
return make_success_response(cmd_id, self.do_scroll(task_id, direction))
|
|
328
|
+
|
|
329
|
+
def _h_go_back(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
330
|
+
task_id = self._require_param(params, "taskId", str)
|
|
331
|
+
return make_success_response(cmd_id, self.do_go_back(task_id))
|
|
332
|
+
|
|
333
|
+
def _h_press(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
334
|
+
task_id = self._require_param(params, "taskId", str)
|
|
335
|
+
key = self._require_param(params, "key", str)
|
|
336
|
+
return make_success_response(cmd_id, self.do_press(task_id, key))
|
|
337
|
+
|
|
338
|
+
def _h_screenshot(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
339
|
+
task_id = self._require_param(params, "taskId", str)
|
|
340
|
+
full_page = params.get("fullPage", False)
|
|
341
|
+
return make_success_response(cmd_id, self.do_screenshot(task_id, full_page))
|
|
342
|
+
|
|
343
|
+
def _h_get_console_messages(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
344
|
+
task_id = self._require_param(params, "taskId", str)
|
|
345
|
+
return make_success_response(cmd_id, self.do_get_console_messages(task_id))
|
|
346
|
+
|
|
347
|
+
def _h_clear_console(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
348
|
+
task_id = self._require_param(params, "taskId", str)
|
|
349
|
+
return make_success_response(cmd_id, self.do_clear_console(task_id))
|
|
350
|
+
|
|
351
|
+
def _h_evaluate(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
352
|
+
task_id = self._require_param(params, "taskId", str)
|
|
353
|
+
expression = self._require_param(params, "expression", str)
|
|
354
|
+
read_only = bool(params.get("readOnly", False))
|
|
355
|
+
result = self.do_evaluate(task_id, expression, read_only=read_only)
|
|
356
|
+
return make_success_response(cmd_id, result)
|
|
357
|
+
|
|
358
|
+
def _h_get_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
359
|
+
task_id = self._require_param(params, "taskId", str)
|
|
360
|
+
return make_success_response(cmd_id, self.do_get_cookies(task_id, params.get("urls")))
|
|
361
|
+
|
|
362
|
+
def _h_add_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
363
|
+
task_id = self._require_param(params, "taskId", str)
|
|
364
|
+
cookies = self._require_param(params, "cookies", list)
|
|
365
|
+
return make_success_response(cmd_id, self.do_add_cookies(task_id, cookies))
|
|
366
|
+
|
|
367
|
+
def _h_clear_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
368
|
+
task_id = self._require_param(params, "taskId", str)
|
|
369
|
+
# No required params beyond taskId — empty call clears ALL cookies
|
|
370
|
+
result = self.do_clear_cookies(
|
|
371
|
+
task_id, params.get("name"), params.get("domain"), params.get("path")
|
|
372
|
+
)
|
|
373
|
+
return make_success_response(cmd_id, result)
|
|
374
|
+
|
|
375
|
+
def _h_get_storage_state(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
376
|
+
task_id = self._require_param(params, "taskId", str)
|
|
377
|
+
return make_success_response(cmd_id, self.do_get_storage_state(task_id))
|
|
378
|
+
|
|
379
|
+
def _h_cleanup(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
380
|
+
task_id = self._require_param(params, "taskId", str)
|
|
381
|
+
return make_success_response(cmd_id, self.do_cleanup(task_id))
|
|
382
|
+
|
|
383
|
+
def _h_describe_quirks(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
384
|
+
# Return the bridge's declared quirks flags. Uses getattr with
|
|
385
|
+
# defaults so a bare BrowserBridge (no Playwright quirks) returns
|
|
386
|
+
# all-defaults rather than raising AttributeError.
|
|
387
|
+
return make_success_response(cmd_id, {
|
|
388
|
+
"fingerprint_managed_context": getattr(
|
|
389
|
+
self, "_fingerprint_managed_context", False
|
|
390
|
+
),
|
|
391
|
+
"eval_prefix": getattr(self, "_eval_prefix", ""),
|
|
392
|
+
"scroll_via_wheel": getattr(self, "_scroll_via_wheel", False),
|
|
393
|
+
"skip_default_viewport": getattr(self, "_skip_default_viewport", False),
|
|
394
|
+
"skip_networkidle": getattr(self, "_skip_networkidle", False),
|
|
395
|
+
"wrap_mw_eval_in_eval": getattr(self, "_wrap_mw_eval_in_eval", False),
|
|
396
|
+
"csp_safe_readonly_via_init_script": getattr(
|
|
397
|
+
self, "_csp_safe_readonly_via_init_script", False
|
|
398
|
+
),
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
#: JSON-RPC method name → handler. Built after the handlers are
|
|
402
|
+
#: defined so the names are in scope. ``handle_command`` looks up
|
|
403
|
+
#: here and calls ``handler(self, params, cmd_id)``.
|
|
404
|
+
_DISPATCH = {
|
|
405
|
+
"ping": _h_ping,
|
|
406
|
+
"browser.init": _h_init,
|
|
407
|
+
"shutdown": _h_shutdown,
|
|
408
|
+
"browser.navigate": _h_navigate,
|
|
409
|
+
"browser.snapshot": _h_snapshot,
|
|
410
|
+
"browser.click": _h_click,
|
|
411
|
+
"browser.type": _h_type,
|
|
412
|
+
"browser.scroll": _h_scroll,
|
|
413
|
+
"browser.goBack": _h_go_back,
|
|
414
|
+
"browser.press": _h_press,
|
|
415
|
+
"browser.screenshot": _h_screenshot,
|
|
416
|
+
"browser.getConsoleMessages": _h_get_console_messages,
|
|
417
|
+
"browser.clearConsole": _h_clear_console,
|
|
418
|
+
"browser.evaluate": _h_evaluate,
|
|
419
|
+
"browser.getCookies": _h_get_cookies,
|
|
420
|
+
"browser.addCookies": _h_add_cookies,
|
|
421
|
+
"browser.clearCookies": _h_clear_cookies,
|
|
422
|
+
"browser.getStorageState": _h_get_storage_state,
|
|
423
|
+
"browser.cleanup": _h_cleanup,
|
|
424
|
+
"browser.describeQuirks": _h_describe_quirks,
|
|
425
|
+
}
|
|
426
|
+
|
|
385
427
|
# ── Command routing ─────────────────────────────────────────
|
|
386
428
|
|
|
387
429
|
def handle_command(self, method: str, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
@@ -390,127 +432,12 @@ class BrowserBridge:
|
|
|
390
432
|
Returns a JSON-RPC response dict (either result or error).
|
|
391
433
|
"""
|
|
392
434
|
try:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
self._running = False
|
|
398
|
-
return make_success_response(cmd_id, "shutting_down")
|
|
399
|
-
|
|
400
|
-
if method == "browser.navigate":
|
|
401
|
-
url = self._require_param(params, "url", str, cmd_id)
|
|
402
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
403
|
-
timeout_ms = params.get("timeoutMs", DEFAULT_NAVIGATION_TIMEOUT_MS)
|
|
404
|
-
storage_state = params.get("storageState")
|
|
405
|
-
profile_name = params.get("profileName")
|
|
406
|
-
profile_mode = params.get("profileMode")
|
|
407
|
-
result = self.do_navigate(
|
|
408
|
-
task_id, url, timeout_ms,
|
|
409
|
-
storageState=storage_state,
|
|
410
|
-
profileName=profile_name,
|
|
411
|
-
profileMode=profile_mode,
|
|
435
|
+
handler = self._DISPATCH.get(method)
|
|
436
|
+
if handler is None:
|
|
437
|
+
return make_error_response(
|
|
438
|
+
cmd_id, METHOD_NOT_FOUND, f"Method not found: {method}"
|
|
412
439
|
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if method == "browser.snapshot":
|
|
416
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
417
|
-
result = self.do_snapshot(task_id)
|
|
418
|
-
return make_success_response(cmd_id, result)
|
|
419
|
-
|
|
420
|
-
if method == "browser.click":
|
|
421
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
422
|
-
ref = self._require_param(params, "ref", str, cmd_id)
|
|
423
|
-
result = self.do_click(task_id, ref)
|
|
424
|
-
return make_success_response(cmd_id, result)
|
|
425
|
-
|
|
426
|
-
if method == "browser.type":
|
|
427
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
428
|
-
ref = self._require_param(params, "ref", str, cmd_id)
|
|
429
|
-
text = self._require_param(params, "text", str, cmd_id)
|
|
430
|
-
result = self.do_type(task_id, ref, text)
|
|
431
|
-
return make_success_response(cmd_id, result)
|
|
432
|
-
|
|
433
|
-
if method == "browser.scroll":
|
|
434
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
435
|
-
direction = self._require_param(params, "direction", str, cmd_id)
|
|
436
|
-
if direction not in ("up", "down"):
|
|
437
|
-
return make_error_response(
|
|
438
|
-
cmd_id, INVALID_PARAMS,
|
|
439
|
-
'direction must be "up" or "down"',
|
|
440
|
-
)
|
|
441
|
-
result = self.do_scroll(task_id, direction)
|
|
442
|
-
return make_success_response(cmd_id, result)
|
|
443
|
-
|
|
444
|
-
if method == "browser.goBack":
|
|
445
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
446
|
-
result = self.do_go_back(task_id)
|
|
447
|
-
return make_success_response(cmd_id, result)
|
|
448
|
-
|
|
449
|
-
if method == "browser.press":
|
|
450
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
451
|
-
key = self._require_param(params, "key", str, cmd_id)
|
|
452
|
-
result = self.do_press(task_id, key)
|
|
453
|
-
return make_success_response(cmd_id, result)
|
|
454
|
-
|
|
455
|
-
if method == "browser.screenshot":
|
|
456
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
457
|
-
full_page = params.get("fullPage", False)
|
|
458
|
-
result = self.do_screenshot(task_id, full_page)
|
|
459
|
-
return make_success_response(cmd_id, result)
|
|
460
|
-
|
|
461
|
-
if method == "browser.getConsoleMessages":
|
|
462
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
463
|
-
result = self.do_get_console_messages(task_id)
|
|
464
|
-
return make_success_response(cmd_id, result)
|
|
465
|
-
|
|
466
|
-
if method == "browser.clearConsole":
|
|
467
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
468
|
-
result = self.do_clear_console(task_id)
|
|
469
|
-
return make_success_response(cmd_id, result)
|
|
470
|
-
|
|
471
|
-
if method == "browser.evaluate":
|
|
472
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
473
|
-
expression = self._require_param(params, "expression", str, cmd_id)
|
|
474
|
-
result = self.do_evaluate(task_id, expression)
|
|
475
|
-
return make_success_response(cmd_id, result)
|
|
476
|
-
|
|
477
|
-
if method == "browser.getCookies":
|
|
478
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
479
|
-
urls = params.get("urls")
|
|
480
|
-
result = self.do_get_cookies(task_id, urls)
|
|
481
|
-
return make_success_response(cmd_id, result)
|
|
482
|
-
|
|
483
|
-
if method == "browser.addCookies":
|
|
484
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
485
|
-
cookies = self._require_param(params, "cookies", list, cmd_id)
|
|
486
|
-
result = self.do_add_cookies(task_id, cookies)
|
|
487
|
-
return make_success_response(cmd_id, result)
|
|
488
|
-
|
|
489
|
-
if method == "browser.clearCookies":
|
|
490
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
491
|
-
name = params.get("name")
|
|
492
|
-
domain = params.get("domain")
|
|
493
|
-
path = params.get("path")
|
|
494
|
-
# No required params beyond taskId — empty call clears ALL cookies
|
|
495
|
-
result = self.do_clear_cookies(task_id, name, domain, path)
|
|
496
|
-
return make_success_response(cmd_id, result)
|
|
497
|
-
|
|
498
|
-
if method == "browser.getStorageState":
|
|
499
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
500
|
-
result = self.do_get_storage_state(task_id)
|
|
501
|
-
return make_success_response(cmd_id, result)
|
|
502
|
-
|
|
503
|
-
if method == "browser.cleanup":
|
|
504
|
-
task_id = self._require_param(params, "taskId", str, cmd_id)
|
|
505
|
-
result = self.do_cleanup(task_id)
|
|
506
|
-
return make_success_response(cmd_id, result)
|
|
507
|
-
|
|
508
|
-
# Unknown method
|
|
509
|
-
return make_error_response(
|
|
510
|
-
cmd_id,
|
|
511
|
-
METHOD_NOT_FOUND,
|
|
512
|
-
f"Method not found: {method}",
|
|
513
|
-
)
|
|
440
|
+
return handler(self, params, cmd_id)
|
|
514
441
|
|
|
515
442
|
except SessionNotFoundError as exc:
|
|
516
443
|
return make_error_response(cmd_id, SESSION_ERROR, str(exc))
|
|
@@ -570,12 +497,12 @@ class BrowserBridge:
|
|
|
570
497
|
params: dict[str, Any],
|
|
571
498
|
key: str,
|
|
572
499
|
expected_type: type,
|
|
573
|
-
cmd_id: Any,
|
|
574
500
|
) -> Any:
|
|
575
501
|
"""Require a param to exist and be of the expected type.
|
|
576
502
|
|
|
577
|
-
Raises
|
|
578
|
-
|
|
503
|
+
Raises ``InvalidParamsError`` if the param is missing or has the
|
|
504
|
+
wrong type. The caller's ``except Exception`` clause turns this
|
|
505
|
+
into a JSON-RPC application-error response.
|
|
579
506
|
"""
|
|
580
507
|
if key not in params:
|
|
581
508
|
raise InvalidParamsError(f'Missing required parameter: "{key}"')
|