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
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Invisible-Py Bridge — Python-side stealth backend using invisible_playwright.
|
|
4
|
+
|
|
5
|
+
**This is a user-installed backend — it does NOT ship in the npm tarball.**
|
|
6
|
+
To install, copy this file and its ``.venv/`` to::
|
|
7
|
+
|
|
8
|
+
~/.pi/agent/pi-lean-portal/user-backends/invisible-py/bridge.py
|
|
9
|
+
|
|
10
|
+
Then create a virtual environment, install dependencies, fetch the binary,
|
|
11
|
+
and add the plugin entry to your ``settings.json``.
|
|
12
|
+
|
|
13
|
+
Lifecycle pattern: engine owns its own Playwright instance
|
|
14
|
+
-----------------------------------------------------------
|
|
15
|
+
Unlike some stealth backends (notably shipped Camoufox) which accept an
|
|
16
|
+
external Playwright instance and only override ``_launch_browser``, this
|
|
17
|
+
bridge uses **lifecycle pattern #2**: the engine (``InvisiblePlaywright``)
|
|
18
|
+
starts its own Playwright instance inside ``__enter__()`` and owns it.
|
|
19
|
+
The base class normally calls ``_ensure_playwright()`` → ``sync_playwright().start()``
|
|
20
|
+
→ ``_launch_browser()``. To avoid two Playwright instances, this bridge
|
|
21
|
+
overrides both ``_ensure_playwright`` and ``_maybe_stop_playwright`` to
|
|
22
|
+
delegate to the ``InvisiblePlaywright`` context manager.
|
|
23
|
+
|
|
24
|
+
For the same reason, ``_launch_browser()`` is NOT overridden — the base's
|
|
25
|
+
``_launch_browser`` is never called because ``_ensure_playwright`` completely
|
|
26
|
+
replaces the launch path.
|
|
27
|
+
|
|
28
|
+
This pattern is the **unique probe** for the architecture's ability to
|
|
29
|
+
handle backends that manage their own Playwright lifecycle.
|
|
30
|
+
|
|
31
|
+
Quirks rationale
|
|
32
|
+
-----------------
|
|
33
|
+
``_fingerprint_managed_context = True``
|
|
34
|
+
The base's ``create_browser_context()`` unconditionally sets
|
|
35
|
+
``viewport: {1280, 720}`` and ``user_agent: effective_user_agent``.
|
|
36
|
+
The patched Firefox binary generates viewport, user_agent, screen, dpr
|
|
37
|
+
from the fingerprint with "user wins" semantics. Without this flag,
|
|
38
|
+
the base's hard-coded values would override the fingerprint — producing
|
|
39
|
+
a detectable mismatch.
|
|
40
|
+
|
|
41
|
+
``_capture_user_agent = False``
|
|
42
|
+
``_capture_ua()`` probes the UA via a throwaway ``self._browser.new_page()``,
|
|
43
|
+
which creates an implicit BrowserContext that is never closed. On the
|
|
44
|
+
patched Firefox binary, that leftover context reliably deadlocks the
|
|
45
|
+
Juggler driver when a subsequent BrowserContext's ``new_page()`` is
|
|
46
|
+
created — the second ``browser.navigate`` for a different task hangs
|
|
47
|
+
forever inside the Playwright sync greenlet. UA capture is also
|
|
48
|
+
pointless here: with ``_fingerprint_managed_context = True``,
|
|
49
|
+
``create_browser_context()`` does NOT pass ``user_agent`` to
|
|
50
|
+
``new_context`` (the fingerprint package sets it), so
|
|
51
|
+
``effective_user_agent`` is never read.
|
|
52
|
+
|
|
53
|
+
``_skip_networkidle = True``
|
|
54
|
+
The patched Firefox binary does not fire ``networkidle`` reliably.
|
|
55
|
+
Waiting for it in ``do_go_back`` / ``_wait_for_page_ready`` either
|
|
56
|
+
times out (30s) or loiters in the Playwright sync greenlet long enough
|
|
57
|
+
to risk deadlocking the Juggler driver when a subsequent BrowserContext
|
|
58
|
+
is created. This flag makes navigation settle use ``load`` instead of
|
|
59
|
+
``networkidle``, matching ``do_navigate``'s load-based settle.
|
|
60
|
+
|
|
61
|
+
Back navigation (``/web back``)
|
|
62
|
+
--------------------------------
|
|
63
|
+
The patched Firefox binary does not support back/forward navigation:
|
|
64
|
+
``page.go_back()`` times out for every ``wait_until`` value and the URL
|
|
65
|
+
never changes. This bridge's ``do_go_back`` implements a
|
|
66
|
+
``document.referrer`` workaround: re-navigate to the referrer URL via
|
|
67
|
+
``page.goto``. This covers the dominant agent pattern (click a link →
|
|
68
|
+
wrong way → go back). When ``document.referrer`` is empty (direct
|
|
69
|
+
navigations, ``rel=noreferrer`` links), the bridge returns ``success: false``
|
|
70
|
+
with a clear error — it is an honest limitation, not a silent no-op.
|
|
71
|
+
|
|
72
|
+
Requires
|
|
73
|
+
--------
|
|
74
|
+
* Python >= 3.10
|
|
75
|
+
* ``invisible-playwright`` installed (``pip install invisible-playwright``)
|
|
76
|
+
* Fetched patched Firefox binary (``python -m invisible_playwright fetch``)
|
|
77
|
+
* ``xvfb`` system package on Linux for ``headless=True``
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
import json
|
|
81
|
+
import os
|
|
82
|
+
import re
|
|
83
|
+
import sys
|
|
84
|
+
import time
|
|
85
|
+
from typing import Any
|
|
86
|
+
|
|
87
|
+
from pi_browser_bridge.playwright_base import PlaywrightBridge
|
|
88
|
+
from pi_browser_bridge.bridge import SessionNotFoundError
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InvisiblePyBridge(PlaywrightBridge):
|
|
92
|
+
"""Stealth bridge using invisible_playwright (Firefox-based).
|
|
93
|
+
|
|
94
|
+
Uses lifecycle pattern #2: overrides ``_ensure_playwright`` and
|
|
95
|
+
``_maybe_stop_playwright`` to delegate to an ``InvisiblePlaywright``
|
|
96
|
+
context manager, which owns its own Playwright instance and patches
|
|
97
|
+
``browser.new_context`` for fingerprint coherence.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
_plugin_name: str = "invisible-py"
|
|
101
|
+
_fingerprint_managed_context: bool = True
|
|
102
|
+
# invisible_playwright's patched Firefox binary routes page.evaluate
|
|
103
|
+
# through eval() in the page's main world (a stealth measure that kills
|
|
104
|
+
# Juggler's isolated-world debugger signature). The page's CSP then
|
|
105
|
+
# applies, so on CSP-strict sites (e.g. Reddit, which forbids
|
|
106
|
+
# unsafe-eval) every page.evaluate fails with "call to eval() blocked
|
|
107
|
+
# by CSP". This is invisible_playwright-specific — Camoufox keeps
|
|
108
|
+
# Juggler's CSP-free isolated-world + MainWorldContext.executeInGlobal
|
|
109
|
+
# paths, so camoufox-py read-only evals bypass CSP and need no quirk.
|
|
110
|
+
# The flag makes do_evaluate(read_only=True) read the EXTRACTOR_SCRIPT
|
|
111
|
+
# result from a <meta> tag that an add_init_script (isolated world,
|
|
112
|
+
# CSP-free) stashes at DOMContentLoaded. See the base quirk docstring.
|
|
113
|
+
_csp_safe_readonly_via_init_script: bool = True
|
|
114
|
+
# invisible_playwright's patched Firefox binary also routes write evals
|
|
115
|
+
# like window.scrollBy through eval() in the page's main world, hitting
|
|
116
|
+
# CSP on strict sites. page.mouse.wheel drives scroll via input events
|
|
117
|
+
# instead — CSP-free. Same fix as camoufox-py.
|
|
118
|
+
_scroll_via_wheel: bool = True
|
|
119
|
+
# The patched Firefox binary doesn't fire `networkidle` reliably; waiting for
|
|
120
|
+
# it in `do_go_back` / `_wait_for_page_ready` either times out (30s) or
|
|
121
|
+
# loiters in the Playwright sync greenlet long enough to deadlock the
|
|
122
|
+
# Juggler driver when a subsequent BrowserContext's `new_page()` is
|
|
123
|
+
# created. Match `do_navigate`'s load-based settle instead.
|
|
124
|
+
_skip_networkidle: bool = True
|
|
125
|
+
# NOTE: UA capture is intentionally DISABLED.
|
|
126
|
+
# `_capture_ua()` probes the UA via a throwaway `self._browser.new_page()`,
|
|
127
|
+
# which creates an implicit BrowserContext that is never closed. On the
|
|
128
|
+
# patched Firefox binary, that leftover context reliably deadlocks the
|
|
129
|
+
# Juggler driver when a subsequent (second) BrowserContext's `new_page()`
|
|
130
|
+
# is created — the second `browser.navigate` for a different task hangs
|
|
131
|
+
# forever inside the Playwright sync greenlet.
|
|
132
|
+
# It is also pointless here: with `_fingerprint_managed_context = True`,
|
|
133
|
+
# `create_browser_context()` does NOT pass `user_agent` to `new_context`
|
|
134
|
+
# (the fingerprint package sets it), so `effective_user_agent` is never
|
|
135
|
+
# read. The base's `_user_agent` fallback is similarly unused.
|
|
136
|
+
_capture_user_agent: bool = False
|
|
137
|
+
_install_hint: str = (
|
|
138
|
+
"invisible-playwright browser not installed.\n"
|
|
139
|
+
"Run the following commands in your invisible-py virtual environment:\n"
|
|
140
|
+
" pip install invisible-playwright\n"
|
|
141
|
+
" python -m invisible_playwright fetch\n"
|
|
142
|
+
"On Linux, also install xvfb:\n"
|
|
143
|
+
" apt-get install xvfb"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
#: The ``InvisiblePlaywright`` context manager instance, held for the
|
|
147
|
+
#: bridge's lifetime. ``None`` before the first call to
|
|
148
|
+
#: ``_ensure_playwright()``.
|
|
149
|
+
_stealth_ctx = None
|
|
150
|
+
|
|
151
|
+
def _ensure_playwright(self): # type: ignore[override]
|
|
152
|
+
"""Override base lifecycle to delegate to InvisiblePlaywright.
|
|
153
|
+
|
|
154
|
+
Instead of calling ``sync_playwright().start()`` +
|
|
155
|
+
``_launch_browser()``, we instantiate ``InvisiblePlaywright`` from
|
|
156
|
+
the plugin config and enter the context manager, which returns a
|
|
157
|
+
patched Playwright Browser. The Playwright handle is read from
|
|
158
|
+
``self._stealth_ctx._pw``.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
``(pw, browser)`` — the Playwright instance and Browser.
|
|
162
|
+
"""
|
|
163
|
+
if self._pw is None:
|
|
164
|
+
# Lazy import invisible_playwright — gives better error messages
|
|
165
|
+
# and avoids import cost when this bridge is disabled.
|
|
166
|
+
try:
|
|
167
|
+
from invisible_playwright import InvisiblePlaywright # type: ignore[import-unresolved]
|
|
168
|
+
except ImportError as exc:
|
|
169
|
+
raise RuntimeError(self._install_hint) from exc
|
|
170
|
+
|
|
171
|
+
# Read launch options from browser.init plugin config
|
|
172
|
+
launch = self.plugin_config.get("launch", {}) or {}
|
|
173
|
+
|
|
174
|
+
# Map camelCase keys from TypeScript settings to snake_case
|
|
175
|
+
# Python kwargs. Only forward keys that
|
|
176
|
+
# invisible_playwright.InvisiblePlaywright accepts.
|
|
177
|
+
kwargs: dict[str, object] = {
|
|
178
|
+
# Default to headless=True for server agents (no X display needed).
|
|
179
|
+
# invisible_playwright's ``headless=True`` uses Xvfb to hide the
|
|
180
|
+
# window while keeping the real rendering pipeline (coherent
|
|
181
|
+
# fingerprint). Users can override via ``headless=False`` for
|
|
182
|
+
# local headed debugging or ``headless='virtual'`` for the
|
|
183
|
+
# PyVirtualDisplay-based approach.
|
|
184
|
+
"headless": launch.get("headless", True),
|
|
185
|
+
}
|
|
186
|
+
_KEY_MAP: dict[str, str] = {
|
|
187
|
+
"seed": "seed",
|
|
188
|
+
"proxy": "proxy",
|
|
189
|
+
"humanize": "humanize",
|
|
190
|
+
"locale": "locale",
|
|
191
|
+
"timezone": "timezone",
|
|
192
|
+
"extraPrefs": "extra_prefs",
|
|
193
|
+
"binaryPath": "binary_path",
|
|
194
|
+
"prepRecaptcha": "prep_recaptcha",
|
|
195
|
+
"pin": "pin",
|
|
196
|
+
}
|
|
197
|
+
for ts_key, py_key in _KEY_MAP.items():
|
|
198
|
+
if ts_key in launch:
|
|
199
|
+
kwargs[py_key] = launch[ts_key]
|
|
200
|
+
|
|
201
|
+
self._stealth_ctx = InvisiblePlaywright(**kwargs)
|
|
202
|
+
try:
|
|
203
|
+
# __enter__() returns a Browser (not a BrowserContext when
|
|
204
|
+
# profile_dir is omitted — which is what we want).
|
|
205
|
+
self._browser = self._stealth_ctx.__enter__()
|
|
206
|
+
except Exception as _exc:
|
|
207
|
+
if re.search(
|
|
208
|
+
r"Executable doesn't exist|browserType\.launch",
|
|
209
|
+
str(_exc),
|
|
210
|
+
re.IGNORECASE,
|
|
211
|
+
):
|
|
212
|
+
raise RuntimeError(self._install_hint) from _exc
|
|
213
|
+
raise
|
|
214
|
+
|
|
215
|
+
# Read the Playwright handle from the InvisiblePlaywright instance.
|
|
216
|
+
# InvisiblePlaywright owns sync_playwright(); the Browser it
|
|
217
|
+
# returns does not expose _pw.
|
|
218
|
+
self._pw = self._stealth_ctx._pw
|
|
219
|
+
|
|
220
|
+
# Probe UA at first launch
|
|
221
|
+
if self._capture_user_agent:
|
|
222
|
+
self._capture_ua()
|
|
223
|
+
return self._pw, self._browser
|
|
224
|
+
|
|
225
|
+
def create_browser_context(self, config: dict[str, Any]) -> Any:
|
|
226
|
+
"""Override base context creation to neutralize invisible_playwright's
|
|
227
|
+
injected ``new_context`` defaults.
|
|
228
|
+
|
|
229
|
+
Why the base's ``_skip_default_viewport`` flag is NOT enough:
|
|
230
|
+
invisible_playwright (lifecycle pattern #2) patches
|
|
231
|
+
``browser.new_context`` to merge in ``viewport`` + ``screen`` +
|
|
232
|
+
``device_scale_factor`` defaults derived from the spoofed fingerprint
|
|
233
|
+
— unlike Camoufox, which injects the fingerprint at browser *launch*
|
|
234
|
+
via ``NewBrowser`` and leaves ``new_context`` vanilla. The base's
|
|
235
|
+
``_skip_default_viewport`` path passes only ``no_viewport=True``, which
|
|
236
|
+
works for Camoufox (vanilla ``new_context``) but fails here because the
|
|
237
|
+
patched ``new_context`` injects ``viewport`` under a *different* key, so
|
|
238
|
+
``no_viewport`` cannot override it — Playwright still issues
|
|
239
|
+
``Browser.setDefaultViewport`` and the patched Firefox binary rejects
|
|
240
|
+
the ``screenSize`` field it includes.
|
|
241
|
+
|
|
242
|
+
Verified empirically against the real patched binary, the only
|
|
243
|
+
combination that works is to override ALL THREE injected defaults to
|
|
244
|
+
``None`` AND pass ``no_viewport=True``:
|
|
245
|
+
* ``viewport=None`` → Playwright sees ``viewport === null`` → sets
|
|
246
|
+
``noDefaultViewport=True`` → the ``setDefaultViewport`` CDP call is
|
|
247
|
+
skipped entirely (its guard is ``if (!viewport) return``).
|
|
248
|
+
* ``device_scale_factor=None`` → clears the injected default that
|
|
249
|
+
otherwise triggers Playwright's "deviceScaleFactor not supported
|
|
250
|
+
with null viewport" validation.
|
|
251
|
+
* ``screen=None`` → clears the remaining injected default.
|
|
252
|
+
|
|
253
|
+
The fingerprint still manages viewport/screen/dpr *inside* the browser
|
|
254
|
+
process (the patched binary spoofs screen at the window level); we are
|
|
255
|
+
only stopping Playwright from re-sending them via the CDP call the
|
|
256
|
+
binary cannot handle — functionally the same as Camoufox's
|
|
257
|
+
``no_viewport=True`` path, just requiring the extra None overrides
|
|
258
|
+
because of the ``new_context``-time injection.
|
|
259
|
+
"""
|
|
260
|
+
_pw, browser = self._ensure_playwright()
|
|
261
|
+
|
|
262
|
+
context_kwargs: dict[str, Any] = {
|
|
263
|
+
"no_viewport": True,
|
|
264
|
+
# Override the three defaults invisible_playwright's patched
|
|
265
|
+
# new_context injects — each must be None, or no_viewport is
|
|
266
|
+
# either ignored (viewport) or rejected (device_scale_factor).
|
|
267
|
+
"viewport": None,
|
|
268
|
+
"screen": None,
|
|
269
|
+
"device_scale_factor": None,
|
|
270
|
+
}
|
|
271
|
+
storage_state = config.get("storageState")
|
|
272
|
+
if storage_state is not None:
|
|
273
|
+
context_kwargs["storage_state"] = storage_state
|
|
274
|
+
|
|
275
|
+
context = browser.new_context(**context_kwargs)
|
|
276
|
+
|
|
277
|
+
# Start Playwright trace capture if BROWSER_TRACE_DIR is set.
|
|
278
|
+
_trace_dir = os.environ.get("BROWSER_TRACE_DIR")
|
|
279
|
+
if _trace_dir:
|
|
280
|
+
try:
|
|
281
|
+
context.tracing.start(
|
|
282
|
+
screenshots=True,
|
|
283
|
+
snapshots=True,
|
|
284
|
+
sources=True,
|
|
285
|
+
)
|
|
286
|
+
self._log("tracing", taskId=config.get("_task_id", "shared"),
|
|
287
|
+
action="start", dir=_trace_dir)
|
|
288
|
+
except Exception:
|
|
289
|
+
pass # Best-effort
|
|
290
|
+
|
|
291
|
+
# CSP-safe read-only eval path — register the EXTRACTOR_SCRIPT as an
|
|
292
|
+
# init script (isolated world, CSP-free) that stashes its result in a
|
|
293
|
+
# <meta> tag at DOMContentLoaded. See _csp_safe_readonly_via_init_script.
|
|
294
|
+
self._register_readonly_extractor_init_script(context)
|
|
295
|
+
|
|
296
|
+
return context
|
|
297
|
+
|
|
298
|
+
def _maybe_stop_playwright(self) -> None:
|
|
299
|
+
"""Stop the InvisiblePlaywright when no sessions remain.
|
|
300
|
+
|
|
301
|
+
Calls ``self._stealth_ctx.__exit__(None, None, None)`` instead of
|
|
302
|
+
``self._pw.stop()`` so the context manager properly cleans up its
|
|
303
|
+
own Playwright instance and the patched Firefox binary.
|
|
304
|
+
"""
|
|
305
|
+
if not self.sessions and self._stealth_ctx is not None:
|
|
306
|
+
try:
|
|
307
|
+
if self._browser:
|
|
308
|
+
self._browser.close()
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
try:
|
|
312
|
+
self._stealth_ctx.__exit__(None, None, None)
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
self._pw = None
|
|
316
|
+
self._browser = None
|
|
317
|
+
self._stealth_ctx = None
|
|
318
|
+
|
|
319
|
+
def do_go_back(self, task_id: str) -> dict[str, Any]:
|
|
320
|
+
"""Navigate back — workaround for the patched Firefox binary's broken
|
|
321
|
+
history navigation.
|
|
322
|
+
|
|
323
|
+
The patched Firefox binary does not support back/forward navigation:
|
|
324
|
+
``page.go_back()`` times out for every ``wait_until`` value
|
|
325
|
+
(``networkidle`` / ``load`` / ``domcontentloaded`` / ``commit`` /
|
|
326
|
+
default) and the URL never changes. Standard Playwright Firefox
|
|
327
|
+
(the ``firefox-py`` backend) works in 0.01s — so this is a
|
|
328
|
+
patched-binary limitation, not a Playwright or bridge bug. Root
|
|
329
|
+
cause is the binary's ``disallowBFCache`` stealth attribute combined
|
|
330
|
+
with Fission and the about:newtab race.
|
|
331
|
+
|
|
332
|
+
Workaround: re-navigate to ``document.referrer`` via ``page.goto``.
|
|
333
|
+
This covers the dominant agent pattern (click a link → wrong way →
|
|
334
|
+
go back), where ``document.referrer`` is populated. It's a re-fetch
|
|
335
|
+
rather than a true history back, but the patched binary has bfcache
|
|
336
|
+
disabled anyway, so a true history back would re-render from scratch
|
|
337
|
+
too — functionally identical.
|
|
338
|
+
|
|
339
|
+
Falls through with ``success: false`` when ``document.referrer``
|
|
340
|
+
is empty (direct navigations, ``rel=noreferrer`` links, POST
|
|
341
|
+
submissions, multi-step back). This is an honest limitation — the
|
|
342
|
+
error message is explicit about what's not supported, so agents can
|
|
343
|
+
react appropriately.
|
|
344
|
+
"""
|
|
345
|
+
_t_start = time.time()
|
|
346
|
+
try:
|
|
347
|
+
page = self._get_page(task_id)
|
|
348
|
+
except SessionNotFoundError:
|
|
349
|
+
raise
|
|
350
|
+
except Exception as exc:
|
|
351
|
+
self._log("goBack", taskId=task_id, success=False,
|
|
352
|
+
time=round((time.time() - _t_start) * 1000))
|
|
353
|
+
return {"success": False, "error": f"GoBack failed: {exc}"}
|
|
354
|
+
|
|
355
|
+
# Try the referrer workaround first.
|
|
356
|
+
referrer: str = ""
|
|
357
|
+
try:
|
|
358
|
+
referrer = page.evaluate("() => document.referrer") or ""
|
|
359
|
+
except Exception:
|
|
360
|
+
referrer = ""
|
|
361
|
+
|
|
362
|
+
if not referrer:
|
|
363
|
+
# No referrer → return an immediate error instead of timing out
|
|
364
|
+
# via ``super().do_go_back()`` (which calls ``page.go_back()`` —
|
|
365
|
+
# broken on this patched binary, always times out).
|
|
366
|
+
self._log("goBack", taskId=task_id, success=False,
|
|
367
|
+
reason="no referrer (fallback not supported on this binary)",
|
|
368
|
+
time=round((time.time() - _t_start) * 1000))
|
|
369
|
+
return {
|
|
370
|
+
"success": False,
|
|
371
|
+
"error": (
|
|
372
|
+
"GoBack not supported for direct/noreferrer navigations "
|
|
373
|
+
"on this patched Firefox binary (history navigation is "
|
|
374
|
+
"non-functional)"
|
|
375
|
+
),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
page.goto(referrer, wait_until="load", timeout=15_000)
|
|
380
|
+
|
|
381
|
+
new_url: str = page.url
|
|
382
|
+
new_title: str = page.title()
|
|
383
|
+
snap_text, element_count, elements = self._take_snapshot_and_cache(
|
|
384
|
+
task_id, page
|
|
385
|
+
)
|
|
386
|
+
self._log("goBack", taskId=task_id, success=True,
|
|
387
|
+
elementCount=element_count, via="referrer",
|
|
388
|
+
time=round((time.time() - _t_start) * 1000))
|
|
389
|
+
return {
|
|
390
|
+
"success": True,
|
|
391
|
+
"snapshot": snap_text,
|
|
392
|
+
"elementCount": element_count,
|
|
393
|
+
"elements": elements,
|
|
394
|
+
"dialogEvents": self._get_dialog_events(task_id),
|
|
395
|
+
"newUrl": new_url,
|
|
396
|
+
"newTitle": new_title,
|
|
397
|
+
}
|
|
398
|
+
except Exception as exc:
|
|
399
|
+
self._log("goBack", taskId=task_id, success=False, via="referrer",
|
|
400
|
+
time=round((time.time() - _t_start) * 1000))
|
|
401
|
+
return {"success": False, "error": f"GoBack failed: {exc}"}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
405
|
+
# Entry point
|
|
406
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
# The stealth engine bundles its own Playwright, so we check for the
|
|
410
|
+
# engine itself rather than the standard playwright package.
|
|
411
|
+
# Print a JSON-RPC error and exit immediately so the TypeScript
|
|
412
|
+
# PythonPluginAdapter gets a parseable error.
|
|
413
|
+
try:
|
|
414
|
+
import invisible_playwright # type: ignore[import-unresolved] # noqa: F401
|
|
415
|
+
except ImportError:
|
|
416
|
+
print(
|
|
417
|
+
json.dumps({
|
|
418
|
+
"jsonrpc": "2.0",
|
|
419
|
+
"id": None,
|
|
420
|
+
"error": {
|
|
421
|
+
"code": -32000,
|
|
422
|
+
"message": (
|
|
423
|
+
"invisible-playwright is not installed.\n"
|
|
424
|
+
"Run: pip install invisible-playwright && "
|
|
425
|
+
"python -m invisible_playwright fetch"
|
|
426
|
+
),
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
)
|
|
430
|
+
sys.stdout.flush()
|
|
431
|
+
sys.exit(1)
|
|
432
|
+
|
|
433
|
+
bridge = InvisiblePyBridge()
|
|
434
|
+
bridge.run()
|
|
@@ -21,18 +21,8 @@ import type { AriaCachedNode } from "./shared/accessibility-tree.js";
|
|
|
21
21
|
* screenshot when unsupported).
|
|
22
22
|
*/
|
|
23
23
|
export interface PluginCapabilities {
|
|
24
|
-
/** Can take full-page screenshots */
|
|
25
24
|
supportsFullPageScreenshot: boolean;
|
|
26
|
-
/** Can capture console messages via CDP or equivalent */
|
|
27
|
-
supportsConsoleCapture: boolean;
|
|
28
|
-
/** Can evaluate arbitrary JavaScript in the page */
|
|
29
25
|
supportsJavaScriptEvaluate: boolean;
|
|
30
|
-
/** Can detect bot/anti-automation signals (Cloudflare, CAPTCHA, etc.) */
|
|
31
|
-
supportsBotDetection: boolean;
|
|
32
|
-
/** Can auto-dismiss JS dialogs (alert/confirm/prompt) */
|
|
33
|
-
supportsDialogAutoDismissal: boolean;
|
|
34
|
-
/** Can accept an AbortSignal for long-running navigations */
|
|
35
|
-
supportsAbortSignal: boolean;
|
|
36
26
|
/** Browser engine (used for display/debugging only) */
|
|
37
27
|
engine: "chromium" | "firefox" | "webkit" | string;
|
|
38
28
|
}
|
|
@@ -40,11 +30,7 @@ export interface PluginCapabilities {
|
|
|
40
30
|
/** Default capabilities matching a full-featured Chromium backend. */
|
|
41
31
|
export const DEFAULT_CAPABILITIES: PluginCapabilities = {
|
|
42
32
|
supportsFullPageScreenshot: true,
|
|
43
|
-
supportsConsoleCapture: true,
|
|
44
33
|
supportsJavaScriptEvaluate: true,
|
|
45
|
-
supportsBotDetection: true,
|
|
46
|
-
supportsDialogAutoDismissal: true,
|
|
47
|
-
supportsAbortSignal: true,
|
|
48
34
|
engine: "chromium",
|
|
49
35
|
};
|
|
50
36
|
|
|
@@ -59,7 +45,6 @@ export const DEFAULT_CAPABILITIES: PluginCapabilities = {
|
|
|
59
45
|
export interface DialogEvent {
|
|
60
46
|
/** Dialog type as reported by the browser */
|
|
61
47
|
type: string;
|
|
62
|
-
/** Dialog message text */
|
|
63
48
|
message: string;
|
|
64
49
|
/** How the dialog was handled (always "accepted" for auto-dismiss) */
|
|
65
50
|
handledAs: "accepted" | "dismissed";
|
|
@@ -76,13 +61,11 @@ export interface ResultBase {
|
|
|
76
61
|
error?: string;
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
/** Result from browser-navigate */
|
|
80
64
|
export interface NavigateResult extends ResultBase {
|
|
81
65
|
url: string;
|
|
82
66
|
title: string;
|
|
83
67
|
/** Accessibility-tree text with @e refs */
|
|
84
68
|
snapshot: string;
|
|
85
|
-
/** Number of interactive elements found */
|
|
86
69
|
elementCount: number;
|
|
87
70
|
/** Plugin-internal signal: page may be blocked by bot detection */
|
|
88
71
|
botDetected?: boolean;
|
|
@@ -90,17 +73,14 @@ export interface NavigateResult extends ResultBase {
|
|
|
90
73
|
dialogDetected?: boolean;
|
|
91
74
|
/** Auto-dismissed JavaScript dialogs (alert/confirm/prompt) since last navigate */
|
|
92
75
|
dialogEvents?: DialogEvent[];
|
|
93
|
-
/** The active profile mode for this session. */
|
|
94
76
|
profileMode?: "none" | "session" | "named";
|
|
95
77
|
/** Which profile was loaded for this session (if any) */
|
|
96
78
|
profileName?: string;
|
|
97
79
|
}
|
|
98
80
|
|
|
99
|
-
/** Result from browser-snapshot */
|
|
100
81
|
export interface SnapshotResult extends ResultBase {
|
|
101
82
|
/** Accessibility-tree text with @e refs */
|
|
102
83
|
snapshot: string;
|
|
103
|
-
/** Number of interactive elements found */
|
|
104
84
|
elementCount: number;
|
|
105
85
|
/** Auto-dismissed JavaScript dialogs (alert/confirm/prompt) since last snapshot */
|
|
106
86
|
dialogEvents?: DialogEvent[];
|
|
@@ -114,19 +94,16 @@ export interface InteractionResult extends ResultBase {
|
|
|
114
94
|
newTitle?: string;
|
|
115
95
|
/** Auto-captured snapshot after the interaction */
|
|
116
96
|
snapshot?: string;
|
|
117
|
-
/** Number of interactive elements in the auto-snapshot */
|
|
118
97
|
elementCount?: number;
|
|
119
98
|
/** Auto-dismissed JavaScript dialogs since the interaction */
|
|
120
99
|
dialogEvents?: DialogEvent[];
|
|
121
100
|
}
|
|
122
101
|
|
|
123
|
-
/** Result from plugin.screenshot() */
|
|
124
102
|
export interface ScreenshotResult extends ResultBase {
|
|
125
103
|
/** JPEG data URI of the screenshot */
|
|
126
104
|
dataUri: string;
|
|
127
105
|
}
|
|
128
106
|
|
|
129
|
-
/** Result from getConsoleMessages */
|
|
130
107
|
export interface ConsoleMessagesResult extends ResultBase {
|
|
131
108
|
messages: Array<{ type: string; text: string }>;
|
|
132
109
|
}
|
|
@@ -157,7 +134,6 @@ export interface Cookie {
|
|
|
157
134
|
sameSite?: "Strict" | "Lax" | "None";
|
|
158
135
|
}
|
|
159
136
|
|
|
160
|
-
/** Result from browser-getCookies */
|
|
161
137
|
export interface CookieResult extends ResultBase {
|
|
162
138
|
cookies: Cookie[];
|
|
163
139
|
}
|
|
@@ -169,7 +145,6 @@ export interface ClearCookiesOptions {
|
|
|
169
145
|
path?: string;
|
|
170
146
|
}
|
|
171
147
|
|
|
172
|
-
/** Result from browser-getStorageState */
|
|
173
148
|
export interface StorageStateResult extends ResultBase {
|
|
174
149
|
cookies: Cookie[];
|
|
175
150
|
origins: Array<{
|
|
@@ -183,7 +158,7 @@ export interface StorageStateResult extends ResultBase {
|
|
|
183
158
|
/**
|
|
184
159
|
* The contract every interactive browser backend must implement.
|
|
185
160
|
*
|
|
186
|
-
* The
|
|
161
|
+
* The 18 required operations (plus getElementCache and cookie/storage
|
|
187
162
|
* methods) make up the full contract. Lifecycle hooks are called by
|
|
188
163
|
* the framework, not the agent.
|
|
189
164
|
*/
|
|
@@ -231,9 +206,6 @@ export interface BrowserPlugin {
|
|
|
231
206
|
|
|
232
207
|
// ── Cookies & storage state ────────────────────────────────
|
|
233
208
|
|
|
234
|
-
/**
|
|
235
|
-
* Get all browser cookies for the session, optionally filtered by URL.
|
|
236
|
-
*/
|
|
237
209
|
getCookies(taskId: string, urls?: string[]): Promise<CookieResult>;
|
|
238
210
|
|
|
239
211
|
/**
|
|
@@ -282,7 +254,11 @@ export interface BrowserPlugin {
|
|
|
282
254
|
|
|
283
255
|
clearConsole(taskId: string): Promise<void>;
|
|
284
256
|
|
|
285
|
-
evaluate(
|
|
257
|
+
evaluate(
|
|
258
|
+
taskId: string,
|
|
259
|
+
expression: string,
|
|
260
|
+
readOnly?: boolean,
|
|
261
|
+
): Promise<EvaluateResult>;
|
|
286
262
|
|
|
287
263
|
// ── Element cache access ─────────────────────────────────
|
|
288
264
|
|
|
@@ -295,8 +271,5 @@ export interface BrowserPlugin {
|
|
|
295
271
|
|
|
296
272
|
// ── Per-task cleanup ──────────────────────────────────────
|
|
297
273
|
|
|
298
|
-
/**
|
|
299
|
-
* Clean up resources for a specific task (browser context, page, etc.).
|
|
300
|
-
*/
|
|
301
274
|
cleanup(taskId: string): Promise<void>;
|
|
302
275
|
}
|