如何在 Deno 中识别 reCAPTCHA:权限与超时

solve recaptcha in deno - How to Solve reCAPTCHA in Deno: Permissions and Timeouts

要在 Deno 中识别 reCAPTCHA,就通过 npm 说明符导入 CapSkip Node.js SDK,并自己从环境变量中读取识别工具的地址。然后运行脚本,并带上只点名 CapSkip 和你的站点的权限标志。CapSkip 运行在你自己的 Windows PC 上,所以没有按次识别的账单。代码很短,难的是 Deno 的权限模型。SDK 会把被拦截的调用报告为网络错误。一个毫无回应的 CapSkip 会让脚本挂起,既不报错也不退出。而最常见的添加超时的做法,会让进程在整个时限内一直保持存活。本指南会逐一讲解这些问题。

你需要什么

  • 在 Windows PC 上运行的 CapSkip,并已开启 reCAPTCHA 识别功能。 设置指南 会带你完成首次启动的每一步。
  • Deno 2。下面的所有内容都是在 Windows 11 上用 Deno 2.9.6 运行的,之后又用 2.9.7 运行了一遍,使用的是 npm 上的 capskip 1.3.0。
  • 一个 PowerShell 提示符。这些命令使用 PowerShell 语法。
  • 一个表单中带有 reCAPTCHA v2 复选框的页面。示例登录的是一个带有 CSRF token 的 Django 风格表单。请把字段名改成与你的站点一致。
  • 识别工具的地址。在 Local 模式下,CapSkip 只在 127.0.0.1 上响应,仅供本机使用。在 Server 模式下,它监听你的网络地址或公网 IP,这样另一台机器上的脚本就能通过 API 调用它。两种模式的设置位置都是 连接设置.

下面的测量数据来自同一台 PC 上的一个模拟 CapSkip 和一个模拟登录页面(都是为测试搭建的替身),配合真实的 Deno 和真实的 SDK 使用。权限错误、轮询节奏和挂起行为都属于 Deno 和 SDK,所以在你自己的站点上同样成立。识别耗时,以及登录页面的接受和拒绝规则,则只属于这两个模拟程序本身。

第 1 步:安装 Deno 并创建项目

用官方安装程序或 winget 安装 Deno。Deno 的 安装页面 还列出了其他安装方式,比如 Scoop 和 Chocolatey。

# PowerShell: the official installer (or: winget install DenoLand.Deno)
irm https://deno.land/install.ps1 | iex
deno --version

然后创建一个项目文件夹,在运行其他任何命令之前先在里面放一个 deno.json。导入映射会锁定 SDK 版本,让脚本只需写 "capskip",而不必写一长串 npm 说明符。

{
  "imports": {
    "capskip": "npm:[email protected]"
  }
}

在这个文件存在之前,不要运行 deno add。如果你的项目上层文件夹里有 package.json(这在用户文件夹中很常见),Deno 就会把这个项目当作 Node 项目。这时导入 npm:[email protected] 会失败,报错 Could not find "capskip" in a node_modules folder。不带版本的导入则会悄悄加载上层安装的任意版本的 capskip。在测试用的 PC 上,那是较旧的 1.0.2。在没有 deno.json 的情况下运行 deno add,甚至改写了上层的那个 package.json。放在你自己文件夹里的 deno.json,会让 Deno 把这个文件夹当作项目根目录。

第 2 步:导入 SDK 并读取设置

把第 2 步到第 5 步的代码按所示顺序保存到一个名为 solve.js 的文件里。完整的文件在本指南接近结尾的地方还会再给出一遍。Deno 把它当作 ES 模块运行,所以 SDK README 里写的 require 调用在这里会失败,报错 require is not defined。请使用具名导入。默认导入也会失败,因为它给你的是整个 exports 对象:new CapSkip() 会抛出 CapSkip is not a constructor。

import { CapSkip } from "capskip";

