mirror of
https://github.com/browser-use/browser-use
synced 2026-04-22 17:45:09 +02:00
Merge branch 'main' into fix/browser-session-close
This commit is contained in:
26
README.md
26
README.md
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://cloud.browser-use.com"><img src="https://media.browser-use.tools/badges/package" height="48" alt="Browser-Use Package Download Statistics"></a>
|
||||
<a href="https://cloud.browser-use.com?utm_source=github&utm_medium=readme-badge-downloads"><img src="https://media.browser-use.tools/badges/package" height="48" alt="Browser-Use Package Download Statistics"></a>
|
||||
</div>
|
||||
|
||||
---
|
||||
@@ -33,12 +33,12 @@
|
||||
<img width="4 height="1" alt="">
|
||||
<a href="https://link.browser-use.com/discord"><img src="https://media.browser-use.tools/badges/discord" alt="Discord"></a>
|
||||
<img width="4" height="1" alt="">
|
||||
<a href="https://cloud.browser-use.com"><img src="https://media.browser-use.tools/badges/cloud" height="48" alt="Browser-Use Cloud"></a>
|
||||
<a href="https://cloud.browser-use.com?utm_source=github&utm_medium=readme-badge-cloud"><img src="https://media.browser-use.tools/badges/cloud" height="48" alt="Browser-Use Cloud"></a>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
|
||||
🌤️ Want to skip the setup? Use our <b>[cloud](https://cloud.browser-use.com)</b> for faster, scalable, stealth-enabled browser automation!
|
||||
🌤️ Want to skip the setup? Use our <b>[cloud](https://cloud.browser-use.com?utm_source=github&utm_medium=readme-skip-setup)</b> for faster, scalable, stealth-enabled browser automation!
|
||||
|
||||
# 🤖 LLM Quickstart
|
||||
|
||||
@@ -55,7 +55,7 @@ uv init && uv add browser-use && uv sync
|
||||
# uvx browser-use install # Run if you don't have Chromium installed
|
||||
```
|
||||
|
||||
**2. [Optional] Get your API key from [Browser Use Cloud](https://cloud.browser-use.com/new-api-key):**
|
||||
**2. [Optional] Get your API key from [Browser Use Cloud](https://cloud.browser-use.com/new-api-key?utm_source=github&utm_medium=readme-quickstart-api-key):**
|
||||
```
|
||||
# .env
|
||||
BROWSER_USE_API_KEY=your-key
|
||||
@@ -88,7 +88,7 @@ if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Check out the [library docs](https://docs.browser-use.com/open-source/introduction) and the [cloud docs](https://docs.cloud.browser-use.com) for more!
|
||||
Check out the [library docs](https://docs.browser-use.com/open-source/introduction) and the [cloud docs](https://docs.cloud.browser-use.com?utm_source=github&utm_medium=readme-cloud-docs) for more!
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -102,20 +102,18 @@ Check out the [library docs](https://docs.browser-use.com/open-source/introducti
|
||||
|
||||
We benchmark Browser Use across 100 real-world browser tasks. Full benchmark is open source: **[browser-use/benchmark](https://github.com/browser-use/benchmark)**.
|
||||
|
||||
**Use Open Source**
|
||||
**Use the Open-Source Agent**
|
||||
- You need [custom tools](https://docs.browser-use.com/customize/tools/basics) or deep code-level integration
|
||||
- You want to self-host and deploy browser agents on your own machines
|
||||
- We recommend pairing with our [cloud browsers](https://docs.browser-use.com/open-source/customize/browser/remote) for leading stealth, proxy rotation, and scaling
|
||||
- Or self-host the open-source agent fully on your own machines
|
||||
|
||||
**Use [Cloud](https://cloud.browser-use.com) (recommended)**
|
||||
- Much better agent for complex tasks (see plot above)
|
||||
**Use the [Fully-Hosted Cloud Agent](https://cloud.browser-use.com?utm_source=github&utm_medium=readme-hosted-agent) (recommended)**
|
||||
- Much more powerful agent for complex tasks (see plot above)
|
||||
- Easiest way to start and scale
|
||||
- Best stealth with proxy rotation and captcha solving
|
||||
- 1000+ integrations (Gmail, Slack, Notion, and more)
|
||||
- Persistent filesystem and memory
|
||||
|
||||
**Use Both**
|
||||
- Use the open-source library with your [custom tools](https://docs.browser-use.com/customize/tools/basics) while running our [cloud browsers](https://docs.browser-use.com/open-source/customize/browser/remote) and [ChatBrowserUse model](https://docs.browser-use.com/open-source/supported-models)
|
||||
|
||||
<br/>
|
||||
|
||||
# Demos
|
||||
@@ -273,7 +271,7 @@ These examples show how to maintain sessions and handle authentication seamlessl
|
||||
<details>
|
||||
<summary><b>How do I solve CAPTCHAs?</b></summary>
|
||||
|
||||
For CAPTCHA handling, you need better browser fingerprinting and proxies. Use [Browser Use Cloud](https://cloud.browser-use.com) which provides stealth browsers designed to avoid detection and CAPTCHA challenges.
|
||||
For CAPTCHA handling, you need better browser fingerprinting and proxies. Use [Browser Use Cloud](https://cloud.browser-use.com?utm_source=github&utm_medium=readme-faq-captcha) which provides stealth browsers designed to avoid detection and CAPTCHA challenges.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -281,7 +279,7 @@ For CAPTCHA handling, you need better browser fingerprinting and proxies. Use [B
|
||||
|
||||
Chrome can consume a lot of memory, and running many agents in parallel can be tricky to manage.
|
||||
|
||||
For production use cases, use our [Browser Use Cloud API](https://cloud.browser-use.com) which handles:
|
||||
For production use cases, use our [Browser Use Cloud API](https://cloud.browser-use.com?utm_source=github&utm_medium=readme-faq-production) which handles:
|
||||
- Scalable browser infrastructure
|
||||
- Memory management
|
||||
- Proxy rotation
|
||||
|
||||
@@ -25,7 +25,12 @@ from browser_use.llm.messages import (
|
||||
UserMessage,
|
||||
)
|
||||
from browser_use.observability import observe_debug
|
||||
from browser_use.utils import match_url_with_domain_pattern, time_execution_sync
|
||||
from browser_use.utils import (
|
||||
collect_sensitive_data_values,
|
||||
match_url_with_domain_pattern,
|
||||
redact_sensitive_string,
|
||||
time_execution_sync,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -573,30 +578,14 @@ class MessageManager:
|
||||
if not self.sensitive_data:
|
||||
return value
|
||||
|
||||
# Collect all sensitive values, immediately converting old format to new format
|
||||
sensitive_values: dict[str, str] = {}
|
||||
|
||||
# Process all sensitive data entries
|
||||
for key_or_domain, content in self.sensitive_data.items():
|
||||
if isinstance(content, dict):
|
||||
# Already in new format: {domain: {key: value}}
|
||||
for key, val in content.items():
|
||||
if val: # Skip empty values
|
||||
sensitive_values[key] = val
|
||||
elif content: # Old format: {key: value} - convert to new format internally
|
||||
# We treat this as if it was {'http*://*': {key_or_domain: content}}
|
||||
sensitive_values[key_or_domain] = content
|
||||
sensitive_values = collect_sensitive_data_values(self.sensitive_data)
|
||||
|
||||
# If there are no valid sensitive data entries, just return the original value
|
||||
if not sensitive_values:
|
||||
logger.warning('No valid entries found in sensitive_data dictionary')
|
||||
return value
|
||||
|
||||
# Replace all valid sensitive data values with their placeholder tags
|
||||
for key, val in sensitive_values.items():
|
||||
value = value.replace(val, f'<secret>{key}</secret>')
|
||||
|
||||
return value
|
||||
return redact_sensitive_string(value, sensitive_values)
|
||||
|
||||
if isinstance(message.content, str):
|
||||
message.content = replace_sensitive(message.content)
|
||||
|
||||
@@ -1647,8 +1647,10 @@ class Agent(Generic[Context, AgentStructuredOutput]):
|
||||
if judgement.failure_reason:
|
||||
judge_log += f' Failure Reason: {judgement.failure_reason}\n'
|
||||
if judgement.reached_captcha:
|
||||
judge_log += ' 🤖 Captcha Detected: Agent encountered captcha challenges\n'
|
||||
judge_log += ' 👉 🥷 Use Browser Use Cloud for the most stealth browser infra: https://docs.browser-use.com/customize/browser/remote\n'
|
||||
self.logger.warning(
|
||||
'Agent was blocked by a captcha. Cloud browsers include stealth fingerprinting and proxy rotation to avoid this.\n'
|
||||
' Try: Browser(use_cloud=True) | Get an API key: https://cloud.browser-use.com?utm_source=oss&utm_medium=captcha_nudge'
|
||||
)
|
||||
judge_log += f' {judgement.reasoning}\n'
|
||||
self.logger.info(judge_log)
|
||||
|
||||
@@ -2160,11 +2162,10 @@ class Agent(Generic[Context, AgentStructuredOutput]):
|
||||
has_captcha_issue = any(keyword in final_result_str for keyword in captcha_keywords)
|
||||
|
||||
if has_captcha_issue:
|
||||
# Suggest use_cloud=True for captcha/cloudflare issues
|
||||
task_preview = self.task[:10] if len(self.task) > 10 else self.task
|
||||
self.logger.info('')
|
||||
self.logger.info('Failed because of CAPTCHA? For better browser stealth, try:')
|
||||
self.logger.info(f' agent = Agent(task="{task_preview}...", browser=Browser(use_cloud=True))')
|
||||
self.logger.warning(
|
||||
'Agent was blocked by a captcha. Cloud browsers include stealth fingerprinting and proxy rotation to avoid this.\n'
|
||||
' Try: Browser(use_cloud=True) | Get an API key: https://cloud.browser-use.com?utm_source=oss&utm_medium=captcha_nudge'
|
||||
)
|
||||
|
||||
# General failure message
|
||||
self.logger.info('')
|
||||
|
||||
@@ -27,6 +27,7 @@ from browser_use.filesystem.file_system import FileSystemState
|
||||
from browser_use.llm.base import BaseChatModel
|
||||
from browser_use.tokens.views import UsageSummary
|
||||
from browser_use.tools.registry.views import ActionModel
|
||||
from browser_use.utils import collect_sensitive_data_values, redact_sensitive_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -512,29 +513,13 @@ class AgentHistory(BaseModel):
|
||||
if not sensitive_data:
|
||||
return value
|
||||
|
||||
# Collect all sensitive values, immediately converting old format to new format
|
||||
sensitive_values: dict[str, str] = {}
|
||||
|
||||
# Process all sensitive data entries
|
||||
for key_or_domain, content in sensitive_data.items():
|
||||
if isinstance(content, dict):
|
||||
# Already in new format: {domain: {key: value}}
|
||||
for key, val in content.items():
|
||||
if val: # Skip empty values
|
||||
sensitive_values[key] = val
|
||||
elif content: # Old format: {key: value} - convert to new format internally
|
||||
# We treat this as if it was {'http*://*': {key_or_domain: content}}
|
||||
sensitive_values[key_or_domain] = content
|
||||
sensitive_values = collect_sensitive_data_values(sensitive_data)
|
||||
|
||||
# If there are no valid sensitive data entries, just return the original value
|
||||
if not sensitive_values:
|
||||
return value
|
||||
|
||||
# Replace all valid sensitive data values with their placeholder tags
|
||||
for key, val in sensitive_values.items():
|
||||
value = value.replace(val, f'<secret>{key}</secret>')
|
||||
|
||||
return value
|
||||
return redact_sensitive_string(value, sensitive_values)
|
||||
|
||||
def _filter_sensitive_data_from_dict(
|
||||
self, data: dict[str, Any], sensitive_data: dict[str, str | dict[str, str]] | None
|
||||
|
||||
@@ -50,7 +50,8 @@ class CloudBrowserClient:
|
||||
|
||||
if not api_token:
|
||||
raise CloudBrowserAuthError(
|
||||
'No authentication token found. Please set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
'BROWSER_USE_API_KEY is not set. To use cloud browsers, get a key at:\n'
|
||||
'https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=use_cloud'
|
||||
)
|
||||
|
||||
headers = {'X-Browser-Use-API-Key': api_token, 'Content-Type': 'application/json', **(extra_headers or {})}
|
||||
@@ -65,7 +66,8 @@ class CloudBrowserClient:
|
||||
|
||||
if response.status_code == 401:
|
||||
raise CloudBrowserAuthError(
|
||||
'Authentication failed. Please make sure you have set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
'BROWSER_USE_API_KEY is invalid. Get a new key at:\n'
|
||||
'https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=use_cloud'
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
raise CloudBrowserAuthError('Access forbidden. Please check your browser-use cloud subscription status.')
|
||||
@@ -137,7 +139,8 @@ class CloudBrowserClient:
|
||||
|
||||
if not api_token:
|
||||
raise CloudBrowserAuthError(
|
||||
'No authentication token found. Please set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
'BROWSER_USE_API_KEY is not set. To use cloud browsers, get a key at:\n'
|
||||
'https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=use_cloud'
|
||||
)
|
||||
|
||||
headers = {'X-Browser-Use-API-Key': api_token, 'Content-Type': 'application/json', **(extra_headers or {})}
|
||||
|
||||
@@ -753,9 +753,7 @@ class BrowserSession(BaseModel):
|
||||
self.browser_profile.is_local = False
|
||||
self.logger.info('🌤️ Successfully connected to cloud browser service')
|
||||
except CloudBrowserAuthError:
|
||||
raise CloudBrowserAuthError(
|
||||
'Authentication failed for cloud browser service. Set BROWSER_USE_API_KEY environment variable. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
)
|
||||
raise
|
||||
except CloudBrowserError as e:
|
||||
raise CloudBrowserError(f'Failed to create cloud browser: {e}')
|
||||
elif self.is_local:
|
||||
@@ -840,6 +838,11 @@ class BrowserSession(BaseModel):
|
||||
details={'cdp_url': self.cdp_url, 'is_local': self.is_local},
|
||||
)
|
||||
)
|
||||
if self.is_local and not isinstance(e, (CloudBrowserAuthError, CloudBrowserError)):
|
||||
self.logger.warning(
|
||||
'Local browser failed to start. Cloud browsers require no local install and work out of the box.\n'
|
||||
' Try: Browser(use_cloud=True) | Get an API key: https://cloud.browser-use.com?utm_source=oss&utm_medium=browser_launch_failure'
|
||||
)
|
||||
raise
|
||||
|
||||
async def on_NavigateToUrlEvent(self, event: NavigateToUrlEvent) -> None:
|
||||
|
||||
@@ -518,6 +518,11 @@ class DefaultActionWatchdog(BaseWatchdog):
|
||||
raise BrowserError(error_msg)
|
||||
|
||||
try:
|
||||
|
||||
def invalidate_dom_cache() -> None:
|
||||
if self.browser_session._dom_watchdog:
|
||||
self.browser_session._dom_watchdog.clear_cache()
|
||||
|
||||
# Convert direction and amount to pixels
|
||||
# Positive pixels = scroll down, negative = scroll up
|
||||
pixels = event.amount if event.direction == 'down' else -event.amount
|
||||
@@ -547,6 +552,7 @@ class DefaultActionWatchdog(BaseWatchdog):
|
||||
# Wait a bit for the scroll to settle and DOM to update
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
invalidate_dom_cache()
|
||||
return None
|
||||
|
||||
# Perform target-level scroll
|
||||
@@ -554,6 +560,7 @@ class DefaultActionWatchdog(BaseWatchdog):
|
||||
|
||||
# Note: We don't clear cached state here - let multi_act handle DOM change detection
|
||||
# by explicitly rebuilding and comparing when needed
|
||||
invalidate_dom_cache()
|
||||
|
||||
# Log success
|
||||
self.logger.debug(f'📜 Scrolled {event.direction} by {event.amount} pixels')
|
||||
|
||||
@@ -126,7 +126,7 @@ class LocalBrowserWatchdog(BaseWatchdog):
|
||||
self.logger.debug(f'[LocalBrowserWatchdog] 📦 Using custom local browser executable_path= {browser_path}')
|
||||
else:
|
||||
# self.logger.debug('[LocalBrowserWatchdog] 🔍 Looking for local browser binary path...')
|
||||
# Try fallback paths first (system browsers preferred)
|
||||
# Try fallback paths first (Playwright's Chromium preferred by default)
|
||||
browser_path = self._find_installed_browser_path(channel=profile.channel)
|
||||
if not browser_path:
|
||||
self.logger.error(
|
||||
@@ -224,9 +224,9 @@ class LocalBrowserWatchdog(BaseWatchdog):
|
||||
Falls back to all known browser paths if the channel-specific search fails.
|
||||
|
||||
Prioritizes:
|
||||
1. Channel-specific paths (if channel is set)
|
||||
2. System Chrome stable
|
||||
3. Playwright chromium
|
||||
1. Channel-specific paths (if channel is set to a non-default value)
|
||||
2. Playwright bundled Chromium (when no channel or default channel specified)
|
||||
3. System Chrome stable
|
||||
4. Other system native browsers (Chromium -> Chrome Canary/Dev -> Brave -> Edge)
|
||||
5. Playwright headless-shell fallback
|
||||
|
||||
@@ -313,14 +313,14 @@ class LocalBrowserWatchdog(BaseWatchdog):
|
||||
BrowserChannel.MSEDGE_CANARY: 'msedge',
|
||||
}
|
||||
|
||||
# If a non-default channel is specified, put matching patterns first, then the rest as fallback
|
||||
# Prioritize the target browser group, then fall back to the rest.
|
||||
if channel and channel != BROWSERUSE_DEFAULT_CHANNEL and channel in _channel_to_group:
|
||||
target_group = _channel_to_group[channel]
|
||||
prioritized = [p for g, p in all_patterns if g == target_group]
|
||||
rest = [p for g, p in all_patterns if g != target_group]
|
||||
patterns = prioritized + rest
|
||||
else:
|
||||
patterns = [p for _, p in all_patterns]
|
||||
target_group = _channel_to_group[BROWSERUSE_DEFAULT_CHANNEL]
|
||||
prioritized = [p for g, p in all_patterns if g == target_group]
|
||||
rest = [p for g, p in all_patterns if g != target_group]
|
||||
patterns = prioritized + rest
|
||||
|
||||
for pattern in patterns:
|
||||
# Expand user home directory
|
||||
@@ -362,7 +362,7 @@ class LocalBrowserWatchdog(BaseWatchdog):
|
||||
import platform
|
||||
|
||||
# Build command - only use --with-deps on Linux (it fails on Windows/macOS)
|
||||
cmd = ['uvx', 'playwright', 'install', 'chrome']
|
||||
cmd = ['uvx', 'playwright', 'install', 'chromium']
|
||||
if platform.system() == 'Linux':
|
||||
cmd.append('--with-deps')
|
||||
|
||||
@@ -380,7 +380,7 @@ class LocalBrowserWatchdog(BaseWatchdog):
|
||||
if browser_path:
|
||||
return browser_path
|
||||
self.logger.error(f'[LocalBrowserWatchdog] ❌ Playwright local browser installation error: \n{stdout}\n{stderr}')
|
||||
raise RuntimeError('No local browser path found after: uvx playwright install chrome')
|
||||
raise RuntimeError('No local browser path found after: uvx playwright install chromium')
|
||||
except TimeoutError:
|
||||
# Kill the subprocess if it times out
|
||||
process.kill()
|
||||
|
||||
@@ -129,7 +129,7 @@ if '--template' in sys.argv:
|
||||
click.echo(' uv pip install browser-use')
|
||||
click.echo(' 2. Set up your API key in .env file or environment:')
|
||||
click.echo(' BROWSER_USE_API_KEY=your-key')
|
||||
click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key)')
|
||||
click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=cli)')
|
||||
click.echo(' 3. Run your script:')
|
||||
click.echo(f' python {output_path.name}')
|
||||
except Exception as e:
|
||||
@@ -178,9 +178,12 @@ except ImportError:
|
||||
try:
|
||||
import readline
|
||||
|
||||
_add_history = getattr(readline, 'add_history', None)
|
||||
if _add_history is None:
|
||||
raise ImportError('readline missing add_history')
|
||||
READLINE_AVAILABLE = True
|
||||
except ImportError:
|
||||
# readline not available on Windows by default
|
||||
_add_history = None
|
||||
READLINE_AVAILABLE = False
|
||||
|
||||
|
||||
@@ -341,12 +344,11 @@ def update_config_with_click_args(config: dict[str, Any], ctx: click.Context) ->
|
||||
|
||||
def setup_readline_history(history: list[str]) -> None:
|
||||
"""Set up readline with command history."""
|
||||
if not READLINE_AVAILABLE:
|
||||
if not _add_history:
|
||||
return
|
||||
|
||||
# Add history items to readline
|
||||
for item in history:
|
||||
readline.add_history(item)
|
||||
_add_history(item)
|
||||
|
||||
|
||||
def get_llm(config: dict[str, Any]):
|
||||
@@ -718,9 +720,9 @@ class BrowserUseApp(App):
|
||||
# Step 2: Set up input history
|
||||
logger.debug('Setting up readline history...')
|
||||
try:
|
||||
if READLINE_AVAILABLE and self.task_history:
|
||||
if READLINE_AVAILABLE and self.task_history and _add_history:
|
||||
for item in self.task_history:
|
||||
readline.add_history(item)
|
||||
_add_history(item)
|
||||
logger.debug(f'Added {len(self.task_history)} items to readline history')
|
||||
else:
|
||||
logger.debug('No readline history to set up')
|
||||
@@ -1127,7 +1129,7 @@ class BrowserUseApp(App):
|
||||
|
||||
# Exit the application
|
||||
self.exit()
|
||||
print('\nTry running tasks on our cloud: https://browser-use.com')
|
||||
print('\nTry running tasks on our cloud: https://browser-use.com?utm_source=oss&utm_medium=cli')
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create the UI layout."""
|
||||
@@ -1142,7 +1144,11 @@ class BrowserUseApp(App):
|
||||
with Container(id='links-panel'):
|
||||
with HorizontalGroup(classes='link-row'):
|
||||
yield Static('Run at scale on cloud: [blink]☁️[/] ', markup=True, classes='link-label')
|
||||
yield Link('https://browser-use.com', url='https://browser-use.com', classes='link-white link-url')
|
||||
yield Link(
|
||||
'https://browser-use.com',
|
||||
url='https://browser-use.com?utm_source=oss&utm_medium=cli',
|
||||
classes='link-white link-url',
|
||||
)
|
||||
|
||||
yield Static('') # Empty line
|
||||
|
||||
@@ -2222,7 +2228,7 @@ def _run_template_generation(template: str, output: str | None, force: bool):
|
||||
click.echo(' uv pip install browser-use')
|
||||
click.echo(' 2. Set up your API key in .env file or environment:')
|
||||
click.echo(' BROWSER_USE_API_KEY=your-key')
|
||||
click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key)')
|
||||
click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=cli)')
|
||||
click.echo(' 3. Run your script:')
|
||||
click.echo(f' python {output_path.name}')
|
||||
else:
|
||||
@@ -2351,7 +2357,7 @@ def init(
|
||||
click.echo(' uv pip install browser-use')
|
||||
click.echo(' 2. Set up your API key in .env file or environment:')
|
||||
click.echo(' BROWSER_USE_API_KEY=your-key')
|
||||
click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key)')
|
||||
click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=cli)')
|
||||
click.echo(' 3. Run your script:')
|
||||
click.echo(f' python {output_path.name}')
|
||||
else:
|
||||
|
||||
@@ -1104,10 +1104,12 @@ class DomService:
|
||||
pagination_buttons: list[dict[str, str | int | bool]] = []
|
||||
|
||||
# Common pagination patterns to look for
|
||||
# `«` and `»` are ambiguous across sites, so treat them only as prev/next
|
||||
# fallback symbols and let word-based first/last signals win
|
||||
next_patterns = ['next', '>', '»', '→', 'siguiente', 'suivant', 'weiter', 'volgende']
|
||||
prev_patterns = ['prev', 'previous', '<', '«', '←', 'anterior', 'précédent', 'zurück', 'vorige']
|
||||
first_patterns = ['first', '⇤', '«', 'primera', 'première', 'erste', 'eerste']
|
||||
last_patterns = ['last', '⇥', '»', 'última', 'dernier', 'letzte', 'laatste']
|
||||
first_patterns = ['first', '⇤', 'primera', 'première', 'erste', 'eerste']
|
||||
last_patterns = ['last', '⇥', 'última', 'dernier', 'letzte', 'laatste']
|
||||
|
||||
for index, node in selector_map.items():
|
||||
# Skip non-clickable elements
|
||||
@@ -1133,18 +1135,18 @@ class DomService:
|
||||
|
||||
button_type: str | None = None
|
||||
|
||||
# Check for next button
|
||||
if any(pattern in all_text for pattern in next_patterns):
|
||||
button_type = 'next'
|
||||
# Check for previous button
|
||||
elif any(pattern in all_text for pattern in prev_patterns):
|
||||
button_type = 'prev'
|
||||
# Check for first button
|
||||
elif any(pattern in all_text for pattern in first_patterns):
|
||||
# Match specific first/last semantics before generic prev/next fallbacks.
|
||||
if any(pattern in all_text for pattern in first_patterns):
|
||||
button_type = 'first'
|
||||
# Check for last button
|
||||
elif any(pattern in all_text for pattern in last_patterns):
|
||||
button_type = 'last'
|
||||
# Check for next button
|
||||
elif any(pattern in all_text for pattern in next_patterns):
|
||||
button_type = 'next'
|
||||
# Check for previous button
|
||||
elif any(pattern in all_text for pattern in prev_patterns):
|
||||
button_type = 'prev'
|
||||
# Check for numeric page buttons (single or double digit)
|
||||
elif text.isdigit() and len(text) <= 2 and role in ['button', 'link', '']:
|
||||
button_type = 'page_number'
|
||||
|
||||
@@ -428,7 +428,7 @@ def main(
|
||||
next_steps.append('4. Set up your API key in .env file or environment:\n', style='bold')
|
||||
next_steps.append(' BROWSER_USE_API_KEY=your-key\n', style='dim')
|
||||
next_steps.append(
|
||||
' (Get your key at https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new)\n\n',
|
||||
' (Get your key at https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new&utm_source=oss&utm_medium=cli)\n\n',
|
||||
style='dim italic',
|
||||
)
|
||||
next_steps.append('5. Run your script:\n', style='bold')
|
||||
|
||||
@@ -90,8 +90,8 @@ class ChatBrowserUse(BaseChatModel):
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError(
|
||||
'You need to set the BROWSER_USE_API_KEY environment variable. '
|
||||
'Get your key at https://cloud.browser-use.com/new-api-key'
|
||||
'BROWSER_USE_API_KEY is not set. To use ChatBrowserUse, get a key at:\n'
|
||||
'https://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=chat_browser_use'
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -275,9 +275,17 @@ class ChatBrowserUse(BaseChatModel):
|
||||
status_code = e.response.status_code
|
||||
|
||||
if status_code == 401:
|
||||
raise ModelProviderError(message=f'Invalid API key. {error_detail}', status_code=401, model=self.name)
|
||||
raise ModelProviderError(
|
||||
message=f'BROWSER_USE_API_KEY is invalid. Get a new key at:\nhttps://cloud.browser-use.com/new-api-key?utm_source=oss&utm_medium=chat_browser_use\n{error_detail}',
|
||||
status_code=401,
|
||||
model=self.name,
|
||||
)
|
||||
elif status_code == 402:
|
||||
raise ModelProviderError(message=f'Insufficient credits. {error_detail}', status_code=402, model=self.name)
|
||||
raise ModelProviderError(
|
||||
message=f'Browser Use credits exhausted. Add more at:\nhttps://cloud.browser-use.com/billing?utm_source=oss&utm_medium=chat_browser_use\n{error_detail}',
|
||||
status_code=402,
|
||||
model=self.name,
|
||||
)
|
||||
elif status_code == 429:
|
||||
raise ModelRateLimitError(message=f'Rate limit exceeded. {error_detail}', status_code=429, model=self.name)
|
||||
elif status_code in {500, 502, 503, 504}:
|
||||
|
||||
@@ -1227,18 +1227,21 @@ class BrowserUseServer:
|
||||
raise RuntimeError('MCP stdio transport requires stdin, but this process was launched without one.')
|
||||
|
||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||
await self.server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name='browser-use',
|
||||
server_version='0.1.0',
|
||||
capabilities=self.server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
try:
|
||||
await self.server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name='browser-use',
|
||||
server_version='0.1.0',
|
||||
capabilities=self.server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
except BrokenPipeError:
|
||||
logger.warning('MCP client disconnected while writing to stdio; shutting down server cleanly.')
|
||||
|
||||
|
||||
async def main(session_timeout_minutes: int = 10):
|
||||
|
||||
@@ -90,7 +90,10 @@ def _get_api_key() -> str:
|
||||
print(' Note: BROWSER_USE_API_KEY env var is set but not used by the CLI.', file=sys.stderr)
|
||||
print(' Run: browser-use config set api_key "$BROWSER_USE_API_KEY"', file=sys.stderr)
|
||||
else:
|
||||
print('Already have an account? Get a key at: https://cloud.browser-use.com/settings?tab=api-keys&new=1', file=sys.stderr)
|
||||
print(
|
||||
'Already have an account? Get a key at: https://cloud.browser-use.com/settings?tab=api-keys&new=1&utm_source=oss&utm_medium=cli',
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(' Then run: browser-use cloud login <key>', file=sys.stderr)
|
||||
print('No account? Run: browser-use cloud signup', file=sys.stderr)
|
||||
print(' This creates an agent account you can claim later with: browser-use cloud signup --claim', file=sys.stderr)
|
||||
|
||||
@@ -1222,8 +1222,7 @@ def main() -> int:
|
||||
if args.command == 'doctor':
|
||||
from browser_use.skill_cli.commands import doctor
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
result = loop.run_until_complete(doctor.handle())
|
||||
result = asyncio.run(doctor.handle())
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result))
|
||||
@@ -1337,9 +1336,9 @@ def main() -> int:
|
||||
port_arg = getattr(args, 'port_arg', None)
|
||||
if getattr(args, 'all', False):
|
||||
# stop --all
|
||||
result = asyncio.get_event_loop().run_until_complete(tunnel.stop_all_tunnels())
|
||||
result = asyncio.run(tunnel.stop_all_tunnels())
|
||||
elif port_arg is not None:
|
||||
result = asyncio.get_event_loop().run_until_complete(tunnel.stop_tunnel(port_arg))
|
||||
result = asyncio.run(tunnel.stop_tunnel(port_arg))
|
||||
else:
|
||||
print('Usage: browser-use tunnel stop <port> | --all', file=sys.stderr)
|
||||
return 1
|
||||
@@ -1349,7 +1348,7 @@ def main() -> int:
|
||||
except ValueError:
|
||||
print(f'Unknown tunnel subcommand: {pos}', file=sys.stderr)
|
||||
return 1
|
||||
result = asyncio.get_event_loop().run_until_complete(tunnel.start_tunnel(port))
|
||||
result = asyncio.run(tunnel.start_tunnel(port))
|
||||
else:
|
||||
print('Usage: browser-use tunnel <port> | list | stop <port>', file=sys.stderr)
|
||||
return 0
|
||||
|
||||
@@ -4,9 +4,7 @@ import logging
|
||||
import os
|
||||
from typing import Any, Literal
|
||||
|
||||
from browser_use_sdk import AsyncBrowserUse
|
||||
from browser_use_sdk.types.execute_skill_response import ExecuteSkillResponse
|
||||
from browser_use_sdk.types.skill_list_response import SkillListResponse
|
||||
from browser_use_sdk import AsyncBrowserUse, ExecuteSkillResponse, SkillListResponse
|
||||
from cdp_use.cdp.network import Cookie
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
@@ -89,7 +87,7 @@ class SkillService:
|
||||
all_items.extend(skills_response.items)
|
||||
|
||||
# Check if we've found all requested skills
|
||||
found_ids = {s.id for s in all_items if s.id in requested_ids}
|
||||
found_ids = {str(s.id) for s in all_items if str(s.id) in requested_ids}
|
||||
if found_ids == requested_ids:
|
||||
break
|
||||
|
||||
@@ -114,10 +112,10 @@ class SkillService:
|
||||
skills_to_load = all_available_skills
|
||||
else:
|
||||
# Load only the requested skill IDs
|
||||
skills_to_load = [skill for skill in all_available_skills if skill.id in requested_ids]
|
||||
skills_to_load = [skill for skill in all_available_skills if str(skill.id) in requested_ids]
|
||||
|
||||
# Warn about any requested skills that weren't found
|
||||
found_ids = {skill.id for skill in skills_to_load}
|
||||
found_ids = {str(skill.id) for skill in skills_to_load}
|
||||
missing_ids = requested_ids - found_ids
|
||||
if missing_ids:
|
||||
logger.warning(f'Requested skills not found or not available: {missing_ids}')
|
||||
@@ -272,7 +270,10 @@ class SkillService:
|
||||
# Return error response
|
||||
return ExecuteSkillResponse(
|
||||
success=False,
|
||||
result=None,
|
||||
error=f'Failed to execute skill: {type(e).__name__}: {str(e)}',
|
||||
stderr=None,
|
||||
latencyMs=None,
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from browser_use_sdk.types.parameter_schema import ParameterSchema
|
||||
from browser_use_sdk.types.skill_response import SkillResponse
|
||||
from browser_use_sdk import ParameterSchema, SkillResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
@@ -40,7 +39,7 @@ class Skill(BaseModel):
|
||||
def from_skill_response(response: SkillResponse) -> 'Skill':
|
||||
"""Create a Skill from SDK SkillResponse"""
|
||||
return Skill(
|
||||
id=response.id,
|
||||
id=str(response.id),
|
||||
title=response.title,
|
||||
description=response.description,
|
||||
parameters=response.parameters,
|
||||
|
||||
@@ -31,6 +31,30 @@ _openai_bad_request_error: type | None = None
|
||||
_groq_bad_request_error: type | None = None
|
||||
|
||||
|
||||
def collect_sensitive_data_values(sensitive_data: dict[str, str | dict[str, str]] | None) -> dict[str, str]:
|
||||
"""Flatten legacy and domain-scoped sensitive data into placeholder -> value mappings."""
|
||||
if not sensitive_data:
|
||||
return {}
|
||||
|
||||
sensitive_values: dict[str, str] = {}
|
||||
for key_or_domain, content in sensitive_data.items():
|
||||
if isinstance(content, dict):
|
||||
for key, val in content.items():
|
||||
if val:
|
||||
sensitive_values[key] = val
|
||||
elif content:
|
||||
sensitive_values[key_or_domain] = content
|
||||
|
||||
return sensitive_values
|
||||
|
||||
|
||||
def redact_sensitive_string(value: str, sensitive_values: dict[str, str]) -> str:
|
||||
"""Replace sensitive values with placeholders, longest matches first to avoid partial leaks."""
|
||||
for key, secret in sorted(sensitive_values.items(), key=lambda item: len(item[1]), reverse=True):
|
||||
value = value.replace(secret, f'<secret>{key}</secret>')
|
||||
return value
|
||||
|
||||
|
||||
def _get_openai_bad_request_error() -> type | None:
|
||||
"""Lazy loader for OpenAI BadRequestError."""
|
||||
global _openai_bad_request_error
|
||||
|
||||
@@ -45,7 +45,7 @@ dependencies = [
|
||||
"cloudpickle==3.1.2",
|
||||
"markdownify==1.2.2",
|
||||
"python-docx==1.2.0",
|
||||
"browser-use-sdk==2.0.15",
|
||||
"browser-use-sdk==3.4.2",
|
||||
]
|
||||
# google-api-core: only used for Google LLM APIs
|
||||
# pyperclip: only used for examples that use copy/paste
|
||||
|
||||
@@ -95,7 +95,7 @@ class TestCloudBrowserClient:
|
||||
with pytest.raises(CloudBrowserAuthError) as exc_info:
|
||||
await client.create_browser(CreateBrowserRequest())
|
||||
|
||||
assert 'BROWSER_USE_API_KEY environment variable' in str(exc_info.value)
|
||||
assert 'BROWSER_USE_API_KEY is not set' in str(exc_info.value)
|
||||
|
||||
async def test_create_browser_http_401(self, mock_auth_config, monkeypatch):
|
||||
"""Test cloud browser creation with HTTP 401 response."""
|
||||
@@ -118,7 +118,7 @@ class TestCloudBrowserClient:
|
||||
with pytest.raises(CloudBrowserAuthError) as exc_info:
|
||||
await client.create_browser(CreateBrowserRequest())
|
||||
|
||||
assert 'Authentication failed' in str(exc_info.value)
|
||||
assert 'BROWSER_USE_API_KEY is invalid' in str(exc_info.value)
|
||||
|
||||
async def test_create_browser_with_env_var(self, temp_config_dir, monkeypatch):
|
||||
"""Test cloud browser creation using BROWSER_USE_API_KEY environment variable."""
|
||||
|
||||
Reference in New Issue
Block a user