如何在 Pipedream 代码步骤中识别验证码(Node.js)

pipedream captcha - How to Solve CAPTCHAs in a Pipedream Code Step (Node.js)

在 Pipedream 里识别验证码比在 Zapier 或 Make.com 里做同样的事更容易,因为代码步骤是一个真正的 Node.js v20 运行时,支持 npm 导入。你什么都不用装,写普通的 JavaScript 即可,CapSkip SDK 的表现和在你笔记本上一模一样。有两点不同,而且都和代码在哪里运行有关。Pipedream 在它自己的云上执行,所以识别程序必须能从公网访问到。另外,一次工作流执行的时间上限比一次 reCAPTCHA 识别还短,这决定了你要写一个步骤还是两个。

你需要什么

  • 一个带代码步骤的 Pipedream 工作流。下文使用的运行时是 Node.js。再往下还有 Python 版本。
  • CapSkip 以 Server 模式运行在一台能从公网访问、并拥有静态公网 IP 的机器上。
  • 你要自动化的站点的 sitekey 和页面 URL。
  • 两个 Pipedream 环境变量,分别保存识别程序的地址和密钥。

为什么回环地址在这里行不通

Pipedream 工作流运行在 Pipedream 自己的基础设施上,位于 AWS us-east-1 网络中。代码步骤内部发往 127.0.0.1 的请求会解析到该步骤所在的容器,而不是你桌上的机器。那里没有任何东西在监听,SDK 会抛出 NetworkException。

CapSkip 有两种连接模式,答案是第二种。Local 绑定 127.0.0.1,只服务本机,当自动化和识别程序在同一台机器上时这样最合适。Server 绑定你的网络地址或公网 IP,这样托管平台就能通过 API 访问同一台 Windows 机器。建议使用静态公网 IP,因为会轮换的家宽地址会在凌晨三点悄无声息地把工作流搞挂。两种模式的设置都在 连接设置.

Server 模式只改变识别程序运行的位置,其他什么都不变。它仍然是你自己的硬件,仍然不计量,所以每月触发一万次的工作流和触发十次的花费完全相同。

第 1 步:把地址放进环境变量

不要把公网 IP 硬编码在步骤里。Pipedream 有工作区环境变量,代码步骤可以从普通的进程环境中读取它们。创建两个。

变量名值
名为 CAPSKIP_HOST 的那个识别程序所在机器的公网 IP,不带协议头,不带端口
名为 CAPSKIP_API_KEY 的那个你在 CapSkip 应用中为这个工作流生成的密钥

给这个工作流单独的密钥,不要共用。当某个密钥泄漏到共享工作区而需要吊销时,不该把你其余的自动化一起拖下水。

第 2 步:一个代码步骤完成整个识别

只要你导入,Pipedream 就会立刻安装对应的 npm 包,所以没有安装步骤,也没有包描述文件。导入说明符同时也是你锁定版本的地方,对于无人值守运行的东西,锁版本很值得做。

// npm install capskip - Pipedream installs it from this import.
// The package is CommonJS, so take the default and destructure.
import capskip from "capskip";

const { CapSkip } = capskip;

export default defineComponent({
  async run({ steps, $ }) {
    const solver = new CapSkip({
      host: process.env.CAPSKIP_HOST,
      port: 8080,
      apiKey: process.env.CAPSKIP_API_KEY,
    });

    const result = await solver.recaptcha(
      "YOUR_SITEKEY",
      "https://example.com/page-with-recaptcha"
    );

    return result.code;   // the token, for the next step
  },
});

这就是全部了。每种 reCAPTCHA 变体都是同一个方法,只是选项对象不同:invisible 设为 1、enterprise 设为 1,或者 version 设为 v3 并带上 action 名称。Turnstile 和极验有各自的方法,形态相同,完整参数列表见 CapSkip API 文档.

步骤返回的任何东西都会进入工作流的导出,因此后面的步骤可以用该步骤自己的名称读到 token。如果你更想给它加个标签,就用导出辅助方法。

// A named export reads better downstream than a bare return.
$.export("token", result.code);

// The next step then reads steps.solve_captcha.token

执行超时,以及一个步骤何时不再够用

下面这个限制决定了其他一切。Pipedream 的一次执行,对 HTTP 和邮件触发器默认时间上限是 30 秒,对定时触发器是 60 秒。你可以在工作流设置里调高,免费档最高 300 秒,付费档最高 750 秒。

一次 reCAPTCHA v2 识别通常远在 30 秒以内完成,但通常不等于总是,而 SDK 自己的上限是 recaptchaTimeout,为 300 秒。所以上面那个单步版本的可靠程度,正好等于你的超时设置。如果工作流上限低于识别耗时,执行会在轮询到一半时被杀掉,你会得到一次失败运行,里面没有任何有用的信息。

