ccfa5bc78b
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).
117 lines
3.4 KiB
Go
117 lines
3.4 KiB
Go
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 (30–90 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
|
||
}
|