生产验证码解决失败。 API 超时、代币过期、余额耗尽。本指南涵盖 Node.js + CaptchaAI 的错误分类、重试策略、断路器和监控。
错误分类
const RETRIABLE_ERRORS = new Set([
"ERROR_NO_SLOT_AVAILABLE",
"CAPCHA_NOT_READY",
]);
const FATAL_ERRORS = new Set([
"ERROR_WRONG_USER_KEY",
"ERROR_KEY_DOES_NOT_EXIST",
"ERROR_ZERO_BALANCE",
"ERROR_CAPTCHA_UNSOLVABLE",
"ERROR_BAD_DUPLICATES",
"ERROR_BAD_PARAMETERS",
"ERROR_WRONG_CAPTCHA_ID",
]);
class CaptchaError extends Error {
constructor(code, message) {
super(message || code);
this.name = "CaptchaError";
this.code = code;
}
}
class RetriableError extends CaptchaError {
constructor(code) {
super(code, `Retriable: ${code}`);
this.name = "RetriableError";
}
}
class FatalError extends CaptchaError {
constructor(code) {
super(code, `Fatal: ${code}`);
this.name = "FatalError";
}
}
function classifyError(code) {
if (FATAL_ERRORS.has(code)) throw new FatalError(code);
throw new RetriableError(code);
}
指数退避
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function withRetry(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 2000,
maxDelay = 30000,
jitter = true,
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error instanceof FatalError) throw error;
lastError = error;
if (attempt < maxRetries) {
let delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
if (jitter) delay *= 0.5 + Math.random();
console.log(
`Retry ${attempt + 1}/${maxRetries} in ${(delay / 1000).toFixed(1)}s: ${error.message}`
);
await sleep(delay);
}
}
}
throw lastError;
}
鲁棒的求解器
const API_KEY = "YOUR_API_KEY";
class RobustSolver {
#apiKey;
#maxRetries;
#pollInterval;
#maxPollTime;
constructor(apiKey, options = {}) {
this.#apiKey = apiKey;
this.#maxRetries = options.maxRetries ?? 3;
this.#pollInterval = options.pollInterval ?? 5000;
this.#maxPollTime = options.maxPollTime ?? 150000;
}
async solve(method, params) {
return withRetry(
() => this.#doSolve(method, params),
{ maxRetries: this.#maxRetries }
);
}
async #doSolve(method, params) {
const taskId = await this.#submit(method, params);
return await this.#poll(taskId);
}
async #submit(method, params) {
for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
try {
const resp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({
key: this.#apiKey,
method,
json: "1",
...params,
}),
signal: AbortSignal.timeout(30000),
});
if (!resp.ok) {
throw new RetriableError(`HTTP_${resp.status}`);
}
const data = await resp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_NO_SLOT_AVAILABLE") {
if (attempt < this.#maxRetries) {
await sleep(3000 * (attempt + 1));
continue;
}
}
classifyError(data.request);
} catch (error) {
if (error instanceof FatalError) throw error;
if (error.name === "TimeoutError" || error.name === "AbortError") {
if (attempt < this.#maxRetries) {
await sleep(2000 * (attempt + 1));
continue;
}
}
throw error;
}
}
throw new RetriableError("MAX_SUBMIT_RETRIES");
}
async #poll(taskId) {
const start = Date.now();
while (Date.now() - start < this.#maxPollTime) {
await sleep(this.#pollInterval);
try {
const resp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: this.#apiKey,
action: "get",
id: taskId,
json: "1",
})}`,
{ signal: AbortSignal.timeout(30000) }
);
const data = await resp.json();
if (data.status === 1) return data.request;
if (data.request === "CAPCHA_NOT_READY") continue;
if (FATAL_ERRORS.has(data.request)) throw new FatalError(data.request);
} catch (error) {
if (error instanceof FatalError) throw error;
// Network errors during poll — keep trying
continue;
}
}
throw new CaptchaError("TIMEOUT", `Timed out after ${this.#maxPollTime}ms`);
}
}
断路器
class CircuitBreaker {
#state = "closed"; // closed | open | half-open
#failures = 0;
#lastFailure = 0;
#threshold;
#resetTimeout;
constructor(threshold = 5, resetTimeout = 60000) {
this.#threshold = threshold;
this.#resetTimeout = resetTimeout;
}
get state() {
return this.#state;
}
canExecute() {
if (this.#state === "closed") return true;
if (this.#state === "open") {
if (Date.now() - this.#lastFailure > this.#resetTimeout) {
this.#state = "half-open";
return true;
}
return false;
}
return true; // half-open: allow test request
}
recordSuccess() {
this.#failures = 0;
this.#state = "closed";
}
recordFailure() {
this.#failures++;
this.#lastFailure = Date.now();
if (this.#failures >= this.#threshold) {
this.#state = "open";
console.log(`Circuit OPEN — pausing for ${this.#resetTimeout / 1000}s`);
}
}
}
class ProtectedSolver {
#solver;
#breaker;
constructor(apiKey) {
this.#solver = new RobustSolver(apiKey);
this.#breaker = new CircuitBreaker(5, 60000);
}
async solve(method, params) {
if (!this.#breaker.canExecute()) {
throw new CaptchaError(
"CIRCUIT_OPEN",
"API appears down — circuit breaker is open"
);
}
try {
const result = await this.#solver.solve(method, params);
this.#breaker.recordSuccess();
return result;
} catch (error) {
if (error instanceof FatalError) throw error;
this.#breaker.recordFailure();
throw error;
}
}
get circuitState() {
return this.#breaker.state;
}
}
令牌过期处理程序
class TokenCache {
#cache = new Map();
#defaultTTL;
constructor(defaultTTL = 110000) {
// reCAPTCHA: ~2 min, Turnstile: ~5 min
this.#defaultTTL = defaultTTL;
}
get(key) {
const entry = this.#cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > this.#defaultTTL) {
this.#cache.delete(key);
return null;
}
return entry.token;
}
set(key, token) {
this.#cache.set(key, { token, timestamp: Date.now() });
}
invalidate(key) {
this.#cache.delete(key);
}
}
class CachedSolver {
#solver;
#cache;
constructor(apiKey) {
this.#solver = new ProtectedSolver(apiKey);
this.#cache = new TokenCache(110000);
}
async getToken(cacheKey, method, params) {
const cached = this.#cache.get(cacheKey);
if (cached) return cached;
const token = await this.#solver.solve(method, params);
this.#cache.set(cacheKey, token);
return token;
}
async solveWithRetryOnReject(method, params, submitFn, maxAttempts = 2) {
for (let i = 0; i < maxAttempts; i++) {
const token = await this.#solver.solve(method, params);
const accepted = await submitFn(token);
if (accepted) return token;
console.log(`Token rejected (attempt ${i + 1}), re-solving...`);
}
throw new CaptchaError("TOKEN_REJECTED", "Token rejected after max attempts");
}
}
日志记录和指标
class SolverMetrics {
#startTime = Date.now();
#solveTimes = [];
#counts = { submitted: 0, solved: 0, failed: 0, retries: 0 };
recordSubmit() { this.#counts.submitted++; }
recordSolved(duration) { this.#counts.solved++; this.#solveTimes.push(duration); }
recordFailed() { this.#counts.failed++; }
recordRetry() { this.#counts.retries++; }
report() {
const elapsed = (Date.now() - this.#startTime) / 1000;
const total = this.#counts.solved + this.#counts.failed;
const avgTime = this.#solveTimes.length > 0
? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length / 1000
: 0;
return {
elapsed: `${elapsed.toFixed(0)}s`,
submitted: this.#counts.submitted,
solved: this.#counts.solved,
failed: this.#counts.failed,
retries: this.#counts.retries,
avgSolveTime: `${avgTime.toFixed(1)}s`,
successRate: total > 0 ? `${((this.#counts.solved / total) * 100).toFixed(1)}%` : "N/A",
throughput: `${(this.#counts.solved / (elapsed / 60)).toFixed(1)}/min`,
};
}
}
class InstrumentedSolver {
#solver;
#metrics;
constructor(apiKey) {
this.#solver = new ProtectedSolver(apiKey);
this.#metrics = new SolverMetrics();
}
async solve(method, params) {
this.#metrics.recordSubmit();
const start = Date.now();
try {
const token = await this.#solver.solve(method, params);
this.#metrics.recordSolved(Date.now() - start);
return token;
} catch (error) {
this.#metrics.recordFailed();
throw error;
}
}
report() {
return this.#metrics.report();
}
}
完整的生产模式
// Combine everything
const solver = new InstrumentedSolver("YOUR_API_KEY");
async function main() {
const tasks = Array.from({ length: 10 }, (_, i) => ({
method: "userrecaptcha",
params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));
const results = await Promise.allSettled(
tasks.map((task) => solver.solve(task.method, task.params))
);
const solved = results.filter((r) => r.status === "fulfilled");
const failed = results.filter((r) => r.status === "rejected");
console.log(`Solved: ${solved.length}, Failed: ${failed.length}`);
console.log("Metrics:", solver.report());
for (const fail of failed) {
console.log(` Error: ${fail.reason.message}`);
}
}
main();
故障排除
| 症状 | 原因 | 处理方式 |
|---|---|---|
| 所有重试都会立即失败 | 正在重试致命错误 | 检查错误分类 |
| 断路器保持打开状态 | API 关闭或密钥错误 | 检查API状态和密钥 |
| 提交时令牌已过期 | 求解时间+延迟太长 | 导航前预求解 |
获取时 AbortError |
超时时间太短 | 增加AbortSignal.timeout |
| 未处理的 Promise 拒绝 | 缺少异步捕获 | 始终处理拒绝 |
常见问题
我应该重试 ERROR_CAPTCHA_UNSOLVABLE 吗?
不。这是一个致命错误 - 无法解决验证码。重试会浪费时间和金钱。
正确的重试次数是多少?
提交重试 3 次,投票结果 30 次。如果 3 次提交重试失败,则说明存在根本性错误。
我如何知道这是网络错误还是 API 错误?
网络错误抛出 TypeError(获取失败)或 AbortError(超时)。 API 错误返回带有错误代码的 JSON。以不同的方式处理它们。
概括
强大的 Node.js 验证码解决CaptchaAI:将错误分类为可重试错误和致命错误,使用带抖动的指数退避,使用断路器进行保护,缓存令牌以供重用,并使用生产可见性指标进行检测。