🚀 Auto-deploy: BotVPS atualizado em 02/05/2026 15:37:40

This commit is contained in:
2026-05-02 15:37:40 +00:00
parent 1c1fac3735
commit 912763b3f1
613 changed files with 169969 additions and 60 deletions

238
browser_cloud.py Normal file
View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
Browser Cloud Service — Playwright-based browser automation server
Exposes a simple REST API for spawning/controlling browser sessions.
"""
import asyncio
import uuid
import json
import base64
from typing import Optional
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from playwright.async_api import async_playwright, TimeoutError as PWTimeoutError, BrowserContext, Page, Playwright
# Max concurrent browsers
MAX_BROWSERS = 5
BROWSER_TIMEOUT = 30000 # ms
# Active browser contexts
active_contexts: dict[str, BrowserContext] = {}
active_pages: dict[str, Page] = {}
active_playwrights: dict[str, Playwright] = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
print("[BrowserCloud] Iniciando Playwright service...")
yield
# Shutdown: close all browsers
print(f"[BrowserCloud] Fechando {len(active_contexts)} contextos...")
for ctx_id in list(active_contexts.keys()):
try:
await active_contexts[ctx_id].close()
except Exception:
pass
active_contexts.clear()
active_pages.clear()
print("[BrowserCloud] Done.")
app = FastAPI(title="BrowserCloud", lifespan=lifespan)
# ─── Models ──────────────────────────────────────────────────────────────────
class BrowserOptions(BaseModel):
headless: bool = True
viewport_width: int = 1280
viewport_height: int = 720
user_agent: Optional[str] = None
class NavigateOptions(BaseModel):
url: str
wait_until: str = "load" # load|domcontentloaded|networkidle
class ScreenshotOptions(BaseModel):
full_page: bool = False
format: str = "png" # png|jpg
# ─── Helpers ───────────────────────────────────────────────────────────────────
def _get_page(ctx_id: str) -> Page:
if ctx_id not in active_pages:
raise HTTPException(404, f"Sessão {ctx_id} não encontrada")
return active_pages[ctx_id]
async def _cleanup(ctx_id: str):
"""Close a session and remove from active dicts."""
if ctx_id in active_pages:
try:
await active_pages[ctx_id].close()
except Exception:
pass
del active_pages[ctx_id]
if ctx_id in active_contexts:
try:
await active_contexts[ctx_id].close()
except Exception:
pass
del active_contexts[ctx_id]
if ctx_id in active_playwrights:
try:
await active_playwrights[ctx_id].stop()
except Exception:
pass
del active_playwrights[ctx_id]
# ─── Endpoints ─────────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return {
"status": "ok",
"active_sessions": len(active_contexts),
"max_sessions": MAX_BROWSERS
}
@app.post("/session")
async def create_session(opts: BrowserOptions = BrowserOptions()):
"""Cria uma nova sessão de browser (contexto + página)."""
if len(active_contexts) >= MAX_BROWSERS:
raise HTTPException(503, f"Limite de {MAX_BROWSERS} sessões atingido")
ctx_id = uuid.uuid4().hex[:12]
try:
pw = await async_playwright().start()
browser = await pw.chromium.launch(headless=opts.headless)
context = await browser.new_context(
viewport={"width": opts.viewport_width, "height": opts.viewport_height},
user_agent=opts.user_agent or "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
accept_downloads=True,
)
page = await context.new_page()
except Exception as e:
await pw.stop()
raise HTTPException(500, f"Falha ao iniciar browser: {e}")
active_contexts[ctx_id] = context
active_pages[ctx_id] = page
active_playwrights[ctx_id] = pw
# Auto-cleanup on context close
context.on("close", lambda: asyncio.create_task(_cleanup(ctx_id)))
return {"session_id": ctx_id, "url": page.url}
@app.get("/session/{ctx_id}")
async def get_session(ctx_id: str):
page = _get_page(ctx_id)
return {
"session_id": ctx_id,
"url": page.url,
"title": await page.title(),
}
@app.delete("/session/{ctx_id}")
async def close_session(ctx_id: str):
await _cleanup(ctx_id)
return {"status": "closed", "session_id": ctx_id}
@app.post("/session/{ctx_id}/navigate")
async def navigate(ctx_id: str, opts: NavigateOptions):
page = _get_page(ctx_id)
try:
await page.goto(opts.url, wait_until=opts.wait_until, timeout=BROWSER_TIMEOUT)
except PWTimeoutError:
raise HTTPException(408, "Navegação deu timeout")
except Exception as e:
raise HTTPException(500, str(e))
return {"ok": True, "url": page.url, "title": await page.title()}
@app.post("/session/{ctx_id}/click")
async def click(ctx_id: str, selector: str, timeout: int = 10000):
page = _get_page(ctx_id)
try:
await page.click(selector, timeout=timeout)
except PWTimeoutError:
raise HTTPException(408, f"Elemento não encontrado: {selector}")
except Exception as e:
raise HTTPException(500, str(e))
return {"ok": True}
@app.post("/session/{ctx_id}/fill")
async def fill(ctx_id: str, selector: str, value: str, submit: bool = False):
page = _get_page(ctx_id)
try:
await page.fill(selector, value, timeout=10000)
if submit:
await page.press(selector, "Enter")
except Exception as e:
raise HTTPException(500, str(e))
return {"ok": True}
@app.get("/session/{ctx_id}/screenshot")
async def screenshot(ctx_id: str, opts: ScreenshotOptions = ScreenshotOptions()):
page = _get_page(ctx_id)
try:
img = await page.screenshot(full_page=opts.full_page, type=opts.format)
b64 = base64.b64encode(img).decode()
return {"format": opts.format, "data": b64}
except Exception as e:
raise HTTPException(500, str(e))
@app.get("/session/{ctx_id}/html")
async def get_html(ctx_id: str):
page = _get_page(ctx_id)
return {"html": await page.content()}
@app.get("/session/{ctx_id}/text")
async def get_text(ctx_id: str, selector: str = "body"):
page = _get_page(ctx_id)
try:
el = await page.query_selector(selector)
if not el:
raise HTTPException(404, f"Elemento {selector} não encontrado")
text = await el.inner_text()
return {"selector": selector, "text": text}
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/session/{ctx_id}/evaluate")
async def evaluate(ctx_id: str, script: str):
page = _get_page(ctx_id)
try:
result = await page.evaluate(script)
return {"result": result}
except Exception as e:
raise HTTPException(500, str(e))
if __name__ == "__main__":
import uvicorn
port = int(__import__("os").getenv("PORT", 8088))
uvicorn.run("browser_cloud:app", host="0.0.0.0", port=port, reload=False)