# ТЗ: расширяемый модуль обхода антибот-проверок и капч ## 1. Контекст и цель Сейчас в проекте обход капчи — это ручное действие: клиент сам должен прислать `wait_for`/`click` на нужный селектор и явное действие `captcha` с готовым `captcha_type` ([src/engine/actions.py](../src/engine/actions.py)). Это работает, только если клиент заранее знает, какая защита стоит на сайте. Цель — добавить **отдельный слой**, который САМ: 1. определяет, что на странице сейчас показан какой-то антибот-барьер (и какой именно); 2. проходит его по единому алгоритму, подходящему для целого класса барьеров; 3. если барьер эскалировался до классической капчи с картинками — передаёт управление уже существующему модулю [src/captcha](../src/captcha) (или его расширению). Важно: это НЕ замена `actions`, а новый action (`pass_challenge`) + фоновая проверка, которую можно вызывать перед/после навигации и перед любым действием, если оно упёрлось в перехваченный клик. Референс алгоритма (как это устроено в Scrapling, `scrapling/engines/_browsers/_stealth.py::_cloudflare_solver` + `_base.py::_detect_cloudflare`) — см. предыдущее обсуждение в чате. Ключевой вывод оттуда: **сам "solve" — это не решение головоломки, а клик по чекбоксу с рандомизацией + поллинг**, вся сложность — в детекте типа барьера и в том, чтобы окружение (фингерпринт) уже выглядело "человеческим" настолько, чтобы клик засчитался. ### 1.1. Главный сценарий отказа, который этот модуль обязан закрыть В отличие от типичных open-source решений, у вас **не стоит вопрос "решать капчу или нет"** — сервис распознавания (2captcha) уже подключён и используется без колебаний ([hcaptcha.py](../src/captcha/hcaptcha.py)). Поэтому основной риск — не "не смогли решить", а: - **API молча возвращает контент JS-challenge'а как будто это целевая страница.** Сейчас в [main.py:143](../src/main.py) `html = await page.inner_html('html')` вызывается без какой-либо проверки, что это действительно страница, а не заглушка "Just a moment...". Если клиент прислал `actions`, ожидающие DOM, которого на challenge-странице никогда не будет, они просто упадут по таймауту с невнятной ошибкой — а если не прислал (например, забрать html целиком) — вернётся мусор с `"status": "ok"`. - **API виснет до общего timeout**, потому что ждёт селектор, а страница вообще не та, и ни один action не расскажет, что происходит на самом деле. Формула, которую должен обеспечить модуль: *"открыли страницу -> если это barrier — осознали это явно (залогировали тип/провайдера) -> подождали или решили -> только после этого либо продолжили выполнять `actions` клиента, либо кинули понятную ошибку с указанием, на какой стадии застряли"*. Ни один ответ не должен уходить клиенту с пометкой `"ok"`, если последнее состояние страницы — это всё ещё распознанный barrier (см. §7, "Финальный гейт перед ответом"). ## 2. Классификация состояний Любой антибот-барьер, который встречается боту при заходе на страницу, укладывается в одну из трёх стадий. Стадии одинаковы для всех провайдеров (Cloudflare, DataDome, Akamai, PerimeterX/Human, Imperva, AWS WAF Captcha, hCaptcha, reCAPTCHA) — отличаются только **сигнатуры детекта**, а не сам алгоритм прохождения. | Стадия | Что видит пользователь | Что делать | Нужен ли внешний solver | |---|---|---|---| | **A. Interstitial / "Please wait"** | Пустая страница-заглушка с спиннером, редиректом или JS-челленджем ("Checking your browser…", "Just a moment…", "DDoS protection by …") | Просто ждать, пока страница сама не смёнится (поллинг DOM/URL), кликать не нужно | Нет — только детект + ожидание | | **B. Checkbox-виджет** | Отдельная плашка/iframe с одним чекбоксом ("Verify you are human", Turnstile, invisible hCaptcha/reCAPTCHA checkbox) | Клик по чекбоксу человекоподобным движением мыши, дождаться результата | Обычно нет (иногда после клика эскалирует в стадию C) | | **C. Image/interactive challenge** | Сетка картинок, "выбери все со светофорами", audio-капча | Нужно реальное распознавание — делегировать в 2captcha/аналог по `sitekey`, как уже сделано в [hcaptcha.py](../src/captcha/hcaptcha.py) | Да | Ваше наблюдение верно: сама механика прохождения (A — просто подождать, B — кликнуть) **не зависит от вендора**. Расширяемость нужна только в детекторах: вы добавляете сигнатуру нового провайдера, а orchestrator и solver'ы переиспользуются. ## 3. Архитектура модуля Новый пакет `src/antibot/` со структурой, идущей по аналогии с уже существующим паттерном в проекте (`_actions: dict` в actions.py, `Captcha.__subclasses__()` в captcha/base.py — то есть реестр через наследование/декоратор, без магии): ``` src/antibot/ __init__.py # экспорт orchestrator + регистрация встроенных детекторов base.py # абстракции: ChallengeInfo, Detector, Solver registry.py # реестр детекторов, порядок проверки stages/ interstitial.py # стадия A: универсальный wait-solver + сигнатуры провайдеров checkbox.py # стадия B: универсальный click-solver + сигнатуры провайдеров interactive.py # стадия C: мост к src/captcha (hcaptcha/recaptcha/...) humanize.py # человекоподобные движения мыши (безье+ускорение), тайминги кликов orchestrator.py # цикл: detect -> solve -> verify -> retry ``` ### 3.1. Базовые абстракции (`base.py`) ```python from dataclasses import dataclass from enum import Enum from patchright.async_api import Page class Stage(str, Enum): INTERSTITIAL = "interstitial" CHECKBOX = "checkbox" INTERACTIVE = "interactive" @dataclass class ChallengeInfo: stage: Stage provider: str # "cloudflare_turnstile", "datadome", "hcaptcha", "recaptcha_v2", ... frame_locator: str | None = None # css/regex для iframe, если применимо site_key: str | None = None # для стадии C, чтобы передать в 2captcha meta: dict | None = None # что-то специфичное для провайдера class Detector: """Один детектор = одна сигнатура одного провайдера на одной стадии.""" provider: str stage: Stage async def detect(self, page: Page) -> ChallengeInfo | None: raise NotImplementedError class Solver: """Один solver на стадию (универсальный), НЕ на провайдера.""" stage: Stage async def solve(self, page: Page, info: ChallengeInfo) -> bool: """Возвращает True, если стадия пройдена (или больше не требуется действий).""" raise NotImplementedError ``` Важно: **Detector -> 1:1 с провайдером, Solver -> 1:1 со стадией**. Не наоборот. Это и даёт расширяемость: новый провайдер = один новый класс детектора на несколько строк, без изменения логики клика/ожидания. ### 3.2. Реестр (`registry.py`) ```python _detectors: dict[Stage, list[Detector]] = {s: [] for s in Stage} def register_detector(detector_cls): instance = detector_cls() _detectors[instance.stage].append(instance) return detector_cls async def detect_any(page: Page, stage: Stage) -> ChallengeInfo | None: for detector in _detectors[stage]: info = await detector.detect(page) if info: return info return None ``` Порядок стадий при детекте всегда **A -> B -> C** (сначала проверяем "не висим ли мы ещё в заглушке", потом "нет ли чекбокса", потом "не показали ли уже картинки"), потому что провайдер может сразу показать любую из них в зависимости от risk score IP/фингерпринта. ## 4. Алгоритм по стадиям ### 4.1. Стадия A — Interstitial (`stages/interstitial.py`) **Детекторы** (примеры сигнатур, добавлять новые — просто дописывать список): - Cloudflare: строка `cType: 'non-interactive'` / `cType: 'managed'` в HTML (как в оригинале), либо `