diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 316ae22e5..3f024a1db 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -5,6 +5,6 @@ "node licenses/update_license.js" ], "*.{js,jsx}": ["eslint --fix --no-warn-ignored", "prettier --write"], - "*.{json,css,md}": ["prettier --write"], + "*.{json,css}": ["prettier --write"], "*.py": ["node licenses/update_license.js"] } diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index cb195f7d3..161c809b4 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import platform +import threading import uuid from camel.messages import BaseMessage @@ -37,6 +38,116 @@ from app.utils.file_utils import get_working_directory +def _get_browser_port(browser: dict) -> int: + """Extract port from a browser config dict, with fallback to env default.""" + return int(browser.get("port", env("browser_port", "9222"))) + + +class CdpBrowserPoolManager: + """Manages CDP browser pool occupation to ensure + parallel tasks use different browsers.""" + + def __init__(self): + self._occupied_ports: dict[int, str] = {} + self._session_to_port: dict[str, int] = {} + self._session_to_task: dict[str, str | None] = {} + self._lock = threading.Lock() + + def acquire_browser( + self, + cdp_browsers: list[dict], + session_id: str, + task_id: str | None = None, + ) -> dict | None: + """Acquire an available browser from the pool. + + Args: + cdp_browsers: List of browser configurations. + session_id: Unique session identifier. + task_id: Optional task identifier for ownership tracking. + + Returns: + Browser configuration dict or None if all occupied. + """ + with self._lock: + for browser in cdp_browsers: + port = browser.get("port") + if port and port not in self._occupied_ports: + self._occupied_ports[port] = session_id + self._session_to_port[session_id] = port + self._session_to_task[session_id] = task_id + logger.info( + f"Acquired browser on port {port} for session " + f"{session_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + return browser + logger.warning( + f"No available browsers for session {session_id}. " + f"All occupied: {list(self._occupied_ports.keys())}" + ) + return None + + def release_browser(self, port: int, session_id: str): + """Release a browser back to the pool.""" + with self._lock: + if ( + port in self._occupied_ports + and self._occupied_ports[port] == session_id + ): + del self._occupied_ports[port] + self._session_to_port.pop(session_id, None) + self._session_to_task.pop(session_id, None) + logger.info( + f"Released browser on port {port} from session " + f"{session_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + else: + logger.warning( + f"Attempted to release browser on port {port} " + f"but it was not occupied by {session_id}" + ) + + def release_by_task(self, task_id: str) -> list[int]: + """Release all browsers associated with a task_id. + + Returns: + List of released ports. + """ + released_ports = [] + with self._lock: + sessions = [ + s for s, t in self._session_to_task.items() if t == task_id + ] + for session_id in sessions: + port = self._session_to_port.get(session_id) + if ( + port is not None + and self._occupied_ports.get(port) == session_id + ): + del self._occupied_ports[port] + released_ports.append(port) + self._session_to_port.pop(session_id, None) + self._session_to_task.pop(session_id, None) + if released_ports: + logger.info( + f"Released {len(released_ports)} browser(s) for " + f"task {task_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + return released_ports + + def get_occupied_ports(self) -> list[int]: + """Get list of currently occupied ports.""" + with self._lock: + return list(self._occupied_ports.keys()) + + +# Global CDP browser pool manager instance +_cdp_pool_manager = CdpBrowserPoolManager() + + def browser_agent(options: Chat): working_directory = get_working_directory(options) logger.info( @@ -49,14 +160,43 @@ def browser_agent(options: Chat): ).send_message_to_user ) + # Acquire CDP browser from pool or use default port + toolkit_session_id = str(uuid.uuid4())[:8] + selected_port = None + selected_is_external = False + + if options.cdp_browsers: + selected_browser = _cdp_pool_manager.acquire_browser( + options.cdp_browsers, toolkit_session_id, options.task_id + ) + if selected_browser: + selected_port = _get_browser_port(selected_browser) + selected_is_external = selected_browser.get("isExternal", False) + logger.info( + f"Acquired CDP browser from pool (initial): " + f"port={selected_port}, isExternal={selected_is_external}, " + f"session_id={toolkit_session_id}" + ) + else: + selected_port = _get_browser_port(options.cdp_browsers[0]) + selected_is_external = options.cdp_browsers[0].get( + "isExternal", False + ) + logger.warning( + f"No available browsers in pool (initial), using first: " + f"port={selected_port}, session_id={toolkit_session_id}" + ) + else: + selected_port = env("browser_port", "9222") + web_toolkit_custom = HybridBrowserToolkit( options.project_id, + cdp_keep_current_page=True, headless=False, browser_log_to_file=True, stealth=True, - session_id=str(uuid.uuid4())[:8], - default_start_url="about:blank", - cdp_url=f"http://localhost:{env('browser_port', '9222')}", + session_id=toolkit_session_id, + cdp_url=f"http://localhost:{selected_port}", enabled_tools=[ "browser_click", "browser_type", @@ -72,6 +212,7 @@ def browser_agent(options: Chat): "browser_sheet_read", "browser_sheet_input", "browser_get_page_snapshot", + "browser_open", ], ) @@ -135,14 +276,28 @@ def browser_agent(options: Chat): *skill_toolkit.get_tools(), ] + # Build external browser notice + external_browser_notice = "" + if selected_is_external: + external_browser_notice = ( + "\n\n" + "**IMPORTANT**: You are connected to an external browser instance. " + "The browser may already be open with active sessions and logged-in " + "websites. When you use browser_open, you will connect to this " + "existing browser and can immediately access its current state and " + "pages.\n" + "\n" + ) + system_message = BROWSER_SYS_PROMPT.format( platform_system=platform.system(), platform_machine=platform.machine(), working_directory=working_directory, now_str=NOW_STR, + external_browser_notice=external_browser_notice, ) - return agent_model( + agent = agent_model( Agents.browser_agent, BaseMessage.make_assistant_message( role_name="Browser Agent", @@ -166,3 +321,45 @@ def browser_agent(options: Chat): ], enable_snapshot_clean=True, ) + + # Attach CDP management callbacks and info to the agent + def acquire_cdp_for_agent(agent_instance): + """Acquire a CDP browser from pool for a cloned agent.""" + if not options.cdp_browsers: + return + session_id = str(uuid.uuid4())[:8] + selected = _cdp_pool_manager.acquire_browser( + options.cdp_browsers, session_id, options.task_id + ) + if selected: + agent_instance._cdp_port = _get_browser_port(selected) + else: + agent_instance._cdp_port = _get_browser_port( + options.cdp_browsers[0] + ) + agent_instance._cdp_session_id = session_id + logger.info( + f"Acquired CDP for cloned agent {agent_instance.agent_id}: " + f"port={agent_instance._cdp_port}, session={session_id}" + ) + + def release_cdp_from_agent(agent_instance): + """Release CDP browser back to pool.""" + port = getattr(agent_instance, "_cdp_port", None) + session_id = getattr(agent_instance, "_cdp_session_id", None) + if port is not None and session_id is not None: + _cdp_pool_manager.release_browser(port, session_id) + logger.info( + f"Released CDP for agent {agent_instance.agent_id}: " + f"port={port}, session={session_id}" + ) + + agent._cdp_acquire_callback = acquire_cdp_for_agent + agent._cdp_release_callback = release_cdp_from_agent + agent._cdp_port = selected_port + agent._cdp_session_id = toolkit_session_id + agent._cdp_task_id = options.task_id + agent._cdp_options = options + agent._browser_toolkit = web_toolkit_for_agent_registration + + return agent diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 6bdb61ca5..7fbaddc6a 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -15,6 +15,7 @@ import asyncio import json import logging +import threading from collections.abc import Callable from threading import Event from typing import Any @@ -52,6 +53,10 @@ class ListenChatAgent(ChatAgent): + _cdp_clone_lock = ( + threading.Lock() + ) # Protects CDP URL mutation during clone + def __init__( self, api_task_id: str, @@ -692,8 +697,62 @@ def clone(self, with_memory: bool = False) -> ChatAgent: """Please see super.clone()""" system_message = None if with_memory else self._original_system_message - # Clone tools and collect toolkits that need registration - cloned_tools, toolkits_to_register = self._clone_tools() + # If this agent has CDP acquire callback, acquire CDP BEFORE cloning + # tools so that HybridBrowserToolkit clones with the correct CDP port + new_cdp_port = None + new_cdp_session = None + has_cdp = hasattr(self, "_cdp_acquire_callback") and callable( + getattr(self, "_cdp_acquire_callback", None) + ) + + need_cdp_clone = False + if has_cdp and hasattr(self, "_cdp_options"): + options = self._cdp_options + cdp_browsers = getattr(options, "cdp_browsers", []) + if cdp_browsers and hasattr(self, "_browser_toolkit"): + need_cdp_clone = True + import uuid as _uuid + + from app.agent.factory.browser import _cdp_pool_manager + + new_cdp_session = str(_uuid.uuid4())[:8] + selected = _cdp_pool_manager.acquire_browser( + cdp_browsers, + new_cdp_session, + getattr(self, "_cdp_task_id", None), + ) + from app.agent.factory.browser import _get_browser_port + + if selected: + new_cdp_port = _get_browser_port(selected) + else: + new_cdp_port = _get_browser_port(cdp_browsers[0]) + + if need_cdp_clone: + # Temporarily override the browser toolkit's CDP URL. + # Lock prevents concurrent clones from clobbering each + # other's cdp_url on the shared parent toolkit. + toolkit = self._browser_toolkit + with ListenChatAgent._cdp_clone_lock: + original_cdp_url = ( + toolkit.config_loader.get_browser_config().cdp_url + ) + toolkit.config_loader.get_browser_config().cdp_url = ( + f"http://localhost:{new_cdp_port}" + ) + try: + cloned_tools, toolkits_to_register = self._clone_tools() + except Exception: + _cdp_pool_manager.release_browser( + new_cdp_port, new_cdp_session + ) + raise + finally: + toolkit.config_loader.get_browser_config().cdp_url = ( + original_cdp_url + ) + else: + cloned_tools, toolkits_to_register = self._clone_tools() new_agent = ListenChatAgent( api_task_id=self.api_task_id, @@ -726,6 +785,31 @@ def clone(self, with_memory: bool = False) -> ChatAgent: new_agent.process_task_id = self.process_task_id + # Copy CDP management data to cloned agent + if has_cdp: + new_agent._cdp_acquire_callback = self._cdp_acquire_callback + new_agent._cdp_release_callback = self._cdp_release_callback + if hasattr(self, "_cdp_options"): + new_agent._cdp_options = self._cdp_options + if hasattr(self, "_cdp_task_id"): + new_agent._cdp_task_id = self._cdp_task_id + + # Find and store the cloned browser toolkit on the new agent + for tk in toolkits_to_register: + if tk.__class__.__name__ == "HybridBrowserToolkit": + new_agent._browser_toolkit = tk + break + + # Set CDP info on cloned agent + if new_cdp_port is not None and new_cdp_session is not None: + new_agent._cdp_port = new_cdp_port + new_agent._cdp_session_id = new_cdp_session + else: + if hasattr(self, "_cdp_port"): + new_agent._cdp_port = self._cdp_port + if hasattr(self, "_cdp_session_id"): + new_agent._cdp_session_id = self._cdp_session_id + # Copy memory if requested if with_memory: # Get all records from the current memory diff --git a/backend/app/agent/prompt.py b/backend/app/agent/prompt.py index 96872f9e8..9f38430f4 100644 --- a/backend/app/agent/prompt.py +++ b/backend/app/agent/prompt.py @@ -635,7 +635,7 @@ -Your approach depends on available search tools: +{external_browser_notice}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 diff --git a/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py b/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py index 096f0d7cb..e4635e178 100644 --- a/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py +++ b/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py @@ -174,6 +174,7 @@ def __init__( enabled_tools: list[str] | None = None, browser_log_to_file: bool = False, session_id: str | None = None, + log_base_dir: str | None = None, default_start_url: str = "https://google.com/", default_timeout: int | None = None, short_timeout: int | None = None, @@ -189,6 +190,7 @@ def __init__( self._stealth = stealth self._cache_dir = cache_dir self._browser_log_to_file = browser_log_to_file + self._log_base_dir = log_base_dir self._default_start_url = default_start_url self._session_id = session_id or "default" @@ -224,7 +226,12 @@ def __init__( # Set up log file if needed if self.log_to_file: # Create log directory if it doesn't exist - log_dir = "browser_log" + # If log_base_dir is provided, use task-specific directory; otherwise use default backend/browser_log + if log_base_dir: + log_dir = os.path.join(log_base_dir, "browser_logs") + else: + log_dir = "browser_log" # Backward compatibility: use default location + os.makedirs(log_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -351,6 +358,8 @@ def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: FunctionTool(browser.browser_visit_page), FunctionTool(browser.browser_scroll), FunctionTool(browser.browser_get_som_screenshot), + FunctionTool(browser.browser_sheet_read), + FunctionTool(browser.browser_sheet_input), # FunctionTool(browser.select), # FunctionTool(browser.wait_user), ] @@ -376,6 +385,7 @@ def clone_for_new_session( enabled_tools=self.enabled_tools.copy(), browser_log_to_file=self._browser_log_to_file, session_id=new_session_id, + log_base_dir=self._log_base_dir, default_start_url=self._default_start_url, default_timeout=self._default_timeout, short_timeout=self._short_timeout, diff --git a/backend/app/agent/toolkit/hybrid_browser_toolkit.py b/backend/app/agent/toolkit/hybrid_browser_toolkit.py index dd9b8d716..988893d91 100644 --- a/backend/app/agent/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/agent/toolkit/hybrid_browser_toolkit.py @@ -456,7 +456,7 @@ def __init__( page_stability_timeout: int | None = None, dom_content_loaded_timeout: int | None = None, viewport_limit: bool = False, - connect_over_cdp: bool = True, + connect_over_cdp: bool = True, # Deprecated: auto-set to True when cdp_url is provided, kept for compatibility cdp_url: str | None = "http://localhost:9222", cdp_keep_current_page: bool = False, full_visual_mode: bool = False, @@ -588,7 +588,7 @@ def clone_for_new_session( dom_content_loaded_timeout=self._dom_content_loaded_timeout, viewport_limit=self._viewport_limit, connect_over_cdp=self.config_loader.get_browser_config().connect_over_cdp, - cdp_url=f"http://localhost:{env('browser_port', '9222')}", + cdp_url=self.config_loader.get_browser_config().cdp_url, cdp_keep_current_page=self.config_loader.get_browser_config().cdp_keep_current_page, full_visual_mode=self._full_visual_mode, ) diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index a3702c6c8..8f13a5aa4 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -62,6 +62,7 @@ class Chat(BaseModel): api_url: str | None = None language: str = "en" browser_port: int = 9222 + cdp_browsers: list[dict] = Field(default_factory=list) max_retries: int = 3 allow_local_system: bool = False installed_mcp: McpServers = {"mcpServers": {}} diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index bd411b86b..1c2c9bbc6 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -171,6 +171,24 @@ def _log_deactivate( ) +def _filter_kwargs_for_callable( + func: Callable[..., Any], kwargs: dict +) -> dict: + """Drop unexpected kwargs unless the callable accepts **kwargs.""" + if not kwargs: + return kwargs + try: + sig = signature(func) + except (TypeError, ValueError): + return kwargs + if any( + param.kind == param.VAR_KEYWORD for param in sig.parameters.values() + ): + return kwargs + allowed = set(sig.parameters.keys()) + return {k: v for k, v in kwargs.items() if k in allowed} + + def _safe_put_queue(task_lock, data): """Safely put data to the queue, handling both sync and async contexts""" try: @@ -308,7 +326,8 @@ async def async_wrapper(*args, **kwargs): error = None res = None try: - res = await func(*args, **kwargs) + safe_kwargs = _filter_kwargs_for_callable(func, kwargs) + res = await func(*args, **safe_kwargs) except Exception as e: error = e diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index 7a4ec4818..dd96b832b 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -36,7 +36,7 @@ def __init__( description: str, worker: ListenChatAgent, use_agent_pool: bool = True, - pool_initial_size: int = 1, + pool_initial_size: int = 0, # Changed from 1 to 0 to avoid pre-creating clones that waste CDP resources pool_max_size: int = 10, auto_scale_pool: bool = True, use_structured_output_handler: bool = True, @@ -86,6 +86,16 @@ async def _process_task( TaskState: `TaskState.DONE` if processed successfully, otherwise `TaskState.FAILED`. """ + # Log task details before getting agent (for clone tracking) + task_content_preview = ( + task.content[:100] + "..." + if len(task.content) > 100 + else task.content + ) + logger.debug( + f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'" + ) + # Get agent efficiently (from pool or by cloning) worker_agent = await self._get_worker_agent() worker_agent.process_task_id = task.id # type: ignore rewrite line diff --git a/backend/app/utils/telemetry/__init__.py b/backend/app/utils/telemetry/__init__.py index e69de29bb..fa7455a0c 100644 --- a/backend/app/utils/telemetry/__init__.py +++ b/backend/app/utils/telemetry/__init__.py @@ -0,0 +1,13 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index d9f70f2fe..e8d749b51 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -922,12 +922,120 @@ def stop_gracefully(self) -> None: f"{self._state.name}, _running: {self._running}" ) logger.info("=" * 80) + self._cleanup_all_agents() super().stop_gracefully() logger.info( f"[WF-LIFECYCLE] ✅ super().stop_gracefully() completed, " f"new state: {self._state.name}, _running: {self._running}" ) + def _cleanup_all_agents(self) -> None: + """Release CDP browser resources for all agents.""" + cleanup_count = 0 + children_count = ( + len(self._children) if hasattr(self, "_children") else 0 + ) + logger.info( + f"[WF-CLEANUP] Starting cleanup, " + f"children={children_count}, api_task_id={self.api_task_id}" + ) + + if hasattr(self, "_children") and self._children: + for child in self._children: + # Cleanup base worker agent + if hasattr(child, "worker"): + agent = child.worker + has_cb = hasattr(agent, "_cdp_release_callback") + port = getattr(agent, "_cdp_port", None) + logger.info( + f"[WF-CLEANUP] Child worker: " + f"agent_id={getattr(agent, 'agent_id', '?')}, " + f"has_release_cb={has_cb}, cdp_port={port}" + ) + cb = getattr(agent, "_cdp_release_callback", None) + if callable(cb): + try: + cb(agent) + cleanup_count += 1 + logger.info( + f"[WF-CLEANUP] Released CDP via callback: " + f"port={port}" + ) + except Exception as e: + logger.error( + f"[WF-CLEANUP] Error releasing CDP for " + f"agent: {e}" + ) + + # Cleanup agents in AgentPool + if hasattr(child, "agent_pool") and child.agent_pool: + pool = child.agent_pool + available = list(getattr(pool, "_available_agents", [])) + logger.info( + f"[WF-CLEANUP] AgentPool available_agents={len(available)}" + ) + for agent in available: + cb = getattr(agent, "_cdp_release_callback", None) + if callable(cb): + try: + cb(agent) + cleanup_count += 1 + except Exception as e: + logger.error( + f"[WF-CLEANUP] Error releasing CDP for " + f"pooled agent: {e}" + ) + + # Force-clear all occupied ports as a safety net + try: + from app.agent.factory.browser import _cdp_pool_manager + + task_ids: set[str] = set() + # Use workforce's own api_task_id as the primary source + if self.api_task_id: + task_ids.add(self.api_task_id) + # Also collect from child agents + if hasattr(self, "_children") and self._children: + for child in self._children: + if hasattr(child, "worker"): + tid = getattr(child.worker, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + if hasattr(child, "agent_pool") and child.agent_pool: + for agent in list(child.agent_pool._available_agents): + tid = getattr(agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + if hasattr(self, "coordinator_agent") and self.coordinator_agent: + tid = getattr(self.coordinator_agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + + if not task_ids: + logger.debug( + "[WF-CLEANUP] No task_id found for CDP release; skipping pool cleanup" + ) + else: + logger.info( + f"[WF-CLEANUP] Force releasing CDP resources for task_ids: {sorted(task_ids)}" + ) + released_ports = [] + for task_id in task_ids: + released_ports.extend( + _cdp_pool_manager.release_by_task(task_id) + ) + + logger.info( + f"[WF-CLEANUP] Released {len(released_ports)} CDP browser(s), remaining: {_cdp_pool_manager.get_occupied_ports()}" + ) + except Exception as e: + logger.error(f"[WF-CLEANUP] Error clearing CDP pool: {e}") + + logger.info( + f"[WF-CLEANUP] Cleanup completed, " + f"{cleanup_count} agent(s) released" + ) + def skip_gracefully(self) -> None: logger.info("=" * 80) logger.info( diff --git a/electron/main/index.ts b/electron/main/index.ts index 271a17450..8277c2551 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -29,7 +29,9 @@ import FormData from 'form-data'; import fsp from 'fs/promises'; import mime from 'mime'; import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; +import crypto from 'node:crypto'; import fs, { existsSync } from 'node:fs'; +import http from 'node:http'; import os, { homedir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -84,8 +86,220 @@ let fileReader: FileReader | null = null; let python_process: ChildProcessWithoutNullStreams | null = null; let backendPort: number = 5001; let browser_port = 9222; +let use_external_cdp = false; let proxyUrl: string | null = null; +// CDP Browser Pool +interface CdpBrowser { + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; +} +let cdp_browser_pool: CdpBrowser[] = []; +let cdpHealthCheckTimer: ReturnType | null = null; + +const CDP_POOL_FILE = path.join(os.homedir(), '.eigent', 'cdp-browsers.json'); + +/** Persist pool to disk. */ +function saveCdpPool(): void { + try { + fs.writeFileSync(CDP_POOL_FILE, JSON.stringify(cdp_browser_pool, null, 2)); + } catch (e) { + log.error(`[CDP POOL] Failed to save pool: ${e}`); + } +} + +/** Load pool from disk. Mark all as external (process handles are lost after restart). */ +function loadCdpPool(): void { + try { + if (fs.existsSync(CDP_POOL_FILE)) { + const data = JSON.parse(fs.readFileSync(CDP_POOL_FILE, 'utf-8')); + cdp_browser_pool = (data as CdpBrowser[]).map((b) => ({ + ...b, + isExternal: true, + })); + log.info( + `[CDP POOL] Loaded ${cdp_browser_pool.length} browser(s) from disk` + ); + } + } catch (e) { + log.error(`[CDP POOL] Failed to load pool: ${e}`); + cdp_browser_pool = []; + } +} + +/** Push current pool to frontend. */ +function notifyCdpPoolChanged(): void { + if (win && !win.isDestroyed()) { + log.info( + `[CDP POOL] Pushing pool update to frontend (size=${cdp_browser_pool.length})` + ); + win.webContents.send('cdp-pool-changed', cdp_browser_pool); + } else { + log.warn('[CDP POOL] Cannot notify: win is null or destroyed'); + } +} + +/** Probe a CDP port. Returns true if alive. */ +async function isCdpPortAlive(port: number): Promise { + try { + const resp = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 1500, + }); + return resp.status === 200; + } catch { + return false; + } +} + +/** Run one health-check cycle: remove dead browsers, persist & notify if changed. */ +async function runPoolHealthCheck(): Promise { + if (cdp_browser_pool.length === 0) return; + // Probe a snapshot so add/remove IPC handlers can run safely in parallel. + const snapshot = [...cdp_browser_pool]; + const results = await Promise.all( + snapshot.map((b) => isCdpPortAlive(b.port)) + ); + const deadIds = snapshot + .filter((_, idx) => !results[idx]) + .map((browser) => browser.id); + if (deadIds.length === 0) return; + + const deadIdSet = new Set(deadIds); + const removedBrowsers = cdp_browser_pool.filter((b) => deadIdSet.has(b.id)); + if (removedBrowsers.length === 0) return; + + cdp_browser_pool = cdp_browser_pool.filter((b) => !deadIdSet.has(b.id)); + const deadPorts = removedBrowsers.map((b) => b.port); + if (deadPorts.length > 0) { + log.info( + `[CDP POOL] Health-check removed dead ports: ${deadPorts.join(', ')}. pool_size=${cdp_browser_pool.length}` + ); + saveCdpPool(); + notifyCdpPoolChanged(); + } +} + +/** Start periodic health check (call after window is created). */ +function startCdpHealthCheck(): void { + if (cdpHealthCheckTimer) { + clearInterval(cdpHealthCheckTimer); + cdpHealthCheckTimer = null; + } + log.info('[CDP POOL] Starting health check (interval=3s)'); + // Run once immediately + runPoolHealthCheck(); + cdpHealthCheckTimer = setInterval(runPoolHealthCheck, 3000); +} + +function stopCdpHealthCheck(): void { + if (cdpHealthCheckTimer) { + clearInterval(cdpHealthCheckTimer); + cdpHealthCheckTimer = null; + } +} + +/** Close a browser via CDP Browser.close() WebSocket command. Best-effort. + * Uses raw Node.js http upgrade (no external ws dependency needed). + * IMPORTANT: Never close the Electron app's own CDP port. */ +async function closeBrowserViaCdp(port: number): Promise { + // Guard: refuse to close the Electron app's own CDP port + if (port === browser_port) { + log.warn( + `[CDP CLOSE] Refusing to close port ${port} (Electron app's own CDP port)` + ); + return; + } + + try { + const resp = await axios.get(`http://localhost:${port}/json/version`, { + timeout: 2000, + }); + const wsUrl: string | undefined = resp.data?.webSocketDebuggerUrl; + if (!wsUrl) { + log.warn(`[CDP CLOSE] No webSocketDebuggerUrl for port ${port}`); + return; + } + + const url = new URL(wsUrl); + const key = crypto.randomBytes(16).toString('base64'); + + await new Promise((resolve) => { + let resolved = false; + const done = () => { + if (!resolved) { + resolved = true; + resolve(); + } + }; + + const req = http.request( + { + hostname: url.hostname, + port: url.port, + path: url.pathname, + method: 'GET', + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': key, + }, + }, + () => done() + ); + + const timer = setTimeout(() => { + req.destroy(); + done(); + }, 3000); + + req.on('upgrade', (_res, socket) => { + // Handle socket errors to prevent uncaught exceptions + socket.on('error', () => {}); + + // Build a masked WebSocket text frame with Browser.close + const payload = Buffer.from( + JSON.stringify({ id: 1, method: 'Browser.close' }) + ); + const mask = crypto.randomBytes(4); + const header = Buffer.alloc(6); + header[0] = 0x81; // FIN + text opcode + header[1] = 0x80 | payload.length; // MASK bit + length (<126) + mask.copy(header, 2); + + const masked = Buffer.alloc(payload.length); + for (let i = 0; i < payload.length; i++) { + masked[i] = payload[i] ^ mask[i & 3]; + } + + socket.write(Buffer.concat([header, masked])); + log.info(`[CDP CLOSE] Sent Browser.close to port ${port}`); + + // Give Chrome a moment to process, then clean up + setTimeout(() => { + clearTimeout(timer); + socket.destroy(); + done(); + }, 500); + }); + + req.on('error', (err) => { + log.warn(`[CDP CLOSE] Request error for port ${port}: ${err.message}`); + clearTimeout(timer); + done(); + }); + + req.end(); + }); + log.info(`[CDP CLOSE] Successfully closed browser on port ${port}`); + } catch (err) { + log.warn(`[CDP CLOSE] Best-effort close failed for port ${port}: ${err}`); + } +} + // Protocol URL queue for handling URLs before window is ready let protocolUrlQueue: string[] = []; let isWindowReady = false; @@ -386,6 +600,253 @@ function registerIpcHandlers() { log.info('Getting browser port'); return browser_port; }); + + // Set browser port + ipcMain.handle( + 'set-browser-port', + (event, port: number, isExternal: boolean = false) => { + log.info(`Setting browser port to ${port}, external: ${isExternal}`); + browser_port = port; + use_external_cdp = isExternal; + return { success: true, port: browser_port, use_external_cdp }; + } + ); + + // Get external CDP flag + ipcMain.handle('get-use-external-cdp', () => { + log.info(`Getting use_external_cdp: ${use_external_cdp}`); + return use_external_cdp; + }); + + // ==================== CDP Browser Pool Management ==================== + + // Get all browsers in the pool + ipcMain.handle('get-cdp-browsers', () => { + log.debug(`[CDP POOL] GET pool (size=${cdp_browser_pool.length})`); + return cdp_browser_pool; + }); + + // Add browser to pool + ipcMain.handle( + 'add-cdp-browser', + (event, port: number, isExternal: boolean, name?: string) => { + const existing = cdp_browser_pool.find((b) => b.port === port); + if (existing) { + log.warn( + `[CDP POOL] ADD rejected: port ${port} already exists (id=${existing.id})` + ); + return { + success: false, + error: 'Browser with this port already exists', + }; + } + + const newBrowser: CdpBrowser = { + id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + port, + isExternal, + name, + addedAt: Date.now(), + }; + + cdp_browser_pool.push(newBrowser); + saveCdpPool(); + notifyCdpPoolChanged(); + log.info( + `[CDP POOL] ADD: port=${port}, isExternal=${isExternal}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` + ); + + return { success: true, browser: newBrowser }; + } + ); + + // Remove browser from pool (also closes the browser via CDP) + ipcMain.handle( + 'remove-cdp-browser', + async (event, browserId: string, closeBrowser: boolean = true) => { + const index = cdp_browser_pool.findIndex((b) => b.id === browserId); + if (index === -1) { + log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`); + return { success: false, error: 'Browser not found' }; + } + + const removed = cdp_browser_pool.splice(index, 1)[0]; + + // Close the browser via CDP (best-effort) + if (closeBrowser) { + await closeBrowserViaCdp(removed.port); + } + + saveCdpPool(); + notifyCdpPoolChanged(); + log.info( + `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, closed=${closeBrowser}, pool_size=${cdp_browser_pool.length}` + ); + return { success: true, browser: removed }; + } + ); + + // Launch CDP browser with automatic port assignment + ipcMain.handle('launch-cdp-browser', async () => { + try { + // 1. Find available port (9223–9300) by checking no CDP browser is listening + let port: number | null = null; + for (let p = 9223; p < 9300; p++) { + if ( + !cdp_browser_pool.some((b) => b.port === p) && + !(await isCdpPortAlive(p)) + ) { + port = p; + break; + } + } + if (port === null) { + return { success: false, error: 'No available port in 9223-9299' }; + } + + // 2. Find Playwright Chromium executable + const platform = process.platform; + let cacheDir: string; + if (platform === 'darwin') + cacheDir = path.join(homedir(), 'Library/Caches/ms-playwright'); + else if (platform === 'linux') + cacheDir = path.join(homedir(), '.cache/ms-playwright'); + else if (platform === 'win32') + cacheDir = path.join(homedir(), 'AppData/Local/ms-playwright'); + else + return { success: false, error: `Unsupported platform: ${platform}` }; + + if (!existsSync(cacheDir)) { + return { + success: false, + error: + 'Playwright Chromium not found. Please run: npx playwright install chromium', + }; + } + + const chromiumDirs = fs + .readdirSync(cacheDir) + .filter((d) => d.startsWith('chromium-')) + .sort() + .reverse(); + if (chromiumDirs.length === 0) { + return { + success: false, + error: + 'No Playwright Chromium found. Run: npx playwright install chromium', + }; + } + + const platformPaths: Record string[]> = { + darwin: (base) => [ + path.join( + base, + 'chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium' + ), + path.join( + base, + 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' + ), + path.join(base, 'chrome-mac/Chromium.app/Contents/MacOS/Chromium'), + path.join( + base, + 'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' + ), + ], + linux: (base) => [path.join(base, 'chrome-linux/chrome')], + win32: (base) => [ + path.join(base, 'chrome-win64/chrome.exe'), + path.join(base, 'chrome-win/chrome.exe'), + ], + }; + + let chromeExe: string | null = null; + for (const dir of chromiumDirs) { + const base = path.join(cacheDir, dir); + const candidates = platformPaths[platform](base); + const found = candidates.find((p) => existsSync(p)); + if (found) { + chromeExe = found; + break; + } + } + if (!chromeExe) { + return { success: false, error: 'Chromium executable not found' }; + } + + // 3. Launch browser + const userDataDir = path.join( + app.getPath('userData'), + `cdp_browser_profile_${port}` + ); + if (!existsSync(userDataDir)) { + await fsp.mkdir(userDataDir, { recursive: true }); + } + + const proc = spawn( + chromeExe, + [ + `--remote-debugging-port=${port}`, + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--disable-blink-features=AutomationControlled', + 'about:blank', + ], + { detached: false, stdio: 'ignore' } + ); + + proc.on('error', (err) => + log.error(`[CDP LAUNCH] Process error port=${port}: ${err}`) + ); + + // 4. Poll for readiness (max 5s) + let data: any = null; + const start = Date.now(); + while (Date.now() - start < 5000) { + try { + const resp = await axios.get( + `http://localhost:${port}/json/version`, + { timeout: 1000 } + ); + if (resp.status === 200) { + data = resp.data; + break; + } + } catch {} + await new Promise((r) => setTimeout(r, 300)); + } + + if (!data) { + proc.kill(); + return { + success: false, + error: `Browser not responding on port ${port} after 5s`, + }; + } + + // 5. Add to pool automatically + const newBrowser: CdpBrowser = { + id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + port, + isExternal: false, + name: `Launched Browser (${port})`, + addedAt: Date.now(), + }; + cdp_browser_pool.push(newBrowser); + saveCdpPool(); + notifyCdpPoolChanged(); + + log.info( + `[CDP LAUNCH] Success: port=${port}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` + ); + return { success: true, port, data }; + } catch (err: any) { + log.error(`[CDP LAUNCH] Failed: ${err}`); + return { success: false, error: err.message }; + } + }); + ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-backend-port', () => backendPort); @@ -2172,6 +2633,9 @@ async function createWindow() { ensureEigentDirectories(); await seedDefaultSkillsIfEmpty(); + // Load persisted CDP browser pool from disk + loadCdpPool(); + log.info( `[PROJECT BROWSER WINDOW] Creating BrowserWindow which will start Chrome with CDP on port ${browser_port}` ); @@ -2353,6 +2817,9 @@ async function createWindow() { setupExternalLinkHandling(); handleBeforeClose(); + // Start CDP health-check polling (probes every 3s, removes dead browsers) + startCdpHealthCheck(); + // ==================== auto update ==================== update(win); @@ -2973,6 +3440,9 @@ app.whenReady().then(async () => { app.on('window-all-closed', () => { log.info('window-all-closed'); + // Stop polling when no window is open (important on macOS reopen flow). + stopCdpHealthCheck(); + // Clean up WebView manager if (webViewManager) { webViewManager.destroy(); @@ -3007,6 +3477,9 @@ app.on('before-quit', async (event) => { log.info('before-quit'); log.info('quit python_process.pid: ' + python_process?.pid); + // Stop CDP health-check polling + stopCdpHealthCheck(); + // Prevent default quit to ensure cleanup completes event.preventDefault(); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 12a2a48c7..910a670d7 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -158,6 +158,23 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('get-project-folder-path', email, projectId), openInIDE: (folderPath: string, ide: string) => ipcRenderer.invoke('open-in-ide', folderPath, ide), + setBrowserPort: (port: number, isExternal?: boolean) => + ipcRenderer.invoke('set-browser-port', port, isExternal), + getBrowserPort: () => ipcRenderer.invoke('get-browser-port'), + getCdpBrowsers: () => ipcRenderer.invoke('get-cdp-browsers'), + addCdpBrowser: (port: number, isExternal: boolean, name?: string) => + ipcRenderer.invoke('add-cdp-browser', port, isExternal, name), + removeCdpBrowser: (browserId: string, closeBrowser?: boolean) => + ipcRenderer.invoke('remove-cdp-browser', browserId, closeBrowser ?? true), + launchCdpBrowser: () => ipcRenderer.invoke('launch-cdp-browser'), + onCdpPoolChanged: (callback: (browsers: any[]) => void) => { + const channel = 'cdp-pool-changed'; + const listener = (_event: any, browsers: any[]) => callback(browsers); + ipcRenderer.on(channel, listener); + return () => { + ipcRenderer.off(channel, listener); + }; + }, // Skills getSkillsDir: () => ipcRenderer.invoke('get-skills-dir'), skillsScan: () => ipcRenderer.invoke('skills-scan'), diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index 56ab973ce..976af1ca8 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -169,5 +169,73 @@ "delete-project": "حذف المشروع", "delete-project-confirmation": "هل أنت متأكد من أنك تريد حذف هذا المشروع وجميع مهامه؟ لا يمكن التراجع عن هذا الإجراء.", "please-select-model": "يرجى اختيار نموذج في الإعدادات > النماذج للمتابعة.", - "capabilities": "القدرات" + "cdp-browser-connection": "اتصال متصفح CDP", + "cdp-browser-connection-description": "الاتصال بمتصفح Chrome مع تمكين التصحيح عن بُعد", + "current-port": "المنفذ الحالي:", + "cdp-port-check-description": "التحقق مما إذا كان المتصفح متاحاً على منفذ محدد", + "port-placeholder": "رقم المنفذ (مثال: 9223)", + "checking": "جارٍ التحقق", + "check-port": "تحقق من المنفذ", + "browser-available": "المتصفح متاح", + "browser-not-available": "المتصفح غير متاح", + "cdp-browser-pool": "مجموعة متصفحات CDP", + "cdp-browser-pool-description": "إدارة عدة متصفحات CDP لتنفيذ المهام", + "external": "خارجي", + "launched": "تم التشغيل", + "stopped": "متوقف", + "port": "المنفذ:", + "no-browsers-in-pool": "لا توجد متصفحات في المجموعة", + "add-browsers-hint": "أضف متصفحات باستخدام أداة فحص المنفذ أعلاه", + "invalid-port": "الرجاء إدخال رقم منفذ صالح (1-65535)", + "cdp-port-check-not-available": "فحص منفذ CDP غير متاح", + "failed-to-check-port": "فشل في فحص المنفذ", + "added-browser-to-pool": "تمت إضافة متصفح خارجي على المنفذ {{port}} إلى المجموعة", + "failed-to-add-browser": "فشل في إضافة المتصفح إلى المجموعة", + "launch-not-available": "تشغيل متصفح CDP غير متاح", + "launching-browser": "جارٍ تشغيل المتصفح على المنفذ {{port}}...", + "browser-launched": "تم تشغيل المتصفح بنجاح على المنفذ {{port}}", + "failed-to-launch-browser": "فشل في تشغيل المتصفح", + "browser-removed": "تمت إزالة المتصفح من المجموعة", + "failed-to-remove-browser": "فشل في إزالة المتصفح", + "remove-browser": "إزالة المتصفح", + "remove-browser-confirm": "سيتم فصل وإغلاق المتصفح \"{{name}}\" على المنفذ {{port}}. هل أنت متأكد؟", + "remove": "إزالة", + "browser-opened": "تم فتح المتصفح بنجاح لتسجيل الدخول", + "restart-not-available": "وظيفة إعادة التشغيل غير متاحة", + "browser-found": "تم العثور على متصفح", + "browser-found-description": "يعمل متصفح على المنفذ {{port}}. هل تريد استخدامه لعمليات المتصفح؟", + "yes-use-browser": "نعم، استخدم هذا المتصفح", + "no-browser-found": "لم يتم العثور على متصفح", + "no-browser-found-description": "لا يوجد متصفح يعمل على المنفذ {{port}}. هل تريد تشغيل متصفح Chrome جديد مع تمكين CDP على هذا المنفذ؟", + "yes-launch-browser": "نعم، تشغيل المتصفح", + "for-more-info": "لمزيد من المعلومات، تحقق من", + "capabilities": "القدرات", + "browser-connection": "الاتصال", + "cookies-management": "ملفات تعريف الارتباط", + "restart-to-enable": "إعادة التشغيل للتفعيل", + "restart-to-enable-cookies-tooltip": "أعد تشغيل العميل لتفعيل إدارة ملفات تعريف الارتباط الجديدة", + "open-new-browser": "فتح متصفح فارغ", + "browser-cookies-management": "إدارة ملفات تعريف الارتباط للمتصفح", + "connect-existing-browser": "توصيل متصفح موجود", + "connect-existing-browser-description": "الاتصال بمتصفح يعمل بالفعل مع تمكين CDP على منفذ محدد.", + "enter-port-number": "أدخل رقم المنفذ", + "check-and-connect": "تحقق واتصل", + "port-already-in-use": "هذا المنفذ موجود بالفعل في مجموعة المتصفحات. الرجاء استخدام منفذ مختلف.", + "no-browser-on-port": "لم يتم العثور على متصفح على المنفذ {{port}}. تأكد من أن المتصفح يعمل مع --remote-debugging-port={{port}}.", + "connected-browser": "متصل بالمتصفح على المنفذ {{port}}", + "cookies-added": "تمت إضافة {{count}} من ملفات تعريف الارتباط", + "failed-to-open-browser": "فشل في فتح المتصفح", + "failed-to-load-cookies": "فشل في تحميل ملفات تعريف الارتباط", + "deleted-cookies-for-domain": "تم حذف ملفات تعريف الارتباط لـ {{domain}} وجميع النطاقات الفرعية", + "failed-to-delete-cookies-for-domain": "فشل في حذف ملفات تعريف الارتباط لـ {{domain}}", + "deleted-all-cookies": "تم حذف جميع ملفات تعريف الارتباط", + "failed-to-delete-all-cookies": "فشل في حذف جميع ملفات تعريف الارتباط", + "cookies-updated": "تم تحديث ملفات تعريف الارتباط", + "cookies-updated-message": "تم تحديث ملفات تعريف الارتباط. هل تريد إعادة تشغيل التطبيق لاستخدام ملفات تعريف الارتباط الجديدة؟", + "yes-restart": "نعم، إعادة التشغيل", + "no-add-more": "لا، إضافة المزيد", + "restart-required": "إعادة التشغيل مطلوبة", + "restart-required-message": "أعد تشغيل التطبيق لتفعيل تغييرات نطاق ملفات تعريف الارتباط.", + "restart": "إعادة التشغيل", + "cookie-count": "{{count}} من ملفات تعريف الارتباط" } diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index 4e1b9f8b3..12d77361a 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -169,5 +169,73 @@ "delete-project": "Projekt löschen", "delete-project-confirmation": "Sind Sie sicher, dass Sie dieses Projekt und alle seine Aufgaben löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", "please-select-model": "Bitte wählen Sie ein Modell unter Einstellungen > Modelle aus, um fortzufahren.", - "capabilities": "Fähigkeiten" + "cdp-browser-connection": "CDP-Browser-Verbindung", + "cdp-browser-connection-description": "Verbindung zu einem Chrome-Browser mit aktiviertem Remote-Debugging herstellen", + "current-port": "Aktueller Port:", + "cdp-port-check-description": "Prüfen, ob ein Browser auf einem bestimmten Port verfügbar ist", + "port-placeholder": "Portnummer (z.B. 9223)", + "checking": "Prüfe", + "check-port": "Port prüfen", + "browser-available": "Browser verfügbar", + "browser-not-available": "Browser nicht verfügbar", + "cdp-browser-pool": "CDP-Browser-Pool", + "cdp-browser-pool-description": "Mehrere CDP-Browser für die Aufgabenausführung verwalten", + "external": "Extern", + "launched": "Gestartet", + "stopped": "Gestoppt", + "port": "Port:", + "no-browsers-in-pool": "Keine Browser im Pool", + "add-browsers-hint": "Browser über das Port-Prüftool oben hinzufügen", + "invalid-port": "Bitte geben Sie eine gültige Portnummer ein (1-65535)", + "cdp-port-check-not-available": "CDP-Portprüfung nicht verfügbar", + "failed-to-check-port": "Portprüfung fehlgeschlagen", + "added-browser-to-pool": "Externer Browser auf Port {{port}} zum Pool hinzugefügt", + "failed-to-add-browser": "Browser konnte nicht zum Pool hinzugefügt werden", + "launch-not-available": "CDP-Browser starten nicht verfügbar", + "launching-browser": "Browser wird auf Port {{port}} gestartet...", + "browser-launched": "Browser erfolgreich auf Port {{port}} gestartet", + "failed-to-launch-browser": "Browser konnte nicht gestartet werden", + "browser-removed": "Browser aus dem Pool entfernt", + "failed-to-remove-browser": "Browser konnte nicht entfernt werden", + "remove-browser": "Browser entfernen", + "remove-browser-confirm": "Dies wird den Browser \"{{name}}\" auf Port {{port}} trennen und schließen. Sind Sie sicher?", + "remove": "Entfernen", + "browser-opened": "Browser erfolgreich zum Anmelden geöffnet", + "restart-not-available": "Neustart-Funktion nicht verfügbar", + "browser-found": "Browser gefunden", + "browser-found-description": "Ein Browser läuft auf Port {{port}}. Möchten Sie ihn für Browser-Operationen verwenden?", + "yes-use-browser": "Ja, diesen Browser verwenden", + "no-browser-found": "Kein Browser gefunden", + "no-browser-found-description": "Kein Browser läuft auf Port {{port}}. Möchten Sie einen neuen Chrome-Browser mit CDP auf diesem Port starten?", + "yes-launch-browser": "Ja, Browser starten", + "for-more-info": "Weitere Informationen finden Sie in unserer", + "capabilities": "Fähigkeiten", + "browser-connection": "Verbindung", + "cookies-management": "Cookies", + "restart-to-enable": "Neustart zum Aktivieren", + "restart-to-enable-cookies-tooltip": "Client neu starten, um die neue Cookie-Verwaltung zu aktivieren", + "open-new-browser": "Leeren Browser öffnen", + "browser-cookies-management": "Browser-Cookie-Verwaltung", + "connect-existing-browser": "Vorhandenen Browser verbinden", + "connect-existing-browser-description": "Verbindung zu einem bereits laufenden Browser mit aktiviertem CDP auf einem bestimmten Port.", + "enter-port-number": "Portnummer eingeben", + "check-and-connect": "Prüfen & Verbinden", + "port-already-in-use": "Dieser Port ist bereits im Browser-Pool. Bitte verwenden Sie einen anderen Port.", + "no-browser-on-port": "Kein Browser auf Port {{port}} gefunden. Stellen Sie sicher, dass ein Browser mit --remote-debugging-port={{port}} läuft.", + "connected-browser": "Verbunden mit Browser auf Port {{port}}", + "cookies-added": "{{count}} Cookie(s) hinzugefügt", + "failed-to-open-browser": "Browser konnte nicht geöffnet werden", + "failed-to-load-cookies": "Cookies konnten nicht geladen werden", + "deleted-cookies-for-domain": "Cookies für {{domain}} und alle Subdomains gelöscht", + "failed-to-delete-cookies-for-domain": "Cookies für {{domain}} konnten nicht gelöscht werden", + "deleted-all-cookies": "Alle Cookies gelöscht", + "failed-to-delete-all-cookies": "Alle Cookies konnten nicht gelöscht werden", + "cookies-updated": "Cookies aktualisiert", + "cookies-updated-message": "Cookies wurden aktualisiert. Möchten Sie die Anwendung neu starten, um die neuen Cookies zu verwenden?", + "yes-restart": "Ja, neu starten", + "no-add-more": "Nein, weitere hinzufügen", + "restart-required": "Neustart erforderlich", + "restart-required-message": "Starten Sie die Anwendung neu, um Ihre Cookie-Domain-Änderungen zu aktivieren.", + "restart": "Neustart", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index 3f6c609da..099f843fd 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -171,5 +171,73 @@ "delete-project": "Delete Project", "delete-project-confirmation": "Are you sure you want to delete this project and all its tasks? This action cannot be undone.", "please-select-model": "Please select a model in Settings > Models to continue.", - "capabilities": "Capabilities" + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Electron Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9223)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "remove-browser": "Remove Browser", + "remove-browser-confirm": "This will disconnect and close the browser \"{{name}}\" on port {{port}}. Are you sure?", + "remove": "Remove", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our", + "capabilities": "Capabilities", + "browser-connection": "Connection", + "cookies-management": "Cookies", + "restart-to-enable": "Restart to enable", + "restart-to-enable-cookies-tooltip": "Restart the client to enable new cookies management", + "open-new-browser": "Open Blank Browser", + "browser-cookies-management": "Browser Cookies Management", + "connect-existing-browser": "Connect Existing Browser", + "connect-existing-browser-description": "Connect to a browser already running with CDP enabled on a specific port.", + "enter-port-number": "Enter Port Number", + "check-and-connect": "Check & Connect", + "port-already-in-use": "This port is already in the browser pool. Please use a different port.", + "no-browser-on-port": "No browser found on port {{port}}. Make sure a browser is running with --remote-debugging-port={{port}}.", + "connected-browser": "Connected to browser on port {{port}}", + "cookies-added": "Added {{count}} cookie(s)", + "failed-to-open-browser": "Failed to open browser", + "failed-to-load-cookies": "Failed to load cookies", + "deleted-cookies-for-domain": "Deleted cookies for {{domain}} and all subdomains", + "failed-to-delete-cookies-for-domain": "Failed to delete cookies for {{domain}}", + "deleted-all-cookies": "Deleted all cookies", + "failed-to-delete-all-cookies": "Failed to delete all cookies", + "cookies-updated": "Cookies Updated", + "cookies-updated-message": "Cookies have been updated. Would you like to restart the application to use the new cookies?", + "yes-restart": "Yes, Restart", + "no-add-more": "No, Add More", + "restart-required": "Restart Required", + "restart-required-message": "Restart the application to enable your cookie domain changes.", + "restart": "Restart", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index db8414254..b5ce47b4e 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -169,5 +169,73 @@ "delete-project": "Eliminar Proyecto", "delete-project-confirmation": "¿Estás seguro de que quieres eliminar este proyecto y todas sus tareas? Esta acción no se puede deshacer.", "please-select-model": "Por favor, selecciona un modelo en Configuración > Modelos para continuar.", - "capabilities": "Capacidades" + "cdp-browser-connection": "Conexión de navegador CDP", + "cdp-browser-connection-description": "Conectar a un navegador Chrome con depuración remota habilitada", + "current-port": "Puerto actual:", + "cdp-port-check-description": "Verificar si hay un navegador disponible en un puerto específico", + "port-placeholder": "Número de puerto (ej. 9223)", + "checking": "Verificando", + "check-port": "Verificar puerto", + "browser-available": "Navegador disponible", + "browser-not-available": "Navegador no disponible", + "cdp-browser-pool": "Pool de navegadores CDP", + "cdp-browser-pool-description": "Gestionar múltiples navegadores CDP para la ejecución de tareas", + "external": "Externo", + "launched": "Iniciado", + "stopped": "Detenido", + "port": "Puerto:", + "no-browsers-in-pool": "No hay navegadores en el pool", + "add-browsers-hint": "Agregue navegadores usando la herramienta de verificación de puerto arriba", + "invalid-port": "Por favor ingrese un número de puerto válido (1-65535)", + "cdp-port-check-not-available": "Verificación de puerto CDP no disponible", + "failed-to-check-port": "Error al verificar el puerto", + "added-browser-to-pool": "Navegador externo en el puerto {{port}} agregado al pool", + "failed-to-add-browser": "Error al agregar navegador al pool", + "launch-not-available": "Inicio de navegador CDP no disponible", + "launching-browser": "Iniciando navegador en el puerto {{port}}...", + "browser-launched": "Navegador iniciado exitosamente en el puerto {{port}}", + "failed-to-launch-browser": "Error al iniciar el navegador", + "browser-removed": "Navegador eliminado del pool", + "failed-to-remove-browser": "Error al eliminar el navegador", + "remove-browser": "Eliminar navegador", + "remove-browser-confirm": "Esto desconectará y cerrará el navegador \"{{name}}\" en el puerto {{port}}. ¿Está seguro?", + "remove": "Eliminar", + "browser-opened": "Navegador abierto exitosamente para iniciar sesión", + "restart-not-available": "Función de reinicio no disponible", + "browser-found": "Navegador encontrado", + "browser-found-description": "Un navegador está ejecutándose en el puerto {{port}}. ¿Desea usarlo para operaciones del navegador?", + "yes-use-browser": "Sí, usar este navegador", + "no-browser-found": "Navegador no encontrado", + "no-browser-found-description": "No hay navegador ejecutándose en el puerto {{port}}. ¿Desea iniciar un nuevo navegador Chrome con CDP habilitado en este puerto?", + "yes-launch-browser": "Sí, iniciar navegador", + "for-more-info": "Para más información, consulte nuestra", + "capabilities": "Capacidades", + "browser-connection": "Conexión", + "cookies-management": "Cookies", + "restart-to-enable": "Reiniciar para habilitar", + "restart-to-enable-cookies-tooltip": "Reiniciar el cliente para habilitar la nueva gestión de cookies", + "open-new-browser": "Abrir navegador vacío", + "browser-cookies-management": "Gestión de cookies del navegador", + "connect-existing-browser": "Conectar navegador existente", + "connect-existing-browser-description": "Conectar a un navegador que ya está ejecutándose con CDP habilitado en un puerto específico.", + "enter-port-number": "Ingrese el número de puerto", + "check-and-connect": "Verificar y conectar", + "port-already-in-use": "Este puerto ya está en el pool de navegadores. Por favor use un puerto diferente.", + "no-browser-on-port": "No se encontró navegador en el puerto {{port}}. Asegúrese de que un navegador esté ejecutándose con --remote-debugging-port={{port}}.", + "connected-browser": "Conectado al navegador en el puerto {{port}}", + "cookies-added": "{{count}} cookie(s) agregadas", + "failed-to-open-browser": "Error al abrir el navegador", + "failed-to-load-cookies": "Error al cargar cookies", + "deleted-cookies-for-domain": "Cookies eliminadas para {{domain}} y todos los subdominios", + "failed-to-delete-cookies-for-domain": "Error al eliminar cookies para {{domain}}", + "deleted-all-cookies": "Todas las cookies eliminadas", + "failed-to-delete-all-cookies": "Error al eliminar todas las cookies", + "cookies-updated": "Cookies actualizadas", + "cookies-updated-message": "Las cookies han sido actualizadas. ¿Desea reiniciar la aplicación para usar las nuevas cookies?", + "yes-restart": "Sí, reiniciar", + "no-add-more": "No, agregar más", + "restart-required": "Reinicio requerido", + "restart-required-message": "Reinicie la aplicación para habilitar los cambios de dominio de cookies.", + "restart": "Reiniciar", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index 4ad84d348..5ffb3c5d8 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -169,5 +169,73 @@ "delete-project": "Supprimer le Projet", "delete-project-confirmation": "Êtes-vous sûr de vouloir supprimer ce projet et toutes ses tâches ? Cette action ne peut pas être annulée.", "please-select-model": "Veuillez sélectionner un modèle dans Paramètres > Modèles pour continuer.", - "capabilities": "Capacités" + "cdp-browser-connection": "Connexion navigateur CDP", + "cdp-browser-connection-description": "Se connecter à un navigateur Chrome avec le débogage à distance activé", + "current-port": "Port actuel :", + "cdp-port-check-description": "Vérifier si un navigateur est disponible sur un port spécifique", + "port-placeholder": "Numéro de port (ex. 9223)", + "checking": "Vérification", + "check-port": "Vérifier le port", + "browser-available": "Navigateur disponible", + "browser-not-available": "Navigateur non disponible", + "cdp-browser-pool": "Pool de navigateurs CDP", + "cdp-browser-pool-description": "Gérer plusieurs navigateurs CDP pour l'exécution des tâches", + "external": "Externe", + "launched": "Lancé", + "stopped": "Arrêté", + "port": "Port :", + "no-browsers-in-pool": "Aucun navigateur dans le pool", + "add-browsers-hint": "Ajoutez des navigateurs en utilisant l'outil de vérification de port ci-dessus", + "invalid-port": "Veuillez entrer un numéro de port valide (1-65535)", + "cdp-port-check-not-available": "Vérification du port CDP non disponible", + "failed-to-check-port": "Échec de la vérification du port", + "added-browser-to-pool": "Navigateur externe sur le port {{port}} ajouté au pool", + "failed-to-add-browser": "Échec de l'ajout du navigateur au pool", + "launch-not-available": "Lancement du navigateur CDP non disponible", + "launching-browser": "Lancement du navigateur sur le port {{port}}...", + "browser-launched": "Navigateur lancé avec succès sur le port {{port}}", + "failed-to-launch-browser": "Échec du lancement du navigateur", + "browser-removed": "Navigateur supprimé du pool", + "failed-to-remove-browser": "Échec de la suppression du navigateur", + "remove-browser": "Supprimer le navigateur", + "remove-browser-confirm": "Cela déconnectera et fermera le navigateur \"{{name}}\" sur le port {{port}}. Êtes-vous sûr ?", + "remove": "Supprimer", + "browser-opened": "Navigateur ouvert avec succès pour la connexion", + "restart-not-available": "Fonction de redémarrage non disponible", + "browser-found": "Navigateur trouvé", + "browser-found-description": "Un navigateur est en cours d'exécution sur le port {{port}}. Souhaitez-vous l'utiliser pour les opérations du navigateur ?", + "yes-use-browser": "Oui, utiliser ce navigateur", + "no-browser-found": "Aucun navigateur trouvé", + "no-browser-found-description": "Aucun navigateur n'est en cours d'exécution sur le port {{port}}. Souhaitez-vous lancer un nouveau navigateur Chrome avec CDP activé sur ce port ?", + "yes-launch-browser": "Oui, lancer le navigateur", + "for-more-info": "Pour plus d'informations, consultez notre", + "capabilities": "Capacités", + "browser-connection": "Connexion", + "cookies-management": "Cookies", + "restart-to-enable": "Redémarrer pour activer", + "restart-to-enable-cookies-tooltip": "Redémarrer le client pour activer la nouvelle gestion des cookies", + "open-new-browser": "Ouvrir un navigateur vide", + "browser-cookies-management": "Gestion des cookies du navigateur", + "connect-existing-browser": "Connecter un navigateur existant", + "connect-existing-browser-description": "Se connecter à un navigateur déjà en cours d'exécution avec CDP activé sur un port spécifique.", + "enter-port-number": "Entrez le numéro de port", + "check-and-connect": "Vérifier et connecter", + "port-already-in-use": "Ce port est déjà dans le pool de navigateurs. Veuillez utiliser un port différent.", + "no-browser-on-port": "Aucun navigateur trouvé sur le port {{port}}. Assurez-vous qu'un navigateur est en cours d'exécution avec --remote-debugging-port={{port}}.", + "connected-browser": "Connecté au navigateur sur le port {{port}}", + "cookies-added": "{{count}} cookie(s) ajouté(s)", + "failed-to-open-browser": "Échec de l'ouverture du navigateur", + "failed-to-load-cookies": "Échec du chargement des cookies", + "deleted-cookies-for-domain": "Cookies supprimés pour {{domain}} et tous les sous-domaines", + "failed-to-delete-cookies-for-domain": "Échec de la suppression des cookies pour {{domain}}", + "deleted-all-cookies": "Tous les cookies supprimés", + "failed-to-delete-all-cookies": "Échec de la suppression de tous les cookies", + "cookies-updated": "Cookies mis à jour", + "cookies-updated-message": "Les cookies ont été mis à jour. Souhaitez-vous redémarrer l'application pour utiliser les nouveaux cookies ?", + "yes-restart": "Oui, redémarrer", + "no-add-more": "Non, en ajouter plus", + "restart-required": "Redémarrage requis", + "restart-required-message": "Redémarrez l'application pour activer vos modifications de domaine de cookies.", + "restart": "Redémarrer", + "cookie-count": "{{count}} Cookies" } diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 75af7991e..0f4447e51 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -169,5 +169,73 @@ "delete-project": "Elimina Progetto", "delete-project-confirmation": "Sei sicuro di voler eliminare questo progetto e tutte le sue attività? Questa azione non può essere annullata.", "please-select-model": "Seleziona un modello in Impostazioni > Modelli per continuare.", - "capabilities": "Capacità" + "cdp-browser-connection": "Connessione browser CDP", + "cdp-browser-connection-description": "Connetti a un browser Chrome con il debug remoto abilitato", + "current-port": "Porta attuale:", + "cdp-port-check-description": "Verifica se un browser è disponibile su una porta specifica", + "port-placeholder": "Numero di porta (es. 9223)", + "checking": "Verifica in corso", + "check-port": "Verifica porta", + "browser-available": "Browser disponibile", + "browser-not-available": "Browser non disponibile", + "cdp-browser-pool": "Pool browser CDP", + "cdp-browser-pool-description": "Gestisci più browser CDP per l'esecuzione delle attività", + "external": "Esterno", + "launched": "Avviato", + "stopped": "Fermato", + "port": "Porta:", + "no-browsers-in-pool": "Nessun browser nel pool", + "add-browsers-hint": "Aggiungi browser utilizzando lo strumento di verifica porta sopra", + "invalid-port": "Inserisci un numero di porta valido (1-65535)", + "cdp-port-check-not-available": "Verifica porta CDP non disponibile", + "failed-to-check-port": "Verifica porta fallita", + "added-browser-to-pool": "Browser esterno sulla porta {{port}} aggiunto al pool", + "failed-to-add-browser": "Impossibile aggiungere il browser al pool", + "launch-not-available": "Avvio browser CDP non disponibile", + "launching-browser": "Avvio del browser sulla porta {{port}}...", + "browser-launched": "Browser avviato con successo sulla porta {{port}}", + "failed-to-launch-browser": "Impossibile avviare il browser", + "browser-removed": "Browser rimosso dal pool", + "failed-to-remove-browser": "Impossibile rimuovere il browser", + "remove-browser": "Rimuovi browser", + "remove-browser-confirm": "Questo disconnetterà e chiuderà il browser \"{{name}}\" sulla porta {{port}}. Sei sicuro?", + "remove": "Rimuovi", + "browser-opened": "Browser aperto con successo per l'accesso", + "restart-not-available": "Funzione di riavvio non disponibile", + "browser-found": "Browser trovato", + "browser-found-description": "Un browser è in esecuzione sulla porta {{port}}. Vuoi utilizzarlo per le operazioni del browser?", + "yes-use-browser": "Sì, usa questo browser", + "no-browser-found": "Nessun browser trovato", + "no-browser-found-description": "Nessun browser in esecuzione sulla porta {{port}}. Vuoi avviare un nuovo browser Chrome con CDP abilitato su questa porta?", + "yes-launch-browser": "Sì, avvia browser", + "for-more-info": "Per maggiori informazioni, consulta la nostra", + "capabilities": "Capacità", + "browser-connection": "Connessione", + "cookies-management": "Cookie", + "restart-to-enable": "Riavvia per abilitare", + "restart-to-enable-cookies-tooltip": "Riavvia il client per abilitare la nuova gestione dei cookie", + "open-new-browser": "Apri browser vuoto", + "browser-cookies-management": "Gestione cookie del browser", + "connect-existing-browser": "Connetti browser esistente", + "connect-existing-browser-description": "Connetti a un browser già in esecuzione con CDP abilitato su una porta specifica.", + "enter-port-number": "Inserisci il numero di porta", + "check-and-connect": "Verifica e connetti", + "port-already-in-use": "Questa porta è già nel pool dei browser. Usa una porta diversa.", + "no-browser-on-port": "Nessun browser trovato sulla porta {{port}}. Assicurati che un browser sia in esecuzione con --remote-debugging-port={{port}}.", + "connected-browser": "Connesso al browser sulla porta {{port}}", + "cookies-added": "{{count}} cookie aggiunti", + "failed-to-open-browser": "Impossibile aprire il browser", + "failed-to-load-cookies": "Impossibile caricare i cookie", + "deleted-cookies-for-domain": "Cookie eliminati per {{domain}} e tutti i sottodomini", + "failed-to-delete-cookies-for-domain": "Impossibile eliminare i cookie per {{domain}}", + "deleted-all-cookies": "Tutti i cookie eliminati", + "failed-to-delete-all-cookies": "Impossibile eliminare tutti i cookie", + "cookies-updated": "Cookie aggiornati", + "cookies-updated-message": "I cookie sono stati aggiornati. Vuoi riavviare l'applicazione per utilizzare i nuovi cookie?", + "yes-restart": "Sì, riavvia", + "no-add-more": "No, aggiungi altri", + "restart-required": "Riavvio necessario", + "restart-required-message": "Riavvia l'applicazione per abilitare le modifiche al dominio dei cookie.", + "restart": "Riavvia", + "cookie-count": "{{count}} Cookie" } diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index b033a43b7..d384a3e1c 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -169,5 +169,73 @@ "delete-project": "プロジェクトを削除", "delete-project-confirmation": "このプロジェクトとそのすべてのタスクを削除してもよろしいですか?この操作は元に戻せません。", "please-select-model": "続行するには、設定 > モデルでモデルを選択してください。", - "capabilities": "機能" + "cdp-browser-connection": "CDP ブラウザ接続", + "cdp-browser-connection-description": "リモートデバッグが有効な Chrome ブラウザに接続", + "current-port": "現在のポート:", + "cdp-port-check-description": "特定のポートでブラウザが利用可能か確認", + "port-placeholder": "ポート番号(例:9223)", + "checking": "確認中", + "check-port": "ポートを確認", + "browser-available": "ブラウザ利用可能", + "browser-not-available": "ブラウザ利用不可", + "cdp-browser-pool": "CDP ブラウザプール", + "cdp-browser-pool-description": "タスク実行用の複数の CDP ブラウザを管理", + "external": "外部", + "launched": "起動済み", + "stopped": "停止済み", + "port": "ポート:", + "no-browsers-in-pool": "プール内にブラウザがありません", + "add-browsers-hint": "上のポート確認ツールを使用してブラウザを追加してください", + "invalid-port": "有効なポート番号を入力してください(1-65535)", + "cdp-port-check-not-available": "CDP ポート確認は利用できません", + "failed-to-check-port": "ポートの確認に失敗しました", + "added-browser-to-pool": "ポート {{port}} の外部ブラウザをプールに追加しました", + "failed-to-add-browser": "ブラウザをプールに追加できませんでした", + "launch-not-available": "CDP ブラウザの起動は利用できません", + "launching-browser": "ポート {{port}} でブラウザを起動中...", + "browser-launched": "ポート {{port}} でブラウザが正常に起動しました", + "failed-to-launch-browser": "ブラウザの起動に失敗しました", + "browser-removed": "ブラウザがプールから削除されました", + "failed-to-remove-browser": "ブラウザの削除に失敗しました", + "remove-browser": "ブラウザを削除", + "remove-browser-confirm": "ポート {{port}} のブラウザ「{{name}}」を切断して閉じます。よろしいですか?", + "remove": "削除", + "browser-opened": "ログイン用にブラウザが正常に開きました", + "restart-not-available": "再起動機能は利用できません", + "browser-found": "ブラウザが見つかりました", + "browser-found-description": "ポート {{port}} でブラウザが実行中です。ブラウザ操作に使用しますか?", + "yes-use-browser": "はい、このブラウザを使用", + "no-browser-found": "ブラウザが見つかりません", + "no-browser-found-description": "ポート {{port}} でブラウザが実行されていません。このポートで CDP 対応の新しい Chrome ブラウザを起動しますか?", + "yes-launch-browser": "はい、ブラウザを起動", + "for-more-info": "詳細については、こちらをご覧ください", + "capabilities": "機能", + "browser-connection": "接続", + "cookies-management": "Cookie", + "restart-to-enable": "再起動して有効化", + "restart-to-enable-cookies-tooltip": "クライアントを再起動して新しい Cookie 管理を有効にする", + "open-new-browser": "空白ブラウザを開く", + "browser-cookies-management": "ブラウザ Cookie 管理", + "connect-existing-browser": "既存のブラウザに接続", + "connect-existing-browser-description": "特定のポートで CDP が有効な状態で実行中のブラウザに接続します。", + "enter-port-number": "ポート番号を入力", + "check-and-connect": "確認して接続", + "port-already-in-use": "このポートは既にブラウザプールにあります。別のポートを使用してください。", + "no-browser-on-port": "ポート {{port}} にブラウザが見つかりません。--remote-debugging-port={{port}} でブラウザが実行されていることを確認してください。", + "connected-browser": "ポート {{port}} のブラウザに接続しました", + "cookies-added": "{{count}} 個の Cookie を追加しました", + "failed-to-open-browser": "ブラウザを開けませんでした", + "failed-to-load-cookies": "Cookie の読み込みに失敗しました", + "deleted-cookies-for-domain": "{{domain}} とすべてのサブドメインの Cookie を削除しました", + "failed-to-delete-cookies-for-domain": "{{domain}} の Cookie の削除に失敗しました", + "deleted-all-cookies": "すべての Cookie を削除しました", + "failed-to-delete-all-cookies": "すべての Cookie の削除に失敗しました", + "cookies-updated": "Cookie が更新されました", + "cookies-updated-message": "Cookie が更新されました。新しい Cookie を使用するためにアプリケーションを再起動しますか?", + "yes-restart": "はい、再起動", + "no-add-more": "いいえ、追加を続ける", + "restart-required": "再起動が必要です", + "restart-required-message": "Cookie ドメインの変更を有効にするためにアプリケーションを再起動してください。", + "restart": "再起動", + "cookie-count": "{{count}} 個の Cookie" } diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index 40809222c..5bf9a77d1 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -169,5 +169,73 @@ "delete-project": "프로젝트 삭제", "delete-project-confirmation": "이 프로젝트와 모든 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "please-select-model": "계속하려면 설정 > 모델에서 모델을 선택하세요.", - "capabilities": "기능" + "cdp-browser-connection": "CDP 브라우저 연결", + "cdp-browser-connection-description": "원격 디버깅이 활성화된 Chrome 브라우저에 연결", + "current-port": "현재 포트:", + "cdp-port-check-description": "특정 포트에서 브라우저를 사용할 수 있는지 확인", + "port-placeholder": "포트 번호 (예: 9223)", + "checking": "확인 중", + "check-port": "포트 확인", + "browser-available": "브라우저 사용 가능", + "browser-not-available": "브라우저 사용 불가", + "cdp-browser-pool": "CDP 브라우저 풀", + "cdp-browser-pool-description": "작업 실행을 위한 여러 CDP 브라우저 관리", + "external": "외부", + "launched": "시작됨", + "stopped": "중지됨", + "port": "포트:", + "no-browsers-in-pool": "풀에 브라우저가 없습니다", + "add-browsers-hint": "위의 포트 확인 도구를 사용하여 브라우저를 추가하세요", + "invalid-port": "유효한 포트 번호를 입력하세요 (1-65535)", + "cdp-port-check-not-available": "CDP 포트 확인을 사용할 수 없습니다", + "failed-to-check-port": "포트 확인에 실패했습니다", + "added-browser-to-pool": "포트 {{port}}의 외부 브라우저가 풀에 추가되었습니다", + "failed-to-add-browser": "브라우저를 풀에 추가하지 못했습니다", + "launch-not-available": "CDP 브라우저 시작을 사용할 수 없습니다", + "launching-browser": "포트 {{port}}에서 브라우저 시작 중...", + "browser-launched": "포트 {{port}}에서 브라우저가 성공적으로 시작되었습니다", + "failed-to-launch-browser": "브라우저 시작에 실패했습니다", + "browser-removed": "브라우저가 풀에서 제거되었습니다", + "failed-to-remove-browser": "브라우저 제거에 실패했습니다", + "remove-browser": "브라우저 제거", + "remove-browser-confirm": "포트 {{port}}의 브라우저 \"{{name}}\"을(를) 연결 해제하고 닫습니다. 계속하시겠습니까?", + "remove": "제거", + "browser-opened": "로그인을 위해 브라우저가 성공적으로 열렸습니다", + "restart-not-available": "재시작 기능을 사용할 수 없습니다", + "browser-found": "브라우저 발견", + "browser-found-description": "포트 {{port}}에서 브라우저가 실행 중입니다. 브라우저 작업에 사용하시겠습니까?", + "yes-use-browser": "예, 이 브라우저 사용", + "no-browser-found": "브라우저를 찾을 수 없습니다", + "no-browser-found-description": "포트 {{port}}에서 실행 중인 브라우저가 없습니다. 이 포트에서 CDP가 활성화된 새 Chrome 브라우저를 시작하시겠습니까?", + "yes-launch-browser": "예, 브라우저 시작", + "for-more-info": "자세한 내용은 다음을 확인하세요", + "capabilities": "기능", + "browser-connection": "연결", + "cookies-management": "쿠키", + "restart-to-enable": "재시작하여 활성화", + "restart-to-enable-cookies-tooltip": "새로운 쿠키 관리를 활성화하려면 클라이언트를 재시작하세요", + "open-new-browser": "빈 브라우저 열기", + "browser-cookies-management": "브라우저 쿠키 관리", + "connect-existing-browser": "기존 브라우저 연결", + "connect-existing-browser-description": "특정 포트에서 CDP가 활성화된 상태로 실행 중인 브라우저에 연결합니다.", + "enter-port-number": "포트 번호 입력", + "check-and-connect": "확인 및 연결", + "port-already-in-use": "이 포트는 이미 브라우저 풀에 있습니다. 다른 포트를 사용하세요.", + "no-browser-on-port": "포트 {{port}}에서 브라우저를 찾을 수 없습니다. --remote-debugging-port={{port}}로 브라우저가 실행 중인지 확인하세요.", + "connected-browser": "포트 {{port}}의 브라우저에 연결되었습니다", + "cookies-added": "{{count}}개의 쿠키가 추가되었습니다", + "failed-to-open-browser": "브라우저를 열 수 없습니다", + "failed-to-load-cookies": "쿠키를 불러오지 못했습니다", + "deleted-cookies-for-domain": "{{domain}} 및 모든 하위 도메인의 쿠키가 삭제되었습니다", + "failed-to-delete-cookies-for-domain": "{{domain}}의 쿠키 삭제에 실패했습니다", + "deleted-all-cookies": "모든 쿠키가 삭제되었습니다", + "failed-to-delete-all-cookies": "모든 쿠키 삭제에 실패했습니다", + "cookies-updated": "쿠키 업데이트됨", + "cookies-updated-message": "쿠키가 업데이트되었습니다. 새 쿠키를 사용하기 위해 애플리케이션을 재시작하시겠습니까?", + "yes-restart": "예, 재시작", + "no-add-more": "아니요, 더 추가", + "restart-required": "재시작 필요", + "restart-required-message": "쿠키 도메인 변경을 활성화하려면 애플리케이션을 재시작하세요.", + "restart": "재시작", + "cookie-count": "{{count}}개 쿠키" } diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index 60241f8b2..15c1f4575 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -169,5 +169,73 @@ "delete-project": "Удалить проект", "delete-project-confirmation": "Вы уверены, что хотите удалить этот проект и все его задачи? Это действие нельзя отменить.", "please-select-model": "Пожалуйста, выберите модель в Настройки > Модели, чтобы продолжить.", - "capabilities": "Возможности" + "cdp-browser-connection": "Подключение CDP-браузера", + "cdp-browser-connection-description": "Подключиться к браузеру Chrome с включённой удалённой отладкой", + "current-port": "Текущий порт:", + "cdp-port-check-description": "Проверить, доступен ли браузер на определённом порту", + "port-placeholder": "Номер порта (напр. 9223)", + "checking": "Проверка", + "check-port": "Проверить порт", + "browser-available": "Браузер доступен", + "browser-not-available": "Браузер недоступен", + "cdp-browser-pool": "Пул CDP-браузеров", + "cdp-browser-pool-description": "Управление несколькими CDP-браузерами для выполнения задач", + "external": "Внешний", + "launched": "Запущен", + "stopped": "Остановлен", + "port": "Порт:", + "no-browsers-in-pool": "Нет браузеров в пуле", + "add-browsers-hint": "Добавьте браузеры с помощью инструмента проверки порта выше", + "invalid-port": "Введите действительный номер порта (1-65535)", + "cdp-port-check-not-available": "Проверка порта CDP недоступна", + "failed-to-check-port": "Не удалось проверить порт", + "added-browser-to-pool": "Внешний браузер на порту {{port}} добавлен в пул", + "failed-to-add-browser": "Не удалось добавить браузер в пул", + "launch-not-available": "Запуск CDP-браузера недоступен", + "launching-browser": "Запуск браузера на порту {{port}}...", + "browser-launched": "Браузер успешно запущен на порту {{port}}", + "failed-to-launch-browser": "Не удалось запустить браузер", + "browser-removed": "Браузер удалён из пула", + "failed-to-remove-browser": "Не удалось удалить браузер", + "remove-browser": "Удалить браузер", + "remove-browser-confirm": "Это отключит и закроет браузер \"{{name}}\" на порту {{port}}. Вы уверены?", + "remove": "Удалить", + "browser-opened": "Браузер успешно открыт для входа", + "restart-not-available": "Функция перезапуска недоступна", + "browser-found": "Браузер найден", + "browser-found-description": "Браузер работает на порту {{port}}. Хотите использовать его для операций браузера?", + "yes-use-browser": "Да, использовать этот браузер", + "no-browser-found": "Браузер не найден", + "no-browser-found-description": "На порту {{port}} не запущен браузер. Хотите запустить новый браузер Chrome с CDP на этом порту?", + "yes-launch-browser": "Да, запустить браузер", + "for-more-info": "Для получения дополнительной информации ознакомьтесь с нашей", + "capabilities": "Возможности", + "browser-connection": "Подключение", + "cookies-management": "Файлы cookie", + "restart-to-enable": "Перезапустить для активации", + "restart-to-enable-cookies-tooltip": "Перезапустите клиент для активации нового управления cookie", + "open-new-browser": "Открыть пустой браузер", + "browser-cookies-management": "Управление cookie-файлами браузера", + "connect-existing-browser": "Подключить существующий браузер", + "connect-existing-browser-description": "Подключиться к уже запущенному браузеру с включённым CDP на определённом порту.", + "enter-port-number": "Введите номер порта", + "check-and-connect": "Проверить и подключить", + "port-already-in-use": "Этот порт уже используется в пуле браузеров. Используйте другой порт.", + "no-browser-on-port": "Браузер не найден на порту {{port}}. Убедитесь, что браузер запущен с --remote-debugging-port={{port}}.", + "connected-browser": "Подключено к браузеру на порту {{port}}", + "cookies-added": "Добавлено {{count}} cookie-файл(ов)", + "failed-to-open-browser": "Не удалось открыть браузер", + "failed-to-load-cookies": "Не удалось загрузить cookie-файлы", + "deleted-cookies-for-domain": "Cookie-файлы для {{domain}} и всех поддоменов удалены", + "failed-to-delete-cookies-for-domain": "Не удалось удалить cookie-файлы для {{domain}}", + "deleted-all-cookies": "Все cookie-файлы удалены", + "failed-to-delete-all-cookies": "Не удалось удалить все cookie-файлы", + "cookies-updated": "Cookie-файлы обновлены", + "cookies-updated-message": "Cookie-файлы были обновлены. Хотите перезапустить приложение, чтобы использовать новые cookie-файлы?", + "yes-restart": "Да, перезапустить", + "no-add-more": "Нет, добавить ещё", + "restart-required": "Требуется перезапуск", + "restart-required-message": "Перезапустите приложение, чтобы активировать изменения домена cookie-файлов.", + "restart": "Перезапустить", + "cookie-count": "{{count}} Cookie" } diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index 933e99d7b..809e13c49 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -171,5 +171,73 @@ "delete-project": "删除项目", "delete-project-confirmation": "您确定要删除此项目及其所有任务吗?此操作无法撤销。", "please-select-model": "请在设置 > 模型中选择一个模型以继续。", - "capabilities": "能力" + "cdp-browser-connection": "CDP 浏览器连接", + "cdp-browser-connection-description": "连接到启用了远程调试的 Chrome 浏览器", + "current-port": "当前端口:", + "cdp-port-check-description": "检查指定端口上是否有可用的浏览器", + "port-placeholder": "端口号(例如 9223)", + "checking": "检查中", + "check-port": "检查端口", + "browser-available": "浏览器可用", + "browser-not-available": "浏览器不可用", + "cdp-browser-pool": "CDP 浏览器池", + "cdp-browser-pool-description": "管理多个 CDP 浏览器以执行任务", + "external": "外部", + "launched": "已启动", + "stopped": "已停止", + "port": "端口:", + "no-browsers-in-pool": "浏览器池中没有浏览器", + "add-browsers-hint": "使用上方的端口检查工具添加浏览器", + "invalid-port": "请输入有效的端口号(1-65535)", + "cdp-port-check-not-available": "CDP 端口检查不可用", + "failed-to-check-port": "端口检查失败", + "added-browser-to-pool": "已将端口 {{port}} 上的外部浏览器添加到池中", + "failed-to-add-browser": "添加浏览器到池失败", + "launch-not-available": "启动 CDP 浏览器不可用", + "launching-browser": "正在启动端口 {{port}} 上的浏览器...", + "browser-launched": "浏览器已在端口 {{port}} 上成功启动", + "failed-to-launch-browser": "启动浏览器失败", + "browser-removed": "浏览器已从池中移除", + "failed-to-remove-browser": "移除浏览器失败", + "remove-browser": "移除浏览器", + "remove-browser-confirm": "将断开并关闭端口 {{port}} 上的浏览器 \"{{name}}\",确定吗?", + "remove": "移除", + "browser-opened": "浏览器已成功打开以登录", + "restart-not-available": "重启功能不可用", + "browser-found": "找到浏览器", + "browser-found-description": "端口 {{port}} 上有浏览器正在运行。您要使用它进行浏览器操作吗?", + "yes-use-browser": "是的,使用此浏览器", + "no-browser-found": "未找到浏览器", + "no-browser-found-description": "端口 {{port}} 上没有浏览器运行。您要在此端口上启动启用 CDP 的新 Chrome 浏览器吗?", + "yes-launch-browser": "是的,启动浏览器", + "for-more-info": "如需更多信息,请查看我们的", + "capabilities": "能力", + "browser-connection": "连接", + "cookies-management": "Cookie 管理", + "restart-to-enable": "重启以启用", + "restart-to-enable-cookies-tooltip": "重启客户端以启用新的 Cookie 管理", + "open-new-browser": "打开空白浏览器", + "browser-cookies-management": "浏览器 Cookie 管理", + "connect-existing-browser": "连接现有浏览器", + "connect-existing-browser-description": "连接到已启用 CDP 并在指定端口运行的浏览器。", + "enter-port-number": "输入端口号", + "check-and-connect": "检查并连接", + "port-already-in-use": "该端口已在浏览器池中。请使用其他端口。", + "no-browser-on-port": "在端口 {{port}} 上未找到浏览器。请确保浏览器正在使用 --remote-debugging-port={{port}} 运行。", + "connected-browser": "已连接到端口 {{port}} 上的浏览器", + "cookies-added": "已添加 {{count}} 个 Cookie", + "failed-to-open-browser": "打开浏览器失败", + "failed-to-load-cookies": "加载 Cookie 失败", + "deleted-cookies-for-domain": "已删除 {{domain}} 及所有子域名的 Cookie", + "failed-to-delete-cookies-for-domain": "删除 {{domain}} 的 Cookie 失败", + "deleted-all-cookies": "已删除所有 Cookie", + "failed-to-delete-all-cookies": "删除所有 Cookie 失败", + "cookies-updated": "Cookie 已更新", + "cookies-updated-message": "Cookie 已更新。您要重启应用程序以使用新的 Cookie 吗?", + "yes-restart": "是的,重启", + "no-add-more": "不,继续添加", + "restart-required": "需要重启", + "restart-required-message": "重启应用程序以启用您的 Cookie 域名更改。", + "restart": "重启", + "cookie-count": "{{count}} 个 Cookie" } diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 1b7a4e966..7eddad72c 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -171,5 +171,73 @@ "delete-project": "刪除專案", "delete-project-confirmation": "您確定要刪除此專案及其所有任務嗎?此操作無法撤銷。", "please-select-model": "請在設定 > 模型中選擇一個模型以繼續。", - "capabilities": "能力" + "cdp-browser-connection": "CDP 瀏覽器連接", + "cdp-browser-connection-description": "連接到啟用了遠端偵錯的 Chrome 瀏覽器", + "current-port": "目前連接埠:", + "cdp-port-check-description": "檢查指定連接埠上是否有可用的瀏覽器", + "port-placeholder": "連接埠號(例如 9223)", + "checking": "檢查中", + "check-port": "檢查連接埠", + "browser-available": "瀏覽器可用", + "browser-not-available": "瀏覽器不可用", + "cdp-browser-pool": "CDP 瀏覽器池", + "cdp-browser-pool-description": "管理多個 CDP 瀏覽器以執行任務", + "external": "外部", + "launched": "已啟動", + "stopped": "已停止", + "port": "連接埠:", + "no-browsers-in-pool": "瀏覽器池中沒有瀏覽器", + "add-browsers-hint": "使用上方的連接埠檢查工具新增瀏覽器", + "invalid-port": "請輸入有效的連接埠號(1-65535)", + "cdp-port-check-not-available": "CDP 連接埠檢查不可用", + "failed-to-check-port": "連接埠檢查失敗", + "added-browser-to-pool": "已將連接埠 {{port}} 上的外部瀏覽器新增到池中", + "failed-to-add-browser": "新增瀏覽器到池失敗", + "launch-not-available": "啟動 CDP 瀏覽器不可用", + "launching-browser": "正在啟動連接埠 {{port}} 上的瀏覽器...", + "browser-launched": "瀏覽器已在連接埠 {{port}} 上成功啟動", + "failed-to-launch-browser": "啟動瀏覽器失敗", + "browser-removed": "瀏覽器已從池中移除", + "failed-to-remove-browser": "移除瀏覽器失敗", + "remove-browser": "移除瀏覽器", + "remove-browser-confirm": "將斷開並關閉連接埠 {{port}} 上的瀏覽器 \"{{name}}\",確定嗎?", + "remove": "移除", + "browser-opened": "瀏覽器已成功開啟以登入", + "restart-not-available": "重新啟動功能不可用", + "browser-found": "找到瀏覽器", + "browser-found-description": "連接埠 {{port}} 上有瀏覽器正在執行。您要使用它進行瀏覽器操作嗎?", + "yes-use-browser": "是的,使用此瀏覽器", + "no-browser-found": "未找到瀏覽器", + "no-browser-found-description": "連接埠 {{port}} 上沒有瀏覽器執行。您要在此連接埠上啟動啟用 CDP 的新 Chrome 瀏覽器嗎?", + "yes-launch-browser": "是的,啟動瀏覽器", + "for-more-info": "如需更多資訊,請查看我們的", + "capabilities": "能力", + "browser-connection": "連接", + "cookies-management": "Cookie 管理", + "restart-to-enable": "重啟以啟用", + "restart-to-enable-cookies-tooltip": "重啟客戶端以啟用新的 Cookie 管理", + "open-new-browser": "開啟空白瀏覽器", + "browser-cookies-management": "瀏覽器 Cookie 管理", + "connect-existing-browser": "連接現有瀏覽器", + "connect-existing-browser-description": "連接到已啟用 CDP 並在指定連接埠執行的瀏覽器。", + "enter-port-number": "輸入連接埠號", + "check-and-connect": "檢查並連接", + "port-already-in-use": "該連接埠已在瀏覽器池中。請使用其他連接埠。", + "no-browser-on-port": "在連接埠 {{port}} 上未找到瀏覽器。請確保瀏覽器正在使用 --remote-debugging-port={{port}} 執行。", + "connected-browser": "已連接到連接埠 {{port}} 上的瀏覽器", + "cookies-added": "已新增 {{count}} 個 Cookie", + "failed-to-open-browser": "開啟瀏覽器失敗", + "failed-to-load-cookies": "載入 Cookie 失敗", + "deleted-cookies-for-domain": "已刪除 {{domain}} 及所有子網域的 Cookie", + "failed-to-delete-cookies-for-domain": "刪除 {{domain}} 的 Cookie 失敗", + "deleted-all-cookies": "已刪除所有 Cookie", + "failed-to-delete-all-cookies": "刪除所有 Cookie 失敗", + "cookies-updated": "Cookie 已更新", + "cookies-updated-message": "Cookie 已更新。您要重新啟動應用程式以使用新的 Cookie 嗎?", + "yes-restart": "是的,重新啟動", + "no-add-more": "不,繼續新增", + "restart-required": "需要重新啟動", + "restart-required-message": "重新啟動應用程式以啟用您的 Cookie 網域變更。", + "restart": "重新啟動", + "cookie-count": "{{count}} 個 Cookie" } diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index 0cd575fc8..5a3e0539c 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -13,9 +13,18 @@ // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { fetchDelete, fetchGet, fetchPost } from '@/api/http'; +import VerticalNavigation from '@/components/Navigation'; import AlertDialog from '@/components/ui/alertDialog'; import { Button } from '@/components/ui/button'; -import { Cookie, Plus, RefreshCw, Trash2 } from 'lucide-react'; +import { + Cookie, + Globe, + Link2, + Loader2, + Plus, + RefreshCw, + Trash2, +} from 'lucide-react'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; @@ -32,16 +41,40 @@ interface GroupedDomain { totalCookies: number; } +interface CdpBrowser { + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; +} + export default function Browser() { const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState('connection'); const [loginLoading, setLoginLoading] = useState(false); const [cookiesLoading, setCookiesLoading] = useState(false); const [cookieDomains, setCookieDomains] = useState([]); const [deletingDomain, setDeletingDomain] = useState(null); const [deletingAll, setDeletingAll] = useState(false); const [showRestartDialog, setShowRestartDialog] = useState(false); - const [_cookiesBeforeBrowser, setCookiesBeforeBrowser] = useState(0); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [showCookieRestartDialog, setShowCookieRestartDialog] = useState(false); + + // CDP port configuration + const [cdpPort, setCdpPort] = useState(9223); + + // CDP Browser Pool + const [cdpBrowsers, setCdpBrowsers] = useState([]); + const [deletingBrowser, setDeletingBrowser] = useState(null); + const [browserToRemove, setBrowserToRemove] = useState( + null + ); + + // Connect Existing Browser dialog + const [showConnectDialog, setShowConnectDialog] = useState(false); + const [connectPort, setConnectPort] = useState(''); + const [connectChecking, setConnectChecking] = useState(false); + const [connectError, setConnectError] = useState(''); // Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com") const getMainDomain = (domain: string): string => { @@ -85,21 +118,156 @@ export default function Browser() { // Auto-load cookies on component mount useEffect(() => { handleLoadCookies(); + // Load current browser port on mount + loadCurrentBrowserPort(); + // Load CDP browser pool + loadCdpBrowsers(); + }, []); + + // Listen for CDP pool push updates from main process (health-check removes dead browsers) + useEffect(() => { + if (!window.electronAPI?.onCdpPoolChanged) return; + const cleanup = window.electronAPI.onCdpPoolChanged( + (browsers: CdpBrowser[]) => { + setCdpBrowsers(browsers); + } + ); + return cleanup; }, []); + const loadCurrentBrowserPort = async () => { + if (window.electronAPI?.getBrowserPort) { + const port = await window.electronAPI.getBrowserPort(); + setCdpPort(port); + } + }; + + const loadCdpBrowsers = async () => { + if (window.electronAPI?.getCdpBrowsers) { + try { + const browsers = await window.electronAPI.getCdpBrowsers(); + setCdpBrowsers(browsers); + } catch (error) { + console.error('Failed to load CDP browsers:', error); + } + } + }; + + const handleRemoveBrowser = async (browserId: string) => { + setDeletingBrowser(browserId); + try { + if (window.electronAPI?.removeCdpBrowser) { + const result = await window.electronAPI.removeCdpBrowser(browserId); + if (result.success) { + toast.success(t('layout.browser-removed')); + } else { + toast.error(result.error || t('layout.failed-to-remove-browser')); + } + } + } catch (error: any) { + toast.error(error.message || t('layout.failed-to-remove-browser')); + } finally { + setDeletingBrowser(null); + setBrowserToRemove(null); + } + }; + + const handleOpenNewBrowser = async () => { + try { + toast.loading(t('layout.launching-browser', { port: '...' }), { + id: 'launch-browser', + }); + + const result = await window.electronAPI?.launchCdpBrowser(); + + if (result?.success) { + toast.success(t('layout.browser-launched', { port: result.port }), { + id: 'launch-browser', + }); + } else { + toast.error(result?.error || t('layout.failed-to-launch-browser'), { + id: 'launch-browser', + }); + } + } catch (error: any) { + toast.error(error.message || t('layout.failed-to-launch-browser'), { + id: 'launch-browser', + }); + } + }; + const handleConnectExistingBrowser = () => { + setConnectPort(''); + setConnectError(''); + setShowConnectDialog(true); + }; + + const handleCheckAndConnect = async () => { + const portNum = parseInt(connectPort, 10); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + setConnectError(t('layout.invalid-port')); + return; + } + + // Check if port is already in the pool + if (cdpBrowsers.some((b) => b.port === portNum)) { + setConnectError(t('layout.port-already-in-use')); + return; + } + + setConnectChecking(true); + setConnectError(''); + + try { + // Probe the port to check if a CDP browser is listening + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + const response = await fetch(`http://localhost:${portNum}/json/version`, { + signal: controller.signal, + }); + clearTimeout(timeoutId); + + if (!response.ok) { + setConnectError(t('layout.no-browser-on-port', { port: portNum })); + return; + } + + // Port is alive — add to CDP pool + if (window.electronAPI?.addCdpBrowser) { + const addResult = await window.electronAPI.addCdpBrowser( + portNum, + true, + `External Browser (${portNum})` + ); + if (!addResult?.success) { + setConnectError( + addResult?.error || t('layout.failed-to-add-browser') + ); + return; + } + } else { + setConnectError(t('layout.failed-to-add-browser')); + return; + } + + toast.success(t('layout.connected-browser', { port: portNum })); + setShowConnectDialog(false); + } catch { + setConnectError(t('layout.no-browser-on-port', { port: portNum })); + } finally { + setConnectChecking(false); + } + }; + const handleBrowserLogin = async () => { setLoginLoading(true); + const currentCookieCount = cookieDomains.reduce( + (sum, item) => sum + item.cookie_count, + 0 + ); try { - // Record current cookie count before opening browser - const currentCookieCount = cookieDomains.reduce( - (sum, item) => sum + item.cookie_count, - 0 - ); - setCookiesBeforeBrowser(currentCookieCount); - const response = await fetchPost('/browser/login'); if (response) { - toast.success('Browser opened successfully for login'); + toast.success(t('layout.browser-opened')); // Listen for browser close event to reload cookies const checkInterval = setInterval(async () => { try { @@ -122,27 +290,23 @@ export default function Browser() { // Cookies were added, show success toast and restart dialog const addedCount = newCookieCount - currentCookieCount; toast.success( - `Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}` + t('layout.cookies-added', { count: addedCount }) ); - setHasUnsavedChanges(true); setShowRestartDialog(true); } else if (newCookieCount < currentCookieCount) { - // Cookies were deleted (shouldn't happen here, but handle it) - setHasUnsavedChanges(true); setShowRestartDialog(true); } } } } catch (error) { // Browser might be closed - console.error(error); clearInterval(checkInterval); await handleLoadCookies(); } - }, 500); // Check every 2 seconds + }, 500); } } catch (error: any) { - toast.error(error?.message || 'Failed to open browser'); + toast.error(error?.message || t('layout.failed-to-open-browser')); } finally { setLoginLoading(false); } @@ -159,7 +323,7 @@ export default function Browser() { setCookieDomains([]); } } catch (error: any) { - toast.error(error?.message || 'Failed to load cookies'); + toast.error(error?.message || t('layout.failed-to-load-cookies')); setCookieDomains([]); } finally { setCookiesLoading(false); @@ -178,39 +342,40 @@ export default function Browser() { ); await Promise.all(deletePromises); - toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); + toast.success( + t('layout.deleted-cookies-for-domain', { domain: mainDomain }) + ); // Remove from local state const domainsToRemove = new Set(subdomains.map((item) => item.domain)); setCookieDomains((prev) => prev.filter((item) => !domainsToRemove.has(item.domain)) ); - // Mark as having unsaved changes - setHasUnsavedChanges(true); // Show restart dialog after successful deletion setShowRestartDialog(true); } catch (error: any) { toast.error( - error?.message || `Failed to delete cookies for ${mainDomain}` + error?.message || + t('layout.failed-to-delete-cookies-for-domain', { + domain: mainDomain, + }) ); } finally { setDeletingDomain(null); } }; - 4; + const handleDeleteAll = async () => { setDeletingAll(true); try { await fetchDelete('/browser/cookies'); - toast.success('Deleted all cookies'); + toast.success(t('layout.deleted-all-cookies')); setCookieDomains([]); - // Mark as having unsaved changes - setHasUnsavedChanges(true); // Show restart dialog after successful deletion setShowRestartDialog(true); } catch (error: any) { - toast.error(error?.message || 'Failed to delete all cookies'); + toast.error(error?.message || t('layout.failed-to-delete-all-cookies')); } finally { setDeletingAll(false); } @@ -220,7 +385,7 @@ export default function Browser() { if (window.electronAPI && window.electronAPI.restartApp) { window.electronAPI.restartApp(); } else { - toast.error('Restart function not available'); + toast.error(t('layout.restart-not-available')); } }; @@ -230,174 +395,326 @@ export default function Browser() { }; return ( -
+
{/* Restart Dialog */} setShowRestartDialog(false)} onConfirm={handleConfirmRestart} - title="Cookies Updated" - message="Cookies have been updated. Would you like to restart the application to use the new cookies?" - confirmText="Yes, Restart" - cancelText="No, Add More" + title={t('layout.cookies-updated')} + message={t('layout.cookies-updated-message')} + confirmText={t('layout.yes-restart')} + cancelText={t('layout.no-add-more')} confirmVariant="information" /> - {/* Header Section */} -
-
-
-
-
- {t('layout.browser-management')} -
-

