mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-26 15:45:50 +00:00
update
This commit is contained in:
parent
a9bff98d08
commit
aa2dba17f1
6 changed files with 462 additions and 4 deletions
|
|
@ -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": {}}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue