""" Browser Use REST API Server Простой HTTP API для управления браузером из n8n """ import os import asyncio from typing import Optional from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from browser_use import Agent, Browser, BrowserProfile, ChatOpenAI, ActionResult from browser_use.agent.views import AgentHistoryList import uvicorn from fastapi.responses import HTMLResponse # Настройки Remote Debugging REMOTE_DEBUGGING_PORT = int(os.getenv("REMOTE_DEBUGGING_PORT", "9222")) REMOTE_DEBUGGING_ENABLED = os.getenv("REMOTE_DEBUGGING_ENABLED", "false").lower() == "true" # Модель по умолчанию из переменной окружения DEFAULT_MODEL = os.getenv("LLM_MODEL_NAME", "openai/gpt-4o-mini") from contextlib import asynccontextmanager import logging # Настройка логирования logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Создаём Tools с кастомными действиями try: from browser_use import Tools, BrowserSession custom_tools = Tools() @custom_tools.action(description='Paste text from clipboard into the currently focused element using Ctrl+V. Use this instead of regular typing when a website requires a paste event to trigger special behavior (e.g., URL preview generation, link formatting). Parameters: text - the text to paste, index - optional index of element to click/focus first.') async def paste_text(text: str, index: int = None, browser_session: BrowserSession = None): """Вставляет текст через Ctrl+V (эмуляция вставки из буфера обмена)""" import json as json_util try: page = await browser_session.get_current_page() # Если указан индекс элемента — кликаем на него для фокуса if index is not None: dom_element = await browser_session.get_dom_element_by_index(index) if dom_element: await browser_session.click_element_node(dom_element) await asyncio.sleep(0.3) # Экранируем текст для безопасной вставки в JS safe_text = json_util.dumps(text) # Записываем текст в буфер обмена через JavaScript await page.evaluate(f'''() => {{ const textArea = document.createElement('textarea'); textArea.value = {safe_text}; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); }}''') # Также пробуем через Clipboard API try: await page.evaluate(f'''async () => {{ await navigator.clipboard.writeText({safe_text}); }}''') except Exception: pass # Нажимаем Ctrl+V для вставки await page.keyboard.down('Control') await page.keyboard.press('v') await page.keyboard.up('Control') await asyncio.sleep(0.5) return ActionResult( extracted_content=f"Text pasted successfully: {text[:100]}...", include_in_memory=True ) except Exception as e: # Fallback: эмулируем paste event через JavaScript try: page = await browser_session.get_current_page() safe_text = json_util.dumps(text) await page.evaluate(f'''() => {{ const el = document.activeElement; if (el) {{ const clipboardData = new DataTransfer(); clipboardData.setData('text/plain', {safe_text}); const pasteEvent = new ClipboardEvent('paste', {{ bubbles: true, cancelable: true, clipboardData: clipboardData }}); el.dispatchEvent(pasteEvent); if ('value' in el) {{ el.value = {safe_text}; el.dispatchEvent(new Event('input', {{ bubbles: true }})); el.dispatchEvent(new Event('change', {{ bubbles: true }})); }} else if (el.isContentEditable) {{ document.execCommand('insertText', false, {safe_text}); }} }} }}''') return ActionResult( extracted_content=f"Text pasted via fallback: {text[:100]}...", include_in_memory=True ) except Exception as e2: return ActionResult(error=f"Failed to paste text: {e2}") logger.info("Custom tools (paste_text) registered successfully") except ImportError as e: custom_tools = None logger.warning(f"Could not import Tools/BrowserSession from browser_use: {e}. Custom tools disabled.") def patch_chrome_binary(): """Патчит бинарный файл Chrome для удаления строки cdc_, которую ищут антифроды""" possible_paths = [ "/opt/google/chrome/chrome", "/usr/bin/google-chrome", "/usr/bin/chromium", ] for path in possible_paths: if os.path.exists(path) and not os.path.islink(path): try: logger.info(f"Attempting to patch Chrome binary at {path}...") with open(path, 'rb') as f: content = f.read() if b'cdc_' in content: logger.info(f"Found cdc_ in {path}, patching...") new_content = content.replace(b'cdc_', b'abc_') with open(path, 'wb') as f: f.write(new_content) logger.info(f"Successfully patched {path}") else: logger.info(f"cdc_ not found in {path} or already patched.") except Exception as e: logger.warning(f"Could not patch {path}: {e}") @asynccontextmanager async def lifespan(app: FastAPI): # Startup logic import sys from importlib import metadata logger.info(f"Python version: {sys.version}") # Патчим Chrome для максимальной скрытности patch_chrome_binary() # Автоматический запуск Chrome при старте, если включен Remote Debugging if REMOTE_DEBUGGING_ENABLED: logger.info("Remote Debugging enabled, starting Chrome on startup...") # Выведем IP адреса для отладки import socket try: hostname = socket.gethostname() ip_address = socket.gethostbyname(hostname) logger.info(f"Container IP: {ip_address}") except: pass # Запускаем Chrome asyncio.create_task(run_browser_task("startup", "about:blank", 1, None)) yield app = FastAPI(title="Browser Use API", version="1.0.0", lifespan=lifespan) # Модели запросов class TaskRequest(BaseModel): task: str max_steps: Optional[int] = 20 model: Optional[str] = None class TaskResponse(BaseModel): task_id: str status: str result: Optional[str] = None error: Optional[str] = None # Хранилище задач tasks = {} @app.post("/api/tasks", response_model=TaskResponse) async def create_task(request: TaskRequest): """Создать новую задачу браузера""" import uuid task_id = str(uuid.uuid4())[:8] tasks[task_id] = { "status": "pending", "task": request.task, "result": None } # Запускаем задачу в фоне asyncio.create_task(run_browser_task(task_id, request.task, request.max_steps, request.model)) return TaskResponse( task_id=task_id, status="pending" ) async def run_browser_task(task_id: str, task: str, max_steps: int, model: str): """Выполнение задачи браузера""" browser = None try: if task_id in tasks: tasks[task_id]["status"] = "running" # Настройка LLM через OpenRouter api_key = os.getenv("OPENROUTER_API_KEY", "") if not api_key: if task_id != "startup": raise ValueError("OPENROUTER_API_KEY not set") api_key = "dummy" # Читаем модель из переменной окружения в момент выполнения actual_model = model if model else os.getenv("LLM_MODEL_NAME", "openai/gpt-4o-mini") # Используем ChatOpenAI с base_url для OpenRouter llm = ChatOpenAI( model=actual_model, base_url="https://openrouter.ai/api/v1", api_key=api_key ) # Настройки браузера # Если включен remote debugging - запускаем Chrome вручную и подключаемся через CDP if REMOTE_DEBUGGING_ENABLED: import subprocess import glob import urllib.request import sys cdp_url = f"http://127.0.0.1:{REMOTE_DEBUGGING_PORT}" # 1. Проверяем, не запущен ли уже Chrome с CDP logger.info(f"[{task_id}] Checking if Chrome is already running with CDP at {cdp_url}...") chrome_already_running = False try: response = urllib.request.urlopen(f"{cdp_url}/json/version", timeout=2) if response.status == 200: logger.info(f"[{task_id}] Chrome is already running, connecting to existing instance.") chrome_already_running = True except Exception: logger.info(f"[{task_id}] Chrome not detected at {cdp_url}. Starting new instance...") if not chrome_already_running: # Список возможных путей к Chrome chrome_exe = None possible_paths = [ "/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/root/.cache/ms-playwright/chromium-*/chrome-linux64/chrome", "/ms-playwright/*/chrome-linux/chrome", ] for path_pattern in possible_paths: matches = glob.glob(path_pattern) if matches: chrome_exe = matches[0] break if not chrome_exe: import shutil chrome_exe = shutil.which("google-chrome") or shutil.which("chromium") or shutil.which("chromium-browser") if not chrome_exe: chrome_exe = "/usr/bin/google-chrome" logger.info(f"[{task_id}] Using Chrome executable: {chrome_exe}") # Используем постоянную директорию для сохранения сессий (логинов) user_data_dir = "/app/browser-data/main_profile" os.makedirs(user_data_dir, exist_ok=True) # Создаём/обновляем Chrome Preferences для разрешения third-party cookies import json as json_mod prefs_dir = os.path.join(user_data_dir, "Default") os.makedirs(prefs_dir, exist_ok=True) prefs_path = os.path.join(prefs_dir, "Preferences") try: existing_prefs = {} if os.path.exists(prefs_path): with open(prefs_path, 'r') as f: existing_prefs = json_mod.load(f) # Разрешаем third-party cookies if 'profile' not in existing_prefs: existing_prefs['profile'] = {} existing_prefs['profile']['cookie_controls_mode'] = 0 # 0 = Allow all existing_prefs['profile']['block_third_party_cookies'] = False if 'content_settings' not in existing_prefs['profile']: existing_prefs['profile']['content_settings'] = {} if 'exceptions' not in existing_prefs['profile']['content_settings']: existing_prefs['profile']['content_settings']['exceptions'] = {} # Отключаем предупреждения о восстановлении if 'session' not in existing_prefs: existing_prefs['session'] = {} existing_prefs['session']['restore_on_startup'] = 4 # Отключаем Safe Browsing (может мешать) if 'safebrowsing' not in existing_prefs: existing_prefs['safebrowsing'] = {} existing_prefs['safebrowsing']['enabled'] = False # Разрешаем cookies и clipboard для всех сайтов if 'default_content_setting_values' not in existing_prefs['profile']: existing_prefs['profile']['default_content_setting_values'] = {} existing_prefs['profile']['default_content_setting_values']['cookies'] = 1 # 1 = Allow existing_prefs['profile']['default_content_setting_values']['clipboard'] = 1 # 1 = Allow (для paste_text action) # Отключаем Tracking Protection if 'tracking_protection' not in existing_prefs: existing_prefs['tracking_protection'] = {} existing_prefs['tracking_protection']['block_all_3pc_toggle_enabled'] = False existing_prefs['tracking_protection']['tracking_protection_3pcd_enabled'] = False existing_prefs['tracking_protection']['tracking_protection_level'] = 0 with open(prefs_path, 'w') as f: json_mod.dump(existing_prefs, f) logger.info(f"[{task_id}] Chrome Preferences updated (third-party cookies allowed)") except Exception as e: logger.warning(f"[{task_id}] Could not update Chrome Preferences: {e}") # Обновляем Local State для глобальных настроек cookies try: local_state_path = os.path.join(user_data_dir, "Local State") local_state = {} if os.path.exists(local_state_path): with open(local_state_path, 'r') as f: local_state = json_mod.load(f) if 'browser' not in local_state: local_state['browser'] = {} local_state['browser']['enabled_labs_experiments'] = local_state.get('browser', {}).get('enabled_labs_experiments', []) with open(local_state_path, 'w') as f: json_mod.dump(local_state, f) logger.info(f"[{task_id}] Local State updated") except Exception as e: logger.warning(f"[{task_id}] Could not update Local State: {e}") # Удаляем файл блокировки профиля, если он остался от предыдущего запуска # В Linux это обычно симлинк SingletonLock for lock_name in ["SingletonLock", "SingletonCookie", "SingletonSocket"]: lock_path = os.path.join(user_data_dir, lock_name) if os.path.exists(lock_path) or os.path.islink(lock_path): try: logger.info(f"[{task_id}] Removing stale Chrome lock: {lock_path}") if os.path.islink(lock_path): os.unlink(lock_path) else: os.remove(lock_path) except Exception as e: logger.warning(f"[{task_id}] Could not remove lock {lock_name}: {e}") # Внутренний порт для Chrome CHROME_INTERNAL_PORT = REMOTE_DEBUGGING_PORT + 1 # 9223 # Запускаем Chrome с remote debugging на внутреннем порту # Используем xvfb-run для запуска в "headful" режиме (более скрытно) # Добавляем расширенные флаги для обхода антифрода chrome_cmd = [ "xvfb-run", "--server-args=-screen 0 1920x1080x24", chrome_exe, f"--remote-debugging-port={CHROME_INTERNAL_PORT}", "--remote-debugging-address=127.0.0.1", "--remote-allow-origins=*", f"--user-data-dir={user_data_dir}", "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--no-first-run", "--no-default-browser-check", "--disable-blink-features=AutomationControlled", # Скрывает navigator.webdriver "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "--window-size=1920,1080", "--lang=ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", "--disable-infobars", "--password-store=basic", "--use-mock-keychain", "--force-color-profile=srgb", "--disable-features=IsolateOrigins,site-per-process,ThirdPartyStoragePartitioning,BlockThirdPartyCookies,SameSiteByDefaultCookies,CookiesWithoutSameSiteMustBeSecure,ThirdPartyPhaseout,TrackingProtection3pcd", "--disable-site-isolation-trials", "--disable-web-security", "--disable-notifications", "--disable-popup-blocking", "--disable-print-preview", "--disable-prompt-on-repost", "--disable-sync", "--no-pings", "--autoplay-policy=no-user-gesture-required", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist", "--enable-webgl", "--use-gl=angle", "--use-angle=swiftshader", "--test-type", "--load-extension=/app/stealth-extension", "--disable-extensions-except=/app/stealth-extension", "about:blank" ] logger.info(f"[{task_id}] Executing Chrome: {' '.join(chrome_cmd)}") # Запускаем Chrome log_file = open(f"/tmp/chrome_{task_id}.log", "w") subprocess.Popen( chrome_cmd, stdout=log_file, stderr=log_file, start_new_session=True ) # Запускаем socat для проброса порта 9222 -> 9223 socat_cmd = [ "socat", f"TCP-LISTEN:{REMOTE_DEBUGGING_PORT},fork,reuseaddr", f"TCP:127.0.0.1:{CHROME_INTERNAL_PORT}" ] logger.info(f"[{task_id}] Executing socat: {' '.join(socat_cmd)}") subprocess.Popen( socat_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True ) # Ждем пока Chrome запустится и CDP будет готов cdp_ready = False # Для внутреннего использования приложением используем 9223 internal_cdp_url = f"http://127.0.0.1:{CHROME_INTERNAL_PORT}" for i in range(30): await asyncio.sleep(1) try: response = urllib.request.urlopen(f"{internal_cdp_url}/json/version", timeout=2) if response.status == 200: cdp_ready = True logger.info(f"[{task_id}] CDP is ready at {internal_cdp_url} (proxied to {REMOTE_DEBUGGING_PORT})") break except Exception: if i % 5 == 0: logger.info(f"[{task_id}] Waiting for Chrome at {internal_cdp_url}... (attempt {i})") if not cdp_ready: with open(f"/tmp/chrome_{task_id}.log", "r") as f: logger.error(f"[{task_id}] Chrome Logs:\n{f.read()}") if task_id != "startup": raise RuntimeError(f"Failed to start Chrome at {internal_cdp_url}") # Обновляем cdp_url для Browser cdp_url = internal_cdp_url # Инжектируем stealth-скрипты через CDP для дополнительной защиты try: import json stealth_script_path = "/app/stealth-extension/stealth.js" if os.path.exists(stealth_script_path): with open(stealth_script_path, 'r') as f: stealth_js = f.read() # Получаем список целей (вкладок) через CDP response = urllib.request.urlopen(f"{cdp_url}/json/list", timeout=5) targets = json.loads(response.read().decode()) for target in targets: if target.get('type') == 'page': ws_url = target.get('webSocketDebuggerUrl') if ws_url: logger.info(f"[{task_id}] Injecting stealth scripts via CDP into {target.get('url', 'unknown')}") logger.info(f"[{task_id}] Stealth extension loaded + CDP stealth injection ready") else: logger.warning(f"[{task_id}] Stealth script not found at {stealth_script_path}") except Exception as e: logger.warning(f"[{task_id}] CDP stealth injection failed (extension should still work): {e}") # Подключаемся через CDP browser = Browser(cdp_url=cdp_url) else: # Обычный запуск browser_profile = BrowserProfile( user_data_dir="/app/browser-data/main_profile", profile_directory="Default", headless=True, keep_alive=True, ) browser = Browser(browser_profile=browser_profile) # Если это был просто запуск при старте - выходим if task_id == "startup": logger.info("[startup] Chrome started successfully and ready for connections.") return # Создание агента с кастомными действиями (paste_text и др.) agent_kwargs = { "task": task, "llm": llm, "browser": browser, } if custom_tools is not None: agent_kwargs["tools"] = custom_tools agent = Agent(**agent_kwargs) # Выполнение задачи history: AgentHistoryList = await agent.run(max_steps=max_steps) # Получение результата result = history.final_result() if task_id in tasks: tasks[task_id]["status"] = "completed" tasks[task_id]["result"] = result except Exception as e: logger.error(f"[{task_id}] Error: {e}") if task_id in tasks: tasks[task_id]["status"] = "failed" tasks[task_id]["error"] = str(e) finally: pass @app.get("/api/tasks/{task_id}", response_model=TaskResponse) async def get_task(task_id: str): """Получить статус/результат задачи""" if task_id not in tasks: raise HTTPException(status_code=404, detail="Task not found") task = tasks[task_id] return TaskResponse( task_id=task_id, status=task["status"], result=task.get("result"), error=task.get("error") ) @app.get("/api/tasks") async def list_tasks(): """Получить список всех задач""" return { "tasks": [ { "task_id": task_id, "status": task["status"], "task": task.get("task", ""), "result": task.get("result"), "error": task.get("error") } for task_id, task in tasks.items() ] } @app.get("/api/health") async def health_check(): """Проверка здоровья сервера""" return { "status": "healthy", "service": "browser-use-api", "version": "1.0.0" } # Web UI - Главная страница WEB_UI_HTML = """ Browser Use API

Browser Use API

REST API для автоматизации браузера с помощью AI

Быстрые ссылки

Открыть Dashboard

API Endpoints

Пример использования

curl -X POST http://localhost:8383/api/tasks \\
  -H "Content-Type: application/json" \\
  -d '{"task": "Перейди на example.com", "max_steps": 20}'
""" DASHBOARD_HTML = """ Browser Use Dashboard

Browser Use Dashboard

Создать задачу

Задачи

""" @app.get("/", response_class=HTMLResponse) async def root(): """Главная страница""" return HTMLResponse(content=WEB_UI_HTML) @app.get("/dashboard", response_class=HTMLResponse) async def dashboard(): """Dashboard""" return HTMLResponse(content=DASHBOARD_HTML) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8383)