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
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PythonPluginAdapter — TypeScript side of the Python bridge protocol.
|
|
3
|
-
*
|
|
4
|
-
* Implements `BrowserPlugin` by spawning a Python subprocess and
|
|
5
|
-
* communicating via JSON-RPC 2.0 over stdin/stdout with newline-delimited
|
|
6
|
-
* framing.
|
|
7
|
-
*
|
|
8
|
-
* Lifecycle
|
|
9
|
-
* ---------
|
|
10
|
-
* 1. Constructed with a plugin name and `PythonBridgeConfig`.
|
|
11
|
-
* 2. `init()` validates paths — no spawn yet (lazy start).
|
|
12
|
-
* 3. First operation triggers `ensureRunning()`, which spawns the Python
|
|
13
|
-
* process and waits for a `ping` handshake.
|
|
14
|
-
* 4. Operations send JSON-RPC requests, the Python bridge handles them.
|
|
15
|
-
* 5. `cleanupAll()` sends `shutdown`, waits for graceful exit, force-kills
|
|
16
|
-
* if unresponsive.
|
|
17
|
-
* 6. Process crashes are detected and auto-restart on the next call.
|
|
18
|
-
*
|
|
19
|
-
* Thread safety: operations are serialised through the single stdin/stdout
|
|
20
|
-
* channel. The Python bridge processes one request at a time.
|
|
21
|
-
*
|
|
22
|
-
* @module
|
|
3
|
+
* Implements BrowserPlugin via JSON-RPC 2.0 over stdin/stdout.
|
|
23
4
|
*/
|
|
24
5
|
|
|
25
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
26
7
|
import type { ChildProcess } from "node:child_process";
|
|
27
8
|
import { existsSync } from "node:fs";
|
|
9
|
+
import { join, delimiter as pathDelimiter } from "node:path";
|
|
28
10
|
|
|
29
11
|
import { sessionManager } from "../core/shared/session-manager.js";
|
|
30
|
-
import {
|
|
12
|
+
import { persistSessionState } from "../core/shared/storage-state.js";
|
|
13
|
+
import { DEFAULT_BACKENDS_ROOT } from "../core/plugin-config.js";
|
|
31
14
|
|
|
32
15
|
import {
|
|
33
16
|
DEFAULT_CAPABILITIES,
|
|
@@ -47,13 +30,12 @@ import {
|
|
|
47
30
|
type ResultBase,
|
|
48
31
|
} from "../core/plugin-api.js";
|
|
49
32
|
import type { AriaCachedNode } from "../core/shared/accessibility-tree.js";
|
|
33
|
+
import { EXTRACTOR_SCRIPT } from "../core/shared/dom-extractor.js";
|
|
50
34
|
|
|
51
35
|
// ─── Constants ─────────────────────────────────────────────────────────
|
|
52
36
|
|
|
53
|
-
/** Default timeout for the JSON-RPC transport layer (milliseconds). */
|
|
54
37
|
const DEFAULT_TRANSPORT_TIMEOUT_MS = 60_000;
|
|
55
38
|
|
|
56
|
-
/** Default timeout for the `ping` handshake on startup. */
|
|
57
39
|
const PING_TIMEOUT_MS = 10_000;
|
|
58
40
|
|
|
59
41
|
/** Grace period after sending `shutdown` before force-killing. */
|
|
@@ -107,8 +89,7 @@ export interface PythonBridgeConfig {
|
|
|
107
89
|
pythonArgs?: string[];
|
|
108
90
|
|
|
109
91
|
/**
|
|
110
|
-
* Advertised capabilities overrides. Defaults to
|
|
111
|
-
* Chromium profile (everything except AbortSignal support).
|
|
92
|
+
* Advertised capabilities overrides. Defaults to DEFAULT_CAPABILITIES.
|
|
112
93
|
*/
|
|
113
94
|
capabilities?: Partial<PluginCapabilities>;
|
|
114
95
|
|
|
@@ -122,24 +103,6 @@ export interface PythonBridgeConfig {
|
|
|
122
103
|
transportTimeoutMs?: number;
|
|
123
104
|
}
|
|
124
105
|
|
|
125
|
-
// ─── Default capabilities ─────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Default capabilities for a Python-based Chromium bridge.
|
|
129
|
-
*
|
|
130
|
-
* - Full-page screenshots: yes (Playwright supports it)
|
|
131
|
-
* - Console capture: yes (page.on("console"))
|
|
132
|
-
* - JavaScript evaluate: yes (page.evaluate)
|
|
133
|
-
* - Bot detection: yes (heuristics via checkPage logic)
|
|
134
|
-
* - Dialog auto-dismissal: yes (page.on("dialog"))
|
|
135
|
-
* - AbortSignal: no (JSON-RPC transport doesn't support it natively)
|
|
136
|
-
* - Engine: chromium
|
|
137
|
-
*/
|
|
138
|
-
const DEFAULT_PYTHON_CAPABILITIES: PluginCapabilities = {
|
|
139
|
-
...DEFAULT_CAPABILITIES,
|
|
140
|
-
supportsAbortSignal: false,
|
|
141
|
-
};
|
|
142
|
-
|
|
143
106
|
// ─── Pending request type ─────────────────────────────────────────────
|
|
144
107
|
|
|
145
108
|
interface PendingRequest {
|
|
@@ -148,6 +111,37 @@ interface PendingRequest {
|
|
|
148
111
|
timer: ReturnType<typeof setTimeout>;
|
|
149
112
|
}
|
|
150
113
|
|
|
114
|
+
// ─── Quirks Descriptor ────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Quirks flags declared by a Python bridge at runtime, read via the
|
|
118
|
+
* ``browser.describeQuirks`` introspection RPC.
|
|
119
|
+
*
|
|
120
|
+
* Each field corresponds to a class attribute on
|
|
121
|
+
* ``PlaywrightBridge`` (defaults shown). Subclasses override these
|
|
122
|
+
* to signal engine-specific behavior to the TypeScript runner, which
|
|
123
|
+
* uses ``skipIf`` to only run tests that apply to the declared quirks.
|
|
124
|
+
*
|
|
125
|
+
* Also extends ResultBase so the adapter method can use
|
|
126
|
+
* ``_rpcCallTyped`` (which requires ``{ success: boolean }``).
|
|
127
|
+
*/
|
|
128
|
+
export interface QuirksDescriptor extends ResultBase {
|
|
129
|
+
/** Bridge owns fingerprint management (viewport, UA). */
|
|
130
|
+
fingerprint_managed_context: boolean;
|
|
131
|
+
/** Prefix prepended to ``page.evaluate`` expressions (e.g. ``"mw:"``). */
|
|
132
|
+
eval_prefix: string;
|
|
133
|
+
/** ``scroll()`` uses ``page.mouse.wheel`` instead of ``window.scrollBy``. */
|
|
134
|
+
scroll_via_wheel: boolean;
|
|
135
|
+
/** ``create_browser_context`` passes ``no_viewport=True``. */
|
|
136
|
+
skip_default_viewport: boolean;
|
|
137
|
+
/** Navigation uses ``load`` instead of ``networkidle``. */
|
|
138
|
+
skip_networkidle: boolean;
|
|
139
|
+
/** ``do_evaluate`` wraps the script in ``eval(<json>)`` to survive main-world wrapper. */
|
|
140
|
+
wrap_mw_eval_in_eval: boolean;
|
|
141
|
+
/** CSP-safe read-only ``do_evaluate`` via init-script (patched Firefox stealth). */
|
|
142
|
+
csp_safe_readonly_via_init_script: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
151
145
|
// ─── PythonPluginAdapter ──────────────────────────────────────────────
|
|
152
146
|
|
|
153
147
|
/**
|
|
@@ -180,6 +174,15 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
180
174
|
private _startupPromise: Promise<void> | null = null;
|
|
181
175
|
private _started = false;
|
|
182
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Plugin config dict forwarded to the bridge via the `browser.init` RPC
|
|
179
|
+
* immediately after the ping handshake. Populated by `init()` from the
|
|
180
|
+
* user's `settings.json` `config.config` object (which carries the
|
|
181
|
+
* `launch` sub-object for stealth backends). Empty when no config was
|
|
182
|
+
* provided — the bridge defaults `_plugin_config` to `{}` either way.
|
|
183
|
+
*/
|
|
184
|
+
private _pluginInitConfig: Record<string, unknown> | undefined;
|
|
185
|
+
|
|
183
186
|
/**
|
|
184
187
|
* Local element caches per task, populated from bridge responses.
|
|
185
188
|
* Map: taskId → Map<ref, AriaCachedNode>
|
|
@@ -224,7 +227,7 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
224
227
|
|
|
225
228
|
// Merge capabilities
|
|
226
229
|
this.capabilities = {
|
|
227
|
-
...
|
|
230
|
+
...DEFAULT_CAPABILITIES,
|
|
228
231
|
...config.capabilities,
|
|
229
232
|
};
|
|
230
233
|
}
|
|
@@ -236,8 +239,17 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
236
239
|
/**
|
|
237
240
|
* Initialise the adapter. Validates that the Python path and bridge
|
|
238
241
|
* script are available, but does NOT spawn the subprocess yet.
|
|
242
|
+
*
|
|
243
|
+
* Stores the supplied plugin config dict (the user's `config.config`
|
|
244
|
+
* from `settings.json`) so it can be forwarded to the bridge via the
|
|
245
|
+
* `browser.init` RPC immediately after the ping handshake. This is the
|
|
246
|
+
* channel that carries stealth launch options (`config.launch`) to the
|
|
247
|
+
* Python-side bridge.
|
|
239
248
|
*/
|
|
240
|
-
async init(
|
|
249
|
+
async init(config?: Record<string, unknown>): Promise<void> {
|
|
250
|
+
// Stash the plugin config for the post-ping browser.init RPC.
|
|
251
|
+
this._pluginInitConfig = config;
|
|
252
|
+
|
|
241
253
|
// Validate pythonPath exists
|
|
242
254
|
const pythonOk = this._checkPythonExists();
|
|
243
255
|
if (!pythonOk) {
|
|
@@ -307,6 +319,13 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
307
319
|
env: {
|
|
308
320
|
...process.env,
|
|
309
321
|
PYTHONUNBUFFERED: "1",
|
|
322
|
+
// Phase 0b: make the shared `pi_browser_bridge` library
|
|
323
|
+
// importable from any venv the user points `pythonPath`
|
|
324
|
+
// at, without requiring a `pip install` of the bridge.
|
|
325
|
+
// Appended to any existing PYTHONPATH so user entries keep
|
|
326
|
+
// precedence; for the shipped chromium-py / firefox-py
|
|
327
|
+
// venvs the editable install points at the same source.
|
|
328
|
+
PYTHONPATH: this._buildPythonPath(),
|
|
310
329
|
},
|
|
311
330
|
},
|
|
312
331
|
);
|
|
@@ -395,17 +414,48 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
395
414
|
setImmediate(async () => {
|
|
396
415
|
try {
|
|
397
416
|
await this._directRpcCall("ping", {}, PING_TIMEOUT_MS);
|
|
417
|
+
|
|
418
|
+
// ── Forward plugin config to the bridge (Phase 0) ──
|
|
419
|
+
// Sent exactly once, immediately after the ping handshake and
|
|
420
|
+
// before any other RPC, so stealth subclasses can read launch
|
|
421
|
+
// options from `self.plugin_config.get("launch", {})` at
|
|
422
|
+
// browser-init time. A bridge too old to know this method
|
|
423
|
+
// returns METHOD_NOT_FOUND, which we surface as a clear error.
|
|
424
|
+
await this._directRpcCall(
|
|
425
|
+
"browser.init",
|
|
426
|
+
{
|
|
427
|
+
config: {
|
|
428
|
+
...(this._pluginInitConfig ?? {}),
|
|
429
|
+
// Plumbing for the CSP-safe read-only eval path (see
|
|
430
|
+
// PlaywrightBridge._csp_safe_readonly_via_init_script).
|
|
431
|
+
// Backends that don't opt into the quirk ignore this key;
|
|
432
|
+
// a stealth backend that flips the quirk registers it as
|
|
433
|
+
// an add_init_script so its EXTRACTOR_SCRIPT result can
|
|
434
|
+
// be read without page.evaluate (CSP-blocked on patched-
|
|
435
|
+
// Firefox binaries that route eval through the main
|
|
436
|
+
// world). Sent for every Python backend so any stealth
|
|
437
|
+
// backend can opt in by flipping the quirk.
|
|
438
|
+
readOnlyExtractorScript: EXTRACTOR_SCRIPT,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
PING_TIMEOUT_MS,
|
|
442
|
+
);
|
|
443
|
+
|
|
398
444
|
this._started = true;
|
|
399
445
|
resolve();
|
|
400
446
|
} catch (err: unknown) {
|
|
401
447
|
// If the process already exited, the exit handler will reject.
|
|
402
|
-
// Only reject here if the process is still alive but ping failed.
|
|
448
|
+
// Only reject here if the process is still alive but ping/init failed.
|
|
403
449
|
if (this._process && this._exitCode === null) {
|
|
404
|
-
// Ping failed — kill and reject
|
|
450
|
+
// Ping/init failed — kill and reject
|
|
405
451
|
this._killProcess();
|
|
452
|
+
const stage =
|
|
453
|
+
err instanceof PythonBridgeError && err.code === -32601
|
|
454
|
+
? "browser.init (bridge too old — upgrade pi-lean-portal)"
|
|
455
|
+
: "Ping handshake";
|
|
406
456
|
reject(
|
|
407
457
|
new Error(
|
|
408
|
-
`PythonPluginAdapter('${this.name}'):
|
|
458
|
+
`PythonPluginAdapter('${this.name}'): ${stage} failed: ` +
|
|
409
459
|
(err instanceof Error ? err.message : String(err)),
|
|
410
460
|
),
|
|
411
461
|
);
|
|
@@ -584,9 +634,6 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
584
634
|
});
|
|
585
635
|
}
|
|
586
636
|
|
|
587
|
-
/**
|
|
588
|
-
* Flush buffered stdout data and process complete lines.
|
|
589
|
-
*/
|
|
590
637
|
private _flushBuffer(): void {
|
|
591
638
|
const lines = this._buffer.split("\n");
|
|
592
639
|
// Keep the last (possibly incomplete) segment in the buffer
|
|
@@ -599,9 +646,6 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
599
646
|
}
|
|
600
647
|
}
|
|
601
648
|
|
|
602
|
-
/**
|
|
603
|
-
* Handle a single complete JSON-RPC response line from stdout.
|
|
604
|
-
*/
|
|
605
649
|
private _handleResponseLine(line: string): void {
|
|
606
650
|
let response: {
|
|
607
651
|
id?: unknown;
|
|
@@ -656,8 +700,7 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
656
700
|
profileMode?: "none" | "session" | "named";
|
|
657
701
|
},
|
|
658
702
|
): Promise<NavigateResult> {
|
|
659
|
-
//
|
|
660
|
-
// but we accept it for interface compatibility.
|
|
703
|
+
// AbortSignal not wired through JSON-RPC; accepted for interface compatibility.
|
|
661
704
|
|
|
662
705
|
try {
|
|
663
706
|
// Build RPC params — include storageState and profileName if provided
|
|
@@ -869,10 +912,16 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
869
912
|
}
|
|
870
913
|
}
|
|
871
914
|
|
|
872
|
-
async evaluate(
|
|
915
|
+
async evaluate(
|
|
916
|
+
taskId: string,
|
|
917
|
+
expression: string,
|
|
918
|
+
readOnly?: boolean,
|
|
919
|
+
): Promise<EvaluateResult> {
|
|
873
920
|
return this._rpcCallTyped(
|
|
874
921
|
"browser.evaluate",
|
|
875
|
-
|
|
922
|
+
readOnly === undefined
|
|
923
|
+
? { taskId, expression }
|
|
924
|
+
: { taskId, expression, readOnly },
|
|
876
925
|
(raw) => ({
|
|
877
926
|
success: !!raw.success,
|
|
878
927
|
result: raw.result,
|
|
@@ -882,6 +931,48 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
882
931
|
);
|
|
883
932
|
}
|
|
884
933
|
|
|
934
|
+
// ═════════════════════════════════════════════════════════════════
|
|
935
|
+
// BrowserPlugin: Quirks introspection
|
|
936
|
+
// ═════════════════════════════════════════════════════════════════
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Query the bridge for its declared quirks flags via the
|
|
940
|
+
* ``browser.describeQuirks`` introspection RPC.
|
|
941
|
+
*
|
|
942
|
+
* Returns the bridge's class-attribute quirks, or all-defaults on
|
|
943
|
+
* transport error (the runner uses ``skipIf`` so tests only run for
|
|
944
|
+
* quirks the bridge actually declares).
|
|
945
|
+
*
|
|
946
|
+
* @internal Only used by test helpers.
|
|
947
|
+
*/
|
|
948
|
+
async describeQuirks(): Promise<QuirksDescriptor> {
|
|
949
|
+
return this._rpcCallTyped<QuirksDescriptor>(
|
|
950
|
+
"browser.describeQuirks",
|
|
951
|
+
{},
|
|
952
|
+
(raw): QuirksDescriptor => ({
|
|
953
|
+
success: true,
|
|
954
|
+
fingerprint_managed_context: !!raw.fingerprint_managed_context,
|
|
955
|
+
eval_prefix: (raw.eval_prefix as string) ?? "",
|
|
956
|
+
scroll_via_wheel: !!raw.scroll_via_wheel,
|
|
957
|
+
skip_default_viewport: !!raw.skip_default_viewport,
|
|
958
|
+
skip_networkidle: !!raw.skip_networkidle,
|
|
959
|
+
wrap_mw_eval_in_eval: !!raw.wrap_mw_eval_in_eval,
|
|
960
|
+
csp_safe_readonly_via_init_script: !!raw.csp_safe_readonly_via_init_script,
|
|
961
|
+
}),
|
|
962
|
+
(error): QuirksDescriptor => ({
|
|
963
|
+
success: false,
|
|
964
|
+
fingerprint_managed_context: false,
|
|
965
|
+
eval_prefix: "",
|
|
966
|
+
scroll_via_wheel: false,
|
|
967
|
+
skip_default_viewport: false,
|
|
968
|
+
skip_networkidle: false,
|
|
969
|
+
wrap_mw_eval_in_eval: false,
|
|
970
|
+
csp_safe_readonly_via_init_script: false,
|
|
971
|
+
error,
|
|
972
|
+
}),
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
885
976
|
// ═════════════════════════════════════════════════════════════════
|
|
886
977
|
// BrowserPlugin: Cookies & storage state
|
|
887
978
|
// ═════════════════════════════════════════════════════════════════
|
|
@@ -1011,30 +1102,21 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
1011
1102
|
taskId: string,
|
|
1012
1103
|
): Promise<{ cookies: unknown[]; origins: unknown[] } | undefined> {
|
|
1013
1104
|
const session = sessionManager.getSession(taskId);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1105
|
+
return persistSessionState(
|
|
1106
|
+
session,
|
|
1107
|
+
async () => {
|
|
1108
|
+
const raw = await this._rpcCall("browser.getStorageState", { taskId });
|
|
1109
|
+
const result = raw as Record<string, unknown>;
|
|
1110
|
+
if (!result.success) {
|
|
1111
|
+
throw new Error("bridge.getStorageState returned failure");
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1022
1114
|
cookies: (result.cookies ?? []) as Record<string, unknown>[],
|
|
1023
1115
|
origins: (result.origins ?? []) as Record<string, unknown>[],
|
|
1024
1116
|
};
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
return undefined;
|
|
1029
|
-
} catch (err) {
|
|
1030
|
-
console.warn(
|
|
1031
|
-
`[pi-lean-portal] Failed to auto-save storage state for profile ` +
|
|
1032
|
-
`'${session.profileName ?? "default"}' via Python bridge: ` +
|
|
1033
|
-
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
1034
|
-
"Session state may be lost.",
|
|
1035
|
-
);
|
|
1036
|
-
return undefined;
|
|
1037
|
-
}
|
|
1117
|
+
},
|
|
1118
|
+
"via Python bridge",
|
|
1119
|
+
);
|
|
1038
1120
|
}
|
|
1039
1121
|
|
|
1040
1122
|
/**
|
|
@@ -1105,9 +1187,6 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
1105
1187
|
}
|
|
1106
1188
|
}
|
|
1107
1189
|
|
|
1108
|
-
/**
|
|
1109
|
-
* Check whether the configured Python interpreter exists in PATH.
|
|
1110
|
-
*/
|
|
1111
1190
|
private _checkPythonExists(): boolean {
|
|
1112
1191
|
try {
|
|
1113
1192
|
const result = spawnSync(this._pythonPath, ["--version"], {
|
|
@@ -1121,8 +1200,28 @@ export class PythonPluginAdapter implements BrowserPlugin {
|
|
|
1121
1200
|
}
|
|
1122
1201
|
|
|
1123
1202
|
/**
|
|
1124
|
-
*
|
|
1203
|
+
* Build the `PYTHONPATH` env value for the spawned bridge process.
|
|
1204
|
+
*
|
|
1205
|
+
* Appends the package's `backends/python-base/` directory (home of the
|
|
1206
|
+
* shared `pi_browser_bridge` Python library) to any existing
|
|
1207
|
+
* `PYTHONPATH` so user-installed stealth backends running in their
|
|
1208
|
+
* own venvs can `from pi_browser_bridge.playwright_base import
|
|
1209
|
+
* PlaywrightBridge` without a separate `pip install` of the bridge
|
|
1210
|
+
* library. Appended (not prepended) so a user's own `PYTHONPATH`
|
|
1211
|
+
* entries keep precedence; for the shipped `chromium-py` / `firefox-py`
|
|
1212
|
+
* venvs the editable install resolves to the same source directory, so
|
|
1213
|
+
* ordering is moot either way.
|
|
1125
1214
|
*/
|
|
1215
|
+
private _buildPythonPath(): string {
|
|
1216
|
+
const pythonBaseDir = join(DEFAULT_BACKENDS_ROOT, "python-base");
|
|
1217
|
+
const existing = process.env.PYTHONPATH;
|
|
1218
|
+
return existing ? existing + pathDelimiter + pythonBaseDir : pythonBaseDir;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1222
|
+
// Result conversion helpers
|
|
1223
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1224
|
+
|
|
1126
1225
|
private _toInteractionResult(raw: unknown): InteractionResult {
|
|
1127
1226
|
const r = raw as Record<string, unknown>;
|
|
1128
1227
|
const result: InteractionResult = {
|
|
@@ -1,47 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
pi_browser_bridge — Shared Python bridge library for pi-lean-portal.
|
|
3
|
-
|
|
4
|
-
Provides the infrastructure needed to build Python browser automation
|
|
5
|
-
backends that communicate with the TypeScript ``PythonPluginAdapter``
|
|
6
|
-
via JSON-RPC 2.0 over stdin/stdout.
|
|
7
|
-
|
|
8
|
-
Key components
|
|
9
|
-
--------------
|
|
10
|
-
* :class:`BrowserBridge` — base class; subclass and override
|
|
11
|
-
``create_browser_session()`` and operation methods.
|
|
12
|
-
* :class:`PlaywrightBridge` — Playwright-specific base extracted from
|
|
13
|
-
the Chromium reference; parameterizes engine, UA, and launch args.
|
|
14
|
-
* :mod:`.transport` — JSON-RPC 2.0 transport over stdin/stdout.
|
|
15
|
-
* :mod:`.accessibility` — Playwright accessibility snapshot parser,
|
|
16
|
-
mirroring the TypeScript version.
|
|
17
|
-
* :mod:`.bot_detection` — anti-automation / bot detection signal
|
|
18
|
-
matcher, mirroring the TypeScript version.
|
|
19
|
-
|
|
20
|
-
Shipped bridges
|
|
21
|
-
---------------
|
|
22
|
-
* ``backends/chromium-py/bridge.py`` — ``ChromiumPyBridge``, a thin
|
|
23
|
-
subclass of :class:`PlaywrightBridge` driving Chromium. Ships as the
|
|
24
|
-
parity reference for Python Chromium-based stealth backends.
|
|
25
|
-
* ``backends/firefox-py/bridge.py`` — ``FirefoxPyBridge``, a thin
|
|
26
|
-
subclass driving Firefox. Ships as the parity reference for Python
|
|
27
|
-
Firefox-based stealth backends.
|
|
28
|
-
|
|
29
|
-
Quick start
|
|
30
|
-
-----------
|
|
31
|
-
::
|
|
32
|
-
|
|
33
|
-
from pi_browser_bridge import BrowserBridge
|
|
34
|
-
|
|
35
|
-
class MyBridge(BrowserBridge):
|
|
36
|
-
def create_browser_session(self, task_id, config):
|
|
37
|
-
# launch browser, return session dict
|
|
38
|
-
...
|
|
39
|
-
def do_navigate(self, task_id, url, timeout_ms):
|
|
40
|
-
# navigate, return result dict
|
|
41
|
-
...
|
|
42
|
-
|
|
43
|
-
if __name__ == "__main__":
|
|
44
|
-
MyBridge().run()
|
|
3
|
+
Provides BrowserBridge and PlaywrightBridge base classes for Python browser backends.
|
|
45
4
|
"""
|
|
46
5
|
|
|
47
6
|
from .bridge import BrowserBridge, SessionNotFoundError, InvalidParamsError
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|