// The SDK never reads the environment, so read it here and pass it in.
const host = Deno.env.get("CAPSKIP_HOST") ?? "127.0.0.1";
const port = Number(Deno.env.get("CAPSKIP_PORT") ?? 8080);
const apiKey = Deno.env.get("CAPSKIP_API_KEY") ?? "capskip";
const solver = new CapSkip({ host, port, apiKey });

// The login page to solve, passed on the command line.
const pageUrl = Deno.args[0] ?? "https://example.com/login";

SDK 自己不会读取环境变量,所以由脚本读取它们并传给构造函数。有了这些默认值,文件不需要任何配置就能运行。只有当你在 CapSkip 中开启了 API key validation 时,CAPSKIP_API_KEY 才有意义。

第 3 步:获取页面并保存它的 cookie

Deno 的 fetch 没有 cookie jar,所以由脚本自己维护一个。它用 getSetCookie 读取每一行 Set-Cookie。如果改为对 set-cookie 调用 headers.get,这些行会被拼成一个字符串。cookie 的 Expires 日期里本身带有逗号,所以简单地按逗号拆分,会把 cookie 切在错误的位置。

// Deno's fetch keeps no cookies, so collect every Set-Cookie line.
// (headers.get("set-cookie") would glue them into one string.)
const jar = new Map();

/** @param {Response} response */
function keepCookies(response) {
  for (const line of response.headers.getSetCookie()) {
    const [pair] = line.split(";");
    const eq = pair.indexOf("=");
    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
  }
}
const cookies = () => Array.from(jar, ([k, v]) => `${k}=${v}`).join("; ");

接下来,脚本加载登录页面,并从中读取 sitekey 和 CSRF token。请按你页面的标记调整这两个正则表达式。

const page = await fetch(pageUrl);
keepCookies(page);
const html = await page.text();

// Adjust these two patterns to your page's markup.
const sitekey = html.match(/data-sitekey="([^"]+)"/)?.[1];
const csrf = html.match(/name="csrfmiddlewaretoken" value="([^"]+)"/)?.[1];
if (!sitekey || !csrf) throw new Error("sitekey or CSRF token not found");

第 4 步:向 CapSkip 请求 token

一次调用就完成所有工作。它提交任务、轮询答案并返回 token。SDK 会先立刻轮询一次,然后等待四分之一秒,之后每次把等待时间翻倍,最长 5 秒。在测试中,CapSkip 用一秒完成的识别,大约 1.8 秒后返回结果,与在 Node 下相同。

// code is typed "string or object" (Capy shares the field), so
// String() keeps deno check quiet.
const { code } = await solver.recaptcha(sitekey, pageUrl);
const token = String(code);

结果里有一个 code 字段,它就是 token。SDK 的类型文件把它声明为字符串或对象,因为有些验证码类型会在同一个字段里返回对象。String() 会把它变成普通字符串。后面有一节会给这次调用加上超时。

第 5 步:提交表单并检查结果

脚本把 token 放在 g-recaptcha-response 字段里,与表单的其他字段一起提交。有两个细节很重要。Deno 的 fetch 不发送 Referer,而 Django 这类严格的站点会拒绝没有 Referer 的 HTTPS 表单提交,所以脚本自己设置了它。另外,fetch 调用使用 redirect manual,这样 302 会原样返回,它带来的新会话 cookie 才能被读取。如果用默认的自动跟随重定向,302 上的 cookie 就丢了,登录会带着 200 回到表单页面。

// fetch sends no Referer, and a strict site rejects the post without
// one. redirect "manual" keeps the 302 so we can read its new cookie.
const response = await fetch(pageUrl, {
  method: "POST",
  redirect: "manual",
  headers: { Cookie: cookies(), Referer: pageUrl },
  body: new URLSearchParams({
    csrfmiddlewaretoken: csrf,
    username: "YOUR_USERNAME",
    password: "YOUR_PASSWORD",
    "g-recaptcha-response": token,
  }),
});
keepCookies(response);

