Files
Nexus/server/infrastructure/browser/worker_client.py
T
Nexus Agent e833b923c8 feat(browser): 远程浏览器 Worker 桥接与顶栏固定悬浮窗
接入 Playwright Worker、会话 WebSocket 与全局浏览器面板(固定于 App Bar 下);
含验证码 stealth、设置项默认音与 URL 安全校验;附带 worker 部署与设计文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:49:20 +08:00

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