From d1d49e45205246c50c4784fa2d8b5e4e77f51294 Mon Sep 17 00:00:00 2001
From: puzhen <1303385763@qq.com>
Date: Tue, 21 Oct 2025 15:55:10 +0100
Subject: [PATCH 1/5] enhance: browser data import
---
backend/app/controller/tool_controller.py | 598 +++++++++++++++++-
backend/app/utils/agent.py | 16 +-
backend/app/utils/cookie_manager.py | 268 ++++++++
.../utils/toolkit/hybrid_browser_toolkit.py | 24 +-
config/browser-profiles.json | 15 +
electron/main/index.ts | 172 ++++-
src/i18n/locales/en-us/setting.json | 29 +-
src/i18n/locales/zh-Hans/setting.json | 29 +-
src/pages/Setting/General.tsx | 62 +-
.../Setting/components/CookieManager.tsx | 226 +++++++
10 files changed, 1410 insertions(+), 29 deletions(-)
create mode 100644 backend/app/utils/cookie_manager.py
create mode 100644 config/browser-profiles.json
create mode 100644 src/pages/Setting/components/CookieManager.tsx
diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py
index 405f69b2..311fd4ae 100644
--- a/backend/app/controller/tool_controller.py
+++ b/backend/app/controller/tool_controller.py
@@ -2,6 +2,12 @@ from fastapi import APIRouter, HTTPException
from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit
from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
from utils import traceroot_wrapper as traceroot
+from camel.toolkits.hybrid_browser_toolkit.hybrid_browser_toolkit_ts import (
+ HybridBrowserToolkit as BaseHybridBrowserToolkit,
+)
+from app.utils.cookie_manager import CookieManager
+import os
+import uuid
logger = traceroot.get_logger("tool_controller")
router = APIRouter(tags=["task"])
@@ -28,8 +34,10 @@ async def install_tool(tool: str):
await toolkit.connect()
# Get available tools to verify connection
- tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
- logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
+ tools = [tool_func.func.__name__ for tool_func in
+ toolkit.get_tools()]
+ logger.info(
+ f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
# Disconnect, authentication info is saved
await toolkit.disconnect()
@@ -42,7 +50,8 @@ async def install_tool(tool: str):
"toolkit_name": "NotionMCPToolkit"
}
except Exception as connect_error:
- logger.warning(f"Could not connect to {tool} MCP server: {connect_error}")
+ logger.warning(
+ f"Could not connect to {tool} MCP server: {connect_error}")
# Even if connection fails, mark as installed so user can use it later
return {
"success": True,
@@ -64,8 +73,10 @@ async def install_tool(tool: str):
toolkit = GoogleCalendarToolkit("install_auth")
# Get available tools to verify connection
- tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
- logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
+ tools = [tool_func.func.__name__ for tool_func in
+ toolkit.get_tools()]
+ logger.info(
+ f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
return {
"success": True,
@@ -113,3 +124,580 @@ async def list_available_tools():
}
]
}
+
+
+@router.post("/browser/login", name="open browser for login")
+async def open_browser_login():
+ """
+ Open an Electron-based Chrome browser for user login with a dedicated user data directory
+
+ Returns:
+ Browser session information
+ """
+ try:
+ import subprocess
+ import platform
+ import socket
+ import json
+
+ # Use fixed profile name for persistent logins (no port suffix)
+ session_id = "user_login"
+ cdp_port = 9223
+
+ # Create user data directory for Chrome profiles
+ user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(user_data_base, "profile_user_login")
+ os.makedirs(user_data_dir, exist_ok=True)
+
+ logger.info(
+ f"Creating browser session {session_id} with profile at: {user_data_dir}")
+
+ # Check if browser is already running on this port
+ def is_port_in_use(port):
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ return s.connect_ex(('localhost', port)) == 0
+
+ if is_port_in_use(cdp_port):
+ logger.info(f"Browser already running on port {cdp_port}")
+ return {
+ "success": True,
+ "session_id": session_id,
+ "user_data_dir": user_data_dir,
+ "cdp_port": cdp_port,
+ "message": "Browser already running. Use existing window to log in.",
+ "note": "Your login data will be saved in the profile."
+ }
+
+ # Create Electron browser script with .cjs extension for CommonJS
+ electron_script_path = os.path.join(os.path.dirname(__file__), "electron_browser.cjs")
+ electron_script_content = '''
+const { app, BrowserWindow, ipcMain } = require('electron');
+const path = require('path');
+
+// Parse command line arguments
+const args = process.argv.slice(2);
+const userDataDir = args[0];
+const cdpPort = args[1];
+const startUrl = args[2] || 'https://www.google.com';
+
+// This must be called before app.ready
+app.commandLine.appendSwitch('remote-debugging-port', cdpPort);
+app.commandLine.appendSwitch('user-data-dir', userDataDir);
+
+console.log('[ELECTRON BROWSER] Starting with:');
+console.log(' Chrome version:', process.versions.chrome);
+console.log(' User data dir (requested):', userDataDir);
+console.log(' CDP port:', cdpPort);
+console.log(' Start URL:', startUrl);
+
+// Try to set app paths
+app.setPath('userData', userDataDir);
+app.setPath('sessionData', userDataDir);
+
+app.whenReady().then(() => {
+ // Log actual paths being used
+ console.log('[ELECTRON BROWSER] Actual paths:');
+ console.log(' app.getPath("userData"):', app.getPath('userData'));
+ console.log(' app.getPath("sessionData"):', app.getPath('sessionData'));
+ console.log(' app.getPath("cache"):', app.getPath('cache'));
+ console.log(' app.getPath("temp"):', app.getPath('temp'));
+ console.log(' process.argv:', process.argv);
+
+ // Check command line switches
+ console.log('[ELECTRON BROWSER] Command line switches:');
+ console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir'));
+ console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port'));
+ const win = new BrowserWindow({
+ width: 1400,
+ height: 900,
+ title: 'Eigent Browser - Login',
+ webPreferences: {
+ nodeIntegration: true,
+ contextIsolation: false,
+ webviewTag: true
+ }
+ });
+
+ // Create navigation bar and webview
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Browser Info
+
+
+
+
+
+
+`;
+
+ win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html));
+
+ // Show window when ready
+ win.once('ready-to-show', () => {
+ win.show();
+ });
+
+ win.on('closed', () => {
+ app.quit();
+ });
+});
+
+app.on('window-all-closed', () => {
+ app.quit();
+});
+'''
+
+ # Write the Electron script
+ with open(electron_script_path, 'w') as f:
+ f.write(electron_script_content)
+
+ # Find Electron executable
+ # Try to use the same Electron version as the main app
+ electron_cmd = "npx"
+ electron_args = [
+ electron_cmd,
+ "electron",
+ electron_script_path,
+ user_data_dir,
+ str(cdp_port),
+ "https://www.google.com"
+ ]
+
+ # Get the app's directory to run npx in the right context
+ app_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+
+ logger.info(f"[PROFILE USER LOGIN] Launching Electron browser with CDP on port {cdp_port}")
+ logger.info(f"[PROFILE USER LOGIN] Working directory: {app_dir}")
+
+ process = subprocess.Popen(
+ electron_args,
+ cwd=app_dir, # Run in app directory to use the right Electron version
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+
+ # Wait a bit for Electron to start
+ import asyncio
+ await asyncio.sleep(3)
+
+ # Clean up the script file after a delay
+ async def cleanup_script():
+ await asyncio.sleep(10)
+ try:
+ os.remove(electron_script_path)
+ except:
+ pass
+
+ asyncio.create_task(cleanup_script())
+
+ logger.info(f"[PROFILE USER LOGIN] Electron browser launched with PID {process.pid}")
+
+ return {
+ "success": True,
+ "session_id": session_id,
+ "user_data_dir": user_data_dir,
+ "cdp_port": cdp_port,
+ "pid": process.pid,
+ "chrome_version": "130.0.6723.191", # Electron 33's Chrome version
+ "message": "Electron browser opened successfully. Please log in to your accounts.",
+ "note": "The browser will remain open for you to log in. Your login data will be saved in the profile."
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to open Electron browser for login: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to open browser: {str(e)}"
+ )
+
+
+@router.get("/browser/cookies", name="list cookie domains")
+async def list_cookie_domains(search: str = None):
+ """
+ 列出所有有cookies的网站域名
+
+ Args:
+ search: 可选的搜索关键词,用于过滤域名
+
+ Returns:
+ 域名列表,包含域名、cookie数量和最后访问时间
+ """
+ try:
+ # Use the same user data directory as the login browser
+ user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(user_data_base, "profile_user_login")
+
+ if not os.path.exists(user_data_dir):
+ return {
+ "success": True,
+ "domains": [],
+ "message": "No browser profile found. Please login first using /browser/login."
+ }
+
+ cookie_manager = CookieManager(user_data_dir)
+
+ if search:
+ domains = cookie_manager.search_cookies(search)
+ else:
+ domains = cookie_manager.get_cookie_domains()
+
+ return {
+ "success": True,
+ "domains": domains,
+ "total": len(domains),
+ "user_data_dir": user_data_dir
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to list cookie domains: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to list cookies: {str(e)}"
+ )
+
+
+@router.get("/browser/cookies/{domain}", name="get domain cookies")
+async def get_domain_cookies(domain: str):
+ """
+ 获取指定域名的cookies详情
+
+ Args:
+ domain: 域名(如 linkedin.com)
+
+ Returns:
+ 该域名的所有cookies
+ """
+ try:
+ user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(user_data_base, "profile_user_login")
+
+ if not os.path.exists(user_data_dir):
+ raise HTTPException(
+ status_code=404,
+ detail="No browser profile found. Please login first using /browser/login."
+ )
+
+ cookie_manager = CookieManager(user_data_dir)
+ cookies = cookie_manager.get_cookies_for_domain(domain)
+
+ return {
+ "success": True,
+ "domain": domain,
+ "cookies": cookies,
+ "count": len(cookies)
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get cookies for domain {domain}: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to get cookies: {str(e)}"
+ )
+
+
+@router.delete("/browser/cookies/{domain}", name="delete domain cookies")
+async def delete_domain_cookies(domain: str):
+ """
+ 删除指定域名的所有cookies
+
+ Args:
+ domain: 域名(如 linkedin.com)
+
+ Returns:
+ 删除结果
+ """
+ try:
+ user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(user_data_base, "profile_user_login")
+
+ if not os.path.exists(user_data_dir):
+ raise HTTPException(
+ status_code=404,
+ detail="No browser profile found. Please login first using /browser/login."
+ )
+
+ cookie_manager = CookieManager(user_data_dir)
+ success = cookie_manager.delete_cookies_for_domain(domain)
+
+ if success:
+ return {
+ "success": True,
+ "message": f"Successfully deleted cookies for domain: {domain}"
+ }
+ else:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to delete cookies for domain: {domain}"
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete cookies for domain {domain}: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to delete cookies: {str(e)}"
+ )
+
+
+@router.delete("/browser/cookies", name="delete all cookies")
+async def delete_all_cookies():
+ """
+ 删除所有cookies
+
+ Returns:
+ 删除结果
+ """
+ try:
+ user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(user_data_base, "profile_user_login")
+
+ if not os.path.exists(user_data_dir):
+ raise HTTPException(
+ status_code=404,
+ detail="No browser profile found."
+ )
+
+ cookie_manager = CookieManager(user_data_dir)
+ success = cookie_manager.delete_all_cookies()
+
+ if success:
+ return {
+ "success": True,
+ "message": "Successfully deleted all cookies"
+ }
+ else:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to delete all cookies"
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete all cookies: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to delete cookies: {str(e)}"
+ )
diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py
index dd3fd4fa..31f5eef3 100644
--- a/backend/app/utils/agent.py
+++ b/backend/app/utils/agent.py
@@ -840,13 +840,19 @@ Your capabilities include:
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
+**If Search Tools are Available:**
+- Initial Search: Start with `search_google` or `search_exa` 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:
+**If Search Tools are NOT Available:**
+- **RECOMMENDED: Use Brave Search**: Navigate to `https://search.brave.com/`
+ using `browser_visit_page`. Brave Search is highly recommended as it:
+ * Works well without requiring login
+ * Has fewer CAPTCHA challenges compared to other search engines
+ * Provides clean, relevant search results
+ * Supports advanced search queries
+- **Alternative Search Engines**: If Brave Search is unavailable, use
+ `browser_visit_page` to go directly to other popular search engines:
* 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
diff --git a/backend/app/utils/cookie_manager.py b/backend/app/utils/cookie_manager.py
new file mode 100644
index 00000000..217ca143
--- /dev/null
+++ b/backend/app/utils/cookie_manager.py
@@ -0,0 +1,268 @@
+"""
+Electron/Chrome Cookie Manager
+用于读取和管理Electron浏览器的cookies
+"""
+import sqlite3
+import os
+from typing import List, Dict, Optional
+from utils import traceroot_wrapper as traceroot
+import shutil
+from datetime import datetime
+
+logger = traceroot.get_logger("cookie_manager")
+
+
+class CookieManager:
+ """Cookie管理器,用于读取和管理浏览器cookies"""
+
+ def __init__(self, user_data_dir: str):
+ """
+ 初始化Cookie管理器
+
+ Args:
+ user_data_dir: 浏览器用户数据目录
+ """
+ self.user_data_dir = user_data_dir
+ self.cookies_db_path = os.path.join(user_data_dir, "Cookies")
+
+ # Check for alternative paths
+ if not os.path.exists(self.cookies_db_path):
+ # Try Network/Cookies path (some Electron versions)
+ alt_path = os.path.join(user_data_dir, "Network", "Cookies")
+ if os.path.exists(alt_path):
+ self.cookies_db_path = alt_path
+ else:
+ logger.warning(f"Cookies database not found at {self.cookies_db_path}")
+
+ def _get_cookies_connection(self) -> Optional[sqlite3.Connection]:
+ """
+ 获取cookies数据库连接
+
+ Returns:
+ 数据库连接或None
+ """
+ if not os.path.exists(self.cookies_db_path):
+ logger.warning(f"Cookies database not found: {self.cookies_db_path}")
+ return None
+
+ try:
+ # Create a temporary copy since the database might be locked
+ temp_db_path = self.cookies_db_path + ".tmp"
+ shutil.copy2(self.cookies_db_path, temp_db_path)
+
+ # Open the temporary copy
+ conn = sqlite3.connect(temp_db_path)
+ conn.row_factory = sqlite3.Row
+ return conn
+ except Exception as e:
+ logger.error(f"Error connecting to cookies database: {e}")
+ return None
+
+ def _cleanup_temp_db(self):
+ """清理临时数据库文件"""
+ temp_db_path = self.cookies_db_path + ".tmp"
+ try:
+ if os.path.exists(temp_db_path):
+ os.remove(temp_db_path)
+ except Exception as e:
+ logger.debug(f"Error cleaning up temp database: {e}")
+
+ def get_cookie_domains(self) -> List[Dict[str, any]]:
+ """
+ 获取所有有cookies的域名列表
+
+ Returns:
+ 域名列表,包含域名和cookie数量
+ """
+ conn = self._get_cookies_connection()
+ if not conn:
+ return []
+
+ try:
+ cursor = conn.cursor()
+
+ # Group by host_key (domain) and count cookies
+ query = """
+ SELECT
+ host_key as domain,
+ COUNT(*) as cookie_count,
+ MAX(last_access_utc) as last_access
+ FROM cookies
+ GROUP BY host_key
+ ORDER BY last_access DESC
+ """
+
+ cursor.execute(query)
+ rows = cursor.fetchall()
+
+ domains = []
+ for row in rows:
+ # Convert Chrome timestamp (microseconds since 1601-01-01) to readable format
+ try:
+ # Chrome timestamp is microseconds since 1601-01-01 UTC
+ chrome_timestamp = row['last_access']
+ if chrome_timestamp:
+ # Convert to seconds since epoch (1970-01-01)
+ # 11644473600 seconds between 1601-01-01 and 1970-01-01
+ seconds_since_epoch = (chrome_timestamp / 1000000.0) - 11644473600
+ last_access = datetime.fromtimestamp(seconds_since_epoch).strftime('%Y-%m-%d %H:%M:%S')
+ else:
+ last_access = "Never"
+ except Exception as e:
+ logger.debug(f"Error converting timestamp: {e}")
+ last_access = "Unknown"
+
+ domains.append({
+ 'domain': row['domain'],
+ 'cookie_count': row['cookie_count'],
+ 'last_access': last_access
+ })
+
+ logger.info(f"Found {len(domains)} domains with cookies")
+ return domains
+
+ except Exception as e:
+ logger.error(f"Error reading cookies: {e}")
+ return []
+ finally:
+ conn.close()
+ self._cleanup_temp_db()
+
+ def get_cookies_for_domain(self, domain: str) -> List[Dict[str, str]]:
+ """
+ 获取指定域名的所有cookies
+
+ Args:
+ domain: 域名
+
+ Returns:
+ Cookie列表
+ """
+ conn = self._get_cookies_connection()
+ if not conn:
+ return []
+
+ try:
+ cursor = conn.cursor()
+
+ query = """
+ SELECT
+ host_key,
+ name,
+ value,
+ path,
+ expires_utc,
+ is_secure,
+ is_httponly
+ FROM cookies
+ WHERE host_key = ? OR host_key LIKE ?
+ ORDER BY name
+ """
+
+ # Match exact domain or subdomain pattern
+ cursor.execute(query, (domain, f'%.{domain}'))
+ rows = cursor.fetchall()
+
+ cookies = []
+ for row in rows:
+ cookies.append({
+ 'domain': row['host_key'],
+ 'name': row['name'],
+ 'value': row['value'][:50] + '...' if len(row['value']) > 50 else row['value'], # Truncate long values
+ 'path': row['path'],
+ 'secure': bool(row['is_secure']),
+ 'httponly': bool(row['is_httponly'])
+ })
+
+ return cookies
+
+ except Exception as e:
+ logger.error(f"Error reading cookies for domain {domain}: {e}")
+ return []
+ finally:
+ conn.close()
+ self._cleanup_temp_db()
+
+ def delete_cookies_for_domain(self, domain: str) -> bool:
+ """
+ 删除指定域名的所有cookies
+
+ Args:
+ domain: 域名
+
+ Returns:
+ 是否删除成功
+ """
+ if not os.path.exists(self.cookies_db_path):
+ logger.warning(f"Cookies database not found: {self.cookies_db_path}")
+ return False
+
+ try:
+ # Direct connection to the actual database (not a copy)
+ conn = sqlite3.connect(self.cookies_db_path)
+ cursor = conn.cursor()
+
+ # Delete cookies for exact domain and subdomains
+ delete_query = """
+ DELETE FROM cookies
+ WHERE host_key = ? OR host_key LIKE ?
+ """
+
+ cursor.execute(delete_query, (domain, f'%.{domain}'))
+ deleted_count = cursor.rowcount
+
+ conn.commit()
+ conn.close()
+
+ logger.info(f"Deleted {deleted_count} cookies for domain {domain}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Error deleting cookies for domain {domain}: {e}")
+ return False
+
+ def delete_all_cookies(self) -> bool:
+ """
+ 删除所有cookies
+
+ Returns:
+ 是否删除成功
+ """
+ if not os.path.exists(self.cookies_db_path):
+ logger.warning(f"Cookies database not found: {self.cookies_db_path}")
+ return False
+
+ try:
+ conn = sqlite3.connect(self.cookies_db_path)
+ cursor = conn.cursor()
+
+ cursor.execute("DELETE FROM cookies")
+ deleted_count = cursor.rowcount
+
+ conn.commit()
+ conn.close()
+
+ logger.info(f"Deleted all {deleted_count} cookies")
+ return True
+
+ except Exception as e:
+ logger.error(f"Error deleting all cookies: {e}")
+ return False
+
+ def search_cookies(self, keyword: str) -> List[Dict[str, any]]:
+ """
+ 搜索包含关键词的cookies
+
+ Args:
+ keyword: 搜索关键词
+
+ Returns:
+ 匹配的域名列表
+ """
+ domains = self.get_cookie_domains()
+ keyword_lower = keyword.lower()
+
+ return [
+ domain for domain in domains
+ if keyword_lower in domain['domain'].lower()
+ ]
diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py
index ff03e092..ea447f56 100644
--- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py
+++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py
@@ -251,6 +251,18 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
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}")
+
+ # Set default user_data_dir if not provided
+ if user_data_dir is None:
+ # Use browser port to determine profile directory
+ browser_port = env('browser_port', '9222')
+ user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(user_data_base, f"profile_{browser_port}")
+ os.makedirs(user_data_dir, exist_ok=True)
+ logger.info(f"[HybridBrowserToolkit] Using port-based user_data_dir: {user_data_dir} (port: {browser_port})")
+ 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}")
super().__init__(
headless=headless,
@@ -286,6 +298,10 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
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}")
+
# 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}")
@@ -302,10 +318,16 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
if new_session_id is None:
new_session_id = str(uuid.uuid4())[:8]
+ # 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}")
+
+ # Use the same session_id to share the same browser instance
+ # This ensures all clones use the same WebSocket connection and browser
return HybridBrowserToolkit(
self.api_task_id,
headless=self._headless,
- user_data_dir=self._user_data_dir,
+ user_data_dir=self._user_data_dir, # Use the same user_data_dir
stealth=self._stealth,
web_agent_model=self._web_agent_model,
cache_dir=f"{self._cache_dir.rstrip('/')}/_clone_{new_session_id}/",
diff --git a/config/browser-profiles.json b/config/browser-profiles.json
new file mode 100644
index 00000000..b0219805
--- /dev/null
+++ b/config/browser-profiles.json
@@ -0,0 +1,15 @@
+{
+ "profiles": {
+ "userLogin": {
+ "name": "profile_user_login",
+ "partition": "user_login",
+ "description": "Profile for user login browser"
+ },
+ "project": {
+ "nameTemplate": "profile_{port}",
+ "partitionTemplate": "project_{port}",
+ "description": "Profile for project browser instances"
+ }
+ },
+ "basePath": "~/.eigent/browser_profiles"
+}
\ No newline at end of file
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 7c87795a..44d5f42d 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -49,10 +49,77 @@ const preload = path.join(__dirname, '../preload/index.mjs');
const indexHtml = path.join(RENDERER_DIST, 'index.html');
const logPath = log.transports.file.getFile().path;
-// Set remote debugging port
-findAvailablePort(browser_port).then(port => {
+// Profile initialization promise
+let profileInitPromise: Promise;
+
+// Store profile paths for cleanup
+let browserProfilePaths: {
+ sourceProfile: string;
+ targetProfile: string;
+} | null = null;
+
+// Set remote debugging port and profile
+profileInitPromise = findAvailablePort(browser_port).then(async port => {
browser_port = port;
app.commandLine.appendSwitch('remote-debugging-port', port + '');
+
+ // Set user data dir for browser profile persistence using port as index
+ const browserProfilesBase = path.join(os.homedir(), '.eigent', 'browser_profiles');
+ const sourceProfile = path.join(browserProfilesBase, 'profile_user_login');
+ const targetProfile = path.join(browserProfilesBase, `profile_${port}`);
+
+ // Store paths for cleanup
+ browserProfilePaths = { sourceProfile, targetProfile };
+
+ // Ensure profile_user_login exists (create empty if not)
+ if (!fs.existsSync(sourceProfile)) {
+ await fsp.mkdir(sourceProfile, { recursive: true });
+ log.info(`[PROJECT BROWSER] Created empty profile_user_login directory at ${sourceProfile}`);
+ }
+
+ // Always copy/update profile_{port} from profile_user_login
+ try {
+ // Remove existing target profile to ensure fresh copy
+ if (fs.existsSync(targetProfile)) {
+ log.info(`[PROJECT BROWSER] Removing existing profile at ${targetProfile} for update`);
+ await fsp.rm(targetProfile, { recursive: true, force: true });
+ }
+
+ // Copy from source profile
+ log.info(`[PROJECT BROWSER] Copying user profile from ${sourceProfile} to ${targetProfile}`);
+ await fsp.cp(sourceProfile, targetProfile, { recursive: true });
+ log.info(`[PROJECT BROWSER] Successfully copied user profile to ${targetProfile}`);
+
+ // Verify the copy
+ const partitionPath = path.join(targetProfile, 'Partitions', 'user_login');
+ if (fs.existsSync(partitionPath)) {
+ const files = await fsp.readdir(partitionPath);
+ log.info(`[PROJECT BROWSER] Partition contains ${files.length} files`);
+ if (files.includes('Cookies')) {
+ const cookieStat = await fsp.stat(path.join(partitionPath, 'Cookies'));
+ log.info(`[PROJECT BROWSER] Cookies file size: ${cookieStat.size} bytes`);
+ }
+ }
+ } catch (error) {
+ log.error(`[PROJECT BROWSER] Failed to copy profile: ${error}`);
+ // Create empty directory if copy fails
+ try {
+ await fsp.mkdir(targetProfile, { recursive: true });
+ log.info(`[PROJECT BROWSER] Created empty profile directory as fallback`);
+ } catch (mkdirError) {
+ log.error(`[PROJECT BROWSER] Failed to create directory: ${mkdirError}`);
+ }
+ }
+
+ // IMPORTANT: Set user-data-dir before app is ready
+ app.commandLine.appendSwitch('user-data-dir', targetProfile);
+
+ // Also set Electron's paths
+ app.setPath('userData', targetProfile);
+ app.setPath('sessionData', targetProfile);
+
+ log.info(`[PROJECT BROWSER STARTING] Chrome DevTools Protocol enabled on port ${port}`);
+ log.info(`[PROJECT BROWSER STARTING] User data directory: ${targetProfile}`);
});
// Memory optimization settings
@@ -249,6 +316,21 @@ function registerIpcHandlers() {
});
ipcMain.handle('get-app-version', () => app.getVersion());
ipcMain.handle('get-backend-port', () => backendPort);
+
+ // ==================== restart app handler ====================
+ ipcMain.handle('restart-app', async () => {
+ log.info('[RESTART] Restarting app to apply user profile changes');
+
+ // Clean up Python process first
+ await cleanupPythonProcess();
+
+ // Schedule relaunch after a short delay
+ setTimeout(() => {
+ app.relaunch();
+ app.quit();
+ }, 100);
+ });
+
ipcMain.handle('restart-backend', async () => {
try {
if (backendPort) {
@@ -975,6 +1057,10 @@ async function createWindow() {
// Ensure .eigent directories exist before anything else
ensureEigentDirectories();
+ log.info(`[PROJECT BROWSER WINDOW] Creating BrowserWindow which will start Chrome with CDP on port ${browser_port}`);
+ log.info(`[PROJECT BROWSER WINDOW] Current user data path: ${app.getPath('userData')}`);
+ log.info(`[PROJECT BROWSER WINDOW] Command line switch user-data-dir: ${app.commandLine.getSwitchValue('user-data-dir')}`);
+
win = new BrowserWindow({
title: 'Eigent',
width: 1200,
@@ -1002,12 +1088,14 @@ async function createWindow() {
// ==================== initialize manager ====================
fileReader = new FileReader(win);
- webViewManager = new WebViewManager(win);
+ webViewManager = new WebViewManager(win, browser_port);
- // create initial webviews (reduced from 8 to 3)
- for (let i = 1; i <= 3; i++) {
+ // create multiple webviews
+ log.info(`[PROJECT BROWSER] Creating WebViews with partition: persist:agent-webview`);
+ for (let i = 1; i <= 8; i++) {
webViewManager.createWebview(i === 1 ? undefined : i.toString());
}
+ log.info('[PROJECT BROWSER] WebViewManager initialized with webviews');
// ==================== set event listeners ====================
setupWindowEventListeners();
@@ -1357,7 +1445,15 @@ const handleBeforeClose = () => {
}
// ==================== app event handle ====================
-app.whenReady().then(() => {
+app.whenReady().then(async () => {
+ // Wait for profile initialization to complete
+ log.info('[MAIN] Waiting for profile initialization...');
+ try {
+ await profileInitPromise;
+ log.info('[MAIN] Profile initialization completed');
+ } catch (error) {
+ log.error('[MAIN] Profile initialization failed:', error);
+ }
// ==================== download handle ====================
session.defaultSession.on('will-download', (event, item, webContents) => {
@@ -1441,39 +1537,87 @@ app.on('activate', () => {
app.on('before-quit', async (event) => {
log.info('before-quit');
log.info('quit python_process.pid: ' + python_process?.pid);
-
+
// Prevent default quit to ensure cleanup completes
event.preventDefault();
-
+
try {
+ // Sync browser profile back to profile_user_login
+ if (browserProfilePaths) {
+ const { sourceProfile, targetProfile } = browserProfilePaths;
+ try {
+ log.info('[PROFILE SYNC] Syncing browser profile from target back to source...');
+ log.info('[PROFILE SYNC] Source:', sourceProfile);
+ log.info('[PROFILE SYNC] Target:', targetProfile);
+
+ // Check if target profile exists and has data
+ if (fs.existsSync(targetProfile)) {
+ const partitionPath = path.join(targetProfile, 'Partitions', 'user_login');
+
+ if (fs.existsSync(partitionPath)) {
+ // Ensure source profile directories exist
+ const sourcePartitionPath = path.join(sourceProfile, 'Partitions', 'user_login');
+ if (!fs.existsSync(sourcePartitionPath)) {
+ fs.mkdirSync(sourcePartitionPath, { recursive: true });
+ }
+
+ // Copy Partitions directory back to source (this contains login data)
+ const sourcePartitionsDir = path.join(sourceProfile, 'Partitions');
+ const targetPartitionsDir = path.join(targetProfile, 'Partitions');
+
+ if (fs.existsSync(targetPartitionsDir)) {
+ // Remove old source partitions and replace with new
+ if (fs.existsSync(sourcePartitionsDir)) {
+ fs.rmSync(sourcePartitionsDir, { recursive: true, force: true });
+ }
+
+ // Copy new partitions
+ fs.cpSync(targetPartitionsDir, sourcePartitionsDir, { recursive: true });
+ log.info('[PROFILE SYNC] Successfully synced browser profile back to source');
+
+ // Log partition size for verification
+ const files = fs.readdirSync(sourcePartitionPath);
+ log.info(`[PROFILE SYNC] Source partition now contains ${files.length} files`);
+ if (files.includes('Cookies')) {
+ const cookieStat = fs.statSync(path.join(sourcePartitionPath, 'Cookies'));
+ log.info(`[PROFILE SYNC] Cookies file size: ${cookieStat.size} bytes`);
+ }
+ }
+ }
+ }
+ } catch (error) {
+ log.error('[PROFILE SYNC] Failed to sync profile:', error);
+ }
+ }
+
// Clean up resources
if (webViewManager) {
webViewManager.destroy();
webViewManager = null;
}
-
+
if (win && !win.isDestroyed()) {
win.destroy();
win = null;
}
-
+
// Wait for Python process cleanup
await cleanupPythonProcess();
-
+
// Clean up file reader if exists
if (fileReader) {
fileReader = null;
}
-
+
// Clear any remaining timeouts/intervals
if (global.gc) {
global.gc();
}
-
+
// Reset protocol handling state
isWindowReady = false;
protocolUrlQueue = [];
-
+
log.info('All cleanup completed, exiting...');
} catch (error) {
log.error('Error during cleanup:', error);
diff --git a/src/i18n/locales/en-us/setting.json b/src/i18n/locales/en-us/setting.json
index c9a4086b..dfa9dddd 100644
--- a/src/i18n/locales/en-us/setting.json
+++ b/src/i18n/locales/en-us/setting.json
@@ -120,6 +120,33 @@
"worker-name-cannot-be-empty": "Worker name cannot be empty",
"worker-name-already-exists": "Worker name already exists",
"warning-google-search-not-configured": "Warning: Google Search not configured",
- "search-functionality-may-be-limited-without-google-api": "Search functionality may be limited without Google API key and Search Engine ID. You can configure these in MCP & Tools settings."
+ "search-functionality-may-be-limited-without-google-api": "Search functionality may be limited without Google API key and Search Engine ID. You can configure these in MCP & Tools settings.",
+
+ "browser-login": "Browser Login",
+ "browser-login-description": "Open a Chrome browser to log in to your accounts. Your login data will be saved locally in a secure profile.",
+ "open-browser-login": "Open Browser for Login",
+ "opening-browser": "Opening Browser...",
+ "browser-opened-successfully": "Browser opened successfully. Please log in to your accounts.",
+ "failed-to-open-browser": "Failed to open browser. Please try again.",
+ "restart-to-apply": "Restart to Apply",
+
+ "cookie-manager": "Cookie Manager",
+ "cookie-manager-description": "Manage cookies saved from your browser sessions. Delete cookies for specific sites or all at once.",
+ "refresh": "Refresh",
+ "delete-all": "Delete All",
+ "search-domains": "Search domains...",
+ "loading-cookies": "Loading cookies...",
+ "no-cookies-found": "No cookies found",
+ "no-matching-domains": "No matching domains",
+ "login-to-save-cookies": "Use the browser login feature above to save cookies from your accounts.",
+ "cookies-count": "{{count}} cookies",
+ "last-access": "Last access",
+ "deleting": "Deleting...",
+ "cookies-deleted-successfully": "Successfully deleted cookies for {{domain}}",
+ "failed-to-load-cookies": "Failed to load cookies. Please try again.",
+ "failed-to-delete-cookies": "Failed to delete cookies. Please try again.",
+ "confirm-delete-all-cookies": "Are you sure you want to delete all cookies? This action cannot be undone.",
+ "all-cookies-deleted": "All cookies have been deleted successfully.",
+ "cookie-delete-warning": "Note: Deleting cookies will log you out of the associated websites. You may need to restart the browser to see changes take effect."
}
diff --git a/src/i18n/locales/zh-Hans/setting.json b/src/i18n/locales/zh-Hans/setting.json
index ca3dbe49..56436c8c 100644
--- a/src/i18n/locales/zh-Hans/setting.json
+++ b/src/i18n/locales/zh-Hans/setting.json
@@ -118,5 +118,32 @@
"search-mcp": "搜索 MCP",
"installed": "已安装",
"worker-name-cannot-be-empty": "Worker 名称不能为空",
- "worker-name-already-exists": "Worker 名称已存在"
+ "worker-name-already-exists": "Worker 名称已存在",
+
+ "browser-login": "浏览器登录",
+ "browser-login-description": "打开 Chrome 浏览器以登录您的账户。您的登录数据将安全地保存在本地配置文件中。",
+ "open-browser-login": "打开浏览器登录",
+ "opening-browser": "正在打开浏览器...",
+ "browser-opened-successfully": "浏览器已成功打开。请登录您的账户。",
+ "failed-to-open-browser": "打开浏览器失败,请重试。",
+ "restart-to-apply": "重启应用",
+
+ "cookie-manager": "Cookie 管理器",
+ "cookie-manager-description": "管理从浏览器会话中保存的 Cookie。可以删除特定网站或全部 Cookie。",
+ "refresh": "刷新",
+ "delete-all": "全部删除",
+ "search-domains": "搜索域名...",
+ "loading-cookies": "正在加载 Cookie...",
+ "no-cookies-found": "未找到 Cookie",
+ "no-matching-domains": "没有匹配的域名",
+ "login-to-save-cookies": "使用上面的浏览器登录功能来保存您账户的 Cookie。",
+ "cookies-count": "{{count}} 个 Cookie",
+ "last-access": "最后访问",
+ "deleting": "删除中...",
+ "cookies-deleted-successfully": "已成功删除 {{domain}} 的 Cookie",
+ "failed-to-load-cookies": "加载 Cookie 失败,请重试。",
+ "failed-to-delete-cookies": "删除 Cookie 失败,请重试。",
+ "confirm-delete-all-cookies": "确定要删除所有 Cookie 吗?此操作无法撤销。",
+ "all-cookies-deleted": "所有 Cookie 已成功删除。",
+ "cookie-delete-warning": "注意:删除 Cookie 会使您从相关网站登出。您可能需要重启浏览器才能看到更改生效。"
}
diff --git a/src/pages/Setting/General.tsx b/src/pages/Setting/General.tsx
index eebce5e8..7b309689 100644
--- a/src/pages/Setting/General.tsx
+++ b/src/pages/Setting/General.tsx
@@ -1,18 +1,20 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { LogOut, Settings, Check } from "lucide-react";
+import { LogOut, Settings, Check, Chrome, RefreshCw } from "lucide-react";
import light from "@/assets/light.png";
import dark from "@/assets/dark.png";
import transparent from "@/assets/transparent.png";
import { useAuthStore } from "@/store/authStore";
import { useNavigate } from "react-router-dom";
-import { proxyFetchPut, proxyFetchGet } from "@/api/http";
+import { proxyFetchPut, proxyFetchGet, fetchPost } from "@/api/http";
import { createRef, RefObject } from "react";
import { useEffect, useState } from "react";
import { useChatStore } from "@/store/chatStore";
import { LocaleEnum, switchLanguage } from "@/i18n";
import { useTranslation } from "react-i18next";
+import { toast } from "sonner";
+import CookieManager from "./components/CookieManager";
import {
Select,
@@ -30,6 +32,7 @@ export default function SettingGeneral() {
const authStore = useAuthStore();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
+ const [isBrowserOpening, setIsBrowserOpening] = useState(false);
const setAppearance = authStore.setAppearance;
const language = authStore.language;
const setLanguage = authStore.setLanguage;
@@ -131,6 +134,25 @@ export default function SettingGeneral() {
}
}, []);
+ const handleOpenBrowserLogin = async () => {
+ try {
+ setIsBrowserOpening(true);
+ const response = await fetchPost("/browser/login");
+
+ if (response.success) {
+ toast.success(t("setting.browser-opened-successfully"));
+ console.log("Browser session info:", response);
+ } else {
+ toast.error(t("setting.failed-to-open-browser"));
+ }
+ } catch (error) {
+ console.error("Error opening browser:", error);
+ toast.error(t("setting.failed-to-open-browser"));
+ } finally {
+ setIsBrowserOpening(false);
+ }
+ };
+
return (
@@ -167,6 +189,42 @@ export default function SettingGeneral() {
+
+
+ {t("setting.browser-login")}
+
+
+ {t("setting.browser-login-description")}
+
+
+
+
+
+
+
{t("setting.language")}
diff --git a/src/pages/Setting/components/CookieManager.tsx b/src/pages/Setting/components/CookieManager.tsx
new file mode 100644
index 00000000..8c39f8b2
--- /dev/null
+++ b/src/pages/Setting/components/CookieManager.tsx
@@ -0,0 +1,226 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { fetchGet, fetchDelete } from "@/api/http";
+import { toast } from "sonner";
+import {
+ Trash2,
+ Search,
+ Cookie,
+ RefreshCw,
+ AlertTriangle
+} from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+interface CookieDomain {
+ domain: string;
+ cookie_count: number;
+ last_access: string;
+}
+
+export default function CookieManager() {
+ const { t } = useTranslation();
+ const [domains, setDomains] = useState
([]);
+ const [filteredDomains, setFilteredDomains] = useState([]);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(null);
+
+ const loadCookies = async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetchGet("/browser/cookies");
+
+ if (response.success) {
+ setDomains(response.domains || []);
+ setFilteredDomains(response.domains || []);
+ } else {
+ toast.error(t("setting.failed-to-load-cookies"));
+ }
+ } catch (error) {
+ console.error("Error loading cookies:", error);
+ toast.error(t("setting.failed-to-load-cookies"));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadCookies();
+ }, []);
+
+ useEffect(() => {
+ if (searchQuery) {
+ const filtered = domains.filter(domain =>
+ domain.domain.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ setFilteredDomains(filtered);
+ } else {
+ setFilteredDomains(domains);
+ }
+ }, [searchQuery, domains]);
+
+ const handleDeleteDomain = async (domain: string) => {
+ try {
+ setIsDeleting(domain);
+ const response = await fetchDelete(`/browser/cookies/${domain}`);
+
+ if (response.success) {
+ toast.success(t("setting.cookies-deleted-successfully", { domain }));
+ // Reload the list
+ await loadCookies();
+ } else {
+ toast.error(t("setting.failed-to-delete-cookies"));
+ }
+ } catch (error) {
+ console.error("Error deleting cookies:", error);
+ toast.error(t("setting.failed-to-delete-cookies"));
+ } finally {
+ setIsDeleting(null);
+ }
+ };
+
+ const handleDeleteAll = async () => {
+ if (!window.confirm(t("setting.confirm-delete-all-cookies"))) {
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await fetchDelete("/browser/cookies");
+
+ if (response.success) {
+ toast.success(t("setting.all-cookies-deleted"));
+ setDomains([]);
+ setFilteredDomains([]);
+ } else {
+ toast.error(t("setting.failed-to-delete-cookies"));
+ }
+ } catch (error) {
+ console.error("Error deleting all cookies:", error);
+ toast.error(t("setting.failed-to-delete-cookies"));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {t("setting.cookie-manager")}
+
+
+ {t("setting.cookie-manager-description")}
+
+
+
+
+ {domains.length > 0 && (
+
+ )}
+
+
+
+ {/* Search Bar */}
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+ {/* Cookie List */}
+
+ {isLoading ? (
+
+
+ {t("setting.loading-cookies")}
+
+ ) : filteredDomains.length === 0 ? (
+
+
+
+ {domains.length === 0
+ ? t("setting.no-cookies-found")
+ : t("setting.no-matching-domains")}
+
+ {domains.length === 0 && (
+
+ {t("setting.login-to-save-cookies")}
+
+ )}
+
+ ) : (
+ filteredDomains.map((item) => (
+
+
+
+ {item.domain}
+
+
+
+ {t("setting.cookies-count", { count: item.cookie_count })}
+
+
+ {t("setting.last-access")}: {item.last_access}
+
+
+
+
+
+ ))
+ )}
+
+
+ {/* Warning */}
+ {domains.length > 0 && (
+
+
+
+
+ {t("setting.cookie-delete-warning")}
+
+
+
+ )}
+
+ );
+}
From 39badf3e531f13e5b4900a7ff0a8aff4bbebf33c Mon Sep 17 00:00:00 2001
From: puzhen <1303385763@qq.com>
Date: Tue, 21 Oct 2025 16:00:46 +0100
Subject: [PATCH 2/5] update
---
backend/app/utils/cookie_manager.py | 91 ++++-------------------------
1 file changed, 10 insertions(+), 81 deletions(-)
diff --git a/backend/app/utils/cookie_manager.py b/backend/app/utils/cookie_manager.py
index 217ca143..37cfc2fd 100644
--- a/backend/app/utils/cookie_manager.py
+++ b/backend/app/utils/cookie_manager.py
@@ -1,7 +1,3 @@
-"""
-Electron/Chrome Cookie Manager
-用于读取和管理Electron浏览器的cookies
-"""
import sqlite3
import os
from typing import List, Dict, Optional
@@ -13,21 +9,14 @@ logger = traceroot.get_logger("cookie_manager")
class CookieManager:
- """Cookie管理器,用于读取和管理浏览器cookies"""
+ """Manager for reading and managing browser cookies
+ from Electron/Chrome SQLite database"""
def __init__(self, user_data_dir: str):
- """
- 初始化Cookie管理器
-
- Args:
- user_data_dir: 浏览器用户数据目录
- """
self.user_data_dir = user_data_dir
self.cookies_db_path = os.path.join(user_data_dir, "Cookies")
- # Check for alternative paths
if not os.path.exists(self.cookies_db_path):
- # Try Network/Cookies path (some Electron versions)
alt_path = os.path.join(user_data_dir, "Network", "Cookies")
if os.path.exists(alt_path):
self.cookies_db_path = alt_path
@@ -35,22 +24,14 @@ class CookieManager:
logger.warning(f"Cookies database not found at {self.cookies_db_path}")
def _get_cookies_connection(self) -> Optional[sqlite3.Connection]:
- """
- 获取cookies数据库连接
-
- Returns:
- 数据库连接或None
- """
+ """Get database connection using a temporary copy to avoid locks"""
if not os.path.exists(self.cookies_db_path):
logger.warning(f"Cookies database not found: {self.cookies_db_path}")
return None
try:
- # Create a temporary copy since the database might be locked
temp_db_path = self.cookies_db_path + ".tmp"
shutil.copy2(self.cookies_db_path, temp_db_path)
-
- # Open the temporary copy
conn = sqlite3.connect(temp_db_path)
conn.row_factory = sqlite3.Row
return conn
@@ -59,7 +40,7 @@ class CookieManager:
return None
def _cleanup_temp_db(self):
- """清理临时数据库文件"""
+ """Clean up temporary database file"""
temp_db_path = self.cookies_db_path + ".tmp"
try:
if os.path.exists(temp_db_path):
@@ -68,20 +49,13 @@ class CookieManager:
logger.debug(f"Error cleaning up temp database: {e}")
def get_cookie_domains(self) -> List[Dict[str, any]]:
- """
- 获取所有有cookies的域名列表
-
- Returns:
- 域名列表,包含域名和cookie数量
- """
+ """Get list of all domains with cookies"""
conn = self._get_cookies_connection()
if not conn:
return []
try:
cursor = conn.cursor()
-
- # Group by host_key (domain) and count cookies
query = """
SELECT
host_key as domain,
@@ -91,19 +65,14 @@ class CookieManager:
GROUP BY host_key
ORDER BY last_access DESC
"""
-
cursor.execute(query)
rows = cursor.fetchall()
domains = []
for row in rows:
- # Convert Chrome timestamp (microseconds since 1601-01-01) to readable format
try:
- # Chrome timestamp is microseconds since 1601-01-01 UTC
chrome_timestamp = row['last_access']
if chrome_timestamp:
- # Convert to seconds since epoch (1970-01-01)
- # 11644473600 seconds between 1601-01-01 and 1970-01-01
seconds_since_epoch = (chrome_timestamp / 1000000.0) - 11644473600
last_access = datetime.fromtimestamp(seconds_since_epoch).strftime('%Y-%m-%d %H:%M:%S')
else:
@@ -129,22 +98,13 @@ class CookieManager:
self._cleanup_temp_db()
def get_cookies_for_domain(self, domain: str) -> List[Dict[str, str]]:
- """
- 获取指定域名的所有cookies
-
- Args:
- domain: 域名
-
- Returns:
- Cookie列表
- """
+ """Get all cookies for a specific domain"""
conn = self._get_cookies_connection()
if not conn:
return []
try:
cursor = conn.cursor()
-
query = """
SELECT
host_key,
@@ -158,8 +118,6 @@ class CookieManager:
WHERE host_key = ? OR host_key LIKE ?
ORDER BY name
"""
-
- # Match exact domain or subdomain pattern
cursor.execute(query, (domain, f'%.{domain}'))
rows = cursor.fetchall()
@@ -168,7 +126,7 @@ class CookieManager:
cookies.append({
'domain': row['host_key'],
'name': row['name'],
- 'value': row['value'][:50] + '...' if len(row['value']) > 50 else row['value'], # Truncate long values
+ 'value': row['value'][:50] + '...' if len(row['value']) > 50 else row['value'],
'path': row['path'],
'secure': bool(row['is_secure']),
'httponly': bool(row['is_httponly'])
@@ -184,33 +142,20 @@ class CookieManager:
self._cleanup_temp_db()
def delete_cookies_for_domain(self, domain: str) -> bool:
- """
- 删除指定域名的所有cookies
-
- Args:
- domain: 域名
-
- Returns:
- 是否删除成功
- """
+ """Delete all cookies for a specific domain"""
if not os.path.exists(self.cookies_db_path):
logger.warning(f"Cookies database not found: {self.cookies_db_path}")
return False
try:
- # Direct connection to the actual database (not a copy)
conn = sqlite3.connect(self.cookies_db_path)
cursor = conn.cursor()
-
- # Delete cookies for exact domain and subdomains
delete_query = """
DELETE FROM cookies
WHERE host_key = ? OR host_key LIKE ?
"""
-
cursor.execute(delete_query, (domain, f'%.{domain}'))
deleted_count = cursor.rowcount
-
conn.commit()
conn.close()
@@ -222,12 +167,7 @@ class CookieManager:
return False
def delete_all_cookies(self) -> bool:
- """
- 删除所有cookies
-
- Returns:
- 是否删除成功
- """
+ """Delete all cookies"""
if not os.path.exists(self.cookies_db_path):
logger.warning(f"Cookies database not found: {self.cookies_db_path}")
return False
@@ -235,10 +175,8 @@ class CookieManager:
try:
conn = sqlite3.connect(self.cookies_db_path)
cursor = conn.cursor()
-
cursor.execute("DELETE FROM cookies")
deleted_count = cursor.rowcount
-
conn.commit()
conn.close()
@@ -250,18 +188,9 @@ class CookieManager:
return False
def search_cookies(self, keyword: str) -> List[Dict[str, any]]:
- """
- 搜索包含关键词的cookies
-
- Args:
- keyword: 搜索关键词
-
- Returns:
- 匹配的域名列表
- """
+ """Search cookies by domain keyword"""
domains = self.get_cookie_domains()
keyword_lower = keyword.lower()
-
return [
domain for domain in domains
if keyword_lower in domain['domain'].lower()
From 69d8a77d9eb1ce1e4e2099592499bd14391ccdc2 Mon Sep 17 00:00:00 2001
From: puzhen <1303385763@qq.com>
Date: Tue, 21 Oct 2025 17:57:02 +0100
Subject: [PATCH 3/5] update
---
backend/app/controller/electron_browser.cjs | 294 ++++++++++++++++++++
backend/app/utils/agent.py | 16 +-
electron/main/fileReader.ts | 9 +
electron/main/index.ts | 4 +-
electron/main/webview.ts | 2 +-
tsconfig.json | 4 +-
6 files changed, 313 insertions(+), 16 deletions(-)
create mode 100644 backend/app/controller/electron_browser.cjs
diff --git a/backend/app/controller/electron_browser.cjs b/backend/app/controller/electron_browser.cjs
new file mode 100644
index 00000000..841e5787
--- /dev/null
+++ b/backend/app/controller/electron_browser.cjs
@@ -0,0 +1,294 @@
+
+const { app, BrowserWindow, ipcMain } = require('electron');
+const path = require('path');
+
+// Parse command line arguments
+const args = process.argv.slice(2);
+const userDataDir = args[0];
+const cdpPort = args[1];
+const startUrl = args[2] || 'https://www.google.com';
+
+// This must be called before app.ready
+app.commandLine.appendSwitch('remote-debugging-port', cdpPort);
+app.commandLine.appendSwitch('user-data-dir', userDataDir);
+
+console.log('[ELECTRON BROWSER] Starting with:');
+console.log(' Chrome version:', process.versions.chrome);
+console.log(' User data dir (requested):', userDataDir);
+console.log(' CDP port:', cdpPort);
+console.log(' Start URL:', startUrl);
+
+// Try to set app paths
+app.setPath('userData', userDataDir);
+app.setPath('sessionData', userDataDir);
+
+app.whenReady().then(() => {
+ // Log actual paths being used
+ console.log('[ELECTRON BROWSER] Actual paths:');
+ console.log(' app.getPath("userData"):', app.getPath('userData'));
+ console.log(' app.getPath("sessionData"):', app.getPath('sessionData'));
+ console.log(' app.getPath("cache"):', app.getPath('cache'));
+ console.log(' app.getPath("temp"):', app.getPath('temp'));
+ console.log(' process.argv:', process.argv);
+
+ // Check command line switches
+ console.log('[ELECTRON BROWSER] Command line switches:');
+ console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir'));
+ console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port'));
+ const win = new BrowserWindow({
+ width: 1400,
+ height: 900,
+ title: 'Eigent Browser - Login',
+ webPreferences: {
+ nodeIntegration: true,
+ contextIsolation: false,
+ webviewTag: true
+ }
+ });
+
+ // Create navigation bar and webview
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Browser Info
+
+
+
+
+
+
+`;
+
+ win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html));
+
+ // Show window when ready
+ win.once('ready-to-show', () => {
+ win.show();
+ });
+
+ win.on('closed', () => {
+ app.quit();
+ });
+});
+
+app.on('window-all-closed', () => {
+ app.quit();
+});
diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py
index 31f5eef3..dd3fd4fa 100644
--- a/backend/app/utils/agent.py
+++ b/backend/app/utils/agent.py
@@ -840,19 +840,13 @@ Your capabilities include:
Your approach depends on available search tools:
-**If Search Tools are Available:**
-- Initial Search: Start with `search_google` or `search_exa` to get a list of relevant URLs
+**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 Search Tools are NOT Available:**
-- **RECOMMENDED: Use Brave Search**: Navigate to `https://search.brave.com/`
- using `browser_visit_page`. Brave Search is highly recommended as it:
- * Works well without requiring login
- * Has fewer CAPTCHA challenges compared to other search engines
- * Provides clean, relevant search results
- * Supports advanced search queries
-- **Alternative Search Engines**: If Brave Search is unavailable, use
- `browser_visit_page` to go directly to other popular search engines:
+**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
diff --git a/electron/main/fileReader.ts b/electron/main/fileReader.ts
index 82905e38..91a06977 100644
--- a/electron/main/fileReader.ts
+++ b/electron/main/fileReader.ts
@@ -9,6 +9,15 @@ import https from 'https'
import http from 'http'
import { URL } from 'url'
+interface FileInfo {
+ path: string;
+ name: string;
+ type: string;
+ isFolder: boolean;
+ relativePath: string;
+ task_id?: string;
+ project_id?: string;
+}
export class FileReader {
private win: BrowserWindow | null = null
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 44d5f42d..fb472ab8 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -1088,10 +1088,10 @@ async function createWindow() {
// ==================== initialize manager ====================
fileReader = new FileReader(win);
- webViewManager = new WebViewManager(win, browser_port);
+ webViewManager = new WebViewManager(win);
// create multiple webviews
- log.info(`[PROJECT BROWSER] Creating WebViews with partition: persist:agent-webview`);
+ log.info(`[PROJECT BROWSER] Creating WebViews with partition: persist:user_login`);
for (let i = 1; i <= 8; i++) {
webViewManager.createWebview(i === 1 ? undefined : i.toString());
}
diff --git a/electron/main/webview.ts b/electron/main/webview.ts
index 8e686e6e..b4dfe4b6 100644
--- a/electron/main/webview.ts
+++ b/electron/main/webview.ts
@@ -66,7 +66,7 @@ export class WebViewManager {
webPreferences: {
// Use a separate session partition for webviews to isolate storage from main window
// This ensures clearing webview storage won't affect main window's auth data
- partition: 'persist:agent-webview',
+ partition: 'persist:user_login',
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: true,
diff --git a/tsconfig.json b/tsconfig.json
index 22e4977a..bf5abc28 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,12 +5,12 @@
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
- "esModuleInterop": false,
+ "esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
- "moduleResolution": "Node",
+ "moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
From 663f7702d4d419e8fb6338d15ad6ca0f24375cce Mon Sep 17 00:00:00 2001
From: puzhen <1303385763@qq.com>
Date: Tue, 21 Oct 2025 22:52:19 +0100
Subject: [PATCH 4/5] update
---
backend/app/controller/electron_browser.cjs | 294 --------------------
backend/app/controller/tool_controller.py | 277 ++++++++++++++++--
backend/app/utils/cookie_manager.py | 23 +-
electron/main/index.ts | 208 +++++++-------
4 files changed, 372 insertions(+), 430 deletions(-)
delete mode 100644 backend/app/controller/electron_browser.cjs
diff --git a/backend/app/controller/electron_browser.cjs b/backend/app/controller/electron_browser.cjs
deleted file mode 100644
index 841e5787..00000000
--- a/backend/app/controller/electron_browser.cjs
+++ /dev/null
@@ -1,294 +0,0 @@
-
-const { app, BrowserWindow, ipcMain } = require('electron');
-const path = require('path');
-
-// Parse command line arguments
-const args = process.argv.slice(2);
-const userDataDir = args[0];
-const cdpPort = args[1];
-const startUrl = args[2] || 'https://www.google.com';
-
-// This must be called before app.ready
-app.commandLine.appendSwitch('remote-debugging-port', cdpPort);
-app.commandLine.appendSwitch('user-data-dir', userDataDir);
-
-console.log('[ELECTRON BROWSER] Starting with:');
-console.log(' Chrome version:', process.versions.chrome);
-console.log(' User data dir (requested):', userDataDir);
-console.log(' CDP port:', cdpPort);
-console.log(' Start URL:', startUrl);
-
-// Try to set app paths
-app.setPath('userData', userDataDir);
-app.setPath('sessionData', userDataDir);
-
-app.whenReady().then(() => {
- // Log actual paths being used
- console.log('[ELECTRON BROWSER] Actual paths:');
- console.log(' app.getPath("userData"):', app.getPath('userData'));
- console.log(' app.getPath("sessionData"):', app.getPath('sessionData'));
- console.log(' app.getPath("cache"):', app.getPath('cache'));
- console.log(' app.getPath("temp"):', app.getPath('temp'));
- console.log(' process.argv:', process.argv);
-
- // Check command line switches
- console.log('[ELECTRON BROWSER] Command line switches:');
- console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir'));
- console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port'));
- const win = new BrowserWindow({
- width: 1400,
- height: 900,
- title: 'Eigent Browser - Login',
- webPreferences: {
- nodeIntegration: true,
- contextIsolation: false,
- webviewTag: true
- }
- });
-
- // Create navigation bar and webview
- const html = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Browser Info
-
-
-
-
-
-
-`;
-
- win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html));
-
- // Show window when ready
- win.once('ready-to-show', () => {
- win.show();
- });
-
- win.on('closed', () => {
- app.quit();
- });
-});
-
-app.on('window-all-closed', () => {
- app.quit();
-});
diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py
index 311fd4ae..f1494a01 100644
--- a/backend/app/controller/tool_controller.py
+++ b/backend/app/controller/tool_controller.py
@@ -144,9 +144,12 @@ async def open_browser_login():
session_id = "user_login"
cdp_port = 9223
- # Create user data directory for Chrome profiles
- user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
- user_data_dir = os.path.join(user_data_base, "profile_user_login")
+ # IMPORTANT: Use dedicated profile for tool_controller browser
+ # This is the SOURCE OF TRUTH for login data
+ # On Eigent startup, this data will be copied to WebView partition (one-way sync)
+ browser_profiles_base = os.path.expanduser("~/.eigent/browser_profiles")
+ user_data_dir = os.path.join(browser_profiles_base, "profile_user_login")
+
os.makedirs(user_data_dir, exist_ok=True)
logger.info(
@@ -182,7 +185,6 @@ const startUrl = args[2] || 'https://www.google.com';
// This must be called before app.ready
app.commandLine.appendSwitch('remote-debugging-port', cdpPort);
-app.commandLine.appendSwitch('user-data-dir', userDataDir);
console.log('[ELECTRON BROWSER] Starting with:');
console.log(' Chrome version:', process.versions.chrome);
@@ -190,11 +192,16 @@ console.log(' User data dir (requested):', userDataDir);
console.log(' CDP port:', cdpPort);
console.log(' Start URL:', startUrl);
-// Try to set app paths
+// Set app paths - must be done before app.ready
+// Do NOT use commandLine.appendSwitch('user-data-dir') as it conflicts with setPath
app.setPath('userData', userDataDir);
app.setPath('sessionData', userDataDir);
-app.whenReady().then(() => {
+app.whenReady().then(async () => {
+ const { session } = require('electron');
+ const fs = require('fs');
+ const path = require('path');
+
// Log actual paths being used
console.log('[ELECTRON BROWSER] Actual paths:');
console.log(' app.getPath("userData"):', app.getPath('userData'));
@@ -202,11 +209,81 @@ app.whenReady().then(() => {
console.log(' app.getPath("cache"):', app.getPath('cache'));
console.log(' app.getPath("temp"):', app.getPath('temp'));
console.log(' process.argv:', process.argv);
-
+
// Check command line switches
console.log('[ELECTRON BROWSER] Command line switches:');
console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir'));
console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port'));
+
+ // Import cookies from JSON backup if exists
+ const mainAppUserData = process.platform === 'darwin'
+ ? path.join(require('os').homedir(), 'Library/Application Support/eigent')
+ : process.platform === 'win32'
+ ? path.join(process.env.APPDATA, 'eigent')
+ : path.join(require('os').homedir(), '.config/eigent');
+
+ const cookiesJsonPath = path.join(mainAppUserData, 'Partitions', 'user_login', 'cookies_backup.json');
+
+ console.log('[ELECTRON BROWSER] Checking for cookies JSON backup:', cookiesJsonPath);
+
+ if (fs.existsSync(cookiesJsonPath)) {
+ try {
+ const cookiesJson = fs.readFileSync(cookiesJsonPath, 'utf8');
+ const cookies = JSON.parse(cookiesJson);
+ console.log('[ELECTRON BROWSER] Found', cookies.length, 'cookies in backup, importing...');
+
+ // Get session after app is ready
+ const userLoginSession = session.fromPartition('persist:user_login');
+
+ // Import each cookie
+ let imported = 0;
+ for (const cookie of cookies) {
+ try {
+ // Remove read-only properties
+ const cookieToSet = {
+ url: `http${cookie.secure ? 's' : ''}://${cookie.domain.startsWith('.') ? cookie.domain.substring(1) : cookie.domain}${cookie.path}`,
+ name: cookie.name,
+ value: cookie.value,
+ domain: cookie.domain,
+ path: cookie.path,
+ secure: cookie.secure,
+ httpOnly: cookie.httpOnly,
+ expirationDate: cookie.expirationDate
+ };
+
+ await userLoginSession.cookies.set(cookieToSet);
+ imported++;
+ } catch (err) {
+ console.error('[ELECTRON BROWSER] Failed to import cookie:', cookie.name, err.message);
+ }
+ }
+
+ console.log('[ELECTRON BROWSER] Successfully imported', imported, 'cookies');
+
+ // Flush to disk immediately
+ await userLoginSession.flushStorageData();
+ console.log('[ELECTRON BROWSER] Cookies flushed to disk');
+ } catch (error) {
+ console.error('[ELECTRON BROWSER] Failed to import cookies from JSON:', error);
+ }
+ } else {
+ console.log('[ELECTRON BROWSER] No cookies backup found');
+ }
+
+ // Log partition session info
+ const userLoginSession = session.fromPartition('persist:user_login');
+ console.log('[ELECTRON BROWSER] Session info:');
+ console.log(' Partition: persist:user_login');
+ console.log(' Session storage path:', userLoginSession.getStoragePath());
+
+ // Check if Cookies file exists
+ const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies');
+ console.log('[ELECTRON BROWSER] Cookies path:', cookiesPath);
+ console.log('[ELECTRON BROWSER] Cookies exists:', fs.existsSync(cookiesPath));
+ if (fs.existsSync(cookiesPath)) {
+ const stats = fs.statSync(cookiesPath);
+ console.log('[ELECTRON BROWSER] Cookies file size:', stats.size, 'bytes');
+ }
const win = new BrowserWindow({
width: 1400,
height: 900,
@@ -450,19 +527,127 @@ app.whenReady().then(() => {