Files
fn_registry/functions/browser/cdp_move_mouse_human.go
T
egutierrez 216cad4c12 perf(browser): acelera CDP — enable cacheado, wait_load por evento, timeout en sendCDP, escritura insertText
Optimiza el dominio browser para que el manejo del navegador via CDP sea mucho más rápido en automatización propia, manteniendo el camino sigiloso disponible.

- CDPConn cachea los enable de Accessibility/Network/Page por conexión (ensureAX/ensureNetwork/ensurePage): elimina un round-trip redundante en cada percepción y espera, que son las operaciones más frecuentes del bucle percibir->actuar del agente.
- sendCDP adquiere timeout (cdpCmdTimeout 30s): antes una respuesta que Chrome nunca enviaba colgaba la goroutine del tool indefinidamente; ahora falla limpio y el retry puede reconectar.
- CdpWaitLoad pasa de polling de document.readyState cada 200ms a esperar el evento Page.loadEventFired, con fast path inicial de readyState y re-chequeo anti-carrera tras suscribir. Si la página ya está cargada retorna en microsegundos.
- cdp_wait_idle usa ensureNetwork y deja de hacer Network.disable al salir (borraba el estado y forzaba el enable de nuevo).
- Nuevas funciones de escritura rápida: CdpInsertText (todo el texto en un solo Input.insertText) y CdpTypeRefFast (focus + insertText). El chequeo de foco se extrajo a assertEditableFocus, compartido con CdpTypeText.
- CdpTypeText pasa su pausa entre caracteres de 10ms fija a aleatoria 15-65ms (ritmo humano irregular).
- El modo 'auto' se añade al perfil de ratón (MouseProfileForMode, mouseHumanDefaults, clickPauseMs) como alias rápido de 'fast'.

No se tocan las firmas públicas existentes; CdpTypeRef y CdpTypeText conservan su comportamiento (camino human).
2026-06-13 14:27:10 +02:00

211 lines
6.6 KiB
Go

