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] 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 fc901083..9f359575 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 4cac7a7f..4f3a0918 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 db2c2abc..c8b2a7fc 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 26d3ee43..b09393bc 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 dffbeda3..c6b67513 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 f50f9957..361729ce 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,