Как решить Capy Puzzle в Node.js и сохранить сессию

solve capy puzzle in node.js - How to Solve Capy Puzzle in Node.js and Keep the Session

Чтобы решить Capy Puzzle в Node.js, загрузите страницу и вытащите ключ PUZZLE_ из скрипта виджета. Передайте его вместе с URL страницы в метод capy пакета capskip. Затем отправьте три возвращённых значения в полях capy_captchakey, capy_challengekey и capy_answer, все вместе и сразу же, в рамках той же сессии, в которой загружалась страница. Именно на последнем пункте код на Node обычно и ломается. Встроенный fetch в Node не хранит cookies и не отправляет Referer. В итоге отправка формы приходит без сессии, которую открыла страница, и сайт её отклоняет, каким бы верным ни был ответ. CapSkip добавил Capy Puzzle в версии 1.4.0. В этом руководстве разобраны ключ, вызов, удержание ответа на две секунды, отправка, повторы и одновременный запуск множества входов в аккаунт.

Что понадобится

  • CapSkip 1.4.0 или новее на машине с Windows. Именно в этом выпуске появился Capy Puzzle, а вместе с ним CaptchaFox и Friendly Captcha.
  • Node.js 20.18.1 или новее: это самый старый выпуск, который поддерживает cheerio. В новом проекте используйте 22 или 24, потому что срок поддержки Node 20 закончился в апреле 2026 года.
  • Пакет capskip версии 1.3.0 или новее (первый выпуск, в котором есть метод capy), а также cheerio для чтения формы.
  • URL страницы, на которой показана головоломка. Ключ, хост Capy и поля формы берутся именно с этой страницы.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства; в режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
# capskip 1.3.0 added capy(); cheerio reads the form
npm install capskip cheerio

Примеры написаны как ES-модули с await верхнего уровня. Сохраняйте их с расширением .mjs или задайте полю type в своём package.json значение module, потому что свежие версии npm init записывают туда commonjs, а package.json без поля type тоже считается CommonJS. Если ваш проект на CommonJS, пакет capskip загружается и через require.

Шаг 1: загружаем страницу, сохраняем её cookies и находим ключ

Формы с Capy обычно стоят на странице входа или регистрации, которая устанавливает сессионный cookie и кладёт в форму CSRF-токен. В Node fetch не хранит cookies между вызовами, поэтому к моменту отправки этот cookie уже потерян, и токен больше ни с чем не совпадает. Переносите cookies сами. Headers.getSetCookie() возвращает каждую строку Set-Cookie отдельно, и это всё, что нужно небольшой вспомогательной функции addCookies ниже; в MDN этот метод описан на странице Headers.getSetCookie().

// npm install capskip cheerio
import * as cheerio from "cheerio";

const PAGE_URL = "https://example.com/login";

// fetch() keeps no cookies between calls, so carry them yourself.
const jar = new Map();
function addCookies(res) {
  for (const line of res.headers.getSetCookie()) {
    const pair = line.split(";")[0];
    const eq = pair.indexOf("=");
    if (eq > 0) jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
  }
}

const page = await fetch(PAGE_URL);
addCookies(page);
const html = await page.text();
const $ = cheerio.load(html);

