4 Commits
Author SHA1 Message Date
protokeyandClaude Opus 5.5 9d616e1f4f chore: make release — образы в реестр по умолчанию, архив по out=file
- out=registry (по умолчанию): release зависит от push, в build/<ver>/ —
  только compose, .env и README с docker compose pull.
- out=file: как раньше — образы и rabbitmq/redis/postgres в .tar.gz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:21:24 +04:00
protokeyandClaude Opus 5.5 0e10fb671d fix: reCAPTCHA — закрытие окна с картинками и вызов callback (#10)
- Окно с картинками закрывается кликом в угол подложки: в её центре лежит
  само окно и перехватывает клик (Locator.click падал по таймауту 60 с).
- form_submit выполняется в основном мире страницы (isolated_context=False):
  префикс "mw:" patchright не поддерживает, в изолированном мире
  ___grecaptcha_cfg не виден — "Recaptcha callback function not found".
- Воркер: LOG_LEVEL выставляется корневому логгеру явно — dramatiq настраивает
  логирование раньше, и basicConfig ничего не делал. make dev — LOG_LEVEL=DEBUG.
- test_main.http: запрос на zennolab с проверкой результата по #main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:17:06 +04:00
protokeyandClaude Opus 5.5 4d90dc4a21 fix: reCAPTCHA — окно с картинками определяется по положению (#10)
- Маркер интерактивного челленджа: iframe bframe, контейнер которого не стоит
  за экраном (top: -10000px); click_and_race ждёт видимости, а не появления в DOM.
- Маркер наличия капчи берётся из исходного HTML (api.js / enterprise.js,
  .g-recaptcha, iframe чекбокса), проверка мгновенная, без ожидания iframe.
- SolverReCaptcha ждёт detect_locator в DOM, а не видимости (script невидим).
- test_main.http: секреты вынесены в http-client.private.env.json, запрос
  на стенд zennolab, исправлены XPath с @class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:30:29 +04:00
protokeyandClaude Opus 5.5 fdee12e2d8 feature: make dev — api и worker на хосте с видимым окном Chrome (#90)
- docker-compose.dev.yml: порты rabbitmq, redis, postgres на 127.0.0.1 (5673, 6380, 5433 —
  стандартные часто заняты другими проектами)
- make dev: останавливает контейнерные api и worker (иначе worker в Docker тоже берёт задачи
  из общей очереди и выполняет их невидимо), поднимает инфраструктуру, запускает uvicorn --reload
  на :8010 и воркер dramatiq; Ctrl+C останавливает оба
- make dev-key name=...: API-ключ без контейнера api
- README: раздел Local development

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:01:05 +04:00
12 changed files with 161 additions and 43 deletions

No files matched your search

+1 -1
View File
@@ -12,5 +12,5 @@ tab_width = 4
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}]
indent_size = 2
[{*.yaml,*.yml,environment.yaml,environment.yml}]
[{*.yaml,*.yml,*.http,environment.yaml,environment.yml}]
indent_size = 2
+1
View File
@@ -9,3 +9,4 @@ build/
client/dist/
client/build/
client/src/*.egg-info/
http-client.private.env.json
+57 -11
View File
@@ -5,10 +5,10 @@ WHEELS_DIR := wheels
REQUIREMENTS := requirements.txt
ver ?= latest
MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push
MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push dev dev-deps dev-key
PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS))
.PHONY: install freeze wheels sync sync-offline clean help venv build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push
.PHONY: install freeze wheels sync sync-offline clean help venv build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push dev dev-deps dev-key
# образы сервисов: api — без Chrome, worker — Chrome + Xvfb; rabbitmq и redis — сторонние, кладутся в релиз
REGISTRY := docker.sokolab.xyz
@@ -18,6 +18,24 @@ DEPS_IMAGES := rabbitmq:4.1-alpine redis:8.2-alpine postgres:17-alpine
API_URL ?= http://localhost:8000
API_KEY ?=
# make dev (#90): rabbitmq, redis, postgres — в Docker с портами на 127.0.0.1 (docker-compose.dev.yml), api и worker —
# на хосте, Chrome открывается на рабочем столе. Порты нестандартные: 5672/5432/8000 часто заняты другими проектами.
DEV_COMPOSE := docker compose -f docker-compose.yml -f docker-compose.dev.yml
DEV_API_PORT ?= 8010
DEV_RABBITMQ_PORT ?= 5673
DEV_REDIS_PORT ?= 6380
DEV_POSTGRES_PORT ?= 5433
# окон Chrome на рабочем столе; потоков dramatiq — вдвое больше, как в Dockerfile
DEV_SLOTS ?= 2
RABBITMQ_PASSWORD ?= robotex
POSTGRES_PASSWORD ?= robotex
export DEV_RABBITMQ_PORT DEV_REDIS_PORT DEV_POSTGRES_PORT RABBITMQ_PASSWORD POSTGRES_PASSWORD
DEV_ENV := RABBITMQ_URL=amqp://robotex:$(RABBITMQ_PASSWORD)@localhost:$(DEV_RABBITMQ_PORT)/ \
REDIS_URL=redis://localhost:$(DEV_REDIS_PORT)/0 \
DATABASE_URL=postgresql+asyncpg://robotex:$(POSTGRES_PASSWORD)@localhost:$(DEV_POSTGRES_PORT)/robotex \
BROWSER_SLOTS=$(DEV_SLOTS) \
LOG_LEVEL=DEBUG
help:
@echo "make install <pkg> [<pkg> ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/"
@echo "make sync - установить все пакеты из $(REQUIREMENTS) (из индекса)"
@@ -27,10 +45,13 @@ help:
@echo "make clean - удалить $(WHEELS_DIR)/"
@echo "make build [ver=latest] - собрать образы $(API_IMAGE):<ver> и $(WORKER_IMAGE):<ver>"
@echo "make push ver=<ver> - собрать образы и отправить в $(REGISTRY)"
@echo "make release ver=<ver> - собрать образы и пакет для деплоя в build/<ver>/ (compose, .env, образы .tar.gz)"
@echo "make release ver=<ver> [out=registry|file] - собрать образы и пакет для деплоя в build/<ver>/ (compose, .env);"
@echo " образы: registry — push в $(REGISTRY) (по умолчанию), file — архив .tar.gz"
@echo "make up - собрать и запустить локально: api, worker, rabbitmq, redis"
@echo "make down - остановить локальный стенд"
@echo "make logs - логи api и worker"
@echo "make dev - api и worker на хосте с видимым Chrome, rabbitmq/redis/postgres в Docker; API на :$(DEV_API_PORT)"
@echo "make dev-key name=<клиент> - выпустить API-ключ клиенту при make dev"
@echo "make smoke API_KEY=... [API_URL=...] - проверить запущенный стенд: ключ, /v1/solve, /v2/tasks, удержание окна"
@echo "make key-create name=<клиент> - выпустить API-ключ клиенту (в запущенном compose)"
@echo "make key-list - клиенты и их ключи (без самих ключей)"
@@ -88,6 +109,21 @@ down:
logs:
docker compose logs -f api worker
# контейнерные api и worker останавливаются: иначе worker в Docker тоже берёт задачи из общей очереди и выполняет их невидимо
dev-deps:
-docker compose stop api worker
$(DEV_COMPOSE) up -d --wait rabbitmq redis postgres
# Ctrl+C останавливает оба процесса; инфраструктура остаётся запущенной (make down — остановить)
dev: dev-deps
cd src && export $(DEV_ENV) && \
{ $(PYTHON) -m uvicorn main:app --reload --port $(DEV_API_PORT) & api=$$!; \
trap 'kill $$api 2>/dev/null' EXIT INT TERM; \
$(PYTHON) -m dramatiq worker.actors --processes 1 --threads $$(( $(DEV_SLOTS) * 2 )); }
dev-key: dev-deps
cd src && $(DEV_ENV) $(PYTHON) -m db.cli create "$(name)"
smoke:
$(PYTHON) tests/smoke.py $(API_URL) $(API_KEY)
@@ -123,33 +159,43 @@ test-browser: build
xvfb-run -a --server-args="-screen 0 1920x1080x24" python -m pytest -m browser -p no:cacheprovider -s'
RELEASE_DIR := build/$(ver)
# куда деваются образы при make release: registry — push в $(REGISTRY), file — архив .tar.gz в $(RELEASE_DIR)/
out ?= registry
ifneq ($(filter release push,$(MAKECMDGOALS)),)
ifeq ($(ver),latest)
$(error Укажите версию: make release ver=v0.0.2)
endif
ifeq ($(filter $(out),registry file),)
$(error out=registry или out=file, а не "$(out)")
endif
endif
release: build
@echo "make release $(ver) -> $(RELEASE_DIR)/"
ifeq ($(out),file)
RELEASE_RUN := docker load -i robotex-$(ver).tar.gz
else
RELEASE_RUN := docker compose pull
endif
release: $(if $(filter file,$(out)),build,push)
@echo "make release $(ver) [out=$(out)] -> $(RELEASE_DIR)/"
@mkdir -p $(RELEASE_DIR)
sed -e 's|image: .*$(API_IMAGE):.*|image: $(REGISTRY)/$(API_IMAGE):$(ver)|' \
-e 's|image: .*$(WORKER_IMAGE):.*|image: $(REGISTRY)/$(WORKER_IMAGE):$(ver)|' \
docker-compose.prod.yml > $(RELEASE_DIR)/docker-compose.yml
cp dot_env $(RELEASE_DIR)/.env
ifeq ($(out),file)
# сторонние образы (rabbitmq, redis, postgres) кладутся в архив; при out=registry сервер берёт их с Docker Hub
for img in $(DEPS_IMAGES); do docker image inspect $$img >/dev/null 2>&1 || docker pull $$img; done
# TODO: я хочу парамтером управлять сохранить в файл или отправить в свой реджестри
# if produce == file:
docker tag $(API_IMAGE):$(ver) $(REGISTRY)/$(API_IMAGE):$(ver)
docker tag $(API_IMAGE):$(ver) $(REGISTRY)/$(API_IMAGE):$(ver)
docker tag $(WORKER_IMAGE):$(ver) $(REGISTRY)/$(WORKER_IMAGE):$(ver)
docker save $(REGISTRY)/$(API_IMAGE):$(ver) $(REGISTRY)/$(WORKER_IMAGE):$(ver) $(DEPS_IMAGES) | gzip > $(RELEASE_DIR)/robotex-$(ver).tar.gz
# else:
# docker push docker.sokolab.xyz/$(API_IMAGE):$(ver) docker.sokolab.xyz/$(WORKER_IMAGE):$(ver) docker.sokolab.xyz/$(DEPS_IMAGES)
endif
@printf '%s\n' \
'# Robotex $(ver)' \
'' \
'```bash' \
'docker load -i robotex-$(ver).tar.gz' \
'$(RELEASE_RUN)' \
'docker compose up -d' \
'```' \
'' \
+11 -7
View File
@@ -167,14 +167,17 @@ Not done yet:
## Local development
```bash
make sync-offline # venv in ~/.py_envs/robotex; another path: make VENV=... <target>
docker compose up -d rabbitmq redis # queue and storage
cd src && RABBITMQ_URL=... REDIS_URL=... uvicorn main:app --reload
cd src && dramatiq worker.actors --processes 1 --threads 4
make sync-offline # venv in ~/.py_envs/robotex; another path: make VENV=... <target>
make dev # api + worker on the host, Chrome windows visible on your desktop; API on :8010
make dev-key name=local # API key for it (stored in the same postgres volume as make up)
```
The compose network is not published, so for running the services on the host publish `5672` and `6379` yourself,
or just use `make up`. Chrome runs headful, so a display is required (locally — your desktop, in Docker — Xvfb).
`make dev` stops the containerized api and worker (otherwise the Docker worker also takes tasks from the common
queue and runs them invisibly), starts rabbitmq, redis and postgres with ports published on 127.0.0.1
(`docker-compose.dev.yml`: 5673, 6380, 5433), then runs uvicorn with `--reload` and the dramatiq worker in the
foreground. Ctrl+C stops both; `make down` stops the infrastructure. Ports, slots: `DEV_API_PORT`,
`DEV_RABBITMQ_PORT`, `DEV_REDIS_PORT`, `DEV_POSTGRES_PORT`, `DEV_SLOTS` (default 2). Chrome runs headful, so a display
is required (locally — your desktop, in Docker — Xvfb).
Tests:
@@ -201,7 +204,8 @@ Releases:
```bash
make build ver=v0.1.0 # docker images robotex-api:v0.1.0 and robotex-worker:v0.1.0
make release ver=v0.1.0 # images + deploy bundle in build/v0.1.0/ (compose, .env, images .tar.gz)
make release ver=v0.1.0 # push images to docker.sokolab.xyz + deploy bundle in build/v0.1.0/ (compose, .env)
make release ver=v0.1.0 out=file # same, but images go to build/v0.1.0/robotex-v0.1.0.tar.gz instead of the registry
```
Details (Xvfb, tini, `/dev/shm`, env settings, load measurements) — [docs/Deployment.md](docs/Deployment.md).
+15
View File
@@ -0,0 +1,15 @@
# Локальная разработка (make dev, #90): поверх docker-compose.yml открывает порты rabbitmq, redis и postgres
# на 127.0.0.1, чтобы api и worker запускались на хосте — с видимым окном Chrome на рабочем столе.
# Порты нестандартные: 5672/5432 на хосте часто заняты другими проектами.
services:
rabbitmq:
ports:
- "127.0.0.1:${DEV_RABBITMQ_PORT:-5673}:5672"
redis:
ports:
- "127.0.0.1:${DEV_REDIS_PORT:-6380}:6379"
postgres:
ports:
- "127.0.0.1:${DEV_POSTGRES_PORT:-5433}:5432"
+3 -1
View File
@@ -23,8 +23,10 @@ async def click_and_race(page, element, challenge_locator, timeout=30_000):
timeout=timeout,
)
)
# challenge_locator должен совпадать только с открытым окном челленджа: элементы челленджа могут быть в DOM
# заранее (reCAPTCHA держит их за экраном), поэтому ждём видимости, а не появления
challenge_task = asyncio.create_task(
challenge_locator.wait_for(state="attached", timeout=timeout)
challenge_locator.first.wait_for(state="visible", timeout=timeout)
)
await element.click()
+28 -12
View File
@@ -1,4 +1,3 @@
import json
import logging
from patchright.async_api import Page
@@ -6,12 +5,26 @@ from patchright.async_api import Page
from captcha.base import Captcha
from ..base import Detector, ChallengeInfo, Solver, AntiBotProvider, click_and_race
from ..base import Stage
from ..base import Stage, PWTimeoutError
from ..registry import register
from ..exceptions import IframeLoadError
log = logging.getLogger(__name__)
# iframe окна с картинками (bframe) вставляется в DOM вместе с чекбоксом, но его контейнер стоит за экраном:
# style="visibility: hidden; ... top: -10000px". Когда выпадает челлендж, контейнер переезжает на экран и
# становится видимым — локатор совпадает только с открытым окном
INTERACTIVE_MARKER_SELECTOR = (
"xpath=//iframe[contains(@src, '/bframe')][not(ancestor::div[contains(@style, 'top: -10000px')])]"
)
# признак reCAPTCHA, который есть уже в исходном HTML, до загрузки iframe с чекбоксом (он грузится долго):
# скрипт api.js / enterprise.js, контейнер .g-recaptcha (у render=explicit его может не быть) или сам iframe.
# #recaptcha-token не подходит — он внутри iframe чекбокса, с основной страницы его не видно
DETECT_MARKER_SELECTOR = (
"script[src*='/recaptcha/api.js'], script[src*='/recaptcha/enterprise.js'], "
".g-recaptcha, iframe[title='reCAPTCHA']"
)
JS_CHALLENGE_SUBMIT = """(token) => {
const clients = window.___grecaptcha_cfg && window.___grecaptcha_cfg.clients;
@@ -42,8 +55,8 @@ class ReCaptchaSolver(Solver):
self.captcha_solver: Captcha = self._opt["captcha_solver"]
async def form_submit(self, token: str) -> bool:
# noinspection UnnecessaryLabelJS
return await self.page.evaluate(f"mw:({JS_CHALLENGE_SUBMIT})({json.dumps(token)})")
# ___grecaptcha_cfg живёт в основном мире страницы, а patchright по умолчанию выполняет скрипт в изолированном
return await self.page.evaluate(JS_CHALLENGE_SUBMIT, token, isolated_context=False)
async def solve(self, page: Page, info: ChallengeInfo):
self.page = page
@@ -55,18 +68,22 @@ class ReCaptchaSolver(Solver):
log.debug("Нужно тыкнуть чекбокс")
checkbox_selector = "#recaptcha-anchor"
element = page.frame_locator("iframe[title='reCAPTCHA']").locator(checkbox_selector)
# TODO: маркер всегда присутствует
interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
challenge_locator = page.locator(interactive_marker_selector)
challenge_locator = page.locator(INTERACTIVE_MARKER_SELECTOR)
result = await click_and_race(page, element, challenge_locator)
log.debug(f"Ожидание завершено: {result}")
log.debug("Возврат к детекту")
return
else:
log.debug("Нужно решать капчу. Закрою окошко с картинками.")
# полупрозрачная подложка на весь экран (position: fixed), клик по ней закрывает окно. В её центре лежит
# само окно с картинками и перехватывает клик — поэтому кликаем в угол
element = page.locator("xpath=//div[@class='g-recaptcha-bubble-arrow']/../div[1]")
if await element.is_visible():
await element.click()
try:
await element.click(position={"x": 1, "y": 1}, timeout=5_000)
except PWTimeoutError:
# не закрылось — не страшно, токен уходит через callback
log.debug("Окошко с картинками закрыть не удалось")
log.debug("Решаем капчу...")
assert info.detect_locator
@@ -87,10 +104,9 @@ class ReCaptchaDetector(Detector):
log.debug("ReCaptchaDetector: попытка определить есть ли преграда")
info.meta = {}
checkbox_selector = "#recaptcha-anchor"
interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
# попытка определить что мы вообще на странице блокировки
marker_selector = info.detect_locator or "#captcha-form"
marker_selector = info.detect_locator or DETECT_MARKER_SELECTOR
info.detect_locator = marker_selector
log.debug(f"ищем по detect: {marker_selector} success: {info.success_locator}")
@@ -111,7 +127,7 @@ class ReCaptchaDetector(Detector):
log.debug("Вижу что это страница с проверкой. Определяю состояние.")
iframe = page.frame_locator("iframe[title='reCAPTCHA']")
# ожидаем когда будет загружен чекбокс
# маркер есть — значит капча будет, ждём пока догрузится iframe с чекбоксом
info.stage = Stage.INTERSTITIAL
checkbox = iframe.locator(checkbox_selector)
@@ -126,7 +142,7 @@ class ReCaptchaDetector(Detector):
log.debug("Чек бокс найден")
info.stage = Stage.CHECKBOX
if await page.locator(interactive_marker_selector).count() > 0:
if await page.locator(INTERACTIVE_MARKER_SELECTOR).first.is_visible():
log.debug("Нужно решать капчу")
# нужно решение
info.stage = Stage.INTERACTIVE
+1
View File
@@ -30,6 +30,7 @@ class Captcha:
return resp
async def turnstile(self, url, site_key):
# TODO: not finished!
resp = await self._run(self._solver.turnstile, site_key, url)
return resp
+2 -2
View File
@@ -25,8 +25,8 @@ class SolverReCaptcha(Captcha):
:param page:
:return:
"""
div_container = page.locator(selector)
await div_container.wait_for()
# selector может указывать на невидимый элемент (script) и совпадать с несколькими
await page.locator(selector).first.wait_for(state="attached")
site_key_input_element = page.locator("iframe[title='reCAPTCHA']")
src_value = await site_key_input_element.get_attribute("src")
+4 -1
View File
@@ -10,10 +10,13 @@ import logging
import os
import socket
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "INFO").upper(),
level=LOG_LEVEL,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
# под dramatiq логирование уже настроено им самим (уровень INFO) и basicConfig ничего не делает — уровень явно
logging.getLogger().setLevel(LOG_LEVEL)
import dramatiq # noqa: E402
from dramatiq.asyncio import get_event_loop_thread # noqa: E402
@@ -0,0 +1,7 @@
{
"dev": {
"api_key": "rbx_...",
"proxy": "http://user:pass@host:port",
"captcha_key": "<ключ 2captcha>"
}
}
+31 -8
View File
@@ -1,4 +1,6 @@
# Test your FastAPI endpoints
# Секреты (api_key, proxy, captcha_key) — в tests/http-client.private.env.json (не в git, шаблон —
# http-client.private.env.example.json); окружение dev выбирается в PyCharm над запросом.
GET http://127.0.0.1:8000/
Accept: application/json
@@ -13,7 +15,7 @@ Accept: application/json
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
{
@@ -36,7 +38,7 @@ X-API-Key: <key>
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
@@ -53,7 +55,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
@@ -72,7 +74,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
@@ -91,7 +93,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
{
@@ -112,11 +114,11 @@ X-API-Key: <key>
POST http://localhost:8002/v2/tasks
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
{
"url": "https://freelancehunt.com/",
"proxy": "http://BQLo3X:poqAWX@168.81.43.154:8000",
"proxy": "{{proxy}}",
"actions": [
{
"type": "inner_text",
@@ -132,6 +134,27 @@ X-API-Key: <key>
GET http://localhost:8002/v2/tasks/4d8105f8-93a8-43ee-aa6e-db917451363b
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
###
# @timeout 600
POST http://localhost:8010/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: {{api_key}}
{
"url": "https://lessons.zennolab.com/captchas/recaptcha/v2_nosubmit.php?level=low",
"proxy": "{{proxy}}",
"actions": [
{
"type": "inner_text",
"selector": "//div[@id='main']/div/h2"
}
],
"captcha": "{{captcha_key}}",
"captcha_type": "recaptcha",
"captcha_success_locator": "//div[@id='main']"
}
###