From aa2dba17f1ab29177a201a356eac3ecb93ffe520 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 19 Nov 2025 01:22:39 +0800 Subject: [PATCH 01/40] update --- backend/app/model/chat.py | 1 + backend/app/utils/agent.py | 13 +- electron/main/index.ts | 183 +++++++++++++++++++++- electron/preload/index.ts | 4 + src/pages/Dashboard/Browser.tsx | 261 +++++++++++++++++++++++++++++++- src/store/chatStore.ts | 4 +- 6 files changed, 462 insertions(+), 4 deletions(-) diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index fc9010839..9f359575d 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -47,6 +47,7 @@ class Chat(BaseModel): api_url: str | None = None # for cloud version, user don't need to set api_url language: str = "en" browser_port: int = 9222 + use_external_cdp: bool = False 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 4cac7a7f0..4f3a09186 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -729,15 +729,26 @@ def search_agent(options: Chat): message_handler=HumanToolkit(options.project_id, Agents.search_agent).send_message_to_user ) + # 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 + + # 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 + default_url = None if use_keep_current_page else "about:blank" + web_toolkit_custom = HybridBrowserToolkit( options.project_id, headless=False, browser_log_to_file=True, stealth=True, session_id=str(uuid.uuid4())[:8], - default_start_url="about:blank", + default_start_url=default_url, + connect_over_cdp=True, cdp_url=f"http://localhost:{env('browser_port', '9222')}", + cdp_keep_current_page=use_keep_current_page, enabled_tools=[ + "browser_open", "browser_click", "browser_type", "browser_back", diff --git a/electron/main/index.ts b/electron/main/index.ts index db2c2abc8..c8b2a7fc0 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -7,7 +7,7 @@ import { update, registerUpdateIpcHandlers } from './update' import { checkToolInstalled, killProcessOnPort, startBackend } from './init' import { WebViewManager } from './webview' import { FileReader } from './fileReader' -import { ChildProcessWithoutNullStreams } from 'node:child_process' +import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import fs, { existsSync, readFileSync } from 'node:fs' import fsp from 'fs/promises' import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig' @@ -39,6 +39,8 @@ let fileReader: FileReader | null = null; 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; // Protocol URL queue for handling URLs before window is ready let protocolUrlQueue: string[] = []; @@ -273,6 +275,185 @@ function registerIpcHandlers() { log.info('Getting browser port') return browser_port }); + + // Set browser port + ipcMain.handle('set-browser-port', (event, port: number, isExternal: boolean = false) => { + log.info(`Setting browser port to ${port}, external: ${isExternal}`) + browser_port = port + use_external_cdp = isExternal + return { success: true, port: browser_port, use_external_cdp } + }); + + // Get external CDP flag + ipcMain.handle('get-use-external-cdp', () => { + log.info(`Getting use_external_cdp: ${use_external_cdp}`) + return use_external_cdp + }); + + // Check if CDP port is available + ipcMain.handle('check-cdp-port', async (event, port: number) => { + log.info(`Checking CDP port availability: ${port}`); + try { + const response = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 3000, + }); + + if (response.status === 200 && response.data) { + log.info(`CDP port ${port} is available and responsive`); + return { + available: true, + data: response.data, + }; + } + return { available: false, error: 'Invalid response from CDP' }; + } catch (error: any) { + log.warn(`CDP port ${port} is not available: ${error.message}`); + return { + available: false, + error: error.code === 'ECONNREFUSED' + ? 'Connection refused - no browser running on this port' + : error.message, + }; + } + }); + + // Launch CDP browser with custom port + ipcMain.handle('launch-cdp-browser', async (event, port: number) => { + log.info(`Launching CDP browser on port ${port}`); + + try { + const platform = process.platform; + let chromePath: string; + let chromeExecutable: string; + + // Determine Chrome path based on platform + if (platform === 'darwin') { + chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + if (!existsSync(chromePath)) { + return { + success: false, + error: 'Google Chrome not found at /Applications/Google Chrome.app', + }; + } + chromeExecutable = chromePath; + } else if (platform === 'win32') { + chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; + if (!existsSync(chromePath)) { + // Try alternative path + const altPath = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'; + if (existsSync(altPath)) { + chromePath = altPath; + } else { + return { + success: false, + error: 'Google Chrome not found', + }; + } + } + chromeExecutable = chromePath; + } else { + return { + success: false, + error: `Unsupported platform: ${platform}`, + }; + } + + // Create/clear user data directory + const userDataDir = path.join(app.getPath('userData'), 'cdp_browser_profile'); + + // 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 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; + } + + // Chrome launch arguments + const args = [ + `--remote-debugging-port=${port}`, + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--disable-blink-features=AutomationControlled', + 'about:blank', + ]; + + log.info(`Launching Chrome with args: ${args.join(' ')}`); + + // Spawn Chrome process + cdp_browser_process = spawn(chromeExecutable, args, { + detached: false, + stdio: 'ignore', + }); + + cdp_browser_process.on('error', (error) => { + log.error(`CDP browser process error: ${error}`); + cdp_browser_process = null; + }); + + cdp_browser_process.on('exit', (code) => { + log.info(`CDP browser process exited with code ${code}`); + cdp_browser_process = null; + }); + + // Wait a bit for browser to start + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Verify browser is accessible + try { + const response = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 5000, + }); + + if (response.status === 200) { + log.info(`CDP browser successfully launched on port ${port}`); + // This is our own launched browser, not external + use_external_cdp = false; + return { + success: true, + port, + data: response.data, + }; + } + } catch (verifyError) { + log.warn(`Failed to verify CDP browser: ${verifyError}`); + return { + success: false, + error: 'Browser launched but not responding on CDP port', + }; + } + + return { + success: true, + port, + }; + } catch (error: any) { + log.error(`Failed to launch CDP browser: ${error}`); + return { + success: false, + error: error.message, + }; + } + }); + ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-backend-port', () => backendPort); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 26d3ee438..b09393bcf 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -89,6 +89,10 @@ contextBridge.exposeInMainWorld('electronAPI', { }, getEmailFolderPath: (email: string) => ipcRenderer.invoke('get-email-folder-path', email), restartApp: () => ipcRenderer.invoke('restart-app'), + checkCdpPort: (port: number) => ipcRenderer.invoke('check-cdp-port', port), + 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'), }); diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index dffbeda38..c6b675136 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -1,10 +1,11 @@ import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { Globe, Cookie, Trash2, RefreshCw, RotateCw, Plus, EllipsisVertical } from "lucide-react"; +import { Globe, Cookie, Trash2, RefreshCw, RotateCw, Plus, EllipsisVertical, CheckCircle2, XCircle, Loader2 } from "lucide-react"; import { fetchPost, fetchGet, fetchDelete } from "@/api/http"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import AlertDialog from "@/components/ui/alertDialog"; +import { Input } from "@/components/ui/input"; interface CookieDomain { domain: string; @@ -18,6 +19,13 @@ interface GroupedDomain { totalCookies: number; } +interface CdpPortStatus { + checking: boolean; + available: boolean | null; + error?: string; + data?: any; +} + export default function Browser() { const { t } = useTranslation(); const [loginLoading, setLoginLoading] = useState(false); @@ -29,6 +37,19 @@ export default function Browser() { const [cookiesBeforeBrowser, setCookiesBeforeBrowser] = useState(0); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + // CDP port configuration + const [cdpPort, setCdpPort] = useState(9222); + const [customPort, setCustomPort] = useState("9222"); + const [portStatus, setPortStatus] = useState({ + checking: false, + available: null, + }); + + // Dialog states + const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); + const [showLaunchNewDialog, setShowLaunchNewDialog] = useState(false); + const [pendingPort, setPendingPort] = useState(null); + // 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 @@ -66,8 +87,126 @@ export default function Browser() { // Auto-load cookies on component mount useEffect(() => { handleLoadCookies(); + // Load current browser port on mount + loadCurrentBrowserPort(); }, []); + const loadCurrentBrowserPort = async () => { + if (window.ipcRenderer) { + const port = await window.ipcRenderer.invoke('get-browser-port'); + setCdpPort(port); + setCustomPort(String(port)); + } + }; + + const handleCheckPort = async () => { + const portNumber = parseInt(customPort); + + if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { + toast.error("Please enter a valid port number (1-65535)"); + return; + } + + setPortStatus({ checking: true, available: null }); + + try { + if (!window.electronAPI?.checkCdpPort) { + toast.error("CDP port check not available"); + setPortStatus({ checking: false, available: false, error: "Not available" }); + return; + } + + const result = await window.electronAPI.checkCdpPort(portNumber); + + if (result.available) { + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + // Browser exists, ask if user wants to use it + setPendingPort(portNumber); + setShowUseExistingDialog(true); + } else { + setPortStatus({ + checking: false, + available: false, + error: result.error, + }); + // No browser on this port, ask if user wants to launch one + setPendingPort(portNumber); + setShowLaunchNewDialog(true); + } + } catch (error: any) { + setPortStatus({ + checking: false, + available: false, + error: error.message, + }); + toast.error(error.message || "Failed to check port"); + } + }; + + const handleUseExistingBrowser = async () => { + 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); + } + setCdpPort(pendingPort); + toast.success(`Now using external browser on port ${pendingPort}`); + } catch (error: any) { + toast.error(error.message || "Failed to set browser port"); + } + } + setPendingPort(null); + }; + + const handleLaunchNewBrowser = async () => { + setShowLaunchNewDialog(false); + + if (!pendingPort) { + return; + } + + const port = pendingPort; + setPendingPort(null); + + try { + if (!window.electronAPI?.launchCdpBrowser) { + toast.error("Launch CDP browser not available"); + return; + } + + toast.loading(`Launching browser on port ${port}...`, { id: 'launch-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' }); + // Update port status + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + } else { + toast.error(result.error || "Failed to launch browser", { id: 'launch-browser' }); + } + } catch (error: any) { + toast.error(error.message || "Failed to launch browser", { id: 'launch-browser' }); + } + }; + const handleBrowserLogin = async () => { setLoginLoading(true); try { @@ -208,6 +347,36 @@ export default function Browser() { confirmVariant="information" /> + {/* Use Existing Browser Dialog */} + { + setShowUseExistingDialog(false); + setPendingPort(null); + }} + onConfirm={handleUseExistingBrowser} + title="Browser Found" + message={`A browser is running on port ${pendingPort}. Would you like to use it for browser operations?`} + confirmText="Yes, Use This Browser" + cancelText="Cancel" + confirmVariant="information" + /> + + {/* Launch New Browser Dialog */} + { + setShowLaunchNewDialog(false); + setPendingPort(null); + }} + onConfirm={handleLaunchNewBrowser} + title="No Browser Found" + message={`No browser is running on port ${pendingPort}. Would you like to launch a new Chrome browser with CDP enabled on this port?`} + confirmText="Yes, Launch Browser" + cancelText="Cancel" + confirmVariant="information" + /> + {/* Header Section */}
@@ -248,6 +417,96 @@ export default function Browser() {
{t("layout.browser-cookies")}

{t("layout.browser-cookies-description")}

+ + {/* CDP Port Configuration Section */} +
+
+
+
+ CDP Browser Connection +
+

+ Connect to a Chrome browser with remote debugging enabled +

+
+
+ +
+
+
+ Current Port: {cdpPort} +
+

+ Check if a browser is available on a specific port +

+
+ +
+ setCustomPort(e.target.value)} + className="flex-1" + min={1} + max={65535} + /> + +
+ + {portStatus.available !== null && ( +
+ {portStatus.available ? ( + <> + +
+
+ Browser Available +
+ {portStatus.data && ( +
+ {portStatus.data['Browser']} - {portStatus.data['User-Agent']?.split(' ')[0]} +
+ )} +
+ + ) : ( + <> + +
+
+ Browser Not Available +
+
+ {portStatus.error} +
+
+ + )} +
+ )} +
+
+ {/* Cookies Section */}
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index f50f99570..361729cec 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -381,7 +381,8 @@ 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'); + // Lock the chatStore reference at the start of SSE session to prevent focus changes // during active message processing let lockedChatStore = targetChatStore; @@ -426,6 +427,7 @@ const chatStore = (initial?: Partial) => createStore()( summary_prompt: ``, new_agents: [...addWorkers], browser_port: browser_port, + use_external_cdp: use_external_cdp, env_path: envPath, search_config: searchConfig }) : undefined, 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 02/40] 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 9f359575d..f3a84de45 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 4f3a09186..43ca01e3d 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -70,6 +70,9 @@ 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 c8b2a7fc0..6bfa5f9ad 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 b09393bcf..f886f27b9 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 c6b675136..3a906b739 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 361729cec..4c0be5f6e 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, From 8478ac3e5787b5739ddb460c31ec9902c7fcaaca Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 19 Nov 2025 22:12:31 +0800 Subject: [PATCH 03/40] update --- backend/app/utils/agent.py | 104 +++++++++++++++--- .../utils/toolkit/hybrid_browser_toolkit.py | 36 ++++++ 2 files changed, 126 insertions(+), 14 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 43ca01e3d..bf22e2f24 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -2,6 +2,7 @@ import json import os import platform +import threading from threading import Event import traceback from typing import Any, Callable, Dict, List, Tuple @@ -73,6 +74,71 @@ # Global counter for round-robin browser selection from pool _browser_selection_counter = 0 +# CDP Browser occupation management +class CdpBrowserPoolManager: + """Manages CDP browser pool occupation to ensure parallel tasks use different browsers.""" + + def __init__(self): + self._occupied_ports = {} # port -> session_id mapping + self._lock = threading.Lock() + + def acquire_browser(self, cdp_browsers: list[dict], session_id: str) -> dict | None: + """ + Acquire an available browser from the pool. + + Args: + cdp_browsers: List of browser configurations + session_id: Unique session identifier for this toolkit instance + + Returns: + Browser configuration dict or None if no browsers available + """ + with self._lock: + # Find first unoccupied browser + for browser in cdp_browsers: + port = browser.get('port') + if port and port not in self._occupied_ports: + self._occupied_ports[port] = session_id + traceroot_logger.info( + f"Acquired browser on port {port} for session {session_id}. " + f"Occupied: {list(self._occupied_ports.keys())}" + ) + return browser + + traceroot_logger.warning( + f"No available browsers in pool for session {session_id}. " + f"All occupied: {list(self._occupied_ports.keys())}" + ) + return None + + def release_browser(self, port: int, session_id: str): + """ + Release a browser back to the pool. + + Args: + port: Browser port to release + session_id: Session identifier + """ + with self._lock: + if port in self._occupied_ports and self._occupied_ports[port] == session_id: + del self._occupied_ports[port] + traceroot_logger.info( + f"Released browser on port {port} from session {session_id}. " + f"Occupied: {list(self._occupied_ports.keys())}" + ) + else: + traceroot_logger.warning( + f"Attempted to release browser on port {port} but it was not occupied by {session_id}" + ) + + def get_occupied_ports(self) -> list[int]: + """Get list of currently occupied ports.""" + with self._lock: + return list(self._occupied_ports.keys()) + +# Global CDP browser pool manager instance +_cdp_pool_manager = CdpBrowserPoolManager() + class ListenChatAgent(ChatAgent): @traceroot.trace() @@ -732,25 +798,35 @@ 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 + # Generate unique session ID for this toolkit instance + toolkit_session_id = str(uuid.uuid4())[:8] + + # Browser selection logic from CDP browser pool using occupation manager 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 CDP browser pool is available and not empty, acquire an available browser 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 + global _cdp_pool_manager + # Acquire an available (unoccupied) browser from the pool + selected_browser = _cdp_pool_manager.acquire_browser(options.cdp_browsers, toolkit_session_id) - selected_port = selected_browser.get('port', selected_port) - selected_is_external = selected_browser.get('isExternal', False) + if selected_browser: + 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')}" - ) + traceroot_logger.info( + f"Acquired browser from pool: port={selected_port}, " + f"isExternal={selected_is_external}, " + f"name={selected_browser.get('name', 'Unnamed')}, " + f"session={toolkit_session_id}" + ) + else: + # No available browsers - fall back to default or log warning + traceroot_logger.warning( + f"No available browsers in pool for session {toolkit_session_id}, " + f"using default port {selected_port}" + ) else: traceroot_logger.info(f"No CDP browser pool available, using default port: {selected_port}") @@ -767,7 +843,7 @@ def search_agent(options: Chat): headless=False, browser_log_to_file=True, stealth=True, - session_id=str(uuid.uuid4())[:8], + session_id=toolkit_session_id, # Use the session ID for pool management default_start_url=default_url, connect_over_cdp=True, cdp_url=f"http://localhost:{selected_port}", diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index f39cb9000..ebdebaaa4 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -217,6 +217,10 @@ async def close_all(self): class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): agent_name: str = Agents.search_agent + # CDP browser pool tracking + _cdp_port: int | None = None + _cdp_session_id: str | None = None + def __init__( self, api_task_id: str, @@ -283,6 +287,38 @@ def __init__( ) logger.info(f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}") + # Extract CDP port for pool management (if using CDP) + if connect_over_cdp and cdp_url: + try: + # Parse port from cdp_url (e.g., "http://localhost:9222") + import re + port_match = re.search(r':(\d+)', cdp_url) + if port_match: + self._cdp_port = int(port_match.group(1)) + self._cdp_session_id = session_id + logger.info( + f"[HybridBrowserToolkit] Registered CDP browser on port {self._cdp_port} " + f"for session {self._cdp_session_id}" + ) + except Exception as e: + logger.warning(f"[HybridBrowserToolkit] Failed to extract CDP port: {e}") + + def __del__(self): + """Release CDP browser back to pool when toolkit is destroyed.""" + if self._cdp_port is not None and self._cdp_session_id is not None: + try: + # Import here to avoid circular dependency + from app.utils.agent import _cdp_pool_manager + _cdp_pool_manager.release_browser(self._cdp_port, self._cdp_session_id) + logger.info( + f"[HybridBrowserToolkit] Released CDP browser on port {self._cdp_port} " + f"for session {self._cdp_session_id}" + ) + except Exception as e: + logger.warning( + f"[HybridBrowserToolkit] Failed to release CDP browser on port {self._cdp_port}: {e}" + ) + async def _ensure_ws_wrapper(self): """Ensure WebSocket wrapper is initialized using connection pool.""" logger.debug(f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}") From d792f54b43db41f494b4873c31b292fb8872a955 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 19 Nov 2025 23:46:45 +0800 Subject: [PATCH 04/40] update --- backend/app/service/chat_service.py | 6 ++ backend/app/utils/agent.py | 4 + .../utils/toolkit/hybrid_browser_toolkit.py | 69 +++++++++---- electron/main/index.ts | 96 ++++++++++++++----- src/store/chatStore.ts | 3 + 5 files changed, 135 insertions(+), 43 deletions(-) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 3a02b491f..e85cc9e89 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -234,6 +234,12 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str: @sync_step @traceroot.trace() async def step_solve(options: Chat, request: Request, task_lock: TaskLock): + # Log CDP browsers received from frontend + logger.info(f"[BACKEND CDP] Received request for project: {options.project_id}") + logger.info(f"[BACKEND CDP] browser_port: {options.browser_port}, use_external_cdp: {options.use_external_cdp}") + logger.info(f"[BACKEND CDP] cdp_browsers count: {len(options.cdp_browsers) if hasattr(options, 'cdp_browsers') else 'N/A'}") + logger.info(f"[BACKEND CDP] cdp_browsers: {options.cdp_browsers if hasattr(options, 'cdp_browsers') else 'N/A'}") + # if True: # import faulthandler diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index bf22e2f24..fbc1be4ec 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -806,8 +806,12 @@ def search_agent(options: Chat): selected_is_external = options.use_external_cdp if hasattr(options, 'use_external_cdp') else False # If CDP browser pool is available and not empty, acquire an available browser + traceroot_logger.info(f"Checking CDP browser pool: hasattr={hasattr(options, 'cdp_browsers')}, " + f"browsers={getattr(options, 'cdp_browsers', None)}") + if hasattr(options, 'cdp_browsers') and options.cdp_browsers: global _cdp_pool_manager + traceroot_logger.info(f"CDP browser pool available with {len(options.cdp_browsers)} browsers") # Acquire an available (unoccupied) browser from the pool selected_browser = _cdp_pool_manager.acquire_browser(options.cdp_browsers, toolkit_session_id) diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index ebdebaaa4..cdc52deef 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -303,22 +303,6 @@ def __init__( except Exception as e: logger.warning(f"[HybridBrowserToolkit] Failed to extract CDP port: {e}") - def __del__(self): - """Release CDP browser back to pool when toolkit is destroyed.""" - if self._cdp_port is not None and self._cdp_session_id is not None: - try: - # Import here to avoid circular dependency - from app.utils.agent import _cdp_pool_manager - _cdp_pool_manager.release_browser(self._cdp_port, self._cdp_session_id) - logger.info( - f"[HybridBrowserToolkit] Released CDP browser on port {self._cdp_port} " - f"for session {self._cdp_session_id}" - ) - except Exception as e: - logger.warning( - f"[HybridBrowserToolkit] Failed to release CDP browser on port {self._cdp_port}: {e}" - ) - async def _ensure_ws_wrapper(self): """Ensure WebSocket wrapper is initialized using connection pool.""" logger.debug(f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}") @@ -352,9 +336,19 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro # This allows multiple agents to use the same browser profile without conflicts logger.info(f"Cloning session {new_session_id} with shared user_data_dir: {self._user_data_dir}") + # Determine the CDP URL to use - preserve parent's CDP port if set + if self._cdp_port is not None: + # Use the same CDP port as parent to share the browser from pool + cdp_url = f"http://localhost:{self._cdp_port}" + logger.info(f"Cloning with parent's CDP port {self._cdp_port} for session {new_session_id}") + else: + # Fall back to default port + cdp_url = f"http://localhost:{env('browser_port', '9222')}" + logger.info(f"Cloning with default CDP port for session {new_session_id}") + # Use the same session_id to share the same browser instance # This ensures all clones use the same WebSocket connection and browser - return HybridBrowserToolkit( + cloned_toolkit = HybridBrowserToolkit( self.api_task_id, headless=self._headless, user_data_dir=self._user_data_dir, # Use the same user_data_dir @@ -374,11 +368,19 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro dom_content_loaded_timeout=self._dom_content_loaded_timeout, viewport_limit=self._viewport_limit, connect_over_cdp=self.config_loader.get_browser_config().connect_over_cdp, - cdp_url=f"http://localhost:{env('browser_port', '9222')}", + cdp_url=cdp_url, cdp_keep_current_page=self.config_loader.get_browser_config().cdp_keep_current_page, full_visual_mode=self._full_visual_mode, ) + # IMPORTANT: Clear the cloned toolkit's CDP tracking to prevent duplicate release + # Only the original toolkit that acquired the browser should release it + cloned_toolkit._cdp_port = None + cloned_toolkit._cdp_session_id = None + logger.debug(f"Cleared CDP tracking for cloned session {new_session_id} to prevent duplicate release") + + return cloned_toolkit + @classmethod def toolkit_name(cls) -> str: return "Browser Toolkit" @@ -397,8 +399,37 @@ async def close(self): await websocket_connection_pool.close_connection(session_id) logger.info(f"Released WebSocket connection for session {session_id}") + # Release CDP browser from pool + self._release_cdp_browser() + + def _release_cdp_browser(self): + """Release CDP browser back to pool.""" + if hasattr(self, '_cdp_port') and self._cdp_port is not None and \ + hasattr(self, '_cdp_session_id') and self._cdp_session_id is not None: + logger.info(f"[HybridBrowserToolkit] Explicitly releasing port {self._cdp_port} for session {self._cdp_session_id}") + try: + from app.utils.agent import _cdp_pool_manager + _cdp_pool_manager.release_browser(self._cdp_port, self._cdp_session_id) + logger.info( + f"[HybridBrowserToolkit] Released CDP browser on port {self._cdp_port} " + f"for session {self._cdp_session_id}" + ) + # Clear the tracking to prevent double release + self._cdp_port = None + self._cdp_session_id = None + except Exception as e: + logger.warning( + f"[HybridBrowserToolkit] Failed to release CDP browser: {e}" + ) + def __del__(self): """Cleanup when object is garbage collected.""" + logger.info(f"[HybridBrowserToolkit] __del__ called for api_task_id: {getattr(self, 'api_task_id', 'UNKNOWN')}") + + # Release CDP browser back to pool + self._release_cdp_browser() + + # Log cleanup if hasattr(self, "_ws_wrapper") and self._ws_wrapper: - session_id = self._ws_config.get("session_id", "default") + session_id = self._ws_config.get("session_id", "default") if hasattr(self, "_ws_config") else "unknown" logger.debug(f"HybridBrowserToolkit for session {session_id} is being garbage collected") diff --git a/electron/main/index.ts b/electron/main/index.ts index 6bfa5f9ad..e6a8cd122 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -306,7 +306,8 @@ function registerIpcHandlers() { // Get all browsers in the pool ipcMain.handle('get-cdp-browsers', () => { - log.info(`Getting CDP browser pool, count: ${cdp_browser_pool.length}`) + log.info(`[CDP POOL] Getting CDP browser pool, count: ${cdp_browser_pool.length}`) + log.info(`[CDP POOL] Pool content: ${JSON.stringify(cdp_browser_pool.map(b => ({id: b.id, port: b.port, name: b.name})))}`) return cdp_browser_pool }); @@ -418,38 +419,85 @@ function registerIpcHandlers() { try { const platform = process.platform; - let chromePath: string; - let chromeExecutable: string; + let chromeExecutable: string | null = null; + + // Use Playwright's Chromium + let playwrightCacheDir: string; - // Determine Chrome path based on platform if (platform === 'darwin') { - chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; - if (!existsSync(chromePath)) { + playwrightCacheDir = path.join(app.getPath('home'), 'Library/Caches/ms-playwright'); + } else if (platform === 'win32') { + playwrightCacheDir = path.join(app.getPath('home'), 'AppData/Local/ms-playwright'); + } else if (platform === 'linux') { + playwrightCacheDir = path.join(app.getPath('home'), '.cache/ms-playwright'); + } else { + return { + success: false, + error: `Unsupported platform: ${platform}`, + }; + } + + log.info(`Looking for Playwright Chromium in: ${playwrightCacheDir}`); + + // Find the latest chromium directory + try { + if (!existsSync(playwrightCacheDir)) { return { success: false, - error: 'Google Chrome not found at /Applications/Google Chrome.app', + error: 'Playwright Chromium not found. Please run: npx playwright install chromium', }; } - chromeExecutable = chromePath; - } else if (platform === 'win32') { - chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; - if (!existsSync(chromePath)) { - // Try alternative path - const altPath = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'; - if (existsSync(altPath)) { - chromePath = altPath; - } else { - return { - success: false, - error: 'Google Chrome not found', - }; - } + + const chromiumDirs = fs.readdirSync(playwrightCacheDir) + .filter(dir => dir.startsWith('chromium-')) + .sort() + .reverse(); + + if (chromiumDirs.length === 0) { + return { + success: false, + error: 'No Playwright Chromium installations found. Please run: npx playwright install chromium', + }; } - chromeExecutable = chromePath; - } else { + + const latestChromiumDir = chromiumDirs[0]; + log.info(`Found Playwright Chromium version: ${latestChromiumDir}`); + + // Build path to Chromium executable based on platform + if (platform === 'darwin') { + chromeExecutable = path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-mac/Chromium.app/Contents/MacOS/Chromium' + ); + } else if (platform === 'win32') { + chromeExecutable = path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-win/chrome.exe' + ); + } else if (platform === 'linux') { + chromeExecutable = path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-linux/chrome' + ); + } + + if (!chromeExecutable || !existsSync(chromeExecutable)) { + return { + success: false, + error: `Chromium executable not found at: ${chromeExecutable}`, + }; + } + + log.info(`Using Chromium at: ${chromeExecutable}`); + + } catch (error: any) { + log.error(`Error finding Playwright Chromium: ${error}`); return { success: false, - error: `Unsupported platform: ${platform}`, + error: `Failed to locate Playwright Chromium: ${error.message}`, }; } diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 4c0be5f6e..dcf541a63 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -383,6 +383,9 @@ 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'); + console.log('[FRONTEND CDP] Project:', project_id, 'Browser port:', browser_port, 'External CDP:', use_external_cdp); + console.log('[FRONTEND CDP] CDP Browsers count:', cdp_browsers?.length || 0); + console.log('[FRONTEND CDP] CDP Browsers details:', JSON.stringify(cdp_browsers)); // Lock the chatStore reference at the start of SSE session to prevent focus changes // during active message processing From fbddc9a97b7aba8075bd4446095d82bae2cc6edd Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Fri, 21 Nov 2025 20:48:51 +0800 Subject: [PATCH 05/40] update --- backend/app/service/chat_service.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 2c2b028e6..aaee0c832 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -464,11 +464,27 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): } yield sse_json("remove_task", returnData) elif item.action == Action.skip_task: + logger.info(f"Processing skip_task action for project {options.project_id}") if workforce is not None and item.project_id == options.project_id: if workforce._state.name == 'PAUSED': # Resume paused workforce to skip the task workforce.resume() workforce.skip_gracefully() + logger.info(f"Workforce skip requested for project {options.project_id}") + + # Important: When skip is requested, we need to properly clean up and exit + # the event loop to prevent duplicate processing when a new conversation starts + task_lock.status = Status.done + + # Stop workforce gracefully to ensure clean shutdown + if workforce is not None: + workforce.stop_gracefully() + logger.info(f"Workforce stopped gracefully after skip for project {options.project_id}") + workforce = None + + # Break the loop to terminate this SSE session + # A new session will be created when user sends next message + break elif item.action == Action.start: # Check conversation history length before starting task is_exceeded, total_length = check_conversation_history_length(task_lock) From 43fd014f5e3f26fac62930543bec24feefd28820 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 26 Nov 2025 17:31:57 +0800 Subject: [PATCH 06/40] update --- backend/app/utils/agent.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index fbc1be4ec..52c1283d4 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -861,8 +861,9 @@ def search_agent(options: Chat): "browser_switch_tab", "browser_enter", "browser_visit_page", - "browser_scroll", - "browser_get_som_screenshot", + "browser_get_page_snapshot" + # "browser_scroll", + # "browser_get_page_snapshot", ], ) @@ -985,7 +986,7 @@ def search_agent(options: Chat): interactive elements, not the full page text. To see more content on long pages, Navigate with `browser_click`, `browser_back`, and `browser_forward`. Manage multiple pages with `browser_switch_tab`. -- **Analysis**: Use `browser_get_som_screenshot` to understand the page +- **Analysis**: Use `browser_get_page_snapshot` to understand the page layout and identify interactive elements. Since this is a heavy operation, only use it when visual analysis is necessary. - **Interaction**: Use `browser_type` to fill out forms and From c958c21ded6f023c87531d126609a82f5346c815 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 26 Nov 2025 18:10:31 +0800 Subject: [PATCH 07/40] update --- electron/main/index.ts | 63 ++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index ebe94bc6b..430515864 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -476,28 +476,57 @@ function registerIpcHandlers() { }; } - const latestChromiumDir = chromiumDirs[0]; - log.info(`Found Playwright Chromium version: ${latestChromiumDir}`); + // Prioritize versions that have Chromium.app over Google Chrome for Testing + let selectedChromiumDir = chromiumDirs[0]; + if (platform === 'darwin') { + for (const dir of chromiumDirs) { + const chromiumAppPaths = [ + path.join(playwrightCacheDir, dir, 'chrome-mac-arm64', 'Chromium.app'), + path.join(playwrightCacheDir, dir, 'chrome-mac', 'Chromium.app'), + ]; + if (chromiumAppPaths.some(p => existsSync(p))) { + selectedChromiumDir = dir; + log.info(`Selected Chromium version with Chromium.app: ${dir}`); + break; + } + } + } + + const latestChromiumDir = selectedChromiumDir; + log.info(`Using Playwright Chromium version: ${latestChromiumDir}`); // Build path to Chromium executable based on platform if (platform === 'darwin') { - chromeExecutable = path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-mac/Chromium.app/Contents/MacOS/Chromium' - ); + // Try to find Chromium executable in both arm64 and regular directories + // Priority: Chromium.app (older versions) > Google Chrome for Testing (newer versions) + const possiblePaths = [ + // ARM64 paths + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-mac-arm64', 'Chromium.app/Contents/MacOS/Chromium'), + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-mac-arm64', 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'), + // Intel/Universal paths + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-mac', 'Chromium.app/Contents/MacOS/Chromium'), + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-mac', 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'), + ]; + + // Find the first path that exists + chromeExecutable = possiblePaths.find(p => existsSync(p)) || null; } else if (platform === 'win32') { - chromeExecutable = path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-win/chrome.exe' - ); + // Windows: Try to find chrome.exe in possible directories + const possiblePaths = [ + // 64-bit paths + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-win64', 'chrome.exe'), + // 32-bit or older versions + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-win', 'chrome.exe'), + ]; + + chromeExecutable = possiblePaths.find(p => existsSync(p)) || null; } else if (platform === 'linux') { - chromeExecutable = path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-linux/chrome' - ); + // Linux: Try to find chrome in possible directories + const possiblePaths = [ + path.join(playwrightCacheDir, latestChromiumDir, 'chrome-linux', 'chrome'), + ]; + + chromeExecutable = possiblePaths.find(p => existsSync(p)) || null; } if (!chromeExecutable || !existsSync(chromeExecutable)) { From 2d5cc5855b19952f2ae3ed15a90e225376c0cdc7 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 26 Nov 2025 18:29:52 +0800 Subject: [PATCH 08/40] update --- backend/app/service/chat_service.py | 3 +- .../utils/toolkit/hybrid_browser_toolkit.py | 33 ++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index e074f9daf..590157a25 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -1242,7 +1242,8 @@ async def construct_workforce(options: Chat) -> tuple[Workforce, ListenChatAgent workforce.add_single_agent_worker( "Search Agent: Can search the web, extract webpage content, " "simulate browser actions, and provide relevant information to " - "solve the given task.", + "solve the given task. " + "NOTE: Opening the browser will automatically open the Salesforce interface.", searcher, ) workforce.add_single_agent_worker( diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index cdc52deef..ed4010315 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -387,20 +387,23 @@ def toolkit_name(cls) -> str: async def close(self): """Close the browser toolkit and release WebSocket connection.""" - try: - # Close browser if needed - if self._ws_wrapper: - await super().browser_close() - except Exception as e: - logger.error(f"Error closing browser: {e}") + logger.info(f"[HybridBrowserToolkit] close() called - browser will remain open for reuse") - # Release connection from pool - session_id = self._ws_config.get("session_id", "default") - await websocket_connection_pool.close_connection(session_id) - logger.info(f"Released WebSocket connection for session {session_id}") + # DISABLED: Do not close browser - keep it open for reuse across tasks + # try: + # # Close browser if needed + # if self._ws_wrapper: + # await super().browser_close() + # except Exception as e: + # logger.error(f"Error closing browser: {e}") + + # DISABLED: Do not release WebSocket connection - keep it in pool for reuse + # session_id = self._ws_config.get("session_id", "default") + # await websocket_connection_pool.close_connection(session_id) + # logger.info(f"Released WebSocket connection for session {session_id}") - # Release CDP browser from pool - self._release_cdp_browser() + # DISABLED: Do not release CDP browser - keep it in pool for reuse + # self._release_cdp_browser() def _release_cdp_browser(self): """Release CDP browser back to pool.""" @@ -424,10 +427,10 @@ def _release_cdp_browser(self): def __del__(self): """Cleanup when object is garbage collected.""" - logger.info(f"[HybridBrowserToolkit] __del__ called for api_task_id: {getattr(self, 'api_task_id', 'UNKNOWN')}") + logger.info(f"[HybridBrowserToolkit] __del__ called for api_task_id: {getattr(self, 'api_task_id', 'UNKNOWN')} - browser will remain open") - # Release CDP browser back to pool - self._release_cdp_browser() + # DISABLED: Do not release CDP browser on garbage collection - keep it in pool for reuse + # self._release_cdp_browser() # Log cleanup if hasattr(self, "_ws_wrapper") and self._ws_wrapper: From b1bf27d37f991de990940d679e2c4b91b61b9a46 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 26 Nov 2025 18:37:57 +0800 Subject: [PATCH 09/40] update --- backend/app/utils/agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 52c1283d4..e9254317e 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -848,10 +848,10 @@ def search_agent(options: Chat): browser_log_to_file=True, stealth=True, session_id=toolkit_session_id, # Use the session ID for pool management - default_start_url=default_url, + default_start_url=None, connect_over_cdp=True, cdp_url=f"http://localhost:{selected_port}", - cdp_keep_current_page=use_keep_current_page, + cdp_keep_current_page=True, enabled_tools=[ "browser_open", "browser_click", From 3ed8049f1cadea8411dfa450565b7edccfcec35d Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 26 Nov 2025 23:18:02 +0800 Subject: [PATCH 10/40] update --- backend/app/utils/agent.py | 79 ++++++------------- .../utils/toolkit/hybrid_browser_toolkit.py | 79 ++----------------- 2 files changed, 29 insertions(+), 129 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index e9254317e..ec2e5be99 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -801,46 +801,30 @@ def search_agent(options: Chat): # Generate unique session ID for this toolkit instance toolkit_session_id = str(uuid.uuid4())[:8] - # Browser selection logic from CDP browser pool using occupation manager - 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, acquire an available browser - traceroot_logger.info(f"Checking CDP browser pool: hasattr={hasattr(options, 'cdp_browsers')}, " - f"browsers={getattr(options, 'cdp_browsers', None)}") - - if hasattr(options, 'cdp_browsers') and options.cdp_browsers: - global _cdp_pool_manager - traceroot_logger.info(f"CDP browser pool available with {len(options.cdp_browsers)} browsers") - # Acquire an available (unoccupied) browser from the pool - selected_browser = _cdp_pool_manager.acquire_browser(options.cdp_browsers, toolkit_session_id) - - if selected_browser: - selected_port = selected_browser.get('port', selected_port) - selected_is_external = selected_browser.get('isExternal', False) - - traceroot_logger.info( - f"Acquired browser from pool: port={selected_port}, " - f"isExternal={selected_is_external}, " - f"name={selected_browser.get('name', 'Unnamed')}, " - f"session={toolkit_session_id}" - ) - else: - # No available browsers - fall back to default or log warning - traceroot_logger.warning( - f"No available browsers in pool for session {toolkit_session_id}, " - f"using default port {selected_port}" - ) + # SIMPLIFIED: Use fixed CDP port from environment or first browser in pool + # No occupation management - all agents share the same browser connection + if hasattr(options, 'cdp_browsers') and options.cdp_browsers and len(options.cdp_browsers) > 0: + # Use first browser from pool + selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) + selected_is_external = options.cdp_browsers[0].get('isExternal', False) + traceroot_logger.info( + f"Using CDP browser: port={selected_port}, " + f"isExternal={selected_is_external}, " + f"name={options.cdp_browsers[0].get('name', 'Unnamed')}" + ) else: - traceroot_logger.info(f"No CDP browser pool available, using default port: {selected_port}") + # Use default port from environment + selected_port = env('browser_port', '9222') + selected_is_external = False + traceroot_logger.info(f"Using default CDP 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 = selected_is_external + # IMPORTANT: Always use cdp_keep_current_page=True to preserve browser state + # across tasks (both internal and external browsers) + use_keep_current_page = True - # 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 - default_url = None if use_keep_current_page else "about:blank" + # When cdp_keep_current_page=True, don't set default_start_url to avoid + # opening a new page and conflicting with keeping the current page + default_url = None web_toolkit_custom = HybridBrowserToolkit( options.project_id, @@ -848,10 +832,10 @@ def search_agent(options: Chat): browser_log_to_file=True, stealth=True, session_id=toolkit_session_id, # Use the session ID for pool management - default_start_url=None, + default_start_url=default_url, connect_over_cdp=True, cdp_url=f"http://localhost:{selected_port}", - cdp_keep_current_page=True, + cdp_keep_current_page=use_keep_current_page, enabled_tools=[ "browser_open", "browser_click", @@ -963,23 +947,6 @@ def search_agent(options: Chat): Your approach depends on available search tools: -**If Google Search is Available:** -- Initial Search: Start with `search_google` to get a list of relevant URLs -- Browser-Based Exploration: Use the browser tools to investigate the URLs - -**If Google Search is NOT Available:** -- **MUST start with direct website search**: Use `browser_visit_page` to go - directly to popular search engines and informational websites such as: - * General search: google.com, bing.com, duckduckgo.com - * Academic: scholar.google.com, pubmed.ncbi.nlm.nih.gov - * News: news.google.com, bbc.com/news, reuters.com - * Technical: stackoverflow.com, github.com - * Reference: wikipedia.org, britannica.com -- **Manual search process**: Type your query into the search boxes on these - sites using `browser_type` and submit with `browser_enter` -- **Extract URLs from results**: Only use URLs that appear in the search - results on these websites - **Common Browser Operations (both scenarios):** - **Navigation and Exploration**: Use `browser_visit_page` to open URLs. `browser_visit_page` provides a snapshot of currently visible diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index ed4010315..8f64c1d3c 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -217,10 +217,6 @@ async def close_all(self): class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): agent_name: str = Agents.search_agent - # CDP browser pool tracking - _cdp_port: int | None = None - _cdp_session_id: str | None = None - def __init__( self, api_task_id: str, @@ -287,22 +283,6 @@ def __init__( ) logger.info(f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}") - # Extract CDP port for pool management (if using CDP) - if connect_over_cdp and cdp_url: - try: - # Parse port from cdp_url (e.g., "http://localhost:9222") - import re - port_match = re.search(r':(\d+)', cdp_url) - if port_match: - self._cdp_port = int(port_match.group(1)) - self._cdp_session_id = session_id - logger.info( - f"[HybridBrowserToolkit] Registered CDP browser on port {self._cdp_port} " - f"for session {self._cdp_session_id}" - ) - except Exception as e: - logger.warning(f"[HybridBrowserToolkit] Failed to extract CDP port: {e}") - async def _ensure_ws_wrapper(self): """Ensure WebSocket wrapper is initialized using connection pool.""" logger.debug(f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}") @@ -336,15 +316,9 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro # This allows multiple agents to use the same browser profile without conflicts logger.info(f"Cloning session {new_session_id} with shared user_data_dir: {self._user_data_dir}") - # Determine the CDP URL to use - preserve parent's CDP port if set - if self._cdp_port is not None: - # Use the same CDP port as parent to share the browser from pool - cdp_url = f"http://localhost:{self._cdp_port}" - logger.info(f"Cloning with parent's CDP port {self._cdp_port} for session {new_session_id}") - else: - # Fall back to default port - cdp_url = f"http://localhost:{env('browser_port', '9222')}" - logger.info(f"Cloning with default CDP port for session {new_session_id}") + # Use the same CDP URL as parent + cdp_url = self.config_loader.get_browser_config().cdp_url + logger.info(f"Cloning with CDP URL: {cdp_url} for session {new_session_id}") # Use the same session_id to share the same browser instance # This ensures all clones use the same WebSocket connection and browser @@ -373,12 +347,6 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro full_visual_mode=self._full_visual_mode, ) - # IMPORTANT: Clear the cloned toolkit's CDP tracking to prevent duplicate release - # Only the original toolkit that acquired the browser should release it - cloned_toolkit._cdp_port = None - cloned_toolkit._cdp_session_id = None - logger.debug(f"Cleared CDP tracking for cloned session {new_session_id} to prevent duplicate release") - return cloned_toolkit @classmethod @@ -386,52 +354,17 @@ def toolkit_name(cls) -> str: return "Browser Toolkit" async def close(self): - """Close the browser toolkit and release WebSocket connection.""" - logger.info(f"[HybridBrowserToolkit] close() called - browser will remain open for reuse") + """Close the browser toolkit - but keep browser and connections open for reuse.""" + logger.info(f"[HybridBrowserToolkit] close() called - browser and connections will remain open for reuse") # DISABLED: Do not close browser - keep it open for reuse across tasks - # try: - # # Close browser if needed - # if self._ws_wrapper: - # await super().browser_close() - # except Exception as e: - # logger.error(f"Error closing browser: {e}") - # DISABLED: Do not release WebSocket connection - keep it in pool for reuse - # session_id = self._ws_config.get("session_id", "default") - # await websocket_connection_pool.close_connection(session_id) - # logger.info(f"Released WebSocket connection for session {session_id}") - - # DISABLED: Do not release CDP browser - keep it in pool for reuse - # self._release_cdp_browser() - - def _release_cdp_browser(self): - """Release CDP browser back to pool.""" - if hasattr(self, '_cdp_port') and self._cdp_port is not None and \ - hasattr(self, '_cdp_session_id') and self._cdp_session_id is not None: - logger.info(f"[HybridBrowserToolkit] Explicitly releasing port {self._cdp_port} for session {self._cdp_session_id}") - try: - from app.utils.agent import _cdp_pool_manager - _cdp_pool_manager.release_browser(self._cdp_port, self._cdp_session_id) - logger.info( - f"[HybridBrowserToolkit] Released CDP browser on port {self._cdp_port} " - f"for session {self._cdp_session_id}" - ) - # Clear the tracking to prevent double release - self._cdp_port = None - self._cdp_session_id = None - except Exception as e: - logger.warning( - f"[HybridBrowserToolkit] Failed to release CDP browser: {e}" - ) + # DISABLED: Do not release CDP browser port - use fixed port, no occupation management def __del__(self): """Cleanup when object is garbage collected.""" logger.info(f"[HybridBrowserToolkit] __del__ called for api_task_id: {getattr(self, 'api_task_id', 'UNKNOWN')} - browser will remain open") - # DISABLED: Do not release CDP browser on garbage collection - keep it in pool for reuse - # self._release_cdp_browser() - # Log cleanup if hasattr(self, "_ws_wrapper") and self._ws_wrapper: session_id = self._ws_config.get("session_id", "default") if hasattr(self, "_ws_config") else "unknown" From 66f105370579cd111616c531f9d1f255d9cd3bbb Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Sun, 7 Dec 2025 19:31:54 +0100 Subject: [PATCH 11/40] update browser cdp pool --- backend/app/utils/agent.py | 60 +++++++++++++++++++++++++++------- backend/app/utils/workforce.py | 34 +++++++++++++++++++ 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index ec2e5be99..78fb82690 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -562,6 +562,7 @@ def agent_model( tool_names: list[str] | None = None, toolkits_to_register_agent: list[RegisteredAgentToolkit] | None = None, enable_snapshot_clean: bool = False, + cleanup_callback: Callable[[], None] | None = None, ): task_lock = get_task_lock(options.project_id) agent_id = str(uuid.uuid4()) @@ -572,7 +573,7 @@ def agent_model( ) ) - return ListenChatAgent( + agent = ListenChatAgent( options.project_id, agent_name, system_message, @@ -600,6 +601,12 @@ def agent_model( enable_snapshot_clean=enable_snapshot_clean, ) + # Attach cleanup callback if provided + if cleanup_callback: + agent._cleanup_callback = cleanup_callback + + return agent + @traceroot.trace() def question_confirm_agent(options: Chat): @@ -801,22 +808,36 @@ def search_agent(options: Chat): # Generate unique session ID for this toolkit instance toolkit_session_id = str(uuid.uuid4())[:8] - # SIMPLIFIED: Use fixed CDP port from environment or first browser in pool - # No occupation management - all agents share the same browser connection + # CDP POOL MANAGEMENT: Acquire browser from pool for parallel task execution + selected_port = None + selected_is_external = False + if hasattr(options, 'cdp_browsers') and options.cdp_browsers and len(options.cdp_browsers) > 0: - # Use first browser from pool - selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) - selected_is_external = options.cdp_browsers[0].get('isExternal', False) - traceroot_logger.info( - f"Using CDP browser: port={selected_port}, " - f"isExternal={selected_is_external}, " - f"name={options.cdp_browsers[0].get('name', 'Unnamed')}" - ) + # Try to acquire an available browser from the pool + selected_browser = _cdp_pool_manager.acquire_browser(options.cdp_browsers, toolkit_session_id) + + if selected_browser: + selected_port = selected_browser.get('port', env('browser_port', '9222')) + selected_is_external = selected_browser.get('isExternal', False) + traceroot_logger.info( + f"Acquired CDP browser from pool: port={selected_port}, " + f"isExternal={selected_is_external}, " + f"name={selected_browser.get('name', 'Unnamed')}, " + f"session_id={toolkit_session_id}" + ) + else: + # No available browsers in pool, fall back to first browser + selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) + selected_is_external = options.cdp_browsers[0].get('isExternal', False) + traceroot_logger.warning( + f"No available browsers in pool, using first browser: port={selected_port}, " + f"session_id={toolkit_session_id}" + ) else: # Use default port from environment selected_port = env('browser_port', '9222') selected_is_external = False - traceroot_logger.info(f"Using default CDP port: {selected_port}") + traceroot_logger.info(f"Using default CDP port: {selected_port}, session_id={toolkit_session_id}") # IMPORTANT: Always use cdp_keep_current_page=True to preserve browser state # across tasks (both internal and external browsers) @@ -851,6 +872,10 @@ def search_agent(options: Chat): ], ) + # Store CDP port and session ID on the toolkit for cleanup + web_toolkit_custom._cdp_port = selected_port + web_toolkit_custom._cdp_session_id = toolkit_session_id + # Save reference before registering for toolkits_to_register_agent web_toolkit_for_agent_registration = web_toolkit_custom web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) @@ -966,6 +991,16 @@ def search_agent(options: Chat): """ + # Define cleanup callback to release CDP browser back to pool + def cleanup_cdp_browser(): + if hasattr(web_toolkit_custom, '_cdp_port') and hasattr(web_toolkit_custom, '_cdp_session_id'): + port = web_toolkit_custom._cdp_port + session_id = web_toolkit_custom._cdp_session_id + _cdp_pool_manager.release_browser(port, session_id) + traceroot_logger.info( + f"Cleanup: Released CDP browser on port {port} for session {session_id}" + ) + return agent_model( Agents.search_agent, BaseMessage.make_assistant_message( @@ -984,6 +1019,7 @@ def search_agent(options: Chat): ], toolkits_to_register_agent=[web_toolkit_for_agent_registration], enable_snapshot_clean=True, + cleanup_callback=cleanup_cdp_browser, ) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index c9793ec10..1ecfccf52 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -398,9 +398,43 @@ def stop_gracefully(self) -> None: logger.info(f"🛑 [WF-LIFECYCLE] stop_gracefully() CALLED", extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}) logger.info(f"[WF-LIFECYCLE] Current state before stop_gracefully: {self._state.name}, _running: {self._running}") logger.info("=" * 80) + + # Cleanup all agents before stopping + self._cleanup_all_agents() + super().stop_gracefully() logger.info(f"[WF-LIFECYCLE] ✅ super().stop_gracefully() completed, new state: {self._state.name}, _running: {self._running}") + def _cleanup_all_agents(self) -> None: + """Call cleanup callbacks for all agents to release resources (e.g., CDP browsers).""" + logger.info(f"[WF-CLEANUP] Starting cleanup for all agents in workforce {id(self)}") + cleanup_count = 0 + + # Cleanup all child workers + if hasattr(self, 'children') and self.children: + for child in self.children: + if hasattr(child, 'worker_agent'): + agent = child.worker_agent + if hasattr(agent, '_cleanup_callback') and callable(agent._cleanup_callback): + try: + agent._cleanup_callback() + cleanup_count += 1 + logger.info(f"[WF-CLEANUP] Called cleanup for agent: {getattr(agent, 'agent_name', 'unknown')}") + except Exception as e: + logger.error(f"[WF-CLEANUP] Error in cleanup callback for agent: {e}", exc_info=True) + + # Cleanup coordinator agent + if hasattr(self, 'coordinator_agent') and self.coordinator_agent: + if hasattr(self.coordinator_agent, '_cleanup_callback') and callable(self.coordinator_agent._cleanup_callback): + try: + self.coordinator_agent._cleanup_callback() + cleanup_count += 1 + logger.info(f"[WF-CLEANUP] Called cleanup for coordinator agent") + except Exception as e: + logger.error(f"[WF-CLEANUP] Error in cleanup callback for coordinator: {e}", exc_info=True) + + logger.info(f"[WF-CLEANUP] ✅ Cleanup completed, {cleanup_count} agent(s) cleaned up") + def skip_gracefully(self) -> None: logger.info("=" * 80) logger.info(f"⏭️ [WF-LIFECYCLE] skip_gracefully() CALLED", extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}) From 504f4d14a8967d58778ceec35da8060d78f8edde Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 10 Dec 2025 16:13:53 +0100 Subject: [PATCH 12/40] inital update --- backend/app/utils/agent.py | 285 ++++++++++++++++-- backend/app/utils/single_agent_worker.py | 9 +- .../utils/toolkit/hybrid_browser_toolkit.py | 40 ++- 3 files changed, 300 insertions(+), 34 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 78fb82690..37817b1ca 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -201,6 +201,10 @@ def __init__( self.api_task_id = api_task_id self.agent_name = agent_name + # CDP management callbacks (set by search_agent) + self._cdp_acquire_callback = None # Called when cloning to acquire new CDP browser + self._cdp_release_callback = None # Called when agent is destroyed to release CDP browser + process_task_id: str = "" @traceroot.trace() @@ -512,11 +516,97 @@ async def _aexecute_tool(self, tool_call_request: ToolCallRequest) -> ToolCallin @traceroot.trace() def clone(self, with_memory: bool = False) -> ChatAgent: """Please see super.clone()""" + import uuid + + # Generate unique clone ID for tracking + clone_id = str(uuid.uuid4())[:8] + + # Get clone context (task info if available) + task_context = "UNKNOWN" + if hasattr(self, 'process_task_id') and self.process_task_id: + task_context = f"task_id={self.process_task_id}" + + traceroot_logger.info( + f"[CLONE START] Clone ID: {clone_id}, " + f"Parent Agent: {self.agent_id}, " + f"Agent Name: {self.agent_name}, " + f"Context: {task_context}" + ) + system_message = None if with_memory else self._original_system_message + # If this agent has CDP acquire callback, acquire CDP BEFORE cloning tools + # This ensures HybridBrowserToolkit clones with the correct CDP port + new_cdp_port = None + new_cdp_session = None + + if hasattr(self, '_cdp_acquire_callback') and callable(self._cdp_acquire_callback): + # Temporarily store this for use during toolkit cloning + import uuid + new_cdp_session = str(uuid.uuid4())[:8] + + # Get the options from the parent agent (stored during agent creation) + if hasattr(self, '_cdp_options'): + options = self._cdp_options + cdp_browsers = options.cdp_browsers if hasattr(options, 'cdp_browsers') else [] + + if cdp_browsers: + from app.component.environment import env + selected_browser = _cdp_pool_manager.acquire_browser(cdp_browsers, new_cdp_session) + + if selected_browser: + new_cdp_port = selected_browser.get('port', env('browser_port', '9222')) + traceroot_logger.info( + f"[CLONE {clone_id}] Pre-acquired CDP browser port={new_cdp_port} for session={new_cdp_session}" + ) + else: + new_cdp_port = cdp_browsers[0].get('port', env('browser_port', '9222')) + traceroot_logger.warning( + f"[CLONE {clone_id}] No available browsers, using first: port={new_cdp_port}" + ) + + # Temporarily modify HybridBrowserToolkit's CDP config for cloning + if hasattr(self, '_browser_toolkit'): + toolkit = self._browser_toolkit + # Temporarily override the CDP URL for cloning + original_cdp_url = toolkit.config_loader.get_browser_config().cdp_url + original_ws_config_cdp = toolkit._ws_config.get('cdpUrl') if hasattr(toolkit, '_ws_config') else None + + # Update both config_loader and _ws_config + toolkit.config_loader.get_browser_config().cdp_url = f"http://localhost:{new_cdp_port}" + if hasattr(toolkit, '_ws_config') and toolkit._ws_config: + toolkit._ws_config['cdpUrl'] = f"http://localhost:{new_cdp_port}" + + # Store originals for restoration + toolkit._temp_original_cdp_url = original_cdp_url + toolkit._temp_original_ws_config_cdp = original_ws_config_cdp + + traceroot_logger.info( + f"[CLONE {clone_id}] Temporarily set CDP URL to http://localhost:{new_cdp_port} for cloning " + f"(parent config was {original_cdp_url}, parent ws_config was {original_ws_config_cdp})" + ) + else: + traceroot_logger.warning(f"[CLONE {clone_id}] No _browser_toolkit found on agent, CDP URL not modified") + # Clone tools and collect toolkits that need registration + traceroot_logger.info(f"[CLONE {clone_id}] Calling _clone_tools()...") cloned_tools, toolkits_to_register = self._clone_tools() - + traceroot_logger.info(f"[CLONE {clone_id}] _clone_tools returned {len(cloned_tools)} tools, {len(toolkits_to_register)} toolkits to register") + for idx, tk in enumerate(toolkits_to_register): + traceroot_logger.info(f"[CLONE {clone_id}] Toolkit {idx}: {tk.__class__.__name__}, session={getattr(tk, '_session_id', 'N/A')}") + + # Restore original CDP URL in parent toolkit + if new_cdp_port is not None and hasattr(self, '_browser_toolkit'): + toolkit = self._browser_toolkit + if hasattr(toolkit, '_temp_original_cdp_url'): + toolkit.config_loader.get_browser_config().cdp_url = toolkit._temp_original_cdp_url + delattr(toolkit, '_temp_original_cdp_url') + if hasattr(toolkit, '_temp_original_ws_config_cdp'): + if toolkit._temp_original_ws_config_cdp and hasattr(toolkit, '_ws_config') and toolkit._ws_config: + toolkit._ws_config['cdpUrl'] = toolkit._temp_original_ws_config_cdp + delattr(toolkit, '_temp_original_ws_config_cdp') + traceroot_logger.info(f"[CLONE {clone_id}] Restored original CDP URL in parent toolkit") + new_agent = ListenChatAgent( api_task_id=self.api_task_id, agent_name=self.agent_name, @@ -541,6 +631,44 @@ def clone(self, with_memory: bool = False) -> ChatAgent: new_agent.process_task_id = self.process_task_id + # Copy CDP management data to cloned agent + new_agent._cdp_acquire_callback = self._cdp_acquire_callback + new_agent._cdp_release_callback = self._cdp_release_callback + if hasattr(self, '_cdp_options'): + new_agent._cdp_options = self._cdp_options + + # Find and store the cloned browser toolkit on the new agent + if toolkits_to_register: + traceroot_logger.info(f"[CLONE {clone_id}] toolkits_to_register has {len(toolkits_to_register)} items") + for toolkit in toolkits_to_register: + toolkit_class_name = toolkit.__class__.__name__ if hasattr(toolkit, '__class__') else 'UNKNOWN' + traceroot_logger.info(f"[CLONE {clone_id}] Checking toolkit: {toolkit_class_name}") + if hasattr(toolkit, '__class__') and toolkit.__class__.__name__ == 'HybridBrowserToolkit': + new_agent._browser_toolkit = toolkit + traceroot_logger.info(f"[CLONE {clone_id}] Set _browser_toolkit to cloned HybridBrowserToolkit") + break + else: + traceroot_logger.warning(f"[CLONE {clone_id}] toolkits_to_register is empty!") + + # Set CDP info on cloned agent if we pre-acquired it + if new_cdp_port is not None and new_cdp_session is not None: + new_agent._cdp_port = new_cdp_port + new_agent._cdp_session_id = new_cdp_session + traceroot_logger.info( + f"[CLONE {clone_id}] Set CDP info on new agent {new_agent.agent_id}: " + f"port={new_cdp_port}, session={new_cdp_session}" + ) + + # Attach cleanup callback + if hasattr(new_agent, '_cdp_release_callback') and callable(new_agent._cdp_release_callback): + new_agent._cleanup_callback = lambda: new_agent._cdp_release_callback(new_agent) + else: + # If no CDP pre-acquisition, copy from parent + if hasattr(self, '_cdp_port'): + new_agent._cdp_port = self._cdp_port + if hasattr(self, '_cdp_session_id'): + new_agent._cdp_session_id = self._cdp_session_id + # Copy memory if requested if with_memory: # Get all records from the current memory @@ -549,6 +677,12 @@ def clone(self, with_memory: bool = False) -> ChatAgent: for context_record in context_records: new_agent.memory.write_record(context_record.memory_record) + traceroot_logger.info( + f"[CLONE COMPLETE] Clone ID: {clone_id}, " + f"New Agent ID: {new_agent.agent_id}, " + f"CDP Port: {new_cdp_port if new_cdp_port else 'N/A'}" + ) + return new_agent @@ -805,39 +939,143 @@ def search_agent(options: Chat): message_handler=HumanToolkit(options.project_id, Agents.search_agent).send_message_to_user ) - # Generate unique session ID for this toolkit instance - toolkit_session_id = str(uuid.uuid4())[:8] + # Define CDP acquire callback for cloning + def acquire_cdp_for_agent(agent): + """Acquire a CDP browser from pool and create new toolkit for the agent.""" + # Generate unique session ID for this agent clone + session_id = str(uuid.uuid4())[:8] + + selected_port = None + selected_is_external = False + + if hasattr(options, 'cdp_browsers') and options.cdp_browsers and len(options.cdp_browsers) > 0: + # Try to acquire an available browser from the pool + selected_browser = _cdp_pool_manager.acquire_browser(options.cdp_browsers, session_id) - # CDP POOL MANAGEMENT: Acquire browser from pool for parallel task execution + if selected_browser: + selected_port = selected_browser.get('port', env('browser_port', '9222')) + selected_is_external = selected_browser.get('isExternal', False) + traceroot_logger.info( + f"Acquired CDP browser from pool for agent {agent.agent_id}: " + f"port={selected_port}, isExternal={selected_is_external}, " + f"name={selected_browser.get('name', 'Unnamed')}, session_id={session_id}" + ) + else: + # No available browsers in pool, fall back to first browser + selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) + selected_is_external = options.cdp_browsers[0].get('isExternal', False) + traceroot_logger.warning( + f"No available browsers in pool for agent {agent.agent_id}, " + f"using first browser: port={selected_port}, session_id={session_id}" + ) + else: + # Use default port from environment + selected_port = env('browser_port', '9222') + selected_is_external = False + traceroot_logger.info( + f"Using default CDP port for agent {agent.agent_id}: " + f"{selected_port}, session_id={session_id}" + ) + + # Create new HybridBrowserToolkit with the acquired CDP port + use_keep_current_page = True + default_url = None + + new_toolkit = HybridBrowserToolkit( + options.project_id, + headless=False, + browser_log_to_file=True, + stealth=True, + session_id=session_id, + default_start_url=default_url, + connect_over_cdp=True, + cdp_url=f"http://localhost:{selected_port}", + cdp_keep_current_page=use_keep_current_page, + enabled_tools=[ + "browser_open", + "browser_click", + "browser_type", + "browser_back", + "browser_forward", + "browser_switch_tab", + "browser_enter", + "browser_visit_page", + "browser_get_page_snapshot" + ], + ) + + # Store CDP info on toolkit + new_toolkit._cdp_port = selected_port + new_toolkit._cdp_session_id = session_id + + # Replace the old toolkit in agent's registered toolkits + if hasattr(agent, '_toolkits_to_register_agent') and agent._toolkits_to_register_agent: + for i, toolkit in enumerate(agent._toolkits_to_register_agent): + if hasattr(toolkit, '__class__') and toolkit.__class__.__name__ == 'HybridBrowserToolkit': + agent._toolkits_to_register_agent[i] = new_toolkit + traceroot_logger.info( + f"Replaced HybridBrowserToolkit for agent {agent.agent_id}: " + f"new port={selected_port}, session_id={session_id}" + ) + break + + # Update agent's tools to use the new toolkit + new_tools = new_toolkit.get_tools() + if hasattr(agent, '_tools') and agent._tools: + # Replace browser tools in agent's tool list + agent._tools = [ + tool for tool in agent._tools + if not any(name in tool.get_function_name() for name in [ + 'browser_open', 'browser_click', 'browser_type', 'browser_back', + 'browser_forward', 'browser_switch_tab', 'browser_enter', + 'browser_visit_page', 'browser_get_page_snapshot' + ]) + ] + agent._tools.extend(new_tools) + traceroot_logger.info(f"Updated agent {agent.agent_id} tools with new browser toolkit") + + # Store CDP info on agent for cleanup + agent._cdp_port = selected_port + agent._cdp_session_id = session_id + + # Define CDP release callback + def release_cdp_from_agent(agent): + """Release CDP browser back to pool.""" + if hasattr(agent, '_cdp_port') and hasattr(agent, '_cdp_session_id'): + port = agent._cdp_port + session_id = agent._cdp_session_id + _cdp_pool_manager.release_browser(port, session_id) + traceroot_logger.info( + f"Released CDP browser for agent {agent.agent_id}: " + f"port={port}, session_id={session_id}" + ) + + # Acquire CDP for initial agent + toolkit_session_id = str(uuid.uuid4())[:8] selected_port = None selected_is_external = False if hasattr(options, 'cdp_browsers') and options.cdp_browsers and len(options.cdp_browsers) > 0: - # Try to acquire an available browser from the pool selected_browser = _cdp_pool_manager.acquire_browser(options.cdp_browsers, toolkit_session_id) - if selected_browser: selected_port = selected_browser.get('port', env('browser_port', '9222')) selected_is_external = selected_browser.get('isExternal', False) traceroot_logger.info( - f"Acquired CDP browser from pool: port={selected_port}, " + f"Acquired CDP browser from pool (initial): port={selected_port}, " f"isExternal={selected_is_external}, " - f"name={selected_browser.get('name', 'Unnamed')}, " - f"session_id={toolkit_session_id}" + f"name={selected_browser.get('name', 'Unnamed')}, session_id={toolkit_session_id}" ) else: - # No available browsers in pool, fall back to first browser selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) selected_is_external = options.cdp_browsers[0].get('isExternal', False) traceroot_logger.warning( - f"No available browsers in pool, using first browser: port={selected_port}, " - f"session_id={toolkit_session_id}" + f"No available browsers in pool (initial), using first browser: " + f"port={selected_port}, session_id={toolkit_session_id}" ) else: - # Use default port from environment selected_port = env('browser_port', '9222') selected_is_external = False - traceroot_logger.info(f"Using default CDP port: {selected_port}, session_id={toolkit_session_id}") + traceroot_logger.info(f"Using default CDP port (initial): {selected_port}, session_id={toolkit_session_id}") # IMPORTANT: Always use cdp_keep_current_page=True to preserve browser state # across tasks (both internal and external browsers) @@ -876,9 +1114,10 @@ def search_agent(options: Chat): web_toolkit_custom._cdp_port = selected_port web_toolkit_custom._cdp_session_id = toolkit_session_id - # Save reference before registering for toolkits_to_register_agent - web_toolkit_for_agent_registration = web_toolkit_custom + # Register toolkit with message_integration web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) + # Use the registered (wrapped) toolkit for both tools and agent registration + web_toolkit_for_agent_registration = web_toolkit_custom terminal_toolkit = TerminalToolkit(options.project_id, Agents.search_agent, safe_mode=True, clone_current_env=False) terminal_toolkit = message_integration.register_functions([terminal_toolkit.shell_exec]) note_toolkit = NoteTakingToolkit(options.project_id, Agents.search_agent, working_directory=working_directory) @@ -1001,7 +1240,7 @@ def cleanup_cdp_browser(): f"Cleanup: Released CDP browser on port {port} for session {session_id}" ) - return agent_model( + agent = agent_model( Agents.search_agent, BaseMessage.make_assistant_message( role_name="Search Agent", @@ -1022,6 +1261,18 @@ def cleanup_cdp_browser(): cleanup_callback=cleanup_cdp_browser, ) + # Attach CDP management callbacks to the agent for clone support + agent._cdp_acquire_callback = acquire_cdp_for_agent + agent._cdp_release_callback = release_cdp_from_agent + agent._cdp_port = selected_port + agent._cdp_session_id = toolkit_session_id + # Store options for use during cloning + agent._cdp_options = options + # Store browser toolkit reference for CDP URL modification during cloning + agent._browser_toolkit = web_toolkit_for_agent_registration + + return agent + @traceroot.trace() async def document_agent(options: Chat): diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index a6e82871b..197bbbfa1 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -19,7 +19,7 @@ def __init__( description: str, worker: ListenChatAgent, use_agent_pool: bool = True, - pool_initial_size: int = 1, + pool_initial_size: int = 0, # Changed from 1 to 0 to avoid pre-creating clones that waste CDP resources pool_max_size: int = 10, auto_scale_pool: bool = True, use_structured_output_handler: bool = True, @@ -64,6 +64,10 @@ async def _process_task(self, task: Task, dependencies: list[Task]) -> TaskState TaskState: `TaskState.DONE` if processed successfully, otherwise `TaskState.FAILED`. """ + # Log task details before getting agent (for clone tracking) + task_content_preview = task.content[:100] + "..." if len(task.content) > 100 else task.content + logger.info(f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'") + # Get agent efficiently (from pool or by cloning) worker_agent = await self._get_worker_agent() worker_agent.process_task_id = task.id # type: ignore rewrite line @@ -71,7 +75,8 @@ async def _process_task(self, task: Task, dependencies: list[Task]) -> TaskState logger.info("Starting task processing", extra={ "task_id": task.id, "worker_agent_id": worker_agent.agent_id, - "dependencies_count": len(dependencies) + "dependencies_count": len(dependencies), + "task_content_preview": task_content_preview }) response_content = "" diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index 8f64c1d3c..2fc265395 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -160,7 +160,7 @@ async def get_connection(self, session_id: str, config: Dict[str, Any]) -> WebSo is_healthy = False if is_healthy: - logger.debug(f"Reusing healthy WebSocket connection for session {session_id}") + logger.info(f"[CONNECTION POOL] Reusing healthy WebSocket connection for session {session_id}") return wrapper else: # Connection is unhealthy, clean it up @@ -172,11 +172,12 @@ async def get_connection(self, session_id: str, config: Dict[str, Any]) -> WebSo del self._connections[session_id] # Create a new connection - logger.info(f"Creating new WebSocket connection for session {session_id}") + cdp_url = config.get('cdpUrl', 'NOT SET') + logger.info(f"[CONNECTION POOL] Creating new WebSocket connection for session {session_id}, CDP URL: {cdp_url}") wrapper = WebSocketBrowserWrapper(config) await wrapper.start() self._connections[session_id] = wrapper - logger.info(f"Successfully created WebSocket connection for session {session_id}") + logger.info(f"[CONNECTION POOL] Successfully created WebSocket connection for session {session_id}, CDP URL: {cdp_url}") return wrapper async def close_connection(self, session_id: str): @@ -258,7 +259,7 @@ def __init__( else: logger.info(f"[HybridBrowserToolkit] Using provided user_data_dir: {user_data_dir}") - logger.debug(f"[HybridBrowserToolkit] Calling super().__init__ with session_id: {session_id}") + logger.info(f"[HybridBrowserToolkit] Calling super().__init__ with session_id: {session_id}, cdp_url: {cdp_url}") super().__init__( headless=headless, user_data_dir=user_data_dir, @@ -281,24 +282,28 @@ def __init__( cdp_keep_current_page=cdp_keep_current_page, full_visual_mode=full_visual_mode, ) - logger.info(f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}") + logger.info(f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}, session_id: {self._session_id}") async def _ensure_ws_wrapper(self): """Ensure WebSocket wrapper is initialized using connection pool.""" - logger.debug(f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}") + import traceback global websocket_connection_pool # Get session ID from config or use default session_id = self._ws_config.get("session_id", "default") - logger.debug(f"[HybridBrowserToolkit] Using session_id: {session_id}") # Log when connecting to browser - cdp_url = self._ws_config.get("cdp_url", f"http://localhost:{env('browser_port', '9222')}") - logger.info(f"[PROJECT BROWSER] Connecting to browser via CDP at {cdp_url}") + cdp_url = self._ws_config.get("cdpUrl", f"http://localhost:{env('browser_port', '9222')}") + + # Log stack trace to see who's calling this + stack = traceback.extract_stack() + caller_info = f"{stack[-2].filename}:{stack[-2].lineno} in {stack[-2].name}" + + logger.info(f"[TOOLKIT SESSION {session_id}] Connecting to browser via CDP at {cdp_url} (api_task_id: {getattr(self, 'api_task_id', 'UNKNOWN')}, toolkit_id: {id(self)}, caller: {caller_info})") # Get or create connection from pool self._ws_wrapper = await websocket_connection_pool.get_connection(session_id, self._ws_config) - logger.info(f"[HybridBrowserToolkit] WebSocket wrapper initialized for session: {session_id}") + logger.info(f"[TOOLKIT SESSION {session_id}] WebSocket wrapper initialized, CDP URL in config: {cdp_url}") # Additional health check if self._ws_wrapper.websocket is None: @@ -314,11 +319,16 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro # For cloned sessions, use the same user_data_dir to share login state # This allows multiple agents to use the same browser profile without conflicts - logger.info(f"Cloning session {new_session_id} with shared user_data_dir: {self._user_data_dir}") + parent_session_id = self._session_id if hasattr(self, '_session_id') else 'UNKNOWN' + logger.info(f"[TOOLKIT CLONE] Parent session {parent_session_id} -> New session {new_session_id}, shared user_data_dir: {self._user_data_dir}") # Use the same CDP URL as parent cdp_url = self.config_loader.get_browser_config().cdp_url - logger.info(f"Cloning with CDP URL: {cdp_url} for session {new_session_id}") + logger.info(f"[TOOLKIT CLONE] Parent session {parent_session_id} -> New session {new_session_id}, CDP URL: {cdp_url}") + + # When cloning with cdp_keep_current_page=True, don't use default_start_url + cdp_keep_current = self.config_loader.get_browser_config().cdp_keep_current_page + start_url = None if cdp_keep_current else self._default_start_url # Use the same session_id to share the same browser instance # This ensures all clones use the same WebSocket connection and browser @@ -328,11 +338,11 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro user_data_dir=self._user_data_dir, # Use the same user_data_dir stealth=self._stealth, cache_dir=f"{self._cache_dir.rstrip('/')}/_clone_{new_session_id}/", - enabled_tools=self.enabled_tools.copy(), + enabled_tools=getattr(self, '_enabled_tools', None).copy() if getattr(self, '_enabled_tools', None) else None, browser_log_to_file=self._browser_log_to_file, log_dir=self.config_loader.get_toolkit_config().log_dir, session_id=new_session_id, - default_start_url=self._default_start_url, + default_start_url=start_url, default_timeout=self._default_timeout, short_timeout=self._short_timeout, navigation_timeout=self._navigation_timeout, @@ -343,7 +353,7 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro viewport_limit=self._viewport_limit, connect_over_cdp=self.config_loader.get_browser_config().connect_over_cdp, cdp_url=cdp_url, - cdp_keep_current_page=self.config_loader.get_browser_config().cdp_keep_current_page, + cdp_keep_current_page=cdp_keep_current, full_visual_mode=self._full_visual_mode, ) From 3d5280fae603523f6f47b2ea2fc225253a6de5b8 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 10 Dec 2025 16:56:26 +0100 Subject: [PATCH 13/40] update --- backend/app/utils/workforce.py | 82 +++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 1ecfccf52..e1a6679e1 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -408,20 +408,72 @@ def stop_gracefully(self) -> None: def _cleanup_all_agents(self) -> None: """Call cleanup callbacks for all agents to release resources (e.g., CDP browsers).""" logger.info(f"[WF-CLEANUP] Starting cleanup for all agents in workforce {id(self)}") + + # ========== 调试信息开始 ========== + logger.info(f"[WF-CLEANUP-DEBUG] hasattr(self, 'children'): {hasattr(self, 'children')}") + logger.info(f"[WF-CLEANUP-DEBUG] hasattr(self, '_children'): {hasattr(self, '_children')}") + + if hasattr(self, 'children'): + try: + logger.info(f"[WF-CLEANUP-DEBUG] self.children = {self.children}") + logger.info(f"[WF-CLEANUP-DEBUG] bool(self.children) = {bool(self.children)}") + except AttributeError as e: + logger.info(f"[WF-CLEANUP-DEBUG] AttributeError accessing self.children: {e}") + + if hasattr(self, '_children'): + logger.info(f"[WF-CLEANUP-DEBUG] self._children exists: {self._children is not None}") + if self._children is not None: + logger.info(f"[WF-CLEANUP-DEBUG] len(self._children) = {len(self._children)}") + for i, child in enumerate(self._children): + logger.info(f"[WF-CLEANUP-DEBUG] _children[{i}]: type={type(child).__name__}, " + f"has_worker_agent={hasattr(child, 'worker_agent')}, " + f"has_agent_pool={hasattr(child, 'agent_pool')}") + + logger.info(f"[WF-CLEANUP-DEBUG] hasattr(self, 'coordinator_agent'): {hasattr(self, 'coordinator_agent')}") + if hasattr(self, 'coordinator_agent'): + logger.info(f"[WF-CLEANUP-DEBUG] self.coordinator_agent is None: {self.coordinator_agent is None}") + if self.coordinator_agent is not None: + logger.info(f"[WF-CLEANUP-DEBUG] coordinator has _cleanup_callback: {hasattr(self.coordinator_agent, '_cleanup_callback')}") + # ========== 调试信息结束 ========== + cleanup_count = 0 - # Cleanup all child workers - if hasattr(self, 'children') and self.children: - for child in self.children: + # Cleanup all child workers - 使用 _children 而不是 children + if hasattr(self, '_children') and self._children: + logger.info(f"[WF-CLEANUP] Processing {len(self._children)} children") + for child in self._children: + # Cleanup base agent if hasattr(child, 'worker_agent'): agent = child.worker_agent if hasattr(agent, '_cleanup_callback') and callable(agent._cleanup_callback): try: agent._cleanup_callback() cleanup_count += 1 - logger.info(f"[WF-CLEANUP] Called cleanup for agent: {getattr(agent, 'agent_name', 'unknown')}") + logger.info(f"[WF-CLEANUP] Called cleanup for base agent: {getattr(agent, 'agent_name', 'unknown')}") except Exception as e: - logger.error(f"[WF-CLEANUP] Error in cleanup callback for agent: {e}", exc_info=True) + logger.error(f"[WF-CLEANUP] Error in cleanup callback for base agent: {e}", exc_info=True) + + # Cleanup agents in AgentPool (cloned agents that actually hold CDP resources) + if hasattr(child, 'agent_pool') and child.agent_pool: + pool = child.agent_pool + logger.info(f"[WF-CLEANUP] Found AgentPool for worker: {getattr(child, 'description', 'unknown')}, " + f"available={len(pool._available_agents)}, in_use={len(pool._in_use_agents)}") + + # Cleanup available agents + for agent in list(pool._available_agents): + if hasattr(agent, '_cleanup_callback') and callable(agent._cleanup_callback): + try: + agent._cleanup_callback() + cleanup_count += 1 + logger.info(f"[WF-CLEANUP] Called cleanup for pooled agent (available): {getattr(agent, 'agent_id', 'unknown')}") + except Exception as e: + logger.error(f"[WF-CLEANUP] Error in cleanup callback for pooled agent: {e}", exc_info=True) + + # Cleanup in-use agents - they are not directly accessible from AgentPool + # AgentPool only stores agent IDs in _in_use_agents set, not the agent objects + if hasattr(pool, '_in_use_agents') and pool._in_use_agents: + logger.info(f"[WF-CLEANUP] Warning: {len(pool._in_use_agents)} agents still in use") + logger.info(f"[WF-CLEANUP] These agents cannot be directly accessed from pool, but their CDP resources will be force-released") # Cleanup coordinator agent if hasattr(self, 'coordinator_agent') and self.coordinator_agent: @@ -435,6 +487,26 @@ def _cleanup_all_agents(self) -> None: logger.info(f"[WF-CLEANUP] ✅ Cleanup completed, {cleanup_count} agent(s) cleaned up") + # Force release all CDP browser resources for this task + # This handles the case where agents are still in-use and cannot be accessed from pool + try: + from app.utils.agent import _cdp_pool_manager + logger.info(f"[WF-CLEANUP] Force releasing all CDP resources for task {self.api_task_id}") + + # Get all occupied ports before cleanup + occupied_before = _cdp_pool_manager.get_occupied_ports().copy() + logger.info(f"[WF-CLEANUP] CDP ports occupied before force release: {occupied_before}") + + # Force release all ports (clear the entire pool) + # This is safe because the task is ending, no agents should be using them anymore + released_count = len(occupied_before) + _cdp_pool_manager._occupied_ports.clear() + + logger.info(f"[WF-CLEANUP] ✅ Force released {released_count} CDP browser(s)") + logger.info(f"[WF-CLEANUP] CDP ports after force release: {_cdp_pool_manager.get_occupied_ports()}") + except Exception as e: + logger.error(f"[WF-CLEANUP] Error during force CDP release: {e}", exc_info=True) + def skip_gracefully(self) -> None: logger.info("=" * 80) logger.info(f"⏭️ [WF-LIFECYCLE] skip_gracefully() CALLED", extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}) From fc13178355bf2998502c9794e66181e47fae4fc9 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 10 Dec 2025 17:30:45 +0100 Subject: [PATCH 14/40] comment message integration --- backend/app/utils/agent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 37817b1ca..66b3fd9c2 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -1115,7 +1115,9 @@ def release_cdp_from_agent(agent): web_toolkit_custom._cdp_session_id = toolkit_session_id # Register toolkit with message_integration - web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) + # DISABLED: message_integration causes parameter mismatch errors after fixing clone + # The wrapper is lost during toolkit cloning, causing tools to reject message_title parameter + # web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) # Use the registered (wrapped) toolkit for both tools and agent registration web_toolkit_for_agent_registration = web_toolkit_custom terminal_toolkit = TerminalToolkit(options.project_id, Agents.search_agent, safe_mode=True, clone_current_env=False) From 5ccb975b886047c8dfd3c0294d0a3bc15f5e3f59 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 11 Dec 2025 14:31:43 +0100 Subject: [PATCH 15/40] fix cdp pool add / remove / detect logic --- backend/app/service/chat_service.py | 22 +++- electron/main/index.ts | 160 +++++++++++++++++++++------- src/pages/Dashboard/Browser.tsx | 20 +++- src/store/chatStore.ts | 17 ++- 4 files changed, 171 insertions(+), 48 deletions(-) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 590157a25..863f54a93 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -235,10 +235,24 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str: @traceroot.trace() async def step_solve(options: Chat, request: Request, task_lock: TaskLock): # Log CDP browsers received from frontend - logger.info(f"[BACKEND CDP] Received request for project: {options.project_id}") - logger.info(f"[BACKEND CDP] browser_port: {options.browser_port}, use_external_cdp: {options.use_external_cdp}") - logger.info(f"[BACKEND CDP] cdp_browsers count: {len(options.cdp_browsers) if hasattr(options, 'cdp_browsers') else 'N/A'}") - logger.info(f"[BACKEND CDP] cdp_browsers: {options.cdp_browsers if hasattr(options, 'cdp_browsers') else 'N/A'}") + logger.info(f"[BACKEND CDP] ========================================") + logger.info(f"[BACKEND CDP] Received task request for project: {options.project_id}") + logger.info(f"[BACKEND CDP] browser_port: {options.browser_port}") + logger.info(f"[BACKEND CDP] use_external_cdp: {options.use_external_cdp}") + + if hasattr(options, 'cdp_browsers') and options.cdp_browsers: + logger.info(f"[BACKEND CDP] cdp_browsers count: {len(options.cdp_browsers)}") + for idx, browser in enumerate(options.cdp_browsers): + port = browser.get('port', 'N/A') + is_external = browser.get('isExternal', 'N/A') + name = browser.get('name', 'Unnamed') + browser_id = browser.get('id', 'N/A') + logger.info(f"[BACKEND CDP] Browser {idx + 1}: port={port}, isExternal={is_external}, name=\"{name}\", id={browser_id}") + else: + logger.warn(f"[BACKEND CDP] ⚠️ NO CDP browsers configured - cdp_browsers is empty or missing") + logger.warn(f"[BACKEND CDP] ⚠️ Agents will all use default browser port: {options.browser_port}") + + logger.info(f"[BACKEND CDP] ========================================") # if True: # import faulthandler diff --git a/electron/main/index.ts b/electron/main/index.ts index 40867355c..ca3782d23 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -322,8 +322,19 @@ function registerIpcHandlers() { // Get all browsers in the pool ipcMain.handle('get-cdp-browsers', () => { - log.info(`[CDP POOL] Getting CDP browser pool, count: ${cdp_browser_pool.length}`) - log.info(`[CDP POOL] Pool content: ${JSON.stringify(cdp_browser_pool.map(b => ({id: b.id, port: b.port, name: b.name})))}`) + log.info(`[CDP POOL GET] ========================================`) + log.info(`[CDP POOL GET] Getting CDP browser pool at ${new Date().toISOString()}`) + log.info(`[CDP POOL GET] Pool size: ${cdp_browser_pool.length}`) + + if (cdp_browser_pool.length > 0) { + cdp_browser_pool.forEach((b, idx) => { + log.info(`[CDP POOL GET] Browser ${idx + 1}: port=${b.port}, isExternal=${b.isExternal}, name="${b.name}", id=${b.id}`) + }) + } else { + log.warn(`[CDP POOL GET] ⚠️ Pool is EMPTY - no browsers configured`) + } + + log.info(`[CDP POOL GET] ========================================`) return cdp_browser_pool }); @@ -336,11 +347,19 @@ function registerIpcHandlers() { // 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}`) + log.info(`[CDP POOL ADD] ========================================`) + log.info(`[CDP POOL ADD] Request to add browser at ${new Date().toISOString()}`) + log.info(`[CDP POOL ADD] Port: ${port}`) + log.info(`[CDP POOL ADD] Is External: ${isExternal}`) + log.info(`[CDP POOL ADD] Name: "${name}"`) + log.info(`[CDP POOL ADD] Current pool size: ${cdp_browser_pool.length}`) // Check if browser with this port already exists const existing = cdp_browser_pool.find(b => b.port === port); if (existing) { + log.warn(`[CDP POOL ADD] ❌ REJECTED - Browser with port ${port} already exists in pool`) + log.warn(`[CDP POOL ADD] Existing browser: id=${existing.id}, name="${existing.name}"`) + log.info(`[CDP POOL ADD] ========================================`) return { success: false, error: 'Browser with this port already exists' }; } @@ -353,35 +372,45 @@ function registerIpcHandlers() { }; cdp_browser_pool.push(newBrowser); - log.info(`Browser added to pool, new count: ${cdp_browser_pool.length}`) + log.info(`[CDP POOL ADD] ✅ SUCCESS - Browser added to pool`) + log.info(`[CDP POOL ADD] Browser ID: ${newBrowser.id}`) + log.info(`[CDP POOL ADD] New pool size: ${cdp_browser_pool.length}`) + log.info(`[CDP POOL ADD] All ports in pool: [${cdp_browser_pool.map(b => b.port).join(', ')}]`) + log.info(`[CDP POOL ADD] ========================================`) return { success: true, browser: newBrowser }; }); // Remove browser from pool ipcMain.handle('remove-cdp-browser', (event, browserId: string) => { - log.info(`Removing CDP browser: ${browserId}`) + log.info(`[CDP POOL REMOVE] ========================================`) + log.info(`[CDP POOL REMOVE] Request to remove browser: ${browserId}`) const index = cdp_browser_pool.findIndex(b => b.id === browserId); if (index === -1) { + log.warn(`[CDP POOL REMOVE] ❌ Browser not found: ${browserId}`) + log.info(`[CDP POOL REMOVE] ========================================`) return { success: false, error: 'Browser not found' }; } const removed = cdp_browser_pool.splice(index, 1)[0]; + log.info(`[CDP POOL REMOVE] Removed browser: port=${removed.port}, name="${removed.name}"`) // 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}`); + log.info(`[CDP POOL REMOVE] Killing launched browser process on port ${removed.port}`); try { const process = cdp_browser_processes.get(removed.port); process?.kill(); cdp_browser_processes.delete(removed.port); + log.info(`[CDP POOL REMOVE] Browser process killed successfully`) } catch (error) { - log.warn(`Failed to kill browser process on port ${removed.port}: ${error}`); + log.warn(`[CDP POOL REMOVE] Failed to kill browser process on port ${removed.port}: ${error}`); } } - log.info(`Browser removed from pool, remaining count: ${cdp_browser_pool.length}`) + log.info(`[CDP POOL REMOVE] ✅ SUCCESS - Remaining pool size: ${cdp_browser_pool.length}`) + log.info(`[CDP POOL REMOVE] ========================================`) return { success: true, browser: removed }; }); @@ -431,7 +460,9 @@ function registerIpcHandlers() { // Launch CDP browser with custom port ipcMain.handle('launch-cdp-browser', async (event, port: number) => { - log.info(`Launching CDP browser on port ${port}`); + log.info(`[CDP LAUNCH] ========================================`) + log.info(`[CDP LAUNCH] Request to launch browser at ${new Date().toISOString()}`) + log.info(`[CDP LAUNCH] Target port: ${port}`) try { const platform = process.platform; @@ -560,7 +591,8 @@ function registerIpcHandlers() { // Check if browser on this port is already running if (cdp_browser_processes.has(port)) { - log.warn(`Browser process already exists on port ${port}`); + log.warn(`[CDP LAUNCH] ❌ Browser process already exists on port ${port}`); + log.info(`[CDP LAUNCH] ========================================`) return { success: false, error: `Browser already running on port ${port}`, @@ -577,7 +609,9 @@ function registerIpcHandlers() { 'about:blank', ]; - log.info(`Launching Chrome with args: ${args.join(' ')}`); + log.info(`[CDP LAUNCH] Spawning Chrome process...`); + log.info(`[CDP LAUNCH] Executable: ${chromeExecutable}`); + log.info(`[CDP LAUNCH] Args: ${args.join(' ')}`); // Spawn Chrome process const browserProcess = spawn(chromeExecutable, args, { @@ -586,51 +620,97 @@ function registerIpcHandlers() { }); browserProcess.on('error', (error) => { - log.error(`CDP browser process on port ${port} error: ${error}`); + log.error(`[CDP LAUNCH] Browser process error on port ${port}: ${error}`); cdp_browser_processes.delete(port); + + // Also remove from pool if it was added + const browserInPool = cdp_browser_pool.find(b => b.port === port && !b.isExternal); + if (browserInPool) { + const index = cdp_browser_pool.indexOf(browserInPool); + cdp_browser_pool.splice(index, 1); + log.warn(`[CDP POOL AUTO-REMOVE] Browser on port ${port} removed from pool due to process error`); + log.info(`[CDP POOL AUTO-REMOVE] New pool size: ${cdp_browser_pool.length}`); + } }); browserProcess.on('exit', (code) => { - log.info(`CDP browser process on port ${port} exited with code ${code}`); + log.info(`[CDP LAUNCH] Browser process on port ${port} exited with code ${code}`); cdp_browser_processes.delete(port); + + // Also remove from pool if it was added + const browserInPool = cdp_browser_pool.find(b => b.port === port && !b.isExternal); + if (browserInPool) { + const index = cdp_browser_pool.indexOf(browserInPool); + cdp_browser_pool.splice(index, 1); + log.warn(`[CDP POOL AUTO-REMOVE] Browser on port ${port} removed from pool due to process exit`); + log.info(`[CDP POOL AUTO-REMOVE] Exited with code: ${code}`); + log.info(`[CDP POOL AUTO-REMOVE] Browser ID: ${browserInPool.id}, Name: "${browserInPool.name}"`); + log.info(`[CDP POOL AUTO-REMOVE] New pool size: ${cdp_browser_pool.length}`); + if (cdp_browser_pool.length > 0) { + log.info(`[CDP POOL AUTO-REMOVE] Remaining ports: [${cdp_browser_pool.map(b => b.port).join(', ')}]`); + } + } }); // Store the process in the Map cdp_browser_processes.set(port, browserProcess); + log.info(`[CDP LAUNCH] Browser process stored in map, PID: ${browserProcess.pid}`); + + // Poll for browser to become ready (max 5 seconds) + log.info(`[CDP LAUNCH] Polling for browser to become ready (max 5 seconds)...`); + const maxWaitTime = 5000; // 5 seconds + const pollInterval = 300; // Check every 300ms + const startTime = Date.now(); + let attempt = 0; + let lastError = null; + + while (Date.now() - startTime < maxWaitTime) { + attempt++; + try { + log.info(`[CDP LAUNCH] Attempt ${attempt}: Checking http://localhost:${port}/json/version`); + const response = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 1000, // Short timeout for each attempt + }); - // Wait a bit for browser to start - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Verify browser is accessible - try { - const response = await axios.get(`http://localhost:${port}/json/version`, { - timeout: 5000, - }); - - if (response.status === 200) { - log.info(`CDP browser successfully launched on port ${port}`); - // This is our own launched browser, not external - use_external_cdp = false; - return { - success: true, - port, - data: response.data, - }; + if (response.status === 200 && response.data) { + const elapsedTime = Date.now() - startTime; + log.info(`[CDP LAUNCH] ✅ SUCCESS - Browser ready on port ${port} after ${elapsedTime}ms (${attempt} attempts)`); + log.info(`[CDP LAUNCH] Browser info: ${JSON.stringify(response.data)}`); + log.info(`[CDP LAUNCH] ⚠️ NOTE: Browser launched but NOT added to pool yet`); + log.info(`[CDP LAUNCH] ⚠️ The UI must call 'add-cdp-browser' to add it to the pool`); + log.info(`[CDP LAUNCH] ========================================`) + // This is our own launched browser, not external + use_external_cdp = false; + return { + success: true, + port, + data: response.data, + }; + } + } catch (pollError: any) { + lastError = pollError; + // Log only every 3rd attempt to avoid spam + if (attempt % 3 === 0) { + log.info(`[CDP LAUNCH] Attempt ${attempt}: Not ready yet (${pollError.code || pollError.message})`); + } } - } catch (verifyError) { - log.warn(`Failed to verify CDP browser: ${verifyError}`); - return { - success: false, - error: 'Browser launched but not responding on CDP port', - }; + + // Wait before next attempt + await new Promise(resolve => setTimeout(resolve, pollInterval)); } + // If we get here, browser didn't respond within max wait time + const totalTime = Date.now() - startTime; + log.warn(`[CDP LAUNCH] ❌ Verification failed after ${totalTime}ms (${attempt} attempts)`); + log.warn(`[CDP LAUNCH] Last error: ${lastError?.code || lastError?.message || 'Unknown'}`); + log.info(`[CDP LAUNCH] ========================================`) return { - success: true, - port, + success: false, + error: `Browser launched but not responding on CDP port after ${totalTime}ms`, }; } catch (error: any) { - log.error(`Failed to launch CDP browser: ${error}`); + log.error(`[CDP LAUNCH] ❌ FAILED to launch browser: ${error}`); + log.info(`[CDP LAUNCH] ========================================`) return { success: false, error: error.message, diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index 3a906b739..5ad6597af 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -117,16 +117,20 @@ export default function Browser() { const loadCdpBrowsers = async () => { if (window.electronAPI?.getCdpBrowsers) { try { + console.log('[FRONTEND CDP LOAD] Loading CDP browser pool...'); const browsers = await window.electronAPI.getCdpBrowsers(); + console.log('[FRONTEND CDP LOAD] Loaded browsers:', browsers); + console.log(`[FRONTEND CDP LOAD] Pool size: ${browsers.length}`); setCdpBrowsers(browsers); // Also load running browser ports if (window.electronAPI?.getRunningBrowserPorts) { const ports = await window.electronAPI.getRunningBrowserPorts(); + console.log('[FRONTEND CDP LOAD] Running browser ports:', ports); setRunningPorts(ports); } } catch (error) { - console.error("Failed to load CDP browsers:", error); + console.error("[FRONTEND CDP LOAD] Failed to load CDP browsers:", error); } } }; @@ -199,17 +203,22 @@ export default function Browser() { setShowUseExistingDialog(false); if (pendingPort) { try { + console.log(`[FRONTEND CDP ADD] Attempting to add external browser on port ${pendingPort}`); // Add browser to pool if (window.electronAPI?.addCdpBrowser) { const result = await window.electronAPI.addCdpBrowser(pendingPort, true, `External Browser (${pendingPort})`); + console.log(`[FRONTEND CDP ADD] Result:`, result); if (result.success) { + console.log(`[FRONTEND CDP ADD] ✅ Successfully added browser ${result.browser.id} on port ${pendingPort}`); toast.success(`Added external browser on port ${pendingPort} to pool`); await loadCdpBrowsers(); } else { + console.error(`[FRONTEND CDP ADD] ❌ Failed to add browser:`, result.error); toast.error(result.error || "Failed to add browser to pool"); } } } catch (error: any) { + console.error(`[FRONTEND CDP ADD] ❌ Exception:`, error); toast.error(error.message || "Failed to add browser to pool"); } } @@ -232,19 +241,26 @@ export default function Browser() { return; } + console.log(`[FRONTEND CDP LAUNCH] Launching browser on port ${port}...`); toast.loading(`Launching browser on port ${port}...`, { id: 'launch-browser' }); const result = await window.electronAPI.launchCdpBrowser(port); + console.log(`[FRONTEND CDP LAUNCH] Launch result:`, result); if (result.success) { + console.log(`[FRONTEND CDP LAUNCH] ✅ Browser launched successfully on port ${port}`); toast.success(`Browser launched successfully on port ${port}`, { id: 'launch-browser' }); // Add launched browser to pool + console.log(`[FRONTEND CDP LAUNCH] Adding launched browser to pool...`); if (window.electronAPI?.addCdpBrowser) { const addResult = await window.electronAPI.addCdpBrowser(port, false, `Launched Browser (${port})`); + console.log(`[FRONTEND CDP LAUNCH] Add to pool result:`, addResult); if (addResult.success) { + console.log(`[FRONTEND CDP LAUNCH] ✅ Browser added to pool: ${addResult.browser.id}`); await loadCdpBrowsers(); } else { + console.error(`[FRONTEND CDP LAUNCH] ❌ Failed to add to pool:`, addResult.error); toast.error(addResult.error || "Failed to add browser to pool"); } } @@ -256,9 +272,11 @@ export default function Browser() { data: result.data, }); } else { + console.error(`[FRONTEND CDP LAUNCH] ❌ Launch failed:`, result.error); toast.error(result.error || "Failed to launch browser", { id: 'launch-browser' }); } } catch (error: any) { + console.error(`[FRONTEND CDP LAUNCH] ❌ Exception:`, error); toast.error(error.message || "Failed to launch browser", { id: 'launch-browser' }); } }; diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index e4f18b275..4ecb1d9fb 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -484,9 +484,20 @@ 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'); - console.log('[FRONTEND CDP] Project:', project_id, 'Browser port:', browser_port, 'External CDP:', use_external_cdp); - console.log('[FRONTEND CDP] CDP Browsers count:', cdp_browsers?.length || 0); - console.log('[FRONTEND CDP] CDP Browsers details:', JSON.stringify(cdp_browsers)); + console.log('[FRONTEND CDP TASK] ========================================'); + console.log('[FRONTEND CDP TASK] Starting new task, requesting CDP configuration...'); + console.log('[FRONTEND CDP TASK] Project:', project_id); + console.log('[FRONTEND CDP TASK] Browser port:', browser_port); + console.log('[FRONTEND CDP TASK] External CDP:', use_external_cdp); + console.log('[FRONTEND CDP TASK] CDP Browsers count:', cdp_browsers?.length || 0); + if (cdp_browsers && cdp_browsers.length > 0) { + cdp_browsers.forEach((browser: any, idx: number) => { + console.log(`[FRONTEND CDP TASK] Browser ${idx + 1}: port=${browser.port}, isExternal=${browser.isExternal}, name="${browser.name}"`); + }); + } else { + console.warn('[FRONTEND CDP TASK] ⚠️ No browsers in pool - all agents will use default port'); + } + console.log('[FRONTEND CDP TASK] ========================================'); // Lock the chatStore reference at the start of SSE session to prevent focus changes // during active message processing From 4ef32afa6cf9b136c7f698b123efd778d669f14e Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Fri, 12 Dec 2025 14:39:04 +0100 Subject: [PATCH 16/40] update browser log --- backend/app/utils/agent.py | 14 ++++++++++++++ .../utils/toolkit/hybrid_browser_python_toolkit.py | 10 +++++++++- .../app/utils/toolkit/hybrid_browser_toolkit.py | 8 ++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index a3ddfe4d2..d88de96a5 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -969,6 +969,18 @@ def search_agent(options: Chat): message_handler=HumanToolkit(options.project_id, Agents.search_agent).send_message_to_user ) + # Build task-specific log directory path for browser logs + import re + from pathlib import Path + email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", options.email.split("@")[0]).strip(".") + task_log_dir = ( + Path.home() + / ".eigent" + / email_sanitized + / f"project_{options.project_id}" + / f"task_{options.task_id}" + ) + # Define CDP acquire callback for cloning def acquire_cdp_for_agent(agent): """Acquire a CDP browser from pool and create new toolkit for the agent.""" @@ -1017,6 +1029,7 @@ def acquire_cdp_for_agent(agent): browser_log_to_file=True, stealth=True, session_id=session_id, + log_dir=str(task_log_dir), default_start_url=default_url, connect_over_cdp=True, cdp_url=f"http://localhost:{selected_port}", @@ -1121,6 +1134,7 @@ def release_cdp_from_agent(agent): browser_log_to_file=True, stealth=True, session_id=toolkit_session_id, # Use the session ID for pool management + log_dir=str(task_log_dir), default_start_url=default_url, connect_over_cdp=True, cdp_url=f"http://localhost:{selected_port}", diff --git a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py index db48abf2f..3a673951b 100644 --- a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py @@ -141,6 +141,7 @@ def __init__( enabled_tools: List[str] | None = None, browser_log_to_file: bool = False, session_id: str | None = None, + log_base_dir: str | None = None, default_start_url: str = "https://google.com/", default_timeout: int | None = None, short_timeout: int | None = None, @@ -156,6 +157,7 @@ def __init__( self._stealth = stealth self._cache_dir = cache_dir self._browser_log_to_file = browser_log_to_file + self._log_base_dir = log_base_dir self._default_start_url = default_start_url self._session_id = session_id or "default" @@ -179,7 +181,12 @@ def __init__( # Set up log file if needed if self.log_to_file: # Create log directory if it doesn't exist - log_dir = "browser_log" + # If log_base_dir is provided, use task-specific directory; otherwise use default backend/browser_log + if log_base_dir: + log_dir = os.path.join(log_base_dir, "browser_logs") + else: + log_dir = "browser_log" # Backward compatibility: use default location + os.makedirs(log_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -320,6 +327,7 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro enabled_tools=self.enabled_tools.copy(), browser_log_to_file=self._browser_log_to_file, session_id=new_session_id, + log_base_dir=self._log_base_dir, default_start_url=self._default_start_url, default_timeout=self._default_timeout, short_timeout=self._short_timeout, diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index 2fc265395..39f204117 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -247,7 +247,10 @@ def __init__( logger.info(f"[HybridBrowserToolkit] Initializing with api_task_id: {api_task_id}") self.api_task_id = api_task_id logger.debug(f"[HybridBrowserToolkit] api_task_id set to: {self.api_task_id}") - + + # Store log_dir for use in clone() + self._log_dir = log_dir + # Set default user_data_dir if not provided if user_data_dir is None: # Use browser port to determine profile directory @@ -267,6 +270,7 @@ def __init__( cache_dir=cache_dir, enabled_tools=enabled_tools, browser_log_to_file=browser_log_to_file, + log_dir=log_dir, session_id=session_id, default_start_url=default_start_url, default_timeout=default_timeout, @@ -340,7 +344,7 @@ def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBro cache_dir=f"{self._cache_dir.rstrip('/')}/_clone_{new_session_id}/", enabled_tools=getattr(self, '_enabled_tools', None).copy() if getattr(self, '_enabled_tools', None) else None, browser_log_to_file=self._browser_log_to_file, - log_dir=self.config_loader.get_toolkit_config().log_dir, + log_dir=self._log_dir, # Use the same log_dir as parent session_id=new_session_id, default_start_url=start_url, default_timeout=self._default_timeout, From f1907c55d521f48f6e6a7e08309366df7bce2c53 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 17 Dec 2025 23:34:26 +0100 Subject: [PATCH 17/40] update dynamic prompt --- backend/app/utils/agent.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index d88de96a5..09b1b8b1e 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -1183,6 +1183,15 @@ def release_cdp_from_agent(agent): *search_tools, ] + # Build external browser connection notice if using external CDP + external_browser_notice = "" + if selected_is_external: + external_browser_notice = """ + +**IMPORTANT**: You are connected to an external browser instance. The browser may already be open with active sessions and logged-in websites. When you use `browser_open`, you will connect to this existing browser and can immediately access its current state and pages. The user may have already logged into required websites, so you can leverage these authenticated sessions. + +""" + system_message = f""" You are a Senior Research Analyst, a key member of a multi-agent team. Your @@ -1256,7 +1265,7 @@ def release_cdp_from_agent(agent): Your approach depends on available search tools: - +{external_browser_notice} **Common Browser Operations (both scenarios):** - **Navigation and Exploration**: Use `browser_visit_page` to open URLs. `browser_visit_page` provides a snapshot of currently visible From a4aee0316d4d979752c39e65b727e004a02e396e Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 1 Jan 2026 14:21:05 +0000 Subject: [PATCH 18/40] update parallel tool call false --- backend/app/utils/agent.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 8dce78d76..62816573f 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -748,9 +748,12 @@ def agent_model( url=options.api_url, model_config_dict={ "user": str(options.project_id), + "parallel_tool_calls": False, } if options.is_cloud() - else None, + else { + "parallel_tool_calls": False, + }, **{ k: v for k, v in (options.extra_params or {}).items() @@ -1285,7 +1288,9 @@ def release_cdp_from_agent(agent): - When encountering verification challenges (like login, CAPTCHAs or robot checks), you MUST request help using the human toolkit. - """ +
+When you select name in salesforce new event, please select the corresponding option after input to confirm, If you only input the name, it would not been confirmed. +
rest """ # Define cleanup callback to release CDP browser back to pool def cleanup_cdp_browser(): @@ -1863,9 +1868,12 @@ async def mcp_agent(options: Chat): url=options.api_url, model_config_dict={ "user": str(options.project_id), + "parallel_tool_calls": False, } if options.is_cloud() - else None, + else { + "parallel_tool_calls": False, + }, **{ k: v for k, v in (options.extra_params or {}).items() From ca61236bb3d917e9a058c8778462a36c3b1dabf0 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 19:16:42 +0000 Subject: [PATCH 19/40] update --- electron/main/index.ts | 47 ++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index e3b3615f2..85ec49d89 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,19 +1,34 @@ -import { app, BrowserWindow, shell, ipcMain, Menu, dialog, nativeTheme, protocol, session } from 'electron' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import os, { homedir } from 'node:os' -import log from 'electron-log' -import { update, registerUpdateIpcHandlers } from './update' -import { checkToolInstalled, killProcessOnPort, startBackend } from './init' -import { WebViewManager } from './webview' -import { FileReader } from './fileReader' -import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' -import fs, { existsSync, readFileSync } from 'node:fs' -import fsp from 'fs/promises' -import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig' -import { getEnvPath, updateEnvBlock, removeEnvKey, getEmailFolderPath } from './utils/envUtil' -import { copyBrowserData } from './copy' -import { findAvailablePort } from './init' +import { + app, + BrowserWindow, + shell, + ipcMain, + Menu, + dialog, + nativeTheme, + protocol, + session, +} from 'electron'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import os, { homedir } from 'node:os'; +import log from 'electron-log'; +import { update, registerUpdateIpcHandlers } from './update'; +import { checkToolInstalled, killProcessOnPort, startBackend } from './init'; +import { WebViewManager } from './webview'; +import { FileReader } from './fileReader'; +import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; +import fs, { existsSync, readFileSync } from 'node:fs'; +import fsp from 'fs/promises'; +import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig'; +import { + getEnvPath, + updateEnvBlock, + removeEnvKey, + getEmailFolderPath, +} from './utils/envUtil'; +import { copyBrowserData } from './copy'; +import { findAvailablePort } from './init'; import kill from 'tree-kill'; import { zipFolder } from './utils/log' import mime from "mime"; From d470f85bd3f1fd75d642060f37700b2c40debc8e Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 19:32:26 +0000 Subject: [PATCH 20/40] update port --- electron/main/index.ts | 2 +- src/pages/Dashboard/Browser.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 85ec49d89..f0b5b17a1 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -54,7 +54,7 @@ let webViewManager: WebViewManager | null = null; let fileReader: FileReader | null = null; let python_process: ChildProcessWithoutNullStreams | null = null; let backendPort: number = 5001; -let browser_port = 9222; +let browser_port = 9223; let use_external_cdp = false; // Flag to track if using external CDP browser // CDP Browser Pool diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index 5ad6597af..c71e3d189 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -46,8 +46,8 @@ export default function Browser() { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // CDP port configuration - const [cdpPort, setCdpPort] = useState(9222); - const [customPort, setCustomPort] = useState("9222"); + const [cdpPort, setCdpPort] = useState(9223); + const [customPort, setCustomPort] = useState("9223"); const [portStatus, setPortStatus] = useState({ checking: false, available: null, From 2b8c0bfbfdeb96692a261c8e97fee1e3df425c40 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 19:43:01 +0000 Subject: [PATCH 21/40] comment parallel tool call --- backend/app/utils/agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 2d957968c..2f4b0c2f7 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -828,11 +828,11 @@ def agent_model( model_config_dict={ "user": str(options.project_id), - "parallel_tool_calls": False, + # "parallel_tool_calls": False, } if options.is_cloud() else { - "parallel_tool_calls": False, + # "parallel_tool_calls": False, }, **{ k: v @@ -1987,11 +1987,11 @@ async def mcp_agent(options: Chat): url=options.api_url, model_config_dict={ "user": str(options.project_id), - "parallel_tool_calls": False, + # "parallel_tool_calls": False, } if options.is_cloud() else { - "parallel_tool_calls": False, + # "parallel_tool_calls": False, }, **{ k: v From 8256eb9e92dea1216ccf342ab1bcf8e35ca41333 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 19:48:46 +0000 Subject: [PATCH 22/40] remove comment message integration --- backend/app/utils/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 2f4b0c2f7..52ba9ef6f 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -1266,7 +1266,7 @@ def release_cdp_from_agent(agent): # Register toolkit with message_integration # DISABLED: message_integration causes parameter mismatch errors after fixing clone # The wrapper is lost during toolkit cloning, causing tools to reject message_title parameter - # web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) + web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) # Use the registered (wrapped) toolkit for both tools and agent registration web_toolkit_for_agent_registration = web_toolkit_custom terminal_toolkit = TerminalToolkit(options.project_id, Agents.search_agent, safe_mode=True, clone_current_env=False) From bc0ff3e389f6a4df98f52d9ba509bd1aa2290c08 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 20:26:58 +0000 Subject: [PATCH 23/40] search agent with parallel tool false --- backend/app/utils/agent.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 52ba9ef6f..7382b6c8c 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -804,6 +804,7 @@ def agent_model( toolkits_to_register_agent: list[RegisteredAgentToolkit] | None = None, enable_snapshot_clean: bool = False, cleanup_callback: Callable[[], None] | None = None, + extra_model_config: dict | None = None, ): task_lock = get_task_lock(options.project_id) agent_id = str(uuid.uuid4()) @@ -828,11 +829,11 @@ def agent_model( model_config_dict={ "user": str(options.project_id), - # "parallel_tool_calls": False, + **(extra_model_config or {}), } if options.is_cloud() else { - # "parallel_tool_calls": False, + **(extra_model_config or {}), }, **{ k: v @@ -1440,6 +1441,7 @@ def cleanup_cdp_browser(): toolkits_to_register_agent=[web_toolkit_for_agent_registration], enable_snapshot_clean=True, cleanup_callback=cleanup_cdp_browser, + extra_model_config={"parallel_tool_calls": False}, ) # Attach CDP management callbacks to the agent for clone support @@ -1991,7 +1993,17 @@ async def mcp_agent(options: Chat): } if options.is_cloud() else { - # "parallel_tool_calls": False, + # " + # for tool in original_tools: + # method_name = tool.func.__name__ + # enhanced_func = self._add_messaging_to_tool(tool.func) + # enhanced_func = self._create_bound_method_wrapper( + # enhanced_func, + # toolkit, + # ) + # enhanced_methods[method_name] = enhanced_func + # setattr(toolkit, method_name, enhanced_func) + # original_get_tools_method = toolkit.get_toolsparallel_tool_calls": False, }, **{ k: v From 5e678d8b074691b94f660b3635640f7763fab0df Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 20:53:54 +0000 Subject: [PATCH 24/40] optimize comment --- backend/app/utils/agent.py | 20 +++----------------- backend/app/utils/workforce.py | 6 +++--- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index c44b46802..632360123 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -1253,8 +1253,6 @@ def release_cdp_from_agent(agent): web_toolkit_custom._cdp_session_id = toolkit_session_id # Register toolkit with message_integration - # DISABLED: message_integration causes parameter mismatch errors after fixing clone - # The wrapper is lost during toolkit cloning, causing tools to reject message_title parameter web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom) # Use the registered (wrapped) toolkit for both tools and agent registration web_toolkit_for_agent_registration = web_toolkit_custom @@ -1976,26 +1974,14 @@ async def mcp_agent(options: Chat): url=options.api_url, model_config_dict={ "user": str(options.project_id), - # "parallel_tool_calls": False, } if options.is_cloud() - else { - # " - # for tool in original_tools: - # method_name = tool.func.__name__ - # enhanced_func = self._add_messaging_to_tool(tool.func) - # enhanced_func = self._create_bound_method_wrapper( - # enhanced_func, - # toolkit, - # ) - # enhanced_methods[method_name] = enhanced_func - # setattr(toolkit, method_name, enhanced_func) - # original_get_tools_method = toolkit.get_toolsparallel_tool_calls": False, - }, + else None, **{ k: v for k, v in (options.extra_params or {}).items() - if k not in ["model_platform", "model_type", "api_key", "url"] + if + k not in ["model_platform", "model_type", "api_key", "url"] }, ), # output_language=options.language, diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index bcfea10f0..990144e18 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -498,7 +498,7 @@ def _cleanup_all_agents(self) -> None: """Call cleanup callbacks for all agents to release resources (e.g., CDP browsers).""" logger.info(f"[WF-CLEANUP] Starting cleanup for all agents in workforce {id(self)}") - # ========== 调试信息开始 ========== + # ========== DEBUG INFO START ========== logger.info(f"[WF-CLEANUP-DEBUG] hasattr(self, 'children'): {hasattr(self, 'children')}") logger.info(f"[WF-CLEANUP-DEBUG] hasattr(self, '_children'): {hasattr(self, '_children')}") @@ -523,11 +523,11 @@ def _cleanup_all_agents(self) -> None: logger.info(f"[WF-CLEANUP-DEBUG] self.coordinator_agent is None: {self.coordinator_agent is None}") if self.coordinator_agent is not None: logger.info(f"[WF-CLEANUP-DEBUG] coordinator has _cleanup_callback: {hasattr(self.coordinator_agent, '_cleanup_callback')}") - # ========== 调试信息结束 ========== + # ========== DEBUG INFO END ========== cleanup_count = 0 - # Cleanup all child workers - 使用 _children 而不是 children + # Cleanup all child workers - use _children instead of children if hasattr(self, '_children') and self._children: logger.info(f"[WF-CLEANUP] Processing {len(self._children)} children") for child in self._children: From 831d03ed70bcf7180aaa829ceac852c4de61b576 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 20:57:37 +0000 Subject: [PATCH 25/40] optimize comment --- backend/app/service/chat_service.py | 3 +-- backend/app/utils/agent.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 1605f1303..5787ed109 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -1390,8 +1390,7 @@ async def construct_workforce(options: Chat) -> tuple[Workforce, ListenChatAgent workforce.add_single_agent_worker( "Search Agent: Can search the web, extract webpage content, " "simulate browser actions, and provide relevant information to " - "solve the given task. " - "NOTE: Opening the browser will automatically open the Salesforce interface.", + "solve the given task. ", searcher, ) workforce.add_single_agent_worker( diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 632360123..367374c7e 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -1383,9 +1383,7 @@ def release_cdp_from_agent(agent): - When encountering verification challenges (like login, CAPTCHAs or robot checks), you MUST request help using the human toolkit. -
-When you select name in salesforce new event, please select the corresponding option after input to confirm, If you only input the name, it would not been confirmed. -
rest """ +""" # Define cleanup callback to release CDP browser back to pool def cleanup_cdp_browser(): From 1387f570a2ec87521338583afc9e3808bfdf6599 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 13 Jan 2026 20:58:33 +0000 Subject: [PATCH 26/40] optimize comment --- backend/app/service/chat_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 5787ed109..94d9a77de 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -1390,7 +1390,7 @@ async def construct_workforce(options: Chat) -> tuple[Workforce, ListenChatAgent workforce.add_single_agent_worker( "Search Agent: Can search the web, extract webpage content, " "simulate browser actions, and provide relevant information to " - "solve the given task. ", + "solve the given task.", searcher, ) workforce.add_single_agent_worker( From dc1c352d0ac49c63552988e7d6ef9162ab6f275a Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Mon, 26 Jan 2026 21:42:38 +0800 Subject: [PATCH 27/40] update --- backend/app/utils/agent.py | 67 ++++++++++--------- .../toolkit/hybrid_browser_python_toolkit.py | 2 + 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index e48078945..1d304d527 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -175,13 +175,13 @@ def acquire_browser(self, cdp_browsers: list[dict], session_id: str) -> dict | N port = browser.get('port') if port and port not in self._occupied_ports: self._occupied_ports[port] = session_id - traceroot_logger.info( + logger.info( f"Acquired browser on port {port} for session {session_id}. " f"Occupied: {list(self._occupied_ports.keys())}" ) return browser - traceroot_logger.warning( + logger.warning( f"No available browsers in pool for session {session_id}. " f"All occupied: {list(self._occupied_ports.keys())}" ) @@ -198,12 +198,12 @@ def release_browser(self, port: int, session_id: str): with self._lock: if port in self._occupied_ports and self._occupied_ports[port] == session_id: del self._occupied_ports[port] - traceroot_logger.info( + logger.info( f"Released browser on port {port} from session {session_id}. " f"Occupied: {list(self._occupied_ports.keys())}" ) else: - traceroot_logger.warning( + logger.warning( f"Attempted to release browser on port {port} but it was not occupied by {session_id}" ) @@ -284,7 +284,7 @@ def __init__( self.api_task_id = api_task_id self.agent_name = agent_name - # CDP management callbacks (set by search_agent) + # CDP management callbacks (set by browser_agent) self._cdp_acquire_callback = None # Called when cloning to acquire new CDP browser self._cdp_release_callback = None # Called when agent is destroyed to release CDP browser @@ -749,7 +749,7 @@ def clone(self, with_memory: bool = False) -> ChatAgent: if hasattr(self, 'process_task_id') and self.process_task_id: task_context = f"task_id={self.process_task_id}" - traceroot_logger.info( + logger.info( f"[CLONE START] Clone ID: {clone_id}, " f"Parent Agent: {self.agent_id}, " f"Agent Name: {self.agent_name}, " @@ -779,12 +779,12 @@ def clone(self, with_memory: bool = False) -> ChatAgent: if selected_browser: new_cdp_port = selected_browser.get('port', env('browser_port', '9222')) - traceroot_logger.info( + logger.info( f"[CLONE {clone_id}] Pre-acquired CDP browser port={new_cdp_port} for session={new_cdp_session}" ) else: new_cdp_port = cdp_browsers[0].get('port', env('browser_port', '9222')) - traceroot_logger.warning( + logger.warning( f"[CLONE {clone_id}] No available browsers, using first: port={new_cdp_port}" ) @@ -804,22 +804,22 @@ def clone(self, with_memory: bool = False) -> ChatAgent: toolkit._temp_original_cdp_url = original_cdp_url toolkit._temp_original_ws_config_cdp = original_ws_config_cdp - traceroot_logger.info( + logger.info( f"[CLONE {clone_id}] Temporarily set CDP URL to http://localhost:{new_cdp_port} for cloning " f"(parent config was {original_cdp_url}, parent ws_config was {original_ws_config_cdp})" ) else: - traceroot_logger.warning(f"[CLONE {clone_id}] No _browser_toolkit found on agent, CDP URL not modified") + logger.warning(f"[CLONE {clone_id}] No _browser_toolkit found on agent, CDP URL not modified") # Clone tools and collect toolkits that need registration - traceroot_logger.info(f"[CLONE {clone_id}] Calling _clone_tools()...") + logger.info(f"[CLONE {clone_id}] Calling _clone_tools()...") cloned_tools, toolkits_to_register = self._clone_tools() - traceroot_logger.info( + logger.info( f"[CLONE {clone_id}] _clone_tools returned {len(cloned_tools)} tools, " f"{len(toolkits_to_register)} toolkits to register" ) for idx, tk in enumerate(toolkits_to_register): - traceroot_logger.info( + logger.info( f"[CLONE {clone_id}] Toolkit {idx}: {tk.__class__.__name__}, " f"session={getattr(tk, '_session_id', 'N/A')}" ) @@ -834,7 +834,7 @@ def clone(self, with_memory: bool = False) -> ChatAgent: if toolkit._temp_original_ws_config_cdp and hasattr(toolkit, '_ws_config') and toolkit._ws_config: toolkit._ws_config['cdpUrl'] = toolkit._temp_original_ws_config_cdp delattr(toolkit, '_temp_original_ws_config_cdp') - traceroot_logger.info(f"[CLONE {clone_id}] Restored original CDP URL in parent toolkit") + logger.info(f"[CLONE {clone_id}] Restored original CDP URL in parent toolkit") new_agent = ListenChatAgent( api_task_id=self.api_task_id, @@ -871,22 +871,22 @@ def clone(self, with_memory: bool = False) -> ChatAgent: # Find and store the cloned browser toolkit on the new agent if toolkits_to_register: - traceroot_logger.info(f"[CLONE {clone_id}] toolkits_to_register has {len(toolkits_to_register)} items") + logger.info(f"[CLONE {clone_id}] toolkits_to_register has {len(toolkits_to_register)} items") for toolkit in toolkits_to_register: toolkit_class_name = toolkit.__class__.__name__ if hasattr(toolkit, '__class__') else 'UNKNOWN' - traceroot_logger.info(f"[CLONE {clone_id}] Checking toolkit: {toolkit_class_name}") + logger.info(f"[CLONE {clone_id}] Checking toolkit: {toolkit_class_name}") if hasattr(toolkit, '__class__') and toolkit.__class__.__name__ == 'HybridBrowserToolkit': new_agent._browser_toolkit = toolkit - traceroot_logger.info(f"[CLONE {clone_id}] Set _browser_toolkit to cloned HybridBrowserToolkit") + logger.info(f"[CLONE {clone_id}] Set _browser_toolkit to cloned HybridBrowserToolkit") break else: - traceroot_logger.warning(f"[CLONE {clone_id}] toolkits_to_register is empty!") + logger.warning(f"[CLONE {clone_id}] toolkits_to_register is empty!") # Set CDP info on cloned agent if we pre-acquired it if new_cdp_port is not None and new_cdp_session is not None: new_agent._cdp_port = new_cdp_port new_agent._cdp_session_id = new_cdp_session - traceroot_logger.info( + logger.info( f"[CLONE {clone_id}] Set CDP info on new agent {new_agent.agent_id}: " f"port={new_cdp_port}, session={new_cdp_session}" ) @@ -909,7 +909,7 @@ def clone(self, with_memory: bool = False) -> ChatAgent: for context_record in context_records: new_agent.memory.write_record(context_record.memory_record) - traceroot_logger.info( + logger.info( f"[CLONE COMPLETE] Clone ID: {clone_id}, " f"New Agent ID: {new_agent.agent_id}, " f"CDP Port: {new_cdp_port if new_cdp_port else 'N/A'}" @@ -1275,7 +1275,7 @@ def acquire_cdp_for_agent(agent): if selected_browser: selected_port = selected_browser.get('port', env('browser_port', '9222')) selected_is_external = selected_browser.get('isExternal', False) - traceroot_logger.info( + logger.info( f"Acquired CDP browser from pool for agent {agent.agent_id}: " f"port={selected_port}, isExternal={selected_is_external}, " f"name={selected_browser.get('name', 'Unnamed')}, session_id={session_id}" @@ -1284,7 +1284,7 @@ def acquire_cdp_for_agent(agent): # No available browsers in pool, fall back to first browser selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) selected_is_external = options.cdp_browsers[0].get('isExternal', False) - traceroot_logger.warning( + logger.warning( f"No available browsers in pool for agent {agent.agent_id}, " f"using first browser: port={selected_port}, session_id={session_id}" ) @@ -1292,7 +1292,7 @@ def acquire_cdp_for_agent(agent): # Use default port from environment selected_port = env('browser_port', '9222') selected_is_external = False - traceroot_logger.info( + logger.info( f"Using default CDP port for agent {agent.agent_id}: " f"{selected_port}, session_id={session_id}" ) @@ -1321,6 +1321,8 @@ def acquire_cdp_for_agent(agent): "browser_switch_tab", "browser_enter", "browser_visit_page", + "browser_sheet_read", + "browser_sheet_input", "browser_get_page_snapshot" ], ) @@ -1334,7 +1336,7 @@ def acquire_cdp_for_agent(agent): for i, toolkit in enumerate(agent._toolkits_to_register_agent): if hasattr(toolkit, '__class__') and toolkit.__class__.__name__ == 'HybridBrowserToolkit': agent._toolkits_to_register_agent[i] = new_toolkit - traceroot_logger.info( + logger.info( f"Replaced HybridBrowserToolkit for agent {agent.agent_id}: " f"new port={selected_port}, session_id={session_id}" ) @@ -1349,11 +1351,12 @@ def acquire_cdp_for_agent(agent): if not any(name in tool.get_function_name() for name in [ 'browser_open', 'browser_click', 'browser_type', 'browser_back', 'browser_forward', 'browser_switch_tab', 'browser_enter', - 'browser_visit_page', 'browser_get_page_snapshot' + 'browser_visit_page', 'browser_get_page_snapshot',"browser_sheet_read", + "browser_sheet_input", ]) ] agent._tools.extend(new_tools) - traceroot_logger.info(f"Updated agent {agent.agent_id} tools with new browser toolkit") + logger.info(f"Updated agent {agent.agent_id} tools with new browser toolkit") # Store CDP info on agent for cleanup agent._cdp_port = selected_port @@ -1366,7 +1369,7 @@ def release_cdp_from_agent(agent): port = agent._cdp_port session_id = agent._cdp_session_id _cdp_pool_manager.release_browser(port, session_id) - traceroot_logger.info( + logger.info( f"Released CDP browser for agent {agent.agent_id}: " f"port={port}, session_id={session_id}" ) @@ -1381,7 +1384,7 @@ def release_cdp_from_agent(agent): if selected_browser: selected_port = selected_browser.get('port', env('browser_port', '9222')) selected_is_external = selected_browser.get('isExternal', False) - traceroot_logger.info( + logger.info( f"Acquired CDP browser from pool (initial): port={selected_port}, " f"isExternal={selected_is_external}, " f"name={selected_browser.get('name', 'Unnamed')}, session_id={toolkit_session_id}" @@ -1389,14 +1392,14 @@ def release_cdp_from_agent(agent): else: selected_port = options.cdp_browsers[0].get('port', env('browser_port', '9222')) selected_is_external = options.cdp_browsers[0].get('isExternal', False) - traceroot_logger.warning( + logger.warning( f"No available browsers in pool (initial), using first browser: " f"port={selected_port}, session_id={toolkit_session_id}" ) else: selected_port = env('browser_port', '9222') selected_is_external = False - traceroot_logger.info(f"Using default CDP port (initial): {selected_port}, session_id={toolkit_session_id}") + logger.info(f"Using default CDP port (initial): {selected_port}, session_id={toolkit_session_id}") # IMPORTANT: Always use cdp_keep_current_page=True to preserve browser state # across tasks (both internal and external browsers) @@ -1589,12 +1592,12 @@ def cleanup_cdp_browser(): port = web_toolkit_custom._cdp_port session_id = web_toolkit_custom._cdp_session_id _cdp_pool_manager.release_browser(port, session_id) - traceroot_logger.info( + logger.info( f"Cleanup: Released CDP browser on port {port} for session {session_id}" ) agent = agent_model( - Agents.search_agent, + Agents.browser_agent, BaseMessage.make_assistant_message( role_name="Browser Agent", diff --git a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py index 49c12e8cb..f941ac7c2 100644 --- a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py @@ -317,6 +317,8 @@ def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: FunctionTool(browser.browser_visit_page), FunctionTool(browser.browser_scroll), FunctionTool(browser.browser_get_som_screenshot), + FunctionTool(browser.browser_sheet_read), + FunctionTool(browser.browser_sheet_input), # FunctionTool(browser.select), # FunctionTool(browser.wait_user), ] From 30fa9fc4cc2aa5e19eed0a017cd164e568e6dcc6 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Tue, 27 Jan 2026 13:18:23 +0800 Subject: [PATCH 28/40] Update toolkit_listen.py --- backend/app/utils/listen/toolkit_listen.py | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index 9c6f280a1..56defb034 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -33,6 +33,20 @@ logger = logging.getLogger("toolkit_listen") +def _filter_kwargs_for_callable(func: Callable[..., Any], kwargs: dict) -> dict: + """Drop unexpected kwargs unless the callable accepts **kwargs.""" + if not kwargs: + return kwargs + try: + sig = signature(func) + except (TypeError, ValueError): + return kwargs + if any(param.kind == param.VAR_KEYWORD for param in sig.parameters.values()): + return kwargs + allowed = set(sig.parameters.keys()) + return {k: v for k, v in kwargs.items() if k in allowed} + + def _safe_put_queue(task_lock, data): """Safely put data to the queue, handling both sync and async contexts""" try: @@ -154,7 +168,8 @@ async def async_wrapper(*args, **kwargs): error = None res = None try: - res = await func(*args, **kwargs) + safe_kwargs = _filter_kwargs_for_callable(func, kwargs) + res = await func(*args, **safe_kwargs) except Exception as e: error = e @@ -212,7 +227,8 @@ def sync_wrapper(*args, **kwargs): # Check if api_task_id exists if not hasattr(toolkit, 'api_task_id'): logger.warning(f"[listen_toolkit] {toolkit.__class__.__name__} missing api_task_id, calling method directly") - return func(*args, **kwargs) + safe_kwargs = _filter_kwargs_for_callable(func, kwargs) + return func(*args, **safe_kwargs) task_lock = get_task_lock(toolkit.api_task_id) @@ -260,7 +276,8 @@ def sync_wrapper(*args, **kwargs): error = None res = None try: - res = func(*args, **kwargs) + safe_kwargs = _filter_kwargs_for_callable(func, kwargs) + res = func(*args, **safe_kwargs) # Safety check: if the result is a coroutine, this is a programming error if asyncio.iscoroutine(res): error_msg = f"Async function {func.__name__} was incorrectly called in sync context. This is a bug - the function should be marked as async or should not return a coroutine." From 2d5f002ccc482cb539794f2f7ffcd67773f0584d Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Sun, 15 Feb 2026 23:39:48 +0000 Subject: [PATCH 29/40] update --- backend/app/agent/listen_chat_agent.py | 4 +--- backend/app/utils/listen/toolkit_listen.py | 8 ++++++-- backend/app/utils/single_agent_worker.py | 10 ++++++++-- backend/app/utils/telemetry/__init__.py | 1 - backend/app/utils/workforce.py | 4 +--- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 786d8a4d9..c3b7da7c6 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -737,9 +737,7 @@ def clone(self, with_memory: bool = False) -> ChatAgent: # Restore original CDP URL in parent toolkit if new_cdp_port is not None and hasattr(self, "_browser_toolkit"): - self._browser_toolkit.config_loader.get_browser_config().cdp_url = ( - original_cdp_url - ) + self._browser_toolkit.config_loader.get_browser_config().cdp_url = original_cdp_url new_agent = ListenChatAgent( api_task_id=self.api_task_id, diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index 8070c1048..1c2c9bbc6 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -171,7 +171,9 @@ def _log_deactivate( ) -def _filter_kwargs_for_callable(func: Callable[..., Any], kwargs: dict) -> dict: +def _filter_kwargs_for_callable( + func: Callable[..., Any], kwargs: dict +) -> dict: """Drop unexpected kwargs unless the callable accepts **kwargs.""" if not kwargs: return kwargs @@ -179,7 +181,9 @@ def _filter_kwargs_for_callable(func: Callable[..., Any], kwargs: dict) -> dict: sig = signature(func) except (TypeError, ValueError): return kwargs - if any(param.kind == param.VAR_KEYWORD for param in sig.parameters.values()): + if any( + param.kind == param.VAR_KEYWORD for param in sig.parameters.values() + ): return kwargs allowed = set(sig.parameters.keys()) return {k: v for k, v in kwargs.items() if k in allowed} diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index 77fffc1b1..6b7de2e40 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -87,8 +87,14 @@ async def _process_task( `TaskState.FAILED`. """ # Log task details before getting agent (for clone tracking) - task_content_preview = task.content[:100] + "..." if len(task.content) > 100 else task.content - logger.info(f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'") + task_content_preview = ( + task.content[:100] + "..." + if len(task.content) > 100 + else task.content + ) + logger.info( + f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'" + ) # Get agent efficiently (from pool or by cloning) worker_agent = await self._get_worker_agent() diff --git a/backend/app/utils/telemetry/__init__.py b/backend/app/utils/telemetry/__init__.py index 3a4d90c0e..fa7455a0c 100644 --- a/backend/app/utils/telemetry/__init__.py +++ b/backend/app/utils/telemetry/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index d50c60c4f..b4bf6764c 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -952,9 +952,7 @@ def _cleanup_all_agents(self) -> None: # Cleanup agents in AgentPool if hasattr(child, "agent_pool") and child.agent_pool: pool = child.agent_pool - for agent in list( - getattr(pool, "_available_agents", []) - ): + for agent in list(getattr(pool, "_available_agents", [])): cb = getattr(agent, "_cdp_release_callback", None) if callable(cb): try: From bd161a720460c55d79b1e713eb3eb39754681aaf Mon Sep 17 00:00:00 2001 From: Tao Sun <168447269+fengju0213@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:02:14 +0800 Subject: [PATCH 30/40] enhance: enhance cdp (#1094) --- backend/app/agent/factory/browser.py | 52 ++++++++++++++++++++++++-- backend/app/agent/listen_chat_agent.py | 21 ++++++++--- backend/app/model/chat.py | 2 +- backend/app/utils/workforce.py | 27 ++++++++++++- 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 414d1103c..2f3dc1f54 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -42,16 +42,22 @@ class CdpBrowserPoolManager: def __init__(self): self._occupied_ports: dict[int, str] = {} + self._session_to_port: dict[str, int] = {} + self._session_to_task: dict[str, str | None] = {} self._lock = threading.Lock() def acquire_browser( - self, cdp_browsers: list[dict], session_id: str + self, + cdp_browsers: list[dict], + session_id: str, + task_id: str | None = None, ) -> dict | None: """Acquire an available browser from the pool. Args: cdp_browsers: List of browser configurations. session_id: Unique session identifier. + task_id: Optional task identifier for ownership tracking. Returns: Browser configuration dict or None if all occupied. @@ -61,6 +67,8 @@ def acquire_browser( port = browser.get("port") if port and port not in self._occupied_ports: self._occupied_ports[port] = session_id + self._session_to_port[session_id] = port + self._session_to_task[session_id] = task_id logger.info( f"Acquired browser on port {port} for session " f"{session_id}. Occupied: " @@ -81,11 +89,48 @@ def release_browser(self, port: int, session_id: str): and self._occupied_ports[port] == session_id ): del self._occupied_ports[port] + self._session_to_port.pop(session_id, None) + self._session_to_task.pop(session_id, None) logger.info( f"Released browser on port {port} from session " f"{session_id}. Occupied: " f"{list(self._occupied_ports.keys())}" ) + else: + logger.warning( + f"Attempted to release browser on port {port} " + f"but it was not occupied by {session_id}" + ) + + def release_by_task(self, task_id: str) -> list[int]: + """Release all browsers associated with a task_id. + + Returns: + List of released ports. + """ + released_ports = [] + with self._lock: + sessions = [ + s for s, t in self._session_to_task.items() + if t == task_id + ] + for session_id in sessions: + port = self._session_to_port.get(session_id) + if ( + port is not None + and self._occupied_ports.get(port) == session_id + ): + del self._occupied_ports[port] + released_ports.append(port) + self._session_to_port.pop(session_id, None) + self._session_to_task.pop(session_id, None) + if released_ports: + logger.info( + f"Released {len(released_ports)} browser(s) for " + f"task {task_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + return released_ports def get_occupied_ports(self) -> list[int]: """Get list of currently occupied ports.""" @@ -116,7 +161,7 @@ def browser_agent(options: Chat): if options.cdp_browsers: selected_browser = _cdp_pool_manager.acquire_browser( - options.cdp_browsers, toolkit_session_id + options.cdp_browsers, toolkit_session_id, options.task_id ) if selected_browser: selected_port = selected_browser.get( @@ -256,7 +301,7 @@ def acquire_cdp_for_agent(agent_instance): return session_id = str(uuid.uuid4())[:8] selected = _cdp_pool_manager.acquire_browser( - options.cdp_browsers, session_id + options.cdp_browsers, session_id, options.task_id ) if selected: agent_instance._cdp_port = selected.get( @@ -287,6 +332,7 @@ def release_cdp_from_agent(agent_instance): agent._cdp_release_callback = release_cdp_from_agent agent._cdp_port = selected_port agent._cdp_session_id = toolkit_session_id + agent._cdp_task_id = options.task_id agent._cdp_options = options agent._browser_toolkit = web_toolkit_for_agent_registration diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index c3b7da7c6..031e11755 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -710,7 +710,9 @@ def clone(self, with_memory: bool = False) -> ChatAgent: new_cdp_session = str(_uuid.uuid4())[:8] selected = _cdp_pool_manager.acquire_browser( - cdp_browsers, new_cdp_session + cdp_browsers, + new_cdp_session, + getattr(self, "_cdp_task_id", None), ) from app.component.environment import env @@ -733,11 +735,16 @@ def clone(self, with_memory: bool = False) -> ChatAgent: ) # Clone tools and collect toolkits that need registration - cloned_tools, toolkits_to_register = self._clone_tools() - - # Restore original CDP URL in parent toolkit - if new_cdp_port is not None and hasattr(self, "_browser_toolkit"): - self._browser_toolkit.config_loader.get_browser_config().cdp_url = original_cdp_url + try: + cloned_tools, toolkits_to_register = self._clone_tools() + except Exception: + if new_cdp_port is not None and new_cdp_session is not None: + _cdp_pool_manager.release_browser(new_cdp_port, new_cdp_session) + raise + finally: + # Restore original CDP URL in parent toolkit + if new_cdp_port is not None and hasattr(self, "_browser_toolkit"): + self._browser_toolkit.config_loader.get_browser_config().cdp_url = original_cdp_url new_agent = ListenChatAgent( api_task_id=self.api_task_id, @@ -776,6 +783,8 @@ def clone(self, with_memory: bool = False) -> ChatAgent: new_agent._cdp_release_callback = self._cdp_release_callback if hasattr(self, "_cdp_options"): new_agent._cdp_options = self._cdp_options + if hasattr(self, "_cdp_task_id"): + new_agent._cdp_task_id = self._cdp_task_id # Find and store the cloned browser toolkit on the new agent for tk in toolkits_to_register: diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index 420f4ea75..eb647ac5c 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -64,7 +64,7 @@ class Chat(BaseModel): language: str = "en" browser_port: int = 9222 use_external_cdp: bool = False - cdp_browsers: list[dict] = [] + cdp_browsers: list[dict] = Field(default_factory=list) max_retries: int = 3 allow_local_system: bool = False installed_mcp: McpServers = {"mcpServers": {}} diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index b4bf6764c..1d925a41e 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -968,7 +968,32 @@ def _cleanup_all_agents(self) -> None: try: from app.agent.factory.browser import _cdp_pool_manager - _cdp_pool_manager._occupied_ports.clear() + task_ids = set() + if hasattr(self, '_children') and self._children: + for child in self._children: + if hasattr(child, 'worker_agent') and hasattr(child.worker_agent, '_cdp_task_id'): + task_ids.add(child.worker_agent._cdp_task_id) + if hasattr(child, 'agent_pool') and child.agent_pool: + for agent in list(child.agent_pool._available_agents): + if hasattr(agent, '_cdp_task_id'): + task_ids.add(agent._cdp_task_id) + if hasattr(self, 'coordinator_agent') and self.coordinator_agent and hasattr(self.coordinator_agent, '_cdp_task_id'): + task_ids.add(self.coordinator_agent._cdp_task_id) + + if not task_ids: + logger.warning("[WF-CLEANUP] No task_id found for CDP release; skipping pool cleanup") + return + + logger.info(f"[WF-CLEANUP] Force releasing CDP resources for task_ids: {sorted(task_ids)}") + occupied_before = _cdp_pool_manager.get_occupied_ports().copy() + logger.info(f"[WF-CLEANUP] CDP ports occupied before force release: {occupied_before}") + + released_ports = [] + for task_id in task_ids: + released_ports.extend(_cdp_pool_manager.release_by_task(task_id)) + + logger.info(f"[WF-CLEANUP] ✅ Force released {len(released_ports)} CDP browser(s)") + logger.info(f"[WF-CLEANUP] CDP ports after force release: {_cdp_pool_manager.get_occupied_ports()}") except Exception as e: logger.error(f"[WF-CLEANUP] Error clearing CDP pool: {e}") From be695fcb1398cbdaa02fc718d27e86b08ef2708f Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 17 Feb 2026 00:41:14 +0000 Subject: [PATCH 31/40] optimize code --- backend/app/agent/factory/browser.py | 28 +- backend/app/agent/listen_chat_agent.py | 47 +- .../agent/toolkit/hybrid_browser_toolkit.py | 2 +- backend/app/utils/single_agent_worker.py | 2 +- backend/app/utils/workforce.py | 2 +- electron/main/index.ts | 150 +- src/pages/Dashboard/Browser.tsx | 1596 +++++++++-------- 7 files changed, 915 insertions(+), 912 deletions(-) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 414d1103c..e7aebf93a 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -36,6 +36,11 @@ from app.utils.file_utils import get_working_directory +def _get_browser_port(browser: dict) -> int: + """Extract port from a browser config dict, with fallback to env default.""" + return int(browser.get("port", env("browser_port", "9222"))) + + class CdpBrowserPoolManager: """Manages CDP browser pool occupation to ensure parallel tasks use different browsers.""" @@ -87,6 +92,13 @@ def release_browser(self, port: int, session_id: str): f"{list(self._occupied_ports.keys())}" ) + def clear_all(self): + """Force-clear all occupied ports (safety net for task cleanup).""" + with self._lock: + count = len(self._occupied_ports) + self._occupied_ports.clear() + return count + def get_occupied_ports(self) -> list[int]: """Get list of currently occupied ports.""" with self._lock: @@ -119,9 +131,7 @@ def browser_agent(options: Chat): options.cdp_browsers, toolkit_session_id ) if selected_browser: - selected_port = selected_browser.get( - "port", env("browser_port", "9222") - ) + selected_port = _get_browser_port(selected_browser) selected_is_external = selected_browser.get("isExternal", False) logger.info( f"Acquired CDP browser from pool (initial): " @@ -129,9 +139,7 @@ def browser_agent(options: Chat): f"session_id={toolkit_session_id}" ) else: - selected_port = options.cdp_browsers[0].get( - "port", env("browser_port", "9222") - ) + selected_port = _get_browser_port(options.cdp_browsers[0]) selected_is_external = options.cdp_browsers[0].get( "isExternal", False ) @@ -259,12 +267,10 @@ def acquire_cdp_for_agent(agent_instance): options.cdp_browsers, session_id ) if selected: - agent_instance._cdp_port = selected.get( - "port", env("browser_port", "9222") - ) + agent_instance._cdp_port = _get_browser_port(selected) else: - agent_instance._cdp_port = options.cdp_browsers[0].get( - "port", env("browser_port", "9222") + agent_instance._cdp_port = _get_browser_port( + options.cdp_browsers[0] ) agent_instance._cdp_session_id = session_id logger.info( diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index c3b7da7c6..e8da8e0bc 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -15,6 +15,7 @@ import asyncio import json import logging +import threading from collections.abc import Callable from threading import Event from typing import Any @@ -52,6 +53,8 @@ class ListenChatAgent(ChatAgent): + _cdp_clone_lock = threading.Lock() # Protects CDP URL mutation during clone + def __init__( self, api_task_id: str, @@ -700,10 +703,12 @@ def clone(self, with_memory: bool = False) -> ChatAgent: getattr(self, "_cdp_acquire_callback", None) ) + need_cdp_clone = False if has_cdp and hasattr(self, "_cdp_options"): options = self._cdp_options cdp_browsers = getattr(options, "cdp_browsers", []) if cdp_browsers and hasattr(self, "_browser_toolkit"): + need_cdp_clone = True import uuid as _uuid from app.agent.factory.browser import _cdp_pool_manager @@ -712,32 +717,40 @@ def clone(self, with_memory: bool = False) -> ChatAgent: selected = _cdp_pool_manager.acquire_browser( cdp_browsers, new_cdp_session ) - from app.component.environment import env + from app.agent.factory.browser import _get_browser_port if selected: - new_cdp_port = selected.get( - "port", env("browser_port", "9222") - ) + new_cdp_port = _get_browser_port(selected) else: - new_cdp_port = cdp_browsers[0].get( - "port", env("browser_port", "9222") - ) - - # Temporarily override the browser toolkit's CDP URL - toolkit = self._browser_toolkit + new_cdp_port = _get_browser_port(cdp_browsers[0]) + + if need_cdp_clone: + # Temporarily override the browser toolkit's CDP URL. + # Lock prevents concurrent clones from clobbering each + # other's cdp_url on the shared parent toolkit. + toolkit = self._browser_toolkit + with ListenChatAgent._cdp_clone_lock: original_cdp_url = ( toolkit.config_loader.get_browser_config().cdp_url ) toolkit.config_loader.get_browser_config().cdp_url = ( f"http://localhost:{new_cdp_port}" ) - - # Clone tools and collect toolkits that need registration - cloned_tools, toolkits_to_register = self._clone_tools() - - # Restore original CDP URL in parent toolkit - if new_cdp_port is not None and hasattr(self, "_browser_toolkit"): - self._browser_toolkit.config_loader.get_browser_config().cdp_url = original_cdp_url + try: + cloned_tools, toolkits_to_register = ( + self._clone_tools() + ) + except Exception: + _cdp_pool_manager.release_browser( + new_cdp_port, new_cdp_session + ) + raise + finally: + toolkit.config_loader.get_browser_config().cdp_url = ( + original_cdp_url + ) + else: + cloned_tools, toolkits_to_register = self._clone_tools() new_agent = ListenChatAgent( api_task_id=self.api_task_id, diff --git a/backend/app/agent/toolkit/hybrid_browser_toolkit.py b/backend/app/agent/toolkit/hybrid_browser_toolkit.py index 0b510cec2..988893d91 100644 --- a/backend/app/agent/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/agent/toolkit/hybrid_browser_toolkit.py @@ -456,7 +456,7 @@ def __init__( page_stability_timeout: int | None = None, dom_content_loaded_timeout: int | None = None, viewport_limit: bool = False, - connect_over_cdp: bool = True, + connect_over_cdp: bool = True, # Deprecated: auto-set to True when cdp_url is provided, kept for compatibility cdp_url: str | None = "http://localhost:9222", cdp_keep_current_page: bool = False, full_visual_mode: bool = False, diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index 6b7de2e40..dd96b832b 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -92,7 +92,7 @@ async def _process_task( if len(task.content) > 100 else task.content ) - logger.info( + logger.debug( f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'" ) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index b4bf6764c..251b6b527 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -968,7 +968,7 @@ def _cleanup_all_agents(self) -> None: try: from app.agent.factory.browser import _cdp_pool_manager - _cdp_pool_manager._occupied_ports.clear() + _cdp_pool_manager.clear_all() except Exception as e: logger.error(f"[WF-CLEANUP] Error clearing CDP pool: {e}") diff --git a/electron/main/index.ts b/electron/main/index.ts index abca22fd7..a757c3ad4 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -100,6 +100,19 @@ let cdp_browser_pool: CdpBrowser[] = []; let cdp_browser_processes: Map = new Map(); +/** Remove a non-external browser from the pool by port (used on process error/exit). */ +function removeFromPoolByPort(port: number, reason: string): void { + const idx = cdp_browser_pool.findIndex( + (b) => b.port === port && !b.isExternal + ); + if (idx !== -1) { + const removed = cdp_browser_pool.splice(idx, 1)[0]; + log.warn( + `[CDP POOL] Auto-removed port=${port} (${reason}), id=${removed.id}, pool_size=${cdp_browser_pool.length}` + ); + } +} + // Protocol URL queue for handling URLs before window is ready let protocolUrlQueue: string[] = []; let isWindowReady = false; @@ -424,56 +437,25 @@ function registerIpcHandlers() { // Get all browsers in the pool ipcMain.handle('get-cdp-browsers', () => { - log.info(`[CDP POOL GET] ========================================`); - log.info( - `[CDP POOL GET] Getting CDP browser pool at ${new Date().toISOString()}` - ); - log.info(`[CDP POOL GET] Pool size: ${cdp_browser_pool.length}`); - - if (cdp_browser_pool.length > 0) { - cdp_browser_pool.forEach((b, idx) => { - log.info( - `[CDP POOL GET] Browser ${idx + 1}: port=${b.port}, isExternal=${b.isExternal}, name="${b.name}", id=${b.id}` - ); - }); - } else { - log.warn(`[CDP POOL GET] ⚠️ Pool is EMPTY - no browsers configured`); - } - - log.info(`[CDP POOL GET] ========================================`); + log.debug(`[CDP POOL] GET pool (size=${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; + return Array.from(cdp_browser_processes.keys()); }); // Add browser to pool ipcMain.handle( 'add-cdp-browser', (event, port: number, isExternal: boolean, name?: string) => { - log.info(`[CDP POOL ADD] ========================================`); - log.info( - `[CDP POOL ADD] Request to add browser at ${new Date().toISOString()}` - ); - log.info(`[CDP POOL ADD] Port: ${port}`); - log.info(`[CDP POOL ADD] Is External: ${isExternal}`); - log.info(`[CDP POOL ADD] Name: "${name}"`); - log.info(`[CDP POOL ADD] Current pool size: ${cdp_browser_pool.length}`); - // Check if browser with this port already exists const existing = cdp_browser_pool.find((b) => b.port === port); if (existing) { log.warn( - `[CDP POOL ADD] ❌ REJECTED - Browser with port ${port} already exists in pool` + `[CDP POOL] ADD rejected: port ${port} already exists (id=${existing.id})` ); - log.warn( - `[CDP POOL ADD] Existing browser: id=${existing.id}, name="${existing.name}"` - ); - log.info(`[CDP POOL ADD] ========================================`); return { success: false, error: 'Browser with this port already exists', @@ -489,13 +471,9 @@ function registerIpcHandlers() { }; cdp_browser_pool.push(newBrowser); - log.info(`[CDP POOL ADD] ✅ SUCCESS - Browser added to pool`); - log.info(`[CDP POOL ADD] Browser ID: ${newBrowser.id}`); - log.info(`[CDP POOL ADD] New pool size: ${cdp_browser_pool.length}`); log.info( - `[CDP POOL ADD] All ports in pool: [${cdp_browser_pool.map((b) => b.port).join(', ')}]` + `[CDP POOL] ADD: port=${port}, isExternal=${isExternal}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` ); - log.info(`[CDP POOL ADD] ========================================`); return { success: true, browser: newBrowser }; } @@ -503,43 +481,30 @@ function registerIpcHandlers() { // Remove browser from pool ipcMain.handle('remove-cdp-browser', (event, browserId: string) => { - log.info(`[CDP POOL REMOVE] ========================================`); - log.info(`[CDP POOL REMOVE] Request to remove browser: ${browserId}`); - const index = cdp_browser_pool.findIndex((b) => b.id === browserId); if (index === -1) { - log.warn(`[CDP POOL REMOVE] ❌ Browser not found: ${browserId}`); - log.info(`[CDP POOL REMOVE] ========================================`); + log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`); return { success: false, error: 'Browser not found' }; } const removed = cdp_browser_pool.splice(index, 1)[0]; - log.info( - `[CDP POOL REMOVE] Removed browser: port=${removed.port}, name="${removed.name}"` - ); // If it's a launched browser, kill the process if (!removed.isExternal && cdp_browser_processes.has(removed.port)) { - log.info( - `[CDP POOL REMOVE] Killing launched browser process on port ${removed.port}` - ); try { const process = cdp_browser_processes.get(removed.port); process?.kill(); cdp_browser_processes.delete(removed.port); - log.info(`[CDP POOL REMOVE] Browser process killed successfully`); } catch (error) { log.warn( - `[CDP POOL REMOVE] Failed to kill browser process on port ${removed.port}: ${error}` + `[CDP POOL] Failed to kill browser process on port ${removed.port}: ${error}` ); } } log.info( - `[CDP POOL REMOVE] ✅ SUCCESS - Remaining pool size: ${cdp_browser_pool.length}` + `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, pool_size=${cdp_browser_pool.length}` ); - log.info(`[CDP POOL REMOVE] ========================================`); - return { success: true, browser: removed }; }); @@ -595,11 +560,7 @@ function registerIpcHandlers() { // Launch CDP browser with custom port ipcMain.handle('launch-cdp-browser', async (event, port: number) => { - log.info(`[CDP LAUNCH] ========================================`); - log.info( - `[CDP LAUNCH] Request to launch browser at ${new Date().toISOString()}` - ); - log.info(`[CDP LAUNCH] Target port: ${port}`); + log.info(`[CDP LAUNCH] Launching browser on port ${port}`); try { const platform = process.platform; @@ -782,10 +743,7 @@ function registerIpcHandlers() { // Check if browser on this port is already running if (cdp_browser_processes.has(port)) { - log.warn( - `[CDP LAUNCH] ❌ Browser process already exists on port ${port}` - ); - log.info(`[CDP LAUNCH] ========================================`); + log.warn(`[CDP LAUNCH] Browser process already exists on port ${port}`); return { success: false, error: `Browser already running on port ${port}`, @@ -802,9 +760,7 @@ function registerIpcHandlers() { 'about:blank', ]; - log.info(`[CDP LAUNCH] Spawning Chrome process...`); - log.info(`[CDP LAUNCH] Executable: ${chromeExecutable}`); - log.info(`[CDP LAUNCH] Args: ${args.join(' ')}`); + log.info(`[CDP LAUNCH] Spawning: ${chromeExecutable} on port ${port}`); // Spawn Chrome process const browserProcess = spawn(chromeExecutable, args, { @@ -817,21 +773,7 @@ function registerIpcHandlers() { `[CDP LAUNCH] Browser process error on port ${port}: ${error}` ); cdp_browser_processes.delete(port); - - // Also remove from pool if it was added - const browserInPool = cdp_browser_pool.find( - (b) => b.port === port && !b.isExternal - ); - if (browserInPool) { - const index = cdp_browser_pool.indexOf(browserInPool); - cdp_browser_pool.splice(index, 1); - log.warn( - `[CDP POOL AUTO-REMOVE] Browser on port ${port} removed from pool due to process error` - ); - log.info( - `[CDP POOL AUTO-REMOVE] New pool size: ${cdp_browser_pool.length}` - ); - } + removeFromPoolByPort(port, 'process error'); }); browserProcess.on('exit', (code) => { @@ -839,30 +781,7 @@ function registerIpcHandlers() { `[CDP LAUNCH] Browser process on port ${port} exited with code ${code}` ); cdp_browser_processes.delete(port); - - // Also remove from pool if it was added - const browserInPool = cdp_browser_pool.find( - (b) => b.port === port && !b.isExternal - ); - if (browserInPool) { - const index = cdp_browser_pool.indexOf(browserInPool); - cdp_browser_pool.splice(index, 1); - log.warn( - `[CDP POOL AUTO-REMOVE] Browser on port ${port} removed from pool due to process exit` - ); - log.info(`[CDP POOL AUTO-REMOVE] Exited with code: ${code}`); - log.info( - `[CDP POOL AUTO-REMOVE] Browser ID: ${browserInPool.id}, Name: "${browserInPool.name}"` - ); - log.info( - `[CDP POOL AUTO-REMOVE] New pool size: ${cdp_browser_pool.length}` - ); - if (cdp_browser_pool.length > 0) { - log.info( - `[CDP POOL AUTO-REMOVE] Remaining ports: [${cdp_browser_pool.map((b) => b.port).join(', ')}]` - ); - } - } + removeFromPoolByPort(port, `exit code ${code}`); }); // Store the process in the Map @@ -905,10 +824,6 @@ function registerIpcHandlers() { log.info( `[CDP LAUNCH] ⚠️ NOTE: Browser launched but NOT added to pool yet` ); - log.info( - `[CDP LAUNCH] ⚠️ The UI must call 'add-cdp-browser' to add it to the pool` - ); - log.info(`[CDP LAUNCH] ========================================`); // This is our own launched browser, not external use_external_cdp = false; return { @@ -932,21 +847,22 @@ function registerIpcHandlers() { } // If we get here, browser didn't respond within max wait time + // Kill the orphaned process to avoid resource leak + const proc = cdp_browser_processes.get(port); + if (proc) { + proc.kill(); + cdp_browser_processes.delete(port); + } const totalTime = Date.now() - startTime; log.warn( - `[CDP LAUNCH] ❌ Verification failed after ${totalTime}ms (${attempt} attempts)` - ); - log.warn( - `[CDP LAUNCH] Last error: ${lastError?.code || lastError?.message || 'Unknown'}` + `[CDP LAUNCH] Verification failed after ${totalTime}ms (${attempt} attempts), last error: ${lastError?.code || lastError?.message || 'Unknown'}` ); - log.info(`[CDP LAUNCH] ========================================`); return { success: false, error: `Browser launched but not responding on CDP port after ${totalTime}ms`, }; } catch (error: any) { - log.error(`[CDP LAUNCH] ❌ FAILED to launch browser: ${error}`); - log.info(`[CDP LAUNCH] ========================================`); + log.error(`[CDP LAUNCH] Failed to launch browser: ${error}`); return { success: false, error: error.message, diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index c71e3d189..8507d7569 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -1,781 +1,849 @@ -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Globe, Cookie, Trash2, RefreshCw, RotateCw, Plus, EllipsisVertical, CheckCircle2, XCircle, Loader2 } from "lucide-react"; -import { fetchPost, fetchGet, fetchDelete } from "@/api/http"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; -import AlertDialog from "@/components/ui/alertDialog"; -import { Input } from "@/components/ui/input"; +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { fetchDelete, fetchGet, fetchPost } from '@/api/http'; +import AlertDialog from '@/components/ui/alertDialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + CheckCircle2, + Cookie, + Globe, + Loader2, + Plus, + RefreshCw, + Trash2, + XCircle, +} from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; interface CookieDomain { - domain: string; - cookie_count: number; - last_access: string; + domain: string; + cookie_count: number; + last_access: string; } interface GroupedDomain { - mainDomain: string; - subdomains: CookieDomain[]; - totalCookies: number; + mainDomain: string; + subdomains: CookieDomain[]; + totalCookies: number; } interface CdpPortStatus { - checking: boolean; - available: boolean | null; - error?: string; - data?: any; + checking: boolean; + available: boolean | null; + error?: string; + data?: any; } interface CdpBrowser { - id: string; - port: number; - isExternal: boolean; - name?: string; - addedAt: number; + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; } export default function Browser() { - const { t } = useTranslation(); - const [loginLoading, setLoginLoading] = useState(false); - const [cookiesLoading, setCookiesLoading] = useState(false); - const [cookieDomains, setCookieDomains] = useState([]); - const [deletingDomain, setDeletingDomain] = useState(null); - const [deletingAll, setDeletingAll] = useState(false); - const [showRestartDialog, setShowRestartDialog] = useState(false); - const [cookiesBeforeBrowser, setCookiesBeforeBrowser] = useState(0); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - - // CDP port configuration - const [cdpPort, setCdpPort] = useState(9223); - const [customPort, setCustomPort] = useState("9223"); - const [portStatus, setPortStatus] = useState({ - checking: false, - available: null, - }); - - // Dialog states - const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); - 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 - const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain; - const parts = cleanDomain.split('.'); - - // For domains with 2 or fewer parts, return as is - if (parts.length <= 2) { - return cleanDomain; - } - - // For domains with more parts, return last 2 parts (main domain) - return parts.slice(-2).join('.'); - }; - - // Group domains by main domain - const groupDomainsByMain = (domains: CookieDomain[]): GroupedDomain[] => { - const grouped = new Map(); - - domains.forEach(item => { - const mainDomain = getMainDomain(item.domain); - if (!grouped.has(mainDomain)) { - grouped.set(mainDomain, []); - } - grouped.get(mainDomain)!.push(item); - }); - - return Array.from(grouped.entries()).map(([mainDomain, subdomains]) => ({ - mainDomain, - subdomains, - totalCookies: subdomains.reduce((sum, item) => sum + item.cookie_count, 0) - })).sort((a, b) => a.mainDomain.localeCompare(b.mainDomain)); - }; - - // Auto-load cookies on component mount - useEffect(() => { - handleLoadCookies(); - // Load current browser port on mount - loadCurrentBrowserPort(); - // Load CDP browser pool - loadCdpBrowsers(); - }, []); - - const loadCurrentBrowserPort = async () => { - if (window.ipcRenderer) { - const port = await window.ipcRenderer.invoke('get-browser-port'); - setCdpPort(port); - setCustomPort(String(port)); - } - }; - - const loadCdpBrowsers = async () => { - if (window.electronAPI?.getCdpBrowsers) { - try { - console.log('[FRONTEND CDP LOAD] Loading CDP browser pool...'); - const browsers = await window.electronAPI.getCdpBrowsers(); - console.log('[FRONTEND CDP LOAD] Loaded browsers:', browsers); - console.log(`[FRONTEND CDP LOAD] Pool size: ${browsers.length}`); - setCdpBrowsers(browsers); - - // Also load running browser ports - if (window.electronAPI?.getRunningBrowserPorts) { - const ports = await window.electronAPI.getRunningBrowserPorts(); - console.log('[FRONTEND CDP LOAD] Running browser ports:', ports); - setRunningPorts(ports); - } - } catch (error) { - console.error("[FRONTEND CDP LOAD] 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); - - if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { - toast.error("Please enter a valid port number (1-65535)"); - return; - } - - setPortStatus({ checking: true, available: null }); - - try { - if (!window.electronAPI?.checkCdpPort) { - toast.error("CDP port check not available"); - setPortStatus({ checking: false, available: false, error: "Not available" }); - return; - } - - const result = await window.electronAPI.checkCdpPort(portNumber); - - if (result.available) { - setPortStatus({ - checking: false, - available: true, - data: result.data, - }); - // Browser exists, ask if user wants to use it - setPendingPort(portNumber); - setShowUseExistingDialog(true); - } else { - setPortStatus({ - checking: false, - available: false, - error: result.error, - }); - // No browser on this port, ask if user wants to launch one - setPendingPort(portNumber); - setShowLaunchNewDialog(true); - } - } catch (error: any) { - setPortStatus({ - checking: false, - available: false, - error: error.message, - }); - toast.error(error.message || "Failed to check port"); - } - }; - - const handleUseExistingBrowser = async () => { - setShowUseExistingDialog(false); - if (pendingPort) { - try { - console.log(`[FRONTEND CDP ADD] Attempting to add external browser on port ${pendingPort}`); - // Add browser to pool - if (window.electronAPI?.addCdpBrowser) { - const result = await window.electronAPI.addCdpBrowser(pendingPort, true, `External Browser (${pendingPort})`); - console.log(`[FRONTEND CDP ADD] Result:`, result); - if (result.success) { - console.log(`[FRONTEND CDP ADD] ✅ Successfully added browser ${result.browser.id} on port ${pendingPort}`); - toast.success(`Added external browser on port ${pendingPort} to pool`); - await loadCdpBrowsers(); - } else { - console.error(`[FRONTEND CDP ADD] ❌ Failed to add browser:`, result.error); - toast.error(result.error || "Failed to add browser to pool"); - } - } - } catch (error: any) { - console.error(`[FRONTEND CDP ADD] ❌ Exception:`, error); - toast.error(error.message || "Failed to add browser to pool"); - } - } - setPendingPort(null); - }; - - const handleLaunchNewBrowser = async () => { - setShowLaunchNewDialog(false); - - if (!pendingPort) { - return; - } - - const port = pendingPort; - setPendingPort(null); - - try { - if (!window.electronAPI?.launchCdpBrowser) { - toast.error("Launch CDP browser not available"); - return; - } - - console.log(`[FRONTEND CDP LAUNCH] Launching browser on port ${port}...`); - toast.loading(`Launching browser on port ${port}...`, { id: 'launch-browser' }); - - const result = await window.electronAPI.launchCdpBrowser(port); - console.log(`[FRONTEND CDP LAUNCH] Launch result:`, result); - - if (result.success) { - console.log(`[FRONTEND CDP LAUNCH] ✅ Browser launched successfully on port ${port}`); - toast.success(`Browser launched successfully on port ${port}`, { id: 'launch-browser' }); - - // Add launched browser to pool - console.log(`[FRONTEND CDP LAUNCH] Adding launched browser to pool...`); - if (window.electronAPI?.addCdpBrowser) { - const addResult = await window.electronAPI.addCdpBrowser(port, false, `Launched Browser (${port})`); - console.log(`[FRONTEND CDP LAUNCH] Add to pool result:`, addResult); - if (addResult.success) { - console.log(`[FRONTEND CDP LAUNCH] ✅ Browser added to pool: ${addResult.browser.id}`); - await loadCdpBrowsers(); - } else { - console.error(`[FRONTEND CDP LAUNCH] ❌ Failed to add to pool:`, addResult.error); - toast.error(addResult.error || "Failed to add browser to pool"); - } - } - - // Update port status - setPortStatus({ - checking: false, - available: true, - data: result.data, - }); - } else { - console.error(`[FRONTEND CDP LAUNCH] ❌ Launch failed:`, result.error); - toast.error(result.error || "Failed to launch browser", { id: 'launch-browser' }); - } - } catch (error: any) { - console.error(`[FRONTEND CDP LAUNCH] ❌ Exception:`, error); - toast.error(error.message || "Failed to launch browser", { id: 'launch-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 { - // Record current cookie count before opening browser - const currentCookieCount = cookieDomains.reduce((sum, item) => sum + item.cookie_count, 0); - setCookiesBeforeBrowser(currentCookieCount); - - const response = await fetchPost("/browser/login"); - if (response) { - toast.success("Browser opened successfully for login"); - // Listen for browser close event to reload cookies - const checkInterval = setInterval(async () => { - try { - // Check if browser is still open by making a request - // When browser closes, reload cookies - const statusResponse = await fetchGet("/browser/status"); - if (!statusResponse || !statusResponse.is_open) { - clearInterval(checkInterval); - await handleLoadCookies(); - // Check if cookies changed - const newResponse = await fetchGet("/browser/cookies"); - if (newResponse && newResponse.success) { - const newDomains = newResponse.domains || []; - const newCookieCount = newDomains.reduce((sum: number, item: CookieDomain) => sum + item.cookie_count, 0); - - if (newCookieCount > currentCookieCount) { - // Cookies were added, show success toast and restart dialog - const addedCount = newCookieCount - currentCookieCount; - toast.success(`Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}`); - setHasUnsavedChanges(true); - setShowRestartDialog(true); - } else if (newCookieCount < currentCookieCount) { - // Cookies were deleted (shouldn't happen here, but handle it) - setHasUnsavedChanges(true); - setShowRestartDialog(true); - } - } - } - } catch (error) { - // Browser might be closed - clearInterval(checkInterval); - await handleLoadCookies(); - } - }, 500); // Check every 2 seconds - } - } catch (error: any) { - toast.error(error?.message || "Failed to open browser"); - } finally { - setLoginLoading(false); - } - }; - - const handleLoadCookies = async () => { - setCookiesLoading(true); - try { - const response = await fetchGet("/browser/cookies"); - if (response && response.success) { - const domains = response.domains || []; - setCookieDomains(domains); - } else { - setCookieDomains([]); - } - } catch (error: any) { - toast.error(error?.message || "Failed to load cookies"); - setCookieDomains([]); - } finally { - setCookiesLoading(false); - } - }; - - const handleDeleteMainDomain = async (mainDomain: string, subdomains: CookieDomain[]) => { - setDeletingDomain(mainDomain); - try { - // Delete all subdomains under this main domain - const deletePromises = subdomains.map(item => - fetchDelete(`/browser/cookies/${encodeURIComponent(item.domain)}`) - ); - await Promise.all(deletePromises); - - toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); - // Remove from local state - const domainsToRemove = new Set(subdomains.map(item => item.domain)); - setCookieDomains(prev => prev.filter(item => !domainsToRemove.has(item.domain))); - - // Mark as having unsaved changes - setHasUnsavedChanges(true); - // Show restart dialog after successful deletion - setShowRestartDialog(true); - } catch (error: any) { - toast.error(error?.message || `Failed to delete cookies for ${mainDomain}`); - } finally { - setDeletingDomain(null); - } - }; -4 - const handleDeleteAll = async () => { - setDeletingAll(true); - try { - await fetchDelete("/browser/cookies"); - toast.success("Deleted all cookies"); - setCookieDomains([]); - - // Mark as having unsaved changes - setHasUnsavedChanges(true); - // Show restart dialog after successful deletion - setShowRestartDialog(true); - } catch (error: any) { - toast.error(error?.message || "Failed to delete all cookies"); - } finally { - setDeletingAll(false); - } - }; - - const handleRestartApp = () => { - if (window.electronAPI && window.electronAPI.restartApp) { - window.electronAPI.restartApp(); - } else { - toast.error("Restart function not available"); - } - }; - - const handleConfirmRestart = () => { - setShowRestartDialog(false); - handleRestartApp(); - }; - - return ( -
- {/* Restart Dialog */} - setShowRestartDialog(false)} - onConfirm={handleConfirmRestart} - title="Cookies Updated" - message="Cookies have been updated. Would you like to restart the application to use the new cookies?" - confirmText="Yes, Restart" - cancelText="No, Add More" - confirmVariant="information" - /> - - {/* Use Existing Browser Dialog */} - { - setShowUseExistingDialog(false); - setPendingPort(null); - }} - onConfirm={handleUseExistingBrowser} - title="Browser Found" - message={`A browser is running on port ${pendingPort}. Would you like to use it for browser operations?`} - confirmText="Yes, Use This Browser" - cancelText="Cancel" - confirmVariant="information" - /> - - {/* Launch New Browser Dialog */} - { - setShowLaunchNewDialog(false); - setPendingPort(null); - }} - onConfirm={handleLaunchNewBrowser} - title="No Browser Found" - message={`No browser is running on port ${pendingPort}. Would you like to launch a new Chrome browser with CDP enabled on this port?`} - confirmText="Yes, Launch Browser" - cancelText="Cancel" - confirmVariant="information" - /> - - {/* Header Section */} -
-
-
-
-
{t("layout.browser-management")}
-