你的情况该怎么做
用量低,而且你可以把执行上限调到 300 秒保留单步版本。在工作流设置里调高超时
用量高,或者你在为执行时长付费拆开它,使用下面介绍的 rerun 辅助方法
耗时更长的 Turnstile 挑战页或极验拆开它。这两类最容易超出较短的时间上限

第 3 步:用 rerun 辅助方法拆开它

Pipedream 有一个大多数自动化平台都没有的轮询原语。flow rerun 辅助方法会结束当前步骤,等待一段时间,然后带着你交给它的一小段状态再次运行同一个步骤。等待期间工作流并不在执行,所以慢速识别不会让你付出代价,也不会撞上时间上限。

有三点让它成立。运行计数从 1 开始,每次 rerun 递增。你传入的上下文对象在下一轮里可以读到。超过重试上限时,工作流会继续走到下一个步骤,而不是失败,所以如果你不想要这个行为,就主动抛出异常。

// No SDK here. The raw endpoints suit a step that exits
// between polls, because nothing has to stay in memory.
const MAX_RETRIES = 20;
const DELAY = 15000;   // 15s, the recommended first wait for v2

export default defineComponent({
  async run({ steps, $ }) {
    const { run } = $.context;
    const base = `http://${process.env.CAPSKIP_HOST}:8080`;
    const key = process.env.CAPSKIP_API_KEY;

    if (run.runs === 1) {
      const params = new URLSearchParams({
        key,
        method: "userrecaptcha",
        googlekey: "YOUR_SITEKEY",
        pageurl: "https://example.com/page-with-recaptcha",
        json: "1",
      });
      const submitted = await fetch(`${base}/in.php?${params}`);
      const { request: id } = await submitted.json();

      // The id survives into the next run through the context.
      return $.flow.rerun(DELAY, { id }, MAX_RETRIES);
    }

    const { id } = $.context.run.context;
    const polled = await fetch(
      `${base}/res.php?key=${key}&action=get&id=${id}&json=1`
    );
    const data = await polled.json();

    if (data.request !== "CAPCHA_NOT_READY") {
      return data.request;   // the token
    }
    if (run.runs === MAX_RETRIES + 1) {
      throw new Error("Solve did not finish in time");
    }
    return $.flow.rerun(DELAY, { id }, MAX_RETRIES);
  },
});

那个未就绪响应的拼写很容易让人栽跟头。它是 CAPCHA_NOT_READY,少了一个 T,而且它不是错误:它表示答案还没算完,你应该再轮询一次。把它当成失败是手写轮询循环里最常见的 bug,这里还有 一篇关于 CAPCHA_NOT_READY 响应的完整说明.

关于那个接口还有一点。结果只能读取一次。如果你先把响应体打印出来,之后又在别的步骤里再读一次,第二次读到的是空的,看起来就像识别失败了。

拿到 token 立刻发出去

一个 reCAPTCHA token 大约有两分钟有效期。在工作流里,这比听上去更容易浪费掉,因为延时步骤、缓慢的 HTTP 调用,或者等待过久的一次 rerun,都在吃同一份预算。把提交 token 的步骤紧接在产生它的步骤之后,不要把 token 攒起来留着以后用。完整说明见 reCAPTCHA token 过期指南.

Python 版本

Pipedream 也能运行 Python 3.12 代码步骤,并以同样的方式根据你的导入安装 pip 包。SDK 不会自己读取环境变量,所以下面这个步骤会读取 CAPSKIP_HOST、CAPSKIP_PORT 和 CAPSKIP_API_KEY,并把它们传给构造函数。

# pip install capskip - Pipedream installs it from this import
import os
from capskip import CapSkip

def handler(pd: "pipedream"):
    # Your Pipedream environment variables. The SDK does not read
    # them by itself, so pass them to the constructor.
    solver = CapSkip(
        host=os.environ["CAPSKIP_HOST"],
        port=int(os.environ["CAPSKIP_PORT"]),
        apiKey=os.environ["CAPSKIP_API_KEY"],
    )

    page_url = pd.steps["trigger"]["event"]["body"]["url"]
    result = solver.recaptcha(sitekey="YOUR_SITEKEY", url=page_url)

    # Downstream steps read pd.steps["solve"]["token"]
    return {"token": result["code"]}

如果走这条路,除了另外两个变量之外,还要把 CAPSKIP_PORT 设为 8080。定下来之前要知道一个限制:rerun 和 delay 辅助方法只在 Node.js 下有文档,所以 Python 步骤只能是一次性的版本。如果你需要拆分轮询的形态,就用 Node 来写那一个步骤。

