mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-26 07:25:58 +00:00
update
This commit is contained in:
parent
aa2dba17f1
commit
f67116a65d
6 changed files with 327 additions and 46 deletions
|
|
@ -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": {}}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ from app.service.task import set_process_task
|
|||
|
||||
NOW_STR = datetime.datetime.now().strftime("%Y-%m-%d %H:00:00")
|
||||
|
||||
# Global counter for round-robin browser selection from pool
|
||||
_browser_selection_counter = 0
|
||||
|
||||
|
||||
class ListenChatAgent(ChatAgent):
|
||||
@traceroot.trace()
|
||||
|
|
@ -729,9 +732,31 @@ def search_agent(options: Chat):
|
|||
message_handler=HumanToolkit(options.project_id, Agents.search_agent).send_message_to_user
|
||||
)
|
||||
|
||||
# Browser selection logic from CDP browser pool
|
||||
selected_port = env('browser_port', '9222')
|
||||
selected_is_external = options.use_external_cdp if hasattr(options, 'use_external_cdp') else False
|
||||
|
||||
# If CDP browser pool is available and not empty, select a browser from the pool
|
||||
if hasattr(options, 'cdp_browsers') and options.cdp_browsers:
|
||||
global _browser_selection_counter
|
||||
# Use round-robin selection from the pool
|
||||
selected_browser = options.cdp_browsers[_browser_selection_counter % len(options.cdp_browsers)]
|
||||
_browser_selection_counter += 1
|
||||
|
||||
selected_port = selected_browser.get('port', selected_port)
|
||||
selected_is_external = selected_browser.get('isExternal', False)
|
||||
|
||||
traceroot_logger.info(
|
||||
f"Selected browser from pool: port={selected_port}, "
|
||||
f"isExternal={selected_is_external}, "
|
||||
f"name={selected_browser.get('name', 'Unnamed')}"
|
||||
)
|
||||
else:
|
||||
traceroot_logger.info(f"No CDP browser pool available, using default port: {selected_port}")
|
||||
|
||||
# Use cdp_keep_current_page=True only when using external CDP browser
|
||||
# to preserve the current page. For internal browser, use False (default behavior)
|
||||
use_keep_current_page = options.use_external_cdp if hasattr(options, 'use_external_cdp') else False
|
||||
use_keep_current_page = selected_is_external
|
||||
|
||||
# When cdp_keep_current_page=True, don't set default_start_url (conflicts with keeping current page)
|
||||
# When cdp_keep_current_page=False, use "about:blank" as default start URL
|
||||
|
|
@ -745,7 +770,7 @@ def search_agent(options: Chat):
|
|||
session_id=str(uuid.uuid4())[:8],
|
||||
default_start_url=default_url,
|
||||
connect_over_cdp=True,
|
||||
cdp_url=f"http://localhost:{env('browser_port', '9222')}",
|
||||
cdp_url=f"http://localhost:{selected_port}",
|
||||
cdp_keep_current_page=use_keep_current_page,
|
||||
enabled_tools=[
|
||||
"browser_open",
|
||||
|
|
|
|||
|
|
@ -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<number, ChildProcessWithoutNullStreams> = 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<CdpBrowser>) => {
|
||||
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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<number | null>(null);
|
||||
|
||||
// CDP Browser Pool
|
||||
const [cdpBrowsers, setCdpBrowsers] = useState<CdpBrowser[]>([]);
|
||||
const [deletingBrowser, setDeletingBrowser] = useState<string | null>(null);
|
||||
const [runningPorts, setRunningPorts] = useState<number[]>([]);
|
||||
|
||||
// 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() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* CDP Browser Pool 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="flex items-center gap-2">
|
||||
<div className="text-body-base font-bold text-text-body">
|
||||
CDP Browser Pool
|
||||
</div>
|
||||
<span className="text-label-xs px-2 py-0.5 rounded bg-tag-fill-info text-text-information">
|
||||
{runningPorts.length} / {cdpBrowsers.length} Running
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-label-xs text-text-label mt-1">
|
||||
Manage multiple CDP browsers for task execution
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cdpBrowsers.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{cdpBrowsers.map((browser) => (
|
||||
<div
|
||||
key={browser.id}
|
||||
className="flex items-center justify-between px-4 py-3 bg-surface-tertiary rounded-xl border-solid border-border-disabled"
|
||||
>
|
||||
<div className="flex flex-col w-full items-start justify-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-body-sm text-text-body font-bold">
|
||||
{browser.name || `Browser ${browser.port}`}
|
||||
</span>
|
||||
<span className={`text-label-xs px-2 py-0.5 rounded ${
|
||||
browser.isExternal
|
||||
? 'bg-tag-fill-info text-text-information'
|
||||
: 'bg-tag-fill-success text-text-success'
|
||||
}`}>
|
||||
{browser.isExternal ? 'External' : 'Launched'}
|
||||
</span>
|
||||
{/* Running status indicator */}
|
||||
{runningPorts.includes(browser.port) ? (
|
||||
<span className="flex items-center gap-1 text-label-xs px-2 py-0.5 rounded bg-tag-fill-success text-text-success">
|
||||
<span className="w-2 h-2 rounded-full bg-text-success animate-pulse"></span>
|
||||
Running
|
||||
</span>
|
||||
) : (
|
||||
!browser.isExternal && (
|
||||
<span className="flex items-center gap-1 text-label-xs px-2 py-0.5 rounded bg-tag-fill-error text-text-cuation">
|
||||
<span className="w-2 h-2 rounded-full bg-text-cuation"></span>
|
||||
Stopped
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<span className="text-label-xs text-text-label mt-1">
|
||||
Port: {browser.port}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveBrowser(browser.id)}
|
||||
disabled={deletingBrowser === browser.id}
|
||||
className="ml-3 flex-shrink-0"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-text-cuation" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 px-4 bg-surface-tertiary rounded-xl">
|
||||
<Globe className="w-12 h-12 text-icon-secondary opacity-50 mb-4" />
|
||||
<div className="text-body-base font-bold text-text-label text-center">
|
||||
No browsers in pool
|
||||
</div>
|
||||
<p className="text-label-xs font-medium text-text-label text-center mt-1">
|
||||
Add browsers using the check port tool above
|
||||
</p>
|
||||
</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">
|
||||
|
||||
|
|
|
|||
|
|
@ -382,6 +382,7 @@ 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');
|
||||
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<ChatStore>) => createStore<ChatStore>()(
|
|||
new_agents: [...addWorkers],
|
||||
browser_port: browser_port,
|
||||
use_external_cdp: use_external_cdp,
|
||||
cdp_browsers: cdp_browsers,
|
||||
env_path: envPath,
|
||||
search_config: searchConfig
|
||||
}) : undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue