如何在 Node.js 中识别 hCaptcha Enterprise(capskip SDK)

要在 Node.js 中识别 hCaptcha Enterprise,先从站点获取新的 rqdata,然后把它作为 data 选项,连同 sitekey 和页面 URL 一起传给 capskip 包的 hcaptcha() 方法。该方法把任务发给 CapSkip,轮询到 token 就绪并返回它。然后带着结果中的 User-Agent 把 token 提交到表单。CapSkip 文档说,站点为每次挑战签发一个 rqdata,而且它很快过期,所以每个任务、每次重试都需要一个新的。下面的代码会与 CapSkip hCaptcha 识别工具 通信,它运行在你自己的 Windows PC 上,没有按次识别的费用。
你需要什么
- CapSkip 4.0.0 或更高版本,安装在你的 Windows 机器上。该版本新增了 hCaptcha 识别,包括 Enterprise,支持这里使用的 in.php 格式和 createTask JSON 格式。
- Node.js 18 或更高版本,以及 capskip 包 1.6.0 或更高版本(第一个带有 hcaptcha 方法的版本)。该包声明 Node.js 18 为最低版本,自身没有任何依赖,表单提交使用内置的 fetch。我只在 20.11.1、24.22.0 和 25.2.1 上运行过代码。
- 一个你拥有或被允许自动化操作、显示 hCaptcha 小组件的站点,以及该小组件的 sitekey。
- 从脚本到 CapSkip 的一条通路。Local 模式让 CapSkip 停留在 127.0.0.1 上,只有那一台设备能访问。Server 模式还会让它监听你的网络地址或公网 IP,这样 Windows VPS、另一台 PC 或托管平台上的脚本也能使用它,并且它要求 CapSkip 以管理员身份运行。选择模式的位置在 连接设置.
目前的情况:我用 capskip 1.6.0 针对我自己写的两个模拟服务器运行了代码,一个代替 CapSkip,另一个代替只接受每个 rqdata 一次的 Enterprise 站点。这些规则是我定的,取自 CapSkip 文档和包的源码。我读过的 hCaptcha 公开文档页面都没有介绍 rqdata,所以这里关于 rqdata 的每个事实都来自 CapSkip 文档。没有发生真实的识别,所以我无法说一次识别要多久,也无法说真实的 Enterprise 站点表现如何。
第 1 步:找到 sitekey、隐形标志和 rqdata
在一个空文件夹里运行 npm install capskip。然后把第 1 到 4 步的代码块按所示顺序复制到该文件夹中名为 hcaptcha-demo.js 的同一个文件里。到第 4 步之前什么都不会运行,除非你在第 2 步末尾加上测试行。recaptchaTimeout 选项是 SDK 针对 hCaptcha 的轮询上限,单位为秒。它的默认值是 300,较短的 120 是我自己选的。
// npm install capskip
const { CapSkip, ApiException } = require("capskip");
const CAPSKIP = { host: "127.0.0.1", port: 8080 }; // Local mode; Server mode: your server
const SITE = "https://www.example.com"; // a site you may automate
const PAGE_URL = `${SITE}/login`; // the page the hCaptcha widget is on
// The SDK's default key. Use your own if API Key Validation is on in CapSkip.
const API_KEY = "capskip";
const PROXY = null; // for example "203.0.113.10:8080"; null means no proxy
const solver = new CapSkip({
...CAPSKIP,
apiKey: API_KEY,
recaptchaTimeout: 120, // poll limit in seconds for hCaptcha; the default is 300
});sitekey 通常写在小组件容器上,名为 data-sitekey。当脚本构建小组件时,就找 hcaptcha.render() 的 sitekey 选项,或者 hCaptcha iframe 地址中的 sitekey= 参数。 hCaptcha 隐形小组件指南 指出隐形小组件的标志是 data-size 设为 invisible,CapSkip 文档则说,这类小组件要发送 invisible=1。
rqdata 是比较特别的一个。CapSkip 文档说,有些 Enterprise 站点会在小组件运行之前,通过 hcaptcha.setData() 把一个 rqdata 值传给它,如果页面这样做,你就把这个值作为 data 发送。如果页面没有这样做,就不要发送 data 字段。这个值从哪里来由站点决定。它可能在页面 HTML 里,也可能来自页面发出的某个请求,所以请在页面脚本中搜索设置它的那个调用,并顺着找到这个字符串。我没有在真实的 Enterprise 站点上试过这一点,下面的函数使用的是我的模拟服务器提供的路径。
// Ask the site for a new challenge. The route and the field names are the
// site's choice; these are the ones in my test mock, so adapt them.
// If the sitekey is fixed in the page, return it as a constant here.
// If the site puts the values in the page HTML, fetch PAGE_URL and
// extract them here instead.
async function getChallenge() {
const resp = await fetch(`${SITE}/api/challenge`);
if (!resp.ok) throw new Error(`challenge failed: HTTP ${resp.status}`);
const { sitekey, rqdata, invisible } = await resp.json();
return { sitekey, rqdata, invisible: Boolean(invisible) };
}第 2 步:提交任务并取回 token
一次调用就能提交任务并轮询结果。hcaptcha() 方法接受 sitekey、页面 URL 和一个选项对象:data 是 rqdata,invisible 为 true 表示隐形小组件,proxy 是识别时使用的代理。SDK 把它们作为表单字段随 method=hcaptcha 发送到 /in.php,并把 invisible 发送为 1。它不发送 userAgent 选项:文档说,只有该选项与 CapSkip 用来识别的浏览器一致时,CapSkip 才会采用它,而结果中带有获得 token 时所用的 User-Agent。
async function solve({ sitekey, rqdata, invisible }) {
const options = {};
// The Enterprise rqdata, fresh for this task. Skip it if the site has none.
if (rqdata) options.data = rqdata;
if (invisible) options.invisible = true; // data-size is invisible
if (PROXY) options.proxy = PROXY;
// Submits the task, polls until the token is ready, and returns the result.
return solver.hcaptcha(sitekey, PAGE_URL, options);
}SDK 会替你轮询 /res.php。第一次轮询立刻发出,之后的等待从四分之一秒开始,每次翻倍,增长到 5 秒为止。空的响应体算作未就绪:文档说,这表示结果已被读取过或 ID 不存在,而 SDK 源码补充说,CapSkip 会在提交之后马上发送一个空响应体。超过上限时,调用会抛出 TimeoutException。
结果只交付一次,SDK 会替你完整保存。code 和 token 字段是同一个字符串。respKey 是 hCaptcha 随 token 一起交给小组件的第二个值。按文档所说,大多数站点从不读取它,而 CapSkip 没发送时,SDK 会把它设为空字符串。要获取得到 token 时所用的 User-Agent,请读取 userAgent。
想单独试第 1 步和第 2 步,就在末尾加一行 getChallenge().then(solve).then(console.log),然后运行 node hcaptcha-demo.js。你应该会看到一个带有 captchaId、code、userAgent、respKey 和 token 的对象。添加下一个代码块之前,请再把这一行删掉。
第 3 步:把 token 交给表单
浏览器中的小组件会自动填写两个隐藏的 textarea 字段,一个是 h-captcha-response,另一个是镜像字段 g-recaptcha-response。脚本没有小组件,所以要把它们作为表单字段发送。根据 CapSkip 文档,这意味着始终发送 h-captcha-response,当表单也有 g-recaptcha-response 字段时再发送它,并使用回复中的 User-Agent。下面的表单只是一个替身,因为你的站点有自己的字段和自己的路径。
token 只能使用一次,CapSkip 文档说它们在签发后约两分钟过期。hCaptcha 自己的文档把 120 秒作为其 expired-input-response 错误的默认值。所以不要提前识别,也不要保留 token 池。每次表单提交都是先识别,再提交。
async function postForm(solved) {
const form = new URLSearchParams({
username: "demo", // the form's own fields
"h-captcha-response": solved.token,
// delete the next line if the form has no g-recaptcha-response field
"g-recaptcha-response": solved.token,
});
// Send the User-Agent the reply names, not Node's default one.
return fetch(PAGE_URL, {
method: "POST",
headers: { "User-Agent": solved.userAgent },
body: form,
});
}第 4 步:每次尝试都用新 rqdata 的完整示例
最后一个代码块把一切接入重试循环。把它追加到 hcaptcha-demo.js,并用 node hcaptcha-demo.js 启动。每次尝试都会先调用 getChallenge,所以每个任务都带着刚刚签发的 rqdata。循环只重试 ERROR_CAPTCHA_UNSOLVABLE,即任务无法被识别时的错误码。CapSkip 文档点名了两种重新提交也会重复出现的失败:轮询时的 ERROR_RECAPTCHA_INVALID_SITEKEY,表示 hCaptcha 对该页面拒绝了这个 sitekey;以及 ERROR_PROXY_CONNECT_REFUSED。循环会原样重新抛出这两个错误以及其他所有错误。
当站点返回的状态码不在 200 到 299 之间时,循环也会再来一轮,因为 token 被拒绝可能意味着 rqdata 已过期。403 也可能有其他原因,例如登录表单里的密码错误,所以尝试在三次之后停止。
// ERROR_CAPTCHA_UNSOLVABLE is the one failure where a new task can help.
async function login() {
for (let attempt = 1; attempt <= 3; attempt += 1) {
const challenge = await getChallenge(); // a new rqdata for every attempt
try {
const solved = await solve(challenge);
const resp = await postForm(solved);
if (resp.ok) return resp;
console.log(`attempt ${attempt}: the site answered HTTP ${resp.status}`);
} catch (err) {
// The message is the text of the app's reply: a bare code or small JSON.
const unsolvable = err instanceof ApiException
&& err.message.includes("ERROR_CAPTCHA_UNSOLVABLE");
if (!unsolvable) throw err;
console.log(`attempt ${attempt}: ERROR_CAPTCHA_UNSOLVABLE`);
}
}
throw new Error("hCaptcha not passed in 3 attempts");
}
login()
.then(async (resp) => console.log(resp.status, await resp.text()))
.catch((err) => {
console.error(`${err.name}: ${err.message}`);
if (err.cause) console.error(err.cause.code); // for example ECONNREFUSED
process.exitCode = 1;
});SDK 自己还会带来三种失败。ValidationException 表示它在发送任何东西之前就拒绝了这次调用:空的 sitekey、不是绝对 http 或 https 的页面 URL、错误的 invisible 值,或者 CapSkip 的 hCaptcha API 不接受的选项,例如 2Captcha 的 domain。提交时出现 NetworkException,表示 CapSkip 的地址上没有任何程序应答;TimeoutException 表示轮询上限已到。新建任务改变不了其中任何一种,所以循环会把它们重新抛出。ApiException 的消息里是应用回复的文本,所以循环用 includes() 去匹配它。
下面是脚本针对模拟服务器的表现,使用 capskip 1.6.0,运行在 Node.js 24.22.0 上。Node.js 20.11.1 和 25.2.1 在每个情形下的结果都一样,退出码也相同。测试脚本只改了地址,以及在超时那几行里改了轮询上限。
| 模拟设置 | 脚本的表现 |
|---|---|
| Enterprise 站点;模拟服务器在第三次轮询时就绪,或在提交后 20 秒就绪 | 两种情况都是一个任务和状态 200。第一种情况下,轮询出现在约 0、0.3 和 0.8 秒。第二种情况下,在三个运行时上轮询都出现在约 0、0.3、0.8、1.8、3.8、7.8、12.8、17.8 和 22.8 秒,所以 token 在就绪后 2.8 秒才到达脚本 |
| 隐形站点;不签发 rqdata 的站点 | 第一种情况下 in.php 收到了 invisible=1,第二种情况下没有收到 data 字段,两次表单提交都通过了 |
| 在一次运行中两次返回 ERROR_CAPTCHA_UNSOLVABLE,在另一次运行中三次返回 | 两次失败:第三个任务返回状态 200,并带有三个不同的 rqdata 值。三次失败:Error “hCaptcha not passed in 3 attempts” 和退出码 1 |
| 模拟站点拒绝第一个 token | “attempt 1: the site answered HTTP 403”,然后是带新 rqdata 的第二个任务和状态 200 |
| 为第二次登录复用缓存的挑战(第一次已通过);省略 data 字段;提交表单时没有带回复中的 User-Agent | 这些尝试中的每一次都从模拟站点得到 403,每次运行都以 Error “hCaptcha not passed in 3 attempts” 结束。这些是我的模拟服务器的规则,不是真实站点的规则 |
| 空 sitekey、ftp 协议的 pageurl、被设成单词 maybe 的 invisible、domain 选项;不是代理的代理字符串、CapSkip 不认识的密钥、被拒绝的任务类型 | 前四种情况是 ValidationException,没有任何任务发送到 in.php。其他情况是 ApiException,分别带 ERROR_PROXY_FORMAT、ERROR_KEY_DOES_NOT_EXIST 和 ERROR_TASK_NOT_SUPPORTED,每次都在一个任务之后。退出码每次都是 1 |
| 轮询返回 ERROR_PROXY_CONNECT_REFUSED 或 ERROR_RECAPTCHA_INVALID_SITEKEY | 只有一个任务。脚本在第一个错误处停止 |
| 代理 203.0.113.10:8080 或 user:[email protected]:8080;模拟服务器一直未就绪(上限缩短为 1.5 秒);CapSkip 地址上没有进程 | in.php 收到的代理字符串原封未动,proxytype 为 HTTP,运行通过;TimeoutException: timeout 1.5 exceeded;NetworkException: connect ECONNREFUSED,下一行再出现 ECONNREFUSED,并且没有任务 |
| 模拟服务器始终未就绪,上限 120 秒;模拟服务器进程在识别期间退出(上限缩短为 4 秒) | 最后一次轮询出现在 118 秒,TimeoutException 出现在约 123 秒。模拟服务器挂掉之后,SDK 仍在继续轮询,最终以 TimeoutException 结束,而不是连接错误 |
常见错误
| 错误 | 原因 | 修复 |
|---|---|---|
| 带 ERROR_WRONG_USER_KEY 或 ERROR_KEY_DOES_NOT_EXIST 的 ApiException | CapSkip 中 API Key Validation 已开启,而 API_KEY 不是它的密钥之一。 | 把 API_KEY 设置为来自 CapSkip 的密钥。 |
| 带 ERROR_TASK_NOT_SUPPORTED 的 ApiException | CapSkip 拒绝了该任务类型。原因之一是版本早于 4.0.0 | 更新到 4.0.0 或更高版本 |
| 带 ERROR_RECAPTCHA_INVALID_SITEKEY 的 ApiException | hCaptcha 不接受该页面的这个 sitekey。空 sitekey 到不了这一步,因为 SDK 会先拒绝它 | 对照页面检查 sitekey 和 pageurl |
| ValidationException | SDK 在发送任何东西之前就拒绝了这次调用,它的消息会指出问题所在 | 改正这次调用 |
| 带 ERROR_PROXY_FORMAT 的 ApiException | 代理字符串的格式是 CapSkip 不接受的 | 使用 IP:PORT、LOGIN:PASSWORD@IP:PORT 或 IP:PORT:LOGIN:PASSWORD |
| 带有 ERROR_PROXY_CONNECT_REFUSED 的 ApiException | CapSkip 无法通过代理到达目标 | 单独试一下该代理,检查登录信息和类型,然后重新运行脚本 |
| 带 ERROR_CAPTCHA_UNSOLVABLE 的 ApiException | 任务无法被识别 | 获取一个新的 rqdata 并发送新任务。循环会这样做 |
| TimeoutException: timeout 120 exceeded | 任务在脚本的上限时间到达时仍在运行,或者 ID 未知,或者其结果已被读取(两者都以空响应体返回) | 查看 CapSkip 中的 Monitor,然后再次运行脚本 |
| NetworkException: connect ECONNREFUSED | CAPSKIP 地址上没有任何响应:CapSkip 已停止,或者主机或端口不对。其后一行显示原因,端口上无人监听时是 ECONNREFUSED | 启动 CapSkip,然后对照你的连接设置检查该地址 |
| 站点在 token 已识别后仍拒绝表单 | token 比站点允许的更旧,User-Agent 或地址与识别时不同,站点把 token 评为有风险,或者 rqdata 已过期或被重复使用(这一点由我的模拟服务器强制执行,文档只说它很快过期) | 识别后立即提交,发送回复中的 userAgent,每个任务获取新的 rqdata,并参阅下面的代理说明 |
代理与 Server 模式
CapSkip 不会为 hCaptcha 强制使用代理。文档说,测试和偶尔的识别用一个地址就行,但 hCaptcha 不仅给浏览器打分,也给 token 所来自的网络打分,而来自同一个 IP 的持续识别请求,正是它的速率限制所要捕捉的。用量大时,请在应用的 Settings、Captcha types、hCaptcha、Configure Proxies 中配置代理池。要让一次识别从特定网络发出,就随任务发送一个代理,这正是 PROXY 设置的作用。随任务发送的代理始终优先于列表。CapSkip 接受 IP:PORT、LOGIN:PASSWORD@IP:PORT 或 IP:PORT:LOGIN:PASSWORD 形式的代理。单独的一个代理字符串会以 proxytype HTTP 发出。对于 SOCKS5,请把 proxy 作为带有 type(可取 HTTP、HTTPS、SOCKS5 或 SOCKS5H)和 uri 的对象传入。在模拟服务器上,它以 proxytype SOCKS5 到达了 in.php。我没有测试真实的 SOCKS5 识别。
这个设置只会改变识别从哪里发出。对严格的站点,文档还说表单要从同一个地址提交,但 Node.js 内置的 fetch 没有针对单个请求的代理选项,所以这里的表单提交是从运行脚本的 PC 发出的。我没有测试经过代理的表单提交。
Server 模式只改变识别工具所在的位置。把 CAPSKIP 设置指向服务器,脚本的其余部分保持不变。Server 模式要求 CapSkip 以管理员身份运行,静态公网 IP 能提供最稳定的远程访问。识别工具仍然是你自己的硬件,没有按次识别的费用。随后 CapSkip 会接受来自所有地址的连接,所以请为不该访问它的地址添加一条 Windows Firewall 阻止规则,并在 CapSkip 中开启 API Key Validation,同时填写 API_KEY。识别现在在 Windows VPS 上进行,所以 hCaptcha 看到的是那台机器的网络,或者你传入的代理。
常见问题
为什么 Node.js 中的 hCaptcha Enterprise 每次都需要新的 rqdata?
CapSkip 文档说,站点是为一次挑战签发该值的,并且它很快过期,所以缓存的值就是旧值。过期的值是 token 被拒绝的可能原因之一,也是本指南能够排除的一个。真实站点遇到旧值时会如何失败,我读过的任何地方都没有记录,所以我的模拟服务器的拒绝只能说明循环会作何反应。
如果站点仍然拒绝 CapSkip 返回的 token 怎么办?
看看 High score mode,它位于 Settings 里 Captcha types 下的 hCaptcha 一行。CapSkip 更新日志说,4.0.0 默认开启了它,并且它在 Google Chrome 中识别。该模式关闭,或者没有安装 Chrome 时,它在 CapSkip Custom Browser 中识别,而没有 Chrome 时该设置是灰色的。API 文档还为严格的站点补充了三个习惯:使用优质代理,从同一个地址提交表单,并发送回复中指明的 User-Agent。
我能从托管平台或 VPS 上运行的脚本调用 CapSkip 吗?
可以,只要 CapSkip 处于 Server 模式。把 CAPSKIP 设置指向它的地址,脚本不需要其他改动。请记住,这时识别在服务器上进行,所以如果站点在意网络,就给任务一个代理。CapSkip 仍然留在你自己的硬件上。
无论站点怎么刁难你,在 Node.js 中识别 hCaptcha Enterprise 始终是同一个循环:新的 rqdata、新的任务、立即提交的 token。用 验证码识别工具 在你自己的机器上运行它,重试就不产生按次识别的费用。更多 Node.js 相关工具见 Node.js 验证码识别页面,每个参数都在 API 文档的 hCaptcha 部分.