- {t('layout.browser-management-description')}. + {/* Cookie Restart Confirm Dialog */} + setShowCookieRestartDialog(false)} + onConfirm={() => { + setShowCookieRestartDialog(false); + handleRestartApp(); + }} + title={t('layout.restart-required')} + message={t('layout.restart-required-message')} + confirmText={t('layout.restart')} + cancelText={t('layout.cancel')} + confirmVariant="information" + /> + + {/* Remove Browser Confirmation Dialog */} + setBrowserToRemove(null)} + onConfirm={() => { + if (browserToRemove) { + handleRemoveBrowser(browserToRemove.id); + } + }} + title={t('layout.remove-browser')} + message={t('layout.remove-browser-confirm', { + name: browserToRemove?.name || `Browser ${browserToRemove?.port}`, + port: browserToRemove?.port, + })} + confirmText={t('layout.remove')} + cancelText={t('layout.cancel')} + confirmVariant="cuation" + /> + + {/* Connect Existing Browser Dialog */} + {showConnectDialog && ( +

+
+
+ {t('layout.connect-existing-browser')} +
+

+ {t('layout.connect-existing-browser-description')} +

+ { + setConnectPort(e.target.value); + setConnectError(''); + }} + placeholder={t('layout.enter-port-number')} + className="w-full rounded-lg border border-border-disabled bg-surface-secondary px-4 py-2 text-body-sm text-text-body outline-none focus:border-border-focus" + onKeyDown={(e) => { + if (e.key === 'Enter') handleCheckAndConnect(); + }} + /> + {connectError && ( +

+ {connectError}

+ )} +
+ +
-
+ )} - {/* Content Section */} -
-
-
-
- -
-
- {t('layout.browser-cookies')} -
-

