e833b923c8
接入 Playwright Worker、会话 WebSocket 与全局浏览器面板(固定于 App Bar 下); 含验证码 stealth、设置项默认音与 URL 安全校验;附带 worker 部署与设计文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
"""HTTP client for Browser Worker VPS."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
from urllib.parse import urlparse, urlunparse
|
|
|
|
import httpx
|
|
|
|
from server.config import settings
|
|
|
|
logger = logging.getLogger("nexus.browser.worker")
|
|
|
|
|
|
class BrowserWorkerError(Exception):
|
|
def __init__(self, message: str, status_code: int = 503) -> None:
|
|
super().__init__(message)
|
|
self.status_code = status_code
|
|
|
|
|
|
def browser_enabled() -> bool:
|
|
return (
|
|
settings.BROWSER_ENABLED.lower() in ("1", "true", "yes")
|
|
and bool(settings.BROWSER_WORKER_URL)
|
|
and bool(settings.BROWSER_WORKER_SECRET)
|
|
)
|
|
|
|
|
|
def _headers() -> dict[str, str]:
|
|
return {"X-Nexus-Worker-Key": settings.BROWSER_WORKER_SECRET}
|
|
|
|
|
|
def worker_stream_url(session_id: str) -> str:
|
|
base = settings.BROWSER_WORKER_URL.rstrip("/")
|
|
parsed = urlparse(base)
|
|
scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
netloc = parsed.netloc or parsed.path
|
|
path = f"/v1/sessions/{session_id}/stream"
|
|
return urlunparse((scheme, netloc, path, "", "", ""))
|
|
|
|
|
|
async def create_worker_session(session_id: str, viewport_w: int, viewport_h: int) -> dict[str, Any]:
|
|
if not browser_enabled():
|
|
raise BrowserWorkerError("Browser worker not enabled", 503)
|
|
url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/v1/sessions"
|
|
payload = {"session_id": session_id, "viewport_w": viewport_w, "viewport_h": viewport_h}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30.0, verify=False) as client:
|
|
resp = await client.post(url, json=payload, headers=_headers())
|
|
except httpx.RequestError as exc:
|
|
logger.warning("worker create failed: %s", exc)
|
|
raise BrowserWorkerError("Browser worker unreachable", 503) from exc
|
|
if resp.status_code >= 400:
|
|
detail = resp.text[:200]
|
|
raise BrowserWorkerError(detail or "Worker rejected session", resp.status_code)
|
|
return resp.json()
|
|
|
|
|
|
async def delete_worker_session(session_id: str) -> None:
|
|
if not browser_enabled():
|
|
return
|
|
url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/v1/sessions/{session_id}"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15.0, verify=False) as client:
|
|
await client.delete(url, headers=_headers())
|
|
except httpx.RequestError as exc:
|
|
logger.warning("worker delete failed session=%s: %s", session_id, exc)
|
|
|
|
|
|
async def worker_health() -> dict[str, Any] | None:
|
|
if not browser_enabled():
|
|
return None
|
|
url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/health"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0, verify=False) as client:
|
|
resp = await client.get(url)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
except httpx.RequestError:
|
|
return None
|
|
return None
|