zelin-bot / src /captcha-solver.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* captcha-solver.js — Sistema de Resolución de CAPTCHAs con IA
* ==============================================================
* Sin servicios de pago. Todo con IA disponible.
*
* ESTRATEGIA POR TIPO DE CAPTCHA:
*
* ① reCAPTCHA v2 (imagen) → Gemini Vision analiza cada tile
* Research: "YOLOv8 fine-tuned achieves 100% success on reCAPTCHAv2 image challenges"
* Nosotros usamos Gemini Vision (gratis) con el mismo enfoque de clasificación
*
* ② reCAPTCHA v2 Audio → Whisper/SpeechRecognition gratis
* Research: "Audio transcription with wit.ai: 70-80% success, FREE"
* GitHub: njraladdin/recaptcha-v2-solver — método audio
*
* ③ reCAPTCHA v3 (behavioral) → NO hay que resolverlo: se evita con buen stealth
* Research: "reCAPTCHA v3 uses behavioral scoring. Good behavior simulation
* achieves 0.9 trust score without solving any challenge"
*
* ④ hCaptcha → Mismo enfoque que reCAPTCHA v2 con Gemini Vision
*
* ⑤ Cloudflare Turnstile → JavaScript challenge, se resuelve solo con buen stealth
*
* ⑥ Text CAPTCHAs → OCR via Gemini Vision
*
* ⑦ CAPTCHAs irresolubles → Notificar al owner para resolución manual
*/
import { readConfig } from './utils.js';
const config = readConfig();
// ── Gemini Vision helper ──────────────────────────────────────────────────────
async function callGeminiVision(base64Image, prompt, mimeType = 'image/jpeg') {
const keys = config.ai?.gemini?.keys ?? [config.ai?.gemini?.apiKey].filter(Boolean);
const key = keys[0];
if (!key) return null;
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`,
{
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({
contents: [{ parts: [
{ text: prompt },
{ inline_data: { mime_type: mimeType, data: base64Image } },
]}],
generationConfig: { maxOutputTokens: 200, temperature: 0.1 },
}),
signal: AbortSignal.timeout(15000),
}
);
if (!res.ok) return null;
return (await res.json()).candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? null;
}
// ── Audio download y transcripción ──────────────────────────────────────────
async function transcribeAudio(audioUrl) {
try {
// Descargar el audio del CAPTCHA
const audioRes = await fetch(audioUrl, { signal: AbortSignal.timeout(15000) });
const audioData = Buffer.from(await audioRes.arrayBuffer());
const base64 = audioData.toString('base64');
// Usar Gemini para transcribir (soporta audio nativo)
const keys = config.ai?.gemini?.keys ?? [config.ai?.gemini?.apiKey].filter(Boolean);
const key = keys[0];
if (!key) return null;
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`,
{
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({
contents: [{ parts: [
{ text: 'This is a CAPTCHA audio challenge. Transcribe the spoken digits or words exactly. Reply with ONLY the numbers/words, nothing else.' },
{ inline_data: { mime_type: 'audio/mp3', data: base64 } },
]}],
generationConfig: { maxOutputTokens: 50, temperature: 0 },
}),
signal: AbortSignal.timeout(20000),
}
);
if (!res.ok) return null;
const text = (await res.json()).candidates?.[0]?.content?.parts?.[0]?.text?.trim();
// Limpiar respuesta: solo números y letras
return text?.replace(/[^a-zA-Z0-9\s]/g, '').trim() ?? null;
} catch {
return null;
}
}
function sleep(ms) { return new Promise(r => setTimeout(r, Math.round(ms))); }
// ═══════════════════════════════════════════════════════════════════════════════
// SOLUCIONADORES POR TIPO
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Resolver reCAPTCHA v2 con imágenes
* Estrategia: screenshot de cada tile → Gemini Vision clasifica → clic en los correctos
*/
export async function solveRecaptchaV2Images(page) {
console.log('[CAPTCHA] 🔍 Intentando resolver reCAPTCHA v2 por imágenes...');
try {
// Intentar primero el checkbox "I'm not a robot" (a veces pasa solo con buen stealth)
const checkbox = await page.$('.recaptcha-checkbox-border, #recaptcha-anchor');
if (checkbox) {
await checkbox.click();
await sleep(2000 + Math.random() * 1000);
// Verificar si el checkbox se marcó sin challenge
const solved = await page.evaluate(() => {
const resp = document.querySelector('textarea#g-recaptcha-response, [name="g-recaptcha-response"]');
return resp && resp.value.length > 10;
}).catch(() => false);
if (solved) {
console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto sin challenge (buen stealth)');
return { success: true, method: 'stealth_no_challenge' };
}
}
// Hay challenge de imágenes — resolver con visión
// Obtener el iframe del CAPTCHA
const frame = page.frames().find(f => f.url().includes('recaptcha'));
if (!frame) return { success: false, method: 'no_frame' };
// Obtener la instrucción del challenge
const instruction = await frame.$eval('.rc-imageselect-desc-wrapper', el => el.textContent?.trim() ?? '').catch(() => '');
console.log('[CAPTCHA] Instrucción:', instruction.slice(0, 80));
// Hacer screenshot del área de imágenes
const grid = await frame.$('.rc-imageselect-table');
if (!grid) return { success: false, method: 'no_grid' };
const gridScreenshot = await grid.screenshot({ type: 'jpeg', quality: 90 });
const base64Grid = gridScreenshot.toString('base64');
// Gemini Vision: analizar qué tiles seleccionar
const prompt = `This is a reCAPTCHA image grid challenge. The instruction says: "${instruction}"
Analyze each tile in the grid (numbered left-to-right, top-to-bottom starting from 1).
Reply with ONLY the tile numbers that match the instruction, separated by commas.
Example: "1,3,7" or "2,5,6,9"
If no tiles match, reply "none".
Do not explain, just give the numbers.`;
const answer = await callGeminiVision(base64Grid, prompt);
if (!answer || answer.toLowerCase() === 'none') {
console.log('[CAPTCHA] Gemini no encontró tiles para seleccionar');
return { success: false, method: 'vision_no_match' };
}
// Parsear números de tiles
const tileNumbers = answer.split(',').map(n => parseInt(n.trim())).filter(n => !isNaN(n) && n >= 1);
console.log('[CAPTCHA] Tiles a seleccionar:', tileNumbers);
// Hacer clic en los tiles indicados con comportamiento humano
for (const tileNum of tileNumbers) {
const tile = await frame.$(`.rc-imageselect-tile:nth-child(${tileNum}), .rc-image-tile-target:nth-child(${tileNum})`);
if (tile) {
const box = await tile.boundingBox();
if (box) {
const x = box.x + box.width * (0.2 + Math.random() * 0.6);
const y = box.y + box.height * (0.2 + Math.random() * 0.6);
await page.mouse.move(x, y);
await sleep(200 + Math.random() * 300);
await page.mouse.click(x, y);
await sleep(300 + Math.random() * 400);
}
}
}
// Hacer clic en Verify
await sleep(1000 + Math.random() * 500);
const verifyBtn = await frame.$('#recaptcha-verify-button, .rc-button-default');
if (verifyBtn) {
await verifyBtn.click();
await sleep(2000 + Math.random() * 1000);
}
// Verificar resultado
const token = await page.evaluate(() => {
const t = document.querySelector('textarea[name="g-recaptcha-response"]');
return t?.value ?? '';
}).catch(() => '');
if (token.length > 20) {
console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto por visión');
return { success: true, method: 'vision', token };
}
return { success: false, method: 'vision_unverified' };
} catch (e) {
console.warn('[CAPTCHA] Error en reCAPTCHA v2:', e.message);
return { success: false, error: e.message };
}
}
/**
* Resolver reCAPTCHA v2 por audio (más fiable que imágenes)
* Estrategia: solicitar versión audio → descargar → transcribir con Gemini → escribir
*/
export async function solveRecaptchaV2Audio(page) {
console.log('[CAPTCHA] 🎵 Intentando reCAPTCHA v2 por audio...');
try {
const frame = page.frames().find(f => f.url().includes('recaptcha'));
if (!frame) return { success: false, method: 'no_frame' };
// Cambiar a modo audio
const audioBtn = await frame.$('#recaptcha-audio-button');
if (!audioBtn) {
// Intentar via checkbox primero
const checkbox = await frame.$('#recaptcha-anchor');
if (checkbox) { await checkbox.click(); await sleep(1500); }
}
const audioBtnRetry = await frame.$('#recaptcha-audio-button');
if (!audioBtnRetry) return { success: false, method: 'no_audio_button' };
await audioBtnRetry.click();
await sleep(2000);
// Obtener la URL del audio
const audioUrl = await frame.$eval('.rc-audiochallenge-tdownload-link', el => el.href).catch(() => null);
if (!audioUrl) return { success: false, method: 'no_audio_url' };
// Transcribir con Gemini
const transcription = await transcribeAudio(audioUrl);
if (!transcription) return { success: false, method: 'transcription_failed' };
console.log('[CAPTCHA] Transcripción:', transcription);
// Escribir la respuesta en el campo
const input = await frame.$('#audio-response');
if (!input) return { success: false, method: 'no_input' };
await input.click();
await sleep(200);
await page.keyboard.type(transcription.toLowerCase().replace(/\s+/g, ' '), { delay: 80 + Math.random() * 40 });
await sleep(500);
// Verificar
const verifyBtn = await frame.$('#recaptcha-verify-button');
if (verifyBtn) { await verifyBtn.click(); await sleep(2000); }
const token = await page.evaluate(() =>
document.querySelector('textarea[name="g-recaptcha-response"]')?.value ?? ''
).catch(() => '');
if (token.length > 20) {
console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto por audio');
return { success: true, method: 'audio', token };
}
return { success: false, method: 'audio_unverified' };
} catch (e) {
return { success: false, error: e.message };
}
}
/**
* Resolver text CAPTCHA (imagen con texto/números distorsionados)
*/
export async function solveTextCaptcha(page, imageSelector) {
try {
const imgEl = await page.$(imageSelector);
if (!imgEl) return null;
const imgBase64 = (await imgEl.screenshot({ type: 'jpeg', quality: 95 })).toString('base64');
const answer = await callGeminiVision(imgBase64,
'This is a text CAPTCHA image with distorted characters. Read the characters carefully and reply with ONLY the text shown, no explanation.'
);
return answer?.replace(/\s/g, '') ?? null;
} catch { return null; }
}
/**
* Detector principal de CAPTCHA + resolutor automático
*/
export async function detectAndSolve(page, notifyOwner = null) {
console.log('[CAPTCHA] Escaneando página...');
// Detectar tipo de CAPTCHA presente
const captchaInfo = await page.evaluate(() => {
const body = document.body.innerHTML.toLowerCase();
const text = document.body.textContent.toLowerCase();
return {
hasRecaptchaV2 : !!document.querySelector('iframe[src*="recaptcha"]') || body.includes('g-recaptcha'),
hasHcaptcha : body.includes('hcaptcha') || !!document.querySelector('iframe[src*="hcaptcha"]'),
hasCloudflare : text.includes('checking your browser') || text.includes('verifying you are') || body.includes('cf-challenge'),
hasTurnstile : body.includes('turnstile') || !!document.querySelector('[data-sitekey]'),
hasTextCaptcha : !!document.querySelector('img[src*="captcha"], img[alt*="captcha"]'),
};
}).catch(() => ({}));
console.log('[CAPTCHA] Detectado:', JSON.stringify(captchaInfo));
// 1. Cloudflare — solo stealth, no hay challenge que resolver directamente
if (captchaInfo.hasCloudflare) {
console.log('[CAPTCHA] Cloudflare detectado — esperando resolución automática...');
await sleep(5000 + Math.random() * 3000);
// Si sigue ahí después de 8s, notificar
const stillCloudflare = await page.evaluate(() =>
document.body.textContent.toLowerCase().includes('checking your browser')
).catch(() => false);
if (stillCloudflare && notifyOwner) {
await notifyOwner('⚠️ Cloudflare no se resolvió automáticamente. Puede necesitar proxy de mayor calidad.');
}
return { type: 'cloudflare', solved: !stillCloudflare };
}
// 2. reCAPTCHA v2 — intentar audio primero (más fiable), luego imagen
if (captchaInfo.hasRecaptchaV2) {
// Intentar método audio
let result = await solveRecaptchaV2Audio(page);
if (!result.success) {
// Fallback a imágenes
result = await solveRecaptchaV2Images(page);
}
if (!result.success && notifyOwner) {
await notifyOwner('⚠️ reCAPTCHA v2 no pudo resolverse automáticamente. Requiere intervención manual.');
}
return { type: 'recaptcha_v2', ...result };
}
// 3. hCaptcha — mismo enfoque que reCAPTCHA v2
if (captchaInfo.hasHcaptcha) {
console.log('[CAPTCHA] hCaptcha detectado — intentando resolver...');
// hCaptcha tiene estructura similar a reCAPTCHA v2
const result = await solveRecaptchaV2Images(page); // funciona por similitud
if (!result.success && notifyOwner) {
await notifyOwner('⚠️ hCaptcha detectado. Requiere intervención manual.');
}
return { type: 'hcaptcha', ...result };
}
// 4. Text CAPTCHA
if (captchaInfo.hasTextCaptcha) {
const answer = await solveTextCaptcha(page, 'img[alt*="captcha"], img[src*="captcha"]');
return { type: 'text_captcha', solved: !!answer, answer };
}
return { type: 'none', solved: true };
}
export async function getCaptchaSolverStats() {
const geminiAvailable = !!(config.ai?.gemini?.keys?.length || config.ai?.gemini?.apiKey);
return {
geminiVision: geminiAvailable,
audioSolver : geminiAvailable,
supportedTypes: ['recaptcha_v2_image', 'recaptcha_v2_audio', 'hcaptcha', 'text_captcha', 'cloudflare_basic'],
notSupported : ['recaptcha_v3_enterprise', 'funcaptcha_advanced', 'geetest_v4'],
};
}