Skip to content

Commit 4c87694

Browse files
committed
add browser-use agent run
1 parent 0d259ef commit 4c87694

18 files changed

+1340
-520
lines changed
File renamed without changes.

src/browser/custom_browser.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@
99
Playwright,
1010
async_playwright,
1111
)
12-
from browser_use.browser.browser import Browser
12+
from browser_use.browser.browser import Browser, IN_DOCKER
1313
from browser_use.browser.context import BrowserContext, BrowserContextConfig
1414
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
1515
import logging
1616

17+
from browser_use.browser.chrome import (
18+
CHROME_ARGS,
19+
CHROME_DETERMINISTIC_RENDERING_ARGS,
20+
CHROME_DISABLE_SECURITY_ARGS,
21+
CHROME_DOCKER_ARGS,
22+
CHROME_HEADLESS_ARGS,
23+
)
24+
from browser_use.browser.context import BrowserContext, BrowserContextConfig
25+
from browser_use.browser.utils.screen_resolution import get_screen_resolution, get_window_adjustments
26+
from browser_use.utils import time_execution_async
27+
import socket
28+
1729
from .custom_context import CustomBrowserContext
1830

1931
logger = logging.getLogger(__name__)
@@ -26,3 +38,62 @@ async def new_context(
2638
config: BrowserContextConfig = BrowserContextConfig()
2739
) -> CustomBrowserContext:
2840
return CustomBrowserContext(config=config, browser=self)
41+
42+
async def _setup_builtin_browser(self, playwright: Playwright) -> PlaywrightBrowser:
43+
"""Sets up and returns a Playwright Browser instance with anti-detection measures."""
44+
assert self.config.browser_binary_path is None, 'browser_binary_path should be None if trying to use the builtin browsers'
45+
46+
if self.config.headless:
47+
screen_size = {'width': 1920, 'height': 1080}
48+
offset_x, offset_y = 0, 0
49+
else:
50+
screen_size = get_screen_resolution()
51+
offset_x, offset_y = get_window_adjustments()
52+
53+
chrome_args = {
54+
*CHROME_ARGS,
55+
*(CHROME_DOCKER_ARGS if IN_DOCKER else []),
56+
*(CHROME_HEADLESS_ARGS if self.config.headless else []),
57+
*(CHROME_DISABLE_SECURITY_ARGS if self.config.disable_security else []),
58+
*(CHROME_DETERMINISTIC_RENDERING_ARGS if self.config.deterministic_rendering else []),
59+
f'--window-position={offset_x},{offset_y}',
60+
*self.config.extra_browser_args,
61+
}
62+
contain_window_size = False
63+
for arg in self.config.extra_browser_args:
64+
if "--window-size" in arg:
65+
contain_window_size = True
66+
break
67+
if not contain_window_size:
68+
chrome_args.add(f'--window-size={screen_size["width"]},{screen_size["height"]}')
69+
70+
# check if port 9222 is already taken, if so remove the remote-debugging-port arg to prevent conflicts
71+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
72+
if s.connect_ex(('localhost', 9222)) == 0:
73+
chrome_args.remove('--remote-debugging-port=9222')
74+
75+
browser_class = getattr(playwright, self.config.browser_class)
76+
args = {
77+
'chromium': list(chrome_args),
78+
'firefox': [
79+
*{
80+
'-no-remote',
81+
*self.config.extra_browser_args,
82+
}
83+
],
84+
'webkit': [
85+
*{
86+
'--no-startup-window',
87+
*self.config.extra_browser_args,
88+
}
89+
],
90+
}
91+
92+
browser = await browser_class.launch(
93+
headless=self.config.headless,
94+
args=args[self.config.browser_class],
95+
proxy=self.config.proxy.model_dump() if self.config.proxy else None,
96+
handle_sigterm=False,
97+
handle_sigint=False,
98+
)
99+
return browser

src/browser/custom_context.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,112 @@
22
import logging
33
import os
44

5-
from browser_use.browser.browser import Browser
5+
from browser_use.browser.browser import Browser, IN_DOCKER
66
from browser_use.browser.context import BrowserContext, BrowserContextConfig
77
from playwright.async_api import Browser as PlaywrightBrowser
88
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
99

1010
logger = logging.getLogger(__name__)
1111

1212

