029dbf57bd
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
package browser
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
|
|
// Delegar en el primitivo compartido: mueve el ratón con trayectoria humana
|
|
// y despacha press/release con micro-pausa.
|
|
if err := CdpClickXYHuman(c, toX, toY, opts); err != nil {
|
|
return fmt.Errorf("cdp click human: %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
|
|
}
|