Spaces:
Paused
Paused
| /** | |
| * 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'], | |
| }; | |
| } | |