API 教程

Node.js Promise.allSettled 用于批量验证码解决

Promise.all 在第一次失败时拒绝 - 如果五十个验证码中有一个失败,您将丢失所有待处理的结果。Promise.allSettled 等待每个承诺完成,为您提供每次成功和每次失败。对于批量验证码解决,这是正确的工具。

Promise.all 与 Promise.allSettled

// Promise.all — REJECTS if ANY task fails
const results = await Promise.all(tasks.map(solve)); // Throws on first error

// Promise.allSettled — RESOLVES always, with status for each
const results = await Promise.allSettled(tasks.map(solve));
// [{status: "fulfilled", value: "..."}, {status: "rejected", reason: Error}]
方法 第一次失败时 退货 最适合
Promise.all 立即拒绝 什么都没有(抛出) 全有或全无的操作
Promise.allSettled 继续 每一个结果 批量验证码解决

核心实施

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function solveCaptcha(sitekey, pageurl) {
  // Submit
  const submitResp = await axios.post(
    "https://ocr.captchaai.com/in.php",
    null,
    {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: sitekey,
        pageurl: pageurl,
        json: 1,
      },
    }
  );

  if (submitResp.data.status !== 1) {
    throw new Error(submitResp.data.request);
  }

  const captchaId = submitResp.data.request;

  // Poll
  for (let i = 0; i < 60; i++) {
    await sleep(5000);
    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
    });

    if (result.data.status === 1) return result.data.request;
    if (result.data.request !== "CAPCHA_NOT_READY") {
      throw new Error(result.data.request);
    }
  }

  throw new Error("TIMEOUT");
}

async function batchSolve(tasks) {
  const promises = tasks.map((task) =>
    solveCaptcha(task.sitekey, task.pageurl).then((solution) => ({
      ...task,
      solution,
    }))
  );

  const results = await Promise.allSettled(promises);

  const solved = [];
  const failed = [];

  for (let i = 0; i < results.length; i++) {
    if (results[i].status === "fulfilled") {
      solved.push(results[i].value);
    } else {
      failed.push({
        task: tasks[i],
        error: results[i].reason.message,
      });
    }
  }

  return { solved, failed };
}

// Usage
(async () => {
  const tasks = [
    {
      sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageurl: "https://example.com/page/1",
    },
    {
      sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageurl: "https://example.com/page/2",
    },
    {
      sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageurl: "https://example.com/page/3",
    },
  ];

  const { solved, failed } = await batchSolve(tasks);
  console.log(`Solved: ${solved.length}, Failed: ${failed.length}`);

  for (const s of solved) {
    console.log(`  ✓ ${s.pageurl}: ${s.solution.substring(0, 30)}...`);
  }
  for (const f of failed) {
    console.log(`  ✗ ${f.task.pageurl}: ${f.error}`);
  }
})();

控制并发

同时发送 1,000 个验证码会导致连接过载。使用并发限制器:

async function batchSolveWithLimit(tasks, concurrency = 10) {
  const results = [];
  let index = 0;

  async function worker() {
    while (index < tasks.length) {
      const i = index++;
      const task = tasks[i];

      try {
        const solution = await solveCaptcha(task.sitekey, task.pageurl);
        results[i] = { status: "fulfilled", value: { ...task, solution } };
      } catch (err) {
        results[i] = { status: "rejected", reason: err };
      }
    }
  }

  // Launch concurrent workers
  const workers = Array.from({ length: concurrency }, () => worker());
  await Promise.allSettled(workers);

  const solved = results
    .filter((r) => r.status === "fulfilled")
    .map((r) => r.value);
  const failed = results
    .filter((r) => r.status === "rejected")
    .map((r, i) => ({ task: tasks[i], error: r.reason.message }));

  return { solved, failed };
}

// Solve 100 CAPTCHAs, 10 at a time
const { solved, failed } = await batchSolveWithLimit(tasks, 10);

重试失败的任务

自动重新提交失败的验证码:

