docs: README и docs приведены к реализации (#50)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-13 14:52:12 +04:00
1 parent 437ea27be7
commit fd6ecaaee8
4 files changed
+130 -81

No files matched your search

+72 -41
View File
@@ -1,60 +1,78 @@
# Robotex # Robotex
Browser automation API with built-in anti-bot stealth. Send a URL and a sequence of declarative actions — fill a field, Browser automation API with built-in anti-bot stealth. Send a URL and a sequence of declarative actions — fill a field,
click, wait for an element — and get back the rendered page, including sites behind Cloudflare-style JS challenges. click, wait for an element — and get back cookies, the rendered page and, optionally, a screenshot, including sites
behind Cloudflare-style JS challenges.
## Status ## Status
Early MVP (v0), under active development. Single browser engine, no queue, no billing yet — see [Roadmap](#roadmap). Early MVP (v0), under active development. Single browser engine, no queue, no billing yet. Tasks and project wiki —
[Redmine](https://redmine.sokolab.xyz/projects/robotex).
Not implemented yet, though planned for v1:
- API-key auth (`X-API-Key` is accepted but **not checked**) — [#4](https://redmine.sokolab.xyz/issues/4)
- persisting request outcomes (success rate) — [#17](https://redmine.sokolab.xyz/issues/17)
- cleanup of old browser profiles — [#20](https://redmine.sokolab.xyz/issues/20)
## Architecture ## Architecture
``` ```
Client → Caddy (TLS) → FastAPI → API-key auth → semaphore acquire Client → Caddy (TLS, on the host) → FastAPI POST /v1/solve → browser slot (semaphore, 503 if none)
│ │
▼ ▼
Chrome (Patchright) context Chrome (Patchright), profile per session_id
(run actions, collect result) pass anti-bot → run actions → collect result
│ │
▼ ▼
release semaphore → response release slot → response
``` ```
- **Engine**: real Google Chrome (`channel="chrome"`) driven via [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python) - **Engine**: real Google Chrome (`channel="chrome"`) driven via [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python)
(undetected Playwright fork), headful under Xvfb. Firefox is not supported yet. (undetected Playwright fork), headful under Xvfb. Browser session wrapper ported from scrapling. Firefox is not
- **API**: FastAPI, single versioned endpoint (`POST /v1/solve`). supported yet.
- **Concurrency**: bounded by an `asyncio.Semaphore` over browser contexts — no external job queue in v1. Simpler to - **API**: FastAPI, single versioned endpoint (`POST /v1/solve`) plus `GET /health`.
run, hard ceiling on throughput per instance (see Roadmap for scaling out). - **Concurrency**: bounded by an `asyncio.Semaphore` of `BROWSER_SLOTS` (env, default 10) — no external job queue in
- **Auth**: per-client API key (`X-API-Key` header), checked against a SQLite table. (SQLAlchemy ORM) v1. When all slots are busy the request gets `503` with `Retry-After` immediately, it is not queued.
- **Logging**: every request's outcome (success/failure/duration) is persisted — this becomes the success-rate data used - **Anti-bot**: detect → solve → verify loop for Cloudflare Turnstile and reCAPTCHA; captchas inside pages (Turnstile,
to evaluate the service and talk to clients about reliability. reCAPTCHA, hCaptcha) are solved via the client's 2captcha key.
- **Proxies**: not managed by the service in v1 — the client supplies their own proxy per request if they need one. - **Proxies**: not managed by the service — the client may supply their own proxy per request. Browser locale and
- **Deployment**: single Docker container behind a Caddy reverse proxy — see [Docker](#docker) and timezone are derived from the proxy's exit IP.
- **Sessions**: `session_id` cookie → persistent Chrome profile (cookies, local storage, user agent) on disk.
- **Logging**: stdout, level from `LOG_LEVEL`; client secrets (proxy credentials, 2captcha key, `fill` values,
cookie values) are masked.
- **Deployment**: single Docker container; TLS is terminated by Caddy on the host — see
[docs/Deployment.md](docs/Deployment.md). [docs/Deployment.md](docs/Deployment.md).
- **Canvas fingerprint** по дизайну генерируется каждую сессию новый. - **Canvas fingerprint** is generated anew for every session by design.
### Project layout ### Project layout
``` ```
src/ src/
main.py # FastAPI app, POST /v1/solve main.py # FastAPI app: /v1/solve, /health, slots, sessions, response
api/ api/
actions.py # fill / click / wait_for execution schemas.py # SolveRequest / ActionRequest / SolveResponse + validation
schemas.py # SolveRequest / Action models actions.py # action registry and execution
geoip.py # proxy → locale / timezone geoip.py # proxy → locale / timezone
engine/ engine/ # Chrome session wrapper (ported from scrapling)
stealthy.py # AsyncStealthySession (Patchright + Chrome) stealthy.py # AsyncStealthySession
session.py # browser / context launch options session.py # browser / context launch options
page_pool.py # page pool navigation.py, page_pool.py, validators.py, proxy_rotation.py, ad_domains.py, ...
antibot/ # anti-bot detection & bypass orchestrator (Cloudflare, reCAPTCHA) antibot/
captcha/ # captcha solvers (Turnstile, reCAPTCHA, hCaptcha) orchestrator.py # detect → solve → verify loop
registry.py, base.py
known_providers/ # cloudflare.py, recaptcha.py
humanize.py # human-like mouse moves and typing
captcha/ # 2captcha solvers: Turnstile, reCAPTCHA, hCaptcha
docs/ # API, deployment, anti-bot notes
tests/ tests/
wheels/ # whl библиотеки для оффлайн установки под Ubuntu Noble loadtest.py # concurrent load test + docker stats
test_main.http # manual requests (JetBrains HTTP client)
wheels/ # wheels for offline install on Ubuntu Noble
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml # local / load testing
.dockerignore docker-compose.prod.yml # template for releases (make release)
requirements.txt Makefile
env.example
``` ```
## API ## API
@@ -81,12 +99,15 @@ Response:
"status": "ok", "status": "ok",
"user_agent": "Mozilla/5.0 (...) Chrome/155.0.0.0 Safari/537.36", "user_agent": "Mozilla/5.0 (...) Chrome/155.0.0.0 Safari/537.36",
"cookies": [{"name": "cf_clearance", "value": "..."}], "cookies": [{"name": "cf_clearance", "value": "..."}],
"actions": [{"type": "wait_for", "ok": true}], "actions": [{"type": "wait_for", "ok": true, "duration_ms": 14}],
"html": "<head>...</head><body>...</body>" "html": "<head>...</head><body>...</body>",
"duration_ms": 6405
} }
``` ```
Full reference — fields, action types, error formats, sessions: [docs/API.md](docs/API.md). On failure `status` is `"err"` with a machine-readable `code` (`no_slots`, `request_failed`, `challenge_failed`,
`action_failed`) and a human-readable `error`. Full reference — fields, action types, errors, sessions:
[docs/API.md](docs/API.md).
## Roadmap ## Roadmap
@@ -94,7 +115,7 @@ Deliberately out of scope for v1:
- Job queue (Redis/RabbitMQ) + multiple worker VPS for horizontal scaling - Job queue (Redis/RabbitMQ) + multiple worker VPS for horizontal scaling
- Real billing/usage plans beyond a flat API-key check - Real billing/usage plans beyond a flat API-key check
- Broader action vocabulary beyond fill/click/wait_for - Broader action vocabulary
- Second engine (Firefox, e.g. Camoufox) for targets where the Chrome fingerprint doesn't fit - Second engine (Firefox, e.g. Camoufox) for targets where the Chrome fingerprint doesn't fit
## Local development ## Local development
@@ -106,6 +127,8 @@ make sync-offline
cd src && uvicorn main:app --reload cd src && uvicorn main:app --reload
``` ```
Chrome runs headful, so a display is required (locally — your desktop, in Docker — Xvfb).
## Docker ## Docker
```bash ```bash
@@ -113,5 +136,13 @@ docker compose up -d --build
``` ```
The API listens on `http://localhost:8000`. Browser profiles (`extra/user_data_dir`) and the GeoIP cache live in the The API listens on `http://localhost:8000`. Browser profiles (`extra/user_data_dir`) and the GeoIP cache live in the
`extra` named volume, so sessions survive container restarts. Details (Xvfb, tini, `/dev/shm`) — `extra` named volume, so sessions survive container restarts.
[docs/Deployment.md](docs/Deployment.md).
Releases:
```bash
make build ver=v0.0.2 # docker image robotex:v0.0.2
make release ver=v0.0.2 # image + deploy bundle in build/v0.0.2/ (compose, .env, image .tar.gz)
```
Details (Xvfb, tini, `/dev/shm`, env settings, load measurements) — [docs/Deployment.md](docs/Deployment.md).
+46 -35
View File
@@ -1,8 +1,12 @@
# API # API
Один эндпоинт: `POST /v1/solve`. Открывает страницу в Chrome через прокси клиента, при необходимости проходит Основной эндпоинт: `POST /v1/solve`. Открывает страницу в Chrome (через прокси клиента, если он задан), при необходимости проходит
антибот (Cloudflare Turnstile, reCAPTCHA), выполняет список действий и возвращает cookies, результаты действий и, антибот (Cloudflare Turnstile, reCAPTCHA), выполняет список действий и возвращает cookies, результаты действий и,
по запросу, HTML. по запросу, HTML и скриншот.
`GET /health` (и `GET /`) — `{"status": "ok", "browser_slots": N, "free_slots": M}`.
> `X-API-Key` пока не проверяется ([#4](https://redmine.sokolab.xyz/issues/4)).
## Запрос ## Запрос
@@ -27,15 +31,16 @@ Cookie: session_id=<id> # необязательно, см. «Сесси
| Поле | Тип | По умолчанию | Описание | | Поле | Тип | По умолчанию | Описание |
|---|---|---|---| |---|---|---|---|
| `url` | string | — | **обязательно**. Страница, которую открываем | | `url` | string | — | **обязательно**. Страница, которую открываем |
| `proxy` | string | — | **обязательно**. `http://`, `https://` или `socks5://` (Chrome не поддерживает socks5 с логином/паролем). По exit-IP прокси выставляются locale и timezone браузера | | `proxy` | string | `null` | `scheme://[user:pass@]host:port`, схема `http`, `https` или `socks5` (Chrome не поддерживает socks5 с логином/паролем). По exit-IP прокси выставляются locale и timezone браузера; без прокси — настройки браузера по умолчанию |
| `actions` | array | — | **обязательно**, может быть пустым. Действия, см. ниже | | `actions` | array | — | **обязательно**, может быть пустым. Действия, см. ниже |
| `captcha_type` | string | `null` | провайдер антибота: `cloudflare` или `recaptcha`. Без него шаг антибота пропускается — для сайтов за Cloudflare обязательно | | `captcha_type` | string | `null` | провайдер антибота: `cloudflare` или `recaptcha`. Без него шаг антибота пропускается — для сайтов за Cloudflare обязательно |
| `captcha` | string | `null` | ключ сервиса распознавания (2captcha), если антибот не проходится без решения капчи | | `captcha` | string | `null` | ключ сервиса распознавания (2captcha), если антибот не проходится без решения капчи |
| `captcha_detect_locator` | string | `null` | локатор, по которому определяется, что на странице антибот | | `captcha_detect_locator` | string | `null` | локатор, по которому определяется, что на странице антибот |
| `captcha_success_locator` | string | `null` | локатор, по которому определяется, что антибот пройден | | `captcha_success_locator` | string | `null` | локатор, по которому определяется, что антибот пройден |
| `html` | bool | `false` | вернуть HTML страницы после выполнения действий | | `html` | bool | `false` | вернуть HTML страницы после выполнения действий |
| `screenshot` | bool | `false` | вернуть скриншот видимой области (PNG, base64) после действий — в том числе при ошибке действия или антибота |
| `timeout` | int, с | `60` | таймаут навигации | | `timeout` | int, с | `60` | таймаут навигации |
| `user_agent` | string | `null` | свой User-Agent; без него берётся UA установленного Chrome | | `user_agent` | string | `null` | свой User-Agent; запоминается в сессии (см. «Сессии»). Без него — UA сессии или UA установленного Chrome |
| `cookies` | array | `null` | cookies для установки до навигации, формат Playwright: `{name, value, domain, path, expires, httpOnly, secure, sameSite}` | | `cookies` | array | `null` | cookies для установки до навигации, формат Playwright: `{name, value, domain, path, expires, httpOnly, secure, sameSite}` |
| `disable_resources` | bool | `false` | не грузить картинки, шрифты, медиа | | `disable_resources` | bool | `false` | не грузить картинки, шрифты, медиа |
| `blocked_domains` | array | `null` | домены, запросы к которым блокируются | | `blocked_domains` | array | `null` | домены, запросы к которым блокируются |
@@ -46,25 +51,30 @@ Cookie: session_id=<id> # необязательно, см. «Сесси
### Действия ### Действия
Выполняются по порядку после прохождения антибота. На первой ошибке выполнение останавливается. Выполняются по порядку после прохождения антибота. Если антибот не пройден, действия не выполняются
(`code: challenge_failed`). На первой ошибке действия выполнение останавливается.
Обязательные поля проверяются до запуска браузера: неизвестный `type`, нет `selector`/`value`, `captcha` без ключа
в запросе, неверный `proxy` — ответ `422` с описанием в `detail` (стандартный формат FastAPI).
| `type` | Поля | Что делает | | `type` | Поля | Что делает |
|---|---|---| |---|---|---|
| `wait_for` | `selector`, `timeout` (1–7200 с) | ждёт появления элемента; при таймауте — ошибка действия | | `wait_for` | `selector`, `timeout` (1–7200 с, по умолчанию 30) | ждёт появления элемента; при таймауте — ошибка действия |
| `click` | `selector` | клик по элементу | | `click` | `selector` | клик по элементу |
| `submit` | `selector` | клик и ожидание навигации | | `submit` | `selector` | клик и ожидание навигации |
| `fill` | `selector`, `value` | вставка значения в поле | | `fill` | `selector`, `value` | вставка значения в поле |
| `mouse_move` | `selector` | наведение курсора на элемент | | `mouse_move` | `selector` | наведение курсора на элемент; нет элемента — ошибка `selector not found` |
| `delay` | `value` (секунды строкой) | пауза | | `delay` | `value` (секунды строкой, 0–600) | пауза |
| `inner_html` | `selector` | возвращает HTML элемента в `result` | | `inner_html` | `selector` | возвращает HTML элемента в `result`; нет элемента — ошибка `selector not found` |
| `captcha` | `selector`, `value` (тип: `cloudflare`, `recaptcha`, `hcaptcha`) | решить капчу внутри страницы через 2captcha, нужен `captcha` | | `captcha` | `selector`, `value` (тип: `cloudflare`, `recaptcha`, `hcaptcha`) | решить капчу внутри страницы через 2captcha, нужен `captcha` в запросе |
| `pass_challenge` | `selector` (локатор успеха, необязательно) | повторно пройти антибот, например после перехода на другую страницу; если не пройден — ошибка действия | | `pass_challenge` | `selector` (локатор успеха, необязательно) | повторно пройти антибот, например после перехода на другую страницу; если не пройден — ошибка действия |
## Ответ ## Ответ
HTTP-статус всегда `200`, результат — в теле. Формат один для всех случаев: `status` — `"ok"` или `"err"`, при ошибке — `code` и `error`. Поля без значения не
возвращаются.
Успех (антибот пройден, действия выполнены): Успех (антибот пройден, действия выполнены), HTTP `200`:
```json ```json
{ {
@@ -72,51 +82,52 @@ HTTP-статус всегда `200`, результат — в теле.
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) ... Chrome/155.0.0.0 Safari/537.36", "user_agent": "Mozilla/5.0 (X11; Linux x86_64) ... Chrome/155.0.0.0 Safari/537.36",
"cookies": [{"name": "cf_clearance", "value": "...", "domain": ".freelancehunt.com", "...": "..."}], "cookies": [{"name": "cf_clearance", "value": "...", "domain": ".freelancehunt.com", "...": "..."}],
"actions": [ "actions": [
{"type": "wait_for", "ok": true} {"type": "wait_for", "ok": true, "duration_ms": 14}
], ],
"html": "<head>...</head><body>...</body>" "html": "<head>...</head><body>...</body>",
"duration_ms": 6405
} }
``` ```
| Поле | Описание | | Поле | Описание |
|---|---| |---|---|
| `status` | `"ok"` — все действия выполнены, `"err"` — одно из действий упало (выполнение на нём остановлено) | | `status` | `"ok"` — антибот пройден и все действия выполнены, иначе `"err"` |
| `code` | при `"err"`: причина, см. таблицу ниже |
| `error` | при `"err"`: текст ошибки; для упавшего действия — `<type>: <текст>` |
| `user_agent` | UA браузера, с которым проходили антибот (нужен клиенту вместе с `cf_clearance`) | | `user_agent` | UA браузера, с которым проходили антибот (нужен клиенту вместе с `cf_clearance`) |
| `cookies` | все cookies контекста | | `cookies` | все cookies контекста |
| `actions` | по элементу на выполненное действие: `type`, `ok`, `result` (результат или текст ошибки) | | `actions` | по элементу на выполненное действие: `type`, `ok`, `result` (результат или текст ошибки), `duration_ms` |
| `html` | только при `"html": true` | | `html` | только при `"html": true` |
| `error` | при `status: "err"`: `<type>: <текст ошибки>` упавшего действия | | `screenshot` | только при `"screenshot": true`: PNG в base64 |
| `duration_ms` | общее время обработки запроса |
Ошибка с упавшим действием: | `code` | HTTP | Когда | Что ещё есть в ответе |
|---|---|---|---|
| `no_slots` | `503` + `Retry-After` | все браузеры заняты (`BROWSER_SLOTS`), запрос не ставится в очередь | — |
| `request_failed` | `200` | ошибка до действий: навигация, прокси, неизвестный `captcha_type` | `actions: []` |
| `challenge_failed` | `200` | антибот не пройден за отведённое время, действия не выполнялись | cookies, html, screenshot |
| `action_failed` | `200` | упало действие, выполнение остановлено на нём | cookies, html, screenshot, `actions` до упавшего включительно |
Примеры:
```json ```json
{ {"status": "err", "code": "action_failed", "error": "inner_html: selector not found: #missing",
"status": "err", "actions": [{"type": "inner_html", "ok": false, "result": "selector not found: #missing", "duration_ms": 26}], "...": "..."}
"actions": [{"type": "wait_for", "ok": false, "result": "Page.wait_for_selector: Timeout 3000ms exceeded. ..."}],
"error": "wait_for: Page.wait_for_selector: Timeout 3000ms exceeded. ..."
}
``` ```
Ошибка до выполнения действий (навигация, прокси, неизвестный `captcha_type`):
```json ```json
{"status": false, "error": "unknown captcha_type 'datadome', expected one of: recaptcha, cloudflare"} {"status": "err", "code": "challenge_failed", "error": "cloudflare not passed: timeout (stage: checkbox)", "actions": [], "...": "..."}
``` ```
Нет свободных браузеров (лимит `BROWSER_SLOTS`):
```json ```json
{"ok": false, "error": "No slots available"} {"status": "err", "code": "no_slots", "error": "No slots available"}
``` ```
> Форматы ошибок пока не унифицированы (тех. долг): `status` бывает `"ok"`/`"err"`/`false`, а при занятых слотах
> поля `status` нет вовсе. Успех — строго `status == "ok"`.
## Сессии ## Сессии
Ответ ставит cookie `session_id`. Если передать её в следующем запросе, браузер откроется с тем же профилем Ответ ставит cookie `session_id` (UUID). Если передать её в следующем запросе, браузер откроется с тем же профилем
(`extra/user_data_dir/<session_id>`): cookies, localStorage и отпечаток сохраняются, повторно проходить Cloudflare (`extra/user_data_dir/<session_id>`): cookies, localStorage, отпечаток и `user_agent` сохраняются, повторно проходить Cloudflare
обычно не нужно. Без cookie каждый запрос получает новый профиль. обычно не нужно. Без cookie или с некорректным значением (не UUID) запрос получает новый профиль.
Один профиль нельзя использовать в двух запросах одновременно: Chrome заблокирует каталог профиля. Один профиль нельзя использовать в двух запросах одновременно: Chrome заблокирует каталог профиля.
+10 -3
View File
@@ -51,6 +51,14 @@ docker compose exec api sh -c 'rm -rf /app/extra/user_data_dir/*'
- `restart: unless-stopped`. - `restart: unless-stopped`.
- `.env` подключается, если существует (шаблон — `env.example`). - `.env` подключается, если существует (шаблон — `env.example`).
## Переменные окружения приложения
| Переменная | По умолчанию | Описание |
|---|---|---|
| `BROWSER_SLOTS` | `10` | сколько Chrome работает параллельно; сверх — `503` |
| `NO_SLOTS_RETRY_AFTER` | `5` | значение `Retry-After` (с) в ответе `503` |
| `LOG_LEVEL` | `INFO` | `DEBUG` — полный дамп запроса/ответа (секреты клиента маскируются) |
## Нагрузка ## Нагрузка
Замер: `freelancehunt.com/projects` за Cloudflare Turnstile, `captcha_type: cloudflare`, ожидание списка проектов, Замер: `freelancehunt.com/projects` за Cloudflare Turnstile, `captcha_type: cloudflare`, ожидание списка проектов,
@@ -73,9 +81,8 @@ docker compose exec api sh -c 'rm -rf /app/extra/user_data_dir/*'
на челлендже). на челлендже).
- Память — около **0,5–0,6 ГБ** на браузер. - Память — около **0,5–0,6 ГБ** на браузер.
- Правило подбора: `BROWSER_SLOTS ≈ число ядер`, `API_MEM_LIMIT ≈ 0,6 ГБ × BROWSER_SLOTS + 1 ГБ`. - Правило подбора: `BROWSER_SLOTS ≈ число ядер`, `API_MEM_LIMIT ≈ 0,6 ГБ × BROWSER_SLOTS + 1 ГБ`.
Пример: сервер 4 ядра / 8 ГБ → 3–4 слота; сейчас `BROWSER_SLOTS = 10` зашит в `src/main.py` и рассчитан Пример: сервер 4 ядра / 8 ГБ → 3–4 слота. `BROWSER_SLOTS` задаётся в `.env` (по умолчанию 10 — на ~10–12 ядер).
на ~10–12 ядер. - Сверх `BROWSER_SLOTS` запросы сразу получают `503` с `code: no_slots`, не ставятся в очередь.
- Сверх `BROWSER_SLOTS` запросы сразу получают `No slots available`, не ставятся в очередь.
Повторить замер: Повторить замер:
+2 -2
View File
@@ -1,8 +1,8 @@
# WebRobo API # Robotex
Сервис позволяет отработать на странице JS и продолжить парсинг. Полезно для ботов, которым нужно авторизоваться на сайте или пройти капчу, там где требуется рендеринг JS. Сервис позволяет отработать на странице JS и продолжить парсинг. Полезно для ботов, которым нужно авторизоваться на сайте или пройти капчу, там где требуется рендеринг JS.
Клиент присылает URL, свой прокси и список действий. Сервис открывает страницу в настоящем Chrome, проходит Клиент присылает URL, список действий и, при необходимости, свой прокси. Сервис открывает страницу в настоящем Chrome, проходит
антибот (Cloudflare Turnstile, reCAPTCHA), выполняет действия и возвращает cookies (включая `cf_clearance`), антибот (Cloudflare Turnstile, reCAPTCHA), выполняет действия и возвращает cookies (включая `cf_clearance`),
User-Agent и, по запросу, HTML. С этими cookies и UA клиент продолжает парсинг своим HTTP-клиентом. User-Agent и, по запросу, HTML. С этими cookies и UA клиент продолжает парсинг своим HTTP-клиентом.