// The widget script's k= parameter, or a capy_captchakey input.
const key = html.match(/PUZZLE_[A-Za-z0-9_-]+/)?.[0];
// Everything before /puzzle/get_js/ is the Capy host.
const apiServer = html.match(/(https:\/\/[^"'\s<>]+?)\/puzzle\/get_js\//)?.[1];
// The form that holds the widget script or a capy_captchakey input.
const form = $("form")
  .has('script[src*="/puzzle/get_js/"], input[name="capy_captchakey"]').first();
console.log(key, apiServer ?? "default host", form.length);

Ключ публичен и одинаков для всех посетителей, а ключ головоломки начинается с PUZZLE_. На большинстве страниц он стоит в параметре k в URL скрипта виджета, а этот URL есть в HTML, который скачивает fetch. Некоторые страницы также выводят в форме скрытое поле capy_captchakey, а после того как виджет отработал в браузере, это поле есть всегда, и именно там его показывают DevTools. Одно регулярное выражение находит ключ в любом из этих мест.

Заодно запомните хост скрипта. Всё, что стоит в URL скрипта до /puzzle/get_js/, указывает на Capy API, за которым стоит ключ, и CapSkip называет его api_server. По умолчанию это https://jp.api.capy.me, где и работает действующий сервис, поэтому он нужен, только если страница загружает виджет откуда-то ещё. В примерах, написанных для других решателей, часто указан api.capy.me без регионального префикса, а этот хост больше не резолвится, так что уберите эту настройку, если скопировали такой пример. Регулярное выражение ожидает URL с https://; если src начинается с //, допишите https: перед хостом, прежде чем его передавать.

Строка с form выбирает форму, в которой есть скрипт виджета или поле capy_captchakey. Если страница загружает скрипт в head или ниже формы, а поля capy_captchakey внутри формы нет, form.length остаётся равным 0. В таком случае выбирайте форму по её id или action.

Шаг 2: вызов solver.capy()

Чтобы решить Capy Puzzle в Node.js, нужен один метод. Он принимает ключ, URL страницы и необязательный объект опций, а для страницы на хосте по умолчанию ему хватает первых двух.

// npm install capskip
import { CapSkip } from "capskip";

const solver = new CapSkip({
  host: "127.0.0.1",
  port: 8080,
  // Capy answers after a two second hold; poll often enough to catch it.
  pollingInterval: 0.5,
});

// undefined (no widget script in the HTML) is dropped, so CapSkip
// uses its default host.
const result = await solver.capy(key, PAGE_URL, { apiServer });

console.log(result.captchakey);     // goes in capy_captchakey
console.log(result.challengekey);   // goes in capy_challengekey
console.log(result.answer);         // goes in capy_answer

Результат представляет собой обычный объект. Читайте три именованных поля. В result.code лежит весь ответ сервера, объект со всеми четырьмя полями, что удобно для логирования, а result.respKey содержит пустую строку, которая нужна только для совместимости с другими сервисами. Ответ (answer) представляет собой длинную строку, которая начинается примерно так: 0xax8ex0xax84x. Это траектория перетаскивания, которую виджет записал бы при движении фрагмента, а не координата.

Опции, которые принимает метод

apiServer является псевдонимом api_server, а метод отбрасывает любую опцию со значением undefined или null, поэтому вызов выше может каждый раз передавать результат регулярного выражения. Метод также принимает объект proxy с ключами type и uri, а ещё useragent, который отправляется в единственном запросе, которым CapSkip загружает головоломку. Есть и опция version, но принимается только значение puzzle. Другое семейство Capy, avatar, представляет собой иное задание за другим эндпоинтом, поэтому SDK отклоняет его, а не возвращает ответ, который сайт отверг бы.

На двух опциях отдельного вызова люди часто спотыкаются. Параметр timeout в секундах заменяет defaultTimeout клиента для этого одного решения. Потолок интервала опроса тоже можно задать на один вызов, но только как polling_interval. pollingInterval в camelCase относится к конструктору, и если передать его в solver.capy(), метод выбросит ValidationException с названием этой опции. TypeScript вас не предупредит, потому что тип опций принимает любой ключ. Пустой ключ или URL, неизвестная опция и тип прокси, отличный от HTTP, HTTPS, SOCKS5 или SOCKS5H, тоже вызывают ValidationException ещё до того, как запрос покинет вашу машину.

Почему решение Capy занимает около двух секунд

Само распознавание происходит быстро. CapSkip загружает изображение головоломки, находит вырез с помощью несложной попиксельной математики и строит траекторию перетаскивания, без всякого браузера и без модели. А затем намеренно ждёт.

Capy измеряет время между выдачей головоломки и получением ответа и отклоняет всё, что приходит быстрее, чем человек успел бы перетащить фрагмент. По собственным тестам CapSkip на Capy эта нижняя граница составляет около одной секунды, а отказ приходит с тем же сообщением, что и при неверном ответе, так что верное решение, доставленное слишком рано, выглядит в точности как сломанный решатель. Поэтому CapSkip придерживает каждый результат Capy, пока с момента получения головоломки не пройдут две секунды. Ожидание сделано через sleep: оно добавляет задержку, но не тратит CPU.

С настройками по умолчанию вызов занимает ближе к четырём секундам, и причина в опросе. SDK запрашивает результат сразу, как только задача принята, а затем ещё раз через четверть секунды. Дальше он удваивает интервал до значения pollingInterval, по умолчанию 5 секунд. Опросы приходятся на 0, 0.25, 0.75, 1.75 и 3.75 секунды, так что ответ, готовый через две секунды, забирается на пятом опросе. С pollingInterval: 0.5 в конструкторе, как в примере выше, в наших тестах вызов возвращался примерно через 2.3 секунды после отправки задачи. Не добавляйте собственную задержку перед отправкой формы и не пытайтесь срезать удержание: именно оно позволяет ответу пройти проверку.

Шаг 3: отправляем все три значения вместе с cookies страницы

Три значения идут в те поля, которые виджет сам записал бы в форму. Отправляйте их вместе, одним запросом, с cookies из шага 1 и страницей в качестве Referer. Кроме того, отправляйте форму с redirect, равным manual, чтобы сессия, которую вернёт сайт, осталась у вас.

// The form's own fields, hidden CSRF token included.
const body = new URLSearchParams(
  form.serializeArray().map((f) => [f.name, f.value]),
);
// Use the page's own field names, password included.
body.set("username", "YOUR_USERNAME");
body.set("capy_captchakey", result.captchakey);
body.set("capy_challengekey", result.challengekey);
body.set("capy_answer", result.answer);

const res = await fetch(new URL(form.attr("action") || PAGE_URL, PAGE_URL), {
  method: "POST",
  headers: {
    cookie: [...jar].map(([k, v]) => `${k}=${v}`).join("; "),
    // fetch sends no Referer; strict servers refuse a post without one.
    referer: PAGE_URL,
  },
  body,
  // A login answers 302 with a new session cookie; keep it.
  redirect: "manual",
});
addCookies(res);
console.log(res.status, res.headers.get("location"));

serializeArray собирает поля, которые отправил бы браузер, включая скрытый CSRF-токен, так что называть их вам не придётся. Кнопку отправки он не включает, поэтому если сервер проверяет имя кнопки, добавьте его через body.set. Кроме того, body.set заменяет поле capy_captchakey, которое страница вывела сама, так что ключ никогда не уходит дважды. Referer важен не меньше, чем cookie. fetch не отправляет ни Referer, ни Origin, а некоторые фреймворки, в том числе Django, отклоняют отправку формы по HTTPS, в которой нет ни того, ни другого, даже с правильными cookie, токеном и ответом.

Настройка redirect сохраняет сессию после успешного входа. На вход сайт обычно отвечает 302 с новым сессионным cookie, потому что фреймворки вроде Django в этот момент обновляют сессию. Если ничего не менять, fetch идёт по редиректу без этого cookie, снова попадает на страницу входа и сообщает 200. С redirect, равным manual, вы получаете сам ответ 302, а addCookies кладёт новый cookie в хранилище для следующих запросов.

Отправляйте ответ ровно в том виде, в каком он вернулся. Бэкенд сайта сверяет его с выданной головоломкой, поэтому обрезка или пересборка делают его недействительным. URLSearchParams применяет к телу процентное кодирование, и это нормально, потому что сервер декодирует его перед проверкой. И отправляйте сразу. CapSkip генерирует новый ключ задания для каждого решения, а Capy привязывает выданную головоломку к этому ключу. Поэтому ключ одноразовый и живёт недолго, а одно решение покрывает одну отправку. Некоторые страницы отправляют данные собственным скриптом в теле JSON, а не обычной отправкой формы. Отправьте форму один раз вручную с открытыми DevTools и поставьте три значения туда, куда их ставит страница.

Шаг 4: повторы при неудачном решении

Когда вы решаете Capy Puzzle в Node.js в сколько-нибудь заметном объёме, часть решений будет проваливаться. Неудачное решение приходит в виде ApiException, в сообщении которого есть ERROR_CAPTCHA_UNSOLVABLE, и через res.php, эндпоинт, который опрашивает SDK, этот единственный код покрывает две разные ситуации. Промах мимо выреза случается изредка, а следующая попытка получает совершенно новую головоломку на другой фотографии, поэтому обычно проходит. Ключ, который отклоняет Capy API, не проходит ни при одной попытке, а счётчик Bad Keys на карточке Capy во вкладке Monitor в CapSkip растёт. Первое повторяйте, второе исправляйте.

import { ApiException } from "capskip";

async function solveCapy(key, url, options = {}, attempts = 3) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await solver.capy(key, url, options);
    } catch (err) {
      // A missed hole reads as unsolvable, and the next try draws a
      // new puzzle. Anything else, such as a wrong API key, is final.
      const miss = err instanceof ApiException && err.message.includes("UNSOLVABLE");
      if (!miss || attempt === attempts) throw err;
    }
  }
}