被拒绝的登录常常会返回 200,并再次显示表单。所以脚本把 302 以外的任何响应都当作失败,然后再查找只有登录后的页面才会显示的标记。请把 Welcome back 换成你自己站点上的文字。对一个被拒绝的登录报告成功的脚本,比直接失败的脚本更糟。

// A good login answers 302 and rotates the session cookie.
if (response.status !== 302) {
  const body = await response.text();
  throw new Error(`login rejected: ${response.status} ${body}`);
}
const next = new URL(response.headers.get("location") ?? "/", pageUrl);
const home = await fetch(next, { headers: { Cookie: cookies() } });

// Swap "Welcome back" for a marker your own site shows after login.
const ok = (await home.text()).includes("Welcome back");
console.log(ok ? "logged in" : "NOT logged in");
if (!ok) Deno.exit(2);

第 6 步:用最小的权限集运行

在你放行之前,Deno 会拒绝网络、环境变量和文件的访问。要在 Deno 中识别 reCAPTCHA,这个脚本需要两样东西,下面的命令恰好只放行这两样。

deno run `
  --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY `
  --allow-net=127.0.0.1:8080,example.com `
  solve.js https://example.com/login

env 列表写明了三个 CAPSKIP 变量。net 列表写明了 CapSkip 的地址和端口,以及站点。请把 example.com 和页面地址换成你自己的站点。去掉任何一项,运行都会失败,而且错误信息会指出缺的是什么。

权限允许什么没有它时你会看到什么
env,三个名称读取 CAPSKIP 变量NotCapable: Requires env access to "CAPSKIP_HOST",后面跟着一条建议,让你带上 env 标志重新运行。未设置的变量同样会被拒绝,所以默认值也帮不上忙。
net,CapSkip 的主机和端口SDK 对 CapSkip 的调用一个 NetworkException,其消息是 Requires net access to "127.0.0.1:8080",后面跟着针对 net 标志的同样建议。
net,站点你的 fetch 对页面和表单的调用一个指明了该站点的 NotCapable 错误。

关于 net 列表的几条规则。不带端口的主机会放行所有端口,所以 example.com 同时涵盖 80 和 443。带端口的条目只放行那一个端口。127.0.0.1 和 127.0.0.2 是不同的主机。而且重定向到另一个主机名时,那个主机也要在列表里。一个页面从 example.com 跳转到 www.example.com,在第二次请求时会失败,直到你把后者加进去。除此之外不需要任何东西:不需要 read、run、sys 或 import 权限。

在终端里,Deno 会询问你,而不是直接失败。它对每个变量和每个主机各询问一次,回答 no 会得到同样的错误。在计划任务中没有终端,所以会直接失败。allow-all 标志(-A)会去掉所有这些检查,所以只在快速试验时使用它。

你可以把同样的命令作为任务放进 deno.json,这样输入的就更少。下面第一个代码块是带有导入映射和 solve 任务的 deno.json,第二个则在 PowerShell 中设置环境变量并运行它。Deno 会把你写在任务名后面的页面地址追加到命令末尾。

{
  "imports": {
    "capskip": "npm:[email protected]"
  },
  "tasks": {
    "solve": "deno run --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY --allow-net=127.0.0.1:8080,example.com solve.js"
  }
}
$env:CAPSKIP_HOST = "127.0.0.1"
$env:CAPSKIP_PORT = "8080"
deno task solve https://example.com/login

CAPSKIP_HOST 请使用 IP 地址。如果你设置的是 localhost 这样的名称,这个名称本身也必须在 net 列表里,否则 SDK 会以 getaddrinfo EPERM localhost 失败。把它列进去同样是个陷阱。在 CapSkip 没有运行时,localhost 会让 Deno 去尝试 IPv6 地址 [::1],而它不在列表上。这个错误是从计时器里抛出的,而不是从 SDK 调用里抛出的,所以它不是 NetworkException,try 和 catch 也捕获不了它。

区分被拦截的调用和没有运行的 CapSkip

两者都会以 NetworkException 的形式出现,所以一个总是写着 "is CapSkip running?" 的处理程序会把你引向错误的方向。区别在于消息和原因。被拦截的调用,其消息以 Requires net access 开头,原因是 Deno 的 NotCapable 错误。如果 CAPSKIP_HOST 是一个名称,消息就是 getaddrinfo EPERM,原因则带有代码 EPERM。没有运行的 CapSkip,其消息以 connect ECONNREFUSED 开头。在 Windows 上的 Deno 下,它还要花大约两秒才会失败,而在 Node 下只要几毫秒。一个在报告错误之前似乎停顿了一下的脚本,并没有挂起。下面的函数会检查这两种拦截情况。最后的完整脚本会在一个包住整个流程的 catch 块里调用它。

import { ApiException, NetworkException, TimeoutException } from "capskip";

// Deno's permission errors arrive wrapped in NetworkException. The
// cause is NotCapable for an IP host, and has the code EPERM for a
// host name. Tell them apart from a CapSkip that is not running.
/** @param {any} err */
function explain(err) {
  const cause = err?.cause;
  if (err instanceof NetworkException) {
    const denied = cause instanceof Deno.errors.NotCapable ||
      cause?.code === "EPERM";
    return denied
      ? `Deno blocked the call, add a flag: ${err.message}`
      : `CapSkip is not reachable: ${err.message || cause?.code}`;
  }
  if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;
  if (err instanceof TimeoutException) return `Too slow: ${err.message}`;
  return String(err?.stack ?? err);
}

防止毫无回应的 CapSkip 让脚本挂起

SDK 没有给它的连接设置任何超时。如果 CapSkip 接受了一次调用,之后却始终不作答,脚本就会一直等下去。Deno 不会输出任何内容,也不会退出。SDK 的 recaptchaTimeout 设置帮不上忙。它只在两次轮询之间检查,所以当提交调用或某次轮询正在等待答案时,它根本不会触发。Node 的表现也一样。不加任何包装的话,唯一的出路就是 Ctrl+C。

请自己给这次调用设一个时间上限。常用的工具是 AbortSignal.timeout,但在 Deno 里,这里恰恰不该用它。带有 abort 监听器的超时信号,会让进程一直存活,直到计时器触发。给一次只花了 1.8 秒的识别套上 30 秒的时限,结果脚本在 30 秒后才退出。而一个普通的 setTimeout,只要在调用结束时把它清除,成功后就会立即退出。

// The SDK sets no socket timeout, so a CapSkip that accepts the call
// and never answers would hang the script. Race it against a timer,
// and clear the timer so the process can exit on success.
/**
 * @param {Promise<any>} promise
 * @param {number} ms
 */
function withTimeout(promise, ms) {
  const { promise: limit, reject } = Promise.withResolvers();
  const timer = setTimeout(
    () => reject(new Error(`no answer in ${ms} ms`)),
    ms,
  );
  return Promise.race([promise, limit]).finally(() => clearTimeout(timer));
}

然后用这一段替换第 4 步的代码块。它包住的是同一个识别调用。把 explain 和 withTimeout 粘贴到使用它们的那几行的上方任意位置。SDK 自己会在 300 秒后放弃,所以 330 秒的时限只有在 CapSkip 毫无回应时才会触发。

// The SDK gives up after 300 s by itself, so a 330 s budget only
// fires when CapSkip goes silent. code is typed "string or object"
// (Capy shares the field), so String() hands the form a plain string.
const solving = solver.recaptcha(sitekey, pageUrl);
const { code } = await withTimeout(solving, 330_000);
const token = String(code);

计时器先到时,尚未结束的连接仍然是打开的,所以完整示例里的 catch 块以 Deno.exit 收尾。没有它,进程会继续在套接字上等下去。测试中,无论是从不应答提交请求的 CapSkip,还是从不应答结果轮询的 CapSkip,都在时限到了之后以这个错误结束。退出码是 1。

对脚本做类型检查

Deno 可以按 SDK 自带的类型来检查 JavaScript,但除非你要求,否则它对普通的 .js 文件什么也不做。对普通 .js 文件运行 deno check 不会报告任何内容,即使代码里有类型错误。在 deno.json 里打开 checkJs,检查才会真正生效。

{
  "imports": {
    "capskip": "npm:[email protected]"
  },
  "compilerOptions": {
    "checkJs": true
  }
}
deno check solve.js

开启检查后,拼错的构造函数选项(比如把 host 写成 hots)会报错。而识别调用内部拼错的选项不会报错,因为选项类型接受任意键,所以那里的拼写错误能通过编译,却什么作用也没有。把这个检查当作辅助,而不是保证。

编译成单个 exe

因为权限是命令的一部分,deno compile 可以把它们固化进去。这样,在 Deno 中识别 reCAPTCHA 的脚本就能作为一个 solve.exe 发布,在没有安装 Deno 的 PC 上也能运行。这很适合同一台 Windows 机器上同时运行 CapSkip 和脚本的情况。

deno compile `
  --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY `
  --allow-net=127.0.0.1:8080,example.com `
  --output solve.exe solve.js
