يعمل بروتوكول Chrome DevTools (CDP) عبر أربع مراحل واضحة لأتمتة اختبار CAPTCHA داخل بيئة تملكها: تتصل بالمتصفح مباشرة عبر WebSocket، تكتشف مفتاح الموقع في الصفحة، ترسل المهمة إلى CaptchaAI، ثم تحقن التوكن الناتج في الحقل المناسب. لا وسيط WebDriver، ولا طبقات تجريد إضافية — بل تحكّم مباشر على مستوى البروتوكول في طلبات الشبكة وتنفيذ الصفحة وسلوك المتصفح.
هذا المستوى من الدقة يجعل CDP خياراً عملياً لفرق ضمان الجودة التي تختبر تدفقات تسجيل الدخول والنماذج المحمية بـ CAPTCHA على بيئات staging مأذونة، حيث يهم التحكم اللحظي في كل طلب واستجابة.
تنقسم الدورة إلى أربع مراحل تقابل كلاً منها أوامر CDP محددة:
- الاتصال: افتح جلسة WebSocket من عنوان التصحيح المعلن في
/json/list، واربطها بالتبويب النشط في المتصفح. - اكتشاف مفتاح الموقع: نفّذ
Runtime.evaluateلقراءة السمةdata-sitekeyمن عناصر الصفحة وتحديد نوع CAPTCHA. - الإرسال إلى CaptchaAI: ابعث المهمة إلى نقطة النهاية
in.php، ثم استطلع النتيجة دورياً منres.phpحتى تكتمل المعالجة. - حقن التوكن: أعد كتابة حقل الاستجابة في الصفحة وأطلق دالة رد النداء لإكمال التحقق دون تدخل يدوي.
متى يناسبك تحكم CDP المباشر؟
ليس كل مشروع أتمتة يستدعي هذا المستوى من التحكم منخفض المستوى، لكن CDP يثبت جدواه تحديداً في الحالات التالية:
- اختبار الانحدار الليلي على بيئات staging تحوي نماذج محمية بـ reCAPTCHA v2 أو Cloudflare Turnstile.
- تشخيص تدفق التحقق حين تحتاج إلى رصد كل طلب شبكة يمرّ عبر عنصر CAPTCHA وتسجيله لحظياً.
- خطوط CI بلا واجهة رسومية التي تشغّل Chrome في وضع headless على خوادم الاختبار.
- التكامل مع أدوات مبنية على CDP مثل Puppeteer، عبر مشاركة الجلسة نفسها دون طبقة وسيطة إضافية.
لماذا تختار CDP بدلاً من WebDriver؟
| الميزة | WebDriver | CDP |
|---|---|---|
| اعتراض طلبات الشبكة | يتطلب أداة Selenium Wire | مدمج عبر Fetch.requestPaused |
| حقن JavaScript | عبر executeScript |
عبر Runtime.evaluate |
| العبء على الأداء | متوسط إلى مرتفع | منخفض |
| بروتوكول الاتصال | HTTP + JSON Wire | WebSocket لحظي |
باختصار، يتعامل WebDriver مع المتصفح من الخارج عبر طبقة وسيطة، بينما يخاطب CDP محرّك Chrome مباشرة — فتحصل على استجابة أسرع وتحكّم أدق عندما تحتاج إلى مراقبة كل طلب CAPTCHA أثناء الاختبار.
الاتصال المباشر بـ Chrome عبر CDP (Node.js)
الاتصال بـ Chrome
# Launch Chrome with remote debugging
chrome --remote-debugging-port=9222 --no-first-run --no-default-browser-check
أولاً، شغّل Chrome مع منفذ التصحيح عن بُعد كما في الأمر أعلاه، ثم يتولى عميل خفيف إدارة جلسة WebSocket وتوجيه الرسائل.
بناء عميل CDP خفيف
const WebSocket = require("ws");
const http = require("http");
class CDPClient {
constructor() {
this.ws = null;
this.id = 0;
this.callbacks = new Map();
this.eventHandlers = new Map();
}
async connect(port = 9222) {
// Get WebSocket URL from Chrome
const targets = await this.httpGet(
`http://127.0.0.1:${port}/json/list`
);
const target = targets.find((t) => t.type === "page");
return new Promise((resolve, reject) => {
this.ws = new WebSocket(target.webSocketDebuggerUrl);
this.ws.on("open", () => resolve());
this.ws.on("error", reject);
this.ws.on("message", (data) => this.handleMessage(JSON.parse(data)));
});
}
httpGet(url) {
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(JSON.parse(body)));
}).on("error", reject);
});
}
handleMessage(msg) {
if (msg.id && this.callbacks.has(msg.id)) {
this.callbacks.get(msg.id)(msg);
this.callbacks.delete(msg.id);
}
if (msg.method && this.eventHandlers.has(msg.method)) {
for (const handler of this.eventHandlers.get(msg.method)) {
handler(msg.params);
}
}
}
send(method, params = {}) {
return new Promise((resolve) => {
const id = ++this.id;
this.callbacks.set(id, resolve);
this.ws.send(JSON.stringify({ id, method, params }));
});
}
on(method, handler) {
if (!this.eventHandlers.has(method)) {
this.eventHandlers.set(method, []);
}
this.eventHandlers.get(method).push(handler);
}
}
يبني هذا الكلاس قناة اتصال دائمة عبر WebSocket: يقرأ عنوان التصحيح من Chrome، يفتح الجلسة، ثم يوجّه كل رسالة واردة إما إلى دالة رد النداء المنتظرة أو إلى مستمع الحدث المناسب.
ربط CaptchaAI بجلسة CDP
يجمع الكلاس التالي بين التحكم في المتصفح عبر CDP ونداءات CaptchaAI في وحدة واحدة تديّر دورة الحل كاملة.
تجميع الحل والحقن في كلاس واحد
const https = require("https");
class CDPCaptchaSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.cdp = new CDPClient();
this.API = "https://ocr.captchaai.com";
}
async init(port = 9222) {
await this.cdp.connect(port);
// Enable required domains
await this.cdp.send("Page.enable");
await this.cdp.send("Runtime.enable");
await this.cdp.send("Network.enable");
await this.cdp.send("DOM.enable");
}
async navigate(url) {
const result = await this.cdp.send("Page.navigate", { url });
await this.waitForLoad();
return result;
}
async waitForLoad() {
return new Promise((resolve) => {
this.cdp.on("Page.loadEventFired", () => resolve());
});
}
async detectSitekey() {
const result = await this.cdp.send("Runtime.evaluate", {
expression: `
(() => {
// reCAPTCHA
const recaptcha = document.querySelector('[data-sitekey]');
if (recaptcha) {
return {
type: 'recaptcha_v2',
sitekey: recaptcha.getAttribute('data-sitekey'),
};
}
// Turnstile
const turnstile = document.querySelector('.cf-turnstile[data-sitekey]');
if (turnstile) {
return {
type: 'turnstile',
sitekey: turnstile.getAttribute('data-sitekey'),
};
}
// reCAPTCHA v3 (script-based)
const scripts = document.querySelectorAll('script[src*="recaptcha"]');
for (const s of scripts) {
const match = s.src.match(/render=([\\w-]+)/);
if (match && match[1] !== 'explicit') {
return { type: 'recaptcha_v3', sitekey: match[1] };
}
}
return null;
})()
`,
returnByValue: true,
});
return result.result?.value || null;
}
async solveCaptcha(siteUrl, sitekey, type = "recaptcha_v2") {
const submitData = {
key: this.apiKey,
pageurl: siteUrl,
json: "1",
};
if (type === "turnstile") {
submitData.method = "turnstile";
submitData.sitekey = sitekey;
} else {
submitData.method = "userrecaptcha";
submitData.googlekey = sitekey;
}
const submitResp = await this.httpPost(
`${this.API}/in.php`,
submitData
);
if (submitResp.status !== 1) {
throw new Error(`Submit: ${submitResp.request}`);
}
const taskId = submitResp.request;
for (let i = 0; i < 60; i++) {
await this.sleep(5000);
const params = new URLSearchParams({
key: this.apiKey,
action: "get",
id: taskId,
json: "1",
});
const result = await this.httpGet(
`${this.API}/res.php?${params}`
);
if (result.request === "CAPCHA_NOT_READY") continue;
if (result.status !== 1) throw new Error(`Solve: ${result.request}`);
return result.request;
}
throw new Error("Timeout");
}
async injectToken(token, type = "recaptcha_v2") {
if (type === "turnstile") {
await this.cdp.send("Runtime.evaluate", {
expression: `
const input = document.querySelector('input[name="cf-turnstile-response"]');
if (input) {
input.value = '${token}';
input.dispatchEvent(new Event('change', { bubbles: true }));
}
`,
});
} else {
await this.cdp.send("Runtime.evaluate", {
expression: `
// Set response textarea
const textarea = document.querySelector('#g-recaptcha-response');
if (textarea) {
textarea.style.display = 'block';
textarea.value = '${token}';
}
// Set all hidden fields
document.querySelectorAll('[name="g-recaptcha-response"]')
.forEach(el => { el.value = '${token}'; });
// Trigger callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
const client = clients[key];
for (const prop in client) {
const val = client[prop];
if (val && typeof val === 'object') {
for (const p in val) {
if (typeof val[p]?.callback === 'function') {
val[p].callback('${token}');
}
}
}
}
}
}
`,
});
}
}
// Full workflow
async solveOnPage(url) {
await this.navigate(url);
await this.sleep(2000);
const captcha = await this.detectSitekey();
if (!captcha) {
console.log("No CAPTCHA detected");
return null;
}
console.log(`Detected: ${captcha.type} (${captcha.sitekey})`);
const token = await this.solveCaptcha(url, captcha.sitekey, captcha.type);
await this.injectToken(token, captcha.type);
console.log("Token injected");
return token;
}
// HTTP helpers
httpPost(url, data) {
return new Promise((resolve, reject) => {
const params = new URLSearchParams(data).toString();
const u = new URL(url);
const req = https.request({
hostname: u.hostname, path: u.pathname,
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
}, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(JSON.parse(body)));
});
req.on("error", reject);
req.write(params);
req.end();
});
}
httpGet(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(JSON.parse(body)));
}).on("error", reject);
});
}
sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
}
يوحّد CDPCaptchaSolver بين طبقتين: جلسة CDP للتحكم في الصفحة، ونقطتا النهاية in.php وres.php لدى CaptchaAI. لاحظ الفرق في الحمولة — يستخدم reCAPTCHA الحقل googlekey مع الأسلوب userrecaptcha، بينما يستخدم Turnstile الحقل sitekey مع الأسلوب turnstile. الدالة solveCaptcha تستطلع النتيجة دورياً كل خمس ثوانٍ حتى تنتهي المعالجة أو تنقضي المهلة.
اعتراض طلبات CAPTCHA على مستوى الشبكة
تفعيل نطاق Fetch لإيقاف الطلبات
async function interceptCaptchaRequests(solver) {
// Enable Fetch domain for request interception
await solver.cdp.send("Fetch.enable", {
patterns: [
{ urlPattern: "*recaptcha*", requestStage: "Request" },
{ urlPattern: "*turnstile*", requestStage: "Request" },
{ urlPattern: "*challenges.cloudflare.com*", requestStage: "Request" },
],
});
solver.cdp.on("Fetch.requestPaused", async (params) => {
const { requestId, request } = params;
console.log(`Intercepted: ${request.method} ${request.url}`);
// Log CAPTCHA-related requests for debugging
if (request.url.includes("userverify") || request.url.includes("reload")) {
console.log("CAPTCHA verification request detected");
}
// Continue the request
await solver.cdp.send("Fetch.continueRequest", { requestId });
});
}
يوقف Fetch.enable طلبات CAPTCHA المطابقة للأنماط المحددة قبل إرسالها، فتستطيع تسجيلها أو تشخيصها ثم متابعتها عبر Fetch.continueRequest. هذه نقطة مراقبة قيّمة أثناء تصحيح تدفق التحقق داخل بيئة الاختبار.
تنفيذ CDP في Python
نقل نفس المنطق إلى asyncio
import asyncio
import aiohttp
import json
class CDPCaptchaSolver:
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, cdp_port=9222):
self.api_key = api_key
self.cdp_port = cdp_port
self.ws = None
self.msg_id = 0
async def connect(self):
async with aiohttp.ClientSession() as session:
async with session.get(
f"http://127.0.0.1:{self.cdp_port}/json/list"
) as resp:
targets = await resp.json()
target = next(t for t in targets if t["type"] == "page")
self.ws = await asyncio.get_event_loop().create_connection(
lambda: CDPProtocol(self),
target["webSocketDebuggerUrl"],
)
async def send(self, method, params=None):
self.msg_id += 1
msg = {"id": self.msg_id, "method": method, "params": params or {}}
self.ws.send(json.dumps(msg))
# Wait for response (simplified)
return await self._wait_response(self.msg_id)
async def solve_and_inject(self, url):
await self.send("Page.navigate", {"url": url})
await asyncio.sleep(3)
# Detect sitekey via Runtime.evaluate
result = await self.send("Runtime.evaluate", {
"expression": "document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')",
"returnByValue": True,
})
sitekey = result.get("result", {}).get("value")
if not sitekey:
return None
# Solve via CaptchaAI
token = await self._solve_recaptcha(url, sitekey)
# Inject
await self.send("Runtime.evaluate", {
"expression": f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
document.querySelectorAll('[name="g-recaptcha-response"]')
.forEach(el => {{ el.value = '{token}'; }});
""",
})
return token
async def _solve_recaptcha(self, site_url, sitekey):
import requests
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key, "method": "userrecaptcha",
"googlekey": sitekey, "pageurl": site_url, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(60):
await asyncio.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] == 1:
return data["request"]
raise TimeoutError("CAPTCHA solve timeout")
تنقل نسخة Python المنطق نفسه إلى بيئة asyncio: تتصل بالجلسة، تكتشف مفتاح الموقع، تحل عبر CaptchaAI، ثم تحقن التوكن — وهو ما يناسب خطوط الاختبار المبنية على aiohttp وrequests.
سيناريو كامل: من الاكتشاف إلى حقن التوكن
// Full workflow
async function main() {
const solver = new CDPCaptchaSolver("YOUR_API_KEY");
await solver.init(9222);
// Enable network monitoring
await interceptCaptchaRequests(solver);
// Solve CAPTCHA on target page
const token = await solver.solveOnPage("https://example.com/login");
if (token) {
// Submit form
await solver.cdp.send("Runtime.evaluate", {
expression: `document.querySelector('form').submit()`,
});
}
}
main().catch(console.error);
مثال عملي: يدير فريق ضمان جودة في منصة تجارة إلكترونية بالخليج بيئة staging مطابقة للإنتاج ومحمية بـ reCAPTCHA v2 على صفحة تسجيل الدخول. بدلاً من الإدخال اليدوي المتكرر في كل دورة اختبار، يشغّل الفريق جلسة CDP تكتشف مفتاح الموقع تلقائياً، ترسل المهمة إلى CaptchaAI، وتحقن التوكن — فتكتمل دورة الاختبار الليلية دون تدخل بشري، مع سجل كامل لكل طلب شبكة اعتُرض أثناء التشغيل.
أفضل ممارسات التشغيل الآمن
قبل تشغيل أي جلسة CDP على نطاق واسع، ثبّت الضوابط التالية لتبقى الأتمتة داخل الإطار المأذون وسهلة الصيانة:
- شغّل الاختبارات على بيئات staging تملكها أو مفوّض لك اختبارها، لا على مواقع إنتاج تابعة لجهات أخرى.
- خزّن مفتاح CaptchaAI في متغيّر بيئة، ولا تضعه مباشرة داخل السكربت أو في سجلات التشغيل.
- سجّل كل طلب شبكة اعتُرض في مسار تدقيق قابل للمراجعة، فهذا يسرّع تشخيص الأعطال لاحقاً.
- راقب الرصيد ومعدل الحل بمرور الوقت لتضبط عدد محاولات الاستطلاع والمهلة بما يناسب حجم الحِمل.
اجعل CDP أداةً لاختبار الجودة وضمان موثوقية سير العمل داخل بيئة تملكها، لا وسيلةً للتعامل مع ضوابط طرف ثالث دون إذن.
حل المشكلات الشائعة في CDP
| المشكلة | السبب | الإجراء |
|---|---|---|
| تعذّر الاتصال بـ Chrome | لم يُفعّل التصحيح عن بُعد | شغّل Chrome بالوسيط --remote-debugging-port=9222 |
| انقطاع اتصال WebSocket | تعطّل المتصفح أو أُغلقت علامة التبويب | أضف منطق إعادة اتصال تلقائي |
فشل Runtime.evaluate |
الصفحة لم تُحمّل بعد | انتظر حدث Page.loadEventFired قبل التنفيذ |
| انتهاء المهلة أثناء الحل | زمن حل أطول من المتوقع تحت الحِمل | ارفع عدد محاولات الاستطلاع أو زد المهلة بين الطلبات |
| فشل حقن التوكن | عنصر CAPTCHA داخل Shadow DOM | استخدم DOM.describeNode لاجتياز جذور الظل (shadow roots) |
أسئلة شائعة حول CDP وCaptchaAI
كيف أتعامل مع CAPTCHA داخل Shadow DOM عبر CDP؟
عندما يكون عنصر CAPTCHA داخل Shadow DOM، لن تصل إليه استعلامات document.querySelector العادية. استخدم DOM.describeNode مع تفعيل خيار pierce لاجتياز جذور الظل والوصول إلى العنصر الهدف قبل قراءة مفتاح الموقع أو حقن التوكن.
ما الفرق بين Fetch.enable وNetwork.enable لاعتراض الطلبات؟
يتيح Network.enable مراقبة الطلبات والاستجابات وتسجيلها فقط، بينما يمنحك Fetch.enable القدرة على إيقاف الطلب مؤقتاً عبر Fetch.requestPaused ثم تعديله أو متابعته. استخدم Fetch حين تحتاج إلى التدخل في الطلب، وNetwork حين يكفيك الرصد.
هل يمكن تشغيل جلسة CDP في وضع headless على خادم اختبار؟
نعم. أضف الوسيط --headless=new عند تشغيل Chrome مع --remote-debugging-port، وستعمل أوامر CDP نفسها دون واجهة رسومية. هذا مناسب لخطوط CI التي تشغّل اختبارات الليل على خوادم بلا شاشة.
هل يتعارض CDP مع سياسات الحماية على الموقع؟
لا. CDP مجرد واجهة تحكم وتشخيص للمتصفح. إذا كان الموقع يفرض تحديات معينة، فاختبرها داخل بيئة تملكها أو بيئة staging مفوّضة، ولا تستخدم CDP وسيلةً لتغيير سياسات الحماية.
روابط ذات صلة
- ابدأ سريعاً مع CaptchaAI: أول عملية حل خلال 5 دقائق
- تأمين الـ Webhook في CaptchaAI: التحقق من صحة تواقيع رد النداء
جاهز للبدء؟ أنشئ مفتاح CaptchaAI الخاص بك واربطه بجلسة CDP لأتمتة اختبار CAPTCHA على مستوى البروتوكول.