This commit is contained in:
puzhen 2025-11-19 01:22:39 +08:00
parent a9bff98d08
commit aa2dba17f1
6 changed files with 462 additions and 4 deletions

View file

@ -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": {}}

View file

@ -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",

View file

@ -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);

View file

@ -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'),
});

View file

@ -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<number>(0);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// CDP port configuration
const [cdpPort, setCdpPort] = useState<number>(9222);
const [customPort, setCustomPort] = useState<string>("9222");
const [portStatus, setPortStatus] = useState<CdpPortStatus>({
checking: false,
available: null,
});
// Dialog states
const [showUseExistingDialog, setShowUseExistingDialog] = useState(false);
const [showLaunchNewDialog, setShowLaunchNewDialog] = useState(false);
const [pendingPort, setPendingPort] = useState<number | null>(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 */}
<AlertDialog
isOpen={showUseExistingDialog}
onClose={() => {
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 */}
<AlertDialog
isOpen={showLaunchNewDialog}
onClose={() => {
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 */}
<div className="flex w-full border-solid border-t-0 border-x-0 border-border-disabled">
<div className="flex px-6 pt-8 pb-4 max-w-[900px] mx-auto w-full items-center justify-between">
@ -248,6 +417,96 @@ export default function Browser() {
<div className="text-body-lg font-bold text-text-heading">{t("layout.browser-cookies")}</div>
<p className="max-w-[600px] text-center text-body-sm text-text-label">{t("layout.browser-cookies-description")}
</p>
{/* CDP Port Configuration Section */}
<div className="flex flex-col max-w-[600px] w-full gap-3 border-[0.5px] border-border-secondary border-b-0 border-x-0 border-solid pt-3 mt-3">
<div className="flex flex-row items-center justify-between py-2">
<div className="flex flex-col items-start">
<div className="text-body-base font-bold text-text-body">
CDP Browser Connection
</div>
<p className="text-label-xs text-text-label mt-1">
Connect to a Chrome browser with remote debugging enabled
</p>
</div>
</div>
<div className="flex flex-col gap-3 px-4 py-3 bg-surface-tertiary rounded-xl">
<div className="flex flex-col gap-2">
<div className="text-label-sm font-medium text-text-body">
Current Port: <span className="font-bold text-text-information">{cdpPort}</span>
</div>
<p className="text-label-xs text-text-label">
Check if a browser is available on a specific port
</p>
</div>
<div className="flex items-center gap-2">
<Input
type="number"
placeholder="Port number (e.g., 9222)"
value={customPort}
onChange={(e) => setCustomPort(e.target.value)}
className="flex-1"
min={1}
max={65535}
/>
<Button
variant="primary"
size="sm"
onClick={handleCheckPort}
disabled={portStatus.checking}
className="min-w-[100px]"
>
{portStatus.checking ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Checking
</>
) : (
"Check Port"
)}
</Button>
</div>
{portStatus.available !== null && (
<div className={`flex items-start gap-2 p-3 rounded-lg ${
portStatus.available
? 'bg-tag-fill-success text-text-success'
: 'bg-tag-fill-error text-text-cuation'
}`}>
{portStatus.available ? (
<>
<CheckCircle2 className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="flex flex-col gap-1">
<div className="text-label-sm font-bold">
Browser Available
</div>
{portStatus.data && (
<div className="text-label-xs opacity-90">
{portStatus.data['Browser']} - {portStatus.data['User-Agent']?.split(' ')[0]}
</div>
)}
</div>
</>
) : (
<>
<XCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="flex flex-col gap-1">
<div className="text-label-sm font-bold">
Browser Not Available
</div>
<div className="text-label-xs opacity-90">
{portStatus.error}
</div>
</div>
</>
)}
</div>
)}
</div>
</div>
{/* Cookies Section */}
<div className="flex flex-col max-w-[600px] w-full gap-3 border-[0.5px] border-border-secondary border-b-0 border-x-0 border-solid pt-3 mt-3">

View file

@ -381,7 +381,8 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
})
}
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<ChatStore>) => createStore<ChatStore>()(
summary_prompt: ``,
new_agents: [...addWorkers],
browser_port: browser_port,
use_external_cdp: use_external_cdp,
env_path: envPath,
search_config: searchConfig
}) : undefined,