mirror of
https://github.com/browser-use/browser-use
synced 2026-04-22 17:45:09 +02:00
Merge branch 'main' into codex/ci-pin-stale-bot-sha
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user