Сообщение ApiException представляет собой сырой ответ CapSkip, поэтому проверка читает код ошибки прямо из него. Отклонённый ключ тоже выглядит как нерешаемый, поэтому и он здесь уходит в повтор, но падает одинаково при каждой попытке: по этому признаку его и можно узнать. Другой путь даёт параметр Retries в разделе Capy в настройках CapSkip: он принимает значения от 0 до 3 и по умолчанию равен 0. Тогда CapSkip сам повторяет попытку после промаха мимо выреза, но никогда не повторяет отклонённый ключ, потому что его отклонят точно так же.

Метод capy опрашивает результат с таймаутом клиента defaultTimeout, 120 секунд, потому что решение Capy сводится к одной загрузке и небольшой арифметике, а не к браузерной сессии. У CapSkip есть и собственный отсчёт времени. В его настройках Capy задача может ждать до 250 секунд (Wait Timeout) одного из 10 потоков (Max. Threads), а на одну попытку отводится 60 секунд (Row Timeout). Обычное решение укладывается во всё это с многократным запасом. Кроме того, SDK продолжает опрос сквозь ошибки соединения, поэтому если CapSkip остановится посреди решения, вызов закончится TimeoutException, а не NetworkException.

Полный рабочий пример

Вот весь сценарий в одном файле. logIn загружает страницу в собственное хранилище cookie, читает ключ и хост, решает головоломку с повторами и отправляет форму с её cookies и Referer. Функция возвращает ответ и хранилище, так что запросы, которые вы делаете после входа, несут новую сессию.