把你刚打开的端口锁紧

Server 模式会在公网上放一个监听器,所以把它当成任何其他对外暴露的服务来对待。有三件事值得第一天就做。

  • 在 CapSkip 应用中打开 API 密钥校验,并给这个工作流单独的密钥。不开的话,任何字符串都会被当作密钥接受。
  • 把识别程序放在防火墙规则之后,而不是把 8080 对所有人敞开。
  • 想清楚这条规则怎么写。Pipedream 普通的出站流量来自标准的 AWS us-east-1 地址段,范围太宽,加进白名单没有实际意义。如果你需要一条精确的规则,Pipedream 提供 VPC,可以为每个工作区分配专用的静态出站 IP,那才是你该加白名单的地址。

常见错误及其含义

你所看到的原因修复
NetworkException,或者 8080 端口连接被拒绝识别程序处于 Local 模式,或者 host 变量写错了切换到 Server 模式,并把 host 变量设为公网 IP
识别进行到一半时执行被杀掉工作流的时间上限短于识别耗时在工作流设置里调高,或者改用 rerun 版本
步骤把表示未就绪的响应原文当作结果返回了轮询分支把 CAPCHA_NOT_READY 当成了答案显式判断它,然后 rerun,而不是把它返回
裸接口返回 ERROR_WRONG_USER_KEY密钥变量是空的,所以发出去的是空字符串检查环境变量名,包括大小写
第二次读取时 token 是空的结果只能读取一次读一次,存进变量,然后把变量传下去
有效的 token 被目标站点拒绝它在识别和提交之间过期了把提交步骤直接挪到识别步骤之后
Cannot use import statement outside a module这个步骤混用了 import 和 require每个步骤只用一种写法。代码步骤是 ES 模块

常见问题

我能在不暴露任何东西的情况下从 Pipedream 使用 CapSkip 吗?

不能直接做到,因为 Pipedream 的算力是 Pipedream 的。总得有一端接受入站连接。开启密钥校验并配上防火墙规则的 Server 模式,就是最直接的答案。如果你的规定完全禁止开放端口,替代做法是把验证码这部分工作留在你自己控制的机器上,让 Pipedream 去调用那台机器自己的接口,但这只是把暴露面挪了个位置,并没有消除它。

一次 rerun 算作单独的一次执行吗?

步骤会再跑一遍,所以代码执行不止一次,这正是计数器存在的原因。这个辅助方法的意义在于等待期间工作流不会被一直挂着,因此慢速识别不会把单次执行推过时间上限。如果计费对你重要,去看你所在方案的计费口径,但无论如何超时问题都已经解决了。

我该用 SDK 还是裸接口?

当一个步骤就能完成全部工作时用 SDK,因为它会处理轮询,从 250 毫秒开始退避而不是按固定间隔休眠,还会给你带类型的错误。当你把流程拆到多次 rerun 里时用裸接口,因为步骤在两次轮询之间就退出了,内存里没有客户端来保住那个任务。两者访问的是同一个端口上的同一个服务。

这和在 n8n 或 Zapier 里做有什么不同?

主要是运行时不同。Pipedream 给你的是真正的 Node.js,支持 npm 导入,所以 SDK 可以直接塞进去,而 rerun 辅助方法能干净地应付慢速识别。Zapier 的代码步骤不能安装包,所以 Zapier 验证码指南 整个都是围绕它的超时来设计的。Make.com 根本没有代码步骤,这就是为什么 Make.com 操作演示 是用 HTTP 模块拼出来的。n8n 可以自托管在识别程序旁边,所以 n8n 指南 往往可以继续用 Local 模式。

简短版结论

把 CapSkip 设为 Server 模式,把它的地址和密钥放在 Pipedream 环境变量里,然后把 SDK 直接导入 Node.js 代码步骤。如果工作流的时间上限明显高于你最慢的一次识别,一个步骤就是全部集成。如果不是,就把步骤拆开:提交到裸接口,把任务 id 交给 rerun 辅助方法,在重新进入时轮询。拿到 token 后立刻提交,因为它大约两分钟就会过期。

Node.js 这一侧的内容见 Node.js 验证码识别页面,复选框挑战见 reCAPTCHA v2 识别页面,以及 Python、PHP 和 C# 中的等价调用见 验证码识别 SDK 页面.

在把它接进全天运行的东西之前,有件事值得知道:CapSkip 提供的是 验证码绕过 ,运行在你已经拥有的硬件上,所以一个持续不断触发的工作流和一个偶尔触发的工作流,花费完全相同。