diff --git a/.github/workflows/build-view.yml b/.github/workflows/build-view.yml index 3546e9fe..67f6cc44 100644 --- a/.github/workflows/build-view.yml +++ b/.github/workflows/build-view.yml @@ -20,6 +20,8 @@ jobs: arch: x64 - os: windows-latest arch: x64 + - os: ubuntu-latest + arch: x64 steps: - name: Checkout Code @@ -46,6 +48,13 @@ jobs: - name: Install Dependencies run: npm install + # Install libfuse2 for Linux AppImage builds + - name: Install libfuse2 (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libfuse2 + # Step for macOS builds with signing - name: Build Release Files (macOS with signing) if: runner.os == 'macOS' @@ -78,6 +87,19 @@ jobs: VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }} USE_NPM_INSTALL_BUN: 'true' + # Step for Linux builds + - name: Build Release Files (Linux) + if: runner.os == 'Linux' + timeout-minutes: 90 + run: npm run build:linux + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }} + VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }} + VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }} + VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }} + USE_NPM_INSTALL_BUN: 'true' + - name: Upload Artifact (macOS - dmg only) if: runner.os == 'macOS' uses: actions/upload-artifact@v6 @@ -95,13 +117,22 @@ jobs: path: | release/*.exe retention-days: 5 + + - name: Upload Artifact (Linux - AppImage only) + if: runner.os == 'Linux' + uses: actions/upload-artifact@v6 + with: + name: release-${{ matrix.os }}-${{ matrix.arch }} + path: | + release/*.AppImage + retention-days: 5 merge-release: needs: build runs-on: ubuntu-latest steps: - name: Create directories run: | - mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 + mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64 # Download all artifacts with correct names - name: Download mac-x64 artifact @@ -122,7 +153,13 @@ jobs: name: release-windows-latest-x64 path: temp-win-x64 - # Move only dmg files for macOS and exe files for Windows + - name: Download linux-x64 artifact + uses: actions/download-artifact@v7 + with: + name: release-ubuntu-latest-x64 + path: temp-linux-x64 + + # Move only dmg files for macOS, exe files for Windows, and AppImage for Linux - name: Move files to clean folders shell: bash run: | @@ -146,3 +183,10 @@ jobs: else find temp-win-x64 -name "*.exe" -exec mv {} release/win-x64/ \; || true fi + + # linux-x64 - only move AppImage files + if [ -d "temp-linux-x64/release" ]; then + find temp-linux-x64/release -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true + else + find temp-linux-x64 -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true + fi diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index 09a8f7c9..b3aef085 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -115,15 +115,12 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI @router.post("/chat", name="start chat") @traceroot.trace() async def post(data: Chat, request: Request): - request_start_time = time.time() chat_logger.info( "Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email} ) task_lock = get_or_create_task_lock(data.project_id) - # Store request start time in task_lock for downstream timing - task_lock.request_start_time = request_start_time # Set user-specific environment path for this thread set_user_env_path(data.env_path) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index e6e6918f..e37f9aa2 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -238,18 +238,9 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str: @sync_step @traceroot.trace() async def step_solve(options: Chat, request: Request, task_lock: TaskLock): - import time as time_module - - # === TIMING: step_solve started === - step_solve_start = time_module.time() - request_start_time = getattr(task_lock, 'request_start_time', step_solve_start) - time_since_request = (step_solve_start - request_start_time) * 1000 - logger.info(f"⏱️ [TIMING] step_solve started, {time_since_request:.2f}ms since request received") - start_event_loop = True - # === TIMING: Task lock initialization === - init_start = time_module.time() + # Initialize task_lock attributes if not hasattr(task_lock, 'conversation_history'): task_lock.conversation_history = [] if not hasattr(task_lock, 'last_task_result'): @@ -258,24 +249,19 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): task_lock.question_agent = None if not hasattr(task_lock, 'summary_generated'): task_lock.summary_generated = False - init_time = (time_module.time() - init_start) * 1000 - logger.info(f"⏱️ [TIMING] Task lock attrs initialized in {init_time:.2f}ms") # Create or reuse persistent question_agent - # === TIMING: question_agent creation === - question_agent_start = time_module.time() if task_lock.question_agent is None: task_lock.question_agent = question_confirm_agent(options) - question_agent_time = (time_module.time() - question_agent_start) * 1000 - logger.info(f"⏱️ [TIMING] question_confirm_agent created in {question_agent_time:.2f}ms") else: - logger.info(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries") + logger.debug(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries") question_agent = task_lock.question_agent # Other variables camel_task = None workforce = None + mcp = None last_completed_task_result = "" # Track the last completed task result summary_task_content = "" # Track task summary loop_iteration = 0 @@ -312,11 +298,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): logger.info(f"[LIFECYCLE] Breaking out of step_solve loop due to client disconnect") break try: - # === TIMING: Waiting for queue item === - queue_wait_start = time_module.time() item = await task_lock.get_queue() - queue_wait_time = (time_module.time() - queue_wait_start) * 1000 - logger.info(f"⏱️ [TIMING] Got item from queue in {queue_wait_time:.2f}ms, action={item.action}") except Exception as e: logger.error("Error getting item from queue", extra={"project_id": options.project_id, "task_id": options.task_id, "error": str(e)}, exc_info=True) # Continue waiting instead of breaking on queue error @@ -352,25 +334,13 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): }) continue - # === TIMING: Complexity determination === - complexity_start = time_module.time() - time_since_request_to_complexity = (complexity_start - request_start_time) * 1000 - logger.info(f"⏱️ [TIMING] Starting complexity check, {time_since_request_to_complexity:.2f}ms since request") - - # Simplified logic: attachments mean workforce, otherwise let agent decide + # Determine task complexity: attachments mean workforce, otherwise let agent decide is_complex_task: bool if len(options.attaches) > 0: - # Questions with attachments always need workforce is_complex_task = True logger.info(f"[NEW-QUESTION] Has attachments, treating as complex task") - complexity_time = (time_module.time() - complexity_start) * 1000 - logger.info(f"⏱️ [TIMING] Complexity check (has attachments) completed in {complexity_time:.2f}ms") else: - logger.info(f"[NEW-QUESTION] Calling question_confirm to determine complexity") - question_confirm_start = time_module.time() is_complex_task = await question_confirm(question_agent, question, task_lock) - question_confirm_time = (time_module.time() - question_confirm_start) * 1000 - logger.info(f"⏱️ [TIMING] question_confirm completed in {question_confirm_time:.2f}ms, is_complex={is_complex_task}") logger.info(f"[NEW-QUESTION] question_confirm result: is_complex={is_complex_task}") if not is_complex_task: @@ -409,83 +379,36 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): except Exception as e: logger.error(f"Error cleaning up folder: {e}") else: - # === TIMING: Complex task processing === - complex_task_start = time_module.time() - time_since_request_to_complex = (complex_task_start - request_start_time) * 1000 - logger.info(f"⏱️ [TIMING] Complex task processing started, {time_since_request_to_complex:.2f}ms since request") - - logger.info(f"[NEW-QUESTION] 🔧 Complex task, creating workforce and decomposing") + logger.info(f"[NEW-QUESTION] Complex task, creating workforce and decomposing") # Update the sync_step with new task_id if hasattr(item, 'new_task_id') and item.new_task_id: set_current_task_id(options.project_id, item.new_task_id) - # Reset summary generation flag for new tasks to ensure proper summaries task_lock.summary_generated = False - logger.info("[NEW-QUESTION] Reset summary_generated flag for new task", extra={"project_id": options.project_id, "new_task_id": item.new_task_id}) - logger.info(f"[NEW-QUESTION] Sending 'confirmed' SSE to frontend") yield sse_json("confirmed", {"question": question}) - # === TIMING: Context building === - context_build_start = time_module.time() - logger.info(f"[NEW-QUESTION] Building context for coordinator") context_for_coordinator = build_context_for_workforce(task_lock, options) - context_build_time = (time_module.time() - context_build_start) * 1000 - logger.info(f"⏱️ [TIMING] Context building completed in {context_build_time:.2f}ms") - # Check if workforce exists - if so, reuse it (agents are preserved) - # Otherwise create new workforce + # Check if workforce exists - if so, reuse it; otherwise create new workforce if workforce is not None: - logger.info(f"[NEW-QUESTION] 🔄 Workforce exists (id={id(workforce)}), state={workforce._state.name}, _running={workforce._running}") - logger.info(f"[NEW-QUESTION] ✅ Reusing existing workforce with preserved agents") - # Workforce is already stopped from skip_task, ready for new decomposition + logger.debug(f"[NEW-QUESTION] Reusing existing workforce (id={id(workforce)})") else: - # === TIMING: Workforce construction === - workforce_construct_start = time_module.time() - logger.info(f"[NEW-QUESTION] 🏭 Creating NEW workforce instance (workforce=None)") + logger.info(f"[NEW-QUESTION] Creating NEW workforce instance") (workforce, mcp) = await construct_workforce(options) - workforce_construct_time = (time_module.time() - workforce_construct_start) * 1000 - logger.info(f"⏱️ [TIMING] Workforce construction completed in {workforce_construct_time:.2f}ms") - logger.info(f"[NEW-QUESTION] ✅ NEW Workforce instance created, id={id(workforce)}") for new_agent in options.new_agents: workforce.add_single_agent_worker( format_agent_description(new_agent), await new_agent_model(new_agent, options) ) task_lock.status = Status.confirmed - # === TIMING: Task creation === - task_create_start = time_module.time() + # Create camel_task for the question + clean_task_content = question + options.summary_prompt + camel_task = Task(content=clean_task_content, id=options.task_id) + if len(options.attaches) > 0: + camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches} - # If camel_task already exists (from previous paused task), add new question as subtask - # Otherwise, create a new camel_task - if camel_task is not None: - logger.info(f"[NEW-QUESTION] 🔄 camel_task exists (id={camel_task.id}), adding new question as context") - # Update the task content with new question - clean_task_content = question + options.summary_prompt - logger.info(f"[NEW-QUESTION] Updating existing camel_task content with new question") - # We keep the existing task structure but update content for new decomposition - camel_task = Task(content=clean_task_content, id=options.task_id) - if len(options.attaches) > 0: - camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches} - else: - clean_task_content = question + options.summary_prompt - logger.info(f"[NEW-QUESTION] Creating NEW camel_task with id={options.task_id}") - camel_task = Task(content=clean_task_content, id=options.task_id) - if len(options.attaches) > 0: - camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches} - - task_create_time = (time_module.time() - task_create_start) * 1000 - logger.info(f"⏱️ [TIMING] Task object created in {task_create_time:.2f}ms") - - # === TIMING: Task decomposition start === - decomposition_start = time_module.time() - time_since_request_to_decompose = (decomposition_start - request_start_time) * 1000 - logger.info(f"⏱️ [TIMING] Starting task decomposition, {time_since_request_to_decompose:.2f}ms since request") - # Store decomposition start time in task_lock for downstream tracking - task_lock.decomposition_start_time = decomposition_start - - # Stream decomposition in background so queue items (decompose_text) are processed immediately - logger.info(f"[NEW-QUESTION] 🧩 Starting task decomposition via workforce.eigent_make_sub_tasks") - stream_state = {"subtasks": [], "seen_ids": set(), "last_content": "", "first_token_logged": False} + # Stream decomposition in background + stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""} state_holder: dict[str, Any] = {"sub_tasks": [], "summary_task": ""} def on_stream_batch(new_tasks: list[Task], is_final: bool = False): @@ -496,8 +419,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): def on_stream_text(chunk): try: - # With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content - # We need to calculate the delta to send only new content to frontend accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk) last_content = stream_state["last_content"] @@ -510,12 +431,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): stream_state["last_content"] = accumulated_content if delta_content: - # === TIMING: Log TTFT (Time to First Token) === - if not stream_state["first_token_logged"]: - stream_state["first_token_logged"] = True - ttft = (time_module.time() - task_lock.decomposition_start_time) * 1000 - logger.info(f"⏱️ [TIMING] 🚀 TTFT (Time to First Token): {ttft:.2f}ms - First streaming token received for task decomposition") - asyncio.run_coroutine_threadsafe( task_lock.put_queue( ActionDecomposeTextData( @@ -534,10 +449,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): async def run_decomposition(): nonlocal camel_task, summary_task_content try: - # === TIMING: LLM decomposition call === - llm_decompose_start = time_module.time() - decomposition_start_time = getattr(task_lock, 'decomposition_start_time', llm_decompose_start) - sub_tasks = await asyncio.to_thread( workforce.eigent_make_sub_tasks, camel_task, @@ -546,60 +457,44 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): on_stream_text, ) - llm_decompose_time = (time_module.time() - llm_decompose_start) * 1000 - total_decompose_time = (time_module.time() - decomposition_start_time) * 1000 - logger.info(f"⏱️ [TIMING] LLM decomposition completed in {llm_decompose_time:.2f}ms (total decompose phase: {total_decompose_time:.2f}ms)") - if stream_state["subtasks"]: sub_tasks = stream_state["subtasks"] state_holder["sub_tasks"] = sub_tasks - logger.info(f"[NEW-QUESTION] ✅ Task decomposed into {len(sub_tasks)} subtasks") + logger.info(f"Task decomposed into {len(sub_tasks)} subtasks") try: setattr(task_lock, "decompose_sub_tasks", sub_tasks) except Exception: pass - # === TIMING: Summary generation === - summary_start = time_module.time() - logger.info(f"[NEW-QUESTION] Generating task summary") + # Generate task summary summary_task_agent = task_summary_agent(options) try: summary_task_content = await asyncio.wait_for( summary_task(summary_task_agent, camel_task), timeout=10 ) - summary_time = (time_module.time() - summary_start) * 1000 task_lock.summary_generated = True - logger.info(f"⏱️ [TIMING] Summary generation completed in {summary_time:.2f}ms") - logger.info("[NEW-QUESTION] ✅ Summary generated successfully", extra={"project_id": options.project_id}) except asyncio.TimeoutError: logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id}) task_lock.summary_generated = True - fallback_name = "Task" content_preview = camel_task.content if hasattr(camel_task, "content") else "" if content_preview is None: content_preview = "" - summary_task_content = ( - (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview - ) - summary_task_content = f"{fallback_name}|{summary_task_content}" + summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview + summary_task_content = f"Task|{summary_task_content}" except Exception: task_lock.summary_generated = True - fallback_name = "Task" content_preview = camel_task.content if hasattr(camel_task, "content") else "" if content_preview is None: content_preview = "" - summary_task_content = ( - (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview - ) - summary_task_content = f"{fallback_name}|{summary_task_content}" + summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview + summary_task_content = f"Task|{summary_task_content}" state_holder["summary_task"] = summary_task_content try: setattr(task_lock, "summary_task_content", summary_task_content) except Exception: pass - logger.info(f"[NEW-QUESTION] 📤 Sending to_sub_tasks SSE to frontend (task card)") - logger.info(f"[NEW-QUESTION] to_sub_tasks data: task_id={camel_task.id}, summary={summary_task_content[:50]}..., subtasks_count={len(camel_task.subtasks)}") + payload = { "project_id": options.project_id, "task_id": options.task_id, @@ -609,12 +504,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "summary_task": summary_task_content, } await task_lock.put_queue(ActionDecomposeProgressData(data=payload)) - logger.info(f"[NEW-QUESTION] ✅ to_sub_tasks SSE sent") - - # === TIMING: Total time from request to decomposition complete === - request_start = getattr(task_lock, 'request_start_time', decomposition_start_time) - total_request_to_decompose = (time_module.time() - request_start) * 1000 - logger.info(f"⏱️ [TIMING] ===== TOTAL: Request → Decomposition Complete: {total_request_to_decompose:.2f}ms =====") except Exception as e: logger.error(f"Error in background decomposition: {e}", exc_info=True) @@ -854,9 +743,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): logger.info(f"[LIFECYCLE] Multi-turn: building context for workforce") context_for_multi_turn = build_context_for_workforce(task_lock, options) - logger.info(f"[LIFECYCLE] Multi-turn: calling workforce.handle_decompose_append_task for new task decomposition") - multi_turn_decompose_start = time_module.time() - stream_state = {"subtasks": [], "seen_ids": set(), "last_content": "", "first_token_logged": False, "start_time": multi_turn_decompose_start} + stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""} def on_stream_batch(new_tasks: list[Task], is_final: bool = False): fresh_tasks = [t for t in new_tasks if t.id not in stream_state["seen_ids"]] @@ -866,8 +753,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): def on_stream_text(chunk): try: - # With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content - # We need to calculate the delta to send only new content to frontend accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk) last_content = stream_state["last_content"] @@ -879,12 +764,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): stream_state["last_content"] = accumulated_content if delta_content: - # === TIMING: Log TTFT (Time to First Token) for multi-turn === - if not stream_state["first_token_logged"]: - stream_state["first_token_logged"] = True - ttft = (time_module.time() - stream_state["start_time"]) * 1000 - logger.info(f"⏱️ [TIMING] 🚀 TTFT (Time to First Token): {ttft:.2f}ms - First streaming token received for multi-turn task decomposition") - asyncio.run_coroutine_threadsafe( task_lock.put_queue( ActionDecomposeTextData( @@ -987,6 +866,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): elif item.action == Action.search_mcp: yield sse_json("search_mcp", item.data) elif item.action == Action.install_mcp: + if mcp is None: + logger.error(f"Cannot install MCP: mcp agent not initialized for project {options.project_id}") + yield sse_json("error", {"message": "MCP agent not initialized. Please start a complex task first."}) + continue task = asyncio.create_task(install_mcp(mcp, item)) task_lock.add_background_task(task) elif item.action == Action.terminal: diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py index 91cfd593..d3ca259d 100644 --- a/backend/app/utils/toolkit/terminal_toolkit.py +++ b/backend/app/utils/toolkit/terminal_toolkit.py @@ -64,6 +64,13 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): safe_mode=safe_mode, allowed_commands=allowed_commands, clone_current_env=clone_current_env, + install_dependencies=[ + "pandas", + "numpy", + "matplotlib", + "requests", + "openpyxl", + ], ) # Auto-register with TaskLock for cleanup when task ends diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 0d17b965..362eb0d4 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -355,6 +355,7 @@ class Workforce(BaseWorkforce): f"Task {task.id} will not be properly tracked on frontend. " f"Available workers: {[c.node_id for c in self._children if hasattr(c, 'node_id')]}" ) + else: await task_lock.put_queue( ActionAssignTaskData( action=Action.assign_task, diff --git a/electron-builder.json b/electron-builder.json index ef5630cb..0ff93ab7 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -67,7 +67,6 @@ } }, "win": { - "certificateFile": null, "icon": "build/icon.ico", "artifactName": "${productName}.Setup.${version}.exe", "target": [ diff --git a/electron/main/index.ts b/electron/main/index.ts index 611d5fcd..1261ec42 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -112,6 +112,32 @@ app.commandLine.appendSwitch('max_old_space_size', '4096'); app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction'); app.commandLine.appendSwitch('renderer-process-limit', '8'); +// ==================== Anti-fingerprint settings ==================== +// Disable automation controlled indicator to avoid detection +app.commandLine.appendSwitch( + 'disable-blink-features', + 'AutomationControlled' +); + +// Override User Agent to remove Electron/eigent identifiers +// Dynamically generate User Agent based on actual platform and Chrome version +const getPlatformUA = () => { + // Use actual Chrome version from Electron instead of hardcoded value + const chromeVersion = process.versions.chrome || '131.0.0.0'; + switch (process.platform) { + case 'darwin': + return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`; + case 'win32': + return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`; + case 'linux': + return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`; + default: + return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`; + } +}; +const normalUserAgent = getPlatformUA(); +app.userAgentFallback = normalUserAgent; + // ==================== protocol privileges ==================== // Register custom protocol privileges before app ready protocol.registerSchemesAsPrivileged([ @@ -1827,6 +1853,15 @@ app.whenReady().then(async () => { } } + // ==================== Anti-fingerprint: Set User Agent for all sessions ==================== + // Use the same dynamic User Agent as app.userAgentFallback + session.defaultSession.setUserAgent(normalUserAgent); + // Also set for the user_login partition used by webviews + session.fromPartition('persist:user_login').setUserAgent(normalUserAgent); + // And for main_window partition + session.fromPartition('persist:main_window').setUserAgent(normalUserAgent); + log.info('[ANTI-FINGERPRINT] User Agent set for all sessions'); + // ==================== download handle ==================== session.defaultSession.on('will-download', (event, item, webContents) => { item.once('done', (event, state) => { diff --git a/electron/main/webview.ts b/electron/main/webview.ts index 8fae65a6..96dd86e1 100644 --- a/electron/main/webview.ts +++ b/electron/main/webview.ts @@ -72,13 +72,108 @@ export class WebViewManager { backgroundThrottling: true, offscreen: false, sandbox: true, - disableBlinkFeatures: 'Accelerated2dCanvas', + disableBlinkFeatures: 'Accelerated2dCanvas,AutomationControlled', enableBlinkFeatures: 'IdleDetection', autoplayPolicy: 'document-user-activation-required', }, }) view.webContents.on('did-finish-load', () => { + // Inject stealth script to avoid bot detection view.webContents.executeJavaScript(` + // Save original values before overriding to maintain consistency + const originalLanguages = navigator.languages ? [...navigator.languages] : ['en-US', 'en']; + const originalHardwareConcurrency = navigator.hardwareConcurrency || 8; + const originalDeviceMemory = navigator.deviceMemory || 8; + + // Hide webdriver property + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined, + configurable: true + }); + + // Override plugins with proper PluginArray-like behavior + Object.defineProperty(navigator, 'plugins', { + get: () => { + const plugins = { + length: 3, + 0: { name: 'Chrome PDF Plugin', description: 'Portable Document Format', filename: 'internal-pdf-viewer' }, + 1: { name: 'Chrome PDF Viewer', description: '', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' }, + 2: { name: 'Native Client', description: '', filename: 'internal-nacl-plugin' }, + item: function(index) { return this[index] || null; }, + namedItem: function(name) { + for (let i = 0; i < this.length; i++) { + if (this[i].name === name) return this[i]; + } + return null; + }, + refresh: function() {}, + [Symbol.iterator]: function* () { + for (let i = 0; i < this.length; i++) { + yield this[i]; + } + } + }; + return plugins; + }, + configurable: true + }); + + // Use original system languages for consistency with other browser data + Object.defineProperty(navigator, 'languages', { + get: () => originalLanguages, + configurable: true + }); + + // Use original hardwareConcurrency, clamped to common range (4-16) to avoid extreme fingerprints + Object.defineProperty(navigator, 'hardwareConcurrency', { + get: () => Math.min(Math.max(originalHardwareConcurrency, 4), 16), + configurable: true + }); + + // Use original deviceMemory, clamped to common range (4-16) to avoid extreme fingerprints + Object.defineProperty(navigator, 'deviceMemory', { + get: () => Math.min(Math.max(originalDeviceMemory, 4), 16), + configurable: true + }); + + // Fix WebGL vendor/renderer for both WebGL and WebGL2 + const getParameter = WebGLRenderingContext.prototype.getParameter; + WebGLRenderingContext.prototype.getParameter = function(parameter) { + if (parameter === 37445) return 'Intel Inc.'; + if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100'; + return getParameter.call(this, parameter); + }; + + // Also patch WebGL2RenderingContext + if (typeof WebGL2RenderingContext !== 'undefined') { + const getParameter2 = WebGL2RenderingContext.prototype.getParameter; + WebGL2RenderingContext.prototype.getParameter = function(parameter) { + if (parameter === 37445) return 'Intel Inc.'; + if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100'; + return getParameter2.call(this, parameter); + }; + } + + // Override chrome runtime - real Chrome has window.chrome but runtime is undefined + if (!window.chrome) { + window.chrome = {}; + } + // In real Chrome, runtime exists but is undefined outside extensions + // Don't set it to an object, that's detectable + + // Hide automation variables + const automationVars = ['__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn', + '__driver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', 'domAutomation', 'domAutomationController']; + automationVars.forEach(v => { + Object.defineProperty(window, v, { + get: () => undefined, + set: () => {}, + configurable: true, + enumerable: false + }); + }); + + // Mouse event handler window.addEventListener('mousedown', (e) => { if (!(e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement)) { e.preventDefault(); diff --git a/src/components/AddWorker/ToolSelect.tsx b/src/components/AddWorker/ToolSelect.tsx index 24e45502..727aa6bf 100644 --- a/src/components/AddWorker/ToolSelect.tsx +++ b/src/components/AddWorker/ToolSelect.tsx @@ -630,7 +630,7 @@ const ToolSelect = forwardRef< key={item.id + item.key + (item.isLocal + "")} className="h-5 bg-button-tertiery-fill-default flex items-center gap-1 w-auto flex-shrink-0 px-xs" > - {item.name || item.mcp_name} + {item.name || item.mcp_name || item.key || `tool_${item.id}`}