When REMOTE_DEBUGGING_ENABLED is true, launch Chrome manually via subprocess and connect via CDP instead of using BrowserProfile. This improves container compatibility and provides better control over browser lifecycle.
361 lines
13 KiB
Python
361 lines
13 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
|
||
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")
|
||
|
||
app = FastAPI(title="Browser Use API", version="1.0.0")
|
||
|
||
# Модели запросов
|
||
class TaskRequest(BaseModel):
|
||
task: str
|
||
max_steps: Optional[int] = 20
|
||
model: Optional[str] = Field(default_factory=lambda: os.getenv("LLM_MODEL_NAME", "openai/gpt-4o-mini"))
|
||
|
||
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:
|
||
tasks[task_id]["status"] = "running"
|
||
|
||
# Настройка LLM через OpenRouter
|
||
api_key = os.getenv("OPENROUTER_API_KEY", "")
|
||
if not api_key:
|
||
raise ValueError("OPENROUTER_API_KEY not set")
|
||
|
||
# Используем ChatOpenAI с base_url для OpenRouter
|
||
llm = ChatOpenAI(
|
||
model=model,
|
||
base_url="https://openrouter.ai/api/v1",
|
||
api_key=api_key
|
||
)
|
||
|
||
# Настройки браузера
|
||
# Если включен remote debugging - запускаем Chrome вручную и подключаемся через CDP
|
||
if REMOTE_DEBUGGING_ENABLED:
|
||
import subprocess
|
||
import tempfile
|
||
|
||
# Создаем временную директорию для профиля
|
||
user_data_dir = "/app/browser-data"
|
||
|
||
# Запускаем Chrome с remote debugging
|
||
chrome_cmd = [
|
||
"chromium",
|
||
f"--remote-debugging-port={REMOTE_DEBUGGING_PORT}",
|
||
f"--user-data-dir={user_data_dir}",
|
||
"--no-first-run",
|
||
"--no-default-browser-check",
|
||
"--disable-extensions",
|
||
"--disable-popup-blocking",
|
||
"--disable-gpu",
|
||
"--no-sandbox",
|
||
"about:blank"
|
||
]
|
||
|
||
# Запускаем процесс
|
||
process = subprocess.Popen(
|
||
chrome_cmd,
|
||
stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.DEVNULL
|
||
)
|
||
|
||
# Даем браузеру время запуститься
|
||
await asyncio.sleep(3)
|
||
|
||
# Подключаемся через CDP
|
||
cdp_url = f"http://localhost:{REMOTE_DEBUGGING_PORT}"
|
||
browser = Browser(cdp_url=cdp_url)
|
||
else:
|
||
# Обычный запуск
|
||
browser_profile = BrowserProfile(
|
||
user_data_dir="/app/browser-data",
|
||
profile_directory="Default",
|
||
headless=True,
|
||
keep_alive=True,
|
||
)
|
||
browser = Browser(browser_profile=browser_profile)
|
||
|
||
# Создание агента
|
||
agent = Agent(
|
||
task=task,
|
||
llm=llm,
|
||
browser=browser,
|
||
)
|
||
|
||
# Выполнение задачи
|
||
history: AgentHistoryList = await agent.run(max_steps=max_steps)
|
||
|
||
# Получение результата
|
||
result = history.final_result()
|
||
|
||
tasks[task_id]["status"] = "completed"
|
||
tasks[task_id]["result"] = result
|
||
|
||
except Exception as e:
|
||
tasks[task_id]["status"] = "failed"
|
||
tasks[task_id]["error"] = str(e)
|
||
finally:
|
||
# Закрытие браузера (правильный способ)
|
||
if browser:
|
||
try:
|
||
# browser_use сам управляет закрытием
|
||
pass
|
||
except:
|
||
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)
|