Files
robotex/src/api/actions.py
T
protokeyandClaude Opus 5.5 437ea27be7 fix: слоты, безопасность, единый формат ответа и валидация
- слоты браузеров: asyncio.Semaphore, размер из BROWSER_SLOTS, при нехватке — 503 + Retry-After;
  устранена утечка счётчика при отказе (#12, #49)
- session_id принимается только как UUID, путь профиля проверяется (#5)
- секреты клиента в логах маскируются, полный дамп — только на DEBUG (#6)
- ответ: status всегда "ok"/"err", поле code (no_slots / request_failed / challenge_failed /
  action_failed), response_model (#7)
- непройденный антибот → challenge_failed, действия не выполняются (#8)
- таймауты провайдеров в мс вынесены в константы; detect переживает навигацию посреди
  проверки; пауза между неудачными solve вместо холостого цикла (#9)
- обязательные поля действий и формат прокси проверяются схемой → 422 до запуска браузера (#13)
- прокси необязателен (#15)
- user_agent сохраняется в профиле сессии и реально передаётся в браузер (#16)
- duration_ms у действий и у запроса (#18), опция screenshot (#19)
- общий конфиг логирования (LOG_LEVEL), print → логгер, CloudflareProvider,
  удалён cached_persistent, GET /health (#22)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:52:12 +04:00

141 lines
3.8 KiB
Python

import asyncio
import time
from patchright.async_api import Page
from antibot.orchestrator import pass_challenges
from captcha import load_captcha
from api.schemas import ActionRequest
_actions: dict = {}
class ChallengeFailed(Exception):
pass
def register(fun) -> None:
_actions[fun.__name__] = fun
return fun
async def _query(page: Page, selector: str):
element = await page.query_selector(selector)
if element is None:
raise LookupError(f"selector not found: {selector}")
return element
@register
async def fill(page: Page, act: ActionRequest, **kwargs) -> str | None:
await page.fill(act.selector, act.value)
@register
async def captcha(page: Page, act: ActionRequest, captcha_key: str, **kwargs) -> str | None:
captcha_solver = load_captcha(captcha_key, act.value)
await captcha_solver.solve(act.selector, page)
@register
async def delay(page: Page, act: ActionRequest, **kwargs) -> str | None:
await asyncio.sleep(int(act.value))
@register
async def mouse_move(page: Page, act: ActionRequest, **kwargs) -> str | None:
element = await _query(page, act.selector)
await element.hover(scroll="auto")
@register
async def click(page: Page, act: ActionRequest, **kwargs) -> str | None:
element = page.locator(act.selector)
await element.click()
@register
async def submit(page: Page, act: ActionRequest, **kwargs) -> str | None:
element = page.locator(act.selector)
async with page.expect_event(
"framenavigated",
predicate=lambda frame: frame == page.main_frame,
):
await element.click()
@register
async def inner_html(page: Page, act: ActionRequest, **kwargs) -> str | None:
element = await _query(page, act.selector)
return await element.inner_html()
@register
async def wait_for(page: Page, act: ActionRequest, **kwargs) -> str | None:
await page.wait_for_selector(act.selector, timeout=act.timeout * 1000)
@register
async def pass_challenge(
page: Page, act: ActionRequest, *,
antibot_name, success_locator=None, captcha_key=None, detect_locator=None,
**kwargs
) -> str | None:
result = await pass_challenges(
antibot_name, page,
captcha=captcha_key,
success_locator=act.selector or success_locator,
detect_locator=detect_locator
)
if not result.cleared:
raise Exception(f"challenge not passed: {result.reason}")
return result.reason
async def execute(
page: Page, actions: list[ActionRequest],
captcha_key: str | None = None, antibot_name: str | None = None,
success_locator: str | None = None,
detect_locator: str | None = None) -> list[dict]:
"""Выполнение очереди автоматизации.
Если антибот не пройден — бросает ChallengeFailed, действия не выполняются: на странице-заглушке
они бы упали с невнятной ошибкой селектора."""
actions_result = []
ctx = {
"captcha_key": captcha_key,
"antibot_name": antibot_name,
"success_locator": success_locator,
"detect_locator": detect_locator,
}
outcome = await pass_challenges(
antibot_name, page,
captcha=captcha_key,
success_locator=success_locator,
detect_locator=detect_locator,
)
if not outcome.cleared:
stage = outcome.last_stage.value if outcome.last_stage else None
raise ChallengeFailed(f"{antibot_name} not passed: {outcome.reason} (stage: {stage})")
for act in actions:
new_line = {
"type": act.type,
}
started = time.monotonic()
try:
result = await _actions[act.type](page, act, **ctx)
except Exception as e:
new_line["ok"] = False
new_line["result"] = str(e) or type(e).__name__
else:
new_line["ok"] = True
if result:
new_line["result"] = result
new_line["duration_ms"] = int((time.monotonic() - started) * 1000)
actions_result.append(new_line)
if not new_line["ok"]:
break
return actions_result