Files
2026-03-10 20:45:53 +03:00

1321 lines
62 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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")
# ===========================================
# Настройки прокси
# ===========================================
PROXY_URL = os.getenv("PROXY_URL", "")
CLOUD_PROXY_COUNTRY = os.getenv("CLOUD_PROXY_COUNTRY", "")
# ===========================================
# Настройки stealth
# ===========================================
# ВНИМАНИЕ: headless=false всегда для поддержки reCAPTCHA
BROWSER_HEADLESS = False # Всегда false для поддержки reCAPTCHA
USE_XVFB = os.getenv("USE_XVFB", "true").lower() == "true"
VIEWPORT_SIZE = os.getenv("VIEWPORT_SIZE", "") # Формат: WIDTHxHEIGHT, напр. 1920x1080
# ===========================================
# Настройки решения капчи (CapSolver)
# ===========================================
CAPSOLVER_API_KEY = os.getenv("CAPSOLVER_API_KEY", "")
AUTO_SOLVE_CAPTCHA = os.getenv("AUTO_SOLVE_CAPTCHA", "false").lower() == "true"
# ===========================================
# Настройки обхода reCAPTCHA_
# ===========================================
RECAPTCHA_BYPASS = os.getenv("RECAPTCHA_BYPASS", "false").lower() == "true"
from contextlib import asynccontextmanager
import logging
# Настройка логирования
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Логирование настроек браузера
logger.info(f"=== Browser Configuration ===")
logger.info(f"BROWSER_HEADLESS: {BROWSER_HEADLESS}")
logger.info(f"USE_XVFB: {USE_XVFB}")
logger.info(f"REMOTE_DEBUGGING_ENABLED: {REMOTE_DEBUGGING_ENABLED}")
logger.info(f"RECAPTCHA_BYPASS: {RECAPTCHA_BYPASS}")
logger.info(f"AUTO_SOLVE_CAPTCHA: {AUTO_SOLVE_CAPTCHA}")
logger.info(f"CAPSOLVER_API_KEY: {'set' if CAPSOLVER_API_KEY else 'not set'}")
logger.info(f"PROXY_URL: {PROXY_URL if PROXY_URL else 'not set'}")
logger.info(f"===========================")
# Настраиваем CapSolver расширение с API ключом
if CAPSOLVER_API_KEY:
capsolver_config_path = "/app/capsolver-extension/assets/config.js"
if os.path.exists(capsolver_config_path):
try:
with open(capsolver_config_path, 'r') as f:
config_content = f.read()
# Заменяем apiKey на реальный
import re
config_content = re.sub(r'apiKey:\s*"[^"]*"', f'apiKey: "{CAPSOLVER_API_KEY}"', config_content)
with open(capsolver_config_path, 'w') as f:
f.write(config_content)
logger.info("CapSolver API key configured in extension")
except Exception as e:
logger.warning(f"Failed to configure CapSolver API key: {e}")
else:
logger.warning(f"CapSolver config not found at {capsolver_config_path}")
# Создаём 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}")
@custom_tools.action(description='Solve CAPTCHA using CapSolver API. Use this when you encounter reCAPTCHA, hCaptcha, or Cloudflare Turnstile. The agent will automatically detect and solve the captcha.')
async def solve_captcha(browser_session: BrowserSession = None):
"""
Решает капчу (reCAPTCHA/hCaptcha/Turnstile) с помощью CapSolver API.
Этот action автоматически:
1. Обнаруживает тип капчи на странице
2. Отправляет задачу в CapSolver
3. Ждёт решения
4. Вставляет ответ в форму
"""
if not CAPSOLVER_API_KEY:
return ActionResult(
error="CapSolver API key not configured. Set CAPSOLVER_API_KEY environment variable.",
include_in_memory=True
)
try:
page = await browser_session.get_current_page()
# Ищем sitekey для разных типов капчи
sitekey = None
captcha_type = None
# reCAPTCHA v2
recaptcha_element = await page.query_selector('[data-sitekey]')
if recaptcha_element:
sitekey = await recaptcha_element.get_attribute('data-sitekey')
if sitekey:
# Проверяем reCAPTCHA vs hCaptcha
iframe = await page.query_selector('iframe[src*="google.com/recaptcha"]')
if iframe:
captcha_type = "RecaptchaV2Task"
# hCaptcha
if not sitekey:
hcaptcha_element = await page.query_selector('[data-sitekey]')
if hcaptcha_element:
sitekey = await hcaptcha_element.get_attribute('data-sitekey')
hcaptcha_frame = await page.query_selector('iframe[src*="hcaptcha.com"]')
if hcaptcha_frame and sitekey:
captcha_type = "HCaptchaTask"
# Cloudflare Turnstile
if not sitekey:
turnstile_element = await page.query_selector('[data-sitekey]')
if turnstile_element:
sitekey = await turnstile_element.get_attribute('data-sitekey')
turnstile_frame = await page.query_selector('iframe[src*="turnstile"]')
if turnstile_frame and sitekey:
captcha_type = "AntiTurnstileTaskProxyLess"
if not sitekey or not captcha_type:
return ActionResult(
extracted_content="No captcha found on current page",
include_in_memory=True
)
page_url = page.url
logger.info(f"Found {captcha_type} with sitekey: {sitekey}")
# Отправляем задачу в CapSolver
import httpx
async with httpx.AsyncClient(timeout=60.0) as client:
# Создаём задачу
task_create_response = await client.post(
"https://api.capsolver.com/createTask",
json={
"clientKey": CAPSOLVER_API_KEY,
"task": {
"type": captcha_type,
"websiteURL": page_url,
"websiteKey": sitekey,
}
}
)
task_data = task_create_response.json()
if task_data.get("errorCode"):
logger.error(f"CapSolver createTask error: {task_data}")
return ActionResult(
error=f"CapSolver error: {task_data.get('errorDescription', 'Unknown error')}",
include_in_memory=True
)
task_id = task_data.get("taskId")
logger.info(f"CapSolver task created: {task_id}")
# Ждём решения (максимум 60 секунд)
solution = None
for attempt in range(30):
await asyncio.sleep(2)
result_response = await client.post(
"https://api.capsolver.com/getTaskResult",
json={
"clientKey": CAPSOLVER_API_KEY,
"taskId": task_id
}
)
result_data = result_response.json()
if result_data.get("status") == "ready":
solution = result_data.get("solution", {})
break
elif result_data.get("errorCode"):
logger.error(f"CapSolver error: {result_data}")
break
if not solution:
return ActionResult(
error="CapSolver timeout - captcha not solved",
include_in_memory=True
)
# Вставляем решение
if captcha_type == "RecaptchaV2Task":
g_recaptcha_response = solution.get("gRecaptchaResponse")
if g_recaptcha_response:
await page.evaluate(f'''() => {{
const textarea = document.getElementById('g-recaptcha-response');
if (textarea) {{
textarea.value = '{g_recaptcha_response}';
textarea.style.display = 'block';
}}
}}''')
logger.info("reCAPTCHA solved successfully!")
return ActionResult(
extracted_content="reCAPTCHA solved successfully!",
include_in_memory=True
)
elif captcha_type == "HCaptchaTask":
h_captcha_response = solution.get("hCaptchaResponse")
if h_captcha_response:
await page.evaluate(f'''() => {{
const textarea = document.querySelector('[name="h-captcha-response"]');
if (textarea) {{
textarea.value = '{h_captcha_response}';
textarea.style.display = 'block';
}}
}}''')
logger.info("hCaptcha solved successfully!")
return ActionResult(
extracted_content="hCaptcha solved successfully!",
include_in_memory=True
)
elif captcha_type == "AntiTurnstileTaskProxyLess":
token = solution.get("token")
if token:
await page.evaluate(f'''(token) => {{
const textarea = document.querySelector('[name="cf-turnstile-response"]');
if (textarea) {{
textarea.value = token;
textarea.style.display = 'block';
}}
// Also try to submit to Turnstile directly
const turnstile = window.__cf_challenge_js;
if (turnstile) {{
try {{
turnstile.submit(token);
}} catch(e) {{}}
}}
}}''', token)
logger.info("Turnstile solved successfully!")
return ActionResult(
extracted_content="Turnstile solved successfully!",
include_in_memory=True
)
return ActionResult(
error="Failed to apply captcha solution",
include_in_memory=True
)
except Exception as e:
logger.error(f"Error solving captcha: {e}")
return ActionResult(
error=f"Error solving captcha: {str(e)}",
include_in_memory=True
)
logger.info("Custom tools (paste_text, solve_captcha) 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}")
async def solve_captcha_with_capsolver(page, capsolver_api_key: str) -> bool:
"""
Решает капчу (reCAPTCHA/hCaptcha/Turnstile) с помощью CapSolver API.
Args:
page: Playwright page object
capsolver_api_key: API ключ CapSolver
Returns:
True если капча решена, False иначе
"""
import httpx
if not capsolver_api_key:
logger.warning("CapSolver API key not provided")
return False
try:
# Ищем sitekey
# reCAPTCHA v2
recaptcha_element = await page.query_selector('*[name="recaptcha-sitekey"]')
if not recaptcha_element:
recaptcha_element = await page.query_selector('[data-sitekey]')
sitekey = None
captcha_type = None
if recaptcha_element:
sitekey = await recaptcha_element.get_attribute('data-sitekey')
if sitekey:
captcha_type = "RecaptchaV2Task"
# hCaptcha
if not sitekey:
hcaptcha_element = await page.query_selector('[data-sitekey]')
if hcaptcha_element:
sitekey = await hcaptcha_element.get_attribute('data-sitekey')
if sitekey:
# Проверяем что это hCaptcha
hcaptcha_frame = await page.query_selector('iframe[src*="hcaptcha.com"]')
if hcaptcha_frame:
captcha_type = "HCaptchaTask"
# Cloudflare Turnstile
if not sitekey:
turnstile_element = await page.query_selector('[data-sitekey]')
if turnstile_element:
sitekey = await turnstile_element.get_attribute('data-sitekey')
if sitekey:
turnstile_frame = await page.query_selector('iframe[src*="turnstile"]')
if turnstile_frame:
captcha_type = "AntiTurnstileTaskProxyLess"
if not sitekey or not captcha_type:
logger.info("No captcha found on page")
return False
page_url = page.url
logger.info(f"Found {captcha_type} with sitekey: {sitekey}")
# Отправляем задачу в CapSolver
async with httpx.AsyncClient(timeout=60.0) as client:
# Создаём задачу
task_create_response = await client.post(
"https://api.capsolver.com/createTask",
json={
"clientKey": capsolver_api_key,
"task": {
"type": captcha_type,
"websiteURL": page_url,
"websiteKey": sitekey,
}
}
)
task_data = task_create_response.json()
if task_data.get("errorCode"):
logger.error(f"CapSolver createTask error: {task_data}")
return False
task_id = task_data.get("taskId")
logger.info(f"CapSolver task created: {task_id}")
# Ждём решения
for attempt in range(30):
await asyncio.sleep(2)
result_response = await client.post(
"https://api.capsolver.com/getTaskResult",
json={
"clientKey": capsolver_api_key,
"taskId": task_id
}
)
result_data = result_response.json()
if result_data.get("status") == "ready":
solution = result_data.get("solution", {})
if captcha_type == "RecaptchaV2Task":
# Вставляем решение в textarea
g_recaptcha_response = solution.get("gRecaptchaResponse")
if g_recaptcha_response:
# Ищем textarea для ответа
textarea = await page.query_selector('#g-recaptcha-response')
if textarea:
await textarea.fill(g_recaptcha_response)
logger.info("reCAPTCHA solved successfully!")
return True
elif captcha_type == "HCaptchaTask":
h_captcha_response = solution.get("hCaptchaResponse")
if h_captcha_response:
# Вставляем в h-captcha-response
textarea = await page.query_selector('[name="h-captcha-response"]')
if textarea:
await textarea.fill(h_captcha_response)
logger.info("hCaptcha solved successfully!")
return True
elif captcha_type == "AntiTurnstileTaskProxyLess":
token = solution.get("token")
if token:
# Вставляем токен
textarea = await page.query_selector('[name="cf-turnstile-response"]')
if textarea:
await textarea.fill(token)
logger.info("Turnstile solved successfully!")
return True
return True
elif result_data.get("errorCode"):
logger.error(f"CapSolver error: {result_data}")
return False
logger.warning("CapSolver timeout - captcha not solved")
return False
except Exception as e:
logger.error(f"Error solving captcha: {e}")
return False
@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
# ===========================================
# Настройка viewport (рандомизация)
# ===========================================
if VIEWPORT_SIZE and 'x' in VIEWPORT_SIZE:
try:
width, height = map(int, VIEWPORT_SIZE.split('x'))
except:
width, height = 1920, 1080
else:
# Рандомизация viewport для большей уникальности
import random
# Популярные разрешения
viewports = [
(1920, 1080),
(1366, 768),
(1536, 864),
(1440, 900),
(1280, 720),
(2560, 1440),
(3840, 2160),
]
width, height = random.choice(viewports)
window_size = f"{width},{height}"
# Рандомизация user-agent
import random
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
]
user_agent = random.choice(user_agents)
# ===========================================
# Настройка прокси
# ===========================================
proxy_args = []
if PROXY_URL:
# Добавляем прокси в аргументы Chrome
proxy_args = [f"--proxy-server={PROXY_URL}"]
logger.info(f"[{task_id}] Using proxy: {PROXY_URL}")
# Запускаем Chrome с remote debugging на внутреннем порту
# Используем xvfb-run для запуска в "headful" режиме (более скрытно)
# Добавляем расширенные флаги для обхода антифрода
chrome_cmd = []
# xvfb если включено
if USE_XVFB:
chrome_cmd.extend([
"xvfb-run",
f"--server-args=-screen 0 {width}x{height}x24",
])
# Всегда добавляем --no-sandbox
chrome_cmd.extend([
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",
f"--user-agent={user_agent}",
f"--window-size={window_size}",
"--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=desktop",
"--ignore-gpu-blocklist",
"--enable-accelerated-2d-canvas",
"--enable-gpu-rasterization",
"--enable-zero-copy",
"--ignore-webgl-blacklist",
# Добавляем экспериментальные флаги для WebGL
"--enable-experimental-webgl",
"--enable-webgl2-compute-context",
])
# Убираем загрузку расширения CapSolver - используем API вместо него
# Расширение конфликтует с Cloudflare Turnstile
# Решение капчи происходит через функцию solve_captcha_with_capsolver
if AUTO_SOLVE_CAPTCHA and CAPSOLVER_API_KEY:
logger.info(f"[{task_id}] CapSolver will solve CAPTCHA via API (not extension)")
# Добавляем специальные флаги для обхода reCAPTCHA если включено
if RECAPTCHA_BYPASS:
chrome_cmd.extend([
# Флаги для обхода reCAPTCHA
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-sandbox",
"--disable-setuid-sandbox",
# Отключаем некоторые функции, которые могут мешать reCAPTCHA
"--disable-features=TranslateUI",
"--disable-ipc-flooding-protection",
])
logger.info(f"[{task_id}] Added reCAPTCHA bypass flags")
else:
# Всегда добавляем --no-sandbox для работы в контейнере
# и --disable-blink-features=AutomationControlled для обхода Cloudflare Turnstile
chrome_cmd.extend([
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
])
logger.info(f"[{task_id}] Added minimal stealth flags for Cloudflare Turnstile")
# Добавляем прокси если есть
chrome_cmd.extend(proxy_args)
chrome_cmd.append("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
# Если Chrome уже был запущен - используем external port (через socat)
# Если Chrome был запущен нами - используем internal port
if chrome_already_running:
cdp_url = f"http://127.0.0.1:{REMOTE_DEBUGGING_PORT}"
logger.info(f"[{task_id}] Using existing Chrome on external port {REMOTE_DEBUGGING_PORT}")
else:
cdp_url = internal_cdp_url
logger.info(f"[{task_id}] Using new Chrome on internal port {CHROME_INTERNAL_PORT}")
# Инжектируем 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:
# Обычный запуск
# Добавляем флаги для работы в контейнере
extra_chromium_args = [
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--enable-webgl",
"--use-gl=desktop",
"--ignore-gpu-blocklist",
"--enable-accelerated-2d-canvas",
"--enable-gpu-rasterization",
"--enable-zero-copy",
"--ignore-webgl-blacklist",
"--enable-experimental-webgl",
]
# Используем Google Chrome если доступен
chrome_path = os.getenv("CHROME_PATH", "/usr/bin/google-chrome")
if os.path.exists(chrome_path):
logger.info(f"Using Google Chrome at {chrome_path}")
else:
logger.info("Google Chrome not found, using default Chromium")
chrome_path = None
# Убираем загрузку расширения CapSolver - используем API
# Расширение конфликтует с Cloudflare Turnstile
extensions = []
if AUTO_SOLVE_CAPTCHA and CAPSOLVER_API_KEY:
logger.info("CapSolver will solve CAPTCHA via API (not extension)")
browser_profile = BrowserProfile(
user_data_dir="/app/browser-data/main_profile",
profile_directory="Default",
headless=False, # Всегда false для поддержки reCAPTCHA
keep_alive=True,
extra_chromium_args=extra_chromium_args,
executable_path=chrome_path,
extensions=extensions,
)
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
# Добавляем информацию о капче в промпт, если включено авто-решение
if AUTO_SOLVE_CAPTCHA and CAPSOLVER_API_KEY:
captcha_hint = "\n\nIMPORTANT: If you encounter a CAPTCHA (reCAPTCHA, hCaptcha, or Cloudflare Turnstile), use the solve_captcha tool to automatically solve it. The tool requires CAPSOLVER_API_KEY to be configured."
agent_kwargs["task"] = task + captcha_hint
agent = Agent(**agent_kwargs)
# Автоматическое решение капчи перед запуском, если включено
if AUTO_SOLVE_CAPTCHA and CAPSOLVER_API_KEY:
logger.info(f"[{task_id}] AUTO_SOLVE_CAPTCHA enabled, checking for captcha...")
try:
# Получаем страницу через browser
page = None
try:
# Пробуем получить страницу через CDP
import urllib.request
import json
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' and target.get('url', '') not in ['about:blank', 'chrome://newtab/']:
# Найдена страница с URL
logger.info(f"[{task_id}] Found page: {target.get('url')}")
# Подключаемся к странице через Playwright
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(cdp_url)
context = browser.contexts[0] if browser.contexts else None
if context:
pages = context.pages
if pages:
page = pages[0]
break
except Exception as e_page:
logger.warning(f"[{task_id}] Could not get page: {e_page}")
if page:
logger.info(f"[{task_id}] Page obtained, checking for captcha elements...")
# Проверяем наличие капчи
try:
captcha_found = await page.query_selector('[data-sitekey]')
turnstile_found = await page.query_selector('iframe[src*="turnstile"]')
logger.info(f"[{task_id}] Captcha found: {captcha_found}, Turnstile found: {turnstile_found}")
if captcha_found or turnstile_found:
logger.info(f"[{task_id}] CAPTCHA detected, attempting to solve automatically...")
# Вызываем функцию решения капчи
solved = await solve_captcha_with_capsolver(page, CAPSOLVER_API_KEY)
if solved:
logger.info(f"[{task_id}] CAPTCHA solved successfully!")
else:
logger.warning(f"[{task_id}] Failed to solve CAPTCHA automatically")
else:
logger.info(f"[{task_id}] No captcha detected on page")
except Exception as e_check:
logger.warning(f"[{task_id}] Error checking captcha: {e_check}")
else:
logger.warning(f"[{task_id}] Could not get page from browser")
except Exception as e:
logger.error(f"[{task_id}] Auto-captcha solve failed: {e}")
else:
logger.info(f"[{task_id}] AUTO_SOLVE_CAPTCHA={AUTO_SOLVE_CAPTCHA}, CAPSOLVER_API_KEY={'set' if CAPSOLVER_API_KEY else 'not set'}")
# Выполнение задачи
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 = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browser Use API</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
h1 { color: #333; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px 0; }
.btn { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.btn:hover { background: #0056b3; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
</style>
</head>
<body>
<h1>Browser Use API</h1>
<p>REST API для автоматизации браузера с помощью AI</p>
<div class="card">
<h2>Быстрые ссылки</h2>
<p><a href="/dashboard" class="btn">Открыть Dashboard</a></p>
</div>
<div class="card">
<h2>API Endpoints</h2>
<ul>
<li><code>POST /api/tasks</code> - Создать задачу</li>
<li><code>GET /api/tasks/{task_id}</code> - Получить результат</li>
<li><code>GET /api/health</code> - Проверка здоровья</li>
</ul>
</div>
<div class="card">
<h2>Пример использования</h2>
<pre>curl -X POST http://localhost:8383/api/tasks \\
-H "Content-Type: application/json" \\
-d '{"task": "Перейди на example.com", "max_steps": 20}'</pre>
</div>
</body>
</html>
"""
DASHBOARD_HTML = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browser Use Dashboard</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.task-form { display: flex; gap: 10px; flex-wrap: wrap; }
input, select { padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
input[type="text"] { flex: 1; min-width: 200px; }
input[type="number"] { width: 100px; }
button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
button:hover { background: #0056b3; }
.task-list { list-style: none; padding: 0; }
.task-item { border: 1px solid #eee; padding: 15px; margin: 10px 0; border-radius: 4px; }
.status-pending { border-left: 4px solid #ffc107; }
.status-running { border-left: 4px solid #17a2b8; }
.status-completed { border-left: 4px solid #28a745; }
.status-failed { border-left: 4px solid #dc3545; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 12px; background: #eee; }
.result { background: #f8f9fa; padding: 10px; border-radius: 4px; margin-top: 10px; white-space: pre-wrap; }
.nav { margin-bottom: 20px; }
.nav a { color: #007bff; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<div class="nav"><a href="/">&larr; На главную</a></div>
<h1>Browser Use Dashboard</h1>
<div class="card">
<h2>Создать задачу</h2>
<form class="task-form" id="taskForm">
<input type="text" id="taskInput" placeholder="Описание задачи (например: Перейди на example.com)" required>
<input type="number" id="maxSteps" value="20" min="1" max="100" placeholder="Шаги">
<select id="modelSelect">
<option value="arcee-ai/trinity-large-preview:free">arcee-ai/trinity-large-preview:free</option>
<option value="openai/gpt-4o-mini">openai/gpt-4o-mini</option>
<option value="google/gemini-2.0-flash-001">google/gemini-2.0-flash-001</option>
</select>
<button type="submit">Запустить</button>
</form>
</div>
<div class="card">
<h2>Задачи</h2>
<button onclick="refreshTasks()">Обновить</button>
<ul class="task-list" id="taskList"></ul>
</div>
</div>
<script>
let pollInterval;
document.getElementById('taskForm').addEventListener('submit', async (e) => {
e.preventDefault();
const task = document.getElementById('taskInput').value;
const max_steps = parseInt(document.getElementById('maxSteps').value);
const model = document.getElementById('modelSelect').value;
const response = await fetch('/api/tasks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({task, max_steps, model})
});
const data = await response.json();
alert('Задача создана! ID: ' + data.task_id);
refreshTasks();
});
async function refreshTasks() {
try {
const res = await fetch('/api/tasks');
const data = await res.json();
const list = document.getElementById('taskList');
list.innerHTML = '';
const taskList = data.tasks || [];
if (taskList.length === 0) {
list.innerHTML = '<li>Нет задач. Создайте первую задачу!</li>';
return;
}
for (const task of taskList) {
const li = document.createElement('li');
li.className = 'task-item status-' + task.status;
li.innerHTML = `
<strong>Task ID:</strong> ${task.task_id}<br>
<strong>Задача:</strong> ${task.task || 'N/A'}<br>
<span class="badge">${task.status}</span><br>
<strong>Результат:</strong>
<div class="result">${task.result || task.error || 'Ожидание...'}</div>
`;
list.appendChild(li);
}
} catch (e) {
console.error(e);
}
}
refreshTasks();
setInterval(refreshTasks, 5000);
</script>
</body>
</html>
"""
@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)