// npm install capskip cheerio
// login.mjs: run with node login.mjs
import * as cheerio from "cheerio";
import {
  CapSkip, ApiException, CapSkipError, TimeoutException, ValidationException,
} from "capskip";

const PAGE_URL = "https://example.com/login";

const solver = new CapSkip({
  apiKey: process.env.CAPSKIP_API_KEY ?? "capskip",
  host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: Number(process.env.CAPSKIP_PORT ?? 8080),
  pollingInterval: 0.5,
});

async function solveCapy(key, url, options = {}, attempts = 3) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await solver.capy(key, url, options);
    } catch (err) {
      // A missed hole reads as unsolvable; the next try draws a new puzzle.
      const miss = err instanceof ApiException && err.message.includes("UNSOLVABLE");
      if (!miss || attempt === attempts) throw err;
      console.log(`attempt ${attempt}: ${err.message}`);
    }
  }
}

function addCookies(jar, res) {
  for (const line of res.headers.getSetCookie()) {
    const pair = line.split(";")[0];
    const eq = pair.indexOf("=");
    if (eq > 0) jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
  }
}

async function logIn(pageUrl, username) {
  // One jar per run, so runs side by side never share a session.
  const jar = new Map();
  const page = await fetch(pageUrl);
  addCookies(jar, page);
  const html = await page.text();
  const $ = cheerio.load(html);

  const key = html.match(/PUZZLE_[A-Za-z0-9_-]+/)?.[0];
  if (!key) throw new Error("No PUZZLE_ key in the HTML; find it in DevTools.");
  const apiServer = html.match(/(https:\/\/[^"'\s<>]+?)\/puzzle\/get_js\//)?.[1];
  const form = $("form")
    .has('script[src*="/puzzle/get_js/"], input[name="capy_captchakey"]').first();
  if (!form.length) throw new Error("No form holds the widget; select it by id.");

  const result = await solveCapy(key, pageUrl, { apiServer });

  // All three together, straight away: the challenge key is single-use.
  const body = new URLSearchParams(form.serializeArray().map((f) => [f.name, f.value]));
  // Use the page's own field names, password included.
  body.set("username", username);
  body.set("capy_captchakey", result.captchakey);
  body.set("capy_challengekey", result.challengekey);
  body.set("capy_answer", result.answer);

  const action = new URL(form.attr("action") || page.url, page.url);
  const res = await fetch(action, {
    method: "POST",
    headers: {
      cookie: [...jar].map(([k, v]) => `${k}=${v}`).join("; "),
      referer: page.url,
    },
    body,
    // A login answers 302 with a new session cookie. Keep the 302, so
    // its Set-Cookie lands in the jar instead of being followed away.
    redirect: "manual",
  });
  addCookies(jar, res);
  return { res, jar };
}

try {
  const { res, jar } = await logIn(PAGE_URL, "YOUR_USERNAME");
  // A 302 to the account page means it worked; jar holds that session.
  console.log(res.status, res.headers.get("location"), [...jar.keys()]);
} catch (err) {
  if (err instanceof ValidationException) console.error(`not sent: ${err.message}`);
  else if (err instanceof TimeoutException) console.error("gave up after defaultTimeout");
  else if (err instanceof CapSkipError) console.error(`solve failed: ${err.name}: ${err.message}`);
  else throw err;
  process.exitCode = 1;
}

Если провалились все три попытки, первым делом проверьте ключ: отклонённый ключ каждый раз отказывает одинаково, а промах почти никогда не случается три раза подряд. Если проверка ключа выбрасывает ошибку, значит, страница собирает виджет из бандла: откройте вкладку Network и скопируйте ключ и хост из собственного запроса виджета. Сырой эндпоинт, который стоит за этим методом, описан в разделе Capy справочника API.

Параллельный запуск множества входов

Каждый метод CapSkip в Node возвращает Promise, а AsyncCapSkip представляет собой просто другое имя того же класса, так что параллельный запуск решений сводится к обычному JavaScript. Нужно лишь ограничение. По умолчанию CapSkip выполняет 10 решений Capy одновременно, а остальные заставляет ждать свободного потока, поэтому с ним согласуется небольшой пул, который держит в работе десять вызовов. Вставьте его вместо блока try в конце полного примера:

// At most `limit` calls in flight; a failure becomes that item's Error.
async function mapLimited(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      results[i] = await fn(items[i]).catch((err) => err);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}

const users = ["YOUR_USERNAME_1", "YOUR_USERNAME_2", "YOUR_USERNAME_3"];
const results = await mapLimited(users, 10, (user) => logIn(PAGE_URL, user));
for (const r of results) console.log(r instanceof Error ? r.message : r.res.status);

logIn при каждом вызове создаёт собственное хранилище cookie, и именно это делает её безопасной при параллельном запуске. Одно хранилище на десять запусков перемешало бы их сессии и CSRF-токены. Кроме того, функция держит каждое решение в паре с его собственной отправкой. Если сначала решить сотню головоломок, а отправлять потом, самые ранние ключи заданий будут стареть, пока завершаются последние решения. На объёме добавьте ещё и прокси. Каждое решение получает новую головоломку от Capy API, а равномерный поток таких запросов с одного адреса и есть тот шаблон, ради которого существует ограничение частоты запросов. Настройте пул прокси в разделе Capy настроек CapSkip или передавайте объект proxy в каждом вызове; он влияет только на загрузку головоломки, единственный запрос, который делает решение Capy.

Запуск решателя на другой машине

Адрес 127.0.0.1 подходит, пока скрипт и CapSkip работают на одном ПК с Windows. Когда код на Node переезжает на VPS, в контейнер, на раннер CI или в serverless-функцию, локальная петля указывает не на ту машину, и первое же решение выбрасывает NetworkException с ECONNREFUSED. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP и включите проверку API-ключа. При запуске режима Server CapSkip открывает свой порт для всех адресов, поэтому, чтобы доступ к нему был только у ожидаемых машин, добавьте в Windows Firewall правило, блокирующее все остальные. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.

Клиент сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их в конструктор, как это делает полный пример, чтобы один и тот же скрипт работал и у вас на столе, и на сервере.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Сайт сообщает, что сессия истекла, хотя solver.capy() вернул все три значенияОтправка ушла без cookies, установленных страницей, поэтому поле CSRF больше не совпадает ни с одной сессиейПеренесите cookies из загрузки страницы, по одному хранилищу на запуск
Отправка возвращает 200 и страницу входа, хотя сайт принял входНа вход сайт ответил 302 с новым сессионным cookie, а fetch пошёл по редиректу без негоОтправляйте форму с redirect, равным manual, и добавляйте строки Set-Cookie из ответа 302 в хранилище
Ответ 403 на отправку, хотя cookie верный и все три значения на местеfetch не отправил ни Referer, ни Origin, а сайт их проверяетПередайте URL страницы в заголовке Referer
Сайт отклоняет сам ответОтвет был изменён, или до формы дошли только одно или два значенияОтправьте result.answer без изменений, вместе с двумя другими значениями, одним запросом
Отправка, которая сработала один раз, не проходит во второйКлюч задания одноразовый и живёт недолгоРешайте заново для каждой отправки и отправляйте сразу
ApiException при каждой попытке для одного ключаCapy API отклонил ключ, или api_server указывает не на тот хостСкопируйте ключ заново, вместе с префиксом, и проверьте хост в теге script
Время от времени ApiException с ERROR_CAPTCHA_UNSOLVABLECapSkip не смог найти вырез в этой головоломкеПовторите; следующая попытка получит другую головоломку
ValidationException ещё до какой-либо отправкиПустой ключ или URL, опция, которую метод не принимает, например pollingInterval в отдельном вызове, avatar в качестве version или тип прокси, который CapSkip не поддерживаетПроверьте, что ключ найден, и исправьте или уберите опцию, названную в сообщении
ApiException с ERROR_KEY_DOES_NOT_EXIST или ERROR_WRONG_USER_KEY при первом вызовеВ CapSkip включена проверка API-ключа, а скрипт отправил ключ по умолчанию или не отправил никакогоЗадайте в CAPSKIP_API_KEY ключ, настроенный в CapSkip
Каждый вызов занимает около четырёх секундИнтервал между опросами удваивается до потолка по умолчанию в 5 секунд, поэтому ответ, готовый через две секунды, ждёт опроса на отметке 3.75Задайте pollingInterval: 0.5 в конструкторе
TimeoutException при высокой параллельной нагрузкеВ работе больше входов, чем 10 потоков успевают обработать за 120 секунд, или CapSkip остановился посреди решенияОграничьте пул значением Max. Threads или передайте для этого вызова более длинный timeout
NetworkException при первом вызове, с ECONNREFUSED или ETIMEDOUT в сообщении, а если хост задан именем, то в err.cause.codeCapSkip не запущен, неверны хост или порт, либо файрвол сбрасывает соединениеЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server

FAQ

Работает ли здесь пакет capskip с TypeScript?

Да, пакет поставляется с собственными определениями типов. Для Capy важны две детали. Тип результата помечает captchakey, challengekey и answer как необязательные, потому что другие методы их не заполняют. Поэтому в режиме strict перед передачей их в body.set нужна проверка или non-null assertion. А тип опций принимает любой ключ, так что опция с опечаткой в имени или переданная не туда, например pollingInterval в отдельном вызове, компилируется без замечаний и падает во время выполнения с ValidationException.

Не отклонят ли ответ из-за user agent node?

Capy не отклонит. Ответ Capy не привязан к браузеру, CapSkip не возвращает для него user agent, а необязательный user agent, который можно передать, влияет только на запрос, загружающий головоломку. Сам сайт всё же может по своим причинам отклонить запрос с user agent node. В таком случае добавьте браузерный заголовок user-agent и к загрузке страницы, и к отправке формы.

Чем это отличается от версии на Python?

Вызов, удержание и правила те же. В Python requests.Session хранит cookies за вас, а в Node вы переносите их сами, и результат там представляет собой dict, а здесь объект. Вариант для Python разобран в руководстве по решению Capy Puzzle на Python.

Может ли Node-скрипт на хостинг-платформе достучаться до решателя?

Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST в своём скрипте и передайте его в конструктор. VPS, хост контейнеров, раннер CI и serverless-функция подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и блокирующее правило Windows Firewall для адресов, у которых не должно быть доступа к порту. Решатель остаётся на вашем собственном железе, поэтому в том, как считаются решения, ничего не меняется.

Коротко

Чтобы решить Capy Puzzle в Node.js, загрузите страницу в хранилище cookie, вытащите ключ PUZZLE_ одним регулярным выражением и запомните хост скрипта виджета на случай, если он не стандартный. Вызовите solver.capy() с ключом и настоящим URL страницы из клиента, созданного с pollingInterval: 0.5, и дайте ему отработать свои две секунды или около того. Отправьте captchakey, challengekey и answer в три поля capy_ без изменений, вместе и сразу, с cookies страницы и самой страницей в качестве Referer, и сохраните cookie, который вернётся с ответом 302. Повторяйте редкие промахи, держите в работе не больше входов, чем Max. Threads, и переходите в режим Server, как только скрипт покидает машину решателя.

И последнее о повторной попытке. Каждая попытка получает другую головоломку, поэтому при промахе правильнее всего просто попробовать снова, а локальный сервис распознавания капчи на вашей собственной машине означает, что вторая попытка стоит пару секунд, а не ещё одно оплачиваемое решение.