feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
+76
View File
@@ -0,0 +1,76 @@
package browser
import (
"encoding/json"
"fmt"
)
// detectCaptchaJS es la unica evaluacion que DetectCaptcha corre en el top frame.
// Detecta reCAPTCHA, hCaptcha y Cloudflare Turnstile por la presencia de sus
// iframes/widgets (los iframe[src] son legibles desde el top aunque su contenido
// sea cross-origin) y el JS-challenge de Cloudflare por texto en innerText.
// Siempre retorna un JSON serializable; en caso de excepcion devuelve detected=false
// con un campo "error" para que el caller no rompa (best-effort).
const detectCaptchaJS = `(function(){
try {
var sigs = [];
var q = function(s){ return document.querySelector(s); };
if (q('iframe[src*="recaptcha/api2"], iframe[src*="recaptcha/enterprise"], .g-recaptcha, #recaptcha')) sigs.push('recaptcha');
if (q('iframe[src*="hcaptcha.com"], .h-captcha')) sigs.push('hcaptcha');
if (q('iframe[src*="challenges.cloudflare.com"], .cf-turnstile')) sigs.push('turnstile');
var t = ((document.body && document.body.innerText) || '').toLowerCase().slice(0, 4000);
if (/checking your browser|verify(ing)? you are human|i'?m not a robot|are you a robot|unusual traffic|complete the security check|press and hold/.test(t)) sigs.push('challenge');
var seen = {}, uniq = [];
for (var i=0;i<sigs.length;i++){ if(!seen[sigs[i]]){seen[sigs[i]]=1;uniq.push(sigs[i]);} }
return JSON.stringify({detected: uniq.length>0, types: uniq, url: location.href});
} catch(e){ return JSON.stringify({detected:false, types:[], url: (location&&location.href)||'', error:String(e)}); }
})()`
// captchaResult es el shape del JSON que produce detectCaptchaJS.
type captchaResult struct {
Detected bool `json:"detected"`
Types []string `json:"types"`
URL string `json:"url"`
Error string `json:"error"`
}
// parseCaptchaSignals parsea el JSON que produce detectCaptchaJS. Es puro y
// testeable sin navegador. Si el JSON trae un campo "error" (excepcion JS en la
// pagina) se trata como detected=false best-effort, no como fallo. types es
// siempre un slice no nulo (vacio si no hay senales). Solo retorna error si el
// JSON es invalido / no parseable.
func parseCaptchaSignals(raw string) (detected bool, types []string, url string, err error) {
var r captchaResult
if err := json.Unmarshal([]byte(raw), &r); err != nil {
return false, nil, "", fmt.Errorf("parse captcha signals: json invalido: %w", err)
}
if r.Types == nil {
r.Types = []string{}
}
return r.Detected, r.Types, r.URL, nil
}
// DetectCaptcha detecta si la pagina actual presenta un captcha o challenge
// anti-bot. Corre UNA evaluacion JS en el top frame y parsea el resultado.
// NO resuelve ni notifica nada — solo detecta. Una responsabilidad.
//
// Retorna detected=true si hay al menos una senal, junto con los tipos
// detectados (subconjunto de: "recaptcha", "hcaptcha", "turnstile",
// "challenge") y la URL del top frame. Best-effort: una excepcion JS en la
// pagina se trata como "no detectado" sin romper.
func DetectCaptcha(c *CDPConn) (detected bool, types []string, url string, err error) {
if c == nil {
return false, nil, "", fmt.Errorf("detect captcha: conexion nula")
}
raw, err := CdpEvaluate(c, detectCaptchaJS)
if err != nil {
return false, nil, "", fmt.Errorf("detect captcha: %w", err)
}
detected, types, url, err = parseCaptchaSignals(raw)
if err != nil {
return false, nil, "", fmt.Errorf("detect captcha: %w", err)
}
return detected, types, url, nil
}