عندما يتجاوز عدد اختبارات CAPTCHA التي تحتاج إلى حلّها بضع عشرات في الدقيقة، لم يعد إرسالها واحدة تلو الأخرى خياراً عملياً؛ الحل هو قائمة انتظار تتحكم في التزامن وتعيد المحاولة عند الفشل وتراقب الإنتاجية. يعرض هذا الدليل خمسة أنماط جاهزة للإنتاج لبناء هذه القائمة في Node.js فوق CaptchaAI API، من دفعة بسيطة تعتمد على Promise.allSettled وصولاً إلى قائمة ذات أولوية وأخرى بإعادة المحاولة وطابور رسائل ميتة.
لماذا Node.js تحديداً؟ لأن حل CAPTCHA عملية مقيّدة بالإدخال والإخراج (I/O) لا بالمعالجة: معظم الوقت يُقضى في انتظار استجابة الـ API، لا في تنفيذ الكود. حلقة الأحداث أحادية الخيط في Node.js تتعامل مع مئات الطلبات المعلّقة في آنٍ واحد دون إنشاء خيط لكل طلب، ما يجعلها بيئة مثالية لتشغيل عشرات عمليات الحل بالتوازي بأقل استهلاك للموارد.
نقطة تستحق الانتباه قبل البدء: يعتمد CaptchaAI على نموذج تسعير قائم على عدد الخيوط (Threads) المتزامنة، لا على عدد عمليات الحل. لكل خطة سقفٌ للخيوط — مثلاً خطة STANDARD بسعر $30 شهرياً تتيح 15 خيطاً — وهذا السقف هو ما ينبغي أن يحدّد قيمة maxConcurrent في أي نمط أدناه. تجاوزه يعيد الخطأ ERROR_NO_SLOT_AVAILABLE.
دفعة بسيطة عبر Promise.allSettled
أبسط شكل لقائمة الانتظار هو إطلاق كل المهام دفعةً واحدة وانتظار اكتمالها جميعاً. تُرسل الدالة solveSingle كل اختبار إلى نقطة النهاية in.php، ثم تستطلع النتيجة دورياً من res.php حتى تعود بحالة نجاح أو تنتهي المهلة. أما Promise.allSettled فتضمن ألا يُسقط فشلُ مهمة واحدة بقيةَ الدفعة؛ فتحصل على نتيجة مستقلة لكل مهمة، ناجحة كانت أم فاشلة. هذا النمط مناسب للدفعات الصغيرة المعروفة مسبقاً (عشرات المهام)، لكنه لا يحدّ من التزامن، لذا تجنّبه حين يكبر الحجم.
const API_KEY = "YOUR_API_KEY";
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function solveSingle(method, params) {
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) throw new Error(submitData.request);
const taskId = submitData.request;
for (let i = 0; i < 30; i++) {
await sleep(5000);
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: API_KEY,
action: "get",
id: taskId,
json: "1",
})}`
);
const data = await pollResp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
}
throw new Error("Timed out");
}
// Solve all at once
async function solveBatch(tasks) {
const results = await Promise.allSettled(
tasks.map((task) => solveSingle(task.method, task.params))
);
return results.map((result, i) => ({
taskId: tasks[i].id,
status: result.status,
value: result.status === "fulfilled" ? result.value : null,
error: result.status === "rejected" ? result.reason.message : null,
}));
}
// Usage
const tasks = Array.from({ length: 10 }, (_, i) => ({
id: i,
method: "userrecaptcha",
params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));
const results = await solveBatch(tasks);
console.log(`Solved: ${results.filter((r) => r.status === "fulfilled").length}/10`);
قائمة انتظار بتزامن محدود
المشكلة في الدفعة البسيطة أنها تُطلق كل الطلبات فوراً؛ ومع مئات المهام قد تتجاوز سقف الخيوط المتاح في خطتك وتصطدم بحدود المعدل. الحل هو ضبط عدد عمليات الحل الجارية في آنٍ واحد. تسحب الفئة ConcurrencyQueue التالية مهمة جديدة من الطابور فقط عند تحرّر مكان، فتبقى دائماً تحت السقف الذي تحدّده في maxConcurrent.
اجعل قيمة maxConcurrent مساوية لعدد الخيوط في خطتك أو أقل منها: 5 لخطة BASIC ($15 شهرياً، 5 خيوط)، و15 لخطة STANDARD، و50 لخطة ADVANCE ($90 شهرياً، 50 خيطاً). بهذا تستغلّ سعة الخطة كاملةً دون أن تتجاوزها وتتلقى ERROR_NO_SLOT_AVAILABLE.
class ConcurrencyQueue {
constructor(maxConcurrent = 5) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
this.results = [];
}
add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.#process();
});
}
async #process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) return;
this.running++;
const { fn, resolve, reject } = this.queue.shift();
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.#process();
}
}
async addBatch(fns) {
return Promise.allSettled(fns.map((fn) => this.add(fn)));
}
}
// Usage
const queue = new ConcurrencyQueue(5);
const tasks = Array.from({ length: 20 }, (_, i) => () =>
solveSingle("userrecaptcha", {
googlekey: `KEY_${i}`,
pageurl: `https://example.com/${i}`,
})
);
const results = await queue.addBatch(tasks);
const solved = results.filter((r) => r.status === "fulfilled");
console.log(`Solved: ${solved.length}/${results.length}`);
قائمة انتظار قائمة على EventEmitter
في الأنظمة الطويلة التي تعالج دفعات كبيرة تحتاج إلى رؤية ما يجري لحظةً بلحظة بدل انتظار اكتمال كل شيء. توسيع EventEmitter يمنحك ذلك: تُطلق القائمة أحداثاً مثل submitted وsolved وfailed وcomplete يمكنك الاستماع إليها لتحديث لوحة مؤشرات، أو كتابة سجلّات مباشرة، أو إشعار خدمة أخرى. المنطق نفسه المستخدم في ConcurrencyQueue — دالة #drain تملأ الفتحات الشاغرة — لكن مع طبقة أحداث فوقها لتتبّع التقدم في الوقت الحقيقي.
const { EventEmitter } = require("events");
class CaptchaQueue extends EventEmitter {
#apiKey;
#maxConcurrent;
#pending;
#active;
constructor(apiKey, maxConcurrent = 5) {
super();
this.#apiKey = apiKey;
this.#maxConcurrent = maxConcurrent;
this.#pending = [];
this.#active = 0;
this.stats = { submitted: 0, solved: 0, failed: 0 };
}
submit(id, method, params) {
this.#pending.push({ id, method, params });
this.stats.submitted++;
this.emit("submitted", { id, total: this.stats.submitted });
this.#drain();
}
async #drain() {
while (this.#active < this.#maxConcurrent && this.#pending.length > 0) {
const task = this.#pending.shift();
this.#active++;
this.#solve(task).finally(() => {
this.#active--;
this.#drain();
if (this.#active === 0 && this.#pending.length === 0) {
this.emit("complete", this.stats);
}
});
}
}
async #solve(task) {
try {
const token = await solveSingle(task.method, task.params);
this.stats.solved++;
this.emit("solved", { id: task.id, token, stats: { ...this.stats } });
} catch (error) {
this.stats.failed++;
this.emit("failed", { id: task.id, error: error.message, stats: { ...this.stats } });
}
}
}
// Usage
const queue = new CaptchaQueue("YOUR_API_KEY", 5);
queue.on("submitted", ({ id, total }) => {
console.log(`Submitted #${id} (total: ${total})`);
});
queue.on("solved", ({ id, stats }) => {
console.log(`Solved #${id} — ${stats.solved}/${stats.submitted}`);
});
queue.on("failed", ({ id, error }) => {
console.log(`Failed #${id}: ${error}`);
});
queue.on("complete", (stats) => {
const rate = ((stats.solved / stats.submitted) * 100).toFixed(1);
console.log(`Done: ${stats.solved}/${stats.submitted} (${rate}%)`);
});
// Submit tasks
for (let i = 0; i < 15; i++) {
queue.submit(i, "userrecaptcha", {
googlekey: `KEY_${i}`,
pageurl: `https://example.com/${i}`,
});
}
قائمة انتظار ذات أولوية
ليست كل المهام متساوية في الإلحاح. تخيّل متجراً إلكترونياً في موسم تخفيضات كبير مثل «الجمعة البيضاء»: حين يضغط عميل على إتمام الشراء وتظهر Cloudflare Turnstile يجب حلّها خلال ثوانٍ وإلا خسرت عملية بيع؛ في المقابل، مهام استخراج أسعار المنافسين في الخلفية يمكن أن تنتظر. قائمة الانتظار ذات الأولوية تخدم المهام الحرجة أولاً بصرف النظر عن ترتيب وصولها. في المثال أدناه تحصل مهمة إتمام الشراء على الأولوية 1 (الأعلى) بينما تأخذ مهام الاستخراج الأولوية 5:
class PriorityQueue {
#items = [];
enqueue(item, priority) {
this.#items.push({ item, priority });
this.#items.sort((a, b) => a.priority - b.priority);
}
dequeue() {
return this.#items.shift()?.item;
}
get length() {
return this.#items.length;
}
}
class PriorityCaptchaQueue {
#apiKey;
#maxConcurrent;
#queue;
#active;
#results;
constructor(apiKey, maxConcurrent = 5) {
this.#apiKey = apiKey;
this.#maxConcurrent = maxConcurrent;
this.#queue = new PriorityQueue();
this.#active = 0;
this.#results = new Map();
}
submit(id, method, params, priority = 5) {
return new Promise((resolve, reject) => {
this.#queue.enqueue({ id, method, params, resolve, reject }, priority);
this.#drain();
});
}
async #drain() {
while (this.#active < this.#maxConcurrent && this.#queue.length > 0) {
const task = this.#queue.dequeue();
this.#active++;
solveSingle(task.method, task.params)
.then((token) => {
this.#results.set(task.id, { status: "solved", token });
task.resolve(token);
})
.catch((err) => {
this.#results.set(task.id, { status: "error", error: err.message });
task.reject(err);
})
.finally(() => {
this.#active--;
this.#drain();
});
}
}
}
// Usage: high-priority checkout, low-priority scraping
const pq = new PriorityCaptchaQueue("YOUR_API_KEY", 3);
// Priority 1 (highest) — checkout
const checkoutToken = pq.submit(
"checkout_1",
"turnstile",
{ sitekey: "KEY", pageurl: "https://shop.com/checkout" },
1
);
// Priority 5 (normal) — product scraping
for (let i = 0; i < 5; i++) {
pq.submit(
`product_${i}`,
"userrecaptcha",
{ googlekey: "KEY", pageurl: `https://shop.com/p/${i}` },
5
);
}
قائمة انتظار بإعادة المحاولة وطابور الرسائل الميتة (Dead-Letter)
بعض حالات الفشل عابرة — انقطاع شبكة أو تجاوز مؤقت لحد المعدل — وتنجح عند إعادة المحاولة. النمط التالي يعيد محاولة كل مهمة فاشلة حتى maxRetries مرات، فإن استمر الفشل بعدها رحّلها إلى «طابور الرسائل الميتة» (Dead-Letter) لفحصها يدوياً بدل أن تحجب بقية الطابور. افصل دائماً الأخطاء العابرة القابلة لإعادة المحاولة عن الأخطاء الدائمة مثل ERROR_CAPTCHA_UNSOLVABLE التي لا جدوى من تكرارها.
class RetryQueue {
#apiKey;
#maxRetries;
#results;
#deadLetter;
constructor(apiKey, maxRetries = 3) {
this.#apiKey = apiKey;
this.#maxRetries = maxRetries;
this.#results = [];
this.#deadLetter = [];
}
async processBatch(tasks, maxConcurrent = 5) {
const queue = tasks.map((t) => ({ ...t, attempts: 0 }));
while (queue.length > 0) {
const batch = queue.splice(0, maxConcurrent);
const results = await Promise.allSettled(
batch.map((task) => this.#solveWithRetry(task))
);
for (let i = 0; i < results.length; i++) {
const result = results[i];
const task = batch[i];
if (result.status === "fulfilled") {
this.#results.push({ id: task.id, token: result.value });
} else {
task.attempts++;
if (task.attempts < this.#maxRetries) {
queue.push(task); // Retry
console.log(`Retry ${task.attempts}/${this.#maxRetries}: ${task.id}`);
} else {
this.#deadLetter.push({
id: task.id,
error: result.reason.message,
attempts: task.attempts,
});
}
}
}
}
return {
solved: this.#results,
failed: this.#deadLetter,
};
}
async #solveWithRetry(task) {
return solveSingle(task.method, task.params);
}
}
لوحة مراقبة الطابور
لا يمكنك تحسين ما لا تقيسه. تجمع الفئة QueueMonitor مؤشرات التشغيل الأساسية — عدد المهام المُرسَلة والجارية والمحلولة والفاشلة، إضافةً إلى متوسط وقت الحل والإنتاجية في الدقيقة ومعدل النجاح. اطبع هذا التقرير دورياً أو اربطه بأحداث CaptchaQueue أعلاه لترصد التدهور مبكراً: ارتفاع معدل الفشل مؤشر على مشكلة في المعلمات أو الخوادم الوسيطة (Proxies)، وانخفاض الإنتاجية مؤشر على حاجتك إلى خيوط أكثر.
class QueueMonitor {
#startTime;
#solveTimes;
constructor() {
this.#startTime = Date.now();
this.#solveTimes = [];
this.counts = { submitted: 0, solving: 0, solved: 0, failed: 0 };
}
recordSubmit() {
this.counts.submitted++;
this.counts.solving++;
}
recordSolved(solveTime) {
this.counts.solving--;
this.counts.solved++;
this.#solveTimes.push(solveTime);
}
recordFailed() {
this.counts.solving--;
this.counts.failed++;
}
report() {
const elapsed = (Date.now() - this.#startTime) / 1000;
const avgTime =
this.#solveTimes.length > 0
? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length
: 0;
const throughput = this.counts.solved / (elapsed / 60);
const successRate =
this.counts.solved + this.counts.failed > 0
? (this.counts.solved / (this.counts.solved + this.counts.failed)) * 100
: 0;
return {
elapsed: `${elapsed.toFixed(0)}s`,
submitted: this.counts.submitted,
solving: this.counts.solving,
solved: this.counts.solved,
failed: this.counts.failed,
avgSolveTime: `${(avgTime / 1000).toFixed(1)}s`,
throughput: `${throughput.toFixed(1)}/min`,
successRate: `${successRate.toFixed(1)}%`,
};
}
}
استكشاف الأعطال وإصلاحها
| العرَض | السبب | الإصلاح |
|---|---|---|
| ترفض كل الوعود (Promises) دفعةً واحدة | بلغتَ حدّ معدل الـ API | خفّض قيمة maxConcurrent |
| نمو استهلاك الذاكرة مع مرور الوقت | تراكم النتائج دون تفريغ | عالِج النتائج وامسحها دورياً |
| يُفرَّغ الطابور لكن تبقى مهام معلّقة | استدعاء drain() مفقود بعد الاكتمال |
تحقّق من مُشغِّل التفريغ داخل كتلة finally |
ERROR_NO_SLOT_AVAILABLE |
عدد استدعاءات API المتزامنة يفوق خيوط خطتك | أضِف تأخيراً بين عمليات الإرسال أو اخفض maxConcurrent |
| امتلاء طابور الرسائل الميتة | أخطاء دائمة متكررة | افحص أنواع الأخطاء — قد تحتاج إلى تصحيح المعلمات |
الأسئلة الشائعة
كيف أختار قيمة maxConcurrent المناسبة؟
اجعلها مساوية لعدد الخيوط في خطتك أو أقل بقليل، ثم راقب ظهور ERROR_NO_SLOT_AVAILABLE كإشارة تنبيه. على خطة BASIC ابدأ بـ 5، وعلى ADVANCE يمكنك بلوغ 50. زِدها تدريجياً ما دام معدل النجاح مستقراً.
ما الفرق بين قائمة الأولوية وقائمة إعادة المحاولة؟
قائمة الأولوية تقرّر ترتيب تنفيذ المهام (الحرجة أولاً)، بينما قائمة إعادة المحاولة تقرّر ما يحدث بعد فشل مهمة (إعادة المحاولة ثم الترحيل إلى طابور الرسائل الميتة). يمكن دمج النمطين في نظام إنتاجي واحد.
هل تحتفظ هذه القوائم بالمهام بعد إعادة تشغيل الخادم؟
لا؛ كل الأنماط أعلاه تعمل في الذاكرة وتُفقد عند إعادة التشغيل. للحصول على استمرارية عبر عدة خوادم استخدم طابوراً مدعوماً بـ Redis مثل bull أو bullmq.
أي أنواع CAPTCHA تدعمها هذه الأنماط؟
الأنماط عامة وتعمل مع أي نوع يدعمه CaptchaAI بتغيير قيمة method والمعلمات: reCAPTCHA v2/v3، وCloudflare Turnstile وChallenge، وGeeTest v3، والصور/OCR. أما hCaptcha وFunCaptcha فغير مدعومين حالياً.
الخلاصة
تجعل طبيعة Node.js المعتمدة على الإدخال والإخراج غير المتزامن منها خياراً ممتازاً لأنظمة طوابير حل CAPTCHA مع CaptchaAI. استخدم Promise.allSettled للدفعات الصغيرة، وEventEmitter لتتبّع التقدم لحظياً، وقائمة الأولوية للمسارات الحرجة تجارياً، وقائمة إعادة المحاولة مع طابور الرسائل الميتة لضمان الموثوقية — مع إبقاء maxConcurrent ضمن حدود خيوط خطتك.
مقالات ذات صلة
الخطوات التالية
- البدء السريع مع CaptchaAI: حلّ أول كابتشا في 5 دقائق
- حلّ reCAPTCHA v2 عبر الـ API خطوة بخطوة
- حلّ Cloudflare Turnstile عبر الـ API
- حلّ GeeTest v3 عبر الـ API