feature: архитектура v2 — api + очередь + worker, удержание окна браузера (#78)
- Сервис api (FastAPI, без Chrome): POST /v2/tasks, GET /v2/tasks/{id} (доступ по cookie
сессии), DELETE /v2/sessions/current; /v1/solve — прежний контракт поверх очереди.
- Сервис worker (dramatiq + RabbitMQ): слоты браузера, окна сессий между задачами
(browser_idle_timeout 0–120 мин, простой — TTL ключа в Redis), heartbeat.
- Маршрутизация задач сессии в персональную очередь воркера с открытым окном.
- Redis: задачи, результаты (RESULT_TTL_HOURS), окна, воркеры. Прокси и ключ 2captcha
в Redis не сохраняются.
- Движок: AsyncStealthySession.run() — действия на текущей странице без навигации.
- Два образа (robotex-api, robotex-worker), compose с rabbitmq и redis,
make up/down/logs/smoke, релиз собирает все образы.
- Тесты: api + worker в одном процессе (fakeredis), настоящий воркер dramatiq на
StubBroker, браузерные тесты на новой схеме, smoke для запущенного стенда.
- Документация: README, API (v2), Deployment, Usage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fe509c25c3
commit
7b511d5d38
38 files changed
+2142
-545
No files matched your search
@@ -0,0 +1,36 @@
|
||||
"""Запросы и ответы для лога без секретов клиента: креды прокси, ключ 2captcha, вводимые значения,
|
||||
значения cookies, html и скриншот."""
|
||||
import json
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from api.schemas import SolveRequest
|
||||
|
||||
|
||||
def mask_proxy(proxy_url: str | None) -> str | None:
|
||||
if not proxy_url:
|
||||
return proxy_url
|
||||
parts = urlsplit(proxy_url)
|
||||
return f"{parts.scheme}://{'***@' if parts.username else ''}{parts.hostname}:{parts.port}"
|
||||
|
||||
|
||||
def safe_request_dump(payload: SolveRequest) -> str:
|
||||
data = payload.model_dump(exclude_none=True)
|
||||
data["proxy"] = mask_proxy(payload.proxy)
|
||||
if payload.captcha:
|
||||
data["captcha"] = "***"
|
||||
if payload.cookies:
|
||||
data["cookies"] = [c.get("name") for c in payload.cookies]
|
||||
for act in data["actions"]:
|
||||
if act["type"] == "fill" and "value" in act:
|
||||
act["value"] = "***"
|
||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def safe_response_dump(result: dict) -> str:
|
||||
data = dict(result)
|
||||
if data.get("cookies"):
|
||||
data["cookies"] = [c.get("name") for c in data["cookies"]]
|
||||
for key in ("html", "screenshot"):
|
||||
if data.get(key):
|
||||
data[key] = f"<{len(data[key])} chars>"
|
||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||
+32
-1
@@ -1,9 +1,11 @@
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
CAPTCHA_TYPES = ("cloudflare", "recaptcha", "hcaptcha")
|
||||
# максимум browser_idle_timeout в минутах: открытое окно держит слот браузера (#78)
|
||||
MAX_BROWSER_IDLE_TIMEOUT = 120
|
||||
PROXY_SCHEMES = ("http", "https", "socks5")
|
||||
|
||||
# тип действия -> обязательные поля (кроме type)
|
||||
@@ -143,3 +145,32 @@ class SolveResponse(BaseModel):
|
||||
html: str | None = None
|
||||
screenshot: str | None = None
|
||||
duration_ms: int | None = None
|
||||
|
||||
|
||||
class TaskRequest(SolveRequest):
|
||||
"""
|
||||
Задача POST /v2/tasks: тело как у /v1/solve плюс удержание окна браузера.
|
||||
|
||||
url необязателен, если окно сессии открыто: тогда действия выполняются на текущей странице.
|
||||
"""
|
||||
url: str | None = None
|
||||
# минуты простоя, через которые окно сессии закроется; 0 — закрыть сразу после задачи
|
||||
browser_idle_timeout: int = Field(0, ge=0, le=MAX_BROWSER_IDLE_TIMEOUT)
|
||||
|
||||
|
||||
class TaskCreated(BaseModel):
|
||||
task_id: str
|
||||
status: str
|
||||
url: str
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
"""
|
||||
GET /v2/tasks/{id}. status: queued → running → done | failed.
|
||||
done — результат в result (в нём свой status ok/err, как в /v1/solve);
|
||||
failed — задачу не удалось выполнить (воркер упал), причина в error.
|
||||
"""
|
||||
task_id: str
|
||||
status: Literal["queued", "running", "done", "failed", "cancelled"]
|
||||
error: str | None = None
|
||||
result: SolveResponse | None = None
|
||||
Reference in new issue
Block a user