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,941 @@
|
|
|
1
|
+
"""Daemon application and action handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import traceback
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from browser_cli import error_codes
|
|
12
|
+
from browser_cli.constants import DEFAULT_PUBLIC_AGENT_ID
|
|
13
|
+
from browser_cli.errors import (
|
|
14
|
+
BrowserCliError,
|
|
15
|
+
InvalidInputError,
|
|
16
|
+
NoActiveTabError,
|
|
17
|
+
OperationFailedError,
|
|
18
|
+
)
|
|
19
|
+
from browser_cli.profiles.discovery import ChromeEnvironment
|
|
20
|
+
|
|
21
|
+
from .models import DaemonRequest, DaemonResponse
|
|
22
|
+
from .runtime_presentation import build_runtime_presentation
|
|
23
|
+
from .state import DaemonState
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
Handler = Callable[[DaemonRequest], Awaitable[dict[str, Any]]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BrowserDaemonApp:
|
|
31
|
+
def __init__(self, state: DaemonState | None = None) -> None:
|
|
32
|
+
self._state = state or DaemonState()
|
|
33
|
+
self._handlers: dict[str, Handler] = {
|
|
34
|
+
"runtime-status": self._handle_runtime_status,
|
|
35
|
+
"open": self._handle_open,
|
|
36
|
+
"search": self._handle_search,
|
|
37
|
+
"tabs": self._handle_tabs,
|
|
38
|
+
"switch-tab": self._handle_switch_tab,
|
|
39
|
+
"new-tab": self._handle_new_tab,
|
|
40
|
+
"close-tab": self._handle_close_tab,
|
|
41
|
+
"close": self._handle_close,
|
|
42
|
+
"stop": self._handle_stop,
|
|
43
|
+
"info": self._handle_info,
|
|
44
|
+
"read-page": self._handle_read_page,
|
|
45
|
+
"html": self._handle_html,
|
|
46
|
+
"snapshot": self._handle_snapshot,
|
|
47
|
+
"reload": self._handle_reload,
|
|
48
|
+
"back": self._handle_back,
|
|
49
|
+
"forward": self._handle_forward,
|
|
50
|
+
"click": self._handle_click,
|
|
51
|
+
"double-click": self._handle_double_click,
|
|
52
|
+
"hover": self._handle_hover,
|
|
53
|
+
"focus": self._handle_focus,
|
|
54
|
+
"fill": self._handle_fill,
|
|
55
|
+
"fill-form": self._handle_fill_form,
|
|
56
|
+
"select": self._handle_select,
|
|
57
|
+
"options": self._handle_options,
|
|
58
|
+
"check": self._handle_check,
|
|
59
|
+
"uncheck": self._handle_uncheck,
|
|
60
|
+
"scroll-to": self._handle_scroll_to,
|
|
61
|
+
"drag": self._handle_drag,
|
|
62
|
+
"upload": self._handle_upload,
|
|
63
|
+
"type": self._handle_type,
|
|
64
|
+
"press": self._handle_press,
|
|
65
|
+
"key-down": self._handle_key_down,
|
|
66
|
+
"key-up": self._handle_key_up,
|
|
67
|
+
"scroll": self._handle_scroll,
|
|
68
|
+
"mouse-click": self._handle_mouse_click,
|
|
69
|
+
"mouse-move": self._handle_mouse_move,
|
|
70
|
+
"mouse-drag": self._handle_mouse_drag,
|
|
71
|
+
"mouse-down": self._handle_mouse_down,
|
|
72
|
+
"mouse-up": self._handle_mouse_up,
|
|
73
|
+
"eval": self._handle_eval,
|
|
74
|
+
"eval-on": self._handle_eval_on,
|
|
75
|
+
"wait": self._handle_wait,
|
|
76
|
+
"wait-network": self._handle_wait_network,
|
|
77
|
+
"screenshot": self._handle_screenshot,
|
|
78
|
+
"pdf": self._handle_pdf,
|
|
79
|
+
"console-start": self._handle_console_start,
|
|
80
|
+
"console": self._handle_console,
|
|
81
|
+
"console-stop": self._handle_console_stop,
|
|
82
|
+
"network-wait": self._handle_network_wait,
|
|
83
|
+
"network-start": self._handle_network_start,
|
|
84
|
+
"network": self._handle_network,
|
|
85
|
+
"network-stop": self._handle_network_stop,
|
|
86
|
+
"dialog-setup": self._handle_dialog_setup,
|
|
87
|
+
"dialog": self._handle_dialog,
|
|
88
|
+
"dialog-remove": self._handle_dialog_remove,
|
|
89
|
+
"cookies": self._handle_cookies,
|
|
90
|
+
"cookie-set": self._handle_cookie_set,
|
|
91
|
+
"cookies-clear": self._handle_cookies_clear,
|
|
92
|
+
"storage-save": self._handle_storage_save,
|
|
93
|
+
"storage-load": self._handle_storage_load,
|
|
94
|
+
"verify-text": self._handle_verify_text,
|
|
95
|
+
"verify-visible": self._handle_verify_visible,
|
|
96
|
+
"verify-url": self._handle_verify_url,
|
|
97
|
+
"verify-title": self._handle_verify_title,
|
|
98
|
+
"verify-state": self._handle_verify_state,
|
|
99
|
+
"verify-value": self._handle_verify_value,
|
|
100
|
+
"trace-start": self._handle_trace_start,
|
|
101
|
+
"trace-chunk": self._handle_trace_chunk,
|
|
102
|
+
"trace-stop": self._handle_trace_stop,
|
|
103
|
+
"video-start": self._handle_video_start,
|
|
104
|
+
"video-stop": self._handle_video_stop,
|
|
105
|
+
"resize": self._handle_resize,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def state(self) -> DaemonState:
|
|
110
|
+
return self._state
|
|
111
|
+
|
|
112
|
+
async def execute(self, request: DaemonRequest) -> DaemonResponse:
|
|
113
|
+
agent_id = request.agent_id or DEFAULT_PUBLIC_AGENT_ID
|
|
114
|
+
request.agent_id = agent_id
|
|
115
|
+
handler = self._handlers.get(request.action)
|
|
116
|
+
if handler is None:
|
|
117
|
+
return self._error_response(
|
|
118
|
+
InvalidInputError(f"Unknown command: {request.action}"),
|
|
119
|
+
request=request,
|
|
120
|
+
)
|
|
121
|
+
runtime_meta: dict[str, Any] = {}
|
|
122
|
+
command_started = False
|
|
123
|
+
try:
|
|
124
|
+
self._maybe_configure_browser_environment(request.args)
|
|
125
|
+
if request.action not in {"runtime-status", "stop"}:
|
|
126
|
+
await self._state.browser_service.begin_command(request.action)
|
|
127
|
+
command_started = True
|
|
128
|
+
data = await handler(request)
|
|
129
|
+
meta = {
|
|
130
|
+
"action": request.action,
|
|
131
|
+
"agent_id": agent_id,
|
|
132
|
+
}
|
|
133
|
+
if command_started:
|
|
134
|
+
runtime_meta = await self._state.browser_service.end_command()
|
|
135
|
+
meta.update(runtime_meta)
|
|
136
|
+
if runtime_meta.get("driver_reason") == "extension-connected":
|
|
137
|
+
meta["runtime_note"] = "Browser CLI restored extension mode at a safe point."
|
|
138
|
+
elif runtime_meta.get("driver_reason") == "workspace-binding-rebuilt":
|
|
139
|
+
meta["runtime_note"] = "Browser CLI rebuilt its owned workspace binding."
|
|
140
|
+
else:
|
|
141
|
+
meta["driver"] = self._state.browser_service.active_driver_name
|
|
142
|
+
chrome_environment = self._state.browser_service.chrome_environment
|
|
143
|
+
if chrome_environment is not None:
|
|
144
|
+
meta["profile_source"] = chrome_environment.source
|
|
145
|
+
return DaemonResponse.success(data, meta=meta)
|
|
146
|
+
except BrowserCliError as exc:
|
|
147
|
+
if command_started:
|
|
148
|
+
runtime_meta = await self._state.browser_service.end_command()
|
|
149
|
+
return self._error_response(exc, request=request)
|
|
150
|
+
except Exception as exc: # pragma: no cover - last-resort daemon guard
|
|
151
|
+
if command_started:
|
|
152
|
+
runtime_meta = await self._state.browser_service.end_command()
|
|
153
|
+
logger.exception("Unexpected daemon failure in action=%s", request.action)
|
|
154
|
+
return self._error_response(
|
|
155
|
+
OperationFailedError(
|
|
156
|
+
f"Unexpected daemon failure: {exc}\n{traceback.format_exc()}",
|
|
157
|
+
error_code=error_codes.INTERNAL_ERROR,
|
|
158
|
+
),
|
|
159
|
+
request=request,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _error_response(self, exc: BrowserCliError, *, request: DaemonRequest) -> DaemonResponse:
|
|
163
|
+
return DaemonResponse.failure(
|
|
164
|
+
error_code=exc.error_code,
|
|
165
|
+
error_message=exc.message,
|
|
166
|
+
meta={
|
|
167
|
+
"action": request.action,
|
|
168
|
+
"agent_id": request.agent_id,
|
|
169
|
+
"driver": self._state.browser_service.active_driver_name,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def _handle_stop(self, request: DaemonRequest) -> dict[str, Any]:
|
|
174
|
+
shutdown = await self._state.browser_service.stop()
|
|
175
|
+
self._state.shutdown_event.set()
|
|
176
|
+
return {"stopped": True, **shutdown}
|
|
177
|
+
|
|
178
|
+
async def _handle_runtime_status(self, request: DaemonRequest) -> dict[str, Any]:
|
|
179
|
+
warmup = bool(request.args.get("warmup"))
|
|
180
|
+
raw_status = await self._state.browser_service.runtime_status(warmup=warmup)
|
|
181
|
+
return {
|
|
182
|
+
**raw_status,
|
|
183
|
+
"presentation": build_runtime_presentation(raw_status),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async def _handle_open(self, request: DaemonRequest) -> dict[str, Any]:
|
|
187
|
+
url = self._require_str(request.args, "url")
|
|
188
|
+
page = await self._state.browser_service.new_tab(url=url)
|
|
189
|
+
await self._state.tabs.add_tab(
|
|
190
|
+
page_id=str(page["page_id"]),
|
|
191
|
+
owner_agent_id=request.agent_id,
|
|
192
|
+
url=str(page["url"]),
|
|
193
|
+
title=str(page["title"]),
|
|
194
|
+
)
|
|
195
|
+
return {"page": page}
|
|
196
|
+
|
|
197
|
+
async def _handle_search(self, request: DaemonRequest) -> dict[str, Any]:
|
|
198
|
+
query = self._require_str(request.args, "query")
|
|
199
|
+
engine = str(request.args.get("engine") or "duckduckgo")
|
|
200
|
+
page = await self._state.browser_service.search(query=query, engine=engine)
|
|
201
|
+
await self._state.tabs.add_tab(
|
|
202
|
+
page_id=str(page["page_id"]),
|
|
203
|
+
owner_agent_id=request.agent_id,
|
|
204
|
+
url=str(page["url"]),
|
|
205
|
+
title=str(page["title"]),
|
|
206
|
+
)
|
|
207
|
+
return {"page": page}
|
|
208
|
+
|
|
209
|
+
async def _handle_new_tab(self, request: DaemonRequest) -> dict[str, Any]:
|
|
210
|
+
url = str(request.args.get("url") or "").strip() or None
|
|
211
|
+
page = await self._state.browser_service.new_tab(url=url)
|
|
212
|
+
await self._state.tabs.add_tab(
|
|
213
|
+
page_id=str(page["page_id"]),
|
|
214
|
+
owner_agent_id=request.agent_id,
|
|
215
|
+
url=str(page["url"]),
|
|
216
|
+
title=str(page["title"]),
|
|
217
|
+
)
|
|
218
|
+
return {"page": page}
|
|
219
|
+
|
|
220
|
+
async def _handle_tabs(self, request: DaemonRequest) -> dict[str, Any]:
|
|
221
|
+
tabs = await self._state.tabs.list_tabs(request.agent_id)
|
|
222
|
+
current_active: str | None = None
|
|
223
|
+
try:
|
|
224
|
+
current_active = (await self._state.tabs.get_active_tab(request.agent_id)).page_id
|
|
225
|
+
except NoActiveTabError:
|
|
226
|
+
current_active = None
|
|
227
|
+
results: list[dict[str, Any]] = []
|
|
228
|
+
for record in tabs:
|
|
229
|
+
try:
|
|
230
|
+
latest = await self._state.browser_service.get_page_summary(record.page_id)
|
|
231
|
+
await self._state.tabs.update_tab(
|
|
232
|
+
record.page_id,
|
|
233
|
+
url=str(latest["url"]),
|
|
234
|
+
title=str(latest["title"]),
|
|
235
|
+
)
|
|
236
|
+
record.url = str(latest["url"])
|
|
237
|
+
record.title = str(latest["title"])
|
|
238
|
+
except BrowserCliError:
|
|
239
|
+
pass
|
|
240
|
+
results.append(
|
|
241
|
+
{
|
|
242
|
+
"page_id": record.page_id,
|
|
243
|
+
"url": record.url,
|
|
244
|
+
"title": record.title,
|
|
245
|
+
"active": record.page_id == current_active,
|
|
246
|
+
"busy": record.busy is not None,
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
return {"tabs": results}
|
|
250
|
+
|
|
251
|
+
async def _handle_switch_tab(self, request: DaemonRequest) -> dict[str, Any]:
|
|
252
|
+
page_id = self._require_str(request.args, "page_id")
|
|
253
|
+
claimed = await self._state.tabs.claim_page(
|
|
254
|
+
agent_id=request.agent_id,
|
|
255
|
+
page_id=page_id,
|
|
256
|
+
request_id=request.request_id,
|
|
257
|
+
command=request.action,
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
await self._state.tabs.set_active_tab(request.agent_id, page_id)
|
|
261
|
+
finally:
|
|
262
|
+
await self._state.tabs.release_tab(
|
|
263
|
+
page_id=claimed.page_id, request_id=request.request_id
|
|
264
|
+
)
|
|
265
|
+
return {"page": await self._state.browser_service.get_page_summary(page_id)}
|
|
266
|
+
|
|
267
|
+
async def _handle_close_tab(self, request: DaemonRequest) -> dict[str, Any]:
|
|
268
|
+
page_id = str(request.args.get("page_id") or "").strip()
|
|
269
|
+
if not page_id:
|
|
270
|
+
page_id = await self._state.tabs.current_active_page_id(request.agent_id)
|
|
271
|
+
claimed = await self._state.tabs.claim_page(
|
|
272
|
+
agent_id=request.agent_id,
|
|
273
|
+
page_id=page_id,
|
|
274
|
+
request_id=request.request_id,
|
|
275
|
+
command=request.action,
|
|
276
|
+
)
|
|
277
|
+
try:
|
|
278
|
+
result = await self._state.browser_service.close_tab(page_id)
|
|
279
|
+
await self._state.tabs.remove_tab(request.agent_id, page_id)
|
|
280
|
+
return result
|
|
281
|
+
finally:
|
|
282
|
+
await self._state.tabs.release_tab(
|
|
283
|
+
page_id=claimed.page_id, request_id=request.request_id
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def _handle_close(self, request: DaemonRequest) -> dict[str, Any]:
|
|
287
|
+
return await self._handle_close_tab(request)
|
|
288
|
+
|
|
289
|
+
async def _handle_info(self, request: DaemonRequest) -> dict[str, Any]:
|
|
290
|
+
page = await self._run_active_page_action(
|
|
291
|
+
request, self._state.browser_service.get_page_info
|
|
292
|
+
)
|
|
293
|
+
return {"page": page}
|
|
294
|
+
|
|
295
|
+
async def _handle_read_page(self, request: DaemonRequest) -> dict[str, Any]:
|
|
296
|
+
url = self._require_str(request.args, "url")
|
|
297
|
+
output_mode = str(request.args.get("output_mode") or "html").strip() or "html"
|
|
298
|
+
scroll_bottom = bool(request.args.get("scroll_bottom"))
|
|
299
|
+
payload = await self._state.browser_service.read_page(
|
|
300
|
+
url=url,
|
|
301
|
+
output_mode=output_mode,
|
|
302
|
+
scroll_bottom=scroll_bottom,
|
|
303
|
+
)
|
|
304
|
+
return payload
|
|
305
|
+
|
|
306
|
+
async def _handle_html(self, request: DaemonRequest) -> dict[str, Any]:
|
|
307
|
+
payload = await self._run_active_page_action(
|
|
308
|
+
request, self._state.browser_service.capture_html
|
|
309
|
+
)
|
|
310
|
+
return payload
|
|
311
|
+
|
|
312
|
+
async def _handle_snapshot(self, request: DaemonRequest) -> dict[str, Any]:
|
|
313
|
+
interactive = bool(request.args.get("interactive"))
|
|
314
|
+
full_page = bool(request.args.get("full_page", True))
|
|
315
|
+
|
|
316
|
+
async def _snapshot(page_id: str) -> dict[str, Any]:
|
|
317
|
+
payload = await self._state.browser_service.capture_snapshot(
|
|
318
|
+
page_id,
|
|
319
|
+
interactive=interactive,
|
|
320
|
+
full_page=full_page,
|
|
321
|
+
)
|
|
322
|
+
refs_summary = payload.get("refs_summary", [])
|
|
323
|
+
ref_ids = {str(item.get("ref")) for item in refs_summary if item.get("ref")}
|
|
324
|
+
await self._state.tabs.update_tab(page_id, last_snapshot_refs=ref_ids)
|
|
325
|
+
await self._state.tabs.update_tab(
|
|
326
|
+
page_id,
|
|
327
|
+
last_snapshot_id=str(payload.get("snapshot_id") or ""),
|
|
328
|
+
last_snapshot_ref_count=len(ref_ids),
|
|
329
|
+
last_snapshot_url=str(payload.get("captured_url") or ""),
|
|
330
|
+
last_snapshot_at=float(payload.get("captured_at") or 0.0),
|
|
331
|
+
)
|
|
332
|
+
return payload
|
|
333
|
+
|
|
334
|
+
return await self._run_active_page_action(request, _snapshot)
|
|
335
|
+
|
|
336
|
+
async def _handle_reload(self, request: DaemonRequest) -> dict[str, Any]:
|
|
337
|
+
payload = await self._run_active_page_action(request, self._state.browser_service.reload)
|
|
338
|
+
return {"page": payload}
|
|
339
|
+
|
|
340
|
+
async def _handle_back(self, request: DaemonRequest) -> dict[str, Any]:
|
|
341
|
+
payload = await self._run_active_page_action(request, self._state.browser_service.go_back)
|
|
342
|
+
return {"page": payload}
|
|
343
|
+
|
|
344
|
+
async def _handle_forward(self, request: DaemonRequest) -> dict[str, Any]:
|
|
345
|
+
payload = await self._run_active_page_action(
|
|
346
|
+
request, self._state.browser_service.go_forward
|
|
347
|
+
)
|
|
348
|
+
return {"page": payload}
|
|
349
|
+
|
|
350
|
+
async def _handle_resize(self, request: DaemonRequest) -> dict[str, Any]:
|
|
351
|
+
width = self._require_int(request.args, "width")
|
|
352
|
+
height = self._require_int(request.args, "height")
|
|
353
|
+
if width <= 0 or height <= 0:
|
|
354
|
+
raise InvalidInputError("width and height must be positive integers.")
|
|
355
|
+
return await self._run_active_page_action(
|
|
356
|
+
request,
|
|
357
|
+
lambda page_id: self._state.browser_service.resize(page_id, width=width, height=height),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
async def _handle_click(self, request: DaemonRequest) -> dict[str, Any]:
|
|
361
|
+
ref = self._require_str(request.args, "ref")
|
|
362
|
+
return await self._run_active_page_action(
|
|
363
|
+
request, lambda page_id: self._state.browser_service.click_ref(page_id, ref)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
async def _handle_double_click(self, request: DaemonRequest) -> dict[str, Any]:
|
|
367
|
+
ref = self._require_str(request.args, "ref")
|
|
368
|
+
return await self._run_active_page_action(
|
|
369
|
+
request, lambda page_id: self._state.browser_service.double_click_ref(page_id, ref)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
async def _handle_hover(self, request: DaemonRequest) -> dict[str, Any]:
|
|
373
|
+
ref = self._require_str(request.args, "ref")
|
|
374
|
+
return await self._run_active_page_action(
|
|
375
|
+
request, lambda page_id: self._state.browser_service.hover_ref(page_id, ref)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
async def _handle_focus(self, request: DaemonRequest) -> dict[str, Any]:
|
|
379
|
+
ref = self._require_str(request.args, "ref")
|
|
380
|
+
return await self._run_active_page_action(
|
|
381
|
+
request, lambda page_id: self._state.browser_service.focus_ref(page_id, ref)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
async def _handle_fill(self, request: DaemonRequest) -> dict[str, Any]:
|
|
385
|
+
ref = self._require_str(request.args, "ref")
|
|
386
|
+
text = self._require_str(request.args, "text")
|
|
387
|
+
submit = bool(request.args.get("submit"))
|
|
388
|
+
return await self._run_active_page_action(
|
|
389
|
+
request,
|
|
390
|
+
lambda page_id: self._state.browser_service.fill_ref(page_id, ref, text, submit=submit),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def _handle_fill_form(self, request: DaemonRequest) -> dict[str, Any]:
|
|
394
|
+
fields = request.args.get("fields") or []
|
|
395
|
+
if not isinstance(fields, list):
|
|
396
|
+
raise InvalidInputError("fill-form expects a JSON array in --fields.")
|
|
397
|
+
submit = bool(request.args.get("submit"))
|
|
398
|
+
return await self._run_active_page_action(
|
|
399
|
+
request,
|
|
400
|
+
lambda page_id: self._state.browser_service.fill_form(page_id, fields, submit=submit),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
async def _handle_select(self, request: DaemonRequest) -> dict[str, Any]:
|
|
404
|
+
ref = self._require_str(request.args, "ref")
|
|
405
|
+
text = self._require_str(request.args, "text")
|
|
406
|
+
return await self._run_active_page_action(
|
|
407
|
+
request, lambda page_id: self._state.browser_service.select_option(page_id, ref, text)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
async def _handle_options(self, request: DaemonRequest) -> dict[str, Any]:
|
|
411
|
+
ref = self._require_str(request.args, "ref")
|
|
412
|
+
return await self._run_active_page_action(
|
|
413
|
+
request, lambda page_id: self._state.browser_service.list_options(page_id, ref)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
async def _handle_check(self, request: DaemonRequest) -> dict[str, Any]:
|
|
417
|
+
ref = self._require_str(request.args, "ref")
|
|
418
|
+
return await self._run_active_page_action(
|
|
419
|
+
request, lambda page_id: self._state.browser_service.check_ref(page_id, ref)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
async def _handle_uncheck(self, request: DaemonRequest) -> dict[str, Any]:
|
|
423
|
+
ref = self._require_str(request.args, "ref")
|
|
424
|
+
return await self._run_active_page_action(
|
|
425
|
+
request, lambda page_id: self._state.browser_service.uncheck_ref(page_id, ref)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
async def _handle_scroll_to(self, request: DaemonRequest) -> dict[str, Any]:
|
|
429
|
+
ref = self._require_str(request.args, "ref")
|
|
430
|
+
return await self._run_active_page_action(
|
|
431
|
+
request, lambda page_id: self._state.browser_service.scroll_to_ref(page_id, ref)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
async def _handle_drag(self, request: DaemonRequest) -> dict[str, Any]:
|
|
435
|
+
start_ref = self._require_str(request.args, "start_ref")
|
|
436
|
+
end_ref = self._require_str(request.args, "end_ref")
|
|
437
|
+
return await self._run_active_page_action(
|
|
438
|
+
request,
|
|
439
|
+
lambda page_id: self._state.browser_service.drag_ref(page_id, start_ref, end_ref),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
async def _handle_upload(self, request: DaemonRequest) -> dict[str, Any]:
|
|
443
|
+
ref = self._require_str(request.args, "ref")
|
|
444
|
+
path = self._require_str(request.args, "path")
|
|
445
|
+
return await self._run_active_page_action(
|
|
446
|
+
request, lambda page_id: self._state.browser_service.upload_file(page_id, ref, path)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
async def _handle_type(self, request: DaemonRequest) -> dict[str, Any]:
|
|
450
|
+
text = self._require_str(request.args, "text")
|
|
451
|
+
submit = bool(request.args.get("submit"))
|
|
452
|
+
return await self._run_active_page_action(
|
|
453
|
+
request,
|
|
454
|
+
lambda page_id: self._state.browser_service.type_text(page_id, text, submit=submit),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
async def _handle_press(self, request: DaemonRequest) -> dict[str, Any]:
|
|
458
|
+
key = self._require_str(request.args, "key")
|
|
459
|
+
return await self._run_active_page_action(
|
|
460
|
+
request, lambda page_id: self._state.browser_service.press_key(page_id, key)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
async def _handle_key_down(self, request: DaemonRequest) -> dict[str, Any]:
|
|
464
|
+
key = self._require_str(request.args, "key")
|
|
465
|
+
return await self._run_active_page_action(
|
|
466
|
+
request, lambda page_id: self._state.browser_service.key_down(page_id, key)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
async def _handle_key_up(self, request: DaemonRequest) -> dict[str, Any]:
|
|
470
|
+
key = self._require_str(request.args, "key")
|
|
471
|
+
return await self._run_active_page_action(
|
|
472
|
+
request, lambda page_id: self._state.browser_service.key_up(page_id, key)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
async def _handle_scroll(self, request: DaemonRequest) -> dict[str, Any]:
|
|
476
|
+
dx = self._optional_int(request.args, "dx") or 0
|
|
477
|
+
dy = self._optional_int(request.args, "dy") or 700
|
|
478
|
+
return await self._run_active_page_action(
|
|
479
|
+
request, lambda page_id: self._state.browser_service.wheel(page_id, dx=dx, dy=dy)
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
async def _handle_mouse_click(self, request: DaemonRequest) -> dict[str, Any]:
|
|
483
|
+
x = self._require_int(request.args, "x")
|
|
484
|
+
y = self._require_int(request.args, "y")
|
|
485
|
+
count = self._optional_int(request.args, "count") or 1
|
|
486
|
+
return await self._run_active_page_action(
|
|
487
|
+
request,
|
|
488
|
+
lambda page_id: self._state.browser_service.mouse_click(
|
|
489
|
+
page_id,
|
|
490
|
+
x=x,
|
|
491
|
+
y=y,
|
|
492
|
+
button=str(request.args.get("button") or "left"),
|
|
493
|
+
count=count,
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
async def _handle_mouse_move(self, request: DaemonRequest) -> dict[str, Any]:
|
|
498
|
+
x = self._require_int(request.args, "x")
|
|
499
|
+
y = self._require_int(request.args, "y")
|
|
500
|
+
return await self._run_active_page_action(
|
|
501
|
+
request,
|
|
502
|
+
lambda page_id: self._state.browser_service.mouse_move(
|
|
503
|
+
page_id,
|
|
504
|
+
x=x,
|
|
505
|
+
y=y,
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
async def _handle_mouse_drag(self, request: DaemonRequest) -> dict[str, Any]:
|
|
510
|
+
x1 = self._require_int(request.args, "x1")
|
|
511
|
+
y1 = self._require_int(request.args, "y1")
|
|
512
|
+
x2 = self._require_int(request.args, "x2")
|
|
513
|
+
y2 = self._require_int(request.args, "y2")
|
|
514
|
+
return await self._run_active_page_action(
|
|
515
|
+
request,
|
|
516
|
+
lambda page_id: self._state.browser_service.mouse_drag(
|
|
517
|
+
page_id,
|
|
518
|
+
x1=x1,
|
|
519
|
+
y1=y1,
|
|
520
|
+
x2=x2,
|
|
521
|
+
y2=y2,
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
async def _handle_mouse_down(self, request: DaemonRequest) -> dict[str, Any]:
|
|
526
|
+
return await self._run_active_page_action(
|
|
527
|
+
request,
|
|
528
|
+
lambda page_id: self._state.browser_service.mouse_down(
|
|
529
|
+
page_id,
|
|
530
|
+
button=str(request.args.get("button") or "left"),
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
async def _handle_mouse_up(self, request: DaemonRequest) -> dict[str, Any]:
|
|
535
|
+
return await self._run_active_page_action(
|
|
536
|
+
request,
|
|
537
|
+
lambda page_id: self._state.browser_service.mouse_up(
|
|
538
|
+
page_id,
|
|
539
|
+
button=str(request.args.get("button") or "left"),
|
|
540
|
+
),
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
async def _handle_eval(self, request: DaemonRequest) -> dict[str, Any]:
|
|
544
|
+
code = self._require_str(request.args, "code")
|
|
545
|
+
return await self._run_active_page_action(
|
|
546
|
+
request, lambda page_id: self._state.browser_service.evaluate(page_id, code)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def _handle_eval_on(self, request: DaemonRequest) -> dict[str, Any]:
|
|
550
|
+
ref = self._require_str(request.args, "ref")
|
|
551
|
+
code = self._require_str(request.args, "code")
|
|
552
|
+
return await self._run_active_page_action(
|
|
553
|
+
request, lambda page_id: self._state.browser_service.evaluate_on_ref(page_id, ref, code)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
async def _handle_wait(self, request: DaemonRequest) -> dict[str, Any]:
|
|
557
|
+
seconds = self._optional_float(request.args, "seconds")
|
|
558
|
+
text = request.args.get("text")
|
|
559
|
+
gone = bool(request.args.get("gone"))
|
|
560
|
+
exact = bool(request.args.get("exact"))
|
|
561
|
+
return await self._run_active_page_action(
|
|
562
|
+
request,
|
|
563
|
+
lambda page_id: self._state.browser_service.wait(
|
|
564
|
+
page_id,
|
|
565
|
+
seconds=seconds,
|
|
566
|
+
text=str(text) if text else None,
|
|
567
|
+
gone=gone,
|
|
568
|
+
exact=exact,
|
|
569
|
+
),
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
async def _handle_wait_network(self, request: DaemonRequest) -> dict[str, Any]:
|
|
573
|
+
timeout_seconds = self._optional_float(request.args, "timeout") or 30.0
|
|
574
|
+
return await self._run_active_page_action(
|
|
575
|
+
request,
|
|
576
|
+
lambda page_id: self._state.browser_service.wait_for_network_idle(
|
|
577
|
+
page_id, timeout_seconds=timeout_seconds
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
async def _handle_screenshot(self, request: DaemonRequest) -> dict[str, Any]:
|
|
582
|
+
path = self._require_str(request.args, "path")
|
|
583
|
+
full_page = bool(request.args.get("full_page"))
|
|
584
|
+
return await self._run_active_page_action(
|
|
585
|
+
request,
|
|
586
|
+
lambda page_id: self._state.browser_service.screenshot(
|
|
587
|
+
page_id, path=path, full_page=full_page
|
|
588
|
+
),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
async def _handle_pdf(self, request: DaemonRequest) -> dict[str, Any]:
|
|
592
|
+
path = self._require_str(request.args, "path")
|
|
593
|
+
return await self._run_active_page_action(
|
|
594
|
+
request, lambda page_id: self._state.browser_service.save_pdf(page_id, path=path)
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
async def _handle_console_start(self, request: DaemonRequest) -> dict[str, Any]:
|
|
598
|
+
return await self._run_active_page_action(
|
|
599
|
+
request, self._state.browser_service.start_console_capture
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
async def _handle_console(self, request: DaemonRequest) -> dict[str, Any]:
|
|
603
|
+
message_type = str(request.args.get("message_type") or "").strip() or None
|
|
604
|
+
clear = not bool(request.args.get("no_clear"))
|
|
605
|
+
return await self._run_active_page_action(
|
|
606
|
+
request,
|
|
607
|
+
lambda page_id: self._state.browser_service.get_console_messages(
|
|
608
|
+
page_id, message_type=message_type, clear=clear
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
async def _handle_console_stop(self, request: DaemonRequest) -> dict[str, Any]:
|
|
613
|
+
return await self._run_active_page_action(
|
|
614
|
+
request, self._state.browser_service.stop_console_capture
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
async def _handle_network_start(self, request: DaemonRequest) -> dict[str, Any]:
|
|
618
|
+
return await self._run_active_page_action(
|
|
619
|
+
request, self._state.browser_service.start_network_capture
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
async def _handle_network_wait(self, request: DaemonRequest) -> dict[str, Any]:
|
|
623
|
+
filters = self._network_filters_from_request(request.args)
|
|
624
|
+
timeout_seconds = self._optional_float(request.args, "timeout_seconds") or 30.0
|
|
625
|
+
return await self._run_active_page_action(
|
|
626
|
+
request,
|
|
627
|
+
lambda page_id: self._state.browser_service.wait_for_network_record(
|
|
628
|
+
page_id,
|
|
629
|
+
**filters,
|
|
630
|
+
timeout_seconds=timeout_seconds,
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
async def _handle_network(self, request: DaemonRequest) -> dict[str, Any]:
|
|
635
|
+
filters = self._network_filters_from_request(request.args)
|
|
636
|
+
clear = not bool(request.args.get("no_clear"))
|
|
637
|
+
return await self._run_active_page_action(
|
|
638
|
+
request,
|
|
639
|
+
lambda page_id: self._state.browser_service.get_network_records(
|
|
640
|
+
page_id, clear=clear, **filters
|
|
641
|
+
),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
async def _handle_network_stop(self, request: DaemonRequest) -> dict[str, Any]:
|
|
645
|
+
return await self._run_active_page_action(
|
|
646
|
+
request, self._state.browser_service.stop_network_capture
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
async def _handle_dialog_setup(self, request: DaemonRequest) -> dict[str, Any]:
|
|
650
|
+
action = str(request.args.get("action") or "accept")
|
|
651
|
+
if action not in {"accept", "dismiss"}:
|
|
652
|
+
raise InvalidInputError("action must be accept or dismiss.")
|
|
653
|
+
return await self._run_active_page_action(
|
|
654
|
+
request,
|
|
655
|
+
lambda page_id: self._state.browser_service.setup_dialog_handler(
|
|
656
|
+
page_id,
|
|
657
|
+
default_action=action,
|
|
658
|
+
default_prompt_text=self._optional_str(request.args, "text"),
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
async def _handle_dialog(self, request: DaemonRequest) -> dict[str, Any]:
|
|
663
|
+
accept = not bool(request.args.get("dismiss"))
|
|
664
|
+
return await self._run_active_page_action(
|
|
665
|
+
request,
|
|
666
|
+
lambda page_id: self._state.browser_service.handle_dialog(
|
|
667
|
+
page_id,
|
|
668
|
+
accept=accept,
|
|
669
|
+
prompt_text=self._optional_str(request.args, "text"),
|
|
670
|
+
),
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
async def _handle_dialog_remove(self, request: DaemonRequest) -> dict[str, Any]:
|
|
674
|
+
return await self._run_active_page_action(
|
|
675
|
+
request, self._state.browser_service.remove_dialog_handler
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
async def _handle_cookies(self, request: DaemonRequest) -> dict[str, Any]:
|
|
679
|
+
return await self._run_active_page_action(
|
|
680
|
+
request,
|
|
681
|
+
lambda page_id: self._state.browser_service.get_cookies(
|
|
682
|
+
page_id,
|
|
683
|
+
name=self._optional_str(request.args, "name"),
|
|
684
|
+
domain=self._optional_str(request.args, "domain"),
|
|
685
|
+
path=self._optional_str(request.args, "path"),
|
|
686
|
+
),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
async def _handle_cookie_set(self, request: DaemonRequest) -> dict[str, Any]:
|
|
690
|
+
return await self._run_active_page_action(
|
|
691
|
+
request,
|
|
692
|
+
lambda page_id: self._state.browser_service.set_cookie(
|
|
693
|
+
page_id,
|
|
694
|
+
name=self._require_str(request.args, "name"),
|
|
695
|
+
value=self._require_str(request.args, "value"),
|
|
696
|
+
domain=self._optional_str(request.args, "domain"),
|
|
697
|
+
path=str(request.args.get("path") or "/"),
|
|
698
|
+
expires=self._optional_float(request.args, "expires"),
|
|
699
|
+
http_only=bool(request.args.get("http_only")),
|
|
700
|
+
secure=bool(request.args.get("secure")),
|
|
701
|
+
same_site=self._optional_str(request.args, "same_site"),
|
|
702
|
+
),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
async def _handle_cookies_clear(self, request: DaemonRequest) -> dict[str, Any]:
|
|
706
|
+
return await self._run_active_page_action(
|
|
707
|
+
request,
|
|
708
|
+
lambda page_id: self._state.browser_service.clear_cookies(
|
|
709
|
+
page_id,
|
|
710
|
+
name=self._optional_str(request.args, "name"),
|
|
711
|
+
domain=self._optional_str(request.args, "domain"),
|
|
712
|
+
path=self._optional_str(request.args, "path"),
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
async def _handle_storage_save(self, request: DaemonRequest) -> dict[str, Any]:
|
|
717
|
+
return await self._run_active_page_action(
|
|
718
|
+
request,
|
|
719
|
+
lambda page_id: self._state.browser_service.save_storage_state(
|
|
720
|
+
page_id,
|
|
721
|
+
path=self._optional_str(request.args, "path"),
|
|
722
|
+
),
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
async def _handle_storage_load(self, request: DaemonRequest) -> dict[str, Any]:
|
|
726
|
+
path = self._require_str(request.args, "path")
|
|
727
|
+
return await self._run_active_page_action(
|
|
728
|
+
request,
|
|
729
|
+
lambda page_id: self._state.browser_service.load_storage_state(page_id, path=path),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
async def _handle_verify_text(self, request: DaemonRequest) -> dict[str, Any]:
|
|
733
|
+
text = self._require_str(request.args, "text")
|
|
734
|
+
exact = bool(request.args.get("exact"))
|
|
735
|
+
timeout = self._optional_float(request.args, "timeout") or 5.0
|
|
736
|
+
return await self._run_active_page_action(
|
|
737
|
+
request,
|
|
738
|
+
lambda page_id: self._state.browser_service.verify_text(
|
|
739
|
+
page_id, text=text, exact=exact, timeout_seconds=timeout
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
async def _handle_verify_visible(self, request: DaemonRequest) -> dict[str, Any]:
|
|
744
|
+
role = self._require_str(request.args, "role")
|
|
745
|
+
name = self._require_str(request.args, "name")
|
|
746
|
+
timeout = self._optional_float(request.args, "timeout") or 5.0
|
|
747
|
+
return await self._run_active_page_action(
|
|
748
|
+
request,
|
|
749
|
+
lambda page_id: self._state.browser_service.verify_visible(
|
|
750
|
+
page_id, role=role, name=name, timeout_seconds=timeout
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
async def _handle_verify_url(self, request: DaemonRequest) -> dict[str, Any]:
|
|
755
|
+
expected = self._require_str(request.args, "expected")
|
|
756
|
+
exact = bool(request.args.get("exact"))
|
|
757
|
+
return await self._run_active_page_action(
|
|
758
|
+
request,
|
|
759
|
+
lambda page_id: self._state.browser_service.verify_url(
|
|
760
|
+
page_id, expected=expected, exact=exact
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
async def _handle_verify_title(self, request: DaemonRequest) -> dict[str, Any]:
|
|
765
|
+
expected = self._require_str(request.args, "expected")
|
|
766
|
+
exact = bool(request.args.get("exact"))
|
|
767
|
+
return await self._run_active_page_action(
|
|
768
|
+
request,
|
|
769
|
+
lambda page_id: self._state.browser_service.verify_title(
|
|
770
|
+
page_id, expected=expected, exact=exact
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
async def _handle_verify_state(self, request: DaemonRequest) -> dict[str, Any]:
|
|
775
|
+
ref = self._require_str(request.args, "ref")
|
|
776
|
+
state = self._require_str(request.args, "state")
|
|
777
|
+
return await self._run_active_page_action(
|
|
778
|
+
request,
|
|
779
|
+
lambda page_id: self._state.browser_service.verify_state(page_id, ref=ref, state=state),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
async def _handle_verify_value(self, request: DaemonRequest) -> dict[str, Any]:
|
|
783
|
+
ref = self._require_str(request.args, "ref")
|
|
784
|
+
expected = self._require_str(request.args, "expected")
|
|
785
|
+
return await self._run_active_page_action(
|
|
786
|
+
request,
|
|
787
|
+
lambda page_id: self._state.browser_service.verify_value(
|
|
788
|
+
page_id, ref=ref, expected=expected
|
|
789
|
+
),
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
async def _handle_trace_start(self, request: DaemonRequest) -> dict[str, Any]:
|
|
793
|
+
screenshots = not bool(request.args.get("no_screenshots"))
|
|
794
|
+
snapshots = not bool(request.args.get("no_snapshots"))
|
|
795
|
+
sources = bool(request.args.get("sources"))
|
|
796
|
+
return await self._run_active_page_action(
|
|
797
|
+
request,
|
|
798
|
+
lambda page_id: self._state.browser_service.start_tracing(
|
|
799
|
+
page_id,
|
|
800
|
+
screenshots=screenshots,
|
|
801
|
+
snapshots=snapshots,
|
|
802
|
+
sources=sources,
|
|
803
|
+
),
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
async def _handle_trace_chunk(self, request: DaemonRequest) -> dict[str, Any]:
|
|
807
|
+
title = self._optional_str(request.args, "title")
|
|
808
|
+
return await self._run_active_page_action(
|
|
809
|
+
request,
|
|
810
|
+
lambda page_id: self._state.browser_service.add_trace_chunk(page_id, title=title),
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
async def _handle_trace_stop(self, request: DaemonRequest) -> dict[str, Any]:
|
|
814
|
+
path = self._optional_str(request.args, "path")
|
|
815
|
+
return await self._run_active_page_action(
|
|
816
|
+
request,
|
|
817
|
+
lambda page_id: self._state.browser_service.stop_tracing(page_id, path=path),
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
async def _handle_video_start(self, request: DaemonRequest) -> dict[str, Any]:
|
|
821
|
+
width = self._optional_int(request.args, "width")
|
|
822
|
+
height = self._optional_int(request.args, "height")
|
|
823
|
+
return await self._run_active_page_action(
|
|
824
|
+
request,
|
|
825
|
+
lambda page_id: self._state.browser_service.start_video(
|
|
826
|
+
page_id,
|
|
827
|
+
width=width,
|
|
828
|
+
height=height,
|
|
829
|
+
),
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
async def _handle_video_stop(self, request: DaemonRequest) -> dict[str, Any]:
|
|
833
|
+
path = self._optional_str(request.args, "path")
|
|
834
|
+
return await self._run_active_page_action(
|
|
835
|
+
request,
|
|
836
|
+
lambda page_id: self._state.browser_service.stop_video(page_id, path=path),
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
async def _run_active_page_action(
|
|
840
|
+
self,
|
|
841
|
+
request: DaemonRequest,
|
|
842
|
+
operation: Callable[[str], Awaitable[dict[str, Any]]],
|
|
843
|
+
) -> dict[str, Any]:
|
|
844
|
+
async with self._state.tabs.claim_active_tab(
|
|
845
|
+
agent_id=request.agent_id,
|
|
846
|
+
request_id=request.request_id,
|
|
847
|
+
command=request.action,
|
|
848
|
+
) as tab:
|
|
849
|
+
payload = await operation(tab.page_id)
|
|
850
|
+
try:
|
|
851
|
+
latest = await self._state.browser_service.get_page_summary(tab.page_id)
|
|
852
|
+
await self._state.tabs.update_tab(
|
|
853
|
+
tab.page_id,
|
|
854
|
+
url=str(latest["url"]),
|
|
855
|
+
title=str(latest["title"]),
|
|
856
|
+
)
|
|
857
|
+
except BrowserCliError:
|
|
858
|
+
pass
|
|
859
|
+
return payload
|
|
860
|
+
|
|
861
|
+
@staticmethod
|
|
862
|
+
def _require_str(args: dict[str, Any], key: str) -> str:
|
|
863
|
+
value = str(args.get(key) or "").strip()
|
|
864
|
+
if not value:
|
|
865
|
+
raise InvalidInputError(f"{key} is required.")
|
|
866
|
+
return value
|
|
867
|
+
|
|
868
|
+
@staticmethod
|
|
869
|
+
def _optional_str(args: dict[str, Any], key: str) -> str | None:
|
|
870
|
+
value = str(args.get(key) or "").strip()
|
|
871
|
+
return value or None
|
|
872
|
+
|
|
873
|
+
@classmethod
|
|
874
|
+
def _require_int(cls, args: dict[str, Any], key: str) -> int:
|
|
875
|
+
raw = args.get(key)
|
|
876
|
+
if raw is None or str(raw).strip() == "":
|
|
877
|
+
raise InvalidInputError(f"{key} is required.")
|
|
878
|
+
try:
|
|
879
|
+
return int(raw)
|
|
880
|
+
except (TypeError, ValueError) as exc:
|
|
881
|
+
raise InvalidInputError(f"{key} must be an integer.") from exc
|
|
882
|
+
|
|
883
|
+
@classmethod
|
|
884
|
+
def _optional_int(cls, args: dict[str, Any], key: str) -> int | None:
|
|
885
|
+
raw = args.get(key)
|
|
886
|
+
if raw is None or str(raw).strip() == "":
|
|
887
|
+
return None
|
|
888
|
+
try:
|
|
889
|
+
return int(raw)
|
|
890
|
+
except (TypeError, ValueError) as exc:
|
|
891
|
+
raise InvalidInputError(f"{key} must be an integer.") from exc
|
|
892
|
+
|
|
893
|
+
@classmethod
|
|
894
|
+
def _require_float(cls, args: dict[str, Any], key: str) -> float:
|
|
895
|
+
raw = args.get(key)
|
|
896
|
+
if raw is None or str(raw).strip() == "":
|
|
897
|
+
raise InvalidInputError(f"{key} is required.")
|
|
898
|
+
try:
|
|
899
|
+
return float(raw)
|
|
900
|
+
except (TypeError, ValueError) as exc:
|
|
901
|
+
raise InvalidInputError(f"{key} must be a number.") from exc
|
|
902
|
+
|
|
903
|
+
@classmethod
|
|
904
|
+
def _optional_float(cls, args: dict[str, Any], key: str) -> float | None:
|
|
905
|
+
raw = args.get(key)
|
|
906
|
+
if raw is None or str(raw).strip() == "":
|
|
907
|
+
return None
|
|
908
|
+
try:
|
|
909
|
+
return float(raw)
|
|
910
|
+
except (TypeError, ValueError) as exc:
|
|
911
|
+
raise InvalidInputError(f"{key} must be a number.") from exc
|
|
912
|
+
|
|
913
|
+
@classmethod
|
|
914
|
+
def _network_filters_from_request(cls, args: dict[str, Any]) -> dict[str, Any]:
|
|
915
|
+
return {
|
|
916
|
+
"url_contains": cls._optional_str(args, "url_contains"),
|
|
917
|
+
"url_regex": cls._optional_str(args, "url_regex"),
|
|
918
|
+
"method": cls._optional_str(args, "method"),
|
|
919
|
+
"status": cls._optional_int(args, "status"),
|
|
920
|
+
"resource_type": cls._optional_str(args, "resource_type"),
|
|
921
|
+
"mime_contains": cls._optional_str(args, "mime_contains"),
|
|
922
|
+
"include_static": bool(args.get("include_static")),
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
def _maybe_configure_browser_environment(self, args: dict[str, Any]) -> None:
|
|
926
|
+
chrome_environment_payload = args.get("chrome_environment")
|
|
927
|
+
if not isinstance(chrome_environment_payload, dict):
|
|
928
|
+
return
|
|
929
|
+
executable_path = self._optional_str(chrome_environment_payload, "executable_path")
|
|
930
|
+
self._state.browser_service.configure_environment(
|
|
931
|
+
ChromeEnvironment(
|
|
932
|
+
executable_path=Path(executable_path) if executable_path else None,
|
|
933
|
+
user_data_dir=Path(self._require_str(chrome_environment_payload, "user_data_dir")),
|
|
934
|
+
profile_directory=str(
|
|
935
|
+
chrome_environment_payload.get("profile_directory") or "Default"
|
|
936
|
+
),
|
|
937
|
+
profile_name=self._optional_str(chrome_environment_payload, "profile_name"),
|
|
938
|
+
source=str(chrome_environment_payload.get("source") or "chrome"),
|
|
939
|
+
fallback_reason=self._optional_str(chrome_environment_payload, "fallback_reason"),
|
|
940
|
+
)
|
|
941
|
+
)
|