13+
class CustomBrowserContextConfig(BrowserContextConfig):
14+
force_new_context: bool = False # force to create new context
15+
16+
1317
class CustomBrowserContext(BrowserContext):
1418
def __init__(
1519
self,
1620
browser: "Browser",
17-
config: BrowserContextConfig = BrowserContextConfig()
21+
config: CustomBrowserContextConfig = CustomBrowserContextConfig(),
1822
):
1923
super(CustomBrowserContext, self).__init__(browser=browser, config=config)
24+
25+
async def _create_context(self, browser: PlaywrightBrowser):
26+
"""Creates a new browser context with anti-detection measures and loads cookies if available."""
27+
if not self.config.force_new_context and self.browser.config.cdp_url and len(browser.contexts) > 0:
28+
context = browser.contexts[0]
29+
elif not self.config.force_new_context and self.browser.config.browser_binary_path and len(
30+
browser.contexts) > 0:
31+
# Connect to existing Chrome instance instead of creating new one
32+
context = browser.contexts[0]
33+
else:
34+
# Original code for creating new context
35+
context = await browser.new_context(
36+
no_viewport=True,
37+
user_agent=self.config.user_agent,
38+
java_script_enabled=True,
39+
bypass_csp=self.config.disable_security,
40+
ignore_https_errors=self.config.disable_security,
41+
record_video_dir=self.config.save_recording_path,
42+
record_video_size=self.config.browser_window_size.model_dump(),
43+
record_har_path=self.config.save_har_path,
44+
locale=self.config.locale,
45+
http_credentials=self.config.http_credentials,
46+
is_mobile=self.config.is_mobile,
47+
has_touch=self.config.has_touch,
48+
geolocation=self.config.geolocation,
49+
permissions=self.config.permissions,
50+
timezone_id=self.config.timezone_id,
51+
)
52+
53+
if self.config.trace_path:
54+
await context.tracing.start(screenshots=True, snapshots=True, sources=True)
55+
56+
# Load cookies if they exist
57+
if self.config.cookies_file and os.path.exists(self.config.cookies_file):
58+
with open(self.config.cookies_file, 'r') as f:
59+
try:
60+
cookies = json.load(f)
61+
62+
valid_same_site_values = ['Strict', 'Lax', 'None']
63+
for cookie in cookies:
64+
if 'sameSite' in cookie:
65+
if cookie['sameSite'] not in valid_same_site_values:
66+
logger.warning(
67+
f"Fixed invalid sameSite value '{cookie['sameSite']}' to 'None' for cookie {cookie.get('name')}"
68+
)
69+
cookie['sameSite'] = 'None'
70+
logger.info(f'🍪 Loaded {len(cookies)} cookies from {self.config.cookies_file}')
71+
await context.add_cookies(cookies)
72+
73+
except json.JSONDecodeError as e:
74+
logger.error(f'Failed to parse cookies file: {str(e)}')
75+
76+
# Expose anti-detection scripts
77+
await context.add_init_script(
78+
"""
79+
// Webdriver property
80+
Object.defineProperty(navigator, 'webdriver', {
81+
get: () => undefined
82+
});
83+
84+
// Languages
85+
Object.defineProperty(navigator, 'languages', {
86+
get: () => ['en-US']
87+
});
88+
89+
// Plugins
90+
Object.defineProperty(navigator, 'plugins', {
91+
get: () => [1, 2, 3, 4, 5]
92+
});
93+
94+
// Chrome runtime
95+
window.chrome = { runtime: {} };
96+
97+
// Permissions
98+
const originalQuery = window.navigator.permissions.query;
99+
window.navigator.permissions.query = (parameters) => (
100+
parameters.name === 'notifications' ?
101+
Promise.resolve({ state: Notification.permission }) :
102+
originalQuery(parameters)
103+
);
104+
(function () {
105+
const originalAttachShadow = Element.prototype.attachShadow;
106+
Element.prototype.attachShadow = function attachShadow(options) {
107+
return originalAttachShadow.call(this, { ...options, mode: "open" });
108+
};
109+
})();
110+
"""
111+
)
112+
113+
return context

src/controller/custom_controller.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,6 @@ def __init__(self, exclude_actions: list[str] = [],
4848
self.mcp_client = None
4949
self.mcp_server_config = None
5050

51-
async def setup_mcp_client(self, mcp_server_config: Optional[Dict[str, Any]] = None):
52-
self.mcp_server_config = mcp_server_config
53-
if self.mcp_server_config:
54-
self.mcp_client = await setup_mcp_client_and_tools(self.mcp_server_config)
55-
self.register_mcp_tools()
56-
57-
def register_mcp_tools(self):
58-
"""
59-
Register the MCP tools used by this controller.
60-
"""
61-
if self.mcp_client:
62-
for server_name in self.mcp_client.server_name_to_tools:
63-
for tool in self.mcp_client.server_name_to_tools[server_name]:
64-
tool_name = f"mcp.{server_name}.{tool.name}"
65-
self.registry.registry.actions[tool_name] = RegisteredAction(
66-
name=tool_name,
67-
description=tool.description,
68-
function=tool,
69-
param_model=create_tool_param_model(tool),
70-
)
71-
logger.info(f"Add mcp tool: {tool_name}")
72-
7351
def _register_custom_actions(self):
7452
"""Register all custom browser actions"""
7553

@@ -173,6 +151,28 @@ async def act(
173151
except Exception as e:
174152
raise e
175153

154+
async def setup_mcp_client(self, mcp_server_config: Optional[Dict[str, Any]] = None):
155+
self.mcp_server_config = mcp_server_config
156+
if self.mcp_server_config:
157+
self.mcp_client = await setup_mcp_client_and_tools(self.mcp_server_config)
158+
self.register_mcp_tools()
159+
160+
def register_mcp_tools(self):
161+
"""
162+
Register the MCP tools used by this controller.
163+
"""
164+
if self.mcp_client:
165+
for server_name in self.mcp_client.server_name_to_tools:
166+
for tool in self.mcp_client.server_name_to_tools[server_name]:
167+
tool_name = f"mcp.{server_name}.{tool.name}"
168+
self.registry.registry.actions[tool_name] = RegisteredAction(
169+
name=tool_name,
170+
description=tool.description,
171+
function=tool,
172+
param_model=create_tool_param_model(tool),
173+
)
174+
logger.info(f"Add mcp tool: {tool_name}")
175+
176176
async def close_mcp_client(self):
177177
if self.mcp_client:
178178
await self.mcp_client.__aexit__(None, None, None)

src/utils/mcp_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ async def setup_mcp_client_and_tools(mcp_server_config: Dict[str, Any]) -> Optio
4040

4141
logger.info("Initializing MultiServerMCPClient...")
4242

43+
if not mcp_server_config:
44+
logger.error("No MCP server configuration provided.")
45+
return None
46+
4347
try:
48+
if "mcpServers" in mcp_server_config:
49+
mcp_server_config = mcp_server_config["mcpServers"]
4450
client = MultiServerMCPClient(mcp_server_config)
4551
await client.__aenter__()
4652
return client

0 commit comments

Comments
 (0)