From f67116a65d5e78776a5dc55cee580e83e82df2a4 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 19 Nov 2025 21:56:18 +0800 Subject: [PATCH] update --- backend/app/model/chat.py | 1 + backend/app/utils/agent.py | 29 ++++- electron/main/index.ts | 153 +++++++++++++++++++++------ electron/preload/index.ts | 6 ++ src/pages/Dashboard/Browser.tsx | 182 +++++++++++++++++++++++++++++--- src/store/chatStore.ts | 2 + 6 files changed, 327 insertions(+), 46 deletions(-) diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index 9f359575..f3a84de4 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -48,6 +48,7 @@ class Chat(BaseModel): language: str = "en" browser_port: int = 9222 use_external_cdp: bool = False + cdp_browsers: list[dict] = [] max_retries: int = 3 allow_local_system: bool = False installed_mcp: McpServers = {"mcpServers": {}} diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 4f3a0918..43ca01e3 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -70,6 +70,9 @@ from app.service.task import set_process_task NOW_STR = datetime.datetime.now().strftime("%Y-%m-%d %H:00:00") +# Global counter for round-robin browser selection from pool +_browser_selection_counter = 0 + class ListenChatAgent(ChatAgent): @traceroot.trace() @@ -729,9 +732,31 @@ def search_agent(options: Chat): message_handler=HumanToolkit(options.project_id, Agents.search_agent).send_message_to_user ) + # Browser selection logic from CDP browser pool + selected_port = env('browser_port', '9222') + selected_is_external = options.use_external_cdp if hasattr(options, 'use_external_cdp') else False + + # If CDP browser pool is available and not empty, select a browser from the pool + if hasattr(options, 'cdp_browsers') and options.cdp_browsers: + global _browser_selection_counter + # Use round-robin selection from the pool + selected_browser = options.cdp_browsers[_browser_selection_counter % len(options.cdp_browsers)] + _browser_selection_counter += 1 + + selected_port = selected_browser.get('port', selected_port) + selected_is_external = selected_browser.get('isExternal', False) + + traceroot_logger.info( + f"Selected browser from pool: port={selected_port}, " + f"isExternal={selected_is_external}, " + f"name={selected_browser.get('name', 'Unnamed')}" + ) + else: + traceroot_logger.info(f"No CDP browser pool available, using default port: {selected_port}") + # Use cdp_keep_current_page=True only when using external CDP browser # to preserve the current page. For internal browser, use False (default behavior) - use_keep_current_page = options.use_external_cdp if hasattr(options, 'use_external_cdp') else False + use_keep_current_page = selected_is_external # When cdp_keep_current_page=True, don't set default_start_url (conflicts with keeping current page) # When cdp_keep_current_page=False, use "about:blank" as default start URL @@ -745,7 +770,7 @@ def search_agent(options: Chat): session_id=str(uuid.uuid4())[:8], default_start_url=default_url, connect_over_cdp=True, - cdp_url=f"http://localhost:{env('browser_port', '9222')}", + cdp_url=f"http://localhost:{selected_port}", cdp_keep_current_page=use_keep_current_page, enabled_tools=[ "browser_open", diff --git a/electron/main/index.ts b/electron/main/index.ts index c8b2a7fc..6bfa5f9a 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -40,7 +40,19 @@ let python_process: ChildProcessWithoutNullStreams | null = null; let backendPort: number = 5001; let browser_port = 9222; let use_external_cdp = false; // Flag to track if using external CDP browser -let cdp_browser_process: ChildProcessWithoutNullStreams | null = null; + +// CDP Browser Pool +interface CdpBrowser { + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; +} +let cdp_browser_pool: CdpBrowser[] = []; + +// Map to store multiple browser processes by port +let cdp_browser_processes: Map = new Map(); // Protocol URL queue for handling URLs before window is ready let protocolUrlQueue: string[] = []; @@ -290,6 +302,89 @@ function registerIpcHandlers() { return use_external_cdp }); + // ==================== CDP Browser Pool Management ==================== + + // Get all browsers in the pool + ipcMain.handle('get-cdp-browsers', () => { + log.info(`Getting CDP browser pool, count: ${cdp_browser_pool.length}`) + return cdp_browser_pool + }); + + // Get running browser processes + ipcMain.handle('get-running-browser-ports', () => { + const runningPorts = Array.from(cdp_browser_processes.keys()); + log.info(`Getting running browser ports: ${runningPorts.join(', ')}`) + return runningPorts; + }); + + // Add browser to pool + ipcMain.handle('add-cdp-browser', (event, port: number, isExternal: boolean, name?: string) => { + log.info(`Adding CDP browser: port=${port}, external=${isExternal}, name=${name}`) + + // Check if browser with this port already exists + const existing = cdp_browser_pool.find(b => b.port === port); + if (existing) { + return { success: false, error: 'Browser with this port already exists' }; + } + + const newBrowser: CdpBrowser = { + id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + port, + isExternal, + name, + addedAt: Date.now(), + }; + + cdp_browser_pool.push(newBrowser); + log.info(`Browser added to pool, new count: ${cdp_browser_pool.length}`) + + return { success: true, browser: newBrowser }; + }); + + // Remove browser from pool + ipcMain.handle('remove-cdp-browser', (event, browserId: string) => { + log.info(`Removing CDP browser: ${browserId}`) + + const index = cdp_browser_pool.findIndex(b => b.id === browserId); + if (index === -1) { + return { success: false, error: 'Browser not found' }; + } + + const removed = cdp_browser_pool.splice(index, 1)[0]; + + // If it's a launched browser, kill the process + if (!removed.isExternal && cdp_browser_processes.has(removed.port)) { + log.info(`Killing browser process on port ${removed.port}`); + try { + const process = cdp_browser_processes.get(removed.port); + process?.kill(); + cdp_browser_processes.delete(removed.port); + } catch (error) { + log.warn(`Failed to kill browser process on port ${removed.port}: ${error}`); + } + } + + log.info(`Browser removed from pool, remaining count: ${cdp_browser_pool.length}`) + + return { success: true, browser: removed }; + }); + + // Update browser in pool + ipcMain.handle('update-cdp-browser', (event, browserId: string, updates: Partial) => { + log.info(`Updating CDP browser: ${browserId}`) + + const browser = cdp_browser_pool.find(b => b.id === browserId); + if (!browser) { + return { success: false, error: 'Browser not found' }; + } + + // Update allowed fields + if (updates.name !== undefined) browser.name = updates.name; + + log.info(`Browser updated in pool`) + return { success: true, browser }; + }); + // Check if CDP port is available ipcMain.handle('check-cdp-port', async (event, port: number) => { log.info(`Checking CDP port availability: ${port}`); @@ -358,32 +453,25 @@ function registerIpcHandlers() { }; } - // Create/clear user data directory - const userDataDir = path.join(app.getPath('userData'), 'cdp_browser_profile'); + // Create user data directory with port number in name + // This allows multiple browsers on different ports to maintain separate profiles + const userDataDir = path.join(app.getPath('userData'), `cdp_browser_profile_${port}`); - // Clear the directory if it exists and is not empty - if (existsSync(userDataDir)) { - log.info(`Clearing existing user data directory: ${userDataDir}`); - try { - await fsp.rm(userDataDir, { recursive: true, force: true }); - } catch (error) { - log.warn(`Failed to clear user data directory: ${error}`); - } + // Create directory if it doesn't exist (preserve existing data) + if (!existsSync(userDataDir)) { + await fsp.mkdir(userDataDir, { recursive: true }); + log.info(`Created new user data directory: ${userDataDir}`); + } else { + log.info(`Using existing user data directory: ${userDataDir}`); } - // Create fresh directory - await fsp.mkdir(userDataDir, { recursive: true }); - log.info(`Created fresh user data directory: ${userDataDir}`); - - // Kill existing CDP browser process if any - if (cdp_browser_process) { - log.info('Killing existing CDP browser process'); - try { - cdp_browser_process.kill(); - } catch (error) { - log.warn(`Failed to kill existing process: ${error}`); - } - cdp_browser_process = null; + // Check if browser on this port is already running + if (cdp_browser_processes.has(port)) { + log.warn(`Browser process already exists on port ${port}`); + return { + success: false, + error: `Browser already running on port ${port}`, + }; } // Chrome launch arguments @@ -399,21 +487,24 @@ function registerIpcHandlers() { log.info(`Launching Chrome with args: ${args.join(' ')}`); // Spawn Chrome process - cdp_browser_process = spawn(chromeExecutable, args, { + const browserProcess = spawn(chromeExecutable, args, { detached: false, stdio: 'ignore', }); - cdp_browser_process.on('error', (error) => { - log.error(`CDP browser process error: ${error}`); - cdp_browser_process = null; + browserProcess.on('error', (error) => { + log.error(`CDP browser process on port ${port} error: ${error}`); + cdp_browser_processes.delete(port); }); - cdp_browser_process.on('exit', (code) => { - log.info(`CDP browser process exited with code ${code}`); - cdp_browser_process = null; + browserProcess.on('exit', (code) => { + log.info(`CDP browser process on port ${port} exited with code ${code}`); + cdp_browser_processes.delete(port); }); + // Store the process in the Map + cdp_browser_processes.set(port, browserProcess); + // Wait a bit for browser to start await new Promise(resolve => setTimeout(resolve, 2000)); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index b09393bc..f886f27b 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -93,6 +93,12 @@ contextBridge.exposeInMainWorld('electronAPI', { launchCdpBrowser: (port: number) => ipcRenderer.invoke('launch-cdp-browser', port), setBrowserPort: (port: number, isExternal?: boolean) => ipcRenderer.invoke('set-browser-port', port, isExternal), getUseExternalCdp: () => ipcRenderer.invoke('get-use-external-cdp'), + // CDP Browser Pool + getCdpBrowsers: () => ipcRenderer.invoke('get-cdp-browsers'), + getRunningBrowserPorts: () => ipcRenderer.invoke('get-running-browser-ports'), + addCdpBrowser: (port: number, isExternal: boolean, name?: string) => ipcRenderer.invoke('add-cdp-browser', port, isExternal, name), + removeCdpBrowser: (browserId: string) => ipcRenderer.invoke('remove-cdp-browser', browserId), + updateCdpBrowser: (browserId: string, updates: any) => ipcRenderer.invoke('update-cdp-browser', browserId, updates), }); diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index c6b67513..3a906b73 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -26,6 +26,14 @@ interface CdpPortStatus { data?: any; } +interface CdpBrowser { + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; +} + export default function Browser() { const { t } = useTranslation(); const [loginLoading, setLoginLoading] = useState(false); @@ -50,6 +58,11 @@ export default function Browser() { const [showLaunchNewDialog, setShowLaunchNewDialog] = useState(false); const [pendingPort, setPendingPort] = useState(null); + // CDP Browser Pool + const [cdpBrowsers, setCdpBrowsers] = useState([]); + const [deletingBrowser, setDeletingBrowser] = useState(null); + const [runningPorts, setRunningPorts] = useState([]); + // Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com") const getMainDomain = (domain: string): string => { // Remove leading dot if present @@ -89,6 +102,8 @@ export default function Browser() { handleLoadCookies(); // Load current browser port on mount loadCurrentBrowserPort(); + // Load CDP browser pool + loadCdpBrowsers(); }, []); const loadCurrentBrowserPort = async () => { @@ -99,6 +114,39 @@ export default function Browser() { } }; + const loadCdpBrowsers = async () => { + if (window.electronAPI?.getCdpBrowsers) { + try { + const browsers = await window.electronAPI.getCdpBrowsers(); + setCdpBrowsers(browsers); + + // Also load running browser ports + if (window.electronAPI?.getRunningBrowserPorts) { + const ports = await window.electronAPI.getRunningBrowserPorts(); + setRunningPorts(ports); + } + } catch (error) { + console.error("Failed to load CDP browsers:", error); + } + } + }; + + // Periodically refresh running browser ports + useEffect(() => { + const interval = setInterval(async () => { + if (window.electronAPI?.getRunningBrowserPorts) { + try { + const ports = await window.electronAPI.getRunningBrowserPorts(); + setRunningPorts(ports); + } catch (error) { + console.error("Failed to refresh running ports:", error); + } + } + }, 3000); // Refresh every 3 seconds + + return () => clearInterval(interval); + }, []); + const handleCheckPort = async () => { const portNumber = parseInt(customPort); @@ -151,15 +199,18 @@ export default function Browser() { setShowUseExistingDialog(false); if (pendingPort) { try { - // Update the browser port in electron - // isExternal=true because we're using an existing external browser - if (window.electronAPI?.setBrowserPort) { - await window.electronAPI.setBrowserPort(pendingPort, true); + // Add browser to pool + if (window.electronAPI?.addCdpBrowser) { + const result = await window.electronAPI.addCdpBrowser(pendingPort, true, `External Browser (${pendingPort})`); + if (result.success) { + toast.success(`Added external browser on port ${pendingPort} to pool`); + await loadCdpBrowsers(); + } else { + toast.error(result.error || "Failed to add browser to pool"); + } } - setCdpPort(pendingPort); - toast.success(`Now using external browser on port ${pendingPort}`); } catch (error: any) { - toast.error(error.message || "Failed to set browser port"); + toast.error(error.message || "Failed to add browser to pool"); } } setPendingPort(null); @@ -186,13 +237,18 @@ export default function Browser() { const result = await window.electronAPI.launchCdpBrowser(port); if (result.success) { - // Update the browser port in electron - // isExternal=false because this is our own launched browser - if (window.electronAPI?.setBrowserPort) { - await window.electronAPI.setBrowserPort(port, false); - } - setCdpPort(port); toast.success(`Browser launched successfully on port ${port}`, { id: 'launch-browser' }); + + // Add launched browser to pool + if (window.electronAPI?.addCdpBrowser) { + const addResult = await window.electronAPI.addCdpBrowser(port, false, `Launched Browser (${port})`); + if (addResult.success) { + await loadCdpBrowsers(); + } else { + toast.error(addResult.error || "Failed to add browser to pool"); + } + } + // Update port status setPortStatus({ checking: false, @@ -207,6 +263,25 @@ export default function Browser() { } }; + const handleRemoveBrowser = async (browserId: string) => { + setDeletingBrowser(browserId); + try { + if (window.electronAPI?.removeCdpBrowser) { + const result = await window.electronAPI.removeCdpBrowser(browserId); + if (result.success) { + toast.success("Browser removed from pool"); + await loadCdpBrowsers(); + } else { + toast.error(result.error || "Failed to remove browser"); + } + } + } catch (error: any) { + toast.error(error.message || "Failed to remove browser"); + } finally { + setDeletingBrowser(null); + } + }; + const handleBrowserLogin = async () => { setLoginLoading(true); try { @@ -507,6 +582,87 @@ export default function Browser() { + {/* CDP Browser Pool Section */} +
+
+
+
+
+ CDP Browser Pool +
+ + {runningPorts.length} / {cdpBrowsers.length} Running + +
+