.\solve.exe https://example.com/login

有四点需要知道。第一次编译会从 dl.deno.land 下载一个约 33 MB 的运行时,生成的 exe 约 80 MB。环境变量仍然有效,所以 CAPSKIP_PORT 会生效。权限在编译时就已固定,exe 没有任何标志可以放宽它们:如果传入 allow-all 标志,它只会变成程序的第一个参数。另外,在 exe 里,被拒绝的调用会提示你要在编译时指定该权限。

测试中,一次模仿 Task Scheduler 的启动运行了这个 exe,并以退出码 0 退出。它用 System32 作为工作文件夹,没有控制台,PATH 里也没有 Deno。没有注册真正的计划任务,所以请阅读 在 Windows Task Scheduler 作业中识别验证码的指南 来了解那里重要的时间限制和重启设置。

在另一台机器上运行 CapSkip

脚本和识别工具不必共用一台 PC。在连接设置里把 CapSkip 切换到 Server 模式,它就会监听你的网络地址或公网 IP。然后改两处:CAPSKIP_HOST 和 net 列表。如果你忘了把新地址加进 net 列表,错误信息里就会指出它。

$env:CAPSKIP_HOST = "192.168.1.50"
$env:CAPSKIP_PORT = "8080"
$env:CAPSKIP_API_KEY = "YOUR_API_KEY"
deno run `
  --allow-env=CAPSKIP_HOST,CAPSKIP_PORT,CAPSKIP_API_KEY `
  --allow-net=192.168.1.50:8080,example.com `
  solve.js https://example.com/login

如果链路要经过公网,请使用静态公网 IP。开启 API key validation,并在 CAPSKIP_API_KEY 里使用与上面代码块所设置的同一个密钥。Server 模式启动时,CapSkip 会向所有地址开放端口。要想只让你预期的机器访问,就为其他地址添加一条 Windows Firewall 阻止规则。识别工具仍然是你自己的 Windows 机器,识别也仍然不计量。

完整脚本

下面是完整的 solve.js,其中超时包装函数和错误处理函数都已就位。把它保存在 deno.json 旁边,然后用第 6 步的命令运行。

import {
  ApiException,
  CapSkip,
  NetworkException,
  TimeoutException,
} from "capskip";

// The SDK never reads the environment, so read it here and pass it in.
const host = Deno.env.get("CAPSKIP_HOST") ?? "127.0.0.1";
const port = Number(Deno.env.get("CAPSKIP_PORT") ?? 8080);
const apiKey = Deno.env.get("CAPSKIP_API_KEY") ?? "capskip";
const solver = new CapSkip({ host, port, apiKey });

// The login page to solve, passed on the command line.
const pageUrl = Deno.args[0] ?? "https://example.com/login";

// Deno's fetch keeps no cookies, so collect every Set-Cookie line.
// (headers.get("set-cookie") would glue them into one string.)
const jar = new Map();

/** @param {Response} response */
function keepCookies(response) {
  for (const line of response.headers.getSetCookie()) {
    const [pair] = line.split(";");
    const eq = pair.indexOf("=");
    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
  }
}
const cookies = () => Array.from(jar, ([k, v]) => `${k}=${v}`).join("; ");

// Deno's permission errors arrive wrapped in NetworkException. The
// cause is NotCapable for an IP host, and has the code EPERM for a
// host name. Tell them apart from a CapSkip that is not running.
/** @param {any} err */
function explain(err) {
  const cause = err?.cause;
  if (err instanceof NetworkException) {
    const denied = cause instanceof Deno.errors.NotCapable ||
      cause?.code === "EPERM";
    return denied
      ? `Deno blocked the call, add a flag: ${err.message}`
      : `CapSkip is not reachable: ${err.message || cause?.code}`;
  }
  if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;
  if (err instanceof TimeoutException) return `Too slow: ${err.message}`;
  return String(err?.stack ?? err);
}

// The SDK sets no socket timeout, so a CapSkip that accepts the call
// and never answers would hang the script. Race it against a timer,
// and clear the timer so the process can exit on success.
/**
 * @param {Promise<any>} promise
 * @param {number} ms
 */
function withTimeout(promise, ms) {
  const { promise: limit, reject } = Promise.withResolvers();
  const timer = setTimeout(
    () => reject(new Error(`no answer in ${ms} ms`)),
    ms,
  );
  return Promise.race([promise, limit]).finally(() => clearTimeout(timer));
}

try {
  const page = await fetch(pageUrl);
  keepCookies(page);
  const html = await page.text();

  // Adjust these two patterns to your page's markup.
  const sitekey = html.match(/data-sitekey="([^"]+)"/)?.[1];
  const csrf = html.match(/name="csrfmiddlewaretoken" value="([^"]+)"/)?.[1];
  if (!sitekey || !csrf) throw new Error("sitekey or CSRF token not found");

  // The SDK gives up after 300 s by itself, so a 330 s budget only
  // fires when CapSkip goes silent. code is typed "string or object"
  // (Capy shares the field), so String() hands the form a plain string.
  const solving = solver.recaptcha(sitekey, pageUrl);
  const { code } = await withTimeout(solving, 330_000);
  const token = String(code);

  // fetch sends no Referer, and a strict site rejects the post without
  // one. redirect "manual" keeps the 302 so we can read its new cookie.
  const response = await fetch(pageUrl, {
    method: "POST",
    redirect: "manual",
    headers: { Cookie: cookies(), Referer: pageUrl },
    body: new URLSearchParams({
      csrfmiddlewaretoken: csrf,
      username: "YOUR_USERNAME",
      password: "YOUR_PASSWORD",
      "g-recaptcha-response": token,
    }),
  });
  keepCookies(response);

  // A good login answers 302 and rotates the session cookie.
  if (response.status !== 302) {
    const body = await response.text();
    throw new Error(`login rejected: ${response.status} ${body}`);
  }
  const next = new URL(response.headers.get("location") ?? "/", pageUrl);
  const home = await fetch(next, { headers: { Cookie: cookies() } });

  // Swap "Welcome back" for a marker your own site shows after login.
  const ok = (await home.text()).includes("Welcome back");
  console.log(ok ? "logged in" : "NOT logged in");
  if (!ok) Deno.exit(2);
} catch (err) {
  console.error(explain(err));
  Deno.exit(1);
}

在 Deno 中识别 reCAPTCHA 时的常见错误

没有 try 块时,Deno 会先打印类名,如第一列所示。完整脚本打印的是同样的消息,只是前面加上了它自己的前缀。

你所看到的原因修复
Requires env access to "CAPSKIP_HOST"env 权限缺少一个名称列出全部三个 CAPSKIP 名称
NetworkException: Requires net access to "127.0.0.1:8080"CapSkip 的主机和端口不在 net 列表里把它们加进去,站点也要加
NetworkException: connect ECONNREFUSED,大约 2 秒之后出现CapSkip 没有在运行,或者端口不对启动 CapSkip,并在连接设置里检查端口
NetworkException: getaddrinfo EPERM localhost用的是主机名,但 net 列表里只有 IP在 CAPSKIP_HOST 中使用 127.0.0.1
ReferenceError: require is not defined在 ES 模块中使用了 CommonJS 的调用使用具名导入
TypeError: CapSkip is not a constructor默认导入得到的是整个 exports 对象按名称导入 { CapSkip }
Could not find "capskip" in a node_modules folder项目上层的 package.json 接管了导入的解析在项目文件夹里放一个 deno.json
没有输出,也不退出CapSkip 接受了调用,却始终没有应答用 withTimeout 包住这次调用,并调用 Deno.exit
ApiException: ERROR_CAPTCHA_UNSOLVABLECapSkip 尝试过,但失败了提交一个新任务,相关说明见 ERROR_CAPTCHA_UNSOLVABLE

常见问题

可以不用这些标志,直接用 allow-all 吗?

可以用,但这样就失去了在 Deno 下运行的意义。脚本会因此拥有对所有内容的网络、文件和环境变量访问权限,连它导入的包也包括在内。一个含三个名称的列表和一个含两个主机的列表,都很短,完全值得保留。

这在 Deno Deploy 或其他托管运行时上能用吗?

这里没有测试 Deno Deploy 和其他托管运行时。别人数据中心里的服务器无法访问你 PC 上的 127.0.0.1。所以请在一台你能控制的 Windows 机器上以 Server 模式运行 CapSkip,配上静态公网 IP,并让 CAPSKIP_HOST 指向它。平台还必须允许向该地址和端口发起出站连接,所以请检查它的网络规则。 在 Netlify Functions 中识别验证码的指南 介绍了如何在托管函数中进行同样的 Server 模式设置。

为什么用 SDK,而不是用 fetch 直接调用 CapSkip?

CapSkip 兼容 2captcha API,所以用 fetch 直接请求 in.php 和 res.php 也行得通。但这样轮询循环、退避和错误映射都要你自己写。SDK 已经按一个从四分之一秒开始的节奏轮询,返回 token,并抛出 ApiException、NetworkException 和 TimeoutException。代价是两个细节:权限错误是被包装后才到达的,而且 SDK 没有设置套接字超时。

其他验证码类型在 Deno 中也是同样的用法吗?

本指南只在 Deno 下运行了 reCAPTCHA v2。Turnstile 和 GeeTest 这类基于 token 的类型调用的是同样的端点,所以同样的两个标志应该适用,但这一点没有测试过。以文件路径给出的图片验证码还需要 read 权限,以 URL 给出的则需要把那个站点加进 net 列表。在复制某个调用之前,请先在 Node.js 验证码识别页面 上查看各个类型的选项。

简短版结论

要在 Deno 中识别 reCAPTCHA,就在你自己的文件夹里放一个带有 SDK 导入的 deno.json,并导入具名导出。自己读取 CapSkip 的设置。运行时只带上 env 列表,以及只包含 CapSkip 的地址和你的站点的 net 列表,不要多放任何东西。通过原因来区分被拒绝的调用和没有运行的 CapSkip。用普通计时器限定识别的时间,因为 AbortSignal.timeout 会让进程一直开着,而毫无回应的 CapSkip 否则会让它永远挂起。

一个编译一次、再按计划运行的脚本,一个月可以做数千次识别。用在你自己的 Windows 机器上运行的 无限量验证码识别工具 来完成这些识别,这个数字就永远不会变成账单。