mirror of
https://github.com/browser-use/browser-use
synced 2026-04-22 17:45:09 +02:00
668 lines
22 KiB
Python
668 lines
22 KiB
Python
"""Event definitions for browser communication."""
|
|
|
|
import inspect
|
|
import os
|
|
from typing import Any, Literal
|
|
|
|
from bubus import BaseEvent
|
|
from bubus.models import T_EventResultType
|
|
from cdp_use.cdp.target import TargetID
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from browser_use.browser.views import BrowserStateSummary
|
|
from browser_use.dom.views import EnhancedDOMTreeNode
|
|
|
|
|
|
def _get_timeout(env_var: str, default: float) -> float | None:
|
|
"""
|
|
Safely parse environment variable timeout values with robust error handling.
|
|
|
|
Args:
|
|
env_var: Environment variable name (e.g. 'TIMEOUT_NavigateToUrlEvent')
|
|
default: Default timeout value as float (e.g. 15.0)
|
|
|
|
Returns:
|
|
Parsed float value or the default if parsing fails
|
|
|
|
Raises:
|
|
ValueError: Only if both env_var and default are invalid (should not happen with valid defaults)
|
|
"""
|
|
# Try environment variable first
|
|
env_value = os.getenv(env_var)
|
|
if env_value:
|
|
try:
|
|
parsed = float(env_value)
|
|
if parsed < 0:
|
|
print(f'Warning: {env_var}={env_value} is negative, using default {default}')
|
|
return default
|
|
return parsed
|
|
except (ValueError, TypeError):
|
|
print(f'Warning: {env_var}={env_value} is not a valid number, using default {default}')
|
|
|
|
# Fall back to default
|
|
return default
|
|
|
|
|
|
# ============================================================================
|
|
# Agent/Tools -> BrowserSession Events (High-level browser actions)
|
|
# ============================================================================
|
|
|
|
|
|
class ElementSelectedEvent(BaseEvent[T_EventResultType]):
|
|
"""An element was selected."""
|
|
|
|
node: EnhancedDOMTreeNode
|
|
|
|
@field_validator('node', mode='before')
|
|
@classmethod
|
|
def serialize_node(cls, data: EnhancedDOMTreeNode | None) -> EnhancedDOMTreeNode | None:
|
|
if data is None:
|
|
return None
|
|
return EnhancedDOMTreeNode(
|
|
node_id=data.node_id,
|
|
backend_node_id=data.backend_node_id,
|
|
session_id=data.session_id,
|
|
frame_id=data.frame_id,
|
|
target_id=data.target_id,
|
|
node_type=data.node_type,
|
|
node_name=data.node_name,
|
|
node_value=data.node_value,
|
|
attributes=data.attributes,
|
|
is_scrollable=data.is_scrollable,
|
|
is_visible=data.is_visible,
|
|
absolute_position=data.absolute_position,
|
|
# override the circular reference fields in EnhancedDOMTreeNode as they cant be serialized and aren't needed by event handlers
|
|
# only used internally by the DOM service during DOM tree building process, not intended public API use
|
|
content_document=None,
|
|
shadow_root_type=None,
|
|
shadow_roots=[],
|
|
parent_node=None,
|
|
children_nodes=[],
|
|
ax_node=None,
|
|
snapshot_node=None,
|
|
)
|
|
|
|
|
|
# TODO: add page handle to events
|
|
# class PageHandle(share a base with browser.session.CDPSession?):
|
|
# url: str
|
|
# target_id: TargetID
|
|
# @classmethod
|
|
# def from_target_id(cls, target_id: TargetID) -> Self:
|
|
# return cls(target_id=target_id)
|
|
# @classmethod
|
|
# def from_target_id(cls, target_id: TargetID) -> Self:
|
|
# return cls(target_id=target_id)
|
|
# @classmethod
|
|
# def from_url(cls, url: str) -> Self:
|
|
# @property
|
|
# def root_frame_id(self) -> str:
|
|
# return self.target_id
|
|
# @property
|
|
# def session_id(self) -> str:
|
|
# return browser_session.get_or_create_cdp_session(self.target_id).session_id
|
|
|
|
# class PageSelectedEvent(BaseEvent[T_EventResultType]):
|
|
# """An event like SwitchToTabEvent(page=PageHandle) or CloseTabEvent(page=PageHandle)"""
|
|
# page: PageHandle
|
|
|
|
|
|
class NavigateToUrlEvent(BaseEvent[None]):
|
|
"""Navigate to a specific URL."""
|
|
|
|
url: str
|
|
wait_until: Literal['load', 'domcontentloaded', 'networkidle', 'commit'] = 'load'
|
|
timeout_ms: int | None = None
|
|
new_tab: bool = Field(
|
|
default=False, description='Set True to leave the current tab alone and open a new tab in the foreground for the new URL'
|
|
)
|
|
# existing_tab: PageHandle | None = None # TODO
|
|
|
|
# time limits enforced by bubus, not exposed to LLM:
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_NavigateToUrlEvent', 30.0)) # seconds
|
|
|
|
|
|
class ClickElementEvent(ElementSelectedEvent[dict[str, Any] | None]):
|
|
"""Click an element."""
|
|
|
|
node: 'EnhancedDOMTreeNode'
|
|
button: Literal['left', 'right', 'middle'] = 'left'
|
|
# click_count: int = 1 # TODO
|
|
# expect_download: bool = False # moved to downloads_watchdog.py
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_ClickElementEvent', 15.0)) # seconds
|
|
|
|
|
|
class ClickCoordinateEvent(BaseEvent[dict]):
|
|
"""Click at specific coordinates."""
|
|
|
|
coordinate_x: int
|
|
coordinate_y: int
|
|
button: Literal['left', 'right', 'middle'] = 'left'
|
|
force: bool = False # If True, skip safety checks (file input, print, select)
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_ClickCoordinateEvent', 15.0)) # seconds
|
|
|
|
|
|
class TypeTextEvent(ElementSelectedEvent[dict | None]):
|
|
"""Type text into an element."""
|
|
|
|
node: 'EnhancedDOMTreeNode'
|
|
text: str
|
|
clear: bool = True
|
|
is_sensitive: bool = False # Flag to indicate if text contains sensitive data
|
|
sensitive_key_name: str | None = None # Name of the sensitive key being typed (e.g., 'username', 'password')
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_TypeTextEvent', 60.0)) # seconds
|
|
|
|
|
|
class ScrollEvent(ElementSelectedEvent[None]):
|
|
"""Scroll the page or element."""
|
|
|
|
direction: Literal['up', 'down', 'left', 'right']
|
|
amount: int # pixels
|
|
node: 'EnhancedDOMTreeNode | None' = None # None means scroll page
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_ScrollEvent', 8.0)) # seconds
|
|
|
|
|
|
class SwitchTabEvent(BaseEvent[TargetID]):
|
|
"""Switch to a different tab."""
|
|
|
|
target_id: TargetID | None = Field(default=None, description='None means switch to the most recently opened tab')
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_SwitchTabEvent', 10.0)) # seconds
|
|
|
|
|
|
class CloseTabEvent(BaseEvent[None]):
|
|
"""Close a tab."""
|
|
|
|
target_id: TargetID
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_CloseTabEvent', 10.0)) # seconds
|
|
|
|
|
|
class ScreenshotEvent(BaseEvent[str]):
|
|
"""Request to take a screenshot."""
|
|
|
|
full_page: bool = False
|
|
clip: dict[str, float] | None = None # {x, y, width, height}
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_ScreenshotEvent', 15.0)) # seconds
|
|
|
|
|
|
class BrowserStateRequestEvent(BaseEvent[BrowserStateSummary]):
|
|
"""Request current browser state."""
|
|
|
|
include_dom: bool = True
|
|
include_screenshot: bool = True
|
|
include_recent_events: bool = False
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserStateRequestEvent', 30.0)) # seconds
|
|
|
|
|
|
# class WaitForConditionEvent(BaseEvent):
|
|
# """Wait for a condition."""
|
|
|
|
# condition: Literal['navigation', 'selector', 'timeout', 'load_state']
|
|
# timeout: float = 30000
|
|
# selector: str | None = None
|
|
# state: Literal['attached', 'detached', 'visible', 'hidden'] | None = None
|
|
|
|
|
|
class GoBackEvent(BaseEvent[None]):
|
|
"""Navigate back in browser history."""
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_GoBackEvent', 15.0)) # seconds
|
|
|
|
|
|
class GoForwardEvent(BaseEvent[None]):
|
|
"""Navigate forward in browser history."""
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_GoForwardEvent', 15.0)) # seconds
|
|
|
|
|
|
class RefreshEvent(BaseEvent[None]):
|
|
"""Refresh/reload the current page."""
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_RefreshEvent', 15.0)) # seconds
|
|
|
|
|
|
class WaitEvent(BaseEvent[None]):
|
|
"""Wait for a specified number of seconds."""
|
|
|
|
seconds: float = 3.0
|
|
max_seconds: float = 10.0 # Safety cap
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_WaitEvent', 60.0)) # seconds
|
|
|
|
|
|
class SendKeysEvent(BaseEvent[None]):
|
|
"""Send keyboard keys/shortcuts."""
|
|
|
|
keys: str # e.g., "ctrl+a", "cmd+c", "Enter"
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_SendKeysEvent', 60.0)) # seconds
|
|
|
|
|
|
class UploadFileEvent(ElementSelectedEvent[None]):
|
|
"""Upload a file to an element."""
|
|
|
|
node: 'EnhancedDOMTreeNode'
|
|
file_path: str
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_UploadFileEvent', 30.0)) # seconds
|
|
|
|
|
|
class GetDropdownOptionsEvent(ElementSelectedEvent[dict[str, str]]):
|
|
"""Get all options from any dropdown (native <select>, ARIA menus, or custom dropdowns).
|
|
|
|
Returns a dict containing dropdown type, options list, and element metadata."""
|
|
|
|
node: 'EnhancedDOMTreeNode'
|
|
|
|
event_timeout: float | None = Field(
|
|
default_factory=lambda: _get_timeout('TIMEOUT_GetDropdownOptionsEvent', 15.0)
|
|
) # 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)
|
|
|
|
|
|
class SelectDropdownOptionEvent(ElementSelectedEvent[dict[str, str]]):
|
|
"""Select a dropdown option by exact text from any dropdown type.
|
|
|
|
Returns a dict containing success status and selection details."""
|
|
|
|
node: 'EnhancedDOMTreeNode'
|
|
text: str # The option text to select
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_SelectDropdownOptionEvent', 8.0)) # seconds
|
|
|
|
|
|
class ScrollToTextEvent(BaseEvent[None]):
|
|
"""Scroll to specific text on the page. Raises exception if text not found."""
|
|
|
|
text: str
|
|
direction: Literal['up', 'down'] = 'down'
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_ScrollToTextEvent', 15.0)) # seconds
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
class BrowserStartEvent(BaseEvent):
|
|
"""Start/connect to browser."""
|
|
|
|
cdp_url: str | None = None
|
|
launch_options: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserStartEvent', 30.0)) # seconds
|
|
|
|
|
|
class BrowserStopEvent(BaseEvent):
|
|
"""Stop/disconnect from browser."""
|
|
|
|
force: bool = False
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserStopEvent', 45.0)) # seconds
|
|
|
|
|
|
class BrowserLaunchResult(BaseModel):
|
|
"""Result of launching a browser."""
|
|
|
|
# TODO: add browser executable_path, pid, version, latency, user_data_dir, X11 $DISPLAY, host IP address, etc.
|
|
cdp_url: str
|
|
|
|
|
|
class BrowserLaunchEvent(BaseEvent[BrowserLaunchResult]):
|
|
"""Launch a local browser process."""
|
|
|
|
# TODO: add executable_path, proxy settings, preferences, extra launch args, etc.
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserLaunchEvent', 30.0)) # seconds
|
|
|
|
|
|
class BrowserKillEvent(BaseEvent):
|
|
"""Kill local browser subprocess."""
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserKillEvent', 30.0)) # seconds
|
|
|
|
|
|
# TODO: replace all Runtime.evaluate() calls with this event
|
|
# class ExecuteJavaScriptEvent(BaseEvent):
|
|
# """Execute JavaScript in page context."""
|
|
|
|
# target_id: TargetID
|
|
# expression: str
|
|
# await_promise: bool = True
|
|
|
|
# event_timeout: float | None = 60.0 # seconds
|
|
|
|
# TODO: add this and use the old BrowserProfile.viewport options to set it
|
|
# class SetViewportEvent(BaseEvent):
|
|
# """Set the viewport size."""
|
|
|
|
# width: int
|
|
# height: int
|
|
# device_scale_factor: float = 1.0
|
|
|
|
# event_timeout: float | None = 15.0 # seconds
|
|
|
|
|
|
# Moved to storage state
|
|
# class SetCookiesEvent(BaseEvent):
|
|
# """Set browser cookies."""
|
|
|
|
# cookies: list[dict[str, Any]]
|
|
|
|
# event_timeout: float | None = (
|
|
# 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)
|
|
# )
|
|
|
|
|
|
# class GetCookiesEvent(BaseEvent):
|
|
# """Get browser cookies."""
|
|
|
|
# urls: list[str] | None = None
|
|
|
|
# event_timeout: float | None = 30.0 # seconds
|
|
|
|
|
|
# ============================================================================
|
|
# DOM-related Events
|
|
# ============================================================================
|
|
|
|
|
|
class BrowserConnectedEvent(BaseEvent):
|
|
"""Browser has started/connected."""
|
|
|
|
cdp_url: str
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserConnectedEvent', 30.0)) # seconds
|
|
|
|
|
|
class BrowserStoppedEvent(BaseEvent):
|
|
"""Browser has stopped/disconnected."""
|
|
|
|
reason: str | None = None
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserStoppedEvent', 30.0)) # seconds
|
|
|
|
|
|
class TabCreatedEvent(BaseEvent):
|
|
"""A new tab was created."""
|
|
|
|
target_id: TargetID
|
|
url: str
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_TabCreatedEvent', 30.0)) # seconds
|
|
|
|
|
|
class TabClosedEvent(BaseEvent):
|
|
"""A tab was closed."""
|
|
|
|
target_id: TargetID
|
|
|
|
# TODO:
|
|
# new_focus_target_id: int | None = None
|
|
# new_focus_url: str | None = None
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_TabClosedEvent', 3.0)) # seconds
|
|
|
|
|
|
# TODO: emit this when DOM changes significantly, inner frame navigates, form submits, history.pushState(), etc.
|
|
# class TabUpdatedEvent(BaseEvent):
|
|
# """Tab information updated (URL changed, etc.)."""
|
|
|
|
# target_id: TargetID
|
|
# url: str
|
|
|
|
|
|
class AgentFocusChangedEvent(BaseEvent):
|
|
"""Agent focus changed to a different tab."""
|
|
|
|
target_id: TargetID
|
|
url: str
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_AgentFocusChangedEvent', 10.0)) # seconds
|
|
|
|
|
|
class TargetCrashedEvent(BaseEvent):
|
|
"""A target has crashed."""
|
|
|
|
target_id: TargetID
|
|
error: str
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_TargetCrashedEvent', 10.0)) # seconds
|
|
|
|
|
|
class NavigationStartedEvent(BaseEvent):
|
|
"""Navigation started."""
|
|
|
|
target_id: TargetID
|
|
url: str
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_NavigationStartedEvent', 30.0)) # seconds
|
|
|
|
|
|
class NavigationCompleteEvent(BaseEvent):
|
|
"""Navigation completed."""
|
|
|
|
target_id: TargetID
|
|
url: str
|
|
status: int | None = None
|
|
error_message: str | None = None # Error/timeout message if navigation had issues
|
|
loading_status: str | None = None # Detailed loading status (e.g., network timeout info)
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_NavigationCompleteEvent', 30.0)) # seconds
|
|
|
|
|
|
# ============================================================================
|
|
# Error Events
|
|
# ============================================================================
|
|
|
|
|
|
class BrowserErrorEvent(BaseEvent):
|
|
"""An error occurred in the browser layer."""
|
|
|
|
error_type: str
|
|
message: str
|
|
details: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserErrorEvent', 30.0)) # seconds
|
|
|
|
|
|
class BrowserReconnectingEvent(BaseEvent):
|
|
"""WebSocket reconnection attempt is starting."""
|
|
|
|
cdp_url: str
|
|
attempt: int
|
|
max_attempts: int
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserReconnectingEvent', 30.0)) # seconds
|
|
|
|
|
|
class BrowserReconnectedEvent(BaseEvent):
|
|
"""WebSocket reconnection succeeded."""
|
|
|
|
cdp_url: str
|
|
attempt: int
|
|
downtime_seconds: float
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_BrowserReconnectedEvent', 30.0)) # seconds
|
|
|
|
|
|
# ============================================================================
|
|
# Storage State Events
|
|
# ============================================================================
|
|
|
|
|
|
class SaveStorageStateEvent(BaseEvent):
|
|
"""Request to save browser storage state."""
|
|
|
|
path: str | None = None # Optional path, uses profile default if not provided
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_SaveStorageStateEvent', 45.0)) # seconds
|
|
|
|
|
|
class StorageStateSavedEvent(BaseEvent):
|
|
"""Notification that storage state was saved."""
|
|
|
|
path: str
|
|
cookies_count: int
|
|
origins_count: int
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_StorageStateSavedEvent', 30.0)) # seconds
|
|
|
|
|
|
class LoadStorageStateEvent(BaseEvent):
|
|
"""Request to load browser storage state."""
|
|
|
|
path: str | None = None # Optional path, uses profile default if not provided
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_LoadStorageStateEvent', 45.0)) # seconds
|
|
|
|
|
|
# TODO: refactor this to:
|
|
# - on_BrowserConnectedEvent() -> dispatch(LoadStorageStateEvent()) -> _copy_storage_state_from_json_to_browser(json_file, new_cdp_session) + return storage_state from handler
|
|
# - on_BrowserStopEvent() -> dispatch(SaveStorageStateEvent()) -> _copy_storage_state_from_browser_to_json(new_cdp_session, json_file)
|
|
# and get rid of StorageStateSavedEvent and StorageStateLoadedEvent, have the original events + provide handler return values for any results
|
|
class StorageStateLoadedEvent(BaseEvent):
|
|
"""Notification that storage state was loaded."""
|
|
|
|
path: str
|
|
cookies_count: int
|
|
origins_count: int
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_StorageStateLoadedEvent', 30.0)) # seconds
|
|
|
|
|
|
# ============================================================================
|
|
# File Download Events
|
|
# ============================================================================
|
|
|
|
|
|
class DownloadStartedEvent(BaseEvent):
|
|
"""A file download has started (CDP downloadWillBegin received)."""
|
|
|
|
guid: str # CDP download GUID to correlate with FileDownloadedEvent
|
|
url: str
|
|
suggested_filename: str
|
|
auto_download: bool = False # Whether this was triggered automatically
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_DownloadStartedEvent', 5.0)) # seconds
|
|
|
|
|
|
class DownloadProgressEvent(BaseEvent):
|
|
"""A file download progress update (CDP downloadProgress received)."""
|
|
|
|
guid: str # CDP download GUID to correlate with other download events
|
|
received_bytes: int
|
|
total_bytes: int # 0 if unknown
|
|
state: str # 'inProgress', 'completed', or 'canceled'
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_DownloadProgressEvent', 5.0)) # seconds
|
|
|
|
|
|
class FileDownloadedEvent(BaseEvent):
|
|
"""A file has been downloaded."""
|
|
|
|
guid: str | None = None # CDP download GUID to correlate with DownloadStartedEvent
|
|
url: str
|
|
path: str
|
|
file_name: str
|
|
file_size: int
|
|
file_type: str | None = None # e.g., 'pdf', 'zip', 'docx', etc.
|
|
mime_type: str | None = None # e.g., 'application/pdf'
|
|
from_cache: bool = False
|
|
auto_download: bool = False # Whether this was an automatic download (e.g., PDF auto-download)
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_FileDownloadedEvent', 30.0)) # seconds
|
|
|
|
|
|
class AboutBlankDVDScreensaverShownEvent(BaseEvent):
|
|
"""AboutBlankWatchdog has shown DVD screensaver animation on an about:blank tab."""
|
|
|
|
target_id: TargetID
|
|
error: str | None = None
|
|
|
|
|
|
class DialogOpenedEvent(BaseEvent):
|
|
"""Event dispatched when a JavaScript dialog is opened and handled."""
|
|
|
|
dialog_type: str # 'alert', 'confirm', 'prompt', or 'beforeunload'
|
|
message: str
|
|
url: str
|
|
frame_id: str | None = None # Can be None when frameId is not provided by CDP
|
|
# target_id: TargetID # TODO: add this to avoid needing target_id_from_frame() later
|
|
|
|
|
|
# ============================================================================
|
|
# Captcha Solver Events
|
|
# ============================================================================
|
|
|
|
|
|
class CaptchaSolverStartedEvent(BaseEvent):
|
|
"""Captcha solving started by the browser proxy.
|
|
|
|
Emitted when the browser proxy detects a CAPTCHA and begins solving it.
|
|
The agent should wait for a corresponding CaptchaSolverFinishedEvent before proceeding.
|
|
"""
|
|
|
|
target_id: TargetID
|
|
vendor: str # e.g. 'cloudflare', 'recaptcha', 'hcaptcha', 'datadome', 'perimeterx', 'geetest'
|
|
url: str
|
|
started_at: int # Unix millis
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_CaptchaSolverStartedEvent', 5.0))
|
|
|
|
|
|
class CaptchaSolverFinishedEvent(BaseEvent):
|
|
"""Captcha solving finished by the browser proxy.
|
|
|
|
Emitted when the browser proxy finishes solving a CAPTCHA (successfully or not).
|
|
"""
|
|
|
|
target_id: TargetID
|
|
vendor: str
|
|
url: str
|
|
duration_ms: int
|
|
finished_at: int # Unix millis
|
|
success: bool # Whether the captcha was solved successfully
|
|
|
|
event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_CaptchaSolverFinishedEvent', 5.0))
|
|
|
|
|
|
# Note: Model rebuilding for forward references is handled in the importing modules
|
|
# Events with 'EnhancedDOMTreeNode' forward references (ClickElementEvent, TypeTextEvent,
|
|
# ScrollEvent, UploadFileEvent) need model_rebuild() called after imports are complete
|
|
|
|
|
|
def _check_event_names_dont_overlap():
|
|
"""
|
|
check that event names defined in this file are valid and non-overlapping
|
|
(naiively n^2 so it's pretty slow but ok for now, optimize when >20 events)
|
|
"""
|
|
event_names = {
|
|
name.split('[')[0]
|
|
for name in globals().keys()
|
|
if not name.startswith('_')
|
|
and inspect.isclass(globals()[name])
|
|
and issubclass(globals()[name], BaseEvent)
|
|
and name != 'BaseEvent'
|
|
}
|
|
for name_a in event_names:
|
|
assert name_a.endswith('Event'), f'Event with name {name_a} does not end with "Event"'
|
|
for name_b in event_names:
|
|
if name_a != name_b: # Skip self-comparison
|
|
assert name_a not in name_b, (
|
|
f'Event with name {name_a} is a substring of {name_b}, all events must be completely unique to avoid find-and-replace accidents'
|
|
)
|
|
|
|
|
|
# overlapping event names are a nightmare to trace and rename later, dont do it!
|
|
# e.g. prevent ClickEvent and FailedClickEvent are terrible names because one is a substring of the other,
|
|
# must be ClickEvent and ClickFailedEvent to preserve the usefulnes of codebase grep/sed/awk as refactoring tools.
|
|
# at import time, we do a quick check that all event names defined above are valid and non-overlapping.
|
|
# 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.
|
|
_check_event_names_dont_overlap()
|