async function batchSolveWithRetry(tasks, maxRetries = 2, concurrency = 10) {
  let currentTasks = [...tasks];
  let allSolved = [];

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    if (currentTasks.length === 0) break;

    console.log(
      `Attempt ${attempt + 1}: solving ${currentTasks.length} tasks...`
    );

    const { solved, failed } = await batchSolveWithLimit(
      currentTasks,
      concurrency
    );

    allSolved = [...allSolved, ...solved];

    // Only retry transient errors
    const retryable = failed.filter(
      (f) =>
        f.error === "TIMEOUT" ||
        f.error === "ERROR_NO_SLOT_AVAILABLE" ||
        f.error === "ERROR_TOO_MUCH_REQUESTS"
    );

    currentTasks = retryable.map((f) => f.task);

    if (retryable.length > 0) {
      console.log(`  Retrying ${retryable.length} failed tasks...`);
    }
  }

  const finalFailed = currentTasks; // Anything left after all retries
  return { solved: allSolved, failed: finalFailed };
}

进度追踪

批量操作时报告实时进度:

async function batchSolveWithProgress(tasks, concurrency = 10) {
  let completed = 0;
  let succeeded = 0;
  let failed = 0;

  const wrapped = tasks.map((task) =>
    solveCaptcha(task.sitekey, task.pageurl)
      .then((solution) => {
        succeeded++;
        completed++;
        process.stdout.write(
          `\rProgress: ${completed}/${tasks.length} (${succeeded} ok, ${failed} err)`
        );
        return { ...task, solution };
      })
      .catch((err) => {
        failed++;
        completed++;
        process.stdout.write(
          `\rProgress: ${completed}/${tasks.length} (${succeeded} ok, ${failed} err)`
        );
        throw err;
      })
  );

  const results = await Promise.allSettled(wrapped);
  console.log("\nDone.");
  return results;
}

结构化结果处理

Promise.allSettled 结果解析为可操作的类别:

function categorizeResults(settled, originalTasks) {
  const categories = {
    solved: [],
    transientErrors: [],
    permanentErrors: [],
  };

  const TRANSIENT = new Set([
    "TIMEOUT",
    "ERROR_NO_SLOT_AVAILABLE",
    "ERROR_TOO_MUCH_REQUESTS",
  ]);

  for (let i = 0; i < settled.length; i++) {
    const r = settled[i];
    if (r.status === "fulfilled") {
      categories.solved.push(r.value);
    } else {
      const error = r.reason.message;
      const entry = { task: originalTasks[i], error };

      if (TRANSIENT.has(error)) {
        categories.transientErrors.push(entry);
      } else {
        categories.permanentErrors.push(entry);
      }
    }
  }

  return categories;
}

故障排除

问题 原因 处理方式
所有任务超时 太多并发请求压垮 CaptchaAI 或代理 将并发数减少到 5-10
ERR_SOCKET_EXHAUSTION 同时 HTTP 连接数过多 使用 http.AgentmaxSockets 限制
结果数组乱序 异步完成顺序与提交不同 使用基于索引的结果存储(如上所示)
大批量期间内存增长 把所有的承诺都记在心里 以 100-500 块为单位进行处理

常问问题

我应该使用什么并发级别?

从 10 个并发任务开始。增加直到您看到收益递减或错误率上升。大多数设置在 10-50 个并发求解时表现良好。

我应该对小批量(< 5)使用 Promise.allSettled 吗?

对于小批量来说,Promise.allPromise.allSettled 之间的差异可以忽略不计。无论如何,请使用 Promise.allSettled - 这是一个好习惯,可以防止部分故障导致数据丢失。

这与异步生成器或流相比如何?

Promise.allSettled 是面向批处理的——您提交所有内容并等待。对于连续任务流(例如,无休止地抓取页面),请使用异步生成器或带有队列的工作池。

下一步

并行解决验证码 -”获取您的 CaptchaAI API 密钥并启动批量操作。

相关指南:

该文章已禁用评论。