Files
fn_registry/functions/browser/cdp_click_human.go
T
Egutierrez ccfa5bc78b feat(browser): funciones anti-deteccion + perfiles para web_scraping
Funciones nuevas del dominio browser (grupo navegator):
- cdp_move_mouse_human / cdp_click_human: movimiento de raton con curva
  de Bezier cubica, easing y micro-jitter para imitar comportamiento
  humano y reducir deteccion de automatizacion.
- cdp_wait_idle: espera network-idle contando requests en vuelo via
  eventos CDP Network.*; inmune a extensiones que mutan el DOM
  (Dark Reader, uBlock) y a animaciones JS.
- list_chrome_profiles: lista perfiles de un user-data-dir (extensiones,
  nombre legible, preferencias).
- prepare_chrome_profile (bash): clona un user-data-dir conservando solo
  una whitelist de extensiones (default uBlock Origin Lite).

Modificadas:
- chrome_launch: Linux-first (chromium/google-chrome/brave antes que
  chrome.exe), KeepExtensions y Setpgid para matar el arbol con cdp_close.
- cdp_close: kill por grupo de proceso.

Todas con tests verdes (go test ./functions/browser ok).
2026-06-05 16:25:11 +02:00

117 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package browser
import (
"fmt"
"math/rand"
"strings"
"time"
)
// CdpClickHuman hace click en el elemento identificado por selector CSS con
// movimiento humano: obtiene el bbox, calcula un punto destino ligeramente
// desplazado del centro, mueve el ratón por una trayectoria de Bézier cúbica
// y luego despacha mousePressed/mouseReleased con una micro-pausa entre ellos.
//
// opts controla la trayectoria del movimiento previo al click.
// Para configurar el origen del movimiento usa opts.FromX / opts.FromY.
func CdpClickHuman(c *CDPConn, selector string, opts MouseHumanOpts) error {
if c == nil {
return fmt.Errorf("cdp click human: conexion nula")
}
// Obtener bounding box del selector
js := fmt.Sprintf(`(function() {
var el = document.querySelector(%q);
if (!el) return null;
var r = el.getBoundingClientRect();
return JSON.stringify({x: r.left, y: r.top, w: r.width, h: r.height});
})()`, selector)
bboxStr, err := CdpEvaluate(c, js)
if err != nil {
return fmt.Errorf("cdp click human: obtener bbox de %q: %w", selector, err)
}
if bboxStr == "" || bboxStr == "null" {
return fmt.Errorf("cdp click human: elemento %q no encontrado en el DOM", selector)
}
bboxStr = strings.Trim(bboxStr, `"`)
bx, by, bw, bh, err := parseBbox(bboxStr)
if err != nil {
return fmt.Errorf("cdp click human: parsear bbox %q: %w", bboxStr, err)
}
// Scroll al elemento para que sea visible
scrollJS := fmt.Sprintf(`document.querySelector(%q).scrollIntoView({block:'center'})`, selector)
if _, err := CdpEvaluate(c, scrollJS); err != nil {
_ = err // no fatal
}
// Punto destino: centro + pequeño offset aleatorio (±15% del tamaño)
offX := (rand.Float64()*2 - 1) * bw * 0.15
offY := (rand.Float64()*2 - 1) * bh * 0.15
toX := bx + bw/2 + offX
toY := by + bh/2 + offY
// Mover el ratón con trayectoria humana
if err := CdpMoveMouseHuman(c, toX, toY, opts); err != nil {
return fmt.Errorf("cdp click human: mover raton: %w", err)
}
// mousePressed
clickParams := map[string]any{
"type": "mousePressed",
"x": toX,
"y": toY,
"button": "left",
"clickCount": 1,
}
if _, err := c.sendCDP("Input.dispatchMouseEvent", clickParams); err != nil {
return fmt.Errorf("cdp click human: mousePressed: %w", err)
}
// Micro-pausa humana entre press y release (3090 ms)
pauseMs := 30 + rand.Intn(61)
time.Sleep(time.Duration(pauseMs) * time.Millisecond)
// mouseReleased
clickParams["type"] = "mouseReleased"
if _, err := c.sendCDP("Input.dispatchMouseEvent", clickParams); err != nil {
return fmt.Errorf("cdp click human: mouseReleased: %w", err)
}
return nil
}
// parseBbox extrae left, top, width, height de un JSON como {"x":10,"y":20,"w":100,"h":40}.
func parseBbox(s string) (left, top, width, height float64, err error) {
// Reutiliza el mismo parser manual que parseCoords para evitar encoding/json
s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "{")
s = strings.TrimSuffix(s, "}")
for part := range strings.SplitSeq(s, ",") {
kv := strings.SplitN(strings.TrimSpace(part), ":", 2)
if len(kv) != 2 {
continue
}
k := strings.Trim(strings.TrimSpace(kv[0]), `"`)
var v float64
if _, e := fmt.Sscanf(strings.TrimSpace(kv[1]), "%f", &v); e != nil {
err = fmt.Errorf("parsear valor %q: %w", kv[1], e)
return
}
switch k {
case "x":
left = v
case "y":
top = v
case "w":
width = v
case "h":
height = v
}
}
return
}