Files
robotex/docs/API.md
T
protokeyandClaude Opus 5.5 1a4bd8d672 feature: API-ключи клиентов и журнал использования в Postgres (#4, #17)
- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests.
- Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без
  выпущенных ключей доступа нет. В базе — только sha256 ключа.
- Задача и окно сессии привязаны к клиенту: чужой ключ их не видит.
- Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу
  (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу.
- python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats.
- smoke и loadtest с ключом.

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

233 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API
Как встраивать API в свой клиент (UA, прокси, сессии, повторы) — [Usage.md](Usage.md).
Задача открывает страницу в Chrome (через прокси клиента, если он задан), при необходимости проходит антибот
(Cloudflare Turnstile, reCAPTCHA), выполняет список действий и возвращает cookies, результаты действий и, по запросу,
HTML и скриншот. Задачи выполняет сервис worker из очереди; API работает в двух режимах:
* `POST /v1/solve` — синхронно: ответ приходит, когда задача выполнена;
* `POST /v2/tasks` + `GET /v2/tasks/{id}` — асинхронно, с возможностью держать окно браузера открытым между
задачами (см. [API v2](#api-v2)).
`GET /health` (и `GET /`) — `{"status": "ok", "browser_slots": N, "free_slots": M, "workers": [...]}`: слоты всех
живых воркеров; `workers` — по каждому `worker_id`, `browser_slots`, `free_slots`, `windows` (открытые окна),
`running` (выполняемые задачи).
## Авторизация
Все эндпоинты, кроме `/health`, требуют ключ клиента в заголовке `X-API-Key`. Без ключа, с неверным или отозванным
ключом — `401`. Ключи выпускает администратор (`make key-create name=<клиент>`, см. [Deployment.md](Deployment.md#клиенты-и-ключи)).
Задача и окно сессии видны только клиенту, который их создал: с другим ключом `GET /v2/tasks/{id}` даёт `404`, а
чужое открытое окно не используется.
## Запрос
```http
POST /v1/solve
Content-Type: application/json
X-API-Key: <key>
Cookie: session_id=<id> # необязательно, см. «Сессии»
{
"url": "https://freelancehunt.com/projects",
"proxy": "http://user:pass@host:port",
"captcha_type": "cloudflare",
"actions": [
{"type": "wait_for", "selector": "a[href*=\"/project/\"]", "timeout": 60}
],
"html": true,
"timeout": 120
}
```
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
| `url` | string | — | **обязательно**. Страница, которую открываем |
| `proxy` | string | `null` | `scheme://[user:pass@]host:port`, схема `http`, `https` или `socks5` (Chrome не поддерживает socks5 с логином/паролем). По exit-IP прокси выставляются locale и timezone браузера; без прокси — настройки браузера по умолчанию |
| `actions` | array | — | **обязательно**, может быть пустым. Действия, см. ниже |
| `captcha_type` | string | `null` | провайдер антибота: `cloudflare` или `recaptcha`. Без него шаг антибота пропускается — для сайтов за Cloudflare обязательно |
| `captcha` | string | `null` | ключ сервиса распознавания (2captcha), если антибот не проходится без решения капчи |
| `captcha_detect_locator` | string | `null` | локатор, по которому определяется, что на странице антибот |
| `captcha_success_locator` | string | `null` | локатор, по которому определяется, что антибот пройден |
| `html` | bool | `false` | вернуть HTML страницы после выполнения действий |
| `screenshot` | bool | `false` | вернуть скриншот видимой области (PNG, base64) после действий — в том числе при ошибке действия или антибота |
| `timeout` | int, с | `60` | таймаут навигации |
| `cookies` | array | `null` | cookies для установки до навигации, формат Playwright: `{name, value, domain, path, expires, httpOnly, secure, sameSite}` или `{name, value, url}`. **Заменяют** cookies профиля: перед установкой все cookies профиля удаляются (`[]` — просто очистить). Без поля cookies профиля сохраняются |
| `disable_resources` | bool | `false` | не грузить картинки, шрифты, медиа |
| `blocked_domains` | array | `null` | домены, запросы к которым блокируются |
| `retries` | int | `1` | попыток навигации |
| `retry_delay` | float, с | `1.0` | пауза между попытками |
| `wait` | int, мс | `0` | пауза после загрузки страницы |
| `google_search` | bool | `false` | выставить Referer как переход из поиска Google |
### Действия
Выполняются по порядку после прохождения антибота. Если антибот не пройден, действия не выполняются
(`code: challenge_failed`). На первой ошибке действия выполнение останавливается.
Обязательные поля проверяются до запуска браузера: неизвестный `type`, нет `selector`/`value`, `captcha` без ключа
в запросе, неверный `proxy` — ответ `422` с описанием в `detail` (стандартный формат FastAPI).
| `type` | Поля | Что делает |
|---|---|---|
| `wait_for` | `selector`, `timeout` (1–7200 с, по умолчанию 30) | ждёт появления элемента; при таймауте — ошибка действия |
| `click` | `selector` | клик по элементу |
| `submit` | `selector` | клик и ожидание навигации |
| `fill` | `selector`, `value` | мгновенная вставка значения в поле |
| `typing` | `selector`, `value` | ввод как у человека: подвод курсора, клик, очистка поля, посимвольный набор с паузами 50–200 мс; медленнее `fill`, но не выдаёт автоматизацию |
| `inner_text` | `selector` | возвращает текст элемента (HTML без тегов) в `result` |
| `mouse_move` | `selector` | наведение курсора на элемент; нет элемента — ошибка `selector not found` |
| `delay` | `value` (секунды строкой, 0–600) | пауза |
| `inner_html` | `selector` | возвращает HTML элемента в `result`; нет элемента — ошибка `selector not found` |
| `captcha` | `selector`, `value` (тип: `cloudflare`, `recaptcha`, `hcaptcha`) | решить капчу внутри страницы через 2captcha, нужен `captcha` в запросе |
| `pass_challenge` | `selector` (локатор успеха, необязательно) | повторно пройти антибот, например после перехода на другую страницу; если не пройден — ошибка действия |
## Ответ
Формат один для всех случаев: `status` — `"ok"` или `"err"`, при ошибке — `code` и `error`. Поля без значения не
возвращаются.
Успех (антибот пройден, действия выполнены), HTTP `200`:
```json
{
"status": "ok",
"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", "...": "..."}],
"actions": [
{"type": "wait_for", "ok": true, "duration_ms": 14}
],
"html": "<head>...</head><body>...</body>",
"duration_ms": 6405
}
```
| Поле | Описание |
|---|---|
| `status` | `"ok"` — антибот пройден и все действия выполнены, иначе `"err"` |
| `code` | при `"err"`: причина, см. таблицу ниже |
| `error` | при `"err"`: текст ошибки; для упавшего действия — `<type>: <текст>` |
| `user_agent` | родной UA браузера, с которым проходили антибот. Используйте его в своём HTTP-клиенте вместе с `cf_clearance` (см. [Usage.md](Usage.md)) |
| `cookies` | все cookies контекста |
| `actions` | по элементу на выполненное действие: `type`, `ok`, `result` (результат или текст ошибки), `duration_ms` |
| `html` | только при `"html": true` |
| `screenshot` | только при `"screenshot": true`: PNG в base64 |
| `duration_ms` | общее время обработки запроса |
| `code` | HTTP | Когда | Что ещё есть в ответе |
|---|---|---|---|
| `no_slots` | `503` + `Retry-After` | только `/v1/solve`: все слоты браузеров заняты, или воркер не взял задачу за `V1_QUEUE_WAIT` секунд (задача отменяется) | — |
| `request_failed` | `200` | ошибка до действий: навигация, прокси, неизвестный `captcha_type` | `actions: []` |
| `challenge_failed` | `200` | антибот не пройден за отведённое время, действия не выполнялись | cookies, html, screenshot |
| `action_failed` | `200` | упало действие, выполнение остановлено на нём | cookies, html, screenshot, `actions` до упавшего включительно |
Примеры:
```json
{"status": "err", "code": "action_failed", "error": "inner_html: selector not found: #missing",
"actions": [{"type": "inner_html", "ok": false, "result": "selector not found: #missing", "duration_ms": 26}], "...": "..."}
```
```json
{"status": "err", "code": "challenge_failed", "error": "cloudflare not passed: timeout (stage: checkbox)", "actions": [], "...": "..."}
```
```json
{"status": "err", "code": "no_slots", "error": "No slots available"}
```
## Сессии
Ответ ставит cookie `session_id` (UUID). Если передать её в следующем запросе, браузер откроется с тем же профилем
(`extra/user_data_dir/<session_id>`): cookies, localStorage и отпечаток сохраняются, повторно проходить Cloudflare
обычно не нужно. Без cookie или с некорректным значением (не UUID) запрос получает новый профиль.
Сессии изолированы друг от друга: у каждой свой профиль Chrome — cookies, localStorage, кэш.
Отпечаток профиля генерируется при первом запросе сессии и дальше не меняется (`robotex_session.json` в каталоге
профиля):
* размер экрана и `devicePixelRatio` десктопного Chrome на Linux из реального распределения (browserforge) —
нативной эмуляцией Chrome;
* шум canvas: свой seed у каждого профиля, поэтому хеш canvas у профилей разный, а внутри профиля стабильный.
Меняется младший бит у редких непрозрачных пикселей при чтении canvas (`toDataURL`, `toBlob`, `getImageData`);
пустой canvas не меняется.
Остальное — WebGL, `hardwareConcurrency`, шрифты, UA — не подменяется: это родные значения Chrome, за маскировку
автоматизации отвечает Patchright. Полная JS-подмена отпечатка ломала прохождение Cloudflare.
User-Agent и размер окна запросом не задаются — это родные значения браузера (почему — [Usage.md](Usage.md#что-задаёт-сервис-а-не-клиент)).
Поле `user_agent` в запросе даёт `422`.
Задачи одной сессии выполняются по очереди: профиль Chrome нельзя открыть дважды. Вторая задача ждёт, пока
закончится первая.
## API v2
Асинхронный режим: задача ставится в очередь, клиент опрашивает её по ссылке. Тело задачи — как у `/v1/solve`, плюс:
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
| `browser_idle_timeout` | int, мин | `0` | держать окно браузера открытым после задачи столько минут простоя, `0`–`120`. `0` — закрыть сразу, как в v1 |
| `url` | string | `null` | необязателен, если окно сессии открыто: тогда действия выполняются на текущей странице, без навигации |
### POST /v2/tasks
```http
POST /v2/tasks
Content-Type: application/json
Cookie: session_id=<id> # необязательно
{"url": "https://freelancehunt.com/projects", "captcha_type": "cloudflare", "actions": [], "browser_idle_timeout": 10}
```
Ответ `202` и cookie `session_id`:
```json
{"task_id": "0b9c…", "status": "queued", "url": "/v2/tasks/0b9c…"}
```
Без `url` и без открытого окна сессии — `422`.
### GET /v2/tasks/{id}
Только с cookie `session_id` той сессии, в которой задача создана; иначе, а также для несуществующей или истёкшей
задачи — `404`.
```json
{"task_id": "0b9c…", "status": "done", "result": {"status": "ok", "user_agent": "…", "cookies": […], "actions": […], "duration_ms": 7120}}
```
| `status` | Значение |
|---|---|
| `queued` | ждёт свободного слота браузера |
| `running` | выполняется |
| `done` | готово, ответ в `result` — в том же формате, что ответ `/v1/solve` (со своим `status` `ok`/`err`) |
| `failed` | задачу не удалось выполнить (например, воркер упал дважды), причина в `error` |
Результат хранится `RESULT_TTL_HOURS` часов (по умолчанию 24), затем `404`. Опрашивать раз в 0,5–2 секунды.
### Окно браузера между задачами
С `browser_idle_timeout > 0` Chrome сессии не закрывается после задачи. Следующая задача с той же cookie
`session_id` выполняется в этом окне: без запуска Chrome и обычно без повторного антибота, а без `url` — прямо на
текущей странице (например, продолжить заполнение формы). Каждая задача в окно заново отсчитывает простой — по
своему `browser_idle_timeout`; задача с `0` закрывает окно после себя.
* Открытое окно занимает слот браузера, пока не закроется. Не держите окна дольше, чем нужно.
* Задача с другим `proxy`, чем у открытого окна, перезапускает Chrome (прокси задаётся при запуске).
* `cookies` в задаче заменяют cookies открытого окна, как и при запуске.
* Если задача в окне упала с `request_failed`, окно закрывается; следующая задача откроет его заново из профиля.
### DELETE /v2/sessions/current
Закрыть окно сессии (по cookie `session_id`) досрочно и освободить слот. Если в окне идёт задача — сразу после неё.
Ответ `{"closed": true}`, или `{"closed": false}`, если окна нет.
## Производительность
Каждое окно — отдельный Chrome: около 0,6 ГБ памяти и до 3–4 ядер на пике при прохождении Cloudflare.
Замеры и рекомендации по лимитам — в [Deployment.md](Deployment.md#нагрузка).