- {t("layout.browser-management-description")}.

-
-
-
-
- - {/* Content Section */} -
-
- -
-
- -
-
{t("layout.browser-cookies")}
-

{t("layout.browser-cookies-description")} -

- - {/* CDP Port Configuration Section */} -
-
-
-
- CDP Browser Connection -
-

- Connect to a Chrome browser with remote debugging enabled -

-
-
- -
-
-
- Current Port: {cdpPort} -
-

- Check if a browser is available on a specific port -

-
- -
- setCustomPort(e.target.value)} - className="flex-1" - min={1} - max={65535} - /> - -
- - {portStatus.available !== null && ( -
- {portStatus.available ? ( - <> - -
-
- Browser Available -
- {portStatus.data && ( -
- {portStatus.data['Browser']} - {portStatus.data['User-Agent']?.split(' ')[0]} -
- )} -
- - ) : ( - <> - -
-
- Browser Not Available -
-
- {portStatus.error} -
-
- - )} -
- )} -
-
- - {/* 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 */} -
- -
-
-
- {t("layout.cookie-domains")} -
- {cookieDomains.length > 0 && ( -
- {groupDomainsByMain(cookieDomains).length} -
- )} -
- -
- {cookieDomains.length > 0 && ( - - )} - - -
-
- - {cookieDomains.length > 0 ? ( -
- {groupDomainsByMain(cookieDomains).map((group, index) => ( -
-
- - {group.mainDomain} - - - {group.totalCookies} Cookie{group.totalCookies !== 1 ? 's' : ''} - -
- -
- ))} -
- ) : ( -
- -
- {t("layout.no-cookies-saved-yet")} -
-

- {t("layout.no-cookies-saved-yet-description")} -

-
- )} -
-
- -
- For more information, check out our - {t("layout.privacy-policy")} + const { t } = useTranslation(); + const [loginLoading, setLoginLoading] = useState(false); + const [cookiesLoading, setCookiesLoading] = useState(false); + const [cookieDomains, setCookieDomains] = useState([]); + const [deletingDomain, setDeletingDomain] = useState(null); + const [deletingAll, setDeletingAll] = useState(false); + const [showRestartDialog, setShowRestartDialog] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + // CDP port configuration + const [cdpPort, setCdpPort] = useState(9223); + const [customPort, setCustomPort] = useState('9223'); + const [portStatus, setPortStatus] = useState({ + checking: false, + available: null, + }); + + // Dialog states + const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); + 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 + const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain; + const parts = cleanDomain.split('.'); + + // For domains with 2 or fewer parts, return as is + if (parts.length <= 2) { + return cleanDomain; + } + + // For domains with more parts, return last 2 parts (main domain) + return parts.slice(-2).join('.'); + }; + + // Group domains by main domain + const groupDomainsByMain = (domains: CookieDomain[]): GroupedDomain[] => { + const grouped = new Map(); + + domains.forEach((item) => { + const mainDomain = getMainDomain(item.domain); + if (!grouped.has(mainDomain)) { + grouped.set(mainDomain, []); + } + grouped.get(mainDomain)!.push(item); + }); + + return Array.from(grouped.entries()) + .map(([mainDomain, subdomains]) => ({ + mainDomain, + subdomains, + totalCookies: subdomains.reduce( + (sum, item) => sum + item.cookie_count, + 0 + ), + })) + .sort((a, b) => a.mainDomain.localeCompare(b.mainDomain)); + }; + + // Auto-load cookies on component mount + useEffect(() => { + handleLoadCookies(); + // Load current browser port on mount + loadCurrentBrowserPort(); + // Load CDP browser pool + loadCdpBrowsers(); + }, []); + + const loadCurrentBrowserPort = async () => { + if (window.ipcRenderer) { + const port = await window.ipcRenderer.invoke('get-browser-port'); + setCdpPort(port); + setCustomPort(String(port)); + } + }; + + 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); + + if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { + toast.error('Please enter a valid port number (1-65535)'); + return; + } + + setPortStatus({ checking: true, available: null }); + + try { + if (!window.electronAPI?.checkCdpPort) { + toast.error('CDP port check not available'); + setPortStatus({ + checking: false, + available: false, + error: 'Not available', + }); + return; + } + + const result = await window.electronAPI.checkCdpPort(portNumber); + + if (result.available) { + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + // Browser exists, ask if user wants to use it + setPendingPort(portNumber); + setShowUseExistingDialog(true); + } else { + setPortStatus({ + checking: false, + available: false, + error: result.error, + }); + // No browser on this port, ask if user wants to launch one + setPendingPort(portNumber); + setShowLaunchNewDialog(true); + } + } catch (error: any) { + setPortStatus({ + checking: false, + available: false, + error: error.message, + }); + toast.error(error.message || 'Failed to check port'); + } + }; + + const handleUseExistingBrowser = async () => { + setShowUseExistingDialog(false); + if (pendingPort) { + try { + 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'); + } + } + } catch (error: any) { + toast.error(error.message || 'Failed to add browser to pool'); + } + } + setPendingPort(null); + }; + + const handleLaunchNewBrowser = async () => { + setShowLaunchNewDialog(false); + + if (!pendingPort) { + return; + } + + const port = pendingPort; + setPendingPort(null); + + try { + if (!window.electronAPI?.launchCdpBrowser) { + toast.error('Launch CDP browser not available'); + return; + } + + toast.loading(`Launching browser on port ${port}...`, { + id: 'launch-browser', + }); + + const result = await window.electronAPI.launchCdpBrowser(port); + + if (result.success) { + 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, + available: true, + data: result.data, + }); + } else { + toast.error(result.error || 'Failed to launch browser', { + id: 'launch-browser', + }); + } + } catch (error: any) { + toast.error(error.message || 'Failed to launch browser', { + id: 'launch-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 { + const response = await fetchPost('/browser/login'); + if (response) { + toast.success('Browser opened successfully for login'); + // Listen for browser close event to reload cookies + const checkInterval = setInterval(async () => { + try { + // Check if browser is still open by making a request + // When browser closes, reload cookies + const statusResponse = await fetchGet('/browser/status'); + if (!statusResponse || !statusResponse.is_open) { + clearInterval(checkInterval); + await handleLoadCookies(); + // Check if cookies changed + const newResponse = await fetchGet('/browser/cookies'); + if (newResponse && newResponse.success) { + const newDomains = newResponse.domains || []; + const newCookieCount = newDomains.reduce( + (sum: number, item: CookieDomain) => sum + item.cookie_count, + 0 + ); + + if (newCookieCount > currentCookieCount) { + // Cookies were added, show success toast and restart dialog + const addedCount = newCookieCount - currentCookieCount; + toast.success( + `Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}` + ); + setHasUnsavedChanges(true); + setShowRestartDialog(true); + } else if (newCookieCount < currentCookieCount) { + // Cookies were deleted (shouldn't happen here, but handle it) + setHasUnsavedChanges(true); + setShowRestartDialog(true); + } + } + } + } catch (error) { + // Browser might be closed + clearInterval(checkInterval); + await handleLoadCookies(); + } + }, 500); // Check every 2 seconds + } + } catch (error: any) { + toast.error(error?.message || 'Failed to open browser'); + } finally { + setLoginLoading(false); + } + }; + + const handleLoadCookies = async () => { + setCookiesLoading(true); + try { + const response = await fetchGet('/browser/cookies'); + if (response && response.success) { + const domains = response.domains || []; + setCookieDomains(domains); + } else { + setCookieDomains([]); + } + } catch (error: any) { + toast.error(error?.message || 'Failed to load cookies'); + setCookieDomains([]); + } finally { + setCookiesLoading(false); + } + }; + + const handleDeleteMainDomain = async ( + mainDomain: string, + subdomains: CookieDomain[] + ) => { + setDeletingDomain(mainDomain); + try { + // Delete all subdomains under this main domain + const deletePromises = subdomains.map((item) => + fetchDelete(`/browser/cookies/${encodeURIComponent(item.domain)}`) + ); + await Promise.all(deletePromises); + + toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); + // Remove from local state + const domainsToRemove = new Set(subdomains.map((item) => item.domain)); + setCookieDomains((prev) => + prev.filter((item) => !domainsToRemove.has(item.domain)) + ); + + // Mark as having unsaved changes + setHasUnsavedChanges(true); + // Show restart dialog after successful deletion + setShowRestartDialog(true); + } catch (error: any) { + toast.error( + error?.message || `Failed to delete cookies for ${mainDomain}` + ); + } finally { + setDeletingDomain(null); + } + }; + + const handleDeleteAll = async () => { + setDeletingAll(true); + try { + await fetchDelete('/browser/cookies'); + toast.success('Deleted all cookies'); + setCookieDomains([]); + + // Mark as having unsaved changes + setHasUnsavedChanges(true); + // Show restart dialog after successful deletion + setShowRestartDialog(true); + } catch (error: any) { + toast.error(error?.message || 'Failed to delete all cookies'); + } finally { + setDeletingAll(false); + } + }; + + const handleRestartApp = () => { + if (window.electronAPI && window.electronAPI.restartApp) { + window.electronAPI.restartApp(); + } else { + toast.error('Restart function not available'); + } + }; + + const handleConfirmRestart = () => { + setShowRestartDialog(false); + handleRestartApp(); + }; + + return ( +
+ {/* Restart Dialog */} + setShowRestartDialog(false)} + onConfirm={handleConfirmRestart} + title="Cookies Updated" + message="Cookies have been updated. Would you like to restart the application to use the new cookies?" + confirmText="Yes, Restart" + cancelText="No, Add More" + confirmVariant="information" + /> + + {/* Use Existing Browser Dialog */} + { + setShowUseExistingDialog(false); + setPendingPort(null); + }} + onConfirm={handleUseExistingBrowser} + title="Browser Found" + message={`A browser is running on port ${pendingPort}. Would you like to use it for browser operations?`} + confirmText="Yes, Use This Browser" + cancelText="Cancel" + confirmVariant="information" + /> + + {/* Launch New Browser Dialog */} + { + setShowLaunchNewDialog(false); + setPendingPort(null); + }} + onConfirm={handleLaunchNewBrowser} + title="No Browser Found" + message={`No browser is running on port ${pendingPort}. Would you like to launch a new Chrome browser with CDP enabled on this port?`} + confirmText="Yes, Launch Browser" + cancelText="Cancel" + confirmVariant="information" + /> + + {/* Header Section */} +
+
+
+
+
+ {t('layout.browser-management')} +
+

