optexity-browser-use 0.9.5__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_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""Event definitions for browser communication."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from bubus import BaseEvent
|
|
8
|
+
from bubus.models import T_EventResultType
|
|
9
|
+
from cdp_use.cdp.target import TargetID
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
from browser_use.browser.views import BrowserStateSummary
|
|
13
|
+
from browser_use.dom.views import EnhancedDOMTreeNode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_timeout(env_var: str, default: float) -> float | None:
|
|
17
|
+
"""
|
|
18
|
+
Safely parse environment variable timeout values with robust error handling.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
env_var: Environment variable name (e.g. 'TIMEOUT_NavigateToUrlEvent')
|
|
22
|
+
default: Default timeout value as float (e.g. 15.0)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Parsed float value or the default if parsing fails
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: Only if both env_var and default are invalid (should not happen with valid defaults)
|
|
29
|
+
"""
|
|
30
|
+
# Try environment variable first
|
|
31
|
+
env_value = os.getenv(env_var)
|
|
32
|
+
if env_value:
|
|
33
|
+
try:
|
|
34
|
+
parsed = float(env_value)
|
|
35
|
+
if parsed < 0:
|
|
36
|
+
print(f'Warning: {env_var}={env_value} is negative, using default {default}')
|
|
37
|
+
return default
|
|
38
|
+
return parsed
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
print(f'Warning: {env_var}={env_value} is not a valid number, using default {default}')
|
|
41
|
+
|
|
42
|
+
# Fall back to default
|
|
43
|
+
return default
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# Agent/Tools -> BrowserSession Events (High-level browser actions)
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ElementSelectedEvent(BaseEvent[T_EventResultType]):
|
|
52
|
+
"""An element was selected."""
|
|
53
|
+
|
|
54
|
+
node: EnhancedDOMTreeNode
|
|
55
|
+
|
|
56
|
+
@field_validator('node', mode='before')
|
|
57
|
+
@classmethod
|
|
58
|
+
def serialize_node(cls, data: EnhancedDOMTreeNode | None) -> EnhancedDOMTreeNode | None:
|
|
59
|
+
if data is None:
|
|
60
|
+
return None
|
|
61
|
+
return EnhancedDOMTreeNode(
|
|
62
|
+
node_id=data.node_id,
|
|
63
|
+
backend_node_id=data.backend_node_id,
|
|
64
|
+
session_id=data.session_id,
|
|
65
|
+
frame_id=data.frame_id,
|
|
66
|
+
target_id=data.target_id,
|
|
67
|
+
node_type=data.node_type,
|
|
68
|
+
node_name=data.node_name,
|
|
69
|
+
node_value=data.node_value,
|
|
70
|
+
attributes=data.attributes,
|
|
71
|
+
is_scrollable=data.is_scrollable,
|
|
72
|
+
is_visible=data.is_visible,
|
|
73
|
+
absolute_position=data.absolute_position,
|
|
74
|
+
# override the circular reference fields in EnhancedDOMTreeNode as they cant be serialized and aren't needed by event handlers
|
|
75
|
+
# only used internally by the DOM service during DOM tree building process, not intended public API use
|
|
76
|
+
content_document=None,
|
|
77
|
+
shadow_root_type=None,
|
|
78
|
+
shadow_roots=[],
|
|
79
|
+
parent_node=None,
|
|
80
|
+
children_nodes=[],
|
|
81
|
+
ax_node=None,
|
|
82
|
+
snapshot_node=None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# TODO: add page handle to events
|
|
87
|
+
# class PageHandle(share a base with browser.session.CDPSession?):
|
|
88
|
+
# url: str
|
|
89
|
+
# target_id: TargetID
|
|
90
|
+
# @classmethod
|
|
91
|
+
# def from_target_id(cls, target_id: TargetID) -> Self:
|
|
92
|
+
# return cls(target_id=target_id)
|
|
93
|
+
# @classmethod
|
|
94
|
+
# def from_target_id(cls, target_id: TargetID) -> Self:
|
|
95
|
+
# return cls(target_id=target_id)
|
|
96
|
+
# @classmethod
|
|
97
|
+
# def from_url(cls, url: str) -> Self:
|
|
98
|
+
# @property
|
|
99
|
+
# def root_frame_id(self) -> str:
|
|
100
|
+
# return self.target_id
|
|
101
|
+
# @property
|
|
102
|
+
# def session_id(self) -> str:
|
|
103
|
+
# return browser_session.get_or_create_cdp_session(self.target_id).session_id
|
|
104
|
+
|
|
105
|
+
# class PageSelectedEvent(BaseEvent[T_EventResultType]):
|
|
106
|
+
# """An event like SwitchToTabEvent(page=PageHandle) or CloseTabEvent(page=PageHandle)"""
|
|
107
|
+
# page: PageHandle
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class NavigateToUrlEvent(BaseEvent[None]):
|
|
111
|
+
"""Navigate to a specific URL."""
|
|
112
|
+
|
|
113
|
+
url: str
|
|
114
|
+
wait_until: Literal['load', 'domcontentloaded', 'networkidle', 'commit'] = 'load'
|
|
115
|
+
timeout_ms: int | None = None
|
|
116
|
+
new_tab: bool = Field(
|
|
117
|
+
default=False, description='Set True to leave the current tab alone and open a new tab in the foreground for the new URL'
|
|
118
|
+
)
|
|
119
|
+
# existing_tab: PageHandle | None = None # TODO
|
|
120
|
+
|
|
121
|
+
# time limits enforced by bubus, not exposed to LLM:
|
|
122
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_NavigateToUrlEvent', 15.0) # seconds
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ClickElementEvent(ElementSelectedEvent[dict[str, Any] | None]):
|
|
126
|
+
"""Click an element."""
|
|
127
|
+
|
|
128
|
+
node: 'EnhancedDOMTreeNode'
|
|
129
|
+
button: Literal['left', 'right', 'middle'] = 'left'
|
|
130
|
+
# click_count: int = 1 # TODO
|
|
131
|
+
# expect_download: bool = False # moved to downloads_watchdog.py
|
|
132
|
+
|
|
133
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_ClickElementEvent', 15.0) # seconds
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TypeTextEvent(ElementSelectedEvent[dict | None]):
|
|
137
|
+
"""Type text into an element."""
|
|
138
|
+
|
|
139
|
+
node: 'EnhancedDOMTreeNode'
|
|
140
|
+
text: str
|
|
141
|
+
clear: bool = True
|
|
142
|
+
is_sensitive: bool = False # Flag to indicate if text contains sensitive data
|
|
143
|
+
sensitive_key_name: str | None = None # Name of the sensitive key being typed (e.g., 'username', 'password')
|
|
144
|
+
|
|
145
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_TypeTextEvent', 15.0) # seconds
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ScrollEvent(ElementSelectedEvent[None]):
|
|
149
|
+
"""Scroll the page or element."""
|
|
150
|
+
|
|
151
|
+
direction: Literal['up', 'down', 'left', 'right']
|
|
152
|
+
amount: int # pixels
|
|
153
|
+
node: 'EnhancedDOMTreeNode | None' = None # None means scroll page
|
|
154
|
+
|
|
155
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_ScrollEvent', 8.0) # seconds
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SwitchTabEvent(BaseEvent[TargetID]):
|
|
159
|
+
"""Switch to a different tab."""
|
|
160
|
+
|
|
161
|
+
target_id: TargetID | None = Field(default=None, description='None means switch to the most recently opened tab')
|
|
162
|
+
|
|
163
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_SwitchTabEvent', 10.0) # seconds
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class CloseTabEvent(BaseEvent[None]):
|
|
167
|
+
"""Close a tab."""
|
|
168
|
+
|
|
169
|
+
target_id: TargetID
|
|
170
|
+
|
|
171
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_CloseTabEvent', 10.0) # seconds
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ScreenshotEvent(BaseEvent[str]):
|
|
175
|
+
"""Request to take a screenshot."""
|
|
176
|
+
|
|
177
|
+
full_page: bool = False
|
|
178
|
+
clip: dict[str, float] | None = None # {x, y, width, height}
|
|
179
|
+
|
|
180
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_ScreenshotEvent', 8.0) # seconds
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class BrowserStateRequestEvent(BaseEvent[BrowserStateSummary]):
|
|
184
|
+
"""Request current browser state."""
|
|
185
|
+
|
|
186
|
+
include_dom: bool = True
|
|
187
|
+
include_screenshot: bool = True
|
|
188
|
+
include_recent_events: bool = False
|
|
189
|
+
|
|
190
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserStateRequestEvent', 30.0) # seconds
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# class WaitForConditionEvent(BaseEvent):
|
|
194
|
+
# """Wait for a condition."""
|
|
195
|
+
|
|
196
|
+
# condition: Literal['navigation', 'selector', 'timeout', 'load_state']
|
|
197
|
+
# timeout: float = 30000
|
|
198
|
+
# selector: str | None = None
|
|
199
|
+
# state: Literal['attached', 'detached', 'visible', 'hidden'] | None = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class GoBackEvent(BaseEvent[None]):
|
|
203
|
+
"""Navigate back in browser history."""
|
|
204
|
+
|
|
205
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_GoBackEvent', 15.0) # seconds
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class GoForwardEvent(BaseEvent[None]):
|
|
209
|
+
"""Navigate forward in browser history."""
|
|
210
|
+
|
|
211
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_GoForwardEvent', 15.0) # seconds
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class RefreshEvent(BaseEvent[None]):
|
|
215
|
+
"""Refresh/reload the current page."""
|
|
216
|
+
|
|
217
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_RefreshEvent', 15.0) # seconds
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class WaitEvent(BaseEvent[None]):
|
|
221
|
+
"""Wait for a specified number of seconds."""
|
|
222
|
+
|
|
223
|
+
seconds: float = 3.0
|
|
224
|
+
max_seconds: float = 10.0 # Safety cap
|
|
225
|
+
|
|
226
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_WaitEvent', 60.0) # seconds
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class SendKeysEvent(BaseEvent[None]):
|
|
230
|
+
"""Send keyboard keys/shortcuts."""
|
|
231
|
+
|
|
232
|
+
keys: str # e.g., "ctrl+a", "cmd+c", "Enter"
|
|
233
|
+
|
|
234
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_SendKeysEvent', 15.0) # seconds
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class UploadFileEvent(ElementSelectedEvent[None]):
|
|
238
|
+
"""Upload a file to an element."""
|
|
239
|
+
|
|
240
|
+
node: 'EnhancedDOMTreeNode'
|
|
241
|
+
file_path: str
|
|
242
|
+
|
|
243
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_UploadFileEvent', 30.0) # seconds
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class GetDropdownOptionsEvent(ElementSelectedEvent[dict[str, str]]):
|
|
247
|
+
"""Get all options from any dropdown (native <select>, ARIA menus, or custom dropdowns).
|
|
248
|
+
|
|
249
|
+
Returns a dict containing dropdown type, options list, and element metadata."""
|
|
250
|
+
|
|
251
|
+
node: 'EnhancedDOMTreeNode'
|
|
252
|
+
|
|
253
|
+
event_timeout: float | None = _get_timeout(
|
|
254
|
+
'TIMEOUT_GetDropdownOptionsEvent',
|
|
255
|
+
15.0,
|
|
256
|
+
) # some dropdowns lazy-load the list of options on first interaction, so we need to wait for them to load (e.g. table filter lists can have thousands of options)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class SelectDropdownOptionEvent(ElementSelectedEvent[dict[str, str]]):
|
|
260
|
+
"""Select a dropdown option by exact text from any dropdown type.
|
|
261
|
+
|
|
262
|
+
Returns a dict containing success status and selection details."""
|
|
263
|
+
|
|
264
|
+
node: 'EnhancedDOMTreeNode'
|
|
265
|
+
text: str # The option text to select
|
|
266
|
+
|
|
267
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_SelectDropdownOptionEvent', 8.0) # seconds
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ScrollToTextEvent(BaseEvent[None]):
|
|
271
|
+
"""Scroll to specific text on the page. Raises exception if text not found."""
|
|
272
|
+
|
|
273
|
+
text: str
|
|
274
|
+
direction: Literal['up', 'down'] = 'down'
|
|
275
|
+
|
|
276
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_ScrollToTextEvent', 15.0) # seconds
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ============================================================================
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class BrowserStartEvent(BaseEvent):
|
|
283
|
+
"""Start/connect to browser."""
|
|
284
|
+
|
|
285
|
+
cdp_url: str | None = None
|
|
286
|
+
launch_options: dict[str, Any] = Field(default_factory=dict)
|
|
287
|
+
|
|
288
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserStartEvent', 30.0) # seconds
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class BrowserStopEvent(BaseEvent):
|
|
292
|
+
"""Stop/disconnect from browser."""
|
|
293
|
+
|
|
294
|
+
force: bool = False
|
|
295
|
+
|
|
296
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserStopEvent', 45.0) # seconds
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class BrowserLaunchResult(BaseModel):
|
|
300
|
+
"""Result of launching a browser."""
|
|
301
|
+
|
|
302
|
+
# TODO: add browser executable_path, pid, version, latency, user_data_dir, X11 $DISPLAY, host IP address, etc.
|
|
303
|
+
cdp_url: str
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class BrowserLaunchEvent(BaseEvent[BrowserLaunchResult]):
|
|
307
|
+
"""Launch a local browser process."""
|
|
308
|
+
|
|
309
|
+
# TODO: add executable_path, proxy settings, preferences, extra launch args, etc.
|
|
310
|
+
|
|
311
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserLaunchEvent', 30.0) # seconds
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class BrowserKillEvent(BaseEvent):
|
|
315
|
+
"""Kill local browser subprocess."""
|
|
316
|
+
|
|
317
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserKillEvent', 30.0) # seconds
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# TODO: replace all Runtime.evaluate() calls with this event
|
|
321
|
+
# class ExecuteJavaScriptEvent(BaseEvent):
|
|
322
|
+
# """Execute JavaScript in page context."""
|
|
323
|
+
|
|
324
|
+
# target_id: TargetID
|
|
325
|
+
# expression: str
|
|
326
|
+
# await_promise: bool = True
|
|
327
|
+
|
|
328
|
+
# event_timeout: float | None = 60.0 # seconds
|
|
329
|
+
|
|
330
|
+
# TODO: add this and use the old BrowserProfile.viewport options to set it
|
|
331
|
+
# class SetViewportEvent(BaseEvent):
|
|
332
|
+
# """Set the viewport size."""
|
|
333
|
+
|
|
334
|
+
# width: int
|
|
335
|
+
# height: int
|
|
336
|
+
# device_scale_factor: float = 1.0
|
|
337
|
+
|
|
338
|
+
# event_timeout: float | None = 15.0 # seconds
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# Moved to storage state
|
|
342
|
+
# class SetCookiesEvent(BaseEvent):
|
|
343
|
+
# """Set browser cookies."""
|
|
344
|
+
|
|
345
|
+
# cookies: list[dict[str, Any]]
|
|
346
|
+
|
|
347
|
+
# event_timeout: float | None = (
|
|
348
|
+
# 30.0 # only long to support the edge case of restoring a big localStorage / on many origins (has to O(n) visit each origin to restore)
|
|
349
|
+
# )
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# class GetCookiesEvent(BaseEvent):
|
|
353
|
+
# """Get browser cookies."""
|
|
354
|
+
|
|
355
|
+
# urls: list[str] | None = None
|
|
356
|
+
|
|
357
|
+
# event_timeout: float | None = 30.0 # seconds
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ============================================================================
|
|
361
|
+
# DOM-related Events
|
|
362
|
+
# ============================================================================
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class BrowserConnectedEvent(BaseEvent):
|
|
366
|
+
"""Browser has started/connected."""
|
|
367
|
+
|
|
368
|
+
cdp_url: str
|
|
369
|
+
|
|
370
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserConnectedEvent', 30.0) # seconds
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class BrowserStoppedEvent(BaseEvent):
|
|
374
|
+
"""Browser has stopped/disconnected."""
|
|
375
|
+
|
|
376
|
+
reason: str | None = None
|
|
377
|
+
|
|
378
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserStoppedEvent', 30.0) # seconds
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class TabCreatedEvent(BaseEvent):
|
|
382
|
+
"""A new tab was created."""
|
|
383
|
+
|
|
384
|
+
target_id: TargetID
|
|
385
|
+
url: str
|
|
386
|
+
|
|
387
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_TabCreatedEvent', 30.0) # seconds
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TabClosedEvent(BaseEvent):
|
|
391
|
+
"""A tab was closed."""
|
|
392
|
+
|
|
393
|
+
target_id: TargetID
|
|
394
|
+
|
|
395
|
+
# TODO:
|
|
396
|
+
# new_focus_target_id: int | None = None
|
|
397
|
+
# new_focus_url: str | None = None
|
|
398
|
+
|
|
399
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_TabClosedEvent', 10.0) # seconds
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# TODO: emit this when DOM changes significantly, inner frame navigates, form submits, history.pushState(), etc.
|
|
403
|
+
# class TabUpdatedEvent(BaseEvent):
|
|
404
|
+
# """Tab information updated (URL changed, etc.)."""
|
|
405
|
+
|
|
406
|
+
# target_id: TargetID
|
|
407
|
+
# url: str
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class AgentFocusChangedEvent(BaseEvent):
|
|
411
|
+
"""Agent focus changed to a different tab."""
|
|
412
|
+
|
|
413
|
+
target_id: TargetID
|
|
414
|
+
url: str
|
|
415
|
+
|
|
416
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_AgentFocusChangedEvent', 10.0) # seconds
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class TargetCrashedEvent(BaseEvent):
|
|
420
|
+
"""A target has crashed."""
|
|
421
|
+
|
|
422
|
+
target_id: TargetID
|
|
423
|
+
error: str
|
|
424
|
+
|
|
425
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_TargetCrashedEvent', 10.0) # seconds
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class NavigationStartedEvent(BaseEvent):
|
|
429
|
+
"""Navigation started."""
|
|
430
|
+
|
|
431
|
+
target_id: TargetID
|
|
432
|
+
url: str
|
|
433
|
+
|
|
434
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_NavigationStartedEvent', 30.0) # seconds
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class NavigationCompleteEvent(BaseEvent):
|
|
438
|
+
"""Navigation completed."""
|
|
439
|
+
|
|
440
|
+
target_id: TargetID
|
|
441
|
+
url: str
|
|
442
|
+
status: int | None = None
|
|
443
|
+
error_message: str | None = None # Error/timeout message if navigation had issues
|
|
444
|
+
loading_status: str | None = None # Detailed loading status (e.g., network timeout info)
|
|
445
|
+
|
|
446
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_NavigationCompleteEvent', 30.0) # seconds
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ============================================================================
|
|
450
|
+
# Error Events
|
|
451
|
+
# ============================================================================
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class BrowserErrorEvent(BaseEvent):
|
|
455
|
+
"""An error occurred in the browser layer."""
|
|
456
|
+
|
|
457
|
+
error_type: str
|
|
458
|
+
message: str
|
|
459
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
460
|
+
|
|
461
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_BrowserErrorEvent', 30.0) # seconds
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ============================================================================
|
|
465
|
+
# Storage State Events
|
|
466
|
+
# ============================================================================
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class SaveStorageStateEvent(BaseEvent):
|
|
470
|
+
"""Request to save browser storage state."""
|
|
471
|
+
|
|
472
|
+
path: str | None = None # Optional path, uses profile default if not provided
|
|
473
|
+
|
|
474
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_SaveStorageStateEvent', 45.0) # seconds
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class StorageStateSavedEvent(BaseEvent):
|
|
478
|
+
"""Notification that storage state was saved."""
|
|
479
|
+
|
|
480
|
+
path: str
|
|
481
|
+
cookies_count: int
|
|
482
|
+
origins_count: int
|
|
483
|
+
|
|
484
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_StorageStateSavedEvent', 30.0) # seconds
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class LoadStorageStateEvent(BaseEvent):
|
|
488
|
+
"""Request to load browser storage state."""
|
|
489
|
+
|
|
490
|
+
path: str | None = None # Optional path, uses profile default if not provided
|
|
491
|
+
|
|
492
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_LoadStorageStateEvent', 45.0) # seconds
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# TODO: refactor this to:
|
|
496
|
+
# - on_BrowserConnectedEvent() -> dispatch(LoadStorageStateEvent()) -> _copy_storage_state_from_json_to_browser(json_file, new_cdp_session) + return storage_state from handler
|
|
497
|
+
# - on_BrowserStopEvent() -> dispatch(SaveStorageStateEvent()) -> _copy_storage_state_from_browser_to_json(new_cdp_session, json_file)
|
|
498
|
+
# and get rid of StorageStateSavedEvent and StorageStateLoadedEvent, have the original events + provide handler return values for any results
|
|
499
|
+
class StorageStateLoadedEvent(BaseEvent):
|
|
500
|
+
"""Notification that storage state was loaded."""
|
|
501
|
+
|
|
502
|
+
path: str
|
|
503
|
+
cookies_count: int
|
|
504
|
+
origins_count: int
|
|
505
|
+
|
|
506
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_StorageStateLoadedEvent', 30.0) # seconds
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ============================================================================
|
|
510
|
+
# File Download Events
|
|
511
|
+
# ============================================================================
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class FileDownloadedEvent(BaseEvent):
|
|
515
|
+
"""A file has been downloaded."""
|
|
516
|
+
|
|
517
|
+
url: str
|
|
518
|
+
path: str
|
|
519
|
+
file_name: str
|
|
520
|
+
file_size: int
|
|
521
|
+
file_type: str | None = None # e.g., 'pdf', 'zip', 'docx', etc.
|
|
522
|
+
mime_type: str | None = None # e.g., 'application/pdf'
|
|
523
|
+
from_cache: bool = False
|
|
524
|
+
auto_download: bool = False # Whether this was an automatic download (e.g., PDF auto-download)
|
|
525
|
+
|
|
526
|
+
event_timeout: float | None = _get_timeout('TIMEOUT_FileDownloadedEvent', 30.0) # seconds
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class AboutBlankDVDScreensaverShownEvent(BaseEvent):
|
|
530
|
+
"""AboutBlankWatchdog has shown DVD screensaver animation on an about:blank tab."""
|
|
531
|
+
|
|
532
|
+
target_id: TargetID
|
|
533
|
+
error: str | None = None
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class DialogOpenedEvent(BaseEvent):
|
|
537
|
+
"""Event dispatched when a JavaScript dialog is opened and handled."""
|
|
538
|
+
|
|
539
|
+
dialog_type: str # 'alert', 'confirm', 'prompt', or 'beforeunload'
|
|
540
|
+
message: str
|
|
541
|
+
url: str
|
|
542
|
+
frame_id: str | None = None # Can be None when frameId is not provided by CDP
|
|
543
|
+
# target_id: TargetID # TODO: add this to avoid needing target_id_from_frame() later
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# Note: Model rebuilding for forward references is handled in the importing modules
|
|
547
|
+
# Events with 'EnhancedDOMTreeNode' forward references (ClickElementEvent, TypeTextEvent,
|
|
548
|
+
# ScrollEvent, UploadFileEvent) need model_rebuild() called after imports are complete
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _check_event_names_dont_overlap():
|
|
552
|
+
"""
|
|
553
|
+
check that event names defined in this file are valid and non-overlapping
|
|
554
|
+
(naiively n^2 so it's pretty slow but ok for now, optimize when >20 events)
|
|
555
|
+
"""
|
|
556
|
+
event_names = {
|
|
557
|
+
name.split('[')[0]
|
|
558
|
+
for name in globals().keys()
|
|
559
|
+
if not name.startswith('_')
|
|
560
|
+
and inspect.isclass(globals()[name])
|
|
561
|
+
and issubclass(globals()[name], BaseEvent)
|
|
562
|
+
and name != 'BaseEvent'
|
|
563
|
+
}
|
|
564
|
+
for name_a in event_names:
|
|
565
|
+
assert name_a.endswith('Event'), f'Event with name {name_a} does not end with "Event"'
|
|
566
|
+
for name_b in event_names:
|
|
567
|
+
if name_a != name_b: # Skip self-comparison
|
|
568
|
+
assert name_a not in name_b, (
|
|
569
|
+
f'Event with name {name_a} is a substring of {name_b}, all events must be completely unique to avoid find-and-replace accidents'
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# overlapping event names are a nightmare to trace and rename later, dont do it!
|
|
574
|
+
# e.g. prevent ClickEvent and FailedClickEvent are terrible names because one is a substring of the other,
|
|
575
|
+
# must be ClickEvent and ClickFailedEvent to preserve the usefulnes of codebase grep/sed/awk as refactoring tools.
|
|
576
|
+
# at import time, we do a quick check that all event names defined above are valid and non-overlapping.
|
|
577
|
+
# this is hand written in blood by a human! not LLM slop. feel free to optimize but do not remove it without a good reason.
|
|
578
|
+
_check_event_names_dont_overlap()
|