- {t('layout.browser-cookies-description')} -

- {/* Cookies Section */} -
-
-
-
- {t('layout.cookie-domains')} -
- {cookieDomains.length > 0 && ( -
- {groupDomainsByMain(cookieDomains).length} + + {t('layout.open-new-browser')} + + +
+ + {/* CDP Browser Pool */} +
+
+
+
+ {t('layout.cdp-browser-pool')}
- )} +
-
- {cookieDomains.length > 0 && ( - - )} - - -
+ {cdpBrowsers.length > 0 ? ( +
+ {cdpBrowsers.map((browser) => ( +
+
+
+
+ + {browser.name || `Browser ${browser.port}`} + + + {t('layout.port')} {browser.port} + +
+
+ +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-browsers-in-pool')} +
+

+ {t('layout.add-browsers-hint')} +

+
+ )}
+
+ )} - {cookieDomains.length > 0 ? ( -
- {groupDomainsByMain(cookieDomains).map((group, index) => ( -
-
- - {group.mainDomain} - - - {group.totalCookies} Cookie - {group.totalCookies !== 1 ? 's' : ''} - + {activeTab === 'cookies' && ( +
+
+ {t('layout.browser-cookies-management')} +
+ + {/* Action Buttons */} +
+ +
+ + {/* Cookie Domains */} +
+
+
+
+ {t('layout.cookie-domains')} +
+ {cookieDomains.length > 0 && ( +
+ {groupDomainsByMain(cookieDomains).length}
+ )} +
+
+ {cookieDomains.length > 0 && ( -
- ))} -
- ) : ( -
- -
- {t('layout.no-cookies-saved-yet')} + )} +
-

- {t('layout.no-cookies-saved-yet-description')} -

- )} -
-
-
- For more information, check out our - - {t('layout.privacy-policy')} - -
+ {cookieDomains.length > 0 ? ( +
+ {groupDomainsByMain(cookieDomains).map((group, index) => ( +
+
+ + {group.mainDomain} + + + {t('layout.cookie-count', { + count: group.totalCookies, + })} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-cookies-saved-yet')} +
+

+ {t('layout.no-cookies-saved-yet-description')} +

+
+ )} +
+
+ )}
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 547f9af4e..45924a6ef 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -651,6 +651,7 @@ const chatStore = (initial?: Partial) => }); } const browser_port = await window.ipcRenderer.invoke('get-browser-port'); + const cdp_browsers = await window.ipcRenderer.invoke('get-cdp-browsers'); // Lock the chatStore reference at the start of SSE session to prevent focus changes // during active message processing @@ -728,6 +729,7 @@ const chatStore = (initial?: Partial) => summary_prompt: ``, new_agents: [...addWorkers], browser_port: browser_port, + cdp_browsers: cdp_browsers, env_path: envPath, search_config: searchConfig, }) diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 739c588b4..b3b9dca2c 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -206,6 +206,25 @@ interface ElectronAPI { userId: string, skillName: string ) => Promise<{ success: boolean; error?: string }>; + setBrowserPort: (port: number, isExternal?: boolean) => Promise; + getBrowserPort: () => Promise; + getCdpBrowsers: () => Promise; + addCdpBrowser: ( + port: number, + isExternal: boolean, + name?: string + ) => Promise<{ success: boolean; browser?: any; error?: string }>; + removeCdpBrowser: ( + browserId: string, + closeBrowser?: boolean + ) => Promise<{ success: boolean; browser?: any; error?: string }>; + onCdpPoolChanged: (callback: (browsers: any[]) => void) => () => void; + launchCdpBrowser: () => Promise<{ + success: boolean; + port?: number; + data?: any; + error?: string; + }>; } declare global {