Как решать капчу в фоновой задаче Trigger.dev

Решение капчи в Trigger.dev падает на первом же деплое по причине, которая никак не связана с кодом. Trigger.dev не обращается к вашему приложению. Он собирает вашу задачу в Docker-образ и запускает её на своих машинах, поэтому 127.0.0.1 внутри задачи указывает на этот контейнер, а не на машину с вашим решателем. Режим Server исправляет это одной настройкой. Вторым пунктом идёт maxDuration, потому что опрос решателя целиком засчитывается в этот бюджет.
Что понадобится
- Проект Trigger.dev с установленным SDK и файлом trigger.config.ts в корне.
- Запущенный CapSkip на машине с Windows и Node-клиент, добавленный в тот же проект, чтобы он попал в разворачиваемый образ.
- Sitekey и URL страницы, которые приходят в payload задачи, а не зашиты в коде, чтобы одна задача обслуживала любые формы.
- Режим Server и доступный извне адрес решателя. В Trigger.dev Cloud это не опция, а обязательное условие, по причине из шага 1.
# npm install capskip npm install @trigger.dev/sdk capskip
Шаг 1: где на самом деле выполняется задача и какой режим для этого нужен
В большинстве руководств по платформам этот вопрос можно оставить на конец. Здесь так не получится, потому что от него зависит, заработает ли всё остальное. Сам Trigger.dev описывает деплой прямо: код упаковывается в Docker-образ и разворачивается в вашем инстансе Trigger.dev, а каждый запуск выполняется в изолированной среде, которой управляет платформа. Ваша задача работает не там, где открыт ваш редактор.
Поэтому адрес обратной петли внутри функции run указывает на контейнер задачи. Порт 8080 там никто не слушает, и сбой выглядит как отказ в соединении, который на каждой попытке приходит в виде NetworkException.
Режима подключения два. Local привязывается к 127.0.0.1 и отвечает только этому устройству, и это правильный выбор, когда ваша автоматизация и решатель работают на одной машине. Server привязывается к вашему сетевому или публичному IP, поэтому другая машина, VPS или облачная платформа обращаются к той же машине с Windows по API. Режим Server меняет только то, какой адрес слушает решатель. Это по-прежнему ваше железо, и платы за каждое решение по-прежнему нет. Оба режима находятся в разделе Настройки подключения.
| Как вы запускаете Trigger.dev | Какой режим подключения |
|---|---|
| CLI в режиме dev на машине с CapSkip | Режим Local. 127.0.0.1 действительно верен |
| Self-hosted в вашей собственной сети | Server mode с локальным адресом решателя |
| Trigger.dev Cloud | Server mode со статическим публичным IP и правилом брандмауэра |
Для третьей строки стоит завести статический публичный IP, чтобы адрес не сменился под уже работающим деплоем. Храните адрес в переменной окружения, а не в исходном коде, потому что CLI в режиме dev и развёрнутая задача требуют разных значений.
// npm install capskip
import { CapSkip } from "capskip";
// 127.0.0.1 while the dev CLI runs it on your machine,
// the solver's reachable address once it is deployed.
export const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: Number(process.env.CAPSKIP_PORT ?? 8080),
recaptchaTimeout: 120,
});Шаг 2: maxDuration должен покрывать решение капчи
Trigger.dev измеряет запуск относительно maxDuration в секундах, а документированный минимум равен пяти. Исключения названы явно: время в wait.for, triggerAndWait и batchTriggerAndWait не учитывается. HTTP-запроса с await в этом списке нет, поэтому каждая секунда, которую клиент тратит на опрос решателя, засчитывается полностью.
Это важно, потому что решение капчи состоит в основном из ожидания. Решение reCAPTCHA v2 обычно занимает от пятнадцати до сорока пяти секунд, а при загруженной очереди и дольше. Задавайте maxDuration так, чтобы в бюджет входило само решение, а не только остальная часть задачи.
// trigger.config.ts sets the project-wide floor.
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "proj_YOUR_PROJECT_REF",
maxDuration: 60,
});
// A solving task overrides it. 60s is not enough on its own:
// the solve alone can use most of that budget.
export const solveAndSubmit = task({
id: "solve-and-submit",
maxDuration: 300,
run: async (payload) => { /* ... */ },
});Собственный потолок клиента держите ниже, чтобы клиент сдавался первым и выбрасывал понятную вам ошибку. По умолчанию Node-клиент ждёт 300 секунд для reCAPTCHA, Turnstile и GeeTest и 120 секунд для капчи-картинки. Если снизить клиентский тайм-аут для reCAPTCHA до 120 при maxDuration 300, останется запас на то, что задача делает с токеном дальше.
Стоит назвать одну ловушку. Поскольку wait.for исключён из maxDuration, он выглядит как бесплатный способ поставить задачу на паузу. Для токена он не бесплатный. Токен reCAPTCHA живёт около двух минут реального времени, а часы платформы и часы токена идут отдельно друг от друга. Решайте капчу и используйте токен в одном и том же участке кода, а перед этим прочитайте, сколько живёт токен reCAPTCHA и почему этот срок стоит учесть до того, как проектировать всё остальное.
Шаг 3: повторы и то, какие ошибки их заслуживают
По умолчанию задачи повторяются три раза с экспоненциальной задержкой, которая настраивается через factor, minTimeoutInMs, maxTimeoutInMs и randomize. Сгенерированный CLI конфиг отключает повторы в окружении DEV, и поэтому задача, которая в проде делает повторы, на вашей машине как будто падает мгновенно.
Повторная попытка выполняет функцию run целиком, поэтому капча решается заново. Унаследовать устаревший токен неоткуда, и значения по умолчанию из-за этого выглядят разумными. Настраивать стоит другое: какие именно сбои вообще заслуживают попытки.
| Какое исключение | Что это значит | Стоит ли повторять? |
|---|---|---|
| NetworkException | CapSkip был недоступен или перезапускается | Да. Именно для этого повторы и нужны |
| TimeoutException | Опрос вышел за собственный предел клиента | Один раз, возможно. Три попытки редко оправданы |
| ApiException | API вернул код ошибки | Зависит от кода ошибки. Обычно нет |
| ValidationException | Параметры были неверными и останутся такими же | Нет. Выбрасывайте AbortTaskRunError |
AbortTaskRunError завершает попытку неудачей и отключает повторы, а именно этого и заслуживает некорректный запрос. Неверный sitekey не станет верным с третьего раза, а три попытки по девяносто секунд каждая означают четыре с половиной минуты, потраченные на доказательство очевидного.
import { task, AbortTaskRunError } from "@trigger.dev/sdk";
import { ValidationException } from "capskip";
try {
const { code } = await solver.recaptcha(sitekey, pageUrl);
return await postForm(pageUrl, code);
} catch (err) {
// Wrong parameters will be wrong on all three attempts.
if (err instanceof ValidationException) {
throw new AbortTaskRunError(err.message);
}
throw err; // everything else takes the normal backoff
}Шаг 4: задача целиком
Всё описанное выше в одном файле. Клиент создаётся на уровне модуля, поэтому он собирается один раз на контейнер, а не на каждый запуск, и не хранит состояние отдельного запуска.
// npm install capskip
import { task, AbortTaskRunError } from "@trigger.dev/sdk";
import { CapSkip, ValidationException } from "capskip";
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: 8080,
recaptchaTimeout: 120,
});
export const submitSignup = task({
id: "submit-signup",
maxDuration: 300,
retry: { maxAttempts: 3, minTimeoutInMs: 2000 },
queue: { concurrencyLimit: 10 },
run: async (payload, { ctx }) => {
const { sitekey, pageUrl, email } = payload;
try {
// Solve and submit together. The token is short lived.
const { code } = await solver.recaptcha(sitekey, pageUrl);
const res = await postSignup(pageUrl, email, code);
return { status: res.status, runId: ctx.run.id };
} catch (err) {
if (err instanceof ValidationException) {
throw new AbortTaskRunError(err.message);
}
throw err;
}
},
});Этот вызов относится к reCAPTCHA v2. Остальные типы устроены так же: передайте invisible или enterprise со значением 1, либо version со значением v3 и действие, либо вызовите turnstile или geetest. Полный набор возможностей описан на странице сервиса распознавания капчи для Node.js.
Шаг 5: параллелизм и где находится настоящий потолок
Опция queue ограничивает, сколько запусков задачи выполняется одновременно. С тарифицируемым решателем это число по сути управляет расходами, и именно поэтому его ставят низким. Здесь же вопрос упирается в мощность одной машины, так что задавайте столько, сколько выдержат решатель и целевой сайт, а не столько, сколько вы готовы оплатить.
Ограничивают его на деле две вещи: машина с Windows, на которой работает CapSkip, и то, насколько быстро принимает запросы сайт, куда вы отправляете форму, прежде чем начнёт вас ограничивать. Второе обычно жёстче. Ничто не стоит в очереди за балансом и ничто не отваливается в конце месяца.
export const submitSignup = task({
id: "submit-signup",
// Sized for the solver machine and the target site,
// not for a credit balance.
queue: { concurrencyLimit: 10 },
maxDuration: 300,
run: async (payload) => { /* ... */ },
});Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Работает с CLI в режиме dev, а после деплоя появляется NetworkException | Развёрнутая задача выполняется в контейнере, поэтому адрес обратной петли ведёт в контейнер | Режим Server и заданный CAPSKIP_HOST для развёрнутого окружения |
| Запуск останавливается прямо посреди решения капчи | maxDuration короче, чем длится решение | Поднимите его у задачи выше собственного тайм-аута клиента |
| Задача мгновенно падает в DEV, но делает повторы в проде | Сгенерированный конфиг отключает повторы в DEV | Так и задумано. Проверяйте поведение повторов в развёрнутом окружении |
| Три попытки, одинаковый сбой, несколько минут потеряно | Ошибку в параметрах считают временной | Выбрасывайте AbortTaskRunError при ValidationException |
| Токен отклонён после wait.for | Ожидание бесплатно для maxDuration, но не для токена | Решайте капчу после ожидания, непосредственно перед отправкой |
| ERROR_WRONG_USER_KEY внутри ApiException | CAPSKIP_API_KEY не задан в развёрнутом окружении | Задайте его в переменных окружения Trigger.dev и выполните деплой заново |
| CAPCHA_NOT_READY при самописном опросе | Результат прочитали до того, как он был готов | Доверьте опрос клиенту. Он сам увеличивает интервал |
Последний ответ пишется именно так, как выглядит, и пропущенная буква не является опечаткой с нашей стороны: API действительно возвращает его в таком виде. Полностью это объясняется в руководстве по CAPCHA_NOT_READY.
FAQ
Может ли задача в Trigger.dev Cloud достучаться до решателя на моей собственной машине?
Да, в режиме Server. Задача выполняется в контейнере, которым управляет Trigger.dev, поэтому адрес обратной петли ведёт там в этот же контейнер. Привяжите CapSkip к своему публичному IP в настройках подключения, поставьте перед ним правило брандмауэра, которое пропускает только ожидаемые адреса, и задайте CAPSKIP_HOST в переменных окружения Trigger.dev. Статический публичный IP рекомендуется, чтобы адрес не сменился у вас под ногами.
Меняет ли что-нибудь самостоятельный хостинг Trigger.dev?
Меняется адрес, но не модель. При самостоятельном хостинге запуски по-прежнему выполняют ваш код в контейнерах на инстансе, а не обращаются к вашему приложению, поэтому адрес обратной петли по-прежнему ведёт в контейнер. Разница в том, что инстанс обычно находится в вашей собственной сети, поэтому в режиме Server можно использовать адрес в LAN вместо публичного, и ни одно правило брандмауэра не придётся открывать в интернет.
Стоит ли выносить решение капчи в отдельную задачу, которую вызывают другие задачи?
Обычно нет. При таком разделении токен пересекает границу задачи и лежит в payload, пока родительская задача возобновляется, а это самый быстрый способ потратить уже просроченный токен. Держите решение капчи и то, что использует токен, в одной функции run и возвращайте результат, а не сам токен. Отдельная задача имеет смысл только тогда, когда возвращает вовсе не токен.
Чем это отличается от того же самого в Inngest?
Модель развёртывания здесь прямо противоположная, и это меняет весь ответ. Inngest обращается к вашему приложению по HTTP, поэтому ваш код работает там, где вы его развернули, и режим подключения превращается в вопрос о вашем собственном хостинге. Trigger.dev запускает ваш код на своих машинах, поэтому в облачном варианте режим Server выбран за вас. Версия для Inngest, включая объяснение, почему решение капчи должно находиться внутри одного шага, разобрана в руководстве по Inngest.
Коротко
Запускайте CapSkip в режиме Server и задавайте CAPSKIP_HOST в окружении Trigger.dev, потому что развёрнутая задача выполняется в контейнере и адрес обратной петли ведёт туда же. Задайте задаче maxDuration, покрывающий решение капчи, поскольку опрос HTTP-эндпоинта не входит в число исключённых ожиданий. Тайм-аут клиента держите ниже него. Оставьте три повтора по умолчанию, но при ошибках в параметрах выбрасывайте AbortTaskRunError. Решайте капчу и отправляйте форму в одной функции run, никогда не разнося это через ожидание или границу задачи.
- Сырые эндпоинты, которые стоят за клиентом, описаны в документации CapSkip API.
- Сама задача с галочкой разобрана на странице сервиса распознавания reCAPTCHA v2.
Что стоит взвесить перед выбором лимита параллелизма: CapSkip обеспечивает распознавание капчи на оборудовании, которое у вас уже есть, поэтому выбранное число определяется запасом мощности, а не бюджетом.