package browser
import (
"fmt"
"math"
"math/rand"
"time"
)
// MouseHumanOpts configura el movimiento humano del ratón.
type MouseHumanOpts struct {
// Mode es la política de velocidad: "auto"/"fast" (rápido), "human" (sigiloso,
// también "") o "instant". Controla los defaults de Steps/DurationMs/JitterPx y
// la pausa press/release:
// - auto/fast: recta ~5 pts, 40-80ms, jitter mínimo (eventos de ratón reales,
// rápido — modo por defecto del MCP para automatización propia).
// - human: Bézier ~25 pts, 350-800ms, jitter 2px (sigilo anti-bot alto).
// - instant: sin movimiento de ratón (CdpMoveMouseHuman es no-op); el click
// por #ref usa element.click() JS. Para tests y fallback sin bbox.
// Los valores explícitos (Steps/DurationMs/JitterPx != 0) ganan al preset del modo.
Mode string
// Steps es el número de puntos intermedios de la curva (default según Mode).
Steps int
// DurationMs es la duración total aproximada del movimiento en milisegundos.
// Si es 0, se elige según Mode.
DurationMs int
// JitterPx es la desviación perpendicular máxima por punto en píxeles (default según Mode).
JitterPx float64
// FromX es la coordenada X de origen. Si < 0, se usa (0, 0) como origen.
FromX float64
// FromY es la coordenada Y de origen. Si < 0, se usa (0, 0) como origen.
FromY float64
}
// MouseProfileForMode construye las opciones de ratón para un modo de velocidad.
// Es la fuente única que MCP, runner YAML y CLI usan para mapear un modo a opts,
// sin duplicar números. El mapeo modo→valores concretos vive en mouseHumanDefaults.
// Un modo desconocido se trata como "human" (el más seguro).
func MouseProfileForMode(mode string) MouseHumanOpts {
switch mode {
case "auto", "fast", "instant", "human", "":
return MouseHumanOpts{Mode: mode, FromX: -1, FromY: -1}
default:
return MouseHumanOpts{Mode: "human", FromX: -1, FromY: -1}
}
}
// mouseHumanDefaults aplica valores por defecto a opts según opts.Mode.
func mouseHumanDefaults(opts MouseHumanOpts) MouseHumanOpts {
switch opts.Mode {
case "instant":
// El movimiento se omite en CdpMoveMouseHuman; valores mínimos por si acaso.
if opts.Steps <= 0 {
opts.Steps = 1
}
if opts.DurationMs <= 0 {
opts.DurationMs = 1
}
// JitterPx se queda en 0.
case "fast", "auto":
if opts.Steps <= 0 {
opts.Steps = 5
}
if opts.DurationMs <= 0 {
opts.DurationMs = 40 + rand.Intn(41) // 40..80
}
// JitterPx se queda en lo recibido (0 por defecto, sin jitter en fast/auto).
default: // "human" o ""
if opts.Steps <= 0 {
opts.Steps = 25
}
if opts.DurationMs <= 0 {
opts.DurationMs = 350 + rand.Intn(451) // 350..800
}
if opts.JitterPx <= 0 {
opts.JitterPx = 2.0
}
}
if opts.FromX < 0 {
opts.FromX = 0
}
if opts.FromY < 0 {
opts.FromY = 0
}
return opts
}
// smoothstep aplica easing suave (ease-in-out) al parámetro t ∈ [0,1].
// Produce aceleración inicial y desaceleración final, imitando movimiento humano.
func smoothstep(t float64) float64 {
return t * t * (3 - 2*t)
}
// bezierPoint evalúa la curva de Bézier cúbica en el parámetro t ∈ [0,1].
// p0 = origen, p1/p2 = puntos de control, p3 = destino.
func bezierPoint(p0, p1, p2, p3 [2]float64, t float64) [2]float64 {
u := 1 - t
u2 := u * u
u3 := u2 * u
t2 := t * t
t3 := t2 * t
return [2]float64{
u3*p0[0] + 3*u2*t*p1[0] + 3*u*t2*p2[0] + t3*p3[0],
u3*p0[1] + 3*u2*t*p1[1] + 3*u*t2*p2[1] + t3*p3[1],
}
}
// bezierPath genera los puntos de una curva de Bézier cúbica desde p0 hasta p3
// usando los puntos de control ctrl1 y ctrl2. Retorna steps+1 puntos
// (incluye origen y destino). Esta función es pura y testeable sin Chrome.
func bezierPath(p0, p3, ctrl1, ctrl2 [2]float64, steps int) [][2]float64 {
if steps < 1 {
steps = 1
}
pts := make([][2]float64, steps+1)
for i := 0; i <= steps; i++ {
t := smoothstep(float64(i) / float64(steps))
pts[i] = bezierPoint(p0, ctrl1, ctrl2, p3, t)
}
return pts
}
// randomControlPoints genera dos puntos de control aleatorios desplazados
// lateralmente del segmento recto p0→p3, produciendo el arco curvo humano.
func randomControlPoints(p0, p3 [2]float64) ([2]float64, [2]float64) {
dx := p3[0] - p0[0]
dy := p3[1] - p0[1]
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 1 {
dist = 1
}
// Vector perpendicular unitario al segmento
perpX := -dy / dist
perpY := dx / dist
// Desplazamiento lateral: entre 10% y 40% de la distancia total
lat1 := dist * (0.1 + rand.Float64()*0.3) * (1 - 2*float64(rand.Intn(2)))
lat2 := dist * (0.1 + rand.Float64()*0.3) * (1 - 2*float64(rand.Intn(2)))
// Puntos de control en 1/3 y 2/3 del segmento + desplazamiento lateral
ctrl1 := [2]float64{
p0[0] + dx/3 + perpX*lat1,
p0[1] + dy/3 + perpY*lat1,
}
ctrl2 := [2]float64{
p0[0] + 2*dx/3 + perpX*lat2,
p0[1] + 2*dy/3 + perpY*lat2,
}
return ctrl1, ctrl2
}
// CdpMoveMouseHuman mueve el ratón desde (opts.FromX, opts.FromY) hasta (toX, toY)
// siguiendo una trayectoria de Bézier cúbica con easing suave y micro-jitter,
// imitando el movimiento humano para reducir la detección de automatización.
//
// Despacha Input.dispatchMouseEvent {type:"mouseMoved"} en cada punto de la curva
// con pausas proporcionales a DurationMs/Steps (±20% de variación aleatoria).
func CdpMoveMouseHuman(c *CDPConn, toX, toY float64, opts MouseHumanOpts) error {
if c == nil {
return fmt.Errorf("cdp move mouse human: conexion nula")
}
opts = mouseHumanDefaults(opts)
// Modo instant: sin movimiento de ratón (el click lo resuelve quien llama,
// por coords directas o por element.click() JS).
if opts.Mode == "instant" {
return nil
}
p0 := [2]float64{opts.FromX, opts.FromY}
p3 := [2]float64{toX, toY}
ctrl1, ctrl2 := randomControlPoints(p0, p3)
pts := bezierPath(p0, p3, ctrl1, ctrl2, opts.Steps)
// Pausa base por paso en microsegundos
baseStepUs := int64(opts.DurationMs) * 1000 / int64(opts.Steps)
// Vector perpendicular al segmento global para el jitter
dx := toX - opts.FromX
dy := toY - opts.FromY
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 1 {
dist = 1
}
perpX := -dy / dist
perpY := dx / dist
for _, pt := range pts {
// Micro-jitter perpendicular aleatorio
jitter := (rand.Float64()*2 - 1) * opts.JitterPx
x := pt[0] + perpX*jitter
y := pt[1] + perpY*jitter
if _, err := c.sendCDP("Input.dispatchMouseEvent", map[string]any{
"type": "mouseMoved",
"x": x,
"y": y,
}); err != nil {
return fmt.Errorf("cdp move mouse human: mouseMoved: %w", err)
}
// Pausa con variación ±20%
variation := int64(float64(baseStepUs) * (0.8 + rand.Float64()*0.4))
time.Sleep(time.Duration(variation) * time.Microsecond)
}
return nil
}