Files
browser-use/app.py
T
2026-03-06 03:21:20 +03:00

552 lines
23 KiB
Python
Raw 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
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__)
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)
# Удаляем файл блокировки профиля, если он остался от предыдущего запуска
# В 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",
"--disable-notifications",
"--disable-popup-blocking",
"--disable-print-preview",
"--disable-prompt-on-repost",
"--disable-sync",
"--no-pings",
"--mute-audio",
"--enable-unsafe-swiftshader", # Исправляет ошибку WebGL в Docker
"--ignore-gpu-blocklist",
"--enable-webgl",
"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
# Подключаемся через 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
# Создание агента
agent = Agent(
task=task,
llm=llm,
browser=browser,
)
# Выполнение задачи
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)