mirror of
https://github.com/browser-use/browser-use.git
synced 2026-09-14 19:59:47 +08:00
a1d20ed81c
Previously only suggested --full, but --remote-only is a lighter-weight option when users only need cloud browser support. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
424 lines
12 KiB
Python
424 lines
12 KiB
Python
"""Cloud session SDK wrappers and CLI handlers.
|
|
|
|
This module provides:
|
|
- SDK wrapper functions for the Browser-Use Cloud Session API
|
|
- CLI command handlers for `browser-use session <command>`
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from typing import Any
|
|
|
|
from browser_use_sdk.types.session_item_view import SessionItemView
|
|
from browser_use_sdk.types.session_view import SessionView
|
|
from browser_use_sdk.types.share_view import ShareView
|
|
|
|
from browser_use.skill_cli.commands.utils import format_duration, get_sdk_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============ SDK Wrappers ============
|
|
|
|
|
|
def create_session(**kwargs: Any) -> SessionItemView:
|
|
"""Create a cloud browser session.
|
|
|
|
Args:
|
|
profile_id: Cloud profile ID for persistent auth/cookies
|
|
proxy_country: Proxy country code (us, gb, de, etc.)
|
|
keep_alive: Keep session alive after task completes
|
|
persist_memory: Share memory between tasks in session
|
|
start_url: URL to navigate to when session starts
|
|
screen_width: Browser screen width in pixels
|
|
screen_height: Browser screen height in pixels
|
|
|
|
Returns:
|
|
SessionItemView with session details
|
|
"""
|
|
# Map our param names to SDK param names
|
|
param_map = {
|
|
'proxy_country': 'proxy_country_code',
|
|
'screen_width': 'browser_screen_width',
|
|
'screen_height': 'browser_screen_height',
|
|
}
|
|
params = {}
|
|
for k, v in kwargs.items():
|
|
if v is not None:
|
|
params[param_map.get(k, k)] = v
|
|
|
|
return get_sdk_client().sessions.create_session(**params)
|
|
|
|
|
|
def list_sessions(limit: int = 10, status: str | None = None) -> list[SessionItemView]:
|
|
"""List cloud browser sessions."""
|
|
client = get_sdk_client()
|
|
response = client.sessions.list_sessions(
|
|
page_size=min(limit, 100),
|
|
filter_by=status,
|
|
)
|
|
return list(response.items) if response.items else []
|
|
|
|
|
|
def get_session(session_id: str) -> SessionView:
|
|
"""Get details of a specific session."""
|
|
return get_sdk_client().sessions.get_session(session_id)
|
|
|
|
|
|
def stop_session(session_id: str) -> SessionView:
|
|
"""Stop a cloud session."""
|
|
return get_sdk_client().sessions.update_session(session_id, action='stop')
|
|
|
|
|
|
def delete_session(session_id: str) -> None:
|
|
"""Delete a cloud session and all its tasks."""
|
|
get_sdk_client().sessions.delete_session(session_id)
|
|
|
|
|
|
def create_public_share(session_id: str) -> ShareView:
|
|
"""Create a public share URL for a session."""
|
|
return get_sdk_client().sessions.create_session_public_share(session_id)
|
|
|
|
|
|
def delete_public_share(session_id: str) -> None:
|
|
"""Delete the public share for a session."""
|
|
get_sdk_client().sessions.delete_session_public_share(session_id)
|
|
|
|
|
|
def stop_sessions_parallel(session_ids: list[str]) -> tuple[list[str], list[dict[str, Any]]]:
|
|
"""Stop multiple cloud sessions in parallel."""
|
|
client = get_sdk_client()
|
|
stopped: list[str] = []
|
|
errors: list[dict[str, Any]] = []
|
|
|
|
def stop_one(sid: str) -> tuple[str, str | None]:
|
|
try:
|
|
client.sessions.update_session(sid, action='stop')
|
|
return (sid, None)
|
|
except Exception as e:
|
|
return (sid, str(e))
|
|
|
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
futures = {executor.submit(stop_one, sid): sid for sid in session_ids}
|
|
for future in as_completed(futures):
|
|
sid, error = future.result()
|
|
if error:
|
|
errors.append({'id': sid, 'error': error})
|
|
else:
|
|
stopped.append(sid)
|
|
|
|
return stopped, errors
|
|
|
|
|
|
# ============ CLI Handlers ============
|
|
|
|
|
|
def handle_session_command(args: argparse.Namespace) -> int:
|
|
"""Handle session subcommands.
|
|
|
|
Session commands manage cloud sessions and always require the cloud API.
|
|
|
|
Args:
|
|
args: Parsed command-line arguments
|
|
|
|
Returns:
|
|
Exit code (0 for success, 1 for error)
|
|
"""
|
|
from browser_use.skill_cli.api_key import APIKeyRequired, require_api_key
|
|
from browser_use.skill_cli.install_config import is_mode_available
|
|
|
|
# Check if remote mode is available
|
|
if not is_mode_available('remote'):
|
|
print(
|
|
'Error: Session management requires remote mode.\n'
|
|
'Remote mode is not installed. Reinstall to enable:\n'
|
|
' curl -fsSL https://browser-use.com/cli/install.sh | bash -s -- --remote-only\n'
|
|
' curl -fsSL https://browser-use.com/cli/install.sh | bash -s -- --full',
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
# Check API key
|
|
try:
|
|
require_api_key('Cloud sessions')
|
|
except APIKeyRequired as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if args.session_command == 'list':
|
|
return _handle_list(args)
|
|
elif args.session_command == 'get':
|
|
return _handle_get(args)
|
|
elif args.session_command == 'stop':
|
|
return _handle_stop(args)
|
|
elif args.session_command == 'create':
|
|
return _handle_create(args)
|
|
elif args.session_command == 'share':
|
|
return _handle_share(args)
|
|
else:
|
|
print('Usage: browser-use session <command>')
|
|
print('Commands: list, get <id>, stop <id>, create, share <id>')
|
|
return 1
|
|
|
|
|
|
# ============ CLI Helper Functions ============
|
|
|
|
|
|
def _session_to_dict(session: Any) -> dict[str, Any]:
|
|
"""Convert SDK session object to dict for JSON output."""
|
|
return {
|
|
'id': session.id,
|
|
'status': session.status,
|
|
'liveUrl': session.live_url,
|
|
'startedAt': session.started_at.isoformat() if session.started_at else None,
|
|
'finishedAt': session.finished_at.isoformat() if session.finished_at else None,
|
|
'keepAlive': session.keep_alive,
|
|
'persistMemory': getattr(session, 'persist_memory', None),
|
|
'proxyCost': session.proxy_cost,
|
|
'publicShareUrl': getattr(session, 'public_share_url', None),
|
|
}
|
|
|
|
|
|
def _handle_list(args: argparse.Namespace) -> int:
|
|
"""Handle 'session list' command."""
|
|
try:
|
|
status_filter = getattr(args, 'status', None)
|
|
sessions = list_sessions(limit=args.limit, status=status_filter)
|
|
except Exception as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if getattr(args, 'json', False):
|
|
print(json.dumps([_session_to_dict(s) for s in sessions]))
|
|
else:
|
|
if not sessions:
|
|
status_msg = f' with status "{status_filter}"' if status_filter else ''
|
|
print(f'No sessions found{status_msg}')
|
|
else:
|
|
header = f'Sessions ({len(sessions)})'
|
|
if status_filter:
|
|
header = f'{status_filter.capitalize()} sessions ({len(sessions)})'
|
|
print(f'{header}:')
|
|
for s in sessions:
|
|
session_id = s.id or 'unknown'
|
|
status = s.status or 'unknown'
|
|
live_url = s.live_url
|
|
started_at = s.started_at
|
|
finished_at = s.finished_at
|
|
keep_alive = '🔄' if s.keep_alive else ''
|
|
|
|
# Status emoji
|
|
status_emoji = {
|
|
'active': '🟢',
|
|
'stopped': '⏹️',
|
|
}.get(status, '❓')
|
|
|
|
# Truncate ID for display
|
|
short_id = session_id[:8] + '...' if len(session_id) > 8 else session_id
|
|
|
|
# Build line with duration
|
|
duration = format_duration(started_at, finished_at)
|
|
line = f' {status_emoji} {short_id} [{status}]'
|
|
if duration:
|
|
line += f' {duration}'
|
|
if keep_alive:
|
|
line += f' {keep_alive}'
|
|
if live_url and status == 'active':
|
|
line += f'\n live: {live_url}'
|
|
print(line)
|
|
|
|
return 0
|
|
|
|
|
|
def _handle_get(args: argparse.Namespace) -> int:
|
|
"""Handle 'session get <session_id>' command."""
|
|
try:
|
|
session = get_session(args.session_id)
|
|
except Exception as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if getattr(args, 'json', False):
|
|
print(json.dumps(_session_to_dict(session)))
|
|
else:
|
|
session_id = session.id or args.session_id
|
|
status = session.status or 'unknown'
|
|
live_url = session.live_url
|
|
started_at = session.started_at
|
|
finished_at = session.finished_at
|
|
keep_alive = session.keep_alive
|
|
proxy_cost = session.proxy_cost
|
|
public_share_url = getattr(session, 'public_share_url', None)
|
|
|
|
# Status emoji
|
|
status_emoji = {
|
|
'active': '🟢',
|
|
'stopped': '⏹️',
|
|
}.get(status, '❓')
|
|
|
|
# Build header with duration
|
|
duration = format_duration(started_at, finished_at)
|
|
header_parts = [f'{status_emoji} {session_id[:8]}... [{status}]']
|
|
if duration:
|
|
header_parts.append(duration)
|
|
if proxy_cost:
|
|
# Format proxy cost to 2 decimal places
|
|
try:
|
|
cost_val = float(proxy_cost)
|
|
header_parts.append(f'${cost_val:.2f}')
|
|
except (ValueError, TypeError):
|
|
header_parts.append(f'${proxy_cost}')
|
|
print(' '.join(header_parts))
|
|
|
|
if keep_alive:
|
|
print(' Keep Alive: Yes')
|
|
if live_url:
|
|
print(f' Live URL: {live_url}')
|
|
if public_share_url:
|
|
print(f' Public Share: {public_share_url}')
|
|
|
|
return 0
|
|
|
|
|
|
def _handle_stop(args: argparse.Namespace) -> int:
|
|
"""Handle 'session stop <session_id>' command."""
|
|
# Handle --all flag
|
|
if getattr(args, 'all', False):
|
|
return _handle_stop_all(args)
|
|
|
|
try:
|
|
stop_session(args.session_id)
|
|
except Exception as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if getattr(args, 'json', False):
|
|
print(json.dumps({'stopped': args.session_id}))
|
|
else:
|
|
print(f'Stopped session: {args.session_id}')
|
|
|
|
return 0
|
|
|
|
|
|
def _handle_stop_all(args: argparse.Namespace) -> int:
|
|
"""Handle 'session stop --all' command."""
|
|
try:
|
|
# Get all active sessions
|
|
sessions = list_sessions(limit=100, status='active')
|
|
except Exception as e:
|
|
print(f'Error listing sessions: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if not sessions:
|
|
print('No active sessions to stop')
|
|
return 0
|
|
|
|
# Extract session IDs
|
|
session_ids = [s.id for s in sessions if s.id]
|
|
|
|
if not session_ids:
|
|
print('No active sessions to stop')
|
|
return 0
|
|
|
|
# Stop all sessions in parallel
|
|
stopped, errors = stop_sessions_parallel(session_ids)
|
|
|
|
if getattr(args, 'json', False):
|
|
print(json.dumps({'stopped': stopped, 'errors': errors}))
|
|
else:
|
|
if stopped:
|
|
print(f'Stopped {len(stopped)} session(s):')
|
|
for sid in stopped:
|
|
print(f' ✓ {sid[:8]}...')
|
|
if errors:
|
|
print(f'Failed to stop {len(errors)} session(s):')
|
|
for err in errors:
|
|
print(f' ✗ {err["id"][:8]}...: {err["error"]}')
|
|
|
|
return 0 if not errors else 1
|
|
|
|
|
|
def _handle_create(args: argparse.Namespace) -> int:
|
|
"""Handle 'session create' command."""
|
|
# Parse screen size if provided
|
|
screen_width = None
|
|
screen_height = None
|
|
if hasattr(args, 'screen_size') and args.screen_size:
|
|
try:
|
|
w, h = args.screen_size.lower().split('x')
|
|
screen_width = int(w)
|
|
screen_height = int(h)
|
|
except ValueError:
|
|
print('Error: Invalid screen size format. Use WxH (e.g., 1920x1080)', file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
session = create_session(
|
|
profile_id=getattr(args, 'profile', None),
|
|
proxy_country=getattr(args, 'proxy_country', None),
|
|
keep_alive=getattr(args, 'keep_alive', None),
|
|
persist_memory=getattr(args, 'persist_memory', None),
|
|
start_url=getattr(args, 'start_url', None),
|
|
screen_width=screen_width,
|
|
screen_height=screen_height,
|
|
)
|
|
except Exception as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if getattr(args, 'json', False):
|
|
print(json.dumps(_session_to_dict(session)))
|
|
else:
|
|
print(f'Created session: {session.id}')
|
|
if session.live_url:
|
|
print(f' Live URL: {session.live_url}')
|
|
|
|
return 0
|
|
|
|
|
|
def _handle_share(args: argparse.Namespace) -> int:
|
|
"""Handle 'session share <session_id>' command."""
|
|
session_id = args.session_id
|
|
|
|
# Delete share if requested
|
|
if getattr(args, 'delete', False):
|
|
try:
|
|
delete_public_share(session_id)
|
|
except Exception as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if getattr(args, 'json', False):
|
|
print(json.dumps({'deleted': session_id}))
|
|
else:
|
|
print(f'Deleted public share for session: {session_id}')
|
|
return 0
|
|
|
|
# Create share
|
|
try:
|
|
share = create_public_share(session_id)
|
|
except Exception as e:
|
|
print(f'Error: {e}', file=sys.stderr)
|
|
return 1
|
|
|
|
if getattr(args, 'json', False):
|
|
print(
|
|
json.dumps(
|
|
{
|
|
'sessionId': session_id,
|
|
'url': share.share_url,
|
|
'shareToken': share.share_token,
|
|
'viewCount': share.view_count,
|
|
}
|
|
)
|
|
)
|
|
else:
|
|
print(f'Public share created for session: {session_id}')
|
|
if share.share_url:
|
|
print(f' URL: {share.share_url}')
|
|
|
|
return 0
|