feat(core): auto-commit con 10 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CdpClickHuman hace click en el elemento identificado por selector CSS con
|
||||
@@ -53,31 +52,10 @@ func CdpClickHuman(c *CDPConn, selector string, opts MouseHumanOpts) error {
|
||||
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)
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package browser
|
||||
|
||||
import "fmt"
|
||||
|
||||
// refBoxCenter resuelve el centro (x,y) en coords de página de un nodo DOM por su
|
||||
// backendDOMNodeId, vía DOM.getBoxModel. El content quad son 8 floats (4 esquinas).
|
||||
func refBoxCenter(c *CDPConn, backendNodeID int) (float64, float64, error) {
|
||||
res, err := c.sendCDP("DOM.getBoxModel", map[string]any{"backendNodeId": backendNodeID})
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("getBoxModel ref %d: %w", backendNodeID, err)
|
||||
}
|
||||
model, ok := res["model"].(map[string]any)
|
||||
if !ok {
|
||||
return 0, 0, fmt.Errorf("ref %d: sin boxModel (nodo no visible o inexistente)", backendNodeID)
|
||||
}
|
||||
content, ok := model["content"].([]any)
|
||||
if !ok || len(content) < 8 {
|
||||
return 0, 0, fmt.Errorf("ref %d: content quad invalido", backendNodeID)
|
||||
}
|
||||
num := func(i int) float64 { f, _ := content[i].(float64); return f }
|
||||
cx := (num(0) + num(2) + num(4) + num(6)) / 4
|
||||
cy := (num(1) + num(3) + num(5) + num(7)) / 4
|
||||
return cx, cy, nil
|
||||
}
|
||||
|
||||
// CdpClickRef hace click humanizado (Bézier + jitter) sobre el elemento del #ref.
|
||||
// El #ref es un backendDOMNodeId extraído del AX outline por page_perceive.
|
||||
// Hace scroll al elemento si es necesario antes de calcular las coordenadas.
|
||||
func CdpClickRef(c *CDPConn, backendNodeID int, opts MouseHumanOpts) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp click ref: conexión nil")
|
||||
}
|
||||
// scroll al elemento si no está visible; ignorar error (no fatal)
|
||||
_, _ = c.sendCDP("DOM.scrollIntoViewIfNeeded", map[string]any{"backendNodeId": backendNodeID})
|
||||
cx, cy, err := refBoxCenter(c, backendNodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp click ref: %w", err)
|
||||
}
|
||||
return CdpClickXYHuman(c, cx, cy, opts)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: cdp_click_ref
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CdpClickRef(c *CDPConn, backendNodeID int, opts MouseHumanOpts) error"
|
||||
description: "Click humanizado (Bézier + jitter) sobre el elemento identificado por su #ref del AX outline. El #ref es el backendDOMNodeId estable del nodo DOM. Hace scroll al elemento si no está en viewport antes de calcular las coordenadas vía DOM.getBoxModel."
|
||||
tags: [cdp, browser, action, ref, humanized, navegator]
|
||||
uses_functions: [cdp_click_xy_human_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa al tab objetivo."
|
||||
- name: backendNodeID
|
||||
desc: "El #ref del AX outline = backendDOMNodeId estable del nodo DOM. Se obtiene de page_perceive / render_ax_outline."
|
||||
- name: opts
|
||||
desc: "Opciones de trayectoria humanizada (jitter, velocidad, curva Bézier). Zero-value da humanización por defecto."
|
||||
output: "nil si el click se completó; error si la conexión es nil, el nodo no tiene boxModel visible, o el click CDP falla."
|
||||
file_path: "functions/browser/cdp_click_ref.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Tras un page_perceive que devuelve outline con #ref=1234:
|
||||
conn, _ := CdpConnect(9222)
|
||||
err := CdpClickRef(conn, 1234, MouseHumanOpts{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras `page_perceive` / `render_ax_outline`, cuando el agente tiene el `#ref` de un elemento del outline y quiere hacer click sobre él sin necesitar un selector CSS — cierra el bucle percibir→actuar. Preferir sobre `CdpClickHuman` cuando el nodo viene del AX outline (más estable que un selector).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El `#ref` es un **backendDOMNodeId**, no el nodeId efímero del AX tree. Si la página recargó o navegó tras el snapshot, el ref puede estar muerto — re-percibir antes de actuar.
|
||||
- `DOM.getBoxModel` falla si el elemento no está en el DOM renderizado (display:none, fuera del shadow DOM accesible, o ya eliminado). El error describe la causa.
|
||||
- `DOM.scrollIntoViewIfNeeded` se invoca antes del cálculo de coordenadas pero su fallo se ignora (no fatal) — si el elemento no es scrollable al viewport el click puede caer en coordenadas incorrectas.
|
||||
- El click va por `CdpClickXYHuman` (Bézier): no despaches `Input.dispatchMouseEvent` crudo en código que use esta función.
|
||||
@@ -0,0 +1,49 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CdpClickXYHuman hace click en las coordenadas absolutas (x, y) de la página con
|
||||
// comportamiento humano: mueve el ratón hasta el punto por una trayectoria de
|
||||
// Bézier cúbica (CdpMoveMouseHuman) y despacha mousePressed/mouseReleased con una
|
||||
// micro-pausa variable (30-90 ms) entre ambos.
|
||||
//
|
||||
// Es el PRIMITIVO de click compartido por las tres vías de acción del agente:
|
||||
// - por selector CSS → CdpClickHuman (obtiene el bbox y llama aquí).
|
||||
// - por #ref del AX tree → CdpClickRef (resuelve backendDOMNodeId → bbox → aquí).
|
||||
// - por visión → click sobre el bounding box que devuelve OCR/YOLO.
|
||||
// Construir un único primitivo evita tener tres caminos de click divergentes.
|
||||
func CdpClickXYHuman(c *CDPConn, x, y float64, opts MouseHumanOpts) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp click xy human: conexion nula")
|
||||
}
|
||||
|
||||
// Mover el ratón hasta el destino con trayectoria humana.
|
||||
if err := CdpMoveMouseHuman(c, x, y, opts); err != nil {
|
||||
return fmt.Errorf("cdp click xy human: mover raton: %w", err)
|
||||
}
|
||||
|
||||
clickParams := map[string]any{
|
||||
"type": "mousePressed",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1,
|
||||
}
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", clickParams); err != nil {
|
||||
return fmt.Errorf("cdp click xy human: mousePressed: %w", err)
|
||||
}
|
||||
|
||||
// Micro-pausa humana entre press y release (30-90 ms).
|
||||
time.Sleep(time.Duration(30+rand.Intn(61)) * time.Millisecond)
|
||||
|
||||
clickParams["type"] = "mouseReleased"
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", clickParams); err != nil {
|
||||
return fmt.Errorf("cdp click xy human: mouseReleased: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: cdp_click_xy_human_go_browser
|
||||
name: cdp_click_xy_human
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Click humanizado en coordenadas absolutas (x,y): mueve el ratón con trayectoria Bézier y despacha mousePressed/mouseReleased con micro-pausa variable. Primitivo de click compartido por las tres vías de acción del agente: por selector, por #ref del AX tree y por visión (bounding box de OCR/YOLO)."
|
||||
tags: [cdp, browser, action, humanized, click, navegator]
|
||||
signature: "func CdpClickXYHuman(c *CDPConn, x, y float64, opts MouseHumanOpts) error"
|
||||
uses_functions:
|
||||
- cdp_move_mouse_human_go_browser
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_click_xy_human.go"
|
||||
example: |
|
||||
conn, _ := browser.CdpConnect(9333)
|
||||
defer browser.CdpClose(conn, 0)
|
||||
// Click humanizado en el centro de un elemento detectado por visión (bbox):
|
||||
browser.CdpClickXYHuman(conn, 412.0, 318.0, browser.MouseHumanOpts{})
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa (de CdpConnect)."
|
||||
- name: x
|
||||
desc: "Coordenada X absoluta en la página, en px CSS del viewport."
|
||||
- name: y
|
||||
desc: "Coordenada Y absoluta en la página, en px CSS del viewport."
|
||||
- name: opts
|
||||
desc: "Opciones de la trayectoria humana (zero-value = defaults). Origen del movimiento via FromX/FromY."
|
||||
output: "error si el movimiento del ratón o el despacho de eventos falla; nil en éxito."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := browser.CdpConnect(9333)
|
||||
defer browser.CdpClose(conn, 0)
|
||||
// El centro del bounding box lo da el #ref del AX tree (DOM.getBoxModel) o la
|
||||
// detección de visión (OCR/YOLO). Aquí, click humanizado sobre ese punto:
|
||||
if err := browser.CdpClickXYHuman(conn, 412.0, 318.0, browser.MouseHumanOpts{}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando ya tienes las coordenadas de píxel del objetivo: el centro del bounding box de un elemento
|
||||
(resuelto por `#ref` del AX outline vía `DOM.getBoxModel`, o detectado por visión OCR/YOLO). Es el
|
||||
único primitivo de click del agente — no despaches `Input.dispatchMouseEvent` a mano.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Coordenadas en el sistema de la página (px CSS del viewport), no de pantalla física.
|
||||
- La humanización añade latencia (movimiento Bézier + micro-pausa). Para scraping masivo de alto
|
||||
volumen, el llamador debe usar un preset rápido de `MouseHumanOpts` (política de sesión `fast`),
|
||||
no humanización completa por acción.
|
||||
- El destino debe estar dentro del viewport visible; haz scroll al elemento antes si hace falta.
|
||||
@@ -0,0 +1,19 @@
|
||||
package browser
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CdpHoverRef mueve el ratón con trayectoria humanizada (Bézier) sobre el
|
||||
// elemento del #ref. Útil para activar menús y tooltips que reaccionan a hover.
|
||||
// El #ref es un backendDOMNodeId extraído del AX outline por page_perceive.
|
||||
func CdpHoverRef(c *CDPConn, backendNodeID int, opts MouseHumanOpts) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp hover ref: conexión nil")
|
||||
}
|
||||
// scroll al elemento si no está visible; ignorar error (no fatal)
|
||||
_, _ = c.sendCDP("DOM.scrollIntoViewIfNeeded", map[string]any{"backendNodeId": backendNodeID})
|
||||
cx, cy, err := refBoxCenter(c, backendNodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp hover ref: %w", err)
|
||||
}
|
||||
return CdpMoveMouseHuman(c, cx, cy, opts)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: cdp_hover_ref
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CdpHoverRef(c *CDPConn, backendNodeID int, opts MouseHumanOpts) error"
|
||||
description: "Mueve el ratón con trayectoria humanizada (Bézier) sobre el elemento identificado por su #ref del AX outline. Útil para activar menús desplegables, tooltips y cualquier interacción que dependa de hover. El #ref es el backendDOMNodeId estable del nodo DOM."
|
||||
tags: [cdp, browser, action, ref, humanized, navegator]
|
||||
uses_functions: [cdp_move_mouse_human_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa al tab objetivo."
|
||||
- name: backendNodeID
|
||||
desc: "El #ref del AX outline = backendDOMNodeId estable del nodo DOM. Se obtiene de page_perceive / render_ax_outline."
|
||||
- name: opts
|
||||
desc: "Opciones de trayectoria humanizada (jitter, velocidad, curva Bézier). Zero-value da humanización por defecto."
|
||||
output: "nil si el movimiento de ratón se completó; error si la conexión es nil, el nodo no tiene boxModel visible, o el movimiento CDP falla."
|
||||
file_path: "functions/browser/cdp_hover_ref.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Activar un menú desplegable cuyo trigger tiene #ref=9999:
|
||||
conn, _ := CdpConnect(9222)
|
||||
err := CdpHoverRef(conn, 9999, MouseHumanOpts{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// esperar a que el menú aparezca y re-percibir el outline
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras `page_perceive` / `render_ax_outline`, cuando el agente necesita hacer hover sobre un elemento del `#ref` para revelar contenido oculto (menús, submenús, tooltips, dropdowns) — cierra el bucle percibir→actuar para interacciones hover. Seguir con otro `page_perceive` tras el hover para capturar el nuevo estado del DOM.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El `#ref` es un **backendDOMNodeId**, no el nodeId efímero del AX tree. Si la página recargó o navegó tras el snapshot, el ref puede estar muerto — re-percibir antes de actuar.
|
||||
- `DOM.getBoxModel` falla si el elemento no está en el DOM renderizado. El error describe la causa.
|
||||
- `DOM.scrollIntoViewIfNeeded` se invoca antes del cálculo de coordenadas pero su fallo se ignora (no fatal).
|
||||
- Solo mueve el ratón — no hace click. Para activar elementos que requieren click usar `CdpClickRef`.
|
||||
- Algunos menús hover requieren un pequeño `time.Sleep` o `CdpWaitIdle` tras el hover para que el DOM se actualice antes del siguiente `page_perceive`.
|
||||
@@ -0,0 +1,16 @@
|
||||
package browser
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CdpTypeRef enfoca el elemento del #ref vía CDP y escribe el texto dado.
|
||||
// El #ref es un backendDOMNodeId extraído del AX outline por page_perceive.
|
||||
// Equivale a: focus(ref) → CdpTypeText. El elemento debe aceptar input de texto.
|
||||
func CdpTypeRef(c *CDPConn, backendNodeID int, text string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp type ref: conexión nil")
|
||||
}
|
||||
if _, err := c.sendCDP("DOM.focus", map[string]any{"backendNodeId": backendNodeID}); err != nil {
|
||||
return fmt.Errorf("cdp type ref: focus ref %d: %w", backendNodeID, err)
|
||||
}
|
||||
return CdpTypeText(c, text)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: cdp_type_ref
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CdpTypeRef(c *CDPConn, backendNodeID int, text string) error"
|
||||
description: "Enfoca el elemento identificado por su #ref del AX outline vía DOM.focus y escribe el texto dado usando CdpTypeText. El #ref es el backendDOMNodeId estable del nodo DOM. El elemento debe aceptar input de texto (input, textarea, contenteditable)."
|
||||
tags: [cdp, browser, action, ref, humanized, navegator]
|
||||
uses_functions: [cdp_type_text_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa al tab objetivo."
|
||||
- name: backendNodeID
|
||||
desc: "El #ref del AX outline = backendDOMNodeId estable del nodo DOM. Se obtiene de page_perceive / render_ax_outline."
|
||||
- name: text
|
||||
desc: "Texto a escribir en el elemento enfocado. Se envía carácter a carácter simulando escritura humana."
|
||||
output: "nil si el focus y la escritura se completaron; error si la conexión es nil, DOM.focus falla, o CdpTypeText falla."
|
||||
file_path: "functions/browser/cdp_type_ref.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Tras un page_perceive que devuelve un input con #ref=5678:
|
||||
conn, _ := CdpConnect(9222)
|
||||
err := CdpTypeRef(conn, 5678, "hola mundo")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras `page_perceive` / `render_ax_outline`, cuando el agente quiere escribir en un campo de texto identificado por su `#ref` — cierra el bucle percibir→actuar para inputs. Preferir sobre secuencias manuales `DOM.focus` + `CdpTypeText` cuando el nodo viene directamente del AX outline.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El `#ref` es un **backendDOMNodeId**, no el nodeId efímero del AX tree. Si la página recargó o navegó tras el snapshot, el ref puede estar muerto — re-percibir antes de actuar.
|
||||
- `DOM.focus` falla si el elemento no es focusable (no es `input`, `textarea`, `contenteditable`, o similar). El error indica el ref y la causa.
|
||||
- Si el elemento necesita un click previo para activarse (algunos inputs con JS custom), combinar con `CdpClickRef` antes de `CdpTypeRef`.
|
||||
- No hace scroll previo — si el elemento no está visible en el viewport el focus CDP puede fallar en algunos navegadores. Combinar con `CdpClickRef` (que sí hace scroll) si hay dudas.
|
||||
Reference in New Issue
Block a user