+ {t('layout.browser-management-description')}. +

+
+
+
+
+ + {/* Content Section */} +
+
+
+
+ +
+
+ {t('layout.browser-cookies')} +
+

+ {t('layout.browser-cookies-description')} +

+ + {/* CDP Port Configuration Section */} +
+
+
+
+ CDP Browser Connection +
+

+ Connect to a Chrome browser with remote debugging enabled +

+
+
+ +
+
+
+ Current Port:{' '} + + {cdpPort} + +
+

+ Check if a browser is available on a specific port +

+
+ +
+ setCustomPort(e.target.value)} + className="flex-1" + min={1} + max={65535} + /> + +
+ + {portStatus.available !== null && ( +
+ {portStatus.available ? ( + <> + +
+
+ Browser Available +
+ {portStatus.data && ( +
+ {portStatus.data['Browser']} -{' '} + {portStatus.data['User-Agent']?.split(' ')[0]} +
+ )} +
+ + ) : ( + <> + +
+
+ Browser Not Available +
+
+ {portStatus.error} +
+
+ + )} +
+ )} +
+
+ + {/* 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 */} +
+
+
+
+ {t('layout.cookie-domains')} +
+ {cookieDomains.length > 0 && ( +
+ {groupDomainsByMain(cookieDomains).length} +
+ )} +
+ +
+ {cookieDomains.length > 0 && ( + + )} + + +
+
+ + {cookieDomains.length > 0 ? ( +
+ {groupDomainsByMain(cookieDomains).map((group, index) => ( +
+
+ + {group.mainDomain} + + + {group.totalCookies} Cookie + {group.totalCookies !== 1 ? 's' : ''} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-cookies-saved-yet')} +
+

+ {t('layout.no-cookies-saved-yet-description')} +

+
+ )} +
-
-
-
- ); +
+ For more information, check out our + + {t('layout.privacy-policy')} + +
+
+
+
+ ); } From fa069a8d2133bd8a5b4c142a1a1e175c1ee36674 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 17 Feb 2026 00:53:39 +0000 Subject: [PATCH 32/40] chore: exclude md files from lint-staged and revert formatting changes Co-Authored-By: Claude Opus 4.6 --- .lintstagedrc.json | 2 +- README.md | 2 +- README_CN.md | 2 +- README_JA.md | 2 +- README_PT-BR.md | 2 +- docs/core/models/gemini.md | 2 +- docs/core/models/kimi.md | 2 +- docs/core/models/minimax.md | 2 +- server/README_CN.md | 6 +++--- server/README_EN.md | 6 +++--- server/README_PT-BR.md | 6 +++--- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 316ae22e5..3f024a1db 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -5,6 +5,6 @@ "node licenses/update_license.js" ], "*.{js,jsx}": ["eslint --fix --no-warn-ignored", "prettier --write"], - "*.{json,css,md}": ["prettier --write"], + "*.{json,css}": ["prettier --write"], "*.py": ["node licenses/update_license.js"] } diff --git a/README.md b/README.md index 65b33bfef..f6dc9eab6 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr - [📄 Open Source License](#-open-source-license) - [🌐 Community & contact](#-community--contact) -#### +####
diff --git a/README_CN.md b/README_CN.md index a8eb26594..664266bfb 100644 --- a/README_CN.md +++ b/README_CN.md @@ -74,7 +74,7 @@ - [📄 开源许可证](#-%E5%BC%80%E6%BA%90%E8%AE%B8%E5%8F%AF%E8%AF%81) - [🌐 社区与联系](#-%E7%A4%BE%E5%8C%BA%E4%B8%8E%E8%81%94%E7%B3%BB) -#### +####
diff --git a/README_JA.md b/README_JA.md index 740cc4df7..7642cb52d 100644 --- a/README_JA.md +++ b/README_JA.md @@ -73,7 +73,7 @@ - [📄 オープンソースライセンス](#-%E3%82%AA%E3%83%BC%E3%83%97%E3%83%B3%E3%82%BD%E3%83%BC%E3%82%B9%E3%83%A9%E3%82%A4%E3%82%BB%E3%83%B3%E3%82%B9) - [🌐 コミュニティ & お問い合わせ](#-%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3--%E3%81%8A%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B) -#### +####
diff --git a/README_PT-BR.md b/README_PT-BR.md index 326d527e8..0b8654b7f 100644 --- a/README_PT-BR.md +++ b/README_PT-BR.md @@ -74,7 +74,7 @@ Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], noss - [📄 Licença Open Source](#-licen%C3%A7a-open-source) - [🌐 Comunidade & Contato](#-comunidade--contato) -#### +####
diff --git a/docs/core/models/gemini.md b/docs/core/models/gemini.md index ef241789c..c07df96e7 100644 --- a/docs/core/models/gemini.md +++ b/docs/core/models/gemini.md @@ -48,7 +48,7 @@ Click on the Gemini Config card and fill in the following fields: ![Gemini 4 Pn](/docs/images/gemini_3.png) ---- +______________________________________________________________________ > **Video Tutorial:** Prefer a visual guide? Watch the full configuration video > here. diff --git a/docs/core/models/kimi.md b/docs/core/models/kimi.md index dfe2c8b70..bb55b8863 100644 --- a/docs/core/models/kimi.md +++ b/docs/core/models/kimi.md @@ -48,4 +48,4 @@ Click on the Moonshot card and fill in the following fields: ![Kimi 4 Pn](/docs/images/kimi_3.png) ---- +______________________________________________________________________ diff --git a/docs/core/models/minimax.md b/docs/core/models/minimax.md index c60a044ae..6e9425491 100644 --- a/docs/core/models/minimax.md +++ b/docs/core/models/minimax.md @@ -47,4 +47,4 @@ Click on the Minimax Config card and fill in the following fields: ![Minimax 4 Pn](/docs/images/minimax_3.png) ---- +______________________________________________________________________ diff --git a/server/README_CN.md b/server/README_CN.md index f0fb305b6..7e9ce222b 100644 --- a/server/README_CN.md +++ b/server/README_CN.md @@ -20,7 +20,7 @@ 说明:上述数据均保存在 Docker 中的本地 PostgreSQL 卷中(见“数据持久化”),不经我们云端。若你配置了外部模型或远程 MCP,则相应请求会发往你指定的第三方服务。 ---- +______________________________________________________________________ ### 快速开始(Docker 推荐) @@ -93,7 +93,7 @@ docker logs -f eigent_postgres | cat 提示:若拉取镜像缓慢,可在 Docker Desktop 配置国内镜像加速后重试。 ---- +______________________________________________________________________ ### 开发模式(可选) @@ -118,7 +118,7 @@ docker logs -f eigent_postgres | cat uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0 ``` ---- +______________________________________________________________________ ### 其它 diff --git a/server/README_EN.md b/server/README_EN.md index 32e95d144..822421465 100644 --- a/server/README_EN.md +++ b/server/README_EN.md @@ -20,7 +20,7 @@ Note: All the above data is stored in the local PostgreSQL volume in Docker (see “Data Persistence” below). If you configure external models or remote MCP, requests go to the third-party services you specify. ---- +______________________________________________________________________ ### Quick Start (Docker) @@ -91,7 +91,7 @@ docker logs -f eigent_api | cat docker logs -f eigent_postgres | cat ``` ---- +______________________________________________________________________ ### Developer Mode (Optional) @@ -110,7 +110,7 @@ export database_url=postgresql://postgres:123456@localhost:5432/eigent uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0 ``` ---- +______________________________________________________________________ ### Others diff --git a/server/README_PT-BR.md b/server/README_PT-BR.md index 44050a626..83beefda3 100644 --- a/server/README_PT-BR.md +++ b/server/README_PT-BR.md @@ -20,7 +20,7 @@ Nota: Todos os dados acima são armazenados no volume PostgreSQL local no Docker (veja "Persistência de Dados" abaixo). Se você configurar modelos externos ou MCP remoto, as solicitações vão para os serviços de terceiros que você especificar. ---- +______________________________________________________________________ ### Início Rápido (Docker) @@ -91,7 +91,7 @@ docker logs -f eigent_api | cat docker logs -f eigent_postgres | cat ``` ---- +______________________________________________________________________ ### Modo Desenvolvedor (Opcional) @@ -110,7 +110,7 @@ export database_url=postgresql://postgres:123456@localhost:5432/eigent uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0 ``` ---- +______________________________________________________________________ ### Outros From f5f3ad075140e29e40441bc3b60faf70147c9269 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 17 Feb 2026 00:58:54 +0000 Subject: [PATCH 33/40] style: fix ruff format in browser.py Co-Authored-By: Claude Opus 4.6 --- backend/app/agent/factory/browser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 00f4032e4..fed741220 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -116,8 +116,7 @@ def release_by_task(self, task_id: str) -> list[int]: released_ports = [] with self._lock: sessions = [ - s for s, t in self._session_to_task.items() - if t == task_id + s for s, t in self._session_to_task.items() if t == task_id ] for session_id in sessions: port = self._session_to_port.get(session_id) From c2149c62669714d6efb94f763223826f6971c131 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 17 Feb 2026 01:52:44 +0000 Subject: [PATCH 34/40] update frontend chore code --- backend/app/agent/factory/browser.py | 7 -- backend/app/model/chat.py | 1 - backend/app/utils/workforce.py | 36 +++++----- electron/preload/index.ts | 2 +- src/i18n/locales/ar/layout.json | 39 +++++++++- src/i18n/locales/de/layout.json | 39 +++++++++- src/i18n/locales/en-us/layout.json | 39 +++++++++- src/i18n/locales/es/layout.json | 39 +++++++++- src/i18n/locales/fr/layout.json | 39 +++++++++- src/i18n/locales/it/layout.json | 39 +++++++++- src/i18n/locales/ja/layout.json | 39 +++++++++- src/i18n/locales/ko/layout.json | 39 +++++++++- src/i18n/locales/ru/layout.json | 39 +++++++++- src/i18n/locales/zh-Hans/layout.json | 39 +++++++++- src/i18n/locales/zh-Hant/layout.json | 39 +++++++++- src/pages/Dashboard/Browser.tsx | 102 +++++++++++++++------------ src/store/chatStore.ts | 4 -- 17 files changed, 491 insertions(+), 90 deletions(-) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index fed741220..4500b4afb 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -136,13 +136,6 @@ def release_by_task(self, task_id: str) -> list[int]: ) return released_ports - def clear_all(self): - """Force-clear all occupied ports (safety net for task cleanup).""" - with self._lock: - count = len(self._occupied_ports) - self._occupied_ports.clear() - return count - def get_occupied_ports(self) -> list[int]: """Get list of currently occupied ports.""" with self._lock: diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index eb647ac5c..24cfa79cf 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -63,7 +63,6 @@ class Chat(BaseModel): api_url: str | None = None language: str = "en" browser_port: int = 9222 - use_external_cdp: bool = False cdp_browsers: list[dict] = Field(default_factory=list) max_retries: int = 3 allow_local_system: bool = False diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index b8f72a991..39be31e79 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -968,33 +968,31 @@ def _cleanup_all_agents(self) -> None: try: from app.agent.factory.browser import _cdp_pool_manager - task_ids = set() + task_ids: set[str] = set() if hasattr(self, "_children") and self._children: for child in self._children: - if hasattr(child, "worker_agent") and hasattr( - child.worker_agent, "_cdp_task_id" - ): - task_ids.add(child.worker_agent._cdp_task_id) + if hasattr(child, "worker_agent"): + tid = getattr(child.worker_agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) if hasattr(child, "agent_pool") and child.agent_pool: for agent in list(child.agent_pool._available_agents): - if hasattr(agent, "_cdp_task_id"): - task_ids.add(agent._cdp_task_id) - if ( - hasattr(self, "coordinator_agent") - and self.coordinator_agent - and hasattr(self.coordinator_agent, "_cdp_task_id") - ): - task_ids.add(self.coordinator_agent._cdp_task_id) + tid = getattr(agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + if hasattr(self, "coordinator_agent") and self.coordinator_agent: + tid = getattr(self.coordinator_agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) if not task_ids: - logger.warning( + logger.debug( "[WF-CLEANUP] No task_id found for CDP release; skipping pool cleanup" ) - return - - logger.info( - f"[WF-CLEANUP] Force releasing CDP resources for task_ids: {sorted(task_ids)}" - ) + else: + logger.info( + f"[WF-CLEANUP] Force releasing CDP resources for task_ids: {sorted(task_ids)}" + ) released_ports = [] for task_id in task_ids: released_ports.extend( diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 0f5e163f5..f84991572 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -160,7 +160,7 @@ contextBridge.exposeInMainWorld('electronAPI', { 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'), + getBrowserPort: () => ipcRenderer.invoke('get-browser-port'), getCdpBrowsers: () => ipcRenderer.invoke('get-cdp-browsers'), getRunningBrowserPorts: () => ipcRenderer.invoke('get-running-browser-ports'), addCdpBrowser: (port: number, isExternal: boolean, name?: string) => diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index 75f758ab6..73c8e1a27 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -166,5 +166,42 @@ "days-ago": "أيام مضت", "delete-project": "حذف المشروع", "delete-project-confirmation": "هل أنت متأكد من أنك تريد حذف هذا المشروع وجميع مهامه؟ لا يمكن التراجع عن هذا الإجراء.", - "please-select-model": "يرجى اختيار نموذج في الإعدادات > النماذج للمتابعة." + "please-select-model": "يرجى اختيار نموذج في الإعدادات > النماذج للمتابعة.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index 4cc133adb..9b379792d 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -166,5 +166,42 @@ "days-ago": "Tage zuvor", "delete-project": "Projekt löschen", "delete-project-confirmation": "Sind Sie sicher, dass Sie dieses Projekt und alle seine Aufgaben löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "please-select-model": "Bitte wählen Sie ein Modell unter Einstellungen > Modelle aus, um fortzufahren." + "please-select-model": "Bitte wählen Sie ein Modell unter Einstellungen > Modelle aus, um fortzufahren.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index 786103b67..260405fc8 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -168,5 +168,42 @@ "days-ago": "days ago", "delete-project": "Delete Project", "delete-project-confirmation": "Are you sure you want to delete this project and all its tasks? This action cannot be undone.", - "please-select-model": "Please select a model in Settings > Models to continue." + "please-select-model": "Please select a model in Settings > Models to continue.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Electron Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9223)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index 3f700d514..f0813c016 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -166,5 +166,42 @@ "days-ago": "días atrás", "delete-project": "Eliminar Proyecto", "delete-project-confirmation": "¿Estás seguro de que quieres eliminar este proyecto y todas sus tareas? Esta acción no se puede deshacer.", - "please-select-model": "Por favor, selecciona un modelo en Configuración > Modelos para continuar." + "please-select-model": "Por favor, selecciona un modelo en Configuración > Modelos para continuar.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index d2415b021..a3fda3079 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -166,5 +166,42 @@ "days-ago": "jours auparavant", "delete-project": "Supprimer le Projet", "delete-project-confirmation": "Êtes-vous sûr de vouloir supprimer ce projet et toutes ses tâches ? Cette action ne peut pas être annulée.", - "please-select-model": "Veuillez sélectionner un modèle dans Paramètres > Modèles pour continuer." + "please-select-model": "Veuillez sélectionner un modèle dans Paramètres > Modèles pour continuer.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 1b5b365d0..be3fdda6d 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -166,5 +166,42 @@ "days-ago": "giorni fa", "delete-project": "Elimina Progetto", "delete-project-confirmation": "Sei sicuro di voler eliminare questo progetto e tutte le sue attività? Questa azione non può essere annullata.", - "please-select-model": "Seleziona un modello in Impostazioni > Modelli per continuare." + "please-select-model": "Seleziona un modello in Impostazioni > Modelli per continuare.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index 6c0e9e45c..c0b67175f 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -166,5 +166,42 @@ "days-ago": "日前", "delete-project": "プロジェクトを削除", "delete-project-confirmation": "このプロジェクトとそのすべてのタスクを削除してもよろしいですか?この操作は元に戻せません。", - "please-select-model": "続行するには、設定 > モデルでモデルを選択してください。" + "please-select-model": "続行するには、設定 > モデルでモデルを選択してください。", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index d6678e942..3e8d67447 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -166,5 +166,42 @@ "days-ago": "일 전", "delete-project": "프로젝트 삭제", "delete-project-confirmation": "이 프로젝트와 모든 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "please-select-model": "계속하려면 설정 > 모델에서 모델을 선택하세요." + "please-select-model": "계속하려면 설정 > 모델에서 모델을 선택하세요.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index bc93cfc18..0d33fd3cf 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -166,5 +166,42 @@ "days-ago": "дней назад", "delete-project": "Удалить проект", "delete-project-confirmation": "Вы уверены, что хотите удалить этот проект и все его задачи? Это действие нельзя отменить.", - "please-select-model": "Пожалуйста, выберите модель в Настройки > Модели, чтобы продолжить." + "please-select-model": "Пожалуйста, выберите модель в Настройки > Модели, чтобы продолжить.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index 4a28cc55e..15932d4ac 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -168,5 +168,42 @@ "days-ago": "天前", "delete-project": "删除项目", "delete-project-confirmation": "您确定要删除此项目及其所有任务吗?此操作无法撤销。", - "please-select-model": "请在设置 > 模型中选择一个模型以继续。" + "please-select-model": "请在设置 > 模型中选择一个模型以继续。", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 1a33236cd..07dd2ae7c 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -168,5 +168,42 @@ "days-ago": "天前", "delete-project": "刪除專案", "delete-project-confirmation": "您確定要刪除此專案及其所有任務嗎?此操作無法撤銷。", - "please-select-model": "請在設定 > 模型中選擇一個模型以繼續。" + "please-select-model": "請在設定 > 模型中選擇一個模型以繼續。", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index 8507d7569..df2585268 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -134,10 +134,9 @@ export default function Browser() { }, []); const loadCurrentBrowserPort = async () => { - if (window.ipcRenderer) { - const port = await window.ipcRenderer.invoke('get-browser-port'); + if (window.electronAPI?.getBrowserPort) { + const port = await window.electronAPI.getBrowserPort(); setCdpPort(port); - setCustomPort(String(port)); } }; @@ -178,7 +177,7 @@ export default function Browser() { const portNumber = parseInt(customPort); if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { - toast.error('Please enter a valid port number (1-65535)'); + toast.error(t('layout.invalid-port')); return; } @@ -186,7 +185,7 @@ export default function Browser() { try { if (!window.electronAPI?.checkCdpPort) { - toast.error('CDP port check not available'); + toast.error(t('layout.cdp-port-check-not-available')); setPortStatus({ checking: false, available: false, @@ -222,7 +221,7 @@ export default function Browser() { available: false, error: error.message, }); - toast.error(error.message || 'Failed to check port'); + toast.error(error.message || t('layout.failed-to-check-port')); } }; @@ -238,15 +237,15 @@ export default function Browser() { ); if (result.success) { toast.success( - `Added external browser on port ${pendingPort} to pool` + t('layout.added-browser-to-pool', { port: pendingPort }) ); await loadCdpBrowsers(); } else { - toast.error(result.error || 'Failed to add browser to pool'); + toast.error(result.error || t('layout.failed-to-add-browser')); } } } catch (error: any) { - toast.error(error.message || 'Failed to add browser to pool'); + toast.error(error.message || t('layout.failed-to-add-browser')); } } setPendingPort(null); @@ -264,18 +263,18 @@ export default function Browser() { try { if (!window.electronAPI?.launchCdpBrowser) { - toast.error('Launch CDP browser not available'); + toast.error(t('layout.launch-not-available')); return; } - toast.loading(`Launching browser on port ${port}...`, { + toast.loading(t('layout.launching-browser', { port }), { id: 'launch-browser', }); const result = await window.electronAPI.launchCdpBrowser(port); if (result.success) { - toast.success(`Browser launched successfully on port ${port}`, { + toast.success(t('layout.browser-launched', { port }), { id: 'launch-browser', }); @@ -289,7 +288,7 @@ export default function Browser() { if (addResult.success) { await loadCdpBrowsers(); } else { - toast.error(addResult.error || 'Failed to add browser to pool'); + toast.error(addResult.error || t('layout.failed-to-add-browser')); } } @@ -300,12 +299,12 @@ export default function Browser() { data: result.data, }); } else { - toast.error(result.error || 'Failed to launch browser', { + toast.error(result.error || t('layout.failed-to-launch-browser'), { id: 'launch-browser', }); } } catch (error: any) { - toast.error(error.message || 'Failed to launch browser', { + toast.error(error.message || t('layout.failed-to-launch-browser'), { id: 'launch-browser', }); } @@ -317,14 +316,14 @@ export default function Browser() { if (window.electronAPI?.removeCdpBrowser) { const result = await window.electronAPI.removeCdpBrowser(browserId); if (result.success) { - toast.success('Browser removed from pool'); + toast.success(t('layout.browser-removed')); await loadCdpBrowsers(); } else { - toast.error(result.error || 'Failed to remove browser'); + toast.error(result.error || t('layout.failed-to-remove-browser')); } } } catch (error: any) { - toast.error(error.message || 'Failed to remove browser'); + toast.error(error.message || t('layout.failed-to-remove-browser')); } finally { setDeletingBrowser(null); } @@ -332,10 +331,14 @@ export default function Browser() { const handleBrowserLogin = async () => { setLoginLoading(true); + const currentCookieCount = cookieDomains.reduce( + (sum, item) => sum + item.cookie_count, + 0 + ); try { const response = await fetchPost('/browser/login'); if (response) { - toast.success('Browser opened successfully for login'); + toast.success(t('layout.browser-opened')); // Listen for browser close event to reload cookies const checkInterval = setInterval(async () => { try { @@ -455,7 +458,7 @@ export default function Browser() { if (window.electronAPI && window.electronAPI.restartApp) { window.electronAPI.restartApp(); } else { - toast.error('Restart function not available'); + toast.error(t('layout.restart-not-available')); } }; @@ -486,10 +489,10 @@ export default function Browser() { setPendingPort(null); }} onConfirm={handleUseExistingBrowser} - title="Browser Found" - message={`A browser is running on port ${pendingPort}. Would you like to use it for browser operations?`} - confirmText="Yes, Use This Browser" - cancelText="Cancel" + title={t('layout.browser-found')} + message={t('layout.browser-found-description', { port: pendingPort })} + confirmText={t('layout.yes-use-browser')} + cancelText={t('layout.cancel')} confirmVariant="information" /> @@ -501,10 +504,12 @@ export default function Browser() { setPendingPort(null); }} onConfirm={handleLaunchNewBrowser} - title="No Browser Found" - message={`No browser is running on port ${pendingPort}. Would you like to launch a new Chrome browser with CDP enabled on this port?`} - confirmText="Yes, Launch Browser" - cancelText="Cancel" + title={t('layout.no-browser-found')} + message={t('layout.no-browser-found-description', { + port: pendingPort, + })} + confirmText={t('layout.yes-launch-browser')} + cancelText={t('layout.cancel')} confirmVariant="information" /> @@ -559,10 +564,10 @@ export default function Browser() {
- CDP Browser Connection + {t('layout.cdp-browser-connection')}

- Connect to a Chrome browser with remote debugging enabled + {t('layout.cdp-browser-connection-description')}

@@ -570,20 +575,20 @@ export default function Browser() {
- Current Port:{' '} + {t('layout.current-port')}{' '} {cdpPort}

- Check if a browser is available on a specific port + {t('layout.cdp-port-check-description')}

setCustomPort(e.target.value)} className="flex-1" @@ -600,10 +605,10 @@ export default function Browser() { {portStatus.checking ? ( <> - Checking + {t('layout.checking')} ) : ( - 'Check Port' + t('layout.check-port') )}
@@ -621,7 +626,7 @@ export default function Browser() {
- Browser Available + {t('layout.browser-available')}
{portStatus.data && (
@@ -636,7 +641,7 @@ export default function Browser() {
- Browser Not Available + {t('layout.browser-not-available')}
{portStatus.error} @@ -655,14 +660,15 @@ export default function Browser() {
- CDP Browser Pool + {t('layout.cdp-browser-pool')}
- {runningPorts.length} / {cdpBrowsers.length} Running + {runningPorts.length} / {cdpBrowsers.length}{' '} + {t('layout.running')}

- Manage multiple CDP browsers for task execution + {t('layout.cdp-browser-pool-description')}

@@ -686,25 +692,27 @@ export default function Browser() { : 'bg-tag-fill-success text-text-success' }`} > - {browser.isExternal ? 'External' : 'Launched'} + {browser.isExternal + ? t('layout.external') + : t('layout.launched')} {/* Running status indicator */} {runningPorts.includes(browser.port) ? ( - Running + {t('layout.running')} ) : ( !browser.isExternal && ( - Stopped + {t('layout.stopped')} ) )}
- Port: {browser.port} + {t('layout.port')} {browser.port}
)} @@ -832,7 +840,7 @@ export default function Browser() {
- For more information, check out our + {t('layout.for-more-info')} ) => }); } 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 @@ -732,7 +729,6 @@ const chatStore = (initial?: Partial) => summary_prompt: ``, new_agents: [...addWorkers], browser_port: browser_port, - use_external_cdp: use_external_cdp, cdp_browsers: cdp_browsers, env_path: envPath, search_config: searchConfig, From 7dbe5ffc673ce3b6d4397d202a8569bb6cd80809 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Sat, 21 Feb 2026 21:15:14 +0000 Subject: [PATCH 35/40] inital_fix --- backend/app/utils/workforce.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 39be31e79..21660b087 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -936,8 +936,8 @@ def _cleanup_all_agents(self) -> None: if hasattr(self, "_children") and self._children: for child in self._children: # Cleanup base worker agent - if hasattr(child, "worker_agent"): - agent = child.worker_agent + if hasattr(child, "worker"): + agent = child.worker cb = getattr(agent, "_cdp_release_callback", None) if callable(cb): try: @@ -969,10 +969,14 @@ def _cleanup_all_agents(self) -> None: from app.agent.factory.browser import _cdp_pool_manager task_ids: set[str] = set() + # Use workforce's own api_task_id as the primary source + if self.api_task_id: + task_ids.add(self.api_task_id) + # Also collect from child agents if hasattr(self, "_children") and self._children: for child in self._children: - if hasattr(child, "worker_agent"): - tid = getattr(child.worker_agent, "_cdp_task_id", None) + if hasattr(child, "worker"): + tid = getattr(child.worker, "_cdp_task_id", None) if tid is not None: task_ids.add(tid) if hasattr(child, "agent_pool") and child.agent_pool: From c5be0eabafe82a51f98062680eafc9aca7d5befd Mon Sep 17 00:00:00 2001 From: Weijie Bai Date: Sun, 22 Feb 2026 22:27:22 +0000 Subject: [PATCH 36/40] chore: Userflow refine_cdp (#1331) Co-authored-by: Puzhen Zhang <91596298+nitpicker55555@users.noreply.github.com> Co-authored-by: puzhen <1303385763@qq.com> --- backend/app/utils/workforce.py | 22 +- electron/main/index.ts | 697 +++++++++++---------- electron/preload/index.ts | 19 +- src/i18n/locales/ar/layout.json | 107 ++-- src/i18n/locales/de/layout.json | 105 ++-- src/i18n/locales/en-us/layout.json | 33 +- src/i18n/locales/es/layout.json | 107 ++-- src/i18n/locales/fr/layout.json | 107 ++-- src/i18n/locales/it/layout.json | 107 ++-- src/i18n/locales/ja/layout.json | 107 ++-- src/i18n/locales/ko/layout.json | 107 ++-- src/i18n/locales/ru/layout.json | 107 ++-- src/i18n/locales/zh-Hans/layout.json | 107 ++-- src/i18n/locales/zh-Hant/layout.json | 107 ++-- src/pages/Dashboard/Browser.tsx | 876 +++++++++++---------------- src/types/electron.d.ts | 19 + 16 files changed, 1475 insertions(+), 1259 deletions(-) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 21660b087..4eb2a2f50 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -932,17 +932,33 @@ def stop_gracefully(self) -> None: def _cleanup_all_agents(self) -> None: """Release CDP browser resources for all agents.""" cleanup_count = 0 + children_count = len(self._children) if hasattr(self, "_children") else 0 + logger.info( + f"[WF-CLEANUP] Starting cleanup, " + f"children={children_count}, api_task_id={self.api_task_id}" + ) if hasattr(self, "_children") and self._children: for child in self._children: # Cleanup base worker agent if hasattr(child, "worker"): agent = child.worker + has_cb = hasattr(agent, "_cdp_release_callback") + port = getattr(agent, "_cdp_port", None) + logger.info( + f"[WF-CLEANUP] Child worker: " + f"agent_id={getattr(agent, 'agent_id', '?')}, " + f"has_release_cb={has_cb}, cdp_port={port}" + ) cb = getattr(agent, "_cdp_release_callback", None) if callable(cb): try: cb(agent) cleanup_count += 1 + logger.info( + f"[WF-CLEANUP] Released CDP via callback: " + f"port={port}" + ) except Exception as e: logger.error( f"[WF-CLEANUP] Error releasing CDP for " @@ -952,7 +968,11 @@ def _cleanup_all_agents(self) -> None: # Cleanup agents in AgentPool if hasattr(child, "agent_pool") and child.agent_pool: pool = child.agent_pool - for agent in list(getattr(pool, "_available_agents", [])): + available = list(getattr(pool, "_available_agents", [])) + logger.info( + f"[WF-CLEANUP] AgentPool available_agents={len(available)}" + ) + for agent in available: cb = getattr(agent, "_cdp_release_callback", None) if callable(cb): try: diff --git a/electron/main/index.ts b/electron/main/index.ts index bad22fe5e..849d8fee5 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -29,7 +29,9 @@ import FormData from 'form-data'; import fsp from 'fs/promises'; import mime from 'mime'; import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; +import crypto from 'node:crypto'; import fs, { existsSync } from 'node:fs'; +import http from 'node:http'; import os, { homedir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -96,21 +98,195 @@ interface CdpBrowser { addedAt: number; } let cdp_browser_pool: CdpBrowser[] = []; +let cdpHealthCheckTimer: ReturnType | null = null; -// Map to store multiple browser processes by port -let cdp_browser_processes: Map = - new Map(); +const CDP_POOL_FILE = path.join(os.homedir(), '.eigent', 'cdp-browsers.json'); -/** Remove a non-external browser from the pool by port (used on process error/exit). */ -function removeFromPoolByPort(port: number, reason: string): void { - const idx = cdp_browser_pool.findIndex( - (b) => b.port === port && !b.isExternal +/** Persist pool to disk. */ +function saveCdpPool(): void { + try { + fs.writeFileSync(CDP_POOL_FILE, JSON.stringify(cdp_browser_pool, null, 2)); + } catch (e) { + log.error(`[CDP POOL] Failed to save pool: ${e}`); + } +} + +/** Load pool from disk. Mark all as external (process handles are lost after restart). */ +function loadCdpPool(): void { + try { + if (fs.existsSync(CDP_POOL_FILE)) { + const data = JSON.parse(fs.readFileSync(CDP_POOL_FILE, 'utf-8')); + cdp_browser_pool = (data as CdpBrowser[]).map((b) => ({ + ...b, + isExternal: true, + })); + log.info( + `[CDP POOL] Loaded ${cdp_browser_pool.length} browser(s) from disk` + ); + } + } catch (e) { + log.error(`[CDP POOL] Failed to load pool: ${e}`); + cdp_browser_pool = []; + } +} + +/** Push current pool to frontend. */ +function notifyCdpPoolChanged(): void { + if (win && !win.isDestroyed()) { + log.info( + `[CDP POOL] Pushing pool update to frontend (size=${cdp_browser_pool.length})` + ); + win.webContents.send('cdp-pool-changed', cdp_browser_pool); + } else { + log.warn('[CDP POOL] Cannot notify: win is null or destroyed'); + } +} + +/** Probe a CDP port. Returns true if alive. */ +async function isCdpPortAlive(port: number): Promise { + try { + const resp = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 1500, + }); + return resp.status === 200; + } catch { + return false; + } +} + +/** Run one health-check cycle: remove dead browsers, persist & notify if changed. */ +async function runPoolHealthCheck(): Promise { + if (cdp_browser_pool.length === 0) return; + const results = await Promise.all( + cdp_browser_pool.map((b) => isCdpPortAlive(b.port)) ); - if (idx !== -1) { - const removed = cdp_browser_pool.splice(idx, 1)[0]; + const deadPorts: number[] = []; + for (let i = results.length - 1; i >= 0; i--) { + if (!results[i]) { + deadPorts.push(cdp_browser_pool[i].port); + cdp_browser_pool.splice(i, 1); + } + } + if (deadPorts.length > 0) { + log.info( + `[CDP POOL] Health-check removed dead ports: ${deadPorts.join(', ')}. pool_size=${cdp_browser_pool.length}` + ); + saveCdpPool(); + notifyCdpPoolChanged(); + } +} + +/** Start periodic health check (call after window is created). */ +function startCdpHealthCheck(): void { + log.info('[CDP POOL] Starting health check (interval=3s)'); + // Run once immediately + runPoolHealthCheck(); + cdpHealthCheckTimer = setInterval(runPoolHealthCheck, 3000); +} + +function stopCdpHealthCheck(): void { + if (cdpHealthCheckTimer) { + clearInterval(cdpHealthCheckTimer); + cdpHealthCheckTimer = null; + } +} + +/** Close a browser via CDP Browser.close() WebSocket command. Best-effort. + * Uses raw Node.js http upgrade (no external ws dependency needed). + * IMPORTANT: Never close the Electron app's own CDP port. */ +async function closeBrowserViaCdp(port: number): Promise { + // Guard: refuse to close the Electron app's own CDP port + if (port === browser_port) { log.warn( - `[CDP POOL] Auto-removed port=${port} (${reason}), id=${removed.id}, pool_size=${cdp_browser_pool.length}` + `[CDP CLOSE] Refusing to close port ${port} (Electron app's own CDP port)` ); + return; + } + + try { + const resp = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 2000, + }); + const wsUrl: string | undefined = resp.data?.webSocketDebuggerUrl; + if (!wsUrl) { + log.warn(`[CDP CLOSE] No webSocketDebuggerUrl for port ${port}`); + return; + } + + const url = new URL(wsUrl); + const key = crypto.randomBytes(16).toString('base64'); + + await new Promise((resolve) => { + let resolved = false; + const done = () => { + if (!resolved) { + resolved = true; + resolve(); + } + }; + + const req = http.request( + { + hostname: url.hostname, + port: url.port, + path: url.pathname, + method: 'GET', + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': key, + }, + }, + () => done() + ); + + const timer = setTimeout(() => { + req.destroy(); + done(); + }, 3000); + + req.on('upgrade', (_res, socket) => { + // Handle socket errors to prevent uncaught exceptions + socket.on('error', () => {}); + + // Build a masked WebSocket text frame with Browser.close + const payload = Buffer.from( + JSON.stringify({ id: 1, method: 'Browser.close' }) + ); + const mask = crypto.randomBytes(4); + const header = Buffer.alloc(6); + header[0] = 0x81; // FIN + text opcode + header[1] = 0x80 | payload.length; // MASK bit + length (<126) + mask.copy(header, 2); + + const masked = Buffer.alloc(payload.length); + for (let i = 0; i < payload.length; i++) { + masked[i] = payload[i] ^ mask[i & 3]; + } + + socket.write(Buffer.concat([header, masked])); + log.info(`[CDP CLOSE] Sent Browser.close to port ${port}`); + + // Give Chrome a moment to process, then clean up + setTimeout(() => { + clearTimeout(timer); + socket.destroy(); + done(); + }, 500); + }); + + req.on('error', (err) => { + log.warn(`[CDP CLOSE] Request error for port ${port}: ${err.message}`); + clearTimeout(timer); + done(); + }); + + req.end(); + }); + log.info(`[CDP CLOSE] Successfully closed browser on port ${port}`); + } catch (err) { + log.warn(`[CDP CLOSE] Best-effort close failed for port ${port}: ${err}`); } } @@ -440,16 +616,10 @@ function registerIpcHandlers() { return cdp_browser_pool; }); - // Get running browser processes - ipcMain.handle('get-running-browser-ports', () => { - return Array.from(cdp_browser_processes.keys()); - }); - // Add browser to pool ipcMain.handle( 'add-cdp-browser', (event, port: number, isExternal: boolean, name?: string) => { - // Check if browser with this port already exists const existing = cdp_browser_pool.find((b) => b.port === port); if (existing) { log.warn( @@ -470,6 +640,8 @@ function registerIpcHandlers() { }; cdp_browser_pool.push(newBrowser); + saveCdpPool(); + notifyCdpPoolChanged(); log.info( `[CDP POOL] ADD: port=${port}, isExternal=${isExternal}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` ); @@ -478,394 +650,190 @@ function registerIpcHandlers() { } ); - // Remove browser from pool - ipcMain.handle('remove-cdp-browser', (event, browserId: string) => { - const index = cdp_browser_pool.findIndex((b) => b.id === browserId); - if (index === -1) { - log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`); - 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)) { - try { - const process = cdp_browser_processes.get(removed.port); - process?.kill(); - cdp_browser_processes.delete(removed.port); - } catch (error) { - log.warn( - `[CDP POOL] Failed to kill browser process on port ${removed.port}: ${error}` - ); - } - } - - log.info( - `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, pool_size=${cdp_browser_pool.length}` - ); - return { success: true, browser: removed }; - }); - - // Update browser in pool + // Remove browser from pool (also closes the browser via CDP) 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) { + 'remove-cdp-browser', + async (event, browserId: string, closeBrowser: boolean = true) => { + const index = cdp_browser_pool.findIndex((b) => b.id === browserId); + if (index === -1) { + log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`); return { success: false, error: 'Browser not found' }; } - // Update allowed fields - if (updates.name !== undefined) browser.name = updates.name; + const removed = cdp_browser_pool.splice(index, 1)[0]; - log.info(`Browser updated in pool`); - return { success: true, browser }; + // Close the browser via CDP (best-effort) + if (closeBrowser) { + await closeBrowserViaCdp(removed.port); + } + + saveCdpPool(); + notifyCdpPoolChanged(); + log.info( + `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, closed=${closeBrowser}, pool_size=${cdp_browser_pool.length}` + ); + return { success: true, browser: removed }; } ); - // Check if CDP port is available - ipcMain.handle('check-cdp-port', async (event, port: number) => { - log.info(`Checking CDP port availability: ${port}`); + // Launch CDP browser with automatic port assignment + ipcMain.handle('launch-cdp-browser', async () => { try { - const response = await axios.get( - `http://localhost:${port}/json/version`, - { - timeout: 3000, + // 1. Find available port (9223–9300) by checking no CDP browser is listening + let port: number | null = null; + for (let p = 9223; p < 9300; p++) { + if ( + !cdp_browser_pool.some((b) => b.port === p) && + !(await isCdpPortAlive(p)) + ) { + port = p; + break; } - ); + } + if (port === null) { + return { success: false, error: 'No available port in 9223-9299' }; + } - if (response.status === 200 && response.data) { - log.info(`CDP port ${port} is available and responsive`); + // 2. Find Playwright Chromium executable + const platform = process.platform; + let cacheDir: string; + if (platform === 'darwin') + cacheDir = path.join(homedir(), 'Library/Caches/ms-playwright'); + else if (platform === 'linux') + cacheDir = path.join(homedir(), '.cache/ms-playwright'); + else if (platform === 'win32') + cacheDir = path.join(homedir(), 'AppData/Local/ms-playwright'); + else + return { success: false, error: `Unsupported platform: ${platform}` }; + + if (!existsSync(cacheDir)) { return { - available: true, - data: response.data, + success: false, + error: + 'Playwright Chromium not found. Please run: npx playwright install chromium', }; } - return { available: false, error: 'Invalid response from CDP' }; - } catch (error: any) { - log.warn(`CDP port ${port} is not available: ${error.message}`); - return { - available: false, - error: - error.code === 'ECONNREFUSED' - ? 'Connection refused - no browser running on this port' - : error.message, - }; - } - }); - - // Launch CDP browser with custom port - ipcMain.handle('launch-cdp-browser', async (event, port: number) => { - log.info(`[CDP LAUNCH] Launching browser on port ${port}`); - - try { - const platform = process.platform; - let chromeExecutable: string | null = null; - - // Use Playwright's Chromium - let playwrightCacheDir: string; - if (platform === 'darwin') { - playwrightCacheDir = path.join( - app.getPath('home'), - 'Library/Caches/ms-playwright' - ); - } else if (platform === 'win32') { - playwrightCacheDir = path.join( - app.getPath('home'), - 'AppData/Local/ms-playwright' - ); - } else if (platform === 'linux') { - playwrightCacheDir = path.join( - app.getPath('home'), - '.cache/ms-playwright' - ); - } else { + const chromiumDirs = fs + .readdirSync(cacheDir) + .filter((d) => d.startsWith('chromium-')) + .sort() + .reverse(); + if (chromiumDirs.length === 0) { return { success: false, - error: `Unsupported platform: ${platform}`, + error: + 'No Playwright Chromium found. Run: npx playwright install chromium', }; } - log.info(`Looking for Playwright Chromium in: ${playwrightCacheDir}`); - - // Find the latest chromium directory - try { - if (!existsSync(playwrightCacheDir)) { - return { - success: false, - error: - 'Playwright Chromium not found. Please run: npx playwright install chromium', - }; - } - - const chromiumDirs = fs - .readdirSync(playwrightCacheDir) - .filter((dir) => dir.startsWith('chromium-')) - .sort() - .reverse(); - - if (chromiumDirs.length === 0) { - return { - success: false, - error: - 'No Playwright Chromium installations found. Please run: npx playwright install chromium', - }; - } - - // Prioritize versions that have Chromium.app over Google Chrome for Testing - let selectedChromiumDir = chromiumDirs[0]; - if (platform === 'darwin') { - for (const dir of chromiumDirs) { - const chromiumAppPaths = [ - path.join( - playwrightCacheDir, - dir, - 'chrome-mac-arm64', - 'Chromium.app' - ), - path.join(playwrightCacheDir, dir, 'chrome-mac', 'Chromium.app'), - ]; - if (chromiumAppPaths.some((p) => existsSync(p))) { - selectedChromiumDir = dir; - log.info(`Selected Chromium version with Chromium.app: ${dir}`); - break; - } - } - } - - const latestChromiumDir = selectedChromiumDir; - log.info(`Using Playwright Chromium version: ${latestChromiumDir}`); - - // Build path to Chromium executable based on platform - if (platform === 'darwin') { - // Try to find Chromium executable in both arm64 and regular directories - // Priority: Chromium.app (older versions) > Google Chrome for Testing (newer versions) - const possiblePaths = [ - // ARM64 paths - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-mac-arm64', - 'Chromium.app/Contents/MacOS/Chromium' - ), - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-mac-arm64', - 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' - ), - // Intel/Universal paths - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-mac', - 'Chromium.app/Contents/MacOS/Chromium' - ), - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-mac', - 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' - ), - ]; - - // Find the first path that exists - chromeExecutable = possiblePaths.find((p) => existsSync(p)) || null; - } else if (platform === 'win32') { - // Windows: Try to find chrome.exe in possible directories - const possiblePaths = [ - // 64-bit paths - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-win64', - 'chrome.exe' - ), - // 32-bit or older versions - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-win', - 'chrome.exe' - ), - ]; - - chromeExecutable = possiblePaths.find((p) => existsSync(p)) || null; - } else if (platform === 'linux') { - // Linux: Try to find chrome in possible directories - const possiblePaths = [ - path.join( - playwrightCacheDir, - latestChromiumDir, - 'chrome-linux', - 'chrome' - ), - ]; - - chromeExecutable = possiblePaths.find((p) => existsSync(p)) || null; - } + const platformPaths: Record string[]> = { + darwin: (base) => [ + path.join( + base, + 'chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium' + ), + path.join( + base, + 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' + ), + path.join(base, 'chrome-mac/Chromium.app/Contents/MacOS/Chromium'), + path.join( + base, + 'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' + ), + ], + linux: (base) => [path.join(base, 'chrome-linux/chrome')], + win32: (base) => [ + path.join(base, 'chrome-win64/chrome.exe'), + path.join(base, 'chrome-win/chrome.exe'), + ], + }; - if (!chromeExecutable || !existsSync(chromeExecutable)) { - return { - success: false, - error: `Chromium executable not found at: ${chromeExecutable}`, - }; + let chromeExe: string | null = null; + for (const dir of chromiumDirs) { + const base = path.join(cacheDir, dir); + const candidates = platformPaths[platform](base); + const found = candidates.find((p) => existsSync(p)); + if (found) { + chromeExe = found; + break; } - - log.info(`Using Chromium at: ${chromeExecutable}`); - } catch (error: any) { - log.error(`Error finding Playwright Chromium: ${error}`); - return { - success: false, - error: `Failed to locate Playwright Chromium: ${error.message}`, - }; + } + if (!chromeExe) { + return { success: false, error: 'Chromium executable not found' }; } - // Create user data directory with port number in name - // This allows multiple browsers on different ports to maintain separate profiles + // 3. Launch browser const userDataDir = path.join( app.getPath('userData'), `cdp_browser_profile_${port}` ); - - // 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}`); } - // Check if browser on this port is already running - if (cdp_browser_processes.has(port)) { - log.warn(`[CDP LAUNCH] Browser process already exists on port ${port}`); - return { - success: false, - error: `Browser already running on port ${port}`, - }; - } - - // Chrome launch arguments - const args = [ - `--remote-debugging-port=${port}`, - `--user-data-dir=${userDataDir}`, - '--no-first-run', - '--no-default-browser-check', - '--disable-blink-features=AutomationControlled', - 'about:blank', - ]; - - log.info(`[CDP LAUNCH] Spawning: ${chromeExecutable} on port ${port}`); - - // Spawn Chrome process - const browserProcess = spawn(chromeExecutable, args, { - detached: false, - stdio: 'ignore', - }); - - browserProcess.on('error', (error) => { - log.error( - `[CDP LAUNCH] Browser process error on port ${port}: ${error}` - ); - cdp_browser_processes.delete(port); - removeFromPoolByPort(port, 'process error'); - }); - - browserProcess.on('exit', (code) => { - log.info( - `[CDP LAUNCH] Browser process on port ${port} exited with code ${code}` - ); - cdp_browser_processes.delete(port); - removeFromPoolByPort(port, `exit code ${code}`); - }); - - // Store the process in the Map - cdp_browser_processes.set(port, browserProcess); - log.info( - `[CDP LAUNCH] Browser process stored in map, PID: ${browserProcess.pid}` + const proc = spawn( + chromeExe, + [ + `--remote-debugging-port=${port}`, + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--disable-blink-features=AutomationControlled', + 'about:blank', + ], + { detached: false, stdio: 'ignore' } ); - // Poll for browser to become ready (max 5 seconds) - log.info( - `[CDP LAUNCH] Polling for browser to become ready (max 5 seconds)...` + proc.on('error', (err) => + log.error(`[CDP LAUNCH] Process error port=${port}: ${err}`) ); - const maxWaitTime = 5000; // 5 seconds - const pollInterval = 300; // Check every 300ms - const startTime = Date.now(); - let attempt = 0; - let lastError = null; - - while (Date.now() - startTime < maxWaitTime) { - attempt++; + + // 4. Poll for readiness (max 5s) + let data: any = null; + const start = Date.now(); + while (Date.now() - start < 5000) { try { - log.info( - `[CDP LAUNCH] Attempt ${attempt}: Checking http://localhost:${port}/json/version` - ); - const response = await axios.get( + const resp = await axios.get( `http://localhost:${port}/json/version`, - { - timeout: 1000, // Short timeout for each attempt - } + { timeout: 1000 } ); - - if (response.status === 200 && response.data) { - const elapsedTime = Date.now() - startTime; - log.info( - `[CDP LAUNCH] ✅ SUCCESS - Browser ready on port ${port} after ${elapsedTime}ms (${attempt} attempts)` - ); - log.info( - `[CDP LAUNCH] Browser info: ${JSON.stringify(response.data)}` - ); - log.info( - `[CDP LAUNCH] ⚠️ NOTE: Browser launched but NOT added to pool yet` - ); - // This is our own launched browser, not external - use_external_cdp = false; - return { - success: true, - port, - data: response.data, - }; - } - } catch (pollError: any) { - lastError = pollError; - // Log only every 3rd attempt to avoid spam - if (attempt % 3 === 0) { - log.info( - `[CDP LAUNCH] Attempt ${attempt}: Not ready yet (${pollError.code || pollError.message})` - ); + if (resp.status === 200) { + data = resp.data; + break; } - } - - // Wait before next attempt - await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } catch {} + await new Promise((r) => setTimeout(r, 300)); } - // If we get here, browser didn't respond within max wait time - // Kill the orphaned process to avoid resource leak - const proc = cdp_browser_processes.get(port); - if (proc) { + if (!data) { proc.kill(); - cdp_browser_processes.delete(port); + return { + success: false, + error: `Browser not responding on port ${port} after 5s`, + }; } - const totalTime = Date.now() - startTime; - log.warn( - `[CDP LAUNCH] Verification failed after ${totalTime}ms (${attempt} attempts), last error: ${lastError?.code || lastError?.message || 'Unknown'}` - ); - return { - success: false, - error: `Browser launched but not responding on CDP port after ${totalTime}ms`, - }; - } catch (error: any) { - log.error(`[CDP LAUNCH] Failed to launch browser: ${error}`); - return { - success: false, - error: error.message, + + // 5. Add to pool automatically + const newBrowser: CdpBrowser = { + id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + port, + isExternal: false, + name: `Launched Browser (${port})`, + addedAt: Date.now(), }; + cdp_browser_pool.push(newBrowser); + saveCdpPool(); + notifyCdpPoolChanged(); + + log.info( + `[CDP LAUNCH] Success: port=${port}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` + ); + return { success: true, port, data }; + } catch (err: any) { + log.error(`[CDP LAUNCH] Failed: ${err}`); + return { success: false, error: err.message }; } }); @@ -2655,6 +2623,9 @@ async function createWindow() { ensureEigentDirectories(); await seedDefaultSkillsIfEmpty(); + // Load persisted CDP browser pool from disk + loadCdpPool(); + log.info( `[PROJECT BROWSER WINDOW] Creating BrowserWindow which will start Chrome with CDP on port ${browser_port}` ); @@ -2836,6 +2807,9 @@ async function createWindow() { setupExternalLinkHandling(); handleBeforeClose(); + // Start CDP health-check polling (probes every 3s, removes dead browsers) + startCdpHealthCheck(); + // ==================== auto update ==================== update(win); @@ -3490,6 +3464,9 @@ app.on('before-quit', async (event) => { log.info('before-quit'); log.info('quit python_process.pid: ' + python_process?.pid); + // Stop CDP health-check polling + stopCdpHealthCheck(); + // Prevent default quit to ensure cleanup completes event.preventDefault(); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 46fe7920f..910a670d7 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -158,20 +158,23 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('get-project-folder-path', email, projectId), openInIDE: (folderPath: string, ide: string) => ipcRenderer.invoke('open-in-ide', folderPath, ide), - checkCdpPort: (port: number) => ipcRenderer.invoke('check-cdp-port', port), - launchCdpBrowser: (port: number) => - ipcRenderer.invoke('launch-cdp-browser', port), setBrowserPort: (port: number, isExternal?: boolean) => ipcRenderer.invoke('set-browser-port', port, isExternal), getBrowserPort: () => ipcRenderer.invoke('get-browser-port'), 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), + removeCdpBrowser: (browserId: string, closeBrowser?: boolean) => + ipcRenderer.invoke('remove-cdp-browser', browserId, closeBrowser ?? true), + launchCdpBrowser: () => ipcRenderer.invoke('launch-cdp-browser'), + onCdpPoolChanged: (callback: (browsers: any[]) => void) => { + const channel = 'cdp-pool-changed'; + const listener = (_event: any, browsers: any[]) => callback(browsers); + ipcRenderer.on(channel, listener); + return () => { + ipcRenderer.off(channel, listener); + }; + }, // Skills getSkillsDir: () => ipcRenderer.invoke('get-skills-dir'), skillsScan: () => ipcRenderer.invoke('skills-scan'), diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index 6bd0cf3e0..976af1ca8 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -169,42 +169,73 @@ "delete-project": "حذف المشروع", "delete-project-confirmation": "هل أنت متأكد من أنك تريد حذف هذا المشروع وجميع مهامه؟ لا يمكن التراجع عن هذا الإجراء.", "please-select-model": "يرجى اختيار نموذج في الإعدادات > النماذج للمتابعة.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "القدرات" + "cdp-browser-connection": "اتصال متصفح CDP", + "cdp-browser-connection-description": "الاتصال بمتصفح Chrome مع تمكين التصحيح عن بُعد", + "current-port": "المنفذ الحالي:", + "cdp-port-check-description": "التحقق مما إذا كان المتصفح متاحاً على منفذ محدد", + "port-placeholder": "رقم المنفذ (مثال: 9223)", + "checking": "جارٍ التحقق", + "check-port": "تحقق من المنفذ", + "browser-available": "المتصفح متاح", + "browser-not-available": "المتصفح غير متاح", + "cdp-browser-pool": "مجموعة متصفحات CDP", + "cdp-browser-pool-description": "إدارة عدة متصفحات CDP لتنفيذ المهام", + "external": "خارجي", + "launched": "تم التشغيل", + "stopped": "متوقف", + "port": "المنفذ:", + "no-browsers-in-pool": "لا توجد متصفحات في المجموعة", + "add-browsers-hint": "أضف متصفحات باستخدام أداة فحص المنفذ أعلاه", + "invalid-port": "الرجاء إدخال رقم منفذ صالح (1-65535)", + "cdp-port-check-not-available": "فحص منفذ CDP غير متاح", + "failed-to-check-port": "فشل في فحص المنفذ", + "added-browser-to-pool": "تمت إضافة متصفح خارجي على المنفذ {{port}} إلى المجموعة", + "failed-to-add-browser": "فشل في إضافة المتصفح إلى المجموعة", + "launch-not-available": "تشغيل متصفح CDP غير متاح", + "launching-browser": "جارٍ تشغيل المتصفح على المنفذ {{port}}...", + "browser-launched": "تم تشغيل المتصفح بنجاح على المنفذ {{port}}", + "failed-to-launch-browser": "فشل في تشغيل المتصفح", + "browser-removed": "تمت إزالة المتصفح من المجموعة", + "failed-to-remove-browser": "فشل في إزالة المتصفح", + "remove-browser": "إزالة المتصفح", + "remove-browser-confirm": "سيتم فصل وإغلاق المتصفح \"{{name}}\" على المنفذ {{port}}. هل أنت متأكد؟", + "remove": "إزالة", + "browser-opened": "تم فتح المتصفح بنجاح لتسجيل الدخول", + "restart-not-available": "وظيفة إعادة التشغيل غير متاحة", + "browser-found": "تم العثور على متصفح", + "browser-found-description": "يعمل متصفح على المنفذ {{port}}. هل تريد استخدامه لعمليات المتصفح؟", + "yes-use-browser": "نعم، استخدم هذا المتصفح", + "no-browser-found": "لم يتم العثور على متصفح", + "no-browser-found-description": "لا يوجد متصفح يعمل على المنفذ {{port}}. هل تريد تشغيل متصفح Chrome جديد مع تمكين CDP على هذا المنفذ؟", + "yes-launch-browser": "نعم، تشغيل المتصفح", + "for-more-info": "لمزيد من المعلومات، تحقق من", + "capabilities": "القدرات", + "browser-connection": "الاتصال", + "cookies-management": "ملفات تعريف الارتباط", + "restart-to-enable": "إعادة التشغيل للتفعيل", + "restart-to-enable-cookies-tooltip": "أعد تشغيل العميل لتفعيل إدارة ملفات تعريف الارتباط الجديدة", + "open-new-browser": "فتح متصفح فارغ", + "browser-cookies-management": "إدارة ملفات تعريف الارتباط للمتصفح", + "connect-existing-browser": "توصيل متصفح موجود", + "connect-existing-browser-description": "الاتصال بمتصفح يعمل بالفعل مع تمكين CDP على منفذ محدد.", + "enter-port-number": "أدخل رقم المنفذ", + "check-and-connect": "تحقق واتصل", + "port-already-in-use": "هذا المنفذ موجود بالفعل في مجموعة المتصفحات. الرجاء استخدام منفذ مختلف.", + "no-browser-on-port": "لم يتم العثور على متصفح على المنفذ {{port}}. تأكد من أن المتصفح يعمل مع --remote-debugging-port={{port}}.", + "connected-browser": "متصل بالمتصفح على المنفذ {{port}}", + "cookies-added": "تمت إضافة {{count}} من ملفات تعريف الارتباط", + "failed-to-open-browser": "فشل في فتح المتصفح", + "failed-to-load-cookies": "فشل في تحميل ملفات تعريف الارتباط", + "deleted-cookies-for-domain": "تم حذف ملفات تعريف الارتباط لـ {{domain}} وجميع النطاقات الفرعية", + "failed-to-delete-cookies-for-domain": "فشل في حذف ملفات تعريف الارتباط لـ {{domain}}", + "deleted-all-cookies": "تم حذف جميع ملفات تعريف الارتباط", + "failed-to-delete-all-cookies": "فشل في حذف جميع ملفات تعريف الارتباط", + "cookies-updated": "تم تحديث ملفات تعريف الارتباط", + "cookies-updated-message": "تم تحديث ملفات تعريف الارتباط. هل تريد إعادة تشغيل التطبيق لاستخدام ملفات تعريف الارتباط الجديدة؟", + "yes-restart": "نعم، إعادة التشغيل", + "no-add-more": "لا، إضافة المزيد", + "restart-required": "إعادة التشغيل مطلوبة", + "restart-required-message": "أعد تشغيل التطبيق لتفعيل تغييرات نطاق ملفات تعريف الارتباط.", + "restart": "إعادة التشغيل", + "cookie-count": "{{count}} من ملفات تعريف الارتباط" } diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index 6062e6485..12d77361a 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -169,42 +169,73 @@ "delete-project": "Projekt löschen", "delete-project-confirmation": "Sind Sie sicher, dass Sie dieses Projekt und alle seine Aufgaben löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", "please-select-model": "Bitte wählen Sie ein Modell unter Einstellungen > Modelle aus, um fortzufahren.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", + "cdp-browser-connection": "CDP-Browser-Verbindung", + "cdp-browser-connection-description": "Verbindung zu einem Chrome-Browser mit aktiviertem Remote-Debugging herstellen", + "current-port": "Aktueller Port:", + "cdp-port-check-description": "Prüfen, ob ein Browser auf einem bestimmten Port verfügbar ist", + "port-placeholder": "Portnummer (z.B. 9223)", + "checking": "Prüfe", + "check-port": "Port prüfen", + "browser-available": "Browser verfügbar", + "browser-not-available": "Browser nicht verfügbar", + "cdp-browser-pool": "CDP-Browser-Pool", + "cdp-browser-pool-description": "Mehrere CDP-Browser für die Aufgabenausführung verwalten", + "external": "Extern", + "launched": "Gestartet", + "stopped": "Gestoppt", "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "Fähigkeiten" + "no-browsers-in-pool": "Keine Browser im Pool", + "add-browsers-hint": "Browser über das Port-Prüftool oben hinzufügen", + "invalid-port": "Bitte geben Sie eine gültige Portnummer ein (1-65535)", + "cdp-port-check-not-available": "CDP-Portprüfung nicht verfügbar", + "failed-to-check-port": "Portprüfung fehlgeschlagen", + "added-browser-to-pool": "Externer Browser auf Port {{port}} zum Pool hinzugefügt", + "failed-to-add-browser": "Browser konnte nicht zum Pool hinzugefügt werden", + "launch-not-available": "CDP-Browser starten nicht verfügbar", + "launching-browser": "Browser wird auf Port {{port}} gestartet...", + "browser-launched": "Browser erfolgreich auf Port {{port}} gestartet", + "failed-to-launch-browser": "Browser konnte nicht gestartet werden", + "browser-removed": "Browser aus dem Pool entfernt", + "failed-to-remove-browser": "Browser konnte nicht entfernt werden", + "remove-browser": "Browser entfernen", + "remove-browser-confirm": "Dies wird den Browser \"{{name}}\" auf Port {{port}} trennen und schließen. Sind Sie sicher?", + "remove": "Entfernen", + "browser-opened": "Browser erfolgreich zum Anmelden geöffnet", + "restart-not-available": "Neustart-Funktion nicht verfügbar", + "browser-found": "Browser gefunden", + "browser-found-description": "Ein Browser läuft auf Port {{port}}. Möchten Sie ihn für Browser-Operationen verwenden?", + "yes-use-browser": "Ja, diesen Browser verwenden", + "no-browser-found": "Kein Browser gefunden", + "no-browser-found-description": "Kein Browser läuft auf Port {{port}}. Möchten Sie einen neuen Chrome-Browser mit CDP auf diesem Port starten?", + "yes-launch-browser": "Ja, Browser starten", + "for-more-info": "Weitere Informationen finden Sie in unserer", + "capabilities": "Fähigkeiten", + "browser-connection": "Verbindung", + "cookies-management": "Cookies", + "restart-to-enable": "Neustart zum Aktivieren", + "restart-to-enable-cookies-tooltip": "Client neu starten, um die neue Cookie-Verwaltung zu aktivieren", + "open-new-browser": "Leeren Browser öffnen", + "browser-cookies-management": "Browser-Cookie-Verwaltung", + "connect-existing-browser": "Vorhandenen Browser verbinden", + "connect-existing-browser-description": "Verbindung zu einem bereits laufenden Browser mit aktiviertem CDP auf einem bestimmten Port.", + "enter-port-number": "Portnummer eingeben", + "check-and-connect": "Prüfen & Verbinden", + "port-already-in-use": "Dieser Port ist bereits im Browser-Pool. Bitte verwenden Sie einen anderen Port.", + "no-browser-on-port": "Kein Browser auf Port {{port}} gefunden. Stellen Sie sicher, dass ein Browser mit --remote-debugging-port={{port}} läuft.", + "connected-browser": "Verbunden mit Browser auf Port {{port}}", + "cookies-added": "{{count}} Cookie(s) hinzugefügt", + "failed-to-open-browser": "Browser konnte nicht geöffnet werden", + "failed-to-load-cookies": "Cookies konnten nicht geladen werden", + "deleted-cookies-for-domain": "Cookies für {{domain}} und alle Subdomains gelöscht", + "failed-to-delete-cookies-for-domain": "Cookies für {{domain}} konnten nicht gelöscht werden", + "deleted-all-cookies": "Alle Cookies gelöscht", + "failed-to-delete-all-cookies": "Alle Cookies konnten nicht gelöscht werden", + "cookies-updated": "Cookies aktualisiert", + "cookies-updated-message": "Cookies wurden aktualisiert. Möchten Sie die Anwendung neu starten, um die neuen Cookies zu verwenden?", + "yes-restart": "Ja, neu starten", + "no-add-more": "Nein, weitere hinzufügen", + "restart-required": "Neustart erforderlich", + "restart-required-message": "Starten Sie die Anwendung neu, um Ihre Cookie-Domain-Änderungen zu aktivieren.", + "restart": "Neustart", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index 77feb3451..099f843fd 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -199,6 +199,9 @@ "failed-to-launch-browser": "Failed to launch browser", "browser-removed": "Browser removed from pool", "failed-to-remove-browser": "Failed to remove browser", + "remove-browser": "Remove Browser", + "remove-browser-confirm": "This will disconnect and close the browser \"{{name}}\" on port {{port}}. Are you sure?", + "remove": "Remove", "browser-opened": "Browser opened successfully for login", "restart-not-available": "Restart function not available", "browser-found": "Browser Found", @@ -208,5 +211,33 @@ "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", "yes-launch-browser": "Yes, Launch Browser", "for-more-info": "For more information, check out our", - "capabilities": "Capabilities" + "capabilities": "Capabilities", + "browser-connection": "Connection", + "cookies-management": "Cookies", + "restart-to-enable": "Restart to enable", + "restart-to-enable-cookies-tooltip": "Restart the client to enable new cookies management", + "open-new-browser": "Open Blank Browser", + "browser-cookies-management": "Browser Cookies Management", + "connect-existing-browser": "Connect Existing Browser", + "connect-existing-browser-description": "Connect to a browser already running with CDP enabled on a specific port.", + "enter-port-number": "Enter Port Number", + "check-and-connect": "Check & Connect", + "port-already-in-use": "This port is already in the browser pool. Please use a different port.", + "no-browser-on-port": "No browser found on port {{port}}. Make sure a browser is running with --remote-debugging-port={{port}}.", + "connected-browser": "Connected to browser on port {{port}}", + "cookies-added": "Added {{count}} cookie(s)", + "failed-to-open-browser": "Failed to open browser", + "failed-to-load-cookies": "Failed to load cookies", + "deleted-cookies-for-domain": "Deleted cookies for {{domain}} and all subdomains", + "failed-to-delete-cookies-for-domain": "Failed to delete cookies for {{domain}}", + "deleted-all-cookies": "Deleted all cookies", + "failed-to-delete-all-cookies": "Failed to delete all cookies", + "cookies-updated": "Cookies Updated", + "cookies-updated-message": "Cookies have been updated. Would you like to restart the application to use the new cookies?", + "yes-restart": "Yes, Restart", + "no-add-more": "No, Add More", + "restart-required": "Restart Required", + "restart-required-message": "Restart the application to enable your cookie domain changes.", + "restart": "Restart", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index 72759b208..b5ce47b4e 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -169,42 +169,73 @@ "delete-project": "Eliminar Proyecto", "delete-project-confirmation": "¿Estás seguro de que quieres eliminar este proyecto y todas sus tareas? Esta acción no se puede deshacer.", "please-select-model": "Por favor, selecciona un modelo en Configuración > Modelos para continuar.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "Capacidades" + "cdp-browser-connection": "Conexión de navegador CDP", + "cdp-browser-connection-description": "Conectar a un navegador Chrome con depuración remota habilitada", + "current-port": "Puerto actual:", + "cdp-port-check-description": "Verificar si hay un navegador disponible en un puerto específico", + "port-placeholder": "Número de puerto (ej. 9223)", + "checking": "Verificando", + "check-port": "Verificar puerto", + "browser-available": "Navegador disponible", + "browser-not-available": "Navegador no disponible", + "cdp-browser-pool": "Pool de navegadores CDP", + "cdp-browser-pool-description": "Gestionar múltiples navegadores CDP para la ejecución de tareas", + "external": "Externo", + "launched": "Iniciado", + "stopped": "Detenido", + "port": "Puerto:", + "no-browsers-in-pool": "No hay navegadores en el pool", + "add-browsers-hint": "Agregue navegadores usando la herramienta de verificación de puerto arriba", + "invalid-port": "Por favor ingrese un número de puerto válido (1-65535)", + "cdp-port-check-not-available": "Verificación de puerto CDP no disponible", + "failed-to-check-port": "Error al verificar el puerto", + "added-browser-to-pool": "Navegador externo en el puerto {{port}} agregado al pool", + "failed-to-add-browser": "Error al agregar navegador al pool", + "launch-not-available": "Inicio de navegador CDP no disponible", + "launching-browser": "Iniciando navegador en el puerto {{port}}...", + "browser-launched": "Navegador iniciado exitosamente en el puerto {{port}}", + "failed-to-launch-browser": "Error al iniciar el navegador", + "browser-removed": "Navegador eliminado del pool", + "failed-to-remove-browser": "Error al eliminar el navegador", + "remove-browser": "Eliminar navegador", + "remove-browser-confirm": "Esto desconectará y cerrará el navegador \"{{name}}\" en el puerto {{port}}. ¿Está seguro?", + "remove": "Eliminar", + "browser-opened": "Navegador abierto exitosamente para iniciar sesión", + "restart-not-available": "Función de reinicio no disponible", + "browser-found": "Navegador encontrado", + "browser-found-description": "Un navegador está ejecutándose en el puerto {{port}}. ¿Desea usarlo para operaciones del navegador?", + "yes-use-browser": "Sí, usar este navegador", + "no-browser-found": "Navegador no encontrado", + "no-browser-found-description": "No hay navegador ejecutándose en el puerto {{port}}. ¿Desea iniciar un nuevo navegador Chrome con CDP habilitado en este puerto?", + "yes-launch-browser": "Sí, iniciar navegador", + "for-more-info": "Para más información, consulte nuestra", + "capabilities": "Capacidades", + "browser-connection": "Conexión", + "cookies-management": "Cookies", + "restart-to-enable": "Reiniciar para habilitar", + "restart-to-enable-cookies-tooltip": "Reiniciar el cliente para habilitar la nueva gestión de cookies", + "open-new-browser": "Abrir navegador vacío", + "browser-cookies-management": "Gestión de cookies del navegador", + "connect-existing-browser": "Conectar navegador existente", + "connect-existing-browser-description": "Conectar a un navegador que ya está ejecutándose con CDP habilitado en un puerto específico.", + "enter-port-number": "Ingrese el número de puerto", + "check-and-connect": "Verificar y conectar", + "port-already-in-use": "Este puerto ya está en el pool de navegadores. Por favor use un puerto diferente.", + "no-browser-on-port": "No se encontró navegador en el puerto {{port}}. Asegúrese de que un navegador esté ejecutándose con --remote-debugging-port={{port}}.", + "connected-browser": "Conectado al navegador en el puerto {{port}}", + "cookies-added": "{{count}} cookie(s) agregadas", + "failed-to-open-browser": "Error al abrir el navegador", + "failed-to-load-cookies": "Error al cargar cookies", + "deleted-cookies-for-domain": "Cookies eliminadas para {{domain}} y todos los subdominios", + "failed-to-delete-cookies-for-domain": "Error al eliminar cookies para {{domain}}", + "deleted-all-cookies": "Todas las cookies eliminadas", + "failed-to-delete-all-cookies": "Error al eliminar todas las cookies", + "cookies-updated": "Cookies actualizadas", + "cookies-updated-message": "Las cookies han sido actualizadas. ¿Desea reiniciar la aplicación para usar las nuevas cookies?", + "yes-restart": "Sí, reiniciar", + "no-add-more": "No, agregar más", + "restart-required": "Reinicio requerido", + "restart-required-message": "Reinicie la aplicación para habilitar los cambios de dominio de cookies.", + "restart": "Reiniciar", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index a0eaba079..5ffb3c5d8 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -169,42 +169,73 @@ "delete-project": "Supprimer le Projet", "delete-project-confirmation": "Êtes-vous sûr de vouloir supprimer ce projet et toutes ses tâches ? Cette action ne peut pas être annulée.", "please-select-model": "Veuillez sélectionner un modèle dans Paramètres > Modèles pour continuer.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "Capacités" + "cdp-browser-connection": "Connexion navigateur CDP", + "cdp-browser-connection-description": "Se connecter à un navigateur Chrome avec le débogage à distance activé", + "current-port": "Port actuel :", + "cdp-port-check-description": "Vérifier si un navigateur est disponible sur un port spécifique", + "port-placeholder": "Numéro de port (ex. 9223)", + "checking": "Vérification", + "check-port": "Vérifier le port", + "browser-available": "Navigateur disponible", + "browser-not-available": "Navigateur non disponible", + "cdp-browser-pool": "Pool de navigateurs CDP", + "cdp-browser-pool-description": "Gérer plusieurs navigateurs CDP pour l'exécution des tâches", + "external": "Externe", + "launched": "Lancé", + "stopped": "Arrêté", + "port": "Port :", + "no-browsers-in-pool": "Aucun navigateur dans le pool", + "add-browsers-hint": "Ajoutez des navigateurs en utilisant l'outil de vérification de port ci-dessus", + "invalid-port": "Veuillez entrer un numéro de port valide (1-65535)", + "cdp-port-check-not-available": "Vérification du port CDP non disponible", + "failed-to-check-port": "Échec de la vérification du port", + "added-browser-to-pool": "Navigateur externe sur le port {{port}} ajouté au pool", + "failed-to-add-browser": "Échec de l'ajout du navigateur au pool", + "launch-not-available": "Lancement du navigateur CDP non disponible", + "launching-browser": "Lancement du navigateur sur le port {{port}}...", + "browser-launched": "Navigateur lancé avec succès sur le port {{port}}", + "failed-to-launch-browser": "Échec du lancement du navigateur", + "browser-removed": "Navigateur supprimé du pool", + "failed-to-remove-browser": "Échec de la suppression du navigateur", + "remove-browser": "Supprimer le navigateur", + "remove-browser-confirm": "Cela déconnectera et fermera le navigateur \"{{name}}\" sur le port {{port}}. Êtes-vous sûr ?", + "remove": "Supprimer", + "browser-opened": "Navigateur ouvert avec succès pour la connexion", + "restart-not-available": "Fonction de redémarrage non disponible", + "browser-found": "Navigateur trouvé", + "browser-found-description": "Un navigateur est en cours d'exécution sur le port {{port}}. Souhaitez-vous l'utiliser pour les opérations du navigateur ?", + "yes-use-browser": "Oui, utiliser ce navigateur", + "no-browser-found": "Aucun navigateur trouvé", + "no-browser-found-description": "Aucun navigateur n'est en cours d'exécution sur le port {{port}}. Souhaitez-vous lancer un nouveau navigateur Chrome avec CDP activé sur ce port ?", + "yes-launch-browser": "Oui, lancer le navigateur", + "for-more-info": "Pour plus d'informations, consultez notre", + "capabilities": "Capacités", + "browser-connection": "Connexion", + "cookies-management": "Cookies", + "restart-to-enable": "Redémarrer pour activer", + "restart-to-enable-cookies-tooltip": "Redémarrer le client pour activer la nouvelle gestion des cookies", + "open-new-browser": "Ouvrir un navigateur vide", + "browser-cookies-management": "Gestion des cookies du navigateur", + "connect-existing-browser": "Connecter un navigateur existant", + "connect-existing-browser-description": "Se connecter à un navigateur déjà en cours d'exécution avec CDP activé sur un port spécifique.", + "enter-port-number": "Entrez le numéro de port", + "check-and-connect": "Vérifier et connecter", + "port-already-in-use": "Ce port est déjà dans le pool de navigateurs. Veuillez utiliser un port différent.", + "no-browser-on-port": "Aucun navigateur trouvé sur le port {{port}}. Assurez-vous qu'un navigateur est en cours d'exécution avec --remote-debugging-port={{port}}.", + "connected-browser": "Connecté au navigateur sur le port {{port}}", + "cookies-added": "{{count}} cookie(s) ajouté(s)", + "failed-to-open-browser": "Échec de l'ouverture du navigateur", + "failed-to-load-cookies": "Échec du chargement des cookies", + "deleted-cookies-for-domain": "Cookies supprimés pour {{domain}} et tous les sous-domaines", + "failed-to-delete-cookies-for-domain": "Échec de la suppression des cookies pour {{domain}}", + "deleted-all-cookies": "Tous les cookies supprimés", + "failed-to-delete-all-cookies": "Échec de la suppression de tous les cookies", + "cookies-updated": "Cookies mis à jour", + "cookies-updated-message": "Les cookies ont été mis à jour. Souhaitez-vous redémarrer l'application pour utiliser les nouveaux cookies ?", + "yes-restart": "Oui, redémarrer", + "no-add-more": "Non, en ajouter plus", + "restart-required": "Redémarrage requis", + "restart-required-message": "Redémarrez l'application pour activer vos modifications de domaine de cookies.", + "restart": "Redémarrer", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 9a7c129fd..0f4447e51 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -169,42 +169,73 @@ "delete-project": "Elimina Progetto", "delete-project-confirmation": "Sei sicuro di voler eliminare questo progetto e tutte le sue attività? Questa azione non può essere annullata.", "please-select-model": "Seleziona un modello in Impostazioni > Modelli per continuare.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "Capacità" + "cdp-browser-connection": "Connessione browser CDP", + "cdp-browser-connection-description": "Connetti a un browser Chrome con il debug remoto abilitato", + "current-port": "Porta attuale:", + "cdp-port-check-description": "Verifica se un browser è disponibile su una porta specifica", + "port-placeholder": "Numero di porta (es. 9223)", + "checking": "Verifica in corso", + "check-port": "Verifica porta", + "browser-available": "Browser disponibile", + "browser-not-available": "Browser non disponibile", + "cdp-browser-pool": "Pool browser CDP", + "cdp-browser-pool-description": "Gestisci più browser CDP per l'esecuzione delle attività", + "external": "Esterno", + "launched": "Avviato", + "stopped": "Fermato", + "port": "Porta:", + "no-browsers-in-pool": "Nessun browser nel pool", + "add-browsers-hint": "Aggiungi browser utilizzando lo strumento di verifica porta sopra", + "invalid-port": "Inserisci un numero di porta valido (1-65535)", + "cdp-port-check-not-available": "Verifica porta CDP non disponibile", + "failed-to-check-port": "Verifica porta fallita", + "added-browser-to-pool": "Browser esterno sulla porta {{port}} aggiunto al pool", + "failed-to-add-browser": "Impossibile aggiungere il browser al pool", + "launch-not-available": "Avvio browser CDP non disponibile", + "launching-browser": "Avvio del browser sulla porta {{port}}...", + "browser-launched": "Browser avviato con successo sulla porta {{port}}", + "failed-to-launch-browser": "Impossibile avviare il browser", + "browser-removed": "Browser rimosso dal pool", + "failed-to-remove-browser": "Impossibile rimuovere il browser", + "remove-browser": "Rimuovi browser", + "remove-browser-confirm": "Questo disconnetterà e chiuderà il browser \"{{name}}\" sulla porta {{port}}. Sei sicuro?", + "remove": "Rimuovi", + "browser-opened": "Browser aperto con successo per l'accesso", + "restart-not-available": "Funzione di riavvio non disponibile", + "browser-found": "Browser trovato", + "browser-found-description": "Un browser è in esecuzione sulla porta {{port}}. Vuoi utilizzarlo per le operazioni del browser?", + "yes-use-browser": "Sì, usa questo browser", + "no-browser-found": "Nessun browser trovato", + "no-browser-found-description": "Nessun browser in esecuzione sulla porta {{port}}. Vuoi avviare un nuovo browser Chrome con CDP abilitato su questa porta?", + "yes-launch-browser": "Sì, avvia browser", + "for-more-info": "Per maggiori informazioni, consulta la nostra", + "capabilities": "Capacità", + "browser-connection": "Connessione", + "cookies-management": "Cookie", + "restart-to-enable": "Riavvia per abilitare", + "restart-to-enable-cookies-tooltip": "Riavvia il client per abilitare la nuova gestione dei cookie", + "open-new-browser": "Apri browser vuoto", + "browser-cookies-management": "Gestione cookie del browser", + "connect-existing-browser": "Connetti browser esistente", + "connect-existing-browser-description": "Connetti a un browser già in esecuzione con CDP abilitato su una porta specifica.", + "enter-port-number": "Inserisci il numero di porta", + "check-and-connect": "Verifica e connetti", + "port-already-in-use": "Questa porta è già nel pool dei browser. Usa una porta diversa.", + "no-browser-on-port": "Nessun browser trovato sulla porta {{port}}. Assicurati che un browser sia in esecuzione con --remote-debugging-port={{port}}.", + "connected-browser": "Connesso al browser sulla porta {{port}}", + "cookies-added": "{{count}} cookie aggiunti", + "failed-to-open-browser": "Impossibile aprire il browser", + "failed-to-load-cookies": "Impossibile caricare i cookie", + "deleted-cookies-for-domain": "Cookie eliminati per {{domain}} e tutti i sottodomini", + "failed-to-delete-cookies-for-domain": "Impossibile eliminare i cookie per {{domain}}", + "deleted-all-cookies": "Tutti i cookie eliminati", + "failed-to-delete-all-cookies": "Impossibile eliminare tutti i cookie", + "cookies-updated": "Cookie aggiornati", + "cookies-updated-message": "I cookie sono stati aggiornati. Vuoi riavviare l'applicazione per utilizzare i nuovi cookie?", + "yes-restart": "Sì, riavvia", + "no-add-more": "No, aggiungi altri", + "restart-required": "Riavvio necessario", + "restart-required-message": "Riavvia l'applicazione per abilitare le modifiche al dominio dei cookie.", + "restart": "Riavvia", + "cookie-count": "{{count}} Cookie" } diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index dd45328b8..d384a3e1c 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -169,42 +169,73 @@ "delete-project": "プロジェクトを削除", "delete-project-confirmation": "このプロジェクトとそのすべてのタスクを削除してもよろしいですか?この操作は元に戻せません。", "please-select-model": "続行するには、設定 > モデルでモデルを選択してください。", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "機能" + "cdp-browser-connection": "CDP ブラウザ接続", + "cdp-browser-connection-description": "リモートデバッグが有効な Chrome ブラウザに接続", + "current-port": "現在のポート:", + "cdp-port-check-description": "特定のポートでブラウザが利用可能か確認", + "port-placeholder": "ポート番号(例:9223)", + "checking": "確認中", + "check-port": "ポートを確認", + "browser-available": "ブラウザ利用可能", + "browser-not-available": "ブラウザ利用不可", + "cdp-browser-pool": "CDP ブラウザプール", + "cdp-browser-pool-description": "タスク実行用の複数の CDP ブラウザを管理", + "external": "外部", + "launched": "起動済み", + "stopped": "停止済み", + "port": "ポート:", + "no-browsers-in-pool": "プール内にブラウザがありません", + "add-browsers-hint": "上のポート確認ツールを使用してブラウザを追加してください", + "invalid-port": "有効なポート番号を入力してください(1-65535)", + "cdp-port-check-not-available": "CDP ポート確認は利用できません", + "failed-to-check-port": "ポートの確認に失敗しました", + "added-browser-to-pool": "ポート {{port}} の外部ブラウザをプールに追加しました", + "failed-to-add-browser": "ブラウザをプールに追加できませんでした", + "launch-not-available": "CDP ブラウザの起動は利用できません", + "launching-browser": "ポート {{port}} でブラウザを起動中...", + "browser-launched": "ポート {{port}} でブラウザが正常に起動しました", + "failed-to-launch-browser": "ブラウザの起動に失敗しました", + "browser-removed": "ブラウザがプールから削除されました", + "failed-to-remove-browser": "ブラウザの削除に失敗しました", + "remove-browser": "ブラウザを削除", + "remove-browser-confirm": "ポート {{port}} のブラウザ「{{name}}」を切断して閉じます。よろしいですか?", + "remove": "削除", + "browser-opened": "ログイン用にブラウザが正常に開きました", + "restart-not-available": "再起動機能は利用できません", + "browser-found": "ブラウザが見つかりました", + "browser-found-description": "ポート {{port}} でブラウザが実行中です。ブラウザ操作に使用しますか?", + "yes-use-browser": "はい、このブラウザを使用", + "no-browser-found": "ブラウザが見つかりません", + "no-browser-found-description": "ポート {{port}} でブラウザが実行されていません。このポートで CDP 対応の新しい Chrome ブラウザを起動しますか?", + "yes-launch-browser": "はい、ブラウザを起動", + "for-more-info": "詳細については、こちらをご覧ください", + "capabilities": "機能", + "browser-connection": "接続", + "cookies-management": "Cookie", + "restart-to-enable": "再起動して有効化", + "restart-to-enable-cookies-tooltip": "クライアントを再起動して新しい Cookie 管理を有効にする", + "open-new-browser": "空白ブラウザを開く", + "browser-cookies-management": "ブラウザ Cookie 管理", + "connect-existing-browser": "既存のブラウザに接続", + "connect-existing-browser-description": "特定のポートで CDP が有効な状態で実行中のブラウザに接続します。", + "enter-port-number": "ポート番号を入力", + "check-and-connect": "確認して接続", + "port-already-in-use": "このポートは既にブラウザプールにあります。別のポートを使用してください。", + "no-browser-on-port": "ポート {{port}} にブラウザが見つかりません。--remote-debugging-port={{port}} でブラウザが実行されていることを確認してください。", + "connected-browser": "ポート {{port}} のブラウザに接続しました", + "cookies-added": "{{count}} 個の Cookie を追加しました", + "failed-to-open-browser": "ブラウザを開けませんでした", + "failed-to-load-cookies": "Cookie の読み込みに失敗しました", + "deleted-cookies-for-domain": "{{domain}} とすべてのサブドメインの Cookie を削除しました", + "failed-to-delete-cookies-for-domain": "{{domain}} の Cookie の削除に失敗しました", + "deleted-all-cookies": "すべての Cookie を削除しました", + "failed-to-delete-all-cookies": "すべての Cookie の削除に失敗しました", + "cookies-updated": "Cookie が更新されました", + "cookies-updated-message": "Cookie が更新されました。新しい Cookie を使用するためにアプリケーションを再起動しますか?", + "yes-restart": "はい、再起動", + "no-add-more": "いいえ、追加を続ける", + "restart-required": "再起動が必要です", + "restart-required-message": "Cookie ドメインの変更を有効にするためにアプリケーションを再起動してください。", + "restart": "再起動", + "cookie-count": "{{count}} 個の Cookie" } diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index 6c3296533..5bf9a77d1 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -169,42 +169,73 @@ "delete-project": "프로젝트 삭제", "delete-project-confirmation": "이 프로젝트와 모든 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "please-select-model": "계속하려면 설정 > 모델에서 모델을 선택하세요.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "기능" + "cdp-browser-connection": "CDP 브라우저 연결", + "cdp-browser-connection-description": "원격 디버깅이 활성화된 Chrome 브라우저에 연결", + "current-port": "현재 포트:", + "cdp-port-check-description": "특정 포트에서 브라우저를 사용할 수 있는지 확인", + "port-placeholder": "포트 번호 (예: 9223)", + "checking": "확인 중", + "check-port": "포트 확인", + "browser-available": "브라우저 사용 가능", + "browser-not-available": "브라우저 사용 불가", + "cdp-browser-pool": "CDP 브라우저 풀", + "cdp-browser-pool-description": "작업 실행을 위한 여러 CDP 브라우저 관리", + "external": "외부", + "launched": "시작됨", + "stopped": "중지됨", + "port": "포트:", + "no-browsers-in-pool": "풀에 브라우저가 없습니다", + "add-browsers-hint": "위의 포트 확인 도구를 사용하여 브라우저를 추가하세요", + "invalid-port": "유효한 포트 번호를 입력하세요 (1-65535)", + "cdp-port-check-not-available": "CDP 포트 확인을 사용할 수 없습니다", + "failed-to-check-port": "포트 확인에 실패했습니다", + "added-browser-to-pool": "포트 {{port}}의 외부 브라우저가 풀에 추가되었습니다", + "failed-to-add-browser": "브라우저를 풀에 추가하지 못했습니다", + "launch-not-available": "CDP 브라우저 시작을 사용할 수 없습니다", + "launching-browser": "포트 {{port}}에서 브라우저 시작 중...", + "browser-launched": "포트 {{port}}에서 브라우저가 성공적으로 시작되었습니다", + "failed-to-launch-browser": "브라우저 시작에 실패했습니다", + "browser-removed": "브라우저가 풀에서 제거되었습니다", + "failed-to-remove-browser": "브라우저 제거에 실패했습니다", + "remove-browser": "브라우저 제거", + "remove-browser-confirm": "포트 {{port}}의 브라우저 \"{{name}}\"을(를) 연결 해제하고 닫습니다. 계속하시겠습니까?", + "remove": "제거", + "browser-opened": "로그인을 위해 브라우저가 성공적으로 열렸습니다", + "restart-not-available": "재시작 기능을 사용할 수 없습니다", + "browser-found": "브라우저 발견", + "browser-found-description": "포트 {{port}}에서 브라우저가 실행 중입니다. 브라우저 작업에 사용하시겠습니까?", + "yes-use-browser": "예, 이 브라우저 사용", + "no-browser-found": "브라우저를 찾을 수 없습니다", + "no-browser-found-description": "포트 {{port}}에서 실행 중인 브라우저가 없습니다. 이 포트에서 CDP가 활성화된 새 Chrome 브라우저를 시작하시겠습니까?", + "yes-launch-browser": "예, 브라우저 시작", + "for-more-info": "자세한 내용은 다음을 확인하세요", + "capabilities": "기능", + "browser-connection": "연결", + "cookies-management": "쿠키", + "restart-to-enable": "재시작하여 활성화", + "restart-to-enable-cookies-tooltip": "새로운 쿠키 관리를 활성화하려면 클라이언트를 재시작하세요", + "open-new-browser": "빈 브라우저 열기", + "browser-cookies-management": "브라우저 쿠키 관리", + "connect-existing-browser": "기존 브라우저 연결", + "connect-existing-browser-description": "특정 포트에서 CDP가 활성화된 상태로 실행 중인 브라우저에 연결합니다.", + "enter-port-number": "포트 번호 입력", + "check-and-connect": "확인 및 연결", + "port-already-in-use": "이 포트는 이미 브라우저 풀에 있습니다. 다른 포트를 사용하세요.", + "no-browser-on-port": "포트 {{port}}에서 브라우저를 찾을 수 없습니다. --remote-debugging-port={{port}}로 브라우저가 실행 중인지 확인하세요.", + "connected-browser": "포트 {{port}}의 브라우저에 연결되었습니다", + "cookies-added": "{{count}}개의 쿠키가 추가되었습니다", + "failed-to-open-browser": "브라우저를 열 수 없습니다", + "failed-to-load-cookies": "쿠키를 불러오지 못했습니다", + "deleted-cookies-for-domain": "{{domain}} 및 모든 하위 도메인의 쿠키가 삭제되었습니다", + "failed-to-delete-cookies-for-domain": "{{domain}}의 쿠키 삭제에 실패했습니다", + "deleted-all-cookies": "모든 쿠키가 삭제되었습니다", + "failed-to-delete-all-cookies": "모든 쿠키 삭제에 실패했습니다", + "cookies-updated": "쿠키 업데이트됨", + "cookies-updated-message": "쿠키가 업데이트되었습니다. 새 쿠키를 사용하기 위해 애플리케이션을 재시작하시겠습니까?", + "yes-restart": "예, 재시작", + "no-add-more": "아니요, 더 추가", + "restart-required": "재시작 필요", + "restart-required-message": "쿠키 도메인 변경을 활성화하려면 애플리케이션을 재시작하세요.", + "restart": "재시작", + "cookie-count": "{{count}}개 쿠키" } diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index af3e53fd9..15c1f4575 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -169,42 +169,73 @@ "delete-project": "Удалить проект", "delete-project-confirmation": "Вы уверены, что хотите удалить этот проект и все его задачи? Это действие нельзя отменить.", "please-select-model": "Пожалуйста, выберите модель в Настройки > Модели, чтобы продолжить.", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "Возможности" + "cdp-browser-connection": "Подключение CDP-браузера", + "cdp-browser-connection-description": "Подключиться к браузеру Chrome с включённой удалённой отладкой", + "current-port": "Текущий порт:", + "cdp-port-check-description": "Проверить, доступен ли браузер на определённом порту", + "port-placeholder": "Номер порта (напр. 9223)", + "checking": "Проверка", + "check-port": "Проверить порт", + "browser-available": "Браузер доступен", + "browser-not-available": "Браузер недоступен", + "cdp-browser-pool": "Пул CDP-браузеров", + "cdp-browser-pool-description": "Управление несколькими CDP-браузерами для выполнения задач", + "external": "Внешний", + "launched": "Запущен", + "stopped": "Остановлен", + "port": "Порт:", + "no-browsers-in-pool": "Нет браузеров в пуле", + "add-browsers-hint": "Добавьте браузеры с помощью инструмента проверки порта выше", + "invalid-port": "Введите действительный номер порта (1-65535)", + "cdp-port-check-not-available": "Проверка порта CDP недоступна", + "failed-to-check-port": "Не удалось проверить порт", + "added-browser-to-pool": "Внешний браузер на порту {{port}} добавлен в пул", + "failed-to-add-browser": "Не удалось добавить браузер в пул", + "launch-not-available": "Запуск CDP-браузера недоступен", + "launching-browser": "Запуск браузера на порту {{port}}...", + "browser-launched": "Браузер успешно запущен на порту {{port}}", + "failed-to-launch-browser": "Не удалось запустить браузер", + "browser-removed": "Браузер удалён из пула", + "failed-to-remove-browser": "Не удалось удалить браузер", + "remove-browser": "Удалить браузер", + "remove-browser-confirm": "Это отключит и закроет браузер \"{{name}}\" на порту {{port}}. Вы уверены?", + "remove": "Удалить", + "browser-opened": "Браузер успешно открыт для входа", + "restart-not-available": "Функция перезапуска недоступна", + "browser-found": "Браузер найден", + "browser-found-description": "Браузер работает на порту {{port}}. Хотите использовать его для операций браузера?", + "yes-use-browser": "Да, использовать этот браузер", + "no-browser-found": "Браузер не найден", + "no-browser-found-description": "На порту {{port}} не запущен браузер. Хотите запустить новый браузер Chrome с CDP на этом порту?", + "yes-launch-browser": "Да, запустить браузер", + "for-more-info": "Для получения дополнительной информации ознакомьтесь с нашей", + "capabilities": "Возможности", + "browser-connection": "Подключение", + "cookies-management": "Файлы cookie", + "restart-to-enable": "Перезапустить для активации", + "restart-to-enable-cookies-tooltip": "Перезапустите клиент для активации нового управления cookie", + "open-new-browser": "Открыть пустой браузер", + "browser-cookies-management": "Управление cookie-файлами браузера", + "connect-existing-browser": "Подключить существующий браузер", + "connect-existing-browser-description": "Подключиться к уже запущенному браузеру с включённым CDP на определённом порту.", + "enter-port-number": "Введите номер порта", + "check-and-connect": "Проверить и подключить", + "port-already-in-use": "Этот порт уже используется в пуле браузеров. Используйте другой порт.", + "no-browser-on-port": "Браузер не найден на порту {{port}}. Убедитесь, что браузер запущен с --remote-debugging-port={{port}}.", + "connected-browser": "Подключено к браузеру на порту {{port}}", + "cookies-added": "Добавлено {{count}} cookie-файл(ов)", + "failed-to-open-browser": "Не удалось открыть браузер", + "failed-to-load-cookies": "Не удалось загрузить cookie-файлы", + "deleted-cookies-for-domain": "Cookie-файлы для {{domain}} и всех поддоменов удалены", + "failed-to-delete-cookies-for-domain": "Не удалось удалить cookie-файлы для {{domain}}", + "deleted-all-cookies": "Все cookie-файлы удалены", + "failed-to-delete-all-cookies": "Не удалось удалить все cookie-файлы", + "cookies-updated": "Cookie-файлы обновлены", + "cookies-updated-message": "Cookie-файлы были обновлены. Хотите перезапустить приложение, чтобы использовать новые cookie-файлы?", + "yes-restart": "Да, перезапустить", + "no-add-more": "Нет, добавить ещё", + "restart-required": "Требуется перезапуск", + "restart-required-message": "Перезапустите приложение, чтобы активировать изменения домена cookie-файлов.", + "restart": "Перезапустить", + "cookie-count": "{{count}} Cookie" } diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index 24f37566b..809e13c49 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -171,42 +171,73 @@ "delete-project": "删除项目", "delete-project-confirmation": "您确定要删除此项目及其所有任务吗?此操作无法撤销。", "please-select-model": "请在设置 > 模型中选择一个模型以继续。", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "能力" + "cdp-browser-connection": "CDP 浏览器连接", + "cdp-browser-connection-description": "连接到启用了远程调试的 Chrome 浏览器", + "current-port": "当前端口:", + "cdp-port-check-description": "检查指定端口上是否有可用的浏览器", + "port-placeholder": "端口号(例如 9223)", + "checking": "检查中", + "check-port": "检查端口", + "browser-available": "浏览器可用", + "browser-not-available": "浏览器不可用", + "cdp-browser-pool": "CDP 浏览器池", + "cdp-browser-pool-description": "管理多个 CDP 浏览器以执行任务", + "external": "外部", + "launched": "已启动", + "stopped": "已停止", + "port": "端口:", + "no-browsers-in-pool": "浏览器池中没有浏览器", + "add-browsers-hint": "使用上方的端口检查工具添加浏览器", + "invalid-port": "请输入有效的端口号(1-65535)", + "cdp-port-check-not-available": "CDP 端口检查不可用", + "failed-to-check-port": "端口检查失败", + "added-browser-to-pool": "已将端口 {{port}} 上的外部浏览器添加到池中", + "failed-to-add-browser": "添加浏览器到池失败", + "launch-not-available": "启动 CDP 浏览器不可用", + "launching-browser": "正在启动端口 {{port}} 上的浏览器...", + "browser-launched": "浏览器已在端口 {{port}} 上成功启动", + "failed-to-launch-browser": "启动浏览器失败", + "browser-removed": "浏览器已从池中移除", + "failed-to-remove-browser": "移除浏览器失败", + "remove-browser": "移除浏览器", + "remove-browser-confirm": "将断开并关闭端口 {{port}} 上的浏览器 \"{{name}}\",确定吗?", + "remove": "移除", + "browser-opened": "浏览器已成功打开以登录", + "restart-not-available": "重启功能不可用", + "browser-found": "找到浏览器", + "browser-found-description": "端口 {{port}} 上有浏览器正在运行。您要使用它进行浏览器操作吗?", + "yes-use-browser": "是的,使用此浏览器", + "no-browser-found": "未找到浏览器", + "no-browser-found-description": "端口 {{port}} 上没有浏览器运行。您要在此端口上启动启用 CDP 的新 Chrome 浏览器吗?", + "yes-launch-browser": "是的,启动浏览器", + "for-more-info": "如需更多信息,请查看我们的", + "capabilities": "能力", + "browser-connection": "连接", + "cookies-management": "Cookie 管理", + "restart-to-enable": "重启以启用", + "restart-to-enable-cookies-tooltip": "重启客户端以启用新的 Cookie 管理", + "open-new-browser": "打开空白浏览器", + "browser-cookies-management": "浏览器 Cookie 管理", + "connect-existing-browser": "连接现有浏览器", + "connect-existing-browser-description": "连接到已启用 CDP 并在指定端口运行的浏览器。", + "enter-port-number": "输入端口号", + "check-and-connect": "检查并连接", + "port-already-in-use": "该端口已在浏览器池中。请使用其他端口。", + "no-browser-on-port": "在端口 {{port}} 上未找到浏览器。请确保浏览器正在使用 --remote-debugging-port={{port}} 运行。", + "connected-browser": "已连接到端口 {{port}} 上的浏览器", + "cookies-added": "已添加 {{count}} 个 Cookie", + "failed-to-open-browser": "打开浏览器失败", + "failed-to-load-cookies": "加载 Cookie 失败", + "deleted-cookies-for-domain": "已删除 {{domain}} 及所有子域名的 Cookie", + "failed-to-delete-cookies-for-domain": "删除 {{domain}} 的 Cookie 失败", + "deleted-all-cookies": "已删除所有 Cookie", + "failed-to-delete-all-cookies": "删除所有 Cookie 失败", + "cookies-updated": "Cookie 已更新", + "cookies-updated-message": "Cookie 已更新。您要重启应用程序以使用新的 Cookie 吗?", + "yes-restart": "是的,重启", + "no-add-more": "不,继续添加", + "restart-required": "需要重启", + "restart-required-message": "重启应用程序以启用您的 Cookie 域名更改。", + "restart": "重启", + "cookie-count": "{{count}} 个 Cookie" } diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 9ec5ba9d6..7eddad72c 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -171,42 +171,73 @@ "delete-project": "刪除專案", "delete-project-confirmation": "您確定要刪除此專案及其所有任務嗎?此操作無法撤銷。", "please-select-model": "請在設定 > 模型中選擇一個模型以繼續。", - "cdp-browser-connection": "CDP Browser Connection", - "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", - "current-port": "Current Port:", - "cdp-port-check-description": "Check if a browser is available on a specific port", - "port-placeholder": "Port number (e.g., 9222)", - "checking": "Checking", - "check-port": "Check Port", - "browser-available": "Browser Available", - "browser-not-available": "Browser Not Available", - "cdp-browser-pool": "CDP Browser Pool", - "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", - "external": "External", - "launched": "Launched", - "stopped": "Stopped", - "port": "Port:", - "no-browsers-in-pool": "No browsers in pool", - "add-browsers-hint": "Add browsers using the check port tool above", - "invalid-port": "Please enter a valid port number (1-65535)", - "cdp-port-check-not-available": "CDP port check not available", - "failed-to-check-port": "Failed to check port", - "added-browser-to-pool": "Added external browser on port {{port}} to pool", - "failed-to-add-browser": "Failed to add browser to pool", - "launch-not-available": "Launch CDP browser not available", - "launching-browser": "Launching browser on port {{port}}...", - "browser-launched": "Browser launched successfully on port {{port}}", - "failed-to-launch-browser": "Failed to launch browser", - "browser-removed": "Browser removed from pool", - "failed-to-remove-browser": "Failed to remove browser", - "browser-opened": "Browser opened successfully for login", - "restart-not-available": "Restart function not available", - "browser-found": "Browser Found", - "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", - "yes-use-browser": "Yes, Use This Browser", - "no-browser-found": "No Browser Found", - "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", - "yes-launch-browser": "Yes, Launch Browser", - "for-more-info": "For more information, check out our", - "capabilities": "能力" + "cdp-browser-connection": "CDP 瀏覽器連接", + "cdp-browser-connection-description": "連接到啟用了遠端偵錯的 Chrome 瀏覽器", + "current-port": "目前連接埠:", + "cdp-port-check-description": "檢查指定連接埠上是否有可用的瀏覽器", + "port-placeholder": "連接埠號(例如 9223)", + "checking": "檢查中", + "check-port": "檢查連接埠", + "browser-available": "瀏覽器可用", + "browser-not-available": "瀏覽器不可用", + "cdp-browser-pool": "CDP 瀏覽器池", + "cdp-browser-pool-description": "管理多個 CDP 瀏覽器以執行任務", + "external": "外部", + "launched": "已啟動", + "stopped": "已停止", + "port": "連接埠:", + "no-browsers-in-pool": "瀏覽器池中沒有瀏覽器", + "add-browsers-hint": "使用上方的連接埠檢查工具新增瀏覽器", + "invalid-port": "請輸入有效的連接埠號(1-65535)", + "cdp-port-check-not-available": "CDP 連接埠檢查不可用", + "failed-to-check-port": "連接埠檢查失敗", + "added-browser-to-pool": "已將連接埠 {{port}} 上的外部瀏覽器新增到池中", + "failed-to-add-browser": "新增瀏覽器到池失敗", + "launch-not-available": "啟動 CDP 瀏覽器不可用", + "launching-browser": "正在啟動連接埠 {{port}} 上的瀏覽器...", + "browser-launched": "瀏覽器已在連接埠 {{port}} 上成功啟動", + "failed-to-launch-browser": "啟動瀏覽器失敗", + "browser-removed": "瀏覽器已從池中移除", + "failed-to-remove-browser": "移除瀏覽器失敗", + "remove-browser": "移除瀏覽器", + "remove-browser-confirm": "將斷開並關閉連接埠 {{port}} 上的瀏覽器 \"{{name}}\",確定嗎?", + "remove": "移除", + "browser-opened": "瀏覽器已成功開啟以登入", + "restart-not-available": "重新啟動功能不可用", + "browser-found": "找到瀏覽器", + "browser-found-description": "連接埠 {{port}} 上有瀏覽器正在執行。您要使用它進行瀏覽器操作嗎?", + "yes-use-browser": "是的,使用此瀏覽器", + "no-browser-found": "未找到瀏覽器", + "no-browser-found-description": "連接埠 {{port}} 上沒有瀏覽器執行。您要在此連接埠上啟動啟用 CDP 的新 Chrome 瀏覽器嗎?", + "yes-launch-browser": "是的,啟動瀏覽器", + "for-more-info": "如需更多資訊,請查看我們的", + "capabilities": "能力", + "browser-connection": "連接", + "cookies-management": "Cookie 管理", + "restart-to-enable": "重啟以啟用", + "restart-to-enable-cookies-tooltip": "重啟客戶端以啟用新的 Cookie 管理", + "open-new-browser": "開啟空白瀏覽器", + "browser-cookies-management": "瀏覽器 Cookie 管理", + "connect-existing-browser": "連接現有瀏覽器", + "connect-existing-browser-description": "連接到已啟用 CDP 並在指定連接埠執行的瀏覽器。", + "enter-port-number": "輸入連接埠號", + "check-and-connect": "檢查並連接", + "port-already-in-use": "該連接埠已在瀏覽器池中。請使用其他連接埠。", + "no-browser-on-port": "在連接埠 {{port}} 上未找到瀏覽器。請確保瀏覽器正在使用 --remote-debugging-port={{port}} 執行。", + "connected-browser": "已連接到連接埠 {{port}} 上的瀏覽器", + "cookies-added": "已新增 {{count}} 個 Cookie", + "failed-to-open-browser": "開啟瀏覽器失敗", + "failed-to-load-cookies": "載入 Cookie 失敗", + "deleted-cookies-for-domain": "已刪除 {{domain}} 及所有子網域的 Cookie", + "failed-to-delete-cookies-for-domain": "刪除 {{domain}} 的 Cookie 失敗", + "deleted-all-cookies": "已刪除所有 Cookie", + "failed-to-delete-all-cookies": "刪除所有 Cookie 失敗", + "cookies-updated": "Cookie 已更新", + "cookies-updated-message": "Cookie 已更新。您要重新啟動應用程式以使用新的 Cookie 嗎?", + "yes-restart": "是的,重新啟動", + "no-add-more": "不,繼續新增", + "restart-required": "需要重新啟動", + "restart-required-message": "重新啟動應用程式以啟用您的 Cookie 網域變更。", + "restart": "重新啟動", + "cookie-count": "{{count}} 個 Cookie" } diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index df2585268..db5011085 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -13,18 +13,17 @@ // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { fetchDelete, fetchGet, fetchPost } from '@/api/http'; +import VerticalNavigation from '@/components/Navigation'; import AlertDialog from '@/components/ui/alertDialog'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { - CheckCircle2, Cookie, Globe, + Link2, Loader2, Plus, RefreshCw, Trash2, - XCircle, } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -42,13 +41,6 @@ interface GroupedDomain { totalCookies: number; } -interface CdpPortStatus { - checking: boolean; - available: boolean | null; - error?: string; - data?: any; -} - interface CdpBrowser { id: string; port: number; @@ -59,31 +51,30 @@ interface CdpBrowser { export default function Browser() { const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState('connection'); const [loginLoading, setLoginLoading] = useState(false); const [cookiesLoading, setCookiesLoading] = useState(false); const [cookieDomains, setCookieDomains] = useState([]); const [deletingDomain, setDeletingDomain] = useState(null); const [deletingAll, setDeletingAll] = useState(false); const [showRestartDialog, setShowRestartDialog] = useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [showCookieRestartDialog, setShowCookieRestartDialog] = useState(false); // CDP port configuration const [cdpPort, setCdpPort] = useState(9223); - const [customPort, setCustomPort] = useState('9223'); - const [portStatus, setPortStatus] = useState({ - checking: false, - available: null, - }); - - // Dialog states - const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); - 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([]); + const [browserToRemove, setBrowserToRemove] = useState( + null + ); + + // Connect Existing Browser dialog + const [showConnectDialog, setShowConnectDialog] = useState(false); + const [connectPort, setConnectPort] = useState(''); + const [connectChecking, setConnectChecking] = useState(false); + const [connectError, setConnectError] = useState(''); // Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com") const getMainDomain = (domain: string): string => { @@ -133,6 +124,17 @@ export default function Browser() { loadCdpBrowsers(); }, []); + // Listen for CDP pool push updates from main process (health-check removes dead browsers) + useEffect(() => { + if (!window.electronAPI?.onCdpPoolChanged) return; + const cleanup = window.electronAPI.onCdpPoolChanged( + (browsers: CdpBrowser[]) => { + setCdpBrowsers(browsers); + } + ); + return cleanup; + }, []); + const loadCurrentBrowserPort = async () => { if (window.electronAPI?.getBrowserPort) { const port = await window.electronAPI.getBrowserPort(); @@ -145,187 +147,105 @@ export default function Browser() { 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); + const handleRemoveBrowser = async (browserId: string) => { + setDeletingBrowser(browserId); + try { + if (window.electronAPI?.removeCdpBrowser) { + const result = await window.electronAPI.removeCdpBrowser(browserId); + if (result.success) { + toast.success(t('layout.browser-removed')); + } else { + toast.error(result.error || t('layout.failed-to-remove-browser')); } } - }, 3000); // Refresh every 3 seconds - - return () => clearInterval(interval); - }, []); - - const handleCheckPort = async () => { - const portNumber = parseInt(customPort); - - if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { - toast.error(t('layout.invalid-port')); - return; + } catch (error: any) { + toast.error(error.message || t('layout.failed-to-remove-browser')); + } finally { + setDeletingBrowser(null); + setBrowserToRemove(null); } + }; - setPortStatus({ checking: true, available: null }); - + const handleOpenNewBrowser = async () => { try { - if (!window.electronAPI?.checkCdpPort) { - toast.error(t('layout.cdp-port-check-not-available')); - setPortStatus({ - checking: false, - available: false, - error: 'Not available', - }); - return; - } + toast.loading(t('layout.launching-browser', { port: '...' }), { + id: 'launch-browser', + }); - const result = await window.electronAPI.checkCdpPort(portNumber); + const result = await window.electronAPI?.launchCdpBrowser(); - if (result.available) { - setPortStatus({ - checking: false, - available: true, - data: result.data, + if (result?.success) { + toast.success(t('layout.browser-launched', { port: result.port }), { + id: 'launch-browser', }); - // Browser exists, ask if user wants to use it - setPendingPort(portNumber); - setShowUseExistingDialog(true); } else { - setPortStatus({ - checking: false, - available: false, - error: result.error, + toast.error(result?.error || t('layout.failed-to-launch-browser'), { + id: 'launch-browser', }); - // No browser on this port, ask if user wants to launch one - setPendingPort(portNumber); - setShowLaunchNewDialog(true); } } catch (error: any) { - setPortStatus({ - checking: false, - available: false, - error: error.message, + toast.error(error.message || t('layout.failed-to-launch-browser'), { + id: 'launch-browser', }); - toast.error(error.message || t('layout.failed-to-check-port')); } }; - - const handleUseExistingBrowser = async () => { - setShowUseExistingDialog(false); - if (pendingPort) { - try { - if (window.electronAPI?.addCdpBrowser) { - const result = await window.electronAPI.addCdpBrowser( - pendingPort, - true, - `External Browser (${pendingPort})` - ); - if (result.success) { - toast.success( - t('layout.added-browser-to-pool', { port: pendingPort }) - ); - await loadCdpBrowsers(); - } else { - toast.error(result.error || t('layout.failed-to-add-browser')); - } - } - } catch (error: any) { - toast.error(error.message || t('layout.failed-to-add-browser')); - } - } - setPendingPort(null); + const handleConnectExistingBrowser = () => { + setConnectPort(''); + setConnectError(''); + setShowConnectDialog(true); }; - const handleLaunchNewBrowser = async () => { - setShowLaunchNewDialog(false); + const handleCheckAndConnect = async () => { + const portNum = parseInt(connectPort, 10); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + setConnectError(t('layout.invalid-port')); + return; + } - if (!pendingPort) { + // Check if port is already in the pool + if (cdpBrowsers.some((b) => b.port === portNum)) { + setConnectError(t('layout.port-already-in-use')); return; } - const port = pendingPort; - setPendingPort(null); + setConnectChecking(true); + setConnectError(''); try { - if (!window.electronAPI?.launchCdpBrowser) { - toast.error(t('layout.launch-not-available')); - return; - } - - toast.loading(t('layout.launching-browser', { port }), { - id: 'launch-browser', + // Probe the port to check if a CDP browser is listening + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + const response = await fetch(`http://localhost:${portNum}/json/version`, { + signal: controller.signal, }); + clearTimeout(timeoutId); - const result = await window.electronAPI.launchCdpBrowser(port); - - if (result.success) { - toast.success(t('layout.browser-launched', { 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 || t('layout.failed-to-add-browser')); - } - } - - // Update port status - setPortStatus({ - checking: false, - available: true, - data: result.data, - }); - } else { - toast.error(result.error || t('layout.failed-to-launch-browser'), { - id: 'launch-browser', - }); + if (!response.ok) { + setConnectError(t('layout.no-browser-on-port', { port: portNum })); + return; } - } catch (error: any) { - toast.error(error.message || t('layout.failed-to-launch-browser'), { - id: 'launch-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(t('layout.browser-removed')); - await loadCdpBrowsers(); - } else { - toast.error(result.error || t('layout.failed-to-remove-browser')); - } + // Port is alive — add to CDP pool + if (window.electronAPI?.addCdpBrowser) { + await window.electronAPI.addCdpBrowser( + portNum, + true, + `External Browser (${portNum})` + ); } - } catch (error: any) { - toast.error(error.message || t('layout.failed-to-remove-browser')); + + toast.success(t('layout.connected-browser', { port: portNum })); + setShowConnectDialog(false); + } catch { + setConnectError(t('layout.no-browser-on-port', { port: portNum })); } finally { - setDeletingBrowser(null); + setConnectChecking(false); } }; @@ -361,13 +281,10 @@ export default function Browser() { // Cookies were added, show success toast and restart dialog const addedCount = newCookieCount - currentCookieCount; toast.success( - `Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}` + t('layout.cookies-added', { count: addedCount }) ); - setHasUnsavedChanges(true); setShowRestartDialog(true); } else if (newCookieCount < currentCookieCount) { - // Cookies were deleted (shouldn't happen here, but handle it) - setHasUnsavedChanges(true); setShowRestartDialog(true); } } @@ -377,10 +294,10 @@ export default function Browser() { clearInterval(checkInterval); await handleLoadCookies(); } - }, 500); // Check every 2 seconds + }, 500); } } catch (error: any) { - toast.error(error?.message || 'Failed to open browser'); + toast.error(error?.message || t('layout.failed-to-open-browser')); } finally { setLoginLoading(false); } @@ -397,7 +314,7 @@ export default function Browser() { setCookieDomains([]); } } catch (error: any) { - toast.error(error?.message || 'Failed to load cookies'); + toast.error(error?.message || t('layout.failed-to-load-cookies')); setCookieDomains([]); } finally { setCookiesLoading(false); @@ -416,20 +333,23 @@ export default function Browser() { ); await Promise.all(deletePromises); - toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); + toast.success( + t('layout.deleted-cookies-for-domain', { domain: mainDomain }) + ); // Remove from local state const domainsToRemove = new Set(subdomains.map((item) => item.domain)); setCookieDomains((prev) => prev.filter((item) => !domainsToRemove.has(item.domain)) ); - // Mark as having unsaved changes - setHasUnsavedChanges(true); // Show restart dialog after successful deletion setShowRestartDialog(true); } catch (error: any) { toast.error( - error?.message || `Failed to delete cookies for ${mainDomain}` + error?.message || + t('layout.failed-to-delete-cookies-for-domain', { + domain: mainDomain, + }) ); } finally { setDeletingDomain(null); @@ -440,15 +360,13 @@ export default function Browser() { setDeletingAll(true); try { await fetchDelete('/browser/cookies'); - toast.success('Deleted all cookies'); + toast.success(t('layout.deleted-all-cookies')); setCookieDomains([]); - // Mark as having unsaved changes - setHasUnsavedChanges(true); // Show restart dialog after successful deletion setShowRestartDialog(true); } catch (error: any) { - toast.error(error?.message || 'Failed to delete all cookies'); + toast.error(error?.message || t('layout.failed-to-delete-all-cookies')); } finally { setDeletingAll(false); } @@ -468,388 +386,326 @@ export default function Browser() { }; return ( -
+
{/* Restart Dialog */} setShowRestartDialog(false)} onConfirm={handleConfirmRestart} - title="Cookies Updated" - message="Cookies have been updated. Would you like to restart the application to use the new cookies?" - confirmText="Yes, Restart" - cancelText="No, Add More" + title={t('layout.cookies-updated')} + message={t('layout.cookies-updated-message')} + confirmText={t('layout.yes-restart')} + cancelText={t('layout.no-add-more')} confirmVariant="information" /> - {/* Use Existing Browser Dialog */} + {/* Cookie Restart Confirm Dialog */} { - setShowUseExistingDialog(false); - setPendingPort(null); + isOpen={showCookieRestartDialog} + onClose={() => setShowCookieRestartDialog(false)} + onConfirm={() => { + setShowCookieRestartDialog(false); + handleRestartApp(); }} - onConfirm={handleUseExistingBrowser} - title={t('layout.browser-found')} - message={t('layout.browser-found-description', { port: pendingPort })} - confirmText={t('layout.yes-use-browser')} + title={t('layout.restart-required')} + message={t('layout.restart-required-message')} + confirmText={t('layout.restart')} cancelText={t('layout.cancel')} confirmVariant="information" /> - {/* Launch New Browser Dialog */} + {/* Remove Browser Confirmation Dialog */} { - setShowLaunchNewDialog(false); - setPendingPort(null); + isOpen={!!browserToRemove} + onClose={() => setBrowserToRemove(null)} + onConfirm={() => { + if (browserToRemove) { + handleRemoveBrowser(browserToRemove.id); + } }} - onConfirm={handleLaunchNewBrowser} - title={t('layout.no-browser-found')} - message={t('layout.no-browser-found-description', { - port: pendingPort, + title={t('layout.remove-browser')} + message={t('layout.remove-browser-confirm', { + name: browserToRemove?.name || `Browser ${browserToRemove?.port}`, + port: browserToRemove?.port, })} - confirmText={t('layout.yes-launch-browser')} + confirmText={t('layout.remove')} cancelText={t('layout.cancel')} - confirmVariant="information" + confirmVariant="cuation" /> - {/* Header Section */} -
-
-
-
-
- {t('layout.browser-management')} -
-

- {t('layout.browser-management-description')}. -

+ {/* Connect Existing Browser Dialog */} + {showConnectDialog && ( +
+
+
+ {t('layout.connect-existing-browser')}
-
-
-
- - {/* Content Section */} -
-
-
-
+

+ {t('layout.connect-existing-browser-description')} +

+ { + setConnectPort(e.target.value); + setConnectError(''); + }} + placeholder={t('layout.enter-port-number')} + className="w-full rounded-lg border border-border-disabled bg-surface-secondary px-4 py-2 text-body-sm text-text-body outline-none focus:border-border-focus" + onKeyDown={(e) => { + if (e.key === 'Enter') handleCheckAndConnect(); + }} + /> + {connectError && ( +

+ {connectError} +

+ )} +
+
-
- {t('layout.browser-cookies')} -
-

- {t('layout.browser-cookies-description')} -

+
+
+ )} + +
+ {/* Left Sidebar */} +
+ + {t('layout.browser-connection')} + + ), + }, + { + value: 'cookies', + label: ( + + {t('layout.cookies-management')} + + ), + }, + ]} + value={activeTab} + onValueChange={setActiveTab} + className="h-full min-h-0 w-full flex-1 gap-0" + listClassName="w-full h-full overflow-y-auto" + contentClassName="hidden" + /> +
- {/* CDP Port Configuration Section */} -
-
-
-
- {t('layout.cdp-browser-connection')} -
-

- {t('layout.cdp-browser-connection-description')} -

-
+ {/* Right Content */} +
+ {activeTab === 'connection' && ( +
+
+ {t('layout.cdp-browser-connection')} +
+ {/* Action Buttons */} +
+ +
-
-
-
- {t('layout.current-port')}{' '} - - {cdpPort} - + {/* CDP Browser Pool */} +
+
+
+
+ {t('layout.cdp-browser-pool')} +
-

- {t('layout.cdp-port-check-description')} -

-
- -
- setCustomPort(e.target.value)} - className="flex-1" - min={1} - max={65535} - /> -
- {portStatus.available !== null && ( -
- {portStatus.available ? ( - <> - -
-
- {t('layout.browser-available')} -
- {portStatus.data && ( -
- {portStatus.data['Browser']} -{' '} - {portStatus.data['User-Agent']?.split(' ')[0]} -
- )} -
- - ) : ( - <> - -
-
- {t('layout.browser-not-available')} -
-
- {portStatus.error} + {cdpBrowsers.length > 0 ? ( +
+ {cdpBrowsers.map((browser) => ( +
+
+
+
+ + {browser.name || `Browser ${browser.port}`} + + + {t('layout.port')} {browser.port} +
- - )} + +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-browsers-in-pool')} +
+

+ {t('layout.add-browsers-hint')} +

)}
+ )} - {/* CDP Browser Pool Section */} -
-
-
-
-
- {t('layout.cdp-browser-pool')} -
- - {runningPorts.length} / {cdpBrowsers.length}{' '} - {t('layout.running')} - -
-

- {t('layout.cdp-browser-pool-description')} -

-
+ {activeTab === 'cookies' && ( +
+
+ {t('layout.browser-cookies-management')}
- {cdpBrowsers.length > 0 ? ( -
- {cdpBrowsers.map((browser) => ( -
-
-
- - {browser.name || `Browser ${browser.port}`} - - - {browser.isExternal - ? t('layout.external') - : t('layout.launched')} - - {/* Running status indicator */} - {runningPorts.includes(browser.port) ? ( - - - {t('layout.running')} - - ) : ( - !browser.isExternal && ( - - - {t('layout.stopped')} - - ) - )} -
- - {t('layout.port')} {browser.port} - + {/* Action Buttons */} +
+ +
+ + {/* Cookie Domains */} +
+
+
+
+ {t('layout.cookie-domains')} +
+ {cookieDomains.length > 0 && ( +
+ {groupDomainsByMain(cookieDomains).length}
+ )} +
+
+ {cookieDomains.length > 0 && ( -
- ))} -
- ) : ( -
- -
- {t('layout.no-browsers-in-pool')} -
-

- {t('layout.add-browsers-hint')} -

-
- )} -
- - {/* Cookies Section */} -
-
-
-
- {t('layout.cookie-domains')} -
- {cookieDomains.length > 0 && ( -
- {groupDomainsByMain(cookieDomains).length} -
- )} -
- -
- {cookieDomains.length > 0 && ( + )} - )} - - +
-
- {cookieDomains.length > 0 ? ( -
- {groupDomainsByMain(cookieDomains).map((group, index) => ( -
-
- - {group.mainDomain} - - - {group.totalCookies} Cookie - {group.totalCookies !== 1 ? 's' : ''} - -
- +
+ + {group.mainDomain} + + + {t('layout.cookie-count', { + count: group.totalCookies, + })} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-cookies-saved-yet')}
- ))} -
- ) : ( -
- -
- {t('layout.no-cookies-saved-yet')} +

+ {t('layout.no-cookies-saved-yet-description')} +

-

- {t('layout.no-cookies-saved-yet-description')} -

-
- )} + )} +
-
- -
- {t('layout.for-more-info')} - - {t('layout.privacy-policy')} - -
+ )}
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 739c588b4..b3b9dca2c 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -206,6 +206,25 @@ interface ElectronAPI { userId: string, skillName: string ) => Promise<{ success: boolean; error?: string }>; + setBrowserPort: (port: number, isExternal?: boolean) => Promise; + getBrowserPort: () => Promise; + getCdpBrowsers: () => Promise; + addCdpBrowser: ( + port: number, + isExternal: boolean, + name?: string + ) => Promise<{ success: boolean; browser?: any; error?: string }>; + removeCdpBrowser: ( + browserId: string, + closeBrowser?: boolean + ) => Promise<{ success: boolean; browser?: any; error?: string }>; + onCdpPoolChanged: (callback: (browsers: any[]) => void) => () => void; + launchCdpBrowser: () => Promise<{ + success: boolean; + port?: number; + data?: any; + error?: string; + }>; } declare global { From b08bd58b29eac3c0940e074b6ffe7983de7fa7b9 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Sun, 22 Feb 2026 22:58:27 +0000 Subject: [PATCH 37/40] inital_fix --- backend/app/utils/workforce.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 4eb2a2f50..e8d749b51 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -932,7 +932,9 @@ def stop_gracefully(self) -> None: def _cleanup_all_agents(self) -> None: """Release CDP browser resources for all agents.""" cleanup_count = 0 - children_count = len(self._children) if hasattr(self, "_children") else 0 + children_count = ( + len(self._children) if hasattr(self, "_children") else 0 + ) logger.info( f"[WF-CLEANUP] Starting cleanup, " f"children={children_count}, api_task_id={self.api_task_id}" From fef3951d4ecf3594f48298a59002f7c49365d68f Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Mon, 23 Feb 2026 12:20:03 +0000 Subject: [PATCH 38/40] update prompt and browser_open for cdp --- backend/app/agent/factory/browser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 19eedee47..7f4387c4e 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -191,11 +191,11 @@ def browser_agent(options: Chat): web_toolkit_custom = HybridBrowserToolkit( options.project_id, + cdp_keep_current_page=True, headless=False, browser_log_to_file=True, stealth=True, session_id=toolkit_session_id, - default_start_url="about:blank", cdp_url=f"http://localhost:{selected_port}", enabled_tools=[ "browser_click", @@ -212,6 +212,7 @@ def browser_agent(options: Chat): "browser_sheet_read", "browser_sheet_input", "browser_get_page_snapshot", + "browser_open" ], ) @@ -282,7 +283,7 @@ def browser_agent(options: Chat): "\n\n" "**IMPORTANT**: You are connected to an external browser instance. " "The browser may already be open with active sessions and logged-in " - "websites. When you use browser tools, you will connect to this " + "websites. When you use browser_open, you will connect to this " "existing browser and can immediately access its current state and " "pages.\n" "\n" From c18b3c3039c318e4b964403309a403139d55c33c Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Mon, 23 Feb 2026 12:23:15 +0000 Subject: [PATCH 39/40] update prompt and browser_open for cdp --- backend/app/agent/factory/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 7f4387c4e..161c809b4 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -212,7 +212,7 @@ def browser_agent(options: Chat): "browser_sheet_read", "browser_sheet_input", "browser_get_page_snapshot", - "browser_open" + "browser_open", ], ) From 1915c47917f84d1e9aebc7725246dc2bd130aa6c Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:47:50 +0000 Subject: [PATCH 40/40] enhance: browser_external_cdp PR696 (#1350) --- electron/main/index.ts | 29 +++++++++++++++++++++-------- src/pages/Dashboard/Browser.tsx | 11 ++++++++++- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 849d8fee5..8277c2551 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -157,16 +157,22 @@ async function isCdpPortAlive(port: number): Promise { /** Run one health-check cycle: remove dead browsers, persist & notify if changed. */ async function runPoolHealthCheck(): Promise { if (cdp_browser_pool.length === 0) return; + // Probe a snapshot so add/remove IPC handlers can run safely in parallel. + const snapshot = [...cdp_browser_pool]; const results = await Promise.all( - cdp_browser_pool.map((b) => isCdpPortAlive(b.port)) + snapshot.map((b) => isCdpPortAlive(b.port)) ); - const deadPorts: number[] = []; - for (let i = results.length - 1; i >= 0; i--) { - if (!results[i]) { - deadPorts.push(cdp_browser_pool[i].port); - cdp_browser_pool.splice(i, 1); - } - } + const deadIds = snapshot + .filter((_, idx) => !results[idx]) + .map((browser) => browser.id); + if (deadIds.length === 0) return; + + const deadIdSet = new Set(deadIds); + const removedBrowsers = cdp_browser_pool.filter((b) => deadIdSet.has(b.id)); + if (removedBrowsers.length === 0) return; + + cdp_browser_pool = cdp_browser_pool.filter((b) => !deadIdSet.has(b.id)); + const deadPorts = removedBrowsers.map((b) => b.port); if (deadPorts.length > 0) { log.info( `[CDP POOL] Health-check removed dead ports: ${deadPorts.join(', ')}. pool_size=${cdp_browser_pool.length}` @@ -178,6 +184,10 @@ async function runPoolHealthCheck(): Promise { /** Start periodic health check (call after window is created). */ function startCdpHealthCheck(): void { + if (cdpHealthCheckTimer) { + clearInterval(cdpHealthCheckTimer); + cdpHealthCheckTimer = null; + } log.info('[CDP POOL] Starting health check (interval=3s)'); // Run once immediately runPoolHealthCheck(); @@ -3430,6 +3440,9 @@ app.whenReady().then(async () => { app.on('window-all-closed', () => { log.info('window-all-closed'); + // Stop polling when no window is open (important on macOS reopen flow). + stopCdpHealthCheck(); + // Clean up WebView manager if (webViewManager) { webViewManager.destroy(); diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index db5011085..5a3e0539c 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -233,11 +233,20 @@ export default function Browser() { // Port is alive — add to CDP pool if (window.electronAPI?.addCdpBrowser) { - await window.electronAPI.addCdpBrowser( + const addResult = await window.electronAPI.addCdpBrowser( portNum, true, `External Browser (${portNum})` ); + if (!addResult?.success) { + setConnectError( + addResult?.error || t('layout.failed-to-add-browser') + ); + return; + } + } else { + setConnectError(t('layout.failed-to-add-browser')); + return; } toast.success(t('layout.connected-browser', { port: portNum }));