عندما تُرسل عشرات اختبارات CAPTCHA في وقت واحد، تحتاج إلى معرفة حالة كلٍّ منها لحظياً: أيُّها قيد الحلّ، وأيُّها اكتمل، وأيُّها فشل أو انتهت مهلته. ناقل الأحداث (Event Bus) في Node.js يمنحك هذه الرؤية عبر بثّ كل تغيير في الحالة إلى نقطة مركزية واحدة، فيستمع إليها المسجِّل ونظام المقاييس ومعالج إعادة المحاولة معاً دون أن يعرف أيٌّ منها بوجود الآخر.
يختلف هذا عن رد النداء المفرد أو الاستطلاع الدوري اللذين يعيدان نتيجة واحدة في النهاية. فبدل انتظار الحصيلة، يبثّ الناقل خمس حالات على مدار عمر كل مهمة — submitted وpending وsolved وfailed وtimeout — فتتفاعل معها أجزاء تطبيقك دون اقتران محكم. في هذا الدليل نبني فئة CaptchaBus فوق EventEmitter ونربطها بـ CaptchaAI، مع مكافئ كامل بلغة Python.
متى تحتاج إلى ناقل أحداث؟
ليس كل تطبيق يحتاج إلى هذه البنية. إذا كنت تحلّ اختباراً واحداً في كل مرة ضمن سكربت بسيط، فالاستطلاع الدوري المباشر يكفي. لكن ناقل الأحداث يصبح الخيار الأنسب حين:
- تتعامل مع تزامن عالٍ — تُرسل عشرات أو مئات المهام معاً وتريد لوحة مقاييس حيّة لمعدل الحل ووقته ونسبة الفشل.
- تفصل المسؤوليات — يجب أن يعمل التسجيل والمراقبة وإعادة المحاولة كوحدات مستقلة يسهل تعديلها منفردة.
- تبني خط معالجة طويل الأمد — خدمة تعمل باستمرار وتراقب صحّتها بدل سكربت يُشغَّل مرة واحدة.
تخيّل فريقاً في القاهرة أو الرياض يبني خدمة لمراقبة أسعار متاجر إلكترونية عبر عدة عمّال متوازين. مع ناقل الأحداث، يبثّ كل عامل حالته إلى المسجِّل ونظام المقاييس نفسه، فيرى الفريق فوراً أيّ نطاق بدأ يرفض الرموز دون أن يوقف بقية العمّال. ولأن CaptchaAI يحاسب على أساس عدد الـ Threads المتزامنة مع حلول غير محدودة لكل Thread، يمكن رفع التزامن — بالانتقال إلى خطة ADVANCE ($90 شهرياً، 50 Thread) مثلاً — دون تغيير منطق الأحداث في الكود.
بنية ناقل الأحداث
[CaptchaBus]
├── emit("submitted", { taskId, type, pageurl })
├── emit("pending", { taskId, elapsed })
├── emit("solved", { taskId, solution, duration })
├── emit("failed", { taskId, error, duration })
└── emit("timeout", { taskId, elapsed })
↓ ↓ ↓
[Logger] [Metrics] [Retry Handler]
يبثّ الناقل خمس حالات على امتداد عمر كل مهمة:
submitted— قُبِلت المهمة وبدأ الاستطلاع الدوري.pending— لا تزال قيد الحلّ بعد مرور فترة من الوقت.solved— وصل الحلّ مصحوباً بمدّته الزمنية.failed— رُفِضت المهمة أو وقع خطأ أثناء المعالجة.timeout— انقضت المهلة القصوى دون نتيجة.
لاحظ أن المستمعين يُسجَّلون بشكل مستقل تماماً. إضافة ميزة جديدة — كنظام مقاييس أو تنبيه أو كتابة سجلّ إلى ملف — لا تتطلّب أي تعديل على كود الحلّ نفسه؛ يكفي أن يشترك المستمع الجديد في الحدث الذي يهمّه. هذا الفصل بين المُصدِر والمستمع هو جوهر البنية القائمة على الأحداث.
بناء فئة CaptchaBus في JavaScript
const EventEmitter = require("events");
const axios = require("axios");
class CaptchaBus extends EventEmitter {
constructor(apiKey, options = {}) {
super();
this.apiKey = apiKey;
this.pollInterval = options.pollInterval || 5000;
this.maxWait = options.maxWait || 300000; // 5 minutes
this.pending = new Map();
}
async submit(params) {
const { method, sitekey, pageurl, ...extra } = params;
const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const submitParams = {
key: this.apiKey,
method: method || "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
...extra,
};
try {
const resp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{ params: submitParams }
);
if (resp.data.status !== 1) {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: 0,
});
return null;
}
const captchaId = resp.data.request;
const startTime = Date.now();
this.emit("submitted", {
taskId,
captchaId,
method: method || "userrecaptcha",
pageurl,
});
// Start polling
this._poll(taskId, captchaId, startTime);
return taskId;
} catch (err) {
this.emit("failed", { taskId, error: err.message, duration: 0 });
return null;
}
}
async _poll(taskId, captchaId, startTime) {
const check = async () => {
const elapsed = Date.now() - startTime;
if (elapsed > this.maxWait) {
this.emit("timeout", { taskId, elapsed });
return;
}
this.emit("pending", { taskId, elapsed });
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: {
key: this.apiKey,
action: "get",
id: captchaId,
json: 1,
},
});
if (resp.data.status === 1) {
this.emit("solved", {
taskId,
captchaId,
solution: resp.data.request,
duration: Date.now() - startTime,
});
} else if (resp.data.request === "CAPCHA_NOT_READY") {
setTimeout(check, this.pollInterval);
} else {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: Date.now() - startTime,
});
}
} catch (err) {
this.emit("failed", {
taskId,
error: err.message,
duration: Date.now() - startTime,
});
}
};
setTimeout(check, this.pollInterval);
}
}
module.exports = CaptchaBus;
تبني الفئة أعلاه على EventEmitter المدمج في Node.js: تُرسل المهمة عبر submit إلى نقطة النهاية in.php، ثم يتولّى _poll الاستطلاع الدوري على res.php حتى تصل النتيجة، فيبثّ الحدث المناسب في كل مرحلة. الفاصل الزمني للاستطلاع ومهلة الانتظار القصوى قابلان للضبط عبر options.
تسجيل المستمعين على الأحداث
const CaptchaBus = require("./captcha-bus");
const bus = new CaptchaBus(process.env.CAPTCHAAI_API_KEY, {
pollInterval: 5000,
maxWait: 120000,
});
// Logging listener
bus.on("submitted", (e) => {
console.log(`[SUBMIT] ${e.taskId} → ${e.method} on ${e.pageurl}`);
});
bus.on("pending", (e) => {
console.log(`[PENDING] ${e.taskId} — ${(e.elapsed / 1000).toFixed(1)}s`);
});
bus.on("solved", (e) => {
console.log(
`[SOLVED] ${e.taskId} in ${(e.duration / 1000).toFixed(1)}s — ${e.solution.substring(0, 30)}...`
);
});
bus.on("failed", (e) => {
console.error(`[FAILED] ${e.taskId} — ${e.error}`);
});
bus.on("timeout", (e) => {
console.error(
`[TIMEOUT] ${e.taskId} after ${(e.elapsed / 1000).toFixed(1)}s`
);
});
// Metrics listener
const metrics = { submitted: 0, solved: 0, failed: 0, totalDuration: 0 };
bus.on("submitted", () => metrics.submitted++);
bus.on("solved", (e) => {
metrics.solved++;
metrics.totalDuration += e.duration;
});
bus.on("failed", () => metrics.failed++);
// Submit a CAPTCHA
bus.submit({
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
هنا يشترك مستمعان منفصلان في الأحداث نفسها:
- مستمع التسجيل — يطبع كل تغيّر في الحالة إلى الطرفية لتتبّع سير العمل لحظياً.
- مستمع المقاييس — يجمع عدد المُرسَل والمحلول والفاشل وإجمالي المدة لحساب معدل الحل ووقته.
كلٌّ منهما يعمل مستقلاً، ويمكنك إضافة مستمع ثالث لاحقاً — للتنبيهات مثلاً — دون لمس ما سبق.
المكافئ نفسه في Python
import os
import time
import threading
from collections import defaultdict
import requests
class CaptchaBus:
def __init__(self, api_key, poll_interval=5, max_wait=300):
self.api_key = api_key
self.poll_interval = poll_interval
self.max_wait = max_wait
self._listeners = defaultdict(list)
def on(self, event, callback):
"""Register a listener for an event."""
self._listeners[event].append(callback)
return self
def emit(self, event, data):
"""Emit an event to all registered listeners."""
for callback in self._listeners.get(event, []):
try:
callback(data)
except Exception as e:
print(f"Listener error on {event}: {e}")
def submit(self, sitekey, pageurl, method="userrecaptcha", **extra):
"""Submit a CAPTCHA and begin tracking."""
task_id = f"task_{int(time.time())}_{id(sitekey) % 10000}"
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
**extra
})
data = resp.json()
if data.get("status") != 1:
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": 0
})
return None
captcha_id = data["request"]
start_time = time.time()
self.emit("submitted", {
"task_id": task_id,
"captcha_id": captcha_id,
"method": method,
"pageurl": pageurl
})
# Poll in a background thread
thread = threading.Thread(
target=self._poll,
args=(task_id, captcha_id, start_time),
daemon=True
)
thread.start()
return task_id
def _poll(self, task_id, captcha_id, start_time):
while True:
elapsed = time.time() - start_time
if elapsed > self.max_wait:
self.emit("timeout", {"task_id": task_id, "elapsed": elapsed})
return
time.sleep(self.poll_interval)
self.emit("pending", {"task_id": task_id, "elapsed": elapsed})
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": captcha_id,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
self.emit("solved", {
"task_id": task_id,
"solution": data["request"],
"duration": time.time() - start_time
})
return
elif data.get("request") != "CAPCHA_NOT_READY":
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": time.time() - start_time
})
return
# Usage
bus = CaptchaBus(os.environ["CAPTCHAAI_API_KEY"])
bus.on("submitted", lambda e: print(f"[SUBMIT] {e['task_id']}"))
bus.on("solved", lambda e: print(f"[SOLVED] {e['task_id']} in {e['duration']:.1f}s"))
bus.on("failed", lambda e: print(f"[FAILED] {e['task_id']} — {e['error']}"))
bus.on("timeout", lambda e: print(f"[TIMEOUT] {e['task_id']}"))
bus.submit("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")
يعيد المثال أعلاه بناء نمط الأحداث نفسه في Python بلا مكتبات خارجية: قاموس defaultdict يحتفظ بالمستمعين، ودالتا on وemit تحاكيان EventEmitter، بينما يجري الاستطلاع في خيط معالجة (Thread) في الخلفية حتى لا يحجب بقية التطبيق. المنطق والحالات الخمس متطابقة مع نسخة Node.js.
متقدّم: معالج إعادة المحاولة كمستمع
// Automatic retry on failure
bus.on("failed", async (e) => {
if (e.retryCount >= 3) {
console.error(`[GIVE UP] ${e.taskId} after 3 retries`);
return;
}
console.log(`[RETRY] ${e.taskId} — attempt ${(e.retryCount || 0) + 1}`);
await bus.submit({
...e.originalParams,
_retryCount: (e.retryCount || 0) + 1,
});
});
من مزايا هذه البنية أن إعادة المحاولة تصبح مجرّد مستمع آخر على حدث failed، لا شرطاً متشابكاً داخل منطق الحلّ. هنا يعيد المعالج إرسال المهمة تلقائياً حتى ثلاث محاولات قبل أن يستسلم ويسجّل ذلك.
متقدّم: تغليف الأحداث في Promise
إن أردت واجهة قائمة على Promise فوق ناقل الأحداث — لتستخدم async/await بدل الاستماع اليدوي — فغلّف الحدثين solved وfailed في وعد واحد:
function solveCaptcha(bus, params) {
return new Promise((resolve, reject) => {
const taskId = bus.submit(params);
function onSolved(e) {
if (e.taskId === taskId) {
cleanup();
resolve(e.solution);
}
}
function onFailed(e) {
if (e.taskId === taskId) {
cleanup();
reject(new Error(e.error));
}
}
function cleanup() {
bus.removeListener("solved", onSolved);
bus.removeListener("failed", onFailed);
bus.removeListener("timeout", onFailed);
}
bus.on("solved", onSolved);
bus.on("failed", onFailed);
bus.on("timeout", onFailed);
});
}
// Usage
const solution = await solveCaptcha(bus, {
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
استكشاف الأخطاء الشائعة وإصلاحها
| المشكلة | السبب | الإجراء |
|---|---|---|
| المستمع لا يعمل رغم صحّة الكود | اسم الحدث لا يتطابق (مثلاً solve بدل solved) |
راجع أسماء الأحداث المستخدمة في emit وon حرفاً بحرف |
| تحذير تسرّب في الذاكرة (memory leak) | عدد كبير من المستمعين على حدث واحد | استخدم setMaxListeners() أو أزِل المستمعين بعد انتهاء المهمة عبر removeListener |
| يُنشأ الرمز لكن الجهة المستهدفة ترفضه | مفتاح الموقع أو الصفحة أو سياق الجلسة لا يتطابق | التقط المعلمات من جديد وأعد استخدام الرمز داخل جلسة HTTP أو المتصفح نفسها |
| تنتهي عملية الاستطلاع بمهلة | الفاصل الزمني أو وقت الانتظار أو معالجة الأخطاء صارمة أكثر من اللازم | استطلع كل 5 إلى 10 ثوانٍ وافصل بين انتهاء المهلة والأخطاء الفعلية وسجّل السبب |
| ينجح المثال محلياً لكنه يفشل داخل سير العمل | حقل النموذج أو حقن الرمز مفقود في السلسلة الفعلية | تحقق من المسار الكامل بين مزوّد الحل والطلب النهائي إلى الموقع المستهدف |
الأسئلة الشائعة
هل يعمل ناقل الأحداث مع كل أنواع CAPTCHA؟
نعم، فالبنية محايدة تجاه نوع الاختبار؛ ما يتغيّر هو قيمة method والمعلمات المرسلة. يدعم CaptchaAI عبر هذا المسار reCAPTCHA v2 وv3 وCloudflare Turnstile وCloudflare Challenge وGeeTest v3 واختبارات الصور وOCR والشبكة. أما hCaptcha وFunCaptcha فغير مدعومين حالياً، وGeeTest v4 قيد الإعداد، فلا تُبنى عليها مهام حلّ بعد.
كيف أربط عدد العمّال المتوازين بخطة CaptchaAI؟
كل مهمة قيد الحلّ تشغل Thread واحداً حتى تكتمل. اجعل الحد الأقصى للعمّال المتزامنين في تطبيقك مساوياً لعدد الـ Threads في خطتك أو أقل منه: خطة STANDARD ($30 شهرياً) تمنحك 15 Thread، وADVANCE ($90 شهرياً) تمنحك 50، مع حلول غير محدودة لكل Thread. حدث submitted نقطة مناسبة لعدّ المهام النشطة وضبط هذا الحد.
ماذا يحدث عند انتهاء المهلة قبل وصول النتيجة؟
يبثّ الناقل حدث timeout بدل solved، ويتوقّف الاستطلاع لتلك المهمة. في المثال ربطنا timeout بمعالج onFailed نفسه داخل غلاف الـ Promise، لكن يمكنك التعامل معه على حدة — بتسجيله في مقياس منفصل أو إعادة الإرسال — لأنه غالباً يشير إلى ضغط عابر لا إلى فشل فعلي.
متى أستبدل EventEmitter بوسيط رسائل خارجي؟
طالما بقي كل شيء داخل عملية واحدة، فالناقل المدمج أبسط وأسرع. انتقل إلى Redis أو RabbitMQ أو Kafka فقط حين تحتاج عدة عمليات منفصلة أن تتفاعل مع أحداث الحل عبر الشبكة.
الخطوات التالية
- البدء السريع مع CaptchaAI: حلّ أول كابتشا في 5 دقائق
- كيفية حلّ reCAPTCHA v2 عبر الـ API: دليل خطوة بخطوة
- كيفية حل Cloudflare Turnstile باستخدام واجهة API
- كيفية حل GeeTest v3 باستخدام API