如何在 Node-RED 中用 function 节点识别验证码

在 Node-RED 里加一个验证码步骤:走原始 API 就是两个节点,愿意写五行 JavaScript 就是一个 function 节点。两种方式调用的都是跑在你自己硬件上的识别工具,所以都不会按次计费。真正让人卡住的不是识别本身。而是 function 节点在你于 settings.js 里打开开关之前无法 require npm 包,以及 Node-RED 常常跑在 Raspberry Pi 上,而识别工具并不在上面。
你需要什么
- Node-RED 3 或更新版本,跑在任何你喜欢的地方。
- CapSkip 已启动并在监听。Node-RED 与它在同一台 Windows 机器上时用 Local mode(本地模式),不在同一台时用 Server mode(服务器模式)。两种模式都说明在 连接设置.
- 受保护表单的页面 URL,以及它的 sitekey。
- 如果你想走 SDK 路线而不是原始 API 路线,需要有编辑 settings.js 的权限。
这里没有任何环节需要浏览器。Node-RED 不是在驱动 Chrome,而是在发 HTTP 请求,所以流程从页面 HTML 里读出 sitekey,再把 token 当成普通表单字段提交回去。
从流程中调用识别工具的两种方式
开始连线之前先选一种,因为两者做出来的流程差别很大。
| 选哪条路线 | 代价是什么 | 什么时候该选它 |
|---|---|---|
| 用 http request 节点直接调原始 API | 一个提交节点、一个 delay、一个轮询节点,再加一个 switch 做循环 | 你无法编辑 settings.js,或者你希望流程在画布上一目了然 |
| 在 function 节点里使用 Node SDK | 一行 settings.js 配置,加上 Setup 标签页里的一个模块 | 你希望轮询、退避和超时都由它替你处理 |
第二条路线更短,原因值得了解。原始 API 让你先等十五秒,然后每五秒轮询一次,手工搭的流程会照字面执行。而 SDK 从 250ms 开始轮询,再逐步退避到一个上限,所以它拿到 token 的速度通常明显快过 delay 节点。
路线一:http request 节点加原始 API
这个 API 兼容 2captcha,也就是两个端点。你向 in.php 提交,拿回一个 id,然后带着这个 id 轮询 res.php,直到它不再回答“结果还没准备好”。四个节点,连成一个循环。
| 流程中的节点 | 设置 |
|---|---|
| 负责提交的 http request 节点 | POST 到 http://127.0.0.1:8080/in.php,Return 设为已解析的 JSON 对象 |
| 保存 id 的 function 节点 | 把 msg.payload.request 存成 msg.captchaId |
| 一个 delay 节点 | 固定延时,reCAPTCHA v2 用 15 秒 |
| 负责轮询的 http request 节点 | GET 到 http://127.0.0.1:8080/res.php,Return 设为已解析的 JSON 对象 |
| 一个 switch 节点 | 只要回复仍然是 CAPCHA_NOT_READY,就绕回 delay 节点 |
提交节点的请求体取自 msg.payload,所以要在它前面用一个 function 节点把这个对象构造好。把 json 设为 1,回复才会是 JSON,而不是旧的竖线分隔文本,这样你就省了一次字符串切分。
// Feed this into the submit node. No npm module needed.
msg.url = "http://127.0.0.1:8080/in.php";
msg.method = "POST";
msg.payload = {
key: env.get("CAPSKIP_KEY") || "capskip",
method: "userrecaptcha",
googlekey: "YOUR_SITEKEY",
pageurl: "https://example.com/page-with-recaptcha",
json: 1
};
return msg;轮询节点需要把 id 放回查询字符串里。在 function 节点里拼好 URL,这样 http request 节点就不需要做任何模板替换。
// After the delay. Loop back here until the answer arrives.
const key = env.get("CAPSKIP_KEY") || "capskip";
msg.url = "http://127.0.0.1:8080/res.php?key=" + key +
"&action=get&id=" + msg.captchaId + "&json=1";
msg.method = "GET";
return msg;关于回复有两点。status 为 0 且 request 为 CAPCHA_NOT_READY 不是错误,而是结果还在处理中,这条分支就是 switch 节点要绕回 delay 的那一条。status 为 1 表示 msg.payload.request 里就是 token。给循环设一个合理的次数上限,这样真正无法识别的挑战才不会一直空转。关于这个轮询状态,更详细的说明见指南 CAPCHA_NOT_READY 回复,全部参数都列在 CapSkip API 文档.
路线二:在 function 节点里使用 SDK
function 节点跑在一个沙箱里,默认无法访问 npm 包。有两个设置项控制这一点,而且它们的行为并不一样。
老一点的那个是 functionGlobalContext:你在 settings.js 里 require 模块,再在节点内部用 global.get 取回来。它能用,但实例里的每个 function 节点都能看到它,而且加一个模块就要重启 Node-RED。
更好的那个是 functionExternalModules。在 settings.js 里把它设为 true,function 节点就会多出一个 Setup 标签页,你在那里填模块名和它对应的变量名。部署时 Node-RED 会把它装进你的用户目录,而且只有那个节点能看到它。
// settings.js, in your Node-RED user directory.
module.exports = {
// Lets a function node declare its own npm modules
// on the Setup tab, installed on deploy.
functionExternalModules: true,
// The older, instance-wide alternative.
// functionGlobalContext: { capskip: require("capskip") },
}重启 Node-RED,打开一个 function 节点,切到 Setup 标签页,把模块 capskip 加进去,变量名也写 capskip。部署一次就会自动安装。从此节点代码里可以直接使用它。
第 1 步:在 function 节点里完成识别
一次识别是一个耗时若干秒的网络请求,所以节点必须异步结束。也就是说不能直接 return 消息。Node-RED 在这里的规则很明确:在 async 块里做事,用 node.send 把消息发出去,然后从节点代码里 return null,避免同一条消息被发两次。
// npm install capskip - or add it on the Setup tab
const solver = new capskip.CapSkip({
host: "127.0.0.1",
port: 8080,
apiKey: env.get("CAPSKIP_KEY") || "capskip"
});
(async () => {
try {
const result = await solver.recaptcha(msg.sitekey, msg.pageUrl);
msg.token = result.code; // inject this into the form
node.send(msg);
} catch (err) {
node.error(err, msg); // routes to a catch node
}
node.done();
})();
return null;把 msg 作为第二个参数传给 node.error,catch 节点才能接住这个失败。不传的话,错误只会落在 debug 侧边栏里,流程就这么停了,这也是 Node-RED 验证码分支看起来什么都没做的最常见原因。
一个方法就覆盖了 reCAPTCHA v2、Invisible、Enterprise 和 v3。这些变体是选项,而不是不同的调用,所以 Invisible 小组件就是同一行代码加一个 invisible 设为 1 的选项对象,v3 则是 version 设为 v3 再加一个 action。Turnstile 和极验(GeeTest)有各自的方法,形态完全一样,全部方法都列在 验证码识别 SDK 页面.
第 2 步:把整个流程放进一个节点
抓取页面,从 HTML 里取出 sitekey,识别,然后把 token 连同表单其余字段一起提交回去。如果你希望整个流程就是一个 inject 节点、这个 function 节点和一个 debug 节点,那就粘贴这个版本。
// Module on the Setup tab: capskip. fetch is built in
// from Node 18, which Node-RED 3 and 4 both require.
const PAGE = "https://example.com/page-with-recaptcha";
const solver = new capskip.CapSkip({ host: "127.0.0.1", port: 8080 });
(async () => {
try {
const html = await (await fetch(PAGE)).text();
const found = html.match(/data-sitekey=["']([^"']+)/);
if (!found) { throw new Error("No data-sitekey on the page."); }
// Solve, then submit straight away. Tokens go stale.
const result = await solver.recaptcha(found[1], PAGE);
const reply = await fetch(PAGE, {
method: "POST",
body: new URLSearchParams({
"g-recaptcha-response": result.code
})
});
msg.payload = { status: reply.status, token: result.code };
node.send(msg);
} catch (err) {
node.error(err, msg);
}
node.done();
})();
return null;识别要放在紧挨着提交的那一步,绝不要放在更早、后面还要等别的东西的分支里。reCAPTCHA token 的有效期大约两分钟,而且只能用一次,所以一个先识别、再停在 delay 节点、最后才提交的流程会被拒绝,而这个拒绝看起来完全不像识别工具的问题。这种失败模式值得读一读指南 reCAPTCHA token 过期.
把 Node-RED 和识别工具跑在不同机器上
这一点在 Node-RED 里比在多数工具里更重要,因为相当大一部分安装是在 Raspberry Pi、NAS 或一台小型 Linux 机器上,而 CapSkip 是 Windows 应用。如果这就是你的情况,那么 127.0.0.1 指的是那台 Pi,识别工具并不在上面,请求还没到 API 就以连接被拒绝告终。
答案是 Server mode(服务器模式),它只是一个设置项,不是另一个产品。Local mode(本地模式)绑定 127.0.0.1,只响应本机。Server mode 绑定你的内网或公网 IP,于是跑在 Pi、容器主机或托管 Node-RED 实例上的流程可以用同一套 API 调用那台 Windows 机器。固定公网 IP 能让这个地址保持不变。两种模式下硬件都还是你自己的,也都不计次,所以繁忙的流程并不比清闲的流程花更多钱。
// Same call, same SDK. Only the host moves.
const solver = new capskip.CapSkip({
host: "10.0.0.12",
port: 8080,
apiKey: env.get("CAPSKIP_KEY")
});一旦识别工具监听在网络地址上,就打开密钥校验,并给每个 Node-RED 实例分配各自的密钥,这样吊销其中一个不会影响其他。把密钥放进环境变量,而不要写在节点代码里:flows 文件就是磁盘上的 JSON,还经常被提交进 git 仓库。两种模式的完整步骤见 CapSkip 设置指南.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| capskip is not defined | 模块根本没有在 Setup 标签页里声明 | 把 functionExternalModules 设为 true,再添加模块并部署 |
| function 节点什么都不输出 | 消息是被 return 的,而不是在 async 块里发出去的 | 调用 node.send,并在节点代码里 return null |
| 流程停住了,也看不到错误 | 调用 node.error 时没有传消息参数 | 把 msg 作为第二个参数传入,并接上一个 catch 节点 |
| connect ECONNREFUSED 127.0.0.1:8080 | Node-RED 不在运行识别工具的那台机器上 | 把识别工具切到 Server mode,并把 host 设为它的地址 |
| 轮询循环停不下来 | switch 节点没有设置尝试次数上限 | 用上下文变量统计次数,超过上限就放弃 |
| 表单拒绝了一个看起来没问题的 token | 识别是在好几个节点之前做的 | 紧挨着提交那一步识别,不要放在更早的分支里 |
| ERROR_GOOGLEKEY | sitekey 与那个页面 URL 不匹配 | 从你要提交的那个页面重新读取 data-sitekey |
常见问题
我一定要用 SDK 吗,还是 http request 节点就够了?
两种都行。http request 路线不需要 settings.js 的访问权限,每一步都留在画布上可见,有些团队为了便于审计更喜欢这样。SDK 路线替你处理轮询、退避和超时,而且通常更快拿到 token,因为它在四分之一秒后就开始查询,而不是十五秒后。
托管的 Node-RED 实例能连到我桌上的识别工具吗?
只有在 Server mode 下可以。托管实例跑在别人的基础设施上,所以那里的 127.0.0.1 是他们的容器,不是你的机器。把识别工具绑定到一个可达的地址,用防火墙规则只放行该平台的出站地址,并打开密钥校验。连接设置页面讲了完整的配置过程。
怎样避免某个流程把识别工具压垮?
在识别节点前面放一个设为限速模式的 delay 节点。它会把消息排队,再按固定速率放出去,当三个定时任务都指向同一台机器时,这正是你需要的限流。在没有上限的循环里做识别,是批量任务变成一堆超时的常见原因。
这和在 n8n 里做是一回事吗?
识别工具这一侧完全一样,流程这一侧不一样。n8n 的 Code 节点跑在一个锁死的沙箱里,不能安装 npm 包,所以在那边只能用 HTTP 节点。Node-RED 则愿意为单个 function 节点安装一个包,这也是这里存在 SDK 路线的原因。n8n 版本写在 n8n 验证码工作流指南.
简短版结论
打开 functionExternalModules,在 Setup 标签页里添加 capskip,然后在 async 块里完成识别,块的结尾调用 node.send 并 return null。接上一个 catch 节点,并把消息传给 node.error,这样失败才看得见。如果 Node-RED 在 Pi 上而识别工具在 Windows 上,那就是 Server mode 加上改一个 host 字符串的事。Node.js 的其余接口见 Node.js 验证码识别页面,reCAPTCHA 的各项选项见 reCAPTCHA v2 识别页面.
在把那个流程设成每五分钟跑一次之前,还有最后一点值得知道。CapSkip 是一款 本地验证码识别工具 ,跑在你已经拥有的硬件上,所以一个永远每五分钟触发一次的流程,和你手动触发一次的成本完全相同。
