browser-control-and-automation-cli 0.1.1.dev3__py3-none-any.whl
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.
- browser_cli/__init__.py +7 -0
- browser_cli/actions/__init__.py +5 -0
- browser_cli/actions/cli_specs.py +868 -0
- browser_cli/agent_scope/__init__.py +14 -0
- browser_cli/automation/__init__.py +11 -0
- browser_cli/automation/api/__init__.py +5 -0
- browser_cli/automation/api/server.py +321 -0
- browser_cli/automation/hooks.py +42 -0
- browser_cli/automation/loader.py +134 -0
- browser_cli/automation/models.py +127 -0
- browser_cli/automation/persistence/__init__.py +5 -0
- browser_cli/automation/persistence/store.py +682 -0
- browser_cli/automation/projections.py +279 -0
- browser_cli/automation/publisher.py +153 -0
- browser_cli/automation/scheduler/__init__.py +5 -0
- browser_cli/automation/scheduler/schedule.py +113 -0
- browser_cli/automation/service/__init__.py +8 -0
- browser_cli/automation/service/__main__.py +64 -0
- browser_cli/automation/service/client.py +232 -0
- browser_cli/automation/service/runtime.py +388 -0
- browser_cli/automation/toml.py +35 -0
- browser_cli/automation/web/__init__.py +5 -0
- browser_cli/automation/web/assets.py +377 -0
- browser_cli/browser/__init__.py +1 -0
- browser_cli/browser/models.py +44 -0
- browser_cli/browser/network_capture.py +392 -0
- browser_cli/browser/service.py +1519 -0
- browser_cli/browser/session.py +158 -0
- browser_cli/browser/snapshot.py +35 -0
- browser_cli/browser/stealth.py +450 -0
- browser_cli/cli/__init__.py +1 -0
- browser_cli/cli/error_hints.py +26 -0
- browser_cli/cli/main.py +298 -0
- browser_cli/commands/__init__.py +1 -0
- browser_cli/commands/action.py +25 -0
- browser_cli/commands/automation.py +304 -0
- browser_cli/commands/doctor.py +336 -0
- browser_cli/commands/install_skills.py +138 -0
- browser_cli/commands/paths.py +31 -0
- browser_cli/commands/read.py +33 -0
- browser_cli/commands/reload.py +51 -0
- browser_cli/commands/status.py +444 -0
- browser_cli/commands/task.py +71 -0
- browser_cli/constants.py +97 -0
- browser_cli/daemon/__init__.py +1 -0
- browser_cli/daemon/__main__.py +34 -0
- browser_cli/daemon/app.py +941 -0
- browser_cli/daemon/browser_service.py +998 -0
- browser_cli/daemon/client.py +280 -0
- browser_cli/daemon/models.py +63 -0
- browser_cli/daemon/runtime_presentation.py +167 -0
- browser_cli/daemon/server.py +103 -0
- browser_cli/daemon/state.py +15 -0
- browser_cli/daemon/transport.py +77 -0
- browser_cli/drivers/__init__.py +6 -0
- browser_cli/drivers/_extension/__init__.py +1 -0
- browser_cli/drivers/_extension/artifact_actions.py +154 -0
- browser_cli/drivers/_extension/dialog_actions.py +35 -0
- browser_cli/drivers/_extension/input_actions.py +63 -0
- browser_cli/drivers/_extension/locator_actions.py +100 -0
- browser_cli/drivers/_extension/observe_actions.py +285 -0
- browser_cli/drivers/_extension/page_actions.py +210 -0
- browser_cli/drivers/_extension/state_actions.py +134 -0
- browser_cli/drivers/base.py +391 -0
- browser_cli/drivers/extension_driver.py +33 -0
- browser_cli/drivers/models.py +21 -0
- browser_cli/drivers/playwright_driver.py +499 -0
- browser_cli/error_codes.py +27 -0
- browser_cli/errors.py +146 -0
- browser_cli/exit_codes.py +9 -0
- browser_cli/extension/__init__.py +32 -0
- browser_cli/extension/protocol.py +222 -0
- browser_cli/extension/session.py +393 -0
- browser_cli/network.py +147 -0
- browser_cli/outputs/__init__.py +1 -0
- browser_cli/outputs/json.py +10 -0
- browser_cli/outputs/render.py +7 -0
- browser_cli/packaged_skills/__init__.py +1 -0
- browser_cli/packaged_skills/browser-cli-converge/SKILL.md +62 -0
- browser_cli/packaged_skills/browser-cli-delivery/SKILL.md +61 -0
- browser_cli/packaged_skills/browser-cli-explore/SKILL.md +77 -0
- browser_cli/profiles/__init__.py +1 -0
- browser_cli/profiles/discovery.py +185 -0
- browser_cli/refs/__init__.py +17 -0
- browser_cli/refs/generator.py +386 -0
- browser_cli/refs/models.py +127 -0
- browser_cli/refs/registry.py +51 -0
- browser_cli/refs/resolver.py +214 -0
- browser_cli/tabs/__init__.py +5 -0
- browser_cli/tabs/registry.py +283 -0
- browser_cli/task_runtime/__init__.py +31 -0
- browser_cli/task_runtime/client.py +107 -0
- browser_cli/task_runtime/entrypoint.py +110 -0
- browser_cli/task_runtime/errors.py +17 -0
- browser_cli/task_runtime/flow.py +171 -0
- browser_cli/task_runtime/models.py +97 -0
- browser_cli/task_runtime/read.py +71 -0
- browser_cli/task_runtime/templates.py +58 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/METADATA +340 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/RECORD +104 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/WHEEL +5 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/entry_points.txt +2 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Playwright-backed browser session wrapper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browser_cli.browser.models import BrowserLaunchConfig
|
|
9
|
+
from browser_cli.browser.snapshot import capture_snapshot
|
|
10
|
+
from browser_cli.browser.stealth import (
|
|
11
|
+
build_context_options,
|
|
12
|
+
build_ignore_default_args,
|
|
13
|
+
build_init_script,
|
|
14
|
+
build_launch_args,
|
|
15
|
+
)
|
|
16
|
+
from browser_cli.errors import BrowserUnavailableError, ProfileUnavailableError, TemporaryReadError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BrowserSession:
|
|
20
|
+
def __init__(self, config: BrowserLaunchConfig) -> None:
|
|
21
|
+
self._config = config
|
|
22
|
+
self._playwright: Any | None = None
|
|
23
|
+
self._context: Any | None = None
|
|
24
|
+
self._page: Any | None = None
|
|
25
|
+
|
|
26
|
+
async def __aenter__(self) -> BrowserSession:
|
|
27
|
+
await self.start()
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
async def __aexit__(
|
|
31
|
+
self,
|
|
32
|
+
exc_type: type[BaseException] | None,
|
|
33
|
+
exc: BaseException | None,
|
|
34
|
+
tb: TracebackType | None,
|
|
35
|
+
) -> None:
|
|
36
|
+
await self.close()
|
|
37
|
+
|
|
38
|
+
async def start(self) -> None:
|
|
39
|
+
try:
|
|
40
|
+
from playwright.async_api import async_playwright
|
|
41
|
+
except ImportError as exc: # pragma: no cover - dependency problem
|
|
42
|
+
raise BrowserUnavailableError(
|
|
43
|
+
"Playwright is not installed in this repo environment. Run: uv sync --dev"
|
|
44
|
+
) from exc
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
self._playwright = await async_playwright().start()
|
|
48
|
+
chromium = self._playwright.chromium
|
|
49
|
+
context_options = build_context_options(
|
|
50
|
+
viewport_width=self._config.viewport_width,
|
|
51
|
+
viewport_height=self._config.viewport_height,
|
|
52
|
+
locale=self._config.locale,
|
|
53
|
+
)
|
|
54
|
+
self._context = await chromium.launch_persistent_context(
|
|
55
|
+
user_data_dir=str(self._config.user_data_dir),
|
|
56
|
+
executable_path=str(self._config.executable_path)
|
|
57
|
+
if self._config.executable_path
|
|
58
|
+
else None,
|
|
59
|
+
headless=self._config.headless,
|
|
60
|
+
viewport={
|
|
61
|
+
"width": self._config.viewport_width,
|
|
62
|
+
"height": self._config.viewport_height,
|
|
63
|
+
},
|
|
64
|
+
ignore_default_args=build_ignore_default_args(),
|
|
65
|
+
args=[
|
|
66
|
+
*build_launch_args(
|
|
67
|
+
headless=self._config.headless,
|
|
68
|
+
viewport_width=self._config.viewport_width,
|
|
69
|
+
viewport_height=self._config.viewport_height,
|
|
70
|
+
locale=self._config.locale,
|
|
71
|
+
),
|
|
72
|
+
f"--profile-directory={self._config.profile_directory}",
|
|
73
|
+
],
|
|
74
|
+
**context_options,
|
|
75
|
+
)
|
|
76
|
+
init_script = build_init_script(
|
|
77
|
+
headless=self._config.headless, locale=self._config.locale
|
|
78
|
+
)
|
|
79
|
+
if init_script:
|
|
80
|
+
await self._context.add_init_script(init_script)
|
|
81
|
+
self._page = (
|
|
82
|
+
self._context.pages[0] if self._context.pages else await self._context.new_page()
|
|
83
|
+
)
|
|
84
|
+
self._page.set_default_navigation_timeout(self._config.navigation_timeout_ms)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
await self.close()
|
|
87
|
+
self._raise_launch_error(exc)
|
|
88
|
+
|
|
89
|
+
async def close(self) -> None:
|
|
90
|
+
if self._context is not None:
|
|
91
|
+
await self._context.close()
|
|
92
|
+
self._context = None
|
|
93
|
+
if self._playwright is not None:
|
|
94
|
+
await self._playwright.stop()
|
|
95
|
+
self._playwright = None
|
|
96
|
+
self._page = None
|
|
97
|
+
|
|
98
|
+
async def navigate(self, url: str) -> None:
|
|
99
|
+
self._ensure_started()
|
|
100
|
+
try:
|
|
101
|
+
await self._page.goto(
|
|
102
|
+
url, wait_until="load", timeout=self._config.navigation_timeout_ms
|
|
103
|
+
)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
raise TemporaryReadError(f"Failed to navigate to {url}: {exc}") from exc
|
|
106
|
+
|
|
107
|
+
async def settle(self) -> None:
|
|
108
|
+
self._ensure_started()
|
|
109
|
+
await self._page.wait_for_timeout(self._config.settle_timeout_ms)
|
|
110
|
+
|
|
111
|
+
async def scroll_to_bottom(self) -> None:
|
|
112
|
+
self._ensure_started()
|
|
113
|
+
stable_rounds = 0
|
|
114
|
+
previous_height = -1
|
|
115
|
+
for _ in range(8):
|
|
116
|
+
current_height = await self._page.evaluate(
|
|
117
|
+
"() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
|
|
118
|
+
)
|
|
119
|
+
await self._page.evaluate(
|
|
120
|
+
"() => window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' })"
|
|
121
|
+
)
|
|
122
|
+
await self._page.wait_for_timeout(450)
|
|
123
|
+
next_height = await self._page.evaluate(
|
|
124
|
+
"() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
|
|
125
|
+
)
|
|
126
|
+
if next_height == current_height == previous_height:
|
|
127
|
+
stable_rounds += 1
|
|
128
|
+
else:
|
|
129
|
+
stable_rounds = 0
|
|
130
|
+
previous_height = next_height
|
|
131
|
+
if stable_rounds >= 1:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
async def capture_html(self) -> str:
|
|
135
|
+
self._ensure_started()
|
|
136
|
+
return await self._page.content()
|
|
137
|
+
|
|
138
|
+
async def capture_snapshot(self) -> str:
|
|
139
|
+
self._ensure_started()
|
|
140
|
+
snapshot = await capture_snapshot(self._page, page_id="read_page")
|
|
141
|
+
return snapshot.tree
|
|
142
|
+
|
|
143
|
+
def _ensure_started(self) -> None:
|
|
144
|
+
if self._page is None:
|
|
145
|
+
raise TemporaryReadError("Browser session was not started.")
|
|
146
|
+
|
|
147
|
+
def _raise_launch_error(self, exc: Exception) -> None:
|
|
148
|
+
message = str(exc)
|
|
149
|
+
lowered = message.lower()
|
|
150
|
+
if (
|
|
151
|
+
"singleton" in lowered
|
|
152
|
+
or "profile" in lowered
|
|
153
|
+
or "user data directory is already in use" in lowered
|
|
154
|
+
):
|
|
155
|
+
raise ProfileUnavailableError(message) from exc
|
|
156
|
+
if "executable" in lowered or "browser" in lowered or "failed to launch" in lowered:
|
|
157
|
+
raise BrowserUnavailableError(message) from exc
|
|
158
|
+
raise TemporaryReadError(message) from exc
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Compatibility wrapper around semantic snapshot generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browser_cli.refs import SemanticSnapshotGenerator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class SnapshotCapture:
|
|
13
|
+
tree: str
|
|
14
|
+
refs: dict[str, dict[str, Any]]
|
|
15
|
+
snapshot_id: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def capture_snapshot(
|
|
19
|
+
page: Any,
|
|
20
|
+
*,
|
|
21
|
+
page_id: str,
|
|
22
|
+
interactive: bool = False,
|
|
23
|
+
full_page: bool = True,
|
|
24
|
+
) -> SnapshotCapture:
|
|
25
|
+
snapshot = await SemanticSnapshotGenerator().get_snapshot(
|
|
26
|
+
page,
|
|
27
|
+
page_id=page_id,
|
|
28
|
+
interactive=interactive,
|
|
29
|
+
full_page=full_page,
|
|
30
|
+
)
|
|
31
|
+
return SnapshotCapture(
|
|
32
|
+
tree=snapshot.tree,
|
|
33
|
+
refs={ref: data.to_summary() for ref, data in snapshot.refs.items()},
|
|
34
|
+
snapshot_id=snapshot.metadata.snapshot_id,
|
|
35
|
+
)
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Stealth helpers adapted from bridgic-browser concepts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
CHROME_DISABLED_COMPONENTS = [
|
|
12
|
+
"AcceptCHFrame",
|
|
13
|
+
"AutoExpandDetailsElement",
|
|
14
|
+
"AvoidUnnecessaryBeforeUnloadCheckSync",
|
|
15
|
+
"CertificateTransparencyComponentUpdater",
|
|
16
|
+
"DestroyProfileOnBrowserClose",
|
|
17
|
+
"DialMediaRouteProvider",
|
|
18
|
+
"ExtensionManifestV2Disabled",
|
|
19
|
+
"GlobalMediaControls",
|
|
20
|
+
"HttpsUpgrades",
|
|
21
|
+
"ImprovedCookieControls",
|
|
22
|
+
"LazyFrameLoading",
|
|
23
|
+
"LensOverlay",
|
|
24
|
+
"MediaRouter",
|
|
25
|
+
"PaintHolding",
|
|
26
|
+
"ThirdPartyStoragePartitioning",
|
|
27
|
+
"Translate",
|
|
28
|
+
"AutomationControlled",
|
|
29
|
+
"BackForwardCache",
|
|
30
|
+
"OptimizationHints",
|
|
31
|
+
"ProcessPerSiteUpToMainFrameThreshold",
|
|
32
|
+
"InterestFeedContentSuggestions",
|
|
33
|
+
"CalculateNativeWinOcclusion",
|
|
34
|
+
"HeavyAdPrivacyMitigations",
|
|
35
|
+
"PrivacySandboxSettings4",
|
|
36
|
+
"AutofillServerCommunication",
|
|
37
|
+
"CrashReporting",
|
|
38
|
+
"OverscrollHistoryNavigation",
|
|
39
|
+
"InfiniteSessionRestore",
|
|
40
|
+
"ExtensionDisableUnsupportedDeveloper",
|
|
41
|
+
"ExtensionManifestV2Unsupported",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
CHROME_DISABLED_COMPONENTS_HEADED = [
|
|
45
|
+
"AutomationControlled",
|
|
46
|
+
"InfiniteSessionRestore",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
CHROME_STEALTH_ARGS = [
|
|
50
|
+
"--disable-field-trial-config",
|
|
51
|
+
"--disable-background-networking",
|
|
52
|
+
"--disable-background-timer-throttling",
|
|
53
|
+
"--disable-backgrounding-occluded-windows",
|
|
54
|
+
"--disable-back-forward-cache",
|
|
55
|
+
"--disable-breakpad",
|
|
56
|
+
"--disable-client-side-phishing-detection",
|
|
57
|
+
"--disable-component-extensions-with-background-pages",
|
|
58
|
+
"--disable-component-update",
|
|
59
|
+
"--no-default-browser-check",
|
|
60
|
+
"--disable-hang-monitor",
|
|
61
|
+
"--disable-ipc-flooding-protection",
|
|
62
|
+
"--disable-popup-blocking",
|
|
63
|
+
"--disable-prompt-on-repost",
|
|
64
|
+
"--disable-renderer-backgrounding",
|
|
65
|
+
"--metrics-recording-only",
|
|
66
|
+
"--no-first-run",
|
|
67
|
+
"--no-service-autorun",
|
|
68
|
+
"--export-tagged-pdf",
|
|
69
|
+
"--disable-search-engine-choice-screen",
|
|
70
|
+
"--unsafely-disable-devtools-self-xss-warnings",
|
|
71
|
+
"--disable-sync",
|
|
72
|
+
"--allow-legacy-extension-manifests",
|
|
73
|
+
"--allow-pre-commit-input",
|
|
74
|
+
"--disable-blink-features=AutomationControlled",
|
|
75
|
+
"--log-level=2",
|
|
76
|
+
"--lang=en-US",
|
|
77
|
+
"--disable-focus-on-load",
|
|
78
|
+
"--disable-window-activation",
|
|
79
|
+
"--generate-pdf-document-outline",
|
|
80
|
+
"--no-pings",
|
|
81
|
+
"--disable-infobars",
|
|
82
|
+
"--hide-crash-restore-bubble",
|
|
83
|
+
"--disable-domain-reliability",
|
|
84
|
+
"--disable-datasaver-prompt",
|
|
85
|
+
"--disable-speech-synthesis-api",
|
|
86
|
+
"--disable-speech-api",
|
|
87
|
+
"--disable-print-preview",
|
|
88
|
+
"--safebrowsing-disable-auto-update",
|
|
89
|
+
"--disable-external-intent-requests",
|
|
90
|
+
"--disable-desktop-notifications",
|
|
91
|
+
"--noerrdialogs",
|
|
92
|
+
"--silent-debugger-extension-api",
|
|
93
|
+
"--disable-extensions-http-throttling",
|
|
94
|
+
"--extensions-on-chrome-urls",
|
|
95
|
+
"--disable-default-apps",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
CHROME_STEALTH_ARGS_HEADED = [
|
|
99
|
+
"--disable-blink-features=AutomationControlled",
|
|
100
|
+
"--no-first-run",
|
|
101
|
+
"--no-default-browser-check",
|
|
102
|
+
"--disable-infobars",
|
|
103
|
+
"--hide-crash-restore-bubble",
|
|
104
|
+
"--no-service-autorun",
|
|
105
|
+
"--log-level=2",
|
|
106
|
+
"--disable-search-engine-choice-screen",
|
|
107
|
+
"--unsafely-disable-devtools-self-xss-warnings",
|
|
108
|
+
"--disable-popup-blocking",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
CHROME_LINUX_ONLY_ARGS = [
|
|
112
|
+
"--disable-dev-shm-usage",
|
|
113
|
+
"--ash-no-nudges",
|
|
114
|
+
"--suppress-message-center-popups",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
CHROME_IGNORE_DEFAULT_ARGS = [
|
|
118
|
+
"--enable-automation",
|
|
119
|
+
"--hide-scrollbars",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
DEFAULT_PERMISSIONS = [
|
|
123
|
+
"clipboard-read",
|
|
124
|
+
"clipboard-write",
|
|
125
|
+
"notifications",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
_STEALTH_INIT_SCRIPT_TEMPLATE = r"""
|
|
129
|
+
(function () {
|
|
130
|
+
const _nativeFns = new WeakSet();
|
|
131
|
+
const _nativeFnNames = new WeakMap();
|
|
132
|
+
const _mkNative = (fn, name) => {
|
|
133
|
+
_nativeFns.add(fn);
|
|
134
|
+
if (name !== undefined) _nativeFnNames.set(fn, name);
|
|
135
|
+
return fn;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const _origFnToString = Function.prototype.toString;
|
|
139
|
+
Function.prototype.toString = _mkNative(function toString() {
|
|
140
|
+
if (_nativeFns.has(this)) {
|
|
141
|
+
const _n = _nativeFnNames.has(this) ? _nativeFnNames.get(this) : (this.name || '');
|
|
142
|
+
return `function ${_n}() { [native code] }`;
|
|
143
|
+
}
|
|
144
|
+
return _origFnToString.call(this);
|
|
145
|
+
}, 'toString');
|
|
146
|
+
|
|
147
|
+
const _wdDesc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
|
|
148
|
+
if (_wdDesc) {
|
|
149
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
150
|
+
get: _mkNative(function () { return undefined; }, ''),
|
|
151
|
+
configurable: true,
|
|
152
|
+
});
|
|
153
|
+
} else if (navigator.webdriver !== undefined) {
|
|
154
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
155
|
+
get: _mkNative(function () { return undefined; }, ''),
|
|
156
|
+
configurable: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const _makeMime = (type, suffixes, description) =>
|
|
161
|
+
({ type, suffixes, description, enabledPlugin: null });
|
|
162
|
+
|
|
163
|
+
const _pdfMimes = [
|
|
164
|
+
_makeMime('application/pdf', 'pdf', 'Portable Document Format'),
|
|
165
|
+
_makeMime('text/pdf', 'pdf', 'Portable Document Format'),
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const _makePlugin = (name, description) => {
|
|
169
|
+
const p = { name, description, filename: 'internal-pdf-viewer', length: _pdfMimes.length };
|
|
170
|
+
_pdfMimes.forEach((m, i) => {
|
|
171
|
+
const localMime = { type: m.type, suffixes: m.suffixes, description: m.description, enabledPlugin: p };
|
|
172
|
+
Object.defineProperty(p, i, { value: localMime, enumerable: true });
|
|
173
|
+
});
|
|
174
|
+
p.item = _mkNative(function item(i) { return p[i] ?? null; }, 'item');
|
|
175
|
+
p.namedItem = _mkNative(function namedItem(n) {
|
|
176
|
+
const idx = _pdfMimes.findIndex(m => m.type === n);
|
|
177
|
+
return idx >= 0 ? p[idx] : null;
|
|
178
|
+
}, 'namedItem');
|
|
179
|
+
return p;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const _plugins = [
|
|
183
|
+
_makePlugin('PDF Viewer', 'Portable Document Format'),
|
|
184
|
+
_makePlugin('Chrome PDF Viewer', 'Portable Document Format'),
|
|
185
|
+
_makePlugin('Chromium PDF Viewer', 'Portable Document Format'),
|
|
186
|
+
_makePlugin('Microsoft Edge PDF Viewer', 'Portable Document Format'),
|
|
187
|
+
_makePlugin('WebKit built-in PDF', 'Portable Document Format'),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
_pdfMimes.forEach((m) => { m.enabledPlugin = _plugins[0]; });
|
|
191
|
+
|
|
192
|
+
const _pluginList = Object.assign([..._plugins], {
|
|
193
|
+
item: _mkNative(function item(i) { return _plugins[i] ?? null; }, 'item'),
|
|
194
|
+
namedItem: _mkNative(function namedItem(n) { return _plugins.find(p => p.name === n) ?? null; }, 'namedItem'),
|
|
195
|
+
refresh: _mkNative(function refresh() {}, 'refresh'),
|
|
196
|
+
length: _plugins.length,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
200
|
+
get: _mkNative(function () { return _pluginList; }, ''),
|
|
201
|
+
configurable: true,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const _mimeList = Object.assign([..._pdfMimes], {
|
|
205
|
+
item: _mkNative(function item(i) { return _pdfMimes[i] ?? null; }, 'item'),
|
|
206
|
+
namedItem: _mkNative(function namedItem(n) { return _pdfMimes.find(m => m.type === n) ?? null; }, 'namedItem'),
|
|
207
|
+
length: _pdfMimes.length,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
Object.defineProperty(navigator, 'mimeTypes', {
|
|
211
|
+
get: _mkNative(function () { return _mimeList; }, ''),
|
|
212
|
+
configurable: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
Object.defineProperty(navigator, 'languages', {
|
|
217
|
+
get: _mkNative(function () { return __BROWSER_CLI_LANGS__; }, ''),
|
|
218
|
+
configurable: true,
|
|
219
|
+
});
|
|
220
|
+
} catch (_) {}
|
|
221
|
+
|
|
222
|
+
if (!window.chrome || !window.chrome.runtime || !window.chrome.csi || !window.chrome.loadTimes) {
|
|
223
|
+
const _chrome = {
|
|
224
|
+
app: {
|
|
225
|
+
isInstalled: false,
|
|
226
|
+
getDetails: _mkNative(function getDetails() { return null; }, 'getDetails'),
|
|
227
|
+
getIsInstalled: _mkNative(function getIsInstalled() { return false; }, 'getIsInstalled'),
|
|
228
|
+
installState: _mkNative(function installState() { return 'not_installed'; }, 'installState'),
|
|
229
|
+
},
|
|
230
|
+
runtime: {
|
|
231
|
+
connect: _mkNative(function connect() {}, 'connect'),
|
|
232
|
+
sendMessage: _mkNative(function sendMessage() {}, 'sendMessage'),
|
|
233
|
+
},
|
|
234
|
+
csi: _mkNative(function csi() {
|
|
235
|
+
return {
|
|
236
|
+
onloadT: Date.now(),
|
|
237
|
+
pageT: Date.now() - (performance.timeOrigin ?? performance.timing?.navigationStart ?? 0),
|
|
238
|
+
startE: Date.now() - 1000,
|
|
239
|
+
tran: 15,
|
|
240
|
+
};
|
|
241
|
+
}, 'csi'),
|
|
242
|
+
loadTimes: _mkNative(function loadTimes() {
|
|
243
|
+
return {
|
|
244
|
+
commitLoadTime: Date.now() / 1000 - 1,
|
|
245
|
+
connectionInfo: 'h2',
|
|
246
|
+
finishDocumentLoadTime: Date.now() / 1000,
|
|
247
|
+
finishLoadTime: Date.now() / 1000,
|
|
248
|
+
firstPaintAfterLoadTime: 0,
|
|
249
|
+
firstPaintTime: Date.now() / 1000 - 0.5,
|
|
250
|
+
navigationType: 'Other',
|
|
251
|
+
npnNegotiatedProtocol: 'h2',
|
|
252
|
+
requestTime: Date.now() / 1000 - 1,
|
|
253
|
+
startLoadTime: Date.now() / 1000 - 1,
|
|
254
|
+
wasAlternateProtocolAvailable: false,
|
|
255
|
+
wasFetchedViaSpdy: true,
|
|
256
|
+
wasNpnNegotiated: true,
|
|
257
|
+
};
|
|
258
|
+
}, 'loadTimes'),
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
Object.defineProperty(window, 'chrome', {
|
|
262
|
+
value: _chrome, writable: false, enumerable: true, configurable: false,
|
|
263
|
+
});
|
|
264
|
+
} catch (_) { window.chrome = _chrome; }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (navigator.permissions && navigator.permissions.query) {
|
|
268
|
+
const _origQuery = navigator.permissions.query.bind(navigator.permissions);
|
|
269
|
+
navigator.permissions.query = _mkNative(function query(params) {
|
|
270
|
+
if (params && params.name === 'notifications') {
|
|
271
|
+
return Promise.resolve({ state: Notification.permission === 'denied' ? 'default' : Notification.permission, onchange: null });
|
|
272
|
+
}
|
|
273
|
+
return _origQuery(params);
|
|
274
|
+
}, 'query');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (window.Notification && Notification.permission === 'denied') {
|
|
278
|
+
try {
|
|
279
|
+
Object.defineProperty(Notification, 'permission', {
|
|
280
|
+
get: _mkNative(function () { return 'default'; }, ''),
|
|
281
|
+
configurable: true,
|
|
282
|
+
});
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try { document.hasFocus = _mkNative(function hasFocus() { return true; }, 'hasFocus'); } catch (_) {}
|
|
287
|
+
try { Object.defineProperty(document, 'hidden', { get: _mkNative(function () { return false; }, ''), configurable: true }); } catch (_) {}
|
|
288
|
+
try { Object.defineProperty(document, 'visibilityState', { get: _mkNative(function () { return 'visible'; }, ''), configurable: true }); } catch (_) {}
|
|
289
|
+
|
|
290
|
+
if (window.outerWidth === 0) {
|
|
291
|
+
try { Object.defineProperty(window, 'outerWidth', { get: _mkNative(function () { return window.innerWidth; }, ''), configurable: true }); } catch (_) {}
|
|
292
|
+
}
|
|
293
|
+
if (window.outerHeight === 0) {
|
|
294
|
+
try { Object.defineProperty(window, 'outerHeight', { get: _mkNative(function () { return window.innerHeight; }, ''), configurable: true }); } catch (_) {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (navigator.deviceMemory === undefined) {
|
|
298
|
+
try { Object.defineProperty(navigator, 'deviceMemory', { get: _mkNative(function () { return 8; }, ''), configurable: true }); } catch (_) {}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!navigator.hardwareConcurrency || navigator.hardwareConcurrency < 2) {
|
|
302
|
+
try { Object.defineProperty(navigator, 'hardwareConcurrency', { get: _mkNative(function () { return 8; }, ''), configurable: true }); } catch (_) {}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!navigator.connection) {
|
|
306
|
+
try {
|
|
307
|
+
Object.defineProperty(navigator, 'connection', {
|
|
308
|
+
get: _mkNative(function () { return { effectiveType: '4g', downlink: 10, rtt: 100, saveData: false }; }, ''),
|
|
309
|
+
configurable: true,
|
|
310
|
+
});
|
|
311
|
+
} catch (_) {}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
(function () {
|
|
315
|
+
const _patchWebGL = (Ctx) => {
|
|
316
|
+
if (!Ctx) return;
|
|
317
|
+
const _orig = Ctx.prototype.getParameter;
|
|
318
|
+
Ctx.prototype.getParameter = _mkNative(function getParameter(parameter) {
|
|
319
|
+
const _val = _orig.call(this, parameter);
|
|
320
|
+
if (parameter === 37445) {
|
|
321
|
+
if (_val && (_val.includes('Google') || _val === '')) return 'Intel Inc.';
|
|
322
|
+
return _val;
|
|
323
|
+
}
|
|
324
|
+
if (parameter === 37446) {
|
|
325
|
+
if (_val && (_val.includes('SwiftShader') || _val === '')) return 'Intel Iris OpenGL Engine';
|
|
326
|
+
return _val;
|
|
327
|
+
}
|
|
328
|
+
return _val;
|
|
329
|
+
}, 'getParameter');
|
|
330
|
+
};
|
|
331
|
+
_patchWebGL(window.WebGLRenderingContext);
|
|
332
|
+
_patchWebGL(window.WebGL2RenderingContext);
|
|
333
|
+
})();
|
|
334
|
+
})();
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _normalize_locale(raw: str | None) -> str | None:
|
|
339
|
+
if raw is None:
|
|
340
|
+
return None
|
|
341
|
+
value = raw.strip().split(".", 1)[0]
|
|
342
|
+
if not value:
|
|
343
|
+
return None
|
|
344
|
+
return value.replace("_", "-")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _languages_for_locale(locale: str | None) -> list[str]:
|
|
348
|
+
normalized = _normalize_locale(locale)
|
|
349
|
+
if not normalized:
|
|
350
|
+
return ["en-US", "en"]
|
|
351
|
+
parts = normalized.split("-")
|
|
352
|
+
base = parts[0]
|
|
353
|
+
langs = [normalized]
|
|
354
|
+
if base != normalized:
|
|
355
|
+
langs.append(base)
|
|
356
|
+
if base.lower() != "en" and "en" not in langs:
|
|
357
|
+
langs.append("en")
|
|
358
|
+
return langs
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _get_playwright_disabled_features() -> list[str]:
|
|
362
|
+
try:
|
|
363
|
+
import playwright as playwright_pkg
|
|
364
|
+
except Exception:
|
|
365
|
+
return []
|
|
366
|
+
try:
|
|
367
|
+
switches_js = (
|
|
368
|
+
Path(playwright_pkg.__file__).parent
|
|
369
|
+
/ "driver/package/lib/server/chromium/chromiumSwitches.js"
|
|
370
|
+
)
|
|
371
|
+
text = switches_js.read_text(encoding="utf-8")
|
|
372
|
+
match = re.search(r"const disabledFeatures\s*=.*?\[(.+?)\]\.filter", text, re.DOTALL)
|
|
373
|
+
if not match:
|
|
374
|
+
return []
|
|
375
|
+
return [feature for feature in re.findall(r'"([^"]+)"', match.group(1)) if feature]
|
|
376
|
+
except Exception:
|
|
377
|
+
return []
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _build_disable_features(headless: bool) -> str:
|
|
381
|
+
playright_features = _get_playwright_disabled_features()
|
|
382
|
+
browser_cli_features = (
|
|
383
|
+
CHROME_DISABLED_COMPONENTS if headless else CHROME_DISABLED_COMPONENTS_HEADED
|
|
384
|
+
)
|
|
385
|
+
combined = list(dict.fromkeys(playright_features + list(browser_cli_features)))
|
|
386
|
+
return f"--disable-features={','.join(combined)}"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def build_launch_args(
|
|
390
|
+
*,
|
|
391
|
+
headless: bool,
|
|
392
|
+
viewport_width: int,
|
|
393
|
+
viewport_height: int,
|
|
394
|
+
locale: str | None = None,
|
|
395
|
+
) -> list[str]:
|
|
396
|
+
normalized_locale = _normalize_locale(locale) or "en-US"
|
|
397
|
+
if headless:
|
|
398
|
+
args = [arg for arg in CHROME_STEALTH_ARGS if not arg.startswith("--lang=")]
|
|
399
|
+
args.append(f"--lang={normalized_locale}")
|
|
400
|
+
if sys.platform == "linux":
|
|
401
|
+
args.extend(CHROME_LINUX_ONLY_ARGS)
|
|
402
|
+
args.append(_build_disable_features(headless=True))
|
|
403
|
+
args.append(f"--window-size={viewport_width},{viewport_height}")
|
|
404
|
+
args.extend(
|
|
405
|
+
[
|
|
406
|
+
"--headless=new",
|
|
407
|
+
"--hide-scrollbars",
|
|
408
|
+
"--mute-audio",
|
|
409
|
+
"--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4",
|
|
410
|
+
]
|
|
411
|
+
)
|
|
412
|
+
return args
|
|
413
|
+
|
|
414
|
+
args = list(CHROME_STEALTH_ARGS_HEADED)
|
|
415
|
+
if sys.platform == "linux":
|
|
416
|
+
args.append("--disable-dev-shm-usage")
|
|
417
|
+
args.append(_build_disable_features(headless=False))
|
|
418
|
+
args.append(f"--lang={normalized_locale}")
|
|
419
|
+
args.append(f"--window-size={viewport_width},{viewport_height}")
|
|
420
|
+
return args
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def build_ignore_default_args() -> list[str]:
|
|
424
|
+
return list(CHROME_IGNORE_DEFAULT_ARGS)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def build_context_options(
|
|
428
|
+
*,
|
|
429
|
+
viewport_width: int,
|
|
430
|
+
viewport_height: int,
|
|
431
|
+
locale: str | None = None,
|
|
432
|
+
) -> dict[str, Any]:
|
|
433
|
+
options: dict[str, Any] = {
|
|
434
|
+
"accept_downloads": True,
|
|
435
|
+
"permissions": list(DEFAULT_PERMISSIONS),
|
|
436
|
+
"screen": {"width": viewport_width, "height": viewport_height},
|
|
437
|
+
}
|
|
438
|
+
normalized_locale = _normalize_locale(locale)
|
|
439
|
+
if normalized_locale:
|
|
440
|
+
options["locale"] = normalized_locale
|
|
441
|
+
return options
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def build_init_script(*, headless: bool, locale: str | None = None) -> str | None:
|
|
445
|
+
if not headless:
|
|
446
|
+
return None
|
|
447
|
+
return _STEALTH_INIT_SCRIPT_TEMPLATE.replace(
|
|
448
|
+
"__BROWSER_CLI_LANGS__",
|
|
449
|
+
json.dumps(_languages_for_locale(locale)),
|
|
450
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Short recovery hints for common CLI failures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from browser_cli import error_codes
|
|
6
|
+
from browser_cli.errors import BrowserCliError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def next_hint_for_error(exc: BrowserCliError) -> str | None:
|
|
10
|
+
if exc.error_code == error_codes.BROWSER_UNAVAILABLE:
|
|
11
|
+
return "install stable Google Chrome and re-run browser-cli doctor"
|
|
12
|
+
if exc.error_code == error_codes.PROFILE_UNAVAILABLE:
|
|
13
|
+
return "close Browser CLI-owned Chrome windows or inspect browser-cli status"
|
|
14
|
+
if exc.error_code in {
|
|
15
|
+
error_codes.DAEMON_NOT_AVAILABLE,
|
|
16
|
+
error_codes.AUTOMATION_SERVICE_NOT_AVAILABLE,
|
|
17
|
+
}:
|
|
18
|
+
return "run browser-cli reload"
|
|
19
|
+
message = str(exc).lower()
|
|
20
|
+
if exc.error_code == error_codes.INVALID_INPUT and "task" in message:
|
|
21
|
+
return "run browser-cli task validate <task-dir>"
|
|
22
|
+
if "version" in message and "not found" in message:
|
|
23
|
+
return "run browser-cli automation versions <automation-id>"
|
|
24
|
+
if "automation" in message and "not found" in message:
|
|
25
|
+
return "run browser-cli automation list"
|
|
26
|
+
return None
|