+ Manage multiple CDP browsers for task execution +

+
+
+ + {cdpBrowsers.length > 0 ? ( +
+ {cdpBrowsers.map((browser) => ( +
+
+
+ + {browser.name || `Browser ${browser.port}`} + + + {browser.isExternal ? 'External' : 'Launched'} + + {/* Running status indicator */} + {runningPorts.includes(browser.port) ? ( + + + Running + + ) : ( + !browser.isExternal && ( + + + Stopped + + ) + )} +
+ + Port: {browser.port} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ No browsers in pool +
+

+ Add browsers using the check port tool above +

+
+ )} +
+ {/* Cookies Section */}
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 361729ce..4c0be5f6 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -382,6 +382,7 @@ const chatStore = (initial?: Partial) => createStore()( } const browser_port = await window.ipcRenderer.invoke('get-browser-port'); const use_external_cdp = await window.ipcRenderer.invoke('get-use-external-cdp'); + const cdp_browsers = await window.ipcRenderer.invoke('get-cdp-browsers'); // Lock the chatStore reference at the start of SSE session to prevent focus changes // during active message processing @@ -428,6 +429,7 @@ const chatStore = (initial?: Partial) => createStore()( new_agents: [...addWorkers], browser_port: browser_port, use_external_cdp: use_external_cdp, + cdp_browsers: cdp_browsers, env_path: envPath, search_config: searchConfig }) : undefined,