Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 029dbf57bd | |||
| 3f6b652f3f |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: fn-analizador
|
||||
description: "Agente analizador (Fase 4) del ciclo reactivo. Lee `e2e_checks` declarados en app.md, ejecuta la suite via `e2e_run_checks_go_infra`, evalua assertions activas, calcula drift de metricas vs historico, persiste resultado en `e2e_runs` de operations.db y devuelve veredicto caveman pass/fail. NO modifica codigo ni propone fixes — eso es trabajo de fn-mejorador (Fase 5)."
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: fn-constructor
|
||||
description: "Agente constructor (Fase 1) del ciclo reactivo. Construye funciones, tests y tipos en Go, Python, TypeScript y Bash para fn_registry."
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: fn-executor
|
||||
description: "Agente ejecutor (Fase 2) del ciclo reactivo. Prepara apps, ejecuta pipelines/funciones Go y Python, y registra ejecuciones en operations.db."
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: fn-mejorador
|
||||
description: "Agente mejorador (Fase 5) del ciclo reactivo. Lee resultados fallidos de fn-analizador desde `e2e_runs`/`assertion_results`, busca contexto en el registry, y crea proposals con evidencia trazable. NO modifica codigo: solo abre proposals para que un humano (o el bucle autonomo del issue 0069) decida."
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: fn-orquestador
|
||||
description: "Meta-orquestador (Fase 6) del ciclo reactivo. Toma un issue o task_spec y recorre CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR despachando a fn-constructor/executor/recopilador/analizador/mejorador hasta convergencia, estancamiento, timeout o tope de iteraciones. Trabaja SIEMPRE en rama sandbox `auto/<issue>`, NUNCA mergea a master, persiste progreso en `task_runs`. Issue 0069."
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: fn-recopilador
|
||||
description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta. Modo extra `design-e2e <app_id>`: propone bloque `e2e_checks` para que la fase 4 (fn-analizador) pueda validar la app sin iteracion humana."
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Convierte una lista de AXNode CDP en un outline indentado legible por LLMs."""
|
||||
|
||||
|
||||
# Roles que se consideran accionables (el LLM puede referirlos con #ref=nodeId).
|
||||
# Roles que se consideran accionables (el LLM puede referirlos con #ref).
|
||||
_ACTIONABLE_ROLES = frozenset({
|
||||
"button",
|
||||
"link",
|
||||
@@ -23,9 +23,12 @@ _ACTIONABLE_ROLES = frozenset({
|
||||
"gridcell",
|
||||
})
|
||||
|
||||
# Roles sin valor semántico para el outline: se omiten si tampoco tienen hijos.
|
||||
# Roles sin valor semántico para el outline: se omiten (sus hijos se elevan).
|
||||
_SKIP_ROLES = frozenset({"none", "presentation", "ignored"})
|
||||
|
||||
# Límite de profundidad: evita RecursionError en árboles AX patológicos.
|
||||
_MAX_DEPTH = 60
|
||||
|
||||
|
||||
def _role_val(node: dict) -> str:
|
||||
"""Extrae el valor de role del nodo CDP."""
|
||||
@@ -43,37 +46,63 @@ def _name_val(node: dict) -> str:
|
||||
return str(n) if n else ""
|
||||
|
||||
|
||||
def _value_val(node: dict) -> str:
|
||||
"""Estado actual del nodo: texto escrito en un input, valor de un slider o
|
||||
combobox, etc. El LLM necesita saber qué hay ya en el campo."""
|
||||
v = node.get("value", {})
|
||||
if isinstance(v, dict):
|
||||
val = v.get("value", "")
|
||||
return str(val) if val not in (None, "") else ""
|
||||
return str(v) if v else ""
|
||||
|
||||
|
||||
def _ref_id(node: dict) -> str:
|
||||
"""Ref ESTABLE para acciones por referencia.
|
||||
|
||||
Usa backendDOMNodeId (apunta al nodo DOM real, estable mientras el nodo viva)
|
||||
en lugar del nodeId del AX tree, que es efímero y cambia en cada
|
||||
Accessibility.getFullAXTree. Esto hace que el #ref que lee el LLM siga siendo
|
||||
válido cuando actúa sobre él un instante después. Fallback al nodeId si el
|
||||
backend no viene poblado.
|
||||
"""
|
||||
bid = node.get("backendDOMNodeId")
|
||||
if bid is not None:
|
||||
return str(bid)
|
||||
return str(node.get("nodeId", ""))
|
||||
|
||||
|
||||
def render_ax_outline(nodes: list[dict], max_chars: int = 0) -> str:
|
||||
"""Convierte nodos AX tree CDP en un outline indentado legible y accionable.
|
||||
|
||||
Reconstruye la jerarquía padre→hijo usando childIds/parentId y genera
|
||||
una representación de texto con indentación de 2 espacios por nivel.
|
||||
Los nodos accionables llevan un marcador #ref=nodeId para que el LLM
|
||||
pueda referenciarlos en acciones posteriores.
|
||||
Reconstruye la jerarquía padre→hijo usando childIds/parentId y genera una
|
||||
representación de texto con indentación de 2 espacios por nivel. Los nodos
|
||||
accionables llevan un marcador #ref=<backendDOMNodeId> para que el LLM pueda
|
||||
referenciarlos en acciones posteriores (click_ref/type_ref/hover_ref). El
|
||||
estado actual de inputs (texto escrito, valor) se muestra como `= 'valor'`.
|
||||
|
||||
Args:
|
||||
nodes: Lista de AXNode en formato CDP (campos: nodeId, role, name,
|
||||
childIds, parentId, ignored). Formato devuelto por
|
||||
cdp_get_ax_tree. Se puede pasar trim_ax_tree(nodes) antes
|
||||
para reducir ruido.
|
||||
max_chars: Si > 0, trunca la salida a ese número de caracteres y
|
||||
añade una línea final '…[outline truncado]'. 0 = sin límite.
|
||||
nodes: Lista de AXNode en formato CDP (campos: nodeId, backendDOMNodeId,
|
||||
role, name, value, childIds, parentId, ignored). Formato devuelto
|
||||
por cdp_get_ax_tree. Se puede pasar trim_ax_tree(nodes) antes para
|
||||
reducir ruido (conserva backendDOMNodeId y value.value).
|
||||
max_chars: Si > 0, trunca la salida a ese número de caracteres y añade una
|
||||
línea final '…[outline truncado]'. 0 = sin límite.
|
||||
|
||||
Returns:
|
||||
String multi-línea con el outline indentado. Vacío si nodes es vacío
|
||||
o todos los nodos son ignorados/sin role útil.
|
||||
String multi-línea con el outline indentado. Vacío si nodes es vacío o
|
||||
todos los nodos son ignorados/sin role útil.
|
||||
"""
|
||||
if not nodes:
|
||||
return ""
|
||||
|
||||
# Construir lookup por nodeId
|
||||
# Lookup por nodeId (la jerarquía childIds usa nodeId, no backendDOMNodeId).
|
||||
by_id: dict[str, dict] = {}
|
||||
for node in nodes:
|
||||
nid = node.get("nodeId")
|
||||
if nid:
|
||||
by_id[nid] = node
|
||||
|
||||
# Detectar nodos raíz: nodeId no aparece como childId de nadie visible
|
||||
# Detectar nodos raíz: nodeId que no aparece como childId de nadie visible.
|
||||
all_child_ids: set[str] = set()
|
||||
for node in nodes:
|
||||
for cid in node.get("childIds", []):
|
||||
@@ -81,19 +110,25 @@ def render_ax_outline(nodes: list[dict], max_chars: int = 0) -> str:
|
||||
|
||||
roots = [n for n in nodes if n.get("nodeId") not in all_child_ids]
|
||||
if not roots:
|
||||
# Fallback: usar el primer nodo
|
||||
roots = [nodes[0]]
|
||||
|
||||
lines: list[str] = []
|
||||
visited: set[str] = set() # guard de ciclo: un nodeId no se renderiza dos veces
|
||||
|
||||
def _render_node(node: dict, depth: int) -> None:
|
||||
"""Renderiza un nodo y sus hijos recursivamente."""
|
||||
nid = node.get("nodeId")
|
||||
if depth > _MAX_DEPTH or (nid and nid in visited):
|
||||
return
|
||||
if nid:
|
||||
visited.add(nid)
|
||||
|
||||
if node.get("ignored", False):
|
||||
return
|
||||
|
||||
role = _role_val(node)
|
||||
if not role or role in _SKIP_ROLES:
|
||||
# Nodos sin role útil: intentar renderizar hijos directamente
|
||||
# Nodos sin role útil: elevar los hijos al nivel actual.
|
||||
for cid in node.get("childIds", []):
|
||||
child = by_id.get(cid)
|
||||
if child:
|
||||
@@ -101,27 +136,26 @@ def render_ax_outline(nodes: list[dict], max_chars: int = 0) -> str:
|
||||
return
|
||||
|
||||
name = _name_val(node)
|
||||
node_id = node.get("nodeId", "")
|
||||
indent = " " * depth
|
||||
|
||||
# Construir línea base: role ["name"]
|
||||
if name:
|
||||
base = f'{indent}{role} "{name}"'
|
||||
else:
|
||||
base = f"{indent}{role}"
|
||||
|
||||
# Añadir #ref si es accionable
|
||||
if role in _ACTIONABLE_ROLES and node_id:
|
||||
# Alinear #ref a columna 60 para legibilidad, o adjunto si la línea es larga
|
||||
ref_tag = f"#ref={node_id}"
|
||||
if len(base) < 58:
|
||||
base = base.ljust(60) + ref_tag
|
||||
else:
|
||||
base = base + " " + ref_tag
|
||||
# Estado actual del campo (texto escrito, valor del slider/combobox).
|
||||
value = _value_val(node)
|
||||
if value:
|
||||
base += f" = {value!r}"
|
||||
|
||||
# Ref accionable, sin padding (el relleno con espacios gastaba tokens).
|
||||
if role in _ACTIONABLE_ROLES:
|
||||
ref = _ref_id(node)
|
||||
if ref:
|
||||
base += f" #ref={ref}"
|
||||
|
||||
lines.append(base)
|
||||
|
||||
# Renderizar hijos en orden
|
||||
for cid in node.get("childIds", []):
|
||||
child = by_id.get(cid)
|
||||
if child:
|
||||
|
||||
Reference in New Issue
Block a user