1227 lines
56 KiB
Python
1227 lines
56 KiB
Python
"""
|
||
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"===========================")
|
||
|
||
# Создаём 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 = "TurnstileTask"
|
||
|
||
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 == "TurnstileTask":
|
||
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 = "TurnstileTask"
|
||
|
||
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 == "TurnstileTask":
|
||
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",
|
||
])
|
||
|
||
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=angle",
|
||
"--use-angle=swiftshader",
|
||
"--test-type",
|
||
# WebGPU флаги для Cloudflare Turnstile
|
||
"--enable-webgpu",
|
||
"--enable-webgpu-developer-features",
|
||
"--use-angle=swiftshader",
|
||
# Отключено: stealth-расширение вызывает ошибки и ломает Cloudflare Turnstile
|
||
# "--load-extension=/app/stealth-extension",
|
||
# "--disable-extensions-except=/app/stealth-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")
|
||
|
||
# Добавляем прокси если есть
|
||
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 для дополнительной защиты
|
||
# Отключено: stealth-скрипты вызывают ошибки и ломают Cloudflare Turnstile
|
||
# 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=False, # Всегда false для поддержки reCAPTCHA
|
||
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)
|
||
|
||
# Автоматическое решение капчи перед запуском, если включено
|
||
if AUTO_SOLVE_CAPTCHA and CAPSOLVER_API_KEY:
|
||
logger.info(f"[{task_id}] AUTO_SOLVE_CAPTCHA enabled, checking for captcha...")
|
||
try:
|
||
# Получаем страницу через browser_use
|
||
from browser_use import BrowserSession
|
||
browser_session = BrowserSession(browser=browser)
|
||
page = await browser_session.get_current_page()
|
||
if page:
|
||
logger.info(f"[{task_id}] Page obtained, checking for captcha elements...")
|
||
# Проверяем наличие капчи
|
||
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")
|
||
else:
|
||
logger.warning(f"[{task_id}] Could not get page")
|
||
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="/">← На главную</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)
|