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 = ` + + + + + + + + + + + + + + +`; + + 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 = ` + + + + + + + + + + + + + + +`; + + 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 = ` - - - - - - - - - - - - - - -`; - - 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(() => { `; win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html)); - + // Show window when ready win.once('ready-to-show', () => { win.show(); + + // Log cookies periodically to track changes + setInterval(async () => { + try { + const cookies = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Current cookies count:', cookies.length); + if (cookies.length > 0) { + console.log('[ELECTRON BROWSER] Cookie domains:', [...new Set(cookies.map(c => c.domain))]); + } + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to get cookies:', error); + } + }, 5000); // Check every 5 seconds }); - win.on('closed', () => { + win.on('closed', async () => { + console.log('[ELECTRON BROWSER] Window closed, preparing to quit...'); + + // Flush storage data before quitting to ensure cookies are saved + try { + const { session } = require('electron'); + const fs = require('fs'); + const path = require('path'); + const userLoginSession = session.fromPartition('persist:user_login'); + + // Log cookies before flush + const cookiesBeforeFlush = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Cookies count before flush:', cookiesBeforeFlush.length); + + // Flush storage + console.log('[ELECTRON BROWSER] Flushing storage data...'); + await userLoginSession.flushStorageData(); + console.log('[ELECTRON BROWSER] Storage data flushed successfully'); + + // Check cookies file after flush + const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies'); + if (fs.existsSync(cookiesPath)) { + const stats = fs.statSync(cookiesPath); + console.log('[ELECTRON BROWSER] Cookies file size after flush:', stats.size, 'bytes'); + } else { + console.log('[ELECTRON BROWSER] WARNING: Cookies file does not exist after flush!'); + } + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to flush storage data:', error); + } app.quit(); }); }); +let isQuitting = false; + +app.on('before-quit', async (event) => { + if (isQuitting) return; + + // Prevent immediate quit to allow storage flush and cookie sync + event.preventDefault(); + isQuitting = true; + + console.log('[ELECTRON BROWSER] before-quit event triggered'); + + try { + const { session } = require('electron'); + const fs = require('fs'); + const path = require('path'); + const userLoginSession = session.fromPartition('persist:user_login'); + + // Log cookies before flush + const cookiesBeforeQuit = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Cookies count before quit:', cookiesBeforeQuit.length); + if (cookiesBeforeQuit.length > 0) { + console.log('[ELECTRON BROWSER] Cookie domains before quit:', [...new Set(cookiesBeforeQuit.map(c => c.domain))]); + } + + // Flush storage + console.log('[ELECTRON BROWSER] Flushing storage on quit...'); + await userLoginSession.flushStorageData(); + console.log('[ELECTRON BROWSER] Storage data flushed on quit'); + + // Export cookies as JSON to sync to main app + try { + 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'); + + // Get all cookies + const allCookies = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Exporting', allCookies.length, 'cookies to JSON'); + + // Ensure directory exists + const cookiesDir = path.dirname(cookiesJsonPath); + if (!fs.existsSync(cookiesDir)) { + fs.mkdirSync(cookiesDir, { recursive: true }); + } + + // Save cookies as JSON + fs.writeFileSync(cookiesJsonPath, JSON.stringify(allCookies, null, 2)); + console.log('[ELECTRON BROWSER] Cookies exported to:', cookiesJsonPath); + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to export cookies:', error); + } + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to sync cookies:', error); + } finally { + console.log('[ELECTRON BROWSER] Exiting now...'); + // Force quit after sync + app.exit(0); + } +}); + app.on('window-all-closed', () => { - app.quit(); + if (!isQuitting) { + app.quit(); + } }); ''' @@ -487,13 +672,27 @@ app.on('window-all-closed', () => { 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}") - + logger.info(f"[PROFILE USER LOGIN] userData path: {user_data_dir}") + logger.info(f"[PROFILE USER LOGIN] Electron args: {electron_args}") + + # Start process and capture output in real-time process = subprocess.Popen( electron_args, - cwd=app_dir, # Run in app directory to use the right Electron version + cwd=app_dir, stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.STDOUT, # Redirect stderr to stdout + universal_newlines=True, + bufsize=1 # Line buffered ) + + # Create async task to log Electron output + async def log_electron_output(): + for line in iter(process.stdout.readline, ''): + if line: + logger.info(f"[ELECTRON OUTPUT] {line.strip()}") + + import asyncio + asyncio.create_task(log_electron_output()) # Wait a bit for Electron to start import asyncio @@ -533,19 +732,47 @@ app.on('window-all-closed', () => { @router.get("/browser/cookies", name="list cookie domains") async def list_cookie_domains(search: str = None): """ - 列出所有有cookies的网站域名 + list cookie domains Args: - search: 可选的搜索关键词,用于过滤域名 + search: url Returns: - 域名列表,包含域名、cookie数量和最后访问时间 + list of cookie domains """ try: - # Use the same user data directory as the login browser + # Use tool_controller browser's user data directory (source of truth) user_data_base = os.path.expanduser("~/.eigent/browser_profiles") user_data_dir = os.path.join(user_data_base, "profile_user_login") + logger.info(f"[COOKIES CHECK] Tool controller user_data_dir: {user_data_dir}") + logger.info(f"[COOKIES CHECK] Tool controller user_data_dir exists: {os.path.exists(user_data_dir)}") + + # Check partition path + partition_path = os.path.join(user_data_dir, "Partitions", "user_login") + logger.info(f"[COOKIES CHECK] partition path: {partition_path}") + logger.info(f"[COOKIES CHECK] partition exists: {os.path.exists(partition_path)}") + + # Check cookies file + cookies_file = os.path.join(partition_path, "Cookies") + logger.info(f"[COOKIES CHECK] cookies file: {cookies_file}") + logger.info(f"[COOKIES CHECK] cookies file exists: {os.path.exists(cookies_file)}") + if os.path.exists(cookies_file): + stat = os.stat(cookies_file) + logger.info(f"[COOKIES CHECK] cookies file size: {stat.st_size} bytes") + + # Try to read actual cookie count + try: + import sqlite3 + conn = sqlite3.connect(cookies_file) + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM cookies") + count = cursor.fetchone()[0] + logger.info(f"[COOKIES CHECK] actual cookie count in database: {count}") + conn.close() + except Exception as e: + logger.error(f"[COOKIES CHECK] failed to read cookie count: {e}") + if not os.path.exists(user_data_dir): return { "success": True, @@ -578,13 +805,13 @@ async def list_cookie_domains(search: str = None): @router.get("/browser/cookies/{domain}", name="get domain cookies") async def get_domain_cookies(domain: str): """ - 获取指定域名的cookies详情 + get domain cookies Args: - domain: 域名(如 linkedin.com) + domain Returns: - 该域名的所有cookies + cookies """ try: user_data_base = os.path.expanduser("~/.eigent/browser_profiles") @@ -619,13 +846,13 @@ async def get_domain_cookies(domain: str): @router.delete("/browser/cookies/{domain}", name="delete domain cookies") async def delete_domain_cookies(domain: str): """ - 删除指定域名的所有cookies + Delete cookies Args: - domain: 域名(如 linkedin.com) + domain Returns: - 删除结果 + deleted cookies """ try: user_data_base = os.path.expanduser("~/.eigent/browser_profiles") @@ -664,10 +891,10 @@ async def delete_domain_cookies(domain: str): @router.delete("/browser/cookies", name="delete all cookies") async def delete_all_cookies(): """ - 删除所有cookies + delete all cookies Returns: - 删除结果 + deleted cookies """ try: user_data_base = os.path.expanduser("~/.eigent/browser_profiles") diff --git a/backend/app/utils/cookie_manager.py b/backend/app/utils/cookie_manager.py index 37cfc2fd..9dfc03d5 100644 --- a/backend/app/utils/cookie_manager.py +++ b/backend/app/utils/cookie_manager.py @@ -14,14 +14,23 @@ class CookieManager: def __init__(self, user_data_dir: str): self.user_data_dir = user_data_dir - self.cookies_db_path = os.path.join(user_data_dir, "Cookies") - if not os.path.exists(self.cookies_db_path): - 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}") + # Check for cookies in partition directory first (for persist:user_login) + partition_cookies_path = os.path.join(user_data_dir, "Partitions", "user_login", "Cookies") + + if os.path.exists(partition_cookies_path): + self.cookies_db_path = partition_cookies_path + logger.info(f"Using partition cookies at: {partition_cookies_path}") + else: + # Fallback to default location + self.cookies_db_path = os.path.join(user_data_dir, "Cookies") + + if not os.path.exists(self.cookies_db_path): + 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} or {partition_cookies_path}") def _get_cookies_connection(self) -> Optional[sqlite3.Connection]: """Get database connection using a temporary copy to avoid locks""" diff --git a/electron/main/index.ts b/electron/main/index.ts index fb472ab8..627f24da 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -52,74 +52,75 @@ const logPath = log.transports.file.getFile().path; // 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 +// Set remote debugging port +// Storage strategy: +// 1. Main window uses partition 'persist:main_window' in app userData - for Eigent account auth +// 2. WebView uses partition 'persist:user_login' in app userData - receives login data from tool_controller +// 3. tool_controller browser uses ~/.eigent/browser_profiles/profile_user_login - persistent source of truth +// 4. On startup: Copy tool_controller login data → WebView (one-way, read-only) +// 5. On shutdown: No sync back (WebView changes are temporary) 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 + // Create a simple isolated profile for CDP browser only const browserProfilesBase = path.join(os.homedir(), '.eigent', 'browser_profiles'); - const sourceProfile = path.join(browserProfilesBase, 'profile_user_login'); - const targetProfile = path.join(browserProfilesBase, `profile_${port}`); + const cdpProfile = path.join(browserProfilesBase, `cdp_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`); + await fsp.mkdir(cdpProfile, { recursive: true }); + log.info(`[CDP BROWSER] Created CDP profile directory at ${cdpProfile}`); + } catch (error) { + log.error(`[CDP BROWSER] Failed to create directory: ${error}`); + } + + // Set user-data-dir for Chrome DevTools Protocol only + // This affects the embedded Chromium browser launched with CDP, not the main Electron app + app.commandLine.appendSwitch('user-data-dir', cdpProfile); + + log.info(`[CDP BROWSER] Chrome DevTools Protocol enabled on port ${port}`); + log.info(`[CDP BROWSER] CDP profile directory: ${cdpProfile}`); + + // ==================== One-way sync: tool_controller → WebView ==================== + // Copy login data from tool_controller browser to WebView partition (if exists) + const toolControllerProfile = path.join(browserProfilesBase, 'profile_user_login'); + const appUserData = app.getPath('userData'); + const webViewPartition = path.join(appUserData, 'Partitions', 'user_login'); + + try { + // Check if tool_controller has login data + const toolControllerPartition = path.join(toolControllerProfile, 'Partitions', 'user_login'); + + if (fs.existsSync(toolControllerPartition)) { + log.info('[LOGIN SYNC] Found tool_controller login data, copying to WebView partition...'); + log.info('[LOGIN SYNC] Source:', toolControllerPartition); + log.info('[LOGIN SYNC] Target:', webViewPartition); + + // Ensure target directory exists + await fsp.mkdir(webViewPartition, { recursive: true }); + + // Copy the entire partition (includes Cookies, Local Storage, etc.) + await fsp.cp(toolControllerPartition, webViewPartition, { + recursive: true, + force: true // Overwrite existing files + }); + + log.info('[LOGIN SYNC] Successfully copied login data to WebView partition'); + + // Log verification + const files = await fsp.readdir(webViewPartition); + log.info(`[LOGIN SYNC] WebView partition now 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`); + const cookieStat = await fsp.stat(path.join(webViewPartition, 'Cookies')); + log.info(`[LOGIN SYNC] Cookies file size: ${cookieStat.size} bytes`); } + } else { + log.info('[LOGIN SYNC] No tool_controller login data found, WebView will start fresh'); } } 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}`); - } + log.error('[LOGIN SYNC] Failed to copy login data:', error); + // Non-fatal error, continue startup } - - // 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 @@ -1077,6 +1078,9 @@ async function createWindow() { icon: path.join(VITE_PUBLIC, 'favicon.ico'), roundedCorners: true, webPreferences: { + // Use a dedicated partition for main window to isolate from webviews + // This ensures main window's auth data (localStorage) is stored separately and persists across restarts + partition: 'persist:main_window', webSecurity: false, preload, nodeIntegration: true, @@ -1086,6 +1090,39 @@ async function createWindow() { }, }); + // ==================== Migrate localStorage from default session to main_window partition ==================== + try { + const defaultLocalStoragePath = path.join(app.getPath('userData'), 'Local Storage', 'leveldb'); + const partitionLocalStoragePath = path.join(app.getPath('userData'), 'Partitions', 'main_window', 'Local Storage', 'leveldb'); + + // Check if default localStorage exists and partition localStorage doesn't + if (fs.existsSync(defaultLocalStoragePath) && !fs.existsSync(partitionLocalStoragePath)) { + log.info('[MIGRATION] Migrating localStorage from default session to main_window partition'); + log.info('[MIGRATION] Source:', defaultLocalStoragePath); + log.info('[MIGRATION] Target:', partitionLocalStoragePath); + + // Ensure target directory exists + const targetDir = path.dirname(partitionLocalStoragePath); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Copy the entire leveldb directory + fs.cpSync(defaultLocalStoragePath, partitionLocalStoragePath, { recursive: true }); + log.info('[MIGRATION] Successfully migrated localStorage to main_window partition'); + + // Optionally remove old default localStorage to avoid confusion + // fs.rmSync(defaultLocalStoragePath, { recursive: true, force: true }); + // log.info('[MIGRATION] Removed old default localStorage'); + } else if (fs.existsSync(partitionLocalStoragePath)) { + log.info('[MIGRATION] Partition localStorage already exists, skipping migration'); + } else { + log.info('[MIGRATION] No default localStorage found, skipping migration'); + } + } catch (error) { + log.error('[MIGRATION] Failed to migrate localStorage:', error); + } + // ==================== initialize manager ==================== fileReader = new FileReader(win); webViewManager = new WebViewManager(win); @@ -1148,7 +1185,9 @@ async function createWindow() { log.info('Installation needed - clearing auth storage to force carousel state'); // Clear the persisted auth storage file to force fresh initialization with carousel - const localStoragePath = path.join(app.getPath('userData'), 'Local Storage'); + // Main window uses partition 'persist:main_window', so data is in Partitions/main_window + const partitionPath = path.join(app.getPath('userData'), 'Partitions', 'main_window'); + const localStoragePath = path.join(partitionPath, 'Local Storage'); const leveldbPath = path.join(localStoragePath, 'leveldb'); try { @@ -1214,8 +1253,10 @@ async function createWindow() { (function() { try { const authStorage = localStorage.getItem('auth-storage'); + console.log('[ELECTRON DEBUG] Current auth-storage:', authStorage); if (authStorage) { const parsed = JSON.parse(authStorage); + console.log('[ELECTRON DEBUG] Parsed state:', parsed.state); if (parsed.state && parsed.state.initState !== 'done') { console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done'); // Only update the initState field, preserve all other data @@ -1229,7 +1270,11 @@ async function createWindow() { localStorage.setItem('auth-storage', JSON.stringify(updatedStorage)); console.log('[ELECTRON] initState updated to done, reloading page...'); return true; // Signal that we need to reload + } else { + console.log('[ELECTRON DEBUG] initState already done or state missing'); } + } else { + console.log('[ELECTRON DEBUG] No auth-storage found in localStorage'); } return false; // No reload needed } catch (e) { @@ -1542,53 +1587,8 @@ app.on('before-quit', async (event) => { 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); - } - } + // NOTE: Profile sync removed - we now use app userData directly for all partitions + // No need to sync between different profile directories // Clean up resources if (webViewManager) { From 960b6f513579fd9add07b72a381fef93603f8fa6 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 21 Oct 2025 23:21:54 +0100 Subject: [PATCH 5/5] update --- electron/main/index.ts | 107 ++++++------------ electron/main/webview.ts | 8 +- utils/__pycache__/__init__.cpython-310.pyc | Bin 155 -> 157 bytes .../traceroot_wrapper.cpython-310.pyc | Bin 2359 -> 2361 bytes 4 files changed, 41 insertions(+), 74 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 627f24da..8a133e2d 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -54,16 +54,15 @@ let profileInitPromise: Promise; // Set remote debugging port // Storage strategy: -// 1. Main window uses partition 'persist:main_window' in app userData - for Eigent account auth -// 2. WebView uses partition 'persist:user_login' in app userData - receives login data from tool_controller -// 3. tool_controller browser uses ~/.eigent/browser_profiles/profile_user_login - persistent source of truth -// 4. On startup: Copy tool_controller login data → WebView (one-way, read-only) -// 5. On shutdown: No sync back (WebView changes are temporary) +// 1. Main window: partition 'persist:main_window' in app userData → Eigent account (persistent) +// 2. WebView: partition 'persist:user_login' in app userData → will import cookies from tool_controller via session API +// 3. tool_controller: ~/.eigent/browser_profiles/profile_user_login → source of truth for login cookies +// 4. CDP browser: uses separate profile (doesn't share with main app) profileInitPromise = findAvailablePort(browser_port).then(async port => { browser_port = port; app.commandLine.appendSwitch('remote-debugging-port', port + ''); - // Create a simple isolated profile for CDP browser only + // Create isolated profile for CDP browser only const browserProfilesBase = path.join(os.homedir(), '.eigent', 'browser_profiles'); const cdpProfile = path.join(browserProfilesBase, `cdp_profile_${port}`); @@ -75,52 +74,11 @@ profileInitPromise = findAvailablePort(browser_port).then(async port => { } // Set user-data-dir for Chrome DevTools Protocol only - // This affects the embedded Chromium browser launched with CDP, not the main Electron app app.commandLine.appendSwitch('user-data-dir', cdpProfile); log.info(`[CDP BROWSER] Chrome DevTools Protocol enabled on port ${port}`); log.info(`[CDP BROWSER] CDP profile directory: ${cdpProfile}`); - - // ==================== One-way sync: tool_controller → WebView ==================== - // Copy login data from tool_controller browser to WebView partition (if exists) - const toolControllerProfile = path.join(browserProfilesBase, 'profile_user_login'); - const appUserData = app.getPath('userData'); - const webViewPartition = path.join(appUserData, 'Partitions', 'user_login'); - - try { - // Check if tool_controller has login data - const toolControllerPartition = path.join(toolControllerProfile, 'Partitions', 'user_login'); - - if (fs.existsSync(toolControllerPartition)) { - log.info('[LOGIN SYNC] Found tool_controller login data, copying to WebView partition...'); - log.info('[LOGIN SYNC] Source:', toolControllerPartition); - log.info('[LOGIN SYNC] Target:', webViewPartition); - - // Ensure target directory exists - await fsp.mkdir(webViewPartition, { recursive: true }); - - // Copy the entire partition (includes Cookies, Local Storage, etc.) - await fsp.cp(toolControllerPartition, webViewPartition, { - recursive: true, - force: true // Overwrite existing files - }); - - log.info('[LOGIN SYNC] Successfully copied login data to WebView partition'); - - // Log verification - const files = await fsp.readdir(webViewPartition); - log.info(`[LOGIN SYNC] WebView partition now contains ${files.length} files`); - if (files.includes('Cookies')) { - const cookieStat = await fsp.stat(path.join(webViewPartition, 'Cookies')); - log.info(`[LOGIN SYNC] Cookies file size: ${cookieStat.size} bytes`); - } - } else { - log.info('[LOGIN SYNC] No tool_controller login data found, WebView will start fresh'); - } - } catch (error) { - log.error('[LOGIN SYNC] Failed to copy login data:', error); - // Non-fatal error, continue startup - } + log.info(`[STORAGE] Main app userData: ${app.getPath('userData')}`); }); // Memory optimization settings @@ -1090,37 +1048,46 @@ async function createWindow() { }, }); - // ==================== Migrate localStorage from default session to main_window partition ==================== - try { - const defaultLocalStoragePath = path.join(app.getPath('userData'), 'Local Storage', 'leveldb'); - const partitionLocalStoragePath = path.join(app.getPath('userData'), 'Partitions', 'main_window', 'Local Storage', 'leveldb'); + // Main window now uses default userData directly with partition 'persist:main_window' + // No migration needed - data is already persistent - // Check if default localStorage exists and partition localStorage doesn't - if (fs.existsSync(defaultLocalStoragePath) && !fs.existsSync(partitionLocalStoragePath)) { - log.info('[MIGRATION] Migrating localStorage from default session to main_window partition'); - log.info('[MIGRATION] Source:', defaultLocalStoragePath); - log.info('[MIGRATION] Target:', partitionLocalStoragePath); + // ==================== Import cookies from tool_controller to WebView BEFORE creating WebViews ==================== + // Copy partition data files before any session accesses them + try { + const browserProfilesBase = path.join(os.homedir(), '.eigent', 'browser_profiles'); + const toolControllerProfile = path.join(browserProfilesBase, 'profile_user_login'); + const toolControllerPartitionPath = path.join(toolControllerProfile, 'Partitions', 'user_login'); + + if (fs.existsSync(toolControllerPartitionPath)) { + log.info('[COOKIE SYNC] Found tool_controller partition, copying to WebView partition...'); + + const targetPartitionPath = path.join(app.getPath('userData'), 'Partitions', 'user_login'); + log.info('[COOKIE SYNC] From:', toolControllerPartitionPath); + log.info('[COOKIE SYNC] To:', targetPartitionPath); // Ensure target directory exists - const targetDir = path.dirname(partitionLocalStoragePath); - if (!fs.existsSync(targetDir)) { - fs.mkdirSync(targetDir, { recursive: true }); + if (!fs.existsSync(path.dirname(targetPartitionPath))) { + fs.mkdirSync(path.dirname(targetPartitionPath), { recursive: true }); } - // Copy the entire leveldb directory - fs.cpSync(defaultLocalStoragePath, partitionLocalStoragePath, { recursive: true }); - log.info('[MIGRATION] Successfully migrated localStorage to main_window partition'); + // Copy the entire partition directory + fs.cpSync(toolControllerPartitionPath, targetPartitionPath, { + recursive: true, + force: true + }); + log.info('[COOKIE SYNC] Successfully copied partition data to WebView'); - // Optionally remove old default localStorage to avoid confusion - // fs.rmSync(defaultLocalStoragePath, { recursive: true, force: true }); - // log.info('[MIGRATION] Removed old default localStorage'); - } else if (fs.existsSync(partitionLocalStoragePath)) { - log.info('[MIGRATION] Partition localStorage already exists, skipping migration'); + // Verify cookies were copied + const targetCookies = path.join(targetPartitionPath, 'Cookies'); + if (fs.existsSync(targetCookies)) { + const stats = fs.statSync(targetCookies); + log.info(`[COOKIE SYNC] Cookies file size: ${stats.size} bytes`); + } } else { - log.info('[MIGRATION] No default localStorage found, skipping migration'); + log.info('[COOKIE SYNC] No tool_controller partition found, WebView will start fresh'); } } catch (error) { - log.error('[MIGRATION] Failed to migrate localStorage:', error); + log.error('[COOKIE SYNC] Failed to sync partition data:', error); } // ==================== initialize manager ==================== diff --git a/electron/main/webview.ts b/electron/main/webview.ts index b4dfe4b6..8fae65a6 100644 --- a/electron/main/webview.ts +++ b/electron/main/webview.ts @@ -272,11 +272,11 @@ export class WebViewManager { if (!webViewInfo.view.webContents.isDestroyed()) { webViewInfo.view.webContents.removeAllListeners() - // Now safe to clear all storage since webviews use separate partition + // DO NOT clear storage data here! + // Multiple webviews share the same partition 'persist:user_login' + // Clearing storage would affect ALL webviews and remove login cookies + // Only clear cache which is per-webContents webViewInfo.view.webContents.session.clearCache() - webViewInfo.view.webContents.session.clearStorageData({ - storages: ['cookies', 'localstorage', 'websql', 'indexdb', 'serviceworkers', 'cachestorage'] - }) } // remove webview from parent container diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc index 9b563d940e9b0cbef526e4e7b35dc01daf8bc128..bb47afeffa73ed23f5e37c0b19a3c7f5da95fcb6 100644 GIT binary patch delta 63 zcmbQuIG2$-pO=@50SIPI{63M}M#Dirv^ce>SihjODkC*d-zBv;yClCrzn~~pKRGcs RH7CBHC_gJTxnyFl9sp-*6?Xst delta 61 zcmbQsIGd3>pO=@50SGu!zD?w|QM1zzElw>e)=$kVO3l=FNiEJU$uH0^N-f9-ie;v! O=9L&37#d8>*8>0${Sx;8 diff --git a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc index efc367eaae5dad27f153ea88fc4f206c53dcd053..955e93d45139df618e6be5920c34113ff97244ff 100644 GIT binary patch delta 66 zcmdlkv{Q&XpO=@50SIPI{JxQU1CxfQerR!OQL%nOX;nsQp1w4FGUo6t(~W