如何在 Node.js 中识别 Capy Puzzle 并保持会话

要在 Node.js 中识别 Capy Puzzle,先获取页面,从小组件脚本里读出 PUZZLE_ key。把它和页面 URL 一起传给 capskip 包的 capy 方法。然后把它返回的三个值分别填入 capy_captchakey、capy_challengekey 和 capy_answer 字段,一起提交,并且立刻提交,而且要在获取页面的那个会话里提交。Node 代码通常就栽在最后这一点上。Node 内置的 fetch 不保留 cookie,也不发送 Referer。于是表单提交到达时,并不带有页面开启的那个会话,不管答案多正确,站点都会拒绝。CapSkip 从 1.4.0 版开始支持 Capy Puzzle。本指南讲这几件事:key、调用、两秒延迟、提交、重试,以及同时运行大量登录流程。
你需要什么
- 在 Windows 机器上运行的 CapSkip 1.4.0 或更高版本。Capy Puzzle 支持就是在该版本中加入的,同时加入的还有 CaptchaFox 和 Friendly Captcha。
- Node.js 20.18.1 或更高版本,这是 cheerio 支持的最旧版本。新项目请用 22 或 24,因为 Node 20 已于 2026 年 4 月结束生命周期。
- capskip 包 1.3.0 或更高版本,这是第一个带有 capy 方法的版本;另外还需要 cheerio 来读取表单。
- 显示拼图的那个页面的 URL。key、Capy 主机和表单字段,全都从这个页面上获取。
- 识别工具的地址。在 Local 模式下,CapSkip 只在 127.0.0.1 上响应,仅供本机使用;在 Server 模式下,它监听你的网络地址或公网 IP,这样另一台机器上的脚本就能通过 API 调用它。两种模式的设置位置都是 连接设置.
# capskip 1.3.0 added capy(); cheerio reads the form npm install capskip cheerio
示例是使用顶层 await 的 ES 模块。把文件保存为 .mjs 扩展名,或者把 package.json 里的 type 字段设为 module,因为较新版本的 npm init 会在那里写入 commonjs,而没有 type 字段的 package.json 同样按 CommonJS 处理。如果你的项目是 CommonJS,capskip 包也可以用 require 加载。
第 1 步:获取页面,保留它的 cookie 并找到 key
Capy 表单通常出现在登录页或注册页上,这类页面会设置一个会话 cookie,并在表单里放一个 CSRF token。Node 的 fetch 不会在多次调用之间保留 cookie,所以等你提交时,那个 cookie 早已不在了,token 也就对不上任何东西了。cookie 要你自己携带。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);key 是公开的,对每个访问者都一样,拼图类的 key 以 PUZZLE_ 开头。大多数页面把它放在小组件脚本 URL 的 k 参数里,而这个 URL 就在 fetch 下载到的 HTML 中。有些页面还会在表单里渲染一个隐藏的 capy_captchakey input;小组件在浏览器里运行过之后,这个 input 一定存在,DevTools 里看到的就是这个位置。用一个正则表达式,两处都能找到 key。
顺便记下脚本的主机。script URL 中 /puzzle/get_js/ 之前的部分,就是这个 key 背后的 Capy API,CapSkip 把它叫作 api_server。它的默认值是 https://jp.api.capy.me,也就是线上服务实际运行的地方,所以只有当页面从别处加载小组件时,你才需要它。为其他识别工具写的示例常常写着没有地区前缀的 api.capy.me,而这个主机已经无法解析了,所以如果你复制过这样的示例,就把它删掉。这个正则要求的是 https:// 开头的 URL;如果 src 以 // 开头,就先在主机前面补上 https:,再把它传进去。
选取表单的那一行代码,会选中包含小组件脚本或 capy_captchakey input 的那个表单。如果页面在 head 里或表单下方加载脚本,而且表单里也没有 capy_captchakey input,form.length 就会是 0。这时请改为按 id 或 action 选中表单。
第 2 步:调用 solver.capy()
在 Node.js 中识别 Capy Puzzle 只需要一个方法。它接受 key、页面 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 是一个空字符串,只是为了与其他服务兼容而存在。答案是一个很长的字符串,开头大致像 0xax8ex0xax84x 这样:它是小组件在拼图块移动时本会记录下来的拖动轨迹,而不是一个坐标。
该方法接受的选项
apiServer 是 api_server 的别名,而且这个方法会丢弃所有值为 undefined 或 null 的选项,所以上面的调用每次都可以直接传入正则的结果。它还接受一个带 type 和 uri 键的 proxy 对象,以及 useragent;useragent 会随 CapSkip 拉取拼图时发出的那唯一一个请求一起发送。还有一个 version 选项,但只接受 puzzle。Capy 的另一个系列 avatar 是另一种挑战,背后是另一个接口,所以 SDK 会拒绝它,而不是返回一个站点会拒收的答案。
有两个单次调用选项容易让人踩坑。以秒为单位的 timeout 会在这一次识别中取代客户端的 defaultTimeout。轮询间隔的上限也可以按单次调用设置,但只能写成 polling_interval。驼峰写法的 pollingInterval 属于构造函数,传给 solver.capy() 时会抛出 ValidationException,并在消息里点出这个选项。TypeScript 不会提醒你,因为选项类型接受任意键。为空的 key 或 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 步:带上页面的 cookie,提交全部三个值
这三个值要填进小组件本来会自己写入表单的那几个字段里。把它们放在一个请求里一起发送,带上第 1 步的 cookie,并把页面作为 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 token,所以你根本不用逐个写出字段名。它不包含提交按钮,所以如果服务器会检查按钮的 name,就用 body.set 把它加上。body.set 还会替换页面自己渲染出来的 capy_captchakey input,所以 key 永远不会发出两次。Referer 和 cookie 同样重要。fetch 既不发送 Referer,也不发送 Origin,而有些框架(Django 就是其中之一)会拒绝两者都没有的 HTTPS 表单提交,即使 cookie、token 和答案全都正确。
redirect 这个设置能让你在登录成功后保住会话。登录通常会返回 302 并带上一个新的会话 cookie,因为 Django 这类框架会在这时轮换会话。如果不加设置,fetch 会不带这个 cookie 就跟随重定向,回到登录页面,并报告一个 200。把 redirect 设为 manual 后,你拿到的就是 302 本身,addCookies 会把新 cookie 放进 cookie jar,供后续请求使用。
答案要按返回的原样提交。站点后端会拿它和当时下发的拼图进行比对,所以截断它或重新拼装它,都会让它失效。URLSearchParams 会对请求体做百分号编码,这没有问题,因为服务器在比对之前会先解码。然后要尽快提交。CapSkip 每次识别都会生成一个新的挑战 key,Capy 会把它下发的拼图绑定到这个 key 上。所以这个 key 只能用一次,而且寿命很短,一次识别只对应一次提交。有些页面通过自己的脚本用 JSON 请求体提交,而不是表单提交。打开 DevTools 手动提交一次,然后把三个值放到页面放它们的位置。
第 4 步:识别失败时重试
只要在 Node.js 中识别 Capy Puzzle 有一定的量,就总会有几次识别失败。失败的识别会以 ApiException 的形式返回,其消息中包含 ERROR_CAPTCHA_UNSOLVABLE;而通过 SDK 轮询的接口 res.php,这一个错误码涵盖了两种不同的情况。没找到缺口只是偶尔发生,下一次尝试会拿到一张基于不同照片的全新拼图,所以通常会成功。被 Capy API 拒绝的 key 则每次尝试都会失败,CapSkip 的 Monitor 标签页里 Capy 卡片上的 Bad Keys 计数会随之上升。第一种情况重试即可,第二种要去修正。
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 的原始回复,所以检查代码直接从中读取错误码。被拒的 key 看起来同样是无法识别,所以这里它也会被重试,而它每次尝试都以同样的方式失败,这正是辨认它的线索。另一种方式是 CapSkip 设置中 Capy 部分的 Retries 设置,取值为 0 到 3,默认为 0。设置之后,CapSkip 会自行重试没找到缺口的情况,但从不重试被拒的 key,因为它会以同样的方式再被拒一次。
capy 方法按客户端的 defaultTimeout 轮询,即 120 秒,因为一次 Capy 识别只是一次拉取加上一些运算,而不是一个浏览器会话。CapSkip 自己也有一套计时。在它的 Capy 设置中,一个任务最多可以等待 250 秒(Wait Timeout)来获得 10 个线程(Max. Threads)中的一个,而单次尝试有 60 秒(Row Timeout)。一次正常的识别,在这些时限内都绰绰有余。SDK 遇到连接错误也会照常继续轮询,所以如果 CapSkip 在识别中途停止,调用最终会以 TimeoutException 结束,而不是 NetworkException。
完整可运行示例
下面是放在一个文件里的完整流程。logIn 把页面获取到它自己的 cookie jar 里,读取 key 和主机,带重试地完成识别,然后带上 cookie 和 Referer 提交表单。它会返回响应和 cookie jar,这样你在登录之后发出的请求都会带上新的会话。
// 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;
}如果三次尝试全部失败,先检查 key,因为被拒的 key 每次都以同样的方式失败,而失手几乎不会连续发生三次。如果检查 key 的那一步抛出异常,说明页面是用打包后的脚本构建小组件的:打开 Network 标签页,从小组件自己发出的请求里复制 key 和主机。这个方法背后的原始接口,详见 API 参考文档的 Capy 部分.
并行运行大量登录流程
在 Node 中,CapSkip 的每个方法都返回一个 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 jar,正是这一点让它可以安全地并行运行。如果十次运行共用一个 cookie jar,它们的会话和 CSRF token 就会混在一起。它还让每次识别都和它自己的提交配对。先识别一百张拼图、之后再统一提交,会让最早拿到的挑战 key 在等最后几次识别完成时不断变旧。量大时还要加代理。每次识别都会从 Capy API 拉取一张新拼图,而从同一个地址源源不断地拉取,正是限流机制要抓的那种模式。在 CapSkip 的 Capy 部分配置代理池,或者在每次调用时传入一个 proxy 对象;代理只作用于拉取拼图这一步,也就是一次 Capy 识别唯一会发出的请求。
把识别工具跑在另一台机器上
只要脚本和 CapSkip 在同一台 Windows 电脑上,127.0.0.1 就是对的。一旦 Node 代码挪到 VPS、容器、CI runner 或 serverless 函数上,回环地址就指向了错误的机器,第一次识别就会抛出带 ECONNREFUSED 的 NetworkException。把 CapSkip 切换到 Server 模式,它就会监听你的网络地址或公网 IP,上面这些环境都能通过同一套 API 调用它。如果链路要经过公网,请使用静态公网 IP,并打开 API 密钥校验。Server 模式启动时,CapSkip 会向所有地址开放端口,所以要想只让你预期的机器访问,就为其他地址添加一条 Windows Firewall 阻止规则。识别工具仍然是你自己的 Windows 机器,识别也仍然不计量。
客户端不会自己读取环境变量。像完整示例那样,在你的代码里读取 CAPSKIP_HOST、CAPSKIP_PORT 和 CAPSKIP_API_KEY 并传给构造函数,这样同一个脚本在你的桌面机和服务器上都能运行。
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| solver.capy() 返回了全部三个值,站点却提示会话已过期 | 提交时没有带上页面设置的 cookie,所以 CSRF 字段对不上任何会话 | 带上获取页面时拿到的 cookie,每次运行用一个 cookie jar |
| 站点其实已接受登录,提交却返回 200 和登录页面 | 登录返回了 302 和一个新的会话 cookie,而 fetch 没带这个 cookie 就跟随了重定向 | 提交时把 redirect 设为 manual,并把 302 响应里的 Set-Cookie 行加进 cookie jar |
| cookie 正确、三个值齐全,提交却返回 403 | fetch 既没发送 Referer 也没发送 Origin,而站点会检查它们 | 把页面 URL 作为 Referer 请求头发送 |
| 站点拒绝了答案本身 | 答案被改动过,或者只有其中一两个值到达了表单 | 把 result.answer 原样连同另外两个值,放在一个请求里提交 |
| 第一次成功的提交,第二次却失败 | 挑战 key 只能用一次,而且寿命很短 | 每次提交都重新识别,并且识别完立刻提交 |
| 某个 key 每次尝试都抛出 ApiException | Capy API 拒绝了这个 key,或者 api_server 指向了错误的主机 | 重新复制 key,连前缀一起,并检查 script 标签的主机 |
| 偶尔出现包含 ERROR_CAPTCHA_UNSOLVABLE 的 ApiException | CapSkip 没能在那张拼图里定位到缺口 | 重试;下一次尝试会拿到另一张拼图 |
| 还没发送任何内容就抛出 ValidationException | key 或 URL 为空、传了这个方法不接受的选项(比如单次调用的 pollingInterval)、version 设成了 avatar,或者用了 CapSkip 不接受的代理类型 | 确认 key 确实找到了,并修正或去掉错误信息中指出的那个选项 |
| 第一次调用就抛出带有 ERROR_KEY_DOES_NOT_EXIST 或 ERROR_WRONG_USER_KEY 的 ApiException | CapSkip 打开了 API 密钥校验,而脚本发送的是默认密钥,或者根本没有发送 | 把 CAPSKIP_API_KEY 设为 CapSkip 中配置好的密钥 |
| 每次调用都要大约四秒 | 轮询间隔不断翻倍,直到默认的 5 秒上限,所以两秒时就绪的答案要等到 3.75 秒那次轮询才被取回 | 在构造函数里设置 pollingInterval: 0.5 |
| 高并行负载下抛出 TimeoutException | 在途的登录流程多于 10 个线程在 120 秒内能处理完的数量,或者 CapSkip 在识别中途停止了 | 把并发池的上限设为 Max. Threads,或者为这次调用传入更长的 timeout |
| 第一次调用就抛出 NetworkException,消息中带有 ECONNREFUSED 或 ETIMEDOUT;主机用的是主机名时,它们出现在 err.cause.code 里 | CapSkip 没有在运行、主机或端口不对,或者防火墙丢弃了连接 | 启动 CapSkip,然后确认它应该处于 Local 模式还是 Server 模式 |
常见问题
这里的 capskip 包能配合 TypeScript 使用吗?
可以,它自带类型定义。对 Capy 来说有两个细节要注意。结果类型把 captchakey、challengekey 和 answer 标为可选,因为其他方法不会设置它们。所以在严格模式下,把它们传给 body.set 之前,需要先做检查或使用非空断言。另外,选项类型接受任意键,所以像单次调用的 pollingInterval 这样写错或放错位置的选项,编译时毫无问题,运行时才会因 ValidationException 失败。
node 的 user agent 会导致答案被拒吗?
Capy 不会因此拒绝。Capy 的答案不绑定浏览器,CapSkip 不会为它返回 user agent,而你可以传入的那个可选 user agent,也只影响拉取拼图的那个请求。不过站点本身可能出于自己的原因,拒绝 user agent 为 node 的请求。如果遇到这种情况,就在获取页面和提交表单这两个请求上都加上浏览器的 user-agent 请求头。
这和 Python 版本有什么不同?
调用、两秒等待和各项规则都一样。Python 的 requests.Session 会替你保管 cookie,而在 Node 中要你自己携带;结果在那边是 dict,在这里是对象。Python 这一侧的写法见 在 Python 中识别 Capy Puzzle 的指南.
托管平台上的 Node 脚本能连到识别工具吗?
可以。在连接设置里把 CapSkip 切换到 Server 模式,让它监听网络地址而不是回环地址,然后在你的脚本里从 CAPSKIP_HOST 读取这个地址,并传给构造函数。VPS、容器宿主机、CI runner 和 serverless 函数都通过同一套 HTTP API 连接。如果链路要经过公网,请使用静态公网 IP,并为不该访问该端口的地址添加一条 Windows Firewall 阻止规则。识别工具始终运行在你自己的硬件上,因此识别次数的计算方式没有任何变化。
简短版结论
要在 Node.js 中识别 Capy Puzzle,先把页面获取到一个 cookie jar 里,用一个正则取出 PUZZLE_ key,并记下小组件脚本的主机,以防它不是默认主机。用设置了 pollingInterval: 0.5 的客户端,以 key 和真实的页面 URL 调用 solver.capy(),让它花上两秒左右。把 captchakey、challengekey 和 answer 放进三个 capy_ 字段,原样、一起、立刻提交,带上页面的 cookie,并把页面作为 Referer,同时保留 302 交回来的 cookie。偶尔失手就重试,在途的登录流程不要多于 Max. Threads,一旦脚本离开识别工具所在的机器,就切换到 Server 模式。
- 这个类型如何运作、识别工具覆盖哪些内容: Capy Puzzle 验证码识别页面.
- Node.js 包能处理的所有验证码类型: Node.js 验证码识别页面.
关于重试,最后再说一点。每次尝试都会拿到一张不同的拼图,所以遇到失手,再试一次就是正确的解决办法;而当 本地验证码识别工具 就在你自己的机器上时,第二次尝试只多花几秒钟,而不是再多一次计费的识别。
