feat(browser): auto-commit con 44 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
---
|
||||
id: cdp_activate_tab_go_browser
|
||||
name: cdp_activate_tab
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Pone una pestaña Chrome en foreground (foco) por su ID via GET /json/activate/<id>. Sin WebSocket — solo HTTP. Útil para traer al frente una pestaña específica antes de capturar pantalla o interactuar con ella."
|
||||
tags: [cdp, browser, tabs, navegator]
|
||||
signature: "func CdpActivateTab(host string, port int, tabID string) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_list_tabs.go"
|
||||
example: |
|
||||
tabs, _ := browser.CdpListTabs("localhost", 9222)
|
||||
// Activar la primera pestaña cuyo título contenga "Dashboard"
|
||||
for _, t := range tabs {
|
||||
if strings.Contains(t.Title, "Dashboard") {
|
||||
_ = browser.CdpActivateTab("localhost", 9222, t.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
params:
|
||||
- name: host
|
||||
desc: "Hostname de la instancia Chrome (vacío = localhost)"
|
||||
- name: port
|
||||
desc: "Puerto CDP de remote debugging (habitualmente 9222)"
|
||||
- name: tabID
|
||||
desc: "ID de la pestaña a activar, obtenido de CdpTab.ID via CdpListTabs"
|
||||
output: "nil si la pestaña pasó a foreground correctamente; error si tabID está vacío, la conexión falla o Chrome devuelve status != 200"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Listar tabs y traer al frente la que corresponda a una URL concreta
|
||||
tabs, err := browser.CdpListTabs("localhost", 9222)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, t := range tabs {
|
||||
if t.URL == "https://metabase.local/dashboard/1" {
|
||||
if err := browser.CdpActivateTab("localhost", 9222, t.ID); err != nil {
|
||||
log.Printf("error activando tab %s: %v", t.ID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de hacer un screenshot o interactuar via CDP con una pestaña concreta que podría estar en segundo plano. También útil en dashboards que muestran el inventario de pestañas y necesitan enfocar una al hacer clic.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- No requiere conexión WebSocket previa; usa HTTP puro contra `/json/activate/<id>`.
|
||||
- Solo cambia el foco dentro del contexto CDP; si la ventana de Chrome está minimizada a nivel de OS, `activate` la pone como pestaña activa dentro de Chrome pero no restaura la ventana.
|
||||
- El ID debe obtenerse de `CdpListTabs` — no es el índice visible del tab, es el UUID interno de Chrome.
|
||||
- Si el tabID no existe, Chrome devuelve un status HTTP distinto de 200 y la función retorna error.
|
||||
@@ -0,0 +1,12 @@
|
||||
package browser
|
||||
|
||||
// CdpClearCookies borra TODAS las cookies del browser via Network.clearBrowserCookies.
|
||||
// Equivalente a "Borrar datos de navegacion > Cookies" en Chrome.
|
||||
// Cierra todas las sesiones activas — usar solo en tests o resets completos.
|
||||
func CdpClearCookies(c *CDPConn) error {
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.sendCDP("Network.clearBrowserCookies", nil)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: cdp_clear_cookies_go_browser
|
||||
name: cdp_clear_cookies
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Borra TODAS las cookies del browser via Network.clearBrowserCookies; equivalente a 'Borrar datos de navegacion > Cookies' en Chrome."
|
||||
tags: [cdp, browser, cookie, network, navegator]
|
||||
signature: "func CdpClearCookies(c *CDPConn) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_clear_cookies.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect(9222)
|
||||
if err := CdpClearCookies(conn); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// browser ahora sin cookies — todas las sesiones cerradas
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa al browser (obtenida con CdpConnect)"
|
||||
output: "nil si se borraron todas las cookies; error si falla la comunicacion CDP."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := CdpConnect(9222)
|
||||
|
||||
// Reset completo antes de un test de login
|
||||
if err := CdpClearCookies(conn); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// A partir de aqui el browser no tiene sesion en ningun dominio
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar al inicio de un test e2e que necesita partir de un browser sin sesion previa, o cuando quieres resetear completamente el estado de autenticacion del browser en un entorno de CI.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Destructivo e irreversible: cierra TODAS las sesiones activas en todos los dominios del browser.
|
||||
- Llama `Network.enable` internamente antes del clear; es idempotente.
|
||||
- No afecta a LocalStorage ni SessionStorage — solo cookies.
|
||||
- Para borrar solo una cookie especifica usar `CdpDeleteCookies` en su lugar.
|
||||
- En un browser de perfil de usuario real (no headless de test) puede cerrar sesiones de trabajo activas.
|
||||
@@ -14,11 +14,19 @@ func CdpClick(c *CDPConn, selector string) error {
|
||||
return fmt.Errorf("cdp click: conexion nula")
|
||||
}
|
||||
|
||||
// Obtener coordenadas del centro del elemento
|
||||
// Obtener coordenadas del centro del elemento, tras hacer scroll para que sea
|
||||
// visible. Verificamos visibilidad: un elemento existente pero oculto
|
||||
// (display:none, visibility:hidden, opacity 0 o tamaño 0) daria un rect en
|
||||
// (0,0) y clicariamos en la esquina sin efecto — devolvemos error en su lugar.
|
||||
js := fmt.Sprintf(`(function() {
|
||||
var el = document.querySelector(%q);
|
||||
if (!el) return null;
|
||||
el.scrollIntoView({block:'center'});
|
||||
var r = el.getBoundingClientRect();
|
||||
var s = window.getComputedStyle(el);
|
||||
var visible = r.width > 0 && r.height > 0 &&
|
||||
s.visibility !== 'hidden' && s.display !== 'none' && s.opacity !== '0';
|
||||
if (!visible) return '__HIDDEN__';
|
||||
return JSON.stringify({x: r.left + r.width/2, y: r.top + r.height/2});
|
||||
})()`, selector)
|
||||
|
||||
@@ -29,6 +37,9 @@ func CdpClick(c *CDPConn, selector string) error {
|
||||
if coordStr == "" || coordStr == "null" {
|
||||
return fmt.Errorf("cdp click: elemento %q no encontrado en el DOM", selector)
|
||||
}
|
||||
if strings.Contains(coordStr, "__HIDDEN__") {
|
||||
return fmt.Errorf("cdp click: elemento %q existe pero no es visible/clickable (display:none, oculto, opacity 0 o tamaño 0)", selector)
|
||||
}
|
||||
|
||||
// Parsear "{x:...,y:...}" — CdpEvaluate ya retorna el JSON como string
|
||||
coordStr = strings.Trim(coordStr, `"`)
|
||||
@@ -37,13 +48,6 @@ func CdpClick(c *CDPConn, selector string) error {
|
||||
return fmt.Errorf("cdp click: parsear coordenadas %q: %w", coordStr, err)
|
||||
}
|
||||
|
||||
// Hacer scroll al elemento para que este visible
|
||||
scrollJS := fmt.Sprintf(`document.querySelector(%q).scrollIntoView({block:'center'})`, selector)
|
||||
if _, err := CdpEvaluate(c, scrollJS); err != nil {
|
||||
// No es fatal si el scroll falla
|
||||
_ = err
|
||||
}
|
||||
|
||||
// Despachar mousedown
|
||||
mouseParams := map[string]any{
|
||||
"type": "mousePressed",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: cdp_close_tab_go_browser
|
||||
name: cdp_close_tab
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Cierra una pestaña Chrome por su ID via GET /json/close/<id>. Sin WebSocket — solo HTTP. Util para limpiar pestañas abiertas por automatizaciones."
|
||||
tags: [cdp, browser, tabs, navegator]
|
||||
signature: "func CdpCloseTab(host string, port int, tabID string) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_list_tabs.go"
|
||||
example: |
|
||||
tabs, _ := browser.CdpListTabs("localhost", 9222)
|
||||
for _, t := range tabs {
|
||||
if t.URL == "https://example.com" {
|
||||
_ = browser.CdpCloseTab("localhost", 9222, t.ID)
|
||||
}
|
||||
}
|
||||
params:
|
||||
- name: host
|
||||
desc: "Hostname de la instancia Chrome (vacío = localhost)"
|
||||
- name: port
|
||||
desc: "Puerto CDP de remote debugging (habitualmente 9222)"
|
||||
- name: tabID
|
||||
desc: "ID de la pestaña a cerrar, obtenido de CdpTab.ID via CdpListTabs"
|
||||
output: "nil si la pestaña se cerró correctamente; error si tabID está vacío, la conexión falla o Chrome devuelve status != 200"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Listar tabs y cerrar la primera que coincida con una URL
|
||||
tabs, err := browser.CdpListTabs("localhost", 9222)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, t := range tabs {
|
||||
if t.URL == "https://example.com/login" {
|
||||
if err := browser.CdpCloseTab("localhost", 9222, t.ID); err != nil {
|
||||
log.Printf("error cerrando tab %s: %v", t.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Después de terminar una sesión de scraping o automatización: cierra las pestañas abiertas programáticamente sin afectar el resto del perfil. También útil para liberar recursos cuando `CdpNewTab` ha creado muchas pestañas temporales.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- No requiere conexión WebSocket previa; usa HTTP puro contra `/json/close/<id>`.
|
||||
- Si Chrome ya cerró la pestaña (o el ID es inválido), devuelve error de status HTTP.
|
||||
- El ID debe obtenerse de `CdpListTabs` — no es el índice visible del tab, es el UUID interno de Chrome.
|
||||
- No espera confirmación de cierre; para saber si la pestaña desapareció, volver a llamar `CdpListTabs`.
|
||||
@@ -67,18 +67,9 @@ func CdpConnect(port int) (*CDPConn, error) {
|
||||
return CdpConnectHost("localhost", port)
|
||||
}
|
||||
|
||||
// CdpConnectHost es como CdpConnect pero permite especificar el host.
|
||||
// Util para WSL2 donde Chrome Windows escucha en una IP distinta a localhost.
|
||||
func CdpConnectHost(host string, port int) (*CDPConn, error) {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
wsURL, err := cdpGetPageWSURL(host, port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdp connect: %w", err)
|
||||
}
|
||||
|
||||
// Parsear la URL del WebSocket para extraer host y path
|
||||
// cdpConnectWS abre la conexion CDP a partir de un webSocketDebuggerUrl ya resuelto.
|
||||
// Es el helper compartido por CdpConnectHost y CdpConnectTarget para evitar duplicacion.
|
||||
func cdpConnectWS(wsURL string, port int) (*CDPConn, error) {
|
||||
u, err := url.Parse(wsURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdp connect: parsear ws url %q: %w", wsURL, err)
|
||||
@@ -96,8 +87,7 @@ func CdpConnectHost(host string, port int) (*CDPConn, error) {
|
||||
}
|
||||
|
||||
// Realizar handshake WebSocket
|
||||
path := u.RequestURI()
|
||||
reader, err := wsHandshake(tcpConn, wsHost, path)
|
||||
reader, err := wsHandshake(tcpConn, wsHost, u.RequestURI())
|
||||
if err != nil {
|
||||
tcpConn.Close()
|
||||
return nil, fmt.Errorf("cdp connect: ws handshake: %w", err)
|
||||
@@ -115,3 +105,16 @@ func CdpConnectHost(host string, port int) (*CDPConn, error) {
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// CdpConnectHost es como CdpConnect pero permite especificar el host.
|
||||
// Util para WSL2 donde Chrome Windows escucha en una IP distinta a localhost.
|
||||
func CdpConnectHost(host string, port int) (*CDPConn, error) {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
wsURL, err := cdpGetPageWSURL(host, port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdp connect: %w", err)
|
||||
}
|
||||
return cdpConnectWS(wsURL, port)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CdpConnectTarget se conecta a un target CDP DETERMINISTA identificado por match.
|
||||
//
|
||||
// Si host es "" se usa "localhost".
|
||||
// match puede ser:
|
||||
// - "" → primer target con Type "page" y WebSocketDebuggerURL no vacío (misma
|
||||
// semántica que CdpConnectHost, útil como fallback compatible).
|
||||
// - ID exacto del target (campo "id" en /json).
|
||||
// - Substring case-insensitive de la URL del target.
|
||||
//
|
||||
// Retorna error si ningún target type=page satisface el match.
|
||||
func CdpConnectTarget(host string, port int, match string) (*CDPConn, error) {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s:%d/json", host, port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdp connect target: listar targets: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var targets []cdpTarget
|
||||
if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil {
|
||||
return nil, fmt.Errorf("cdp connect target: decode targets: %w", err)
|
||||
}
|
||||
|
||||
matchLower := strings.ToLower(match)
|
||||
|
||||
for _, t := range targets {
|
||||
if t.Type != "page" || t.WebSocketDebuggerURL == "" {
|
||||
continue
|
||||
}
|
||||
if match == "" {
|
||||
// Sin filtro: primera tab page disponible.
|
||||
return cdpConnectWS(t.WebSocketDebuggerURL, port)
|
||||
}
|
||||
// Coincidencia por ID exacto o substring de URL (case-insensitive).
|
||||
if t.ID == match || strings.Contains(strings.ToLower(t.URL), matchLower) {
|
||||
return cdpConnectWS(t.WebSocketDebuggerURL, port)
|
||||
}
|
||||
}
|
||||
|
||||
if match == "" {
|
||||
return nil, fmt.Errorf("cdp connect target: no hay ninguna tab 'page' disponible en %s:%d", host, port)
|
||||
}
|
||||
return nil, fmt.Errorf("cdp connect target: no hay tab 'page' que matchee %q en %s:%d", match, host, port)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: cdp_connect_target
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CdpConnectTarget(host string, port int, match string) (*CDPConn, error)"
|
||||
description: "Conecta por CDP a un target DETERMINISTA elegido por ID exacto o substring de URL, evitando engancharse a una pestaña al azar con el CDP global en 9222."
|
||||
tags: [cdp, browser, connection, security, navegator]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: host
|
||||
desc: "Host donde escucha el CDP. Vacío usa 'localhost'. Útil en WSL2 para apuntar a la IP de Windows."
|
||||
- name: port
|
||||
desc: "Puerto CDP del navegador (habitualmente 9222)."
|
||||
- name: match
|
||||
desc: "Filtro de target: vacío = primera tab page (compat con CdpConnectHost); ID exacto del target; o substring case-insensitive de la URL de la pestaña."
|
||||
output: "*CDPConn listo para enviar comandos CDP al target elegido. Error si ninguna tab 'page' satisface el match."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/browser/cdp_connect_target.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Fijar la pestaña de GitHub para que el agente no toque otras abiertas
|
||||
conn, err := browser.CdpConnectTarget("", 9222, "github.com")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Por ID exacto de target (obtenido de GET http://localhost:9222/json)
|
||||
conn2, err := browser.CdpConnectTarget("", 9222, "ABCD1234-target-id")
|
||||
|
||||
// Compatibilidad: sin filtro = primera tab page (igual que CdpConnect)
|
||||
conn3, err := browser.CdpConnectTarget("", 9222, "")
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un agente debe atarse a UNA pestaña concreta (por URL) y NO a la primera al azar — crítico con CDP global en 9222 para no operar sobre pestañas ajenas (banca, correo, sesiones activas). Usar en lugar de `CdpConnect`/`CdpConnectHost` siempre que el contexto del agente sea "esta URL concreta" y no "cualquier tab disponible".
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Si hay varias tabs cuya URL contiene el substring dado, se elige la **primera** que aparezca en `/json` (orden interno del navegador). Para mayor precisión, usar el ID exacto del target.
|
||||
- El match de URL es substring **case-insensitive**; `"github"` matchea `"https://github.com/usuario/repo"`.
|
||||
- Con CDP global en 9222 y muchas pestañas abiertas, un `match=""` sigue siendo tan arriesgado como `CdpConnect`. Especificar siempre el match en producción.
|
||||
- La forma más segura para agentes automatizados es lanzar un perfil Chromium dedicado con `--user-data-dir` aislado y `--remote-debugging-port` propio, de modo que `/json` solo exponga las pestañas del agente.
|
||||
- `WebSocketDebuggerURL` puede cambiar entre reinicios del navegador; recalcular en cada sesión, no cachear entre ejecuciones.
|
||||
@@ -0,0 +1,15 @@
|
||||
package browser
|
||||
|
||||
// CdpDeleteCookies borra las cookies que coincidan con name (y opcionalmente domain)
|
||||
// via Network.deleteCookies. Si domain es "" se borran todas las cookies con ese
|
||||
// nombre en cualquier dominio.
|
||||
func CdpDeleteCookies(c *CDPConn, name, domain string) error {
|
||||
params := map[string]any{
|
||||
"name": name,
|
||||
}
|
||||
if domain != "" {
|
||||
params["domain"] = domain
|
||||
}
|
||||
_, err := c.sendCDP("Network.deleteCookies", params)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
id: cdp_delete_cookies_go_browser
|
||||
name: cdp_delete_cookies
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Borra las cookies que coincidan con name (+ domain opcional) via Network.deleteCookies; si domain es vacío elimina en todos los dominios."
|
||||
tags: [cdp, browser, cookie, network, navegator]
|
||||
signature: "func CdpDeleteCookies(c *CDPConn, name, domain string) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_delete_cookies.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect(9222)
|
||||
// Borrar cookie de sesion solo en el dominio concreto
|
||||
err := CdpDeleteCookies(conn, "session_id", "app.example.com")
|
||||
// Borrar en todos los dominios (sin filtro de dominio)
|
||||
err = CdpDeleteCookies(conn, "tracking_cookie", "")
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa al browser (obtenida con CdpConnect)"
|
||||
- name: name
|
||||
desc: "Nombre exacto de la cookie a borrar; obligatorio para Network.deleteCookies"
|
||||
- name: domain
|
||||
desc: "Dominio donde borrar la cookie; cadena vacía borra en todos los dominios que tengan esa cookie"
|
||||
output: "nil si la cookie fue borrada (o no existia); error si falla la comunicacion CDP."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := CdpConnect(9222)
|
||||
|
||||
// Borrar cookie de sesion solo en dominio especifico
|
||||
if err := CdpDeleteCookies(conn, "session_id", "app.example.com"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Borrar cookie en todos los dominios
|
||||
if err := CdpDeleteCookies(conn, "analytics_token", ""); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar cuando necesitas forzar un logout de sesion especifica, limpiar una cookie de tracking antes de un test, o resetear el estado de autenticacion de un dominio concreto sin tocar el resto de cookies.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `name` es obligatorio en `Network.deleteCookies`; CDP devuelve error si se omite.
|
||||
- Sin `domain`, CDP borra la cookie en TODOS los dominios que tengan esa cookie — puede cerrar sesiones inesperadas en otros dominios abiertos.
|
||||
- No devuelve error si la cookie no existia; la operacion es idempotente.
|
||||
- Para borrar todas las cookies de golpe usar `CdpClearCookies` en su lugar.
|
||||
@@ -0,0 +1,83 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CdpEvalInFrame ejecuta una expresion JavaScript en el contexto aislado de un iframe
|
||||
// especifico usando Page.createIsolatedWorld + Runtime.evaluate con el contextId del frame.
|
||||
// Retorna el resultado serializado como string.
|
||||
func CdpEvalInFrame(c *CDPConn, frameID, expression string) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: conexion nula")
|
||||
}
|
||||
if frameID == "" {
|
||||
return "", fmt.Errorf("cdp eval in frame: frameID vacio")
|
||||
}
|
||||
|
||||
// Page.enable es idempotente; necesario antes de crear mundos aislados
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: Page.enable: %w", err)
|
||||
}
|
||||
|
||||
// Crear un mundo aislado en el frame indicado para no contaminar su contexto JS
|
||||
ctxRes, err := c.sendCDP("Page.createIsolatedWorld", map[string]any{
|
||||
"frameId": frameID,
|
||||
"worldName": "fn_registry_isolated",
|
||||
"grantUniveralAccess": false,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: createIsolatedWorld: %w", err)
|
||||
}
|
||||
|
||||
ctxIDRaw, ok := ctxRes["executionContextId"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cdp eval in frame: executionContextId no encontrado en respuesta")
|
||||
}
|
||||
ctxID, ok := ctxIDRaw.(float64)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cdp eval in frame: executionContextId tipo inesperado: %T", ctxIDRaw)
|
||||
}
|
||||
|
||||
// Evaluar la expresion en el contexto aislado del frame
|
||||
evRes, err := c.sendCDP("Runtime.evaluate", map[string]any{
|
||||
"expression": expression,
|
||||
"contextId": int(ctxID),
|
||||
"returnByValue": true,
|
||||
"awaitPromise": true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: Runtime.evaluate: %w", err)
|
||||
}
|
||||
|
||||
// Verificar excepcion JS
|
||||
if exc, ok := evRes["exceptionDetails"]; ok && exc != nil {
|
||||
excMap, _ := exc.(map[string]any)
|
||||
text, _ := excMap["text"].(string)
|
||||
return "", fmt.Errorf("cdp eval in frame: excepcion JS en frame %q: %s", frameID, text)
|
||||
}
|
||||
|
||||
// Extraer valor del resultado (mismo patron que CdpEvaluate)
|
||||
resVal, ok := evRes["result"].(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cdp eval in frame: resultado inesperado: %v", evRes)
|
||||
}
|
||||
|
||||
value, ok := resVal["value"]
|
||||
if !ok {
|
||||
// undefined u otro tipo no serializable
|
||||
typ, _ := resVal["type"].(string)
|
||||
return typ, nil
|
||||
}
|
||||
|
||||
// Strings tal cual; objetos/arrays JS a JSON real (no la repr de Go de "%v").
|
||||
if s, ok := value.(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", value), nil
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: cdp_eval_in_frame_go_browser
|
||||
name: cdp_eval_in_frame
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Ejecuta una expresión JavaScript en el contexto aislado de un iframe concreto usando Page.createIsolatedWorld + Runtime.evaluate con el contextId del frame."
|
||||
tags: [cdp, browser, iframe, javascript, eval, navegator]
|
||||
signature: "func CdpEvalInFrame(c *CDPConn, frameID string, expression string) (string, error)"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_eval_in_frame.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect("localhost", 9222, "")
|
||||
frames, _ := CdpListFrames(conn)
|
||||
// Tomar el primer iframe (índice 1, el 0 es el frame raíz)
|
||||
result, err := CdpEvalInFrame(conn, frames[1].ID, "document.title")
|
||||
fmt.Println(result) // "Título del iframe"
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa obtenida con CdpConnect."
|
||||
- name: frameID
|
||||
desc: "ID del frame donde ejecutar el JS; obtenido de CdpListFrames (campo CdpFrame.ID)."
|
||||
- name: expression
|
||||
desc: "Expresión JavaScript a evaluar en el contexto del frame; puede ser una expresión simple o una Promise."
|
||||
output: "Resultado de la expresión serializado como string (fmt.Sprintf del valor CDP); error si la conexión es nula, el frameID está vacío, la comunicación CDP falla o la expresión lanza una excepción JS."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := CdpConnect("localhost", 9222, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
frames, err := CdpListFrames(conn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// frames[0] es el frame raíz; frames[1] sería el primer iframe
|
||||
iframeID := frames[1].ID
|
||||
title, err := CdpEvalInFrame(conn, iframeID, "document.title")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Título del iframe:", title)
|
||||
|
||||
// Leer un elemento del DOM del iframe
|
||||
text, _ := CdpEvalInFrame(conn, iframeID, "document.querySelector('h1').innerText")
|
||||
fmt.Println("H1 del iframe:", text)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites leer o manipular el DOM de un iframe específico sin afectar el contexto JS de la página principal. Útil para extraer datos de iframes de terceros, formularios embebidos o widgets. Obtén el `frameID` con `CdpListFrames` antes de llamar a esta función.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El mundo aislado (`fn_registry_isolated`) puede leer el DOM del iframe pero NO accede a variables JS definidas en el page-world del iframe (ej. `window.miVariable`). Para acceder a variables JS del frame, evalúa sin `createIsolatedWorld` usando el `contextId` principal del frame (no expuesto por esta función).
|
||||
- Requiere `Page.enable` (se llama internamente, idempotente).
|
||||
- Si el iframe tiene `sandbox` attribute sin `allow-scripts`, el CDP puede crear el mundo aislado pero las evaluaciones fallarán con excepción de seguridad.
|
||||
- Cross-origin iframes en Chrome permiten evaluación CDP siempre que la conexión tenga acceso al target; no aplican las restricciones CORS de JS normal.
|
||||
- El `frameID` debe obtenerse con `CdpListFrames`; si se pasa un ID obsoleto (frame recargado o destruido), `createIsolatedWorld` retorna error.
|
||||
@@ -1,6 +1,7 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -44,5 +45,16 @@ func CdpEvaluate(c *CDPConn, expression string) (string, error) {
|
||||
return typ, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", value), nil
|
||||
// Strings se devuelven tal cual (sin comillas). Objetos y arrays JS, que Chrome
|
||||
// deserializa a map/slice cuando returnByValue=true, se serializan a JSON real
|
||||
// en vez de la repr de Go de fmt.Sprintf("%v") (que produciria "map[a:1]" en lugar
|
||||
// de {"a":1}). Asi el caller puede parsear datos estructurados.
|
||||
if s, ok := value.(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", value), nil
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ type FindByTextOpts struct {
|
||||
// - "#<id>" si el elemento tiene id.
|
||||
// - path "tag:nth-of-type(n) > tag:nth-of-type(n) > ..." si no.
|
||||
//
|
||||
// Retorna ("", nil) si no encuentra nada (no es error). Error solo si la
|
||||
// evaluacion JS rompe (conexion CDP caida).
|
||||
// Retorna error si no encuentra ningun elemento con ese texto. Antes devolvia
|
||||
// ("", nil) en silencio, lo que hacia que el caller creyera que habia encontrado
|
||||
// algo y operara sobre un selector vacio. Tambien error si la evaluacion JS rompe
|
||||
// (conexion CDP caida).
|
||||
func CdpFindByText(c *CDPConn, text string, opts FindByTextOpts) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("cdp find by text: conexion nula")
|
||||
@@ -96,7 +98,7 @@ func CdpFindByText(c *CDPConn, text string, opts FindByTextOpts) (string, error)
|
||||
// CdpEvaluate retorna el valor stringificado. Para "" devuelve cadena vacia.
|
||||
res = strings.TrimSpace(res)
|
||||
if res == "" || res == "<nil>" {
|
||||
return "", nil
|
||||
return "", fmt.Errorf("cdp find by text: no se encontro elemento con texto %q", text)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package browser
|
||||
|
||||
// CdpCookie representa una cookie del browser tal como la devuelve CDP.
|
||||
type CdpCookie struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Domain string `json:"domain"`
|
||||
Path string `json:"path"`
|
||||
Expires float64 `json:"expires"`
|
||||
HTTPOnly bool `json:"httpOnly"`
|
||||
Secure bool `json:"secure"`
|
||||
SameSite string `json:"sameSite"`
|
||||
}
|
||||
|
||||
// cookieFromMap convierte un map[string]any CDP a CdpCookie con casts defensivos.
|
||||
func cookieFromMap(m map[string]any) CdpCookie {
|
||||
c := CdpCookie{}
|
||||
if v, ok := m["name"].(string); ok {
|
||||
c.Name = v
|
||||
}
|
||||
if v, ok := m["value"].(string); ok {
|
||||
c.Value = v
|
||||
}
|
||||
if v, ok := m["domain"].(string); ok {
|
||||
c.Domain = v
|
||||
}
|
||||
if v, ok := m["path"].(string); ok {
|
||||
c.Path = v
|
||||
}
|
||||
if v, ok := m["expires"].(float64); ok {
|
||||
c.Expires = v
|
||||
}
|
||||
if v, ok := m["httpOnly"].(bool); ok {
|
||||
c.HTTPOnly = v
|
||||
}
|
||||
if v, ok := m["secure"].(bool); ok {
|
||||
c.Secure = v
|
||||
}
|
||||
if v, ok := m["sameSite"].(string); ok {
|
||||
c.SameSite = v
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// CdpGetCookies devuelve todas las cookies del browser via Network.getAllCookies.
|
||||
// El caller puede filtrar por dominio, nombre, etc. sobre el slice retornado.
|
||||
func CdpGetCookies(c *CDPConn) ([]CdpCookie, error) {
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := c.sendCDP("Network.getAllCookies", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, _ := result["cookies"].([]any)
|
||||
cookies := make([]CdpCookie, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
cookies = append(cookies, cookieFromMap(m))
|
||||
}
|
||||
}
|
||||
return cookies, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: cdp_get_cookies_go_browser
|
||||
name: cdp_get_cookies
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Devuelve todas las cookies del browser via Network.getAllCookies; el caller filtra por dominio o nombre sobre el slice []CdpCookie."
|
||||
tags: [cdp, browser, cookie, network, navegator]
|
||||
signature: "func CdpGetCookies(c *CDPConn) ([]CdpCookie, error)"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_get_cookies.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect(9222)
|
||||
cookies, err := CdpGetCookies(conn)
|
||||
if err != nil { log.Fatal(err) }
|
||||
for _, ck := range cookies {
|
||||
if ck.Domain == "app.example.com" {
|
||||
fmt.Printf("name=%s value=%s httpOnly=%v\n", ck.Name, ck.Value, ck.HTTPOnly)
|
||||
}
|
||||
}
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa al browser (obtenida con CdpConnect)"
|
||||
output: "Slice de CdpCookie con todas las cookies del browser; error si falla la comunicacion CDP."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := CdpConnect(9222)
|
||||
cookies, err := CdpGetCookies(conn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, ck := range cookies {
|
||||
if ck.Domain == "app.example.com" {
|
||||
fmt.Printf("name=%s value=%s httpOnly=%v\n", ck.Name, ck.Value, ck.HTTPOnly)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar cuando necesitas inspeccionar el estado de cookies del browser tras un login CDP, antes de propagarlas a otro contexto, o para auditar sesiones activas en tests e2e.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Llama `Network.enable` internamente antes de `getAllCookies`; es idempotente pero suma latencia en la primera llamada.
|
||||
- `Network.getAllCookies` devuelve cookies de TODOS los dominios del browser, no solo la tab activa. Filtrar por `Domain` en el caller.
|
||||
- Las cookies HttpOnly son visibles via CDP aunque no lo sean desde JavaScript del browser.
|
||||
- `Expires == -1` indica cookie de sesion (sin fecha de expiración).
|
||||
@@ -0,0 +1,23 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CdpGetFrameHTML retorna el HTML completo (outerHTML del documentElement) de un iframe
|
||||
// especifico usando CdpEvalInFrame con la expresion "document.documentElement.outerHTML".
|
||||
func CdpGetFrameHTML(c *CDPConn, frameID string) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("cdp get frame html: conexion nula")
|
||||
}
|
||||
if frameID == "" {
|
||||
return "", fmt.Errorf("cdp get frame html: frameID vacio")
|
||||
}
|
||||
|
||||
html, err := CdpEvalInFrame(c, frameID, "document.documentElement.outerHTML")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp get frame html: %w", err)
|
||||
}
|
||||
|
||||
return html, nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: cdp_get_frame_html_go_browser
|
||||
name: cdp_get_frame_html
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Devuelve el HTML completo (document.documentElement.outerHTML) de un iframe concreto componiendo sobre CdpEvalInFrame con un mundo aislado CDP."
|
||||
tags: [cdp, browser, iframe, html, scraping, navegator]
|
||||
signature: "func CdpGetFrameHTML(c *CDPConn, frameID string) (string, error)"
|
||||
uses_functions: [cdp_eval_in_frame_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_get_frame_html.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect("localhost", 9222, "")
|
||||
frames, _ := CdpListFrames(conn)
|
||||
html, err := CdpGetFrameHTML(conn, frames[1].ID)
|
||||
fmt.Println(html[:200]) // primeros 200 chars del HTML del iframe
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa obtenida con CdpConnect."
|
||||
- name: frameID
|
||||
desc: "ID del frame cuyo HTML se quiere obtener; obtenido de CdpListFrames (campo CdpFrame.ID)."
|
||||
output: "String con el HTML completo del iframe (outerHTML del documentElement); error si la conexión es nula, el frameID está vacío o la evaluación CDP falla."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := CdpConnect("localhost", 9222, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 1. Listar frames para obtener el ID del iframe deseado
|
||||
frames, err := CdpListFrames(conn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// frames[0] = frame raíz, frames[1] = primer iframe
|
||||
for _, f := range frames {
|
||||
if f.ParentID != "" { // es un iframe, no el raíz
|
||||
html, err := CdpGetFrameHTML(conn, f.ID)
|
||||
if err != nil {
|
||||
log.Printf("error en frame %s: %v", f.ID, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("=== iframe %s (%s) ===\n%s\n", f.ID, f.URL, html[:min(500, len(html))])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites el HTML completo de un iframe para parsearlo, scrapearlo o inspeccionarlo. Flujo típico: `CdpListFrames` → elegir frame por URL → `CdpGetFrameHTML` → parsear con `golang.org/x/net/html` o regexp.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El mundo aislado ve el DOM pero NO las variables JS del page-world del iframe; suficiente para leer `outerHTML` y hacer scraping estructural.
|
||||
- `frameID` debe obtenerse de `CdpListFrames`; un ID obsoleto (frame recargado) provoca error en `CdpEvalInFrame`.
|
||||
- Para iframes con contenido dinámico (renderizado por JS), espera a que el iframe termine de cargar antes de llamar a esta función; de lo contrario el HTML puede estar incompleto.
|
||||
- En páginas con muchos iframes pesados, el outerHTML puede ser muy grande (MBs); considera evaluar selectores más específicos con `CdpEvalInFrame` si solo necesitas parte del DOM.
|
||||
@@ -0,0 +1,54 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// CdpGetText retorna el texto visible (innerText) de la pagina o de un elemento.
|
||||
// Si selector es "" lee document.body.innerText completo.
|
||||
// Si selector no matchea ningun elemento retorna error.
|
||||
// Si maxBytes > 0 trunca al limite dado (corte rune-safe) y añade sufijo con total original.
|
||||
// Si maxBytes <= 0 no hay limite.
|
||||
func CdpGetText(c *CDPConn, selector string, maxBytes int) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("cdp get text: conexion nula")
|
||||
}
|
||||
|
||||
var expr string
|
||||
if selector == "" {
|
||||
expr = `document.body ? document.body.innerText : ""`
|
||||
} else {
|
||||
// Escapa el selector como string JSON para evitar inyeccion via comillas/backslash.
|
||||
selectorJSON, err := json.Marshal(selector)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp get text: escapar selector: %w", err)
|
||||
}
|
||||
expr = fmt.Sprintf(
|
||||
`(function(){var e=document.querySelector(%s); return e ? e.innerText : "__FN_GET_TEXT_NOTFOUND__";})()`,
|
||||
string(selectorJSON),
|
||||
)
|
||||
}
|
||||
|
||||
text, err := CdpEvaluate(c, expr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp get text: %w", err)
|
||||
}
|
||||
|
||||
if selector != "" && text == "__FN_GET_TEXT_NOTFOUND__" {
|
||||
return "", fmt.Errorf("cdp get text: elemento no encontrado: %s", selector)
|
||||
}
|
||||
|
||||
if maxBytes > 0 && len(text) > maxBytes {
|
||||
total := len(text)
|
||||
// Corte rune-safe: retrocede hasta encontrar un rune valido completo.
|
||||
cut := maxBytes
|
||||
for cut > 0 && !utf8.RuneStart(text[cut]) {
|
||||
cut--
|
||||
}
|
||||
text = text[:cut] + fmt.Sprintf("\n…[truncado, total %d bytes]", total)
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: cdp_get_text
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CdpGetText(c *CDPConn, selector string, maxBytes int) (string, error)"
|
||||
description: "Retorna el texto visible (innerText) de la pagina o de un elemento CSS, con truncado opcional. Alternativa compacta a cdp_get_html cuando solo se necesita el texto legible."
|
||||
tags: [cdp, browser, read, perception, navegator]
|
||||
uses_functions: [cdp_evaluate_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [encoding/json, fmt, unicode/utf8]
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa a una tab de Chrome. Debe estar conectada a una tab tipo 'page'."
|
||||
- name: selector
|
||||
desc: "Selector CSS del elemento del que leer el innerText. Si es cadena vacia, lee document.body.innerText (toda la pagina)."
|
||||
- name: maxBytes
|
||||
desc: "Limite maximo de bytes del texto retornado. Si es <= 0 no hay limite. Si el texto supera el limite, se trunca con corte rune-safe y se añade un sufijo con el total original."
|
||||
output: "Texto visible del elemento o de toda la pagina. Si maxBytes > 0 y el texto supera el limite, retorna el texto truncado con sufijo '…[truncado, total N bytes]'. Error si el selector no matchea ningun elemento o si la conexion falla."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/browser/cdp_get_text.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Leer todo el body con limite de 20000 bytes (apto para LLM)
|
||||
text, err := CdpGetText(conn, "", 20000)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(text)
|
||||
|
||||
// Leer un elemento concreto sin limite
|
||||
price, err := CdpGetText(conn, ".product-price", 0)
|
||||
if err != nil {
|
||||
// err contiene "elemento no encontrado: .product-price" si no existe en el DOM
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(price)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Para que un LLM lea el contenido de una pagina sin reventar su ventana de contexto. Preferir sobre `cdp_get_html` cuando solo necesitas el texto — innerText es 5-50x mas compacto que el HTML crudo. Usar `selector` para acotar a la seccion relevante (articulo, tabla, formulario) y `maxBytes` para garantizar el presupuesto de tokens.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `innerText` solo devuelve el texto de nodos visibles: elementos con `display:none` o `visibility:hidden` quedan excluidos. Si necesitas leer contenido oculto usa `cdp_get_html` y parsea.
|
||||
- El truncado corta en boundary de rune pero puede partir a media frase o a medio parrafo. Si necesitas preservar estructura semantica, ajusta `maxBytes` con margen o usa el selector para acotar la region.
|
||||
- Requiere conexion activa a una tab de tipo `page` (no `background_page`, no `service_worker`). Tabs en estado de carga pueden devolver texto parcial; esperar con `cdp_wait_load` si el contenido es dinamico.
|
||||
- El selector se escapa via `json.Marshal` — caracteres especiales como comillas simples, backslash o comillas dobles en el selector CSS son seguros.
|
||||
@@ -0,0 +1,35 @@
|
||||
package browser
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CdpHandleDialog instala un auto-handler que responde automaticamente a todos
|
||||
// los dialogos JS (alert, confirm, prompt, beforeunload) hasta que se llame
|
||||
// la funcion cancel devuelta. Usa el evento Page.javascriptDialogOpening y
|
||||
// Page.handleJavaScriptDialog del protocolo CDP.
|
||||
//
|
||||
// IMPORTANTE: el handler interno despacha la respuesta en una goroutine nueva
|
||||
// para evitar deadlock — el evento llega en la goroutine de lectura del
|
||||
// WebSocket, y sendCDP bloquea esperando una respuesta que leeria esa misma
|
||||
// goroutine si se llamara de forma sincrona.
|
||||
func CdpHandleDialog(c *CDPConn, accept bool, promptText string) (func(), error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("cdp handle dialog: conexion nula")
|
||||
}
|
||||
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return nil, fmt.Errorf("cdp handle dialog: %w", err)
|
||||
}
|
||||
|
||||
cancel := c.OnEvent("Page.javascriptDialogOpening", func(method string, params map[string]any) {
|
||||
p := map[string]any{"accept": accept}
|
||||
if promptText != "" {
|
||||
p["promptText"] = promptText
|
||||
}
|
||||
// go es OBLIGATORIO: el handler corre en la goroutine de lectura del
|
||||
// WebSocket. Llamar sendCDP aqui directamente provoca deadlock porque
|
||||
// sendCDP espera una respuesta que la misma goroutine deberia leer.
|
||||
go c.sendCDP("Page.handleJavaScriptDialog", p) //nolint:errcheck
|
||||
})
|
||||
|
||||
return cancel, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: cdp_handle_dialog_go_browser
|
||||
name: cdp_handle_dialog
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
description: "Instala un auto-handler que responde automaticamente a dialogos JS (alert/confirm/prompt/beforeunload) via Page.javascriptDialogOpening CDP hasta que se llame el cancel devuelto."
|
||||
tags: [cdp, browser, dialog, input, navegator]
|
||||
signature: "func CdpHandleDialog(c *CDPConn, accept bool, promptText string) (func(), error)"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_handle_dialog.go"
|
||||
example: |
|
||||
// Aceptar automaticamente confirm() antes de navegar
|
||||
cancel, _ := CdpHandleDialog(c, true, "")
|
||||
defer cancel()
|
||||
_ = CdpClick(c, "#delete-account-btn")
|
||||
_ = CdpWaitIdle(c, 2000)
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa obtenida con CdpConnect."
|
||||
- name: accept
|
||||
desc: "true para aceptar/OK el dialogo; false para rechazar/Cancel. Para alert() el valor no importa (siempre se cierra), para confirm() determina el valor de retorno, para prompt() determina si se devuelve el texto o null."
|
||||
- name: promptText
|
||||
desc: "Texto a inyectar en dialogos prompt(). Vacio string para no inyectar texto. Ignorado en alert() y confirm()."
|
||||
output: "cancel func() para des-registrar el handler cuando ya no se necesite, y error si la conexion es nula o Page.enable falla. El cancel devuelto es seguro llamarlo multiples veces."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := CdpConnect(9222)
|
||||
_ = CdpNavigate(conn, "https://example.com/admin")
|
||||
_ = CdpWaitLoad(conn, 3000)
|
||||
|
||||
// Instalar handler antes de la accion que dispara el dialogo
|
||||
cancel, err := CdpHandleDialog(conn, true, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
// Este boton dispara confirm("¿Seguro que quieres borrar?")
|
||||
// El handler lo acepta automaticamente sin bloquear
|
||||
_ = CdpClick(conn, "#btn-delete-all")
|
||||
_ = CdpWaitIdle(conn, 2000)
|
||||
|
||||
// Ejemplo con prompt(): responder con texto especifico
|
||||
cancelPrompt, _ := CdpHandleDialog(conn, true, "mi-respuesta-secreta")
|
||||
defer cancelPrompt()
|
||||
_ = CdpClick(conn, "#btn-ask-password")
|
||||
_ = CdpWaitIdle(conn, 1000)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Instalar antes de cualquier accion que pueda disparar `alert()`, `confirm()`, `prompt()` o `beforeunload` en la pagina. Sin este handler, el dialogo bloquea el tab del navegador indefinidamente y todas las llamadas CDP siguientes se quedan colgadas esperando. Imprescindible en scraping de paneles de administracion, flujos de borrado con confirmacion, y paginas con `beforeunload` que pregunta si quieres salir.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- DEADLOCK GARANTIZADO si se llama `sendCDP` de forma sincrona dentro del handler de evento. El handler corre en la goroutine de lectura del WebSocket; `sendCDP` espera una respuesta que esa misma goroutine deberia leer. La implementacion ya usa `go c.sendCDP(...)` para evitarlo — no modificar este patron.
|
||||
- El handler se instala de forma permanente hasta que se llame `cancel()`. Si la pagina dispara multiples dialogos, todos seran respondidos con los mismos parametros `accept` y `promptText`.
|
||||
- `Page.enable` es idempotente pero tiene coste de red; no llamar CdpHandleDialog en bucles tight.
|
||||
- Para `beforeunload` (cuando el usuario cierra/navega fuera), `accept: true` permite la navegacion y `accept: false` la bloquea.
|
||||
- Llamar `cancel()` no cierra dialogos ya abiertos; solo evita que los futuros sean respondidos automaticamente.
|
||||
@@ -0,0 +1,73 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CdpFrame representa un frame/iframe del arbol de navegacion.
|
||||
type CdpFrame struct {
|
||||
ID string `json:"id"`
|
||||
ParentID string `json:"parentId"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CdpListFrames lista todos los frames de la pagina actual (frame raiz + iframes anidados)
|
||||
// usando Page.getFrameTree. Retorna el arbol aplanado con cada frame y su parentId.
|
||||
func CdpListFrames(c *CDPConn) ([]CdpFrame, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("cdp list frames: conexion nula")
|
||||
}
|
||||
|
||||
// Page.enable es idempotente; necesario para que Page.getFrameTree funcione
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return nil, fmt.Errorf("cdp list frames: Page.enable: %w", err)
|
||||
}
|
||||
|
||||
result, err := c.sendCDP("Page.getFrameTree", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdp list frames: Page.getFrameTree: %w", err)
|
||||
}
|
||||
|
||||
frameTree, ok := result["frameTree"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cdp list frames: frameTree no encontrado en respuesta")
|
||||
}
|
||||
|
||||
var frames []CdpFrame
|
||||
frameFlatten(frameTree, "", &frames)
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
// frameFlatten recorre recursivamente el arbol de frames CDP y acumula CdpFrame.
|
||||
// parentID es el ID del nodo padre; el frame raiz lo recibe vacio.
|
||||
func frameFlatten(node map[string]any, parentID string, acc *[]CdpFrame) {
|
||||
frameData, ok := node["frame"].(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
f := CdpFrame{
|
||||
ID: stringField(frameData, "id"),
|
||||
ParentID: parentID,
|
||||
URL: stringField(frameData, "url"),
|
||||
Name: stringField(frameData, "name"),
|
||||
}
|
||||
*acc = append(*acc, f)
|
||||
|
||||
// Recorrer hijos
|
||||
children, _ := node["childFrames"].([]any)
|
||||
for _, child := range children {
|
||||
childNode, ok := child.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
frameFlatten(childNode, f.ID, acc)
|
||||
}
|
||||
}
|
||||
|
||||
// stringField extrae un campo string de un map[string]any de forma segura.
|
||||
func stringField(m map[string]any, key string) string {
|
||||
v, _ := m[key].(string)
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: cdp_list_frames_go_browser
|
||||
name: cdp_list_frames
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Lista todos los frames/iframes de la pestaña activa usando Page.getFrameTree y devuelve el árbol aplanado con ID, parentID, URL y nombre de cada frame."
|
||||
tags: [cdp, browser, iframe, frames, page, navegator]
|
||||
signature: "func CdpListFrames(c *CDPConn) ([]CdpFrame, error)"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_list_frames.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect("localhost", 9222, "")
|
||||
frames, err := CdpListFrames(conn)
|
||||
for _, f := range frames {
|
||||
fmt.Printf("frame %s parent=%s url=%s\n", f.ID, f.ParentID, f.URL)
|
||||
}
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa obtenida con CdpConnect; apunta a la pestaña cuyo árbol de frames se quiere inspeccionar."
|
||||
output: "Slice de CdpFrame con ID, ParentID, URL y Name de cada frame aplanado; error si la conexión es nula, Page.enable falla o la respuesta CDP es inesperada."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := CdpConnect("localhost", 9222, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
frames, err := CdpListFrames(conn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, f := range frames {
|
||||
fmt.Printf("id=%-40s parent=%-40s url=%s\n", f.ID, f.ParentID, f.URL)
|
||||
}
|
||||
// Salida ejemplo:
|
||||
// id=ABCD1234 parent= url=https://example.com
|
||||
// id=EFGH5678 parent=ABCD1234 url=https://ads.example.com/iframe
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de evaluar JS en un iframe con `CdpEvalInFrame`: necesitas el `frameID` exacto que usa CDP, no el `src` del iframe. También útil para auditar la estructura de frames de una página o detectar iframes de terceros.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Requiere que la pestaña ya esté cargada; si se llama justo tras `CdpNavigate` en páginas con lazy-load de iframes, puede devolver un listado incompleto — espera a `Page.loadEventFired` o usa un breve delay.
|
||||
- `Page.enable` se llama internamente (idempotente); no hace falta llamarlo manualmente antes.
|
||||
- El frame raíz tiene `ParentID` vacío. Los iframes anidados tienen como `ParentID` el `ID` del frame contenedor.
|
||||
- `Name` puede ser vacío si el `<iframe>` no tiene atributo `name`.
|
||||
@@ -0,0 +1,98 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// jsQuote serializa s como literal string JavaScript con comillas dobles y
|
||||
// caracteres escapados correctamente. Usa json.Marshal internamente para
|
||||
// reutilizar el mismo escapado que JSON (compatible con JS).
|
||||
func jsQuote(s string) string {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
// Fallback seguro: comillas dobles escapando backslash y comilla doble
|
||||
return fmt.Sprintf("%q", s)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// CdpLoadStorageState lee el JSON generado por CdpSaveStorageState y restaura
|
||||
// cookies y localStorage en la pestaña activa. Permite retomar una sesion
|
||||
// autenticada sin repetir el login.
|
||||
//
|
||||
// CRITICO: el localStorage es por-origen. Antes de llamar a esta funcion hay
|
||||
// que haber navegado al origen correcto (CdpNavigate al dominio). Orden
|
||||
// correcto: navegar -> CdpLoadStorageState -> recargar pagina.
|
||||
func CdpLoadStorageState(c *CDPConn, inPath string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp load storage state: conexion nula")
|
||||
}
|
||||
if inPath == "" {
|
||||
return fmt.Errorf("cdp load storage state: inPath vacio")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(inPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp load storage state: leer archivo: %w", err)
|
||||
}
|
||||
|
||||
var state CdpStorageState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return fmt.Errorf("cdp load storage state: unmarshal: %w", err)
|
||||
}
|
||||
|
||||
// Habilitar dominio Network para manipular cookies
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return fmt.Errorf("cdp load storage state: Network.enable: %w", err)
|
||||
}
|
||||
|
||||
// Restaurar cookies. Network.setCookies aplica de forma fiable las cookies
|
||||
// (sobre todo httpOnly y de sesión) cuando cada una lleva el campo `url`: de
|
||||
// ahí deriva scheme y scope. getAllCookies no lo incluye, así que lo
|
||||
// sintetizamos a partir de domain/secure/path cuando falta.
|
||||
if len(state.Cookies) > 0 {
|
||||
for _, ck := range state.Cookies {
|
||||
if _, has := ck["url"]; has {
|
||||
continue
|
||||
}
|
||||
dom, _ := ck["domain"].(string)
|
||||
dom = strings.TrimPrefix(dom, ".")
|
||||
if dom == "" {
|
||||
continue
|
||||
}
|
||||
scheme := "http"
|
||||
if sec, _ := ck["secure"].(bool); sec {
|
||||
scheme = "https"
|
||||
}
|
||||
path, _ := ck["path"].(string)
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
ck["url"] = scheme + "://" + dom + path
|
||||
}
|
||||
if _, err := c.sendCDP("Network.setCookies", map[string]any{
|
||||
"cookies": state.Cookies,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("cdp load storage state: setCookies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Restaurar localStorage y sessionStorage — setItem por cada par clave/valor
|
||||
for k, v := range state.LocalStorage {
|
||||
expr := fmt.Sprintf("window.localStorage.setItem(%s, %s)", jsQuote(k), jsQuote(v))
|
||||
if _, err := CdpEvaluate(c, expr); err != nil {
|
||||
return fmt.Errorf("cdp load storage state: localStorage setItem %q: %w", k, err)
|
||||
}
|
||||
}
|
||||
for k, v := range state.SessionStorage {
|
||||
expr := fmt.Sprintf("window.sessionStorage.setItem(%s, %s)", jsQuote(k), jsQuote(v))
|
||||
if _, err := CdpEvaluate(c, expr); err != nil {
|
||||
return fmt.Errorf("cdp load storage state: sessionStorage setItem %q: %w", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: cdp_load_storage_state_go_browser
|
||||
name: cdp_load_storage_state
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Restaura cookies y localStorage desde un archivo JSON (generado por CdpSaveStorageState) en la pestaña activa, reanudando una sesión autenticada sin repetir el login."
|
||||
tags: [cdp, browser, storage, session, cookies, localStorage, auth, navegator]
|
||||
signature: "func CdpLoadStorageState(c *CDPConn, inPath string) error"
|
||||
uses_functions:
|
||||
- cdp_evaluate_go_browser
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_load_storage_state.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect(9222)
|
||||
defer CdpClose(conn)
|
||||
CdpNavigate(conn, "https://app.example.com")
|
||||
CdpLoadStorageState(conn, "/tmp/session.json")
|
||||
CdpNavigate(conn, "https://app.example.com") // reload para que la app lea el localStorage restaurado
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa apuntando a la pestaña donde se restaurará el estado."
|
||||
- name: inPath
|
||||
desc: "Ruta del archivo JSON producido previamente por CdpSaveStorageState."
|
||||
output: "nil si cookies y localStorage se restauraron correctamente; error con contexto si el archivo no existe, el JSON es inválido o falla algún comando CDP."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := CdpConnect(9222)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer CdpClose(conn)
|
||||
|
||||
// 1. Navegar al origen correcto ANTES de restaurar
|
||||
CdpNavigate(conn, "https://app.example.com")
|
||||
|
||||
// 2. Restaurar cookies + localStorage
|
||||
if err := CdpLoadStorageState(conn, "/tmp/session.json"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 3. Recargar para que la app lea el localStorage restaurado
|
||||
CdpNavigate(conn, "https://app.example.com")
|
||||
|
||||
// A partir de aquí la sesión está activa — no se necesitó login
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al inicio de un script de scraping autenticado, después de `CdpNavigate` al dominio objetivo y antes de cualquier interacción. Sustituye el flujo de login cuando ya existe un archivo de estado guardado con `CdpSaveStorageState`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Orden obligatorio: navegar → load → reload**. El localStorage es por-origen: si llamas a esta función antes de navegar al dominio correcto, los `setItem` escriben en el origen equivocado (p.ej. `about:blank`) y la app no los ve. Secuencia correcta: `CdpNavigate(dominio)` → `CdpLoadStorageState(...)` → `CdpNavigate(dominio)` de nuevo.
|
||||
- **Cookies globales del perfil**: `Network.setCookies` restaura todas las cookies del archivo, que pueden ser de múltiples dominios. Esto es el comportamiento esperado y compatible con cómo las guardó `CdpSaveStorageState`.
|
||||
- **Archivo inexistente o corrupto**: la función devuelve error explícito; comprueba que el archivo existe antes de llamarla (por ejemplo con `os.Stat`) si quieres un fallback a login completo.
|
||||
- **Sesión expirada**: restaurar el estado no renueva tokens del servidor. Si la sesión expiró (cookies caducadas, JWT vencido), la app redirigirá a login igualmente. En ese caso re-autentícate y vuelve a guardar el estado.
|
||||
@@ -0,0 +1,67 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CdpNavBack retrocede una entrada en el historial de navegacion de la pestana activa.
|
||||
// Obtiene el historial via Page.getNavigationHistory, calcula el indice anterior y
|
||||
// navega a esa entrada via Page.navigateToHistoryEntry.
|
||||
// Retorna error si ya estamos al inicio del historial.
|
||||
func CdpNavBack(c *CDPConn) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp nav back: conexion nula")
|
||||
}
|
||||
|
||||
result, err := c.sendCDP("Page.getNavigationHistory", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp nav back: obtener historial: %w", err)
|
||||
}
|
||||
|
||||
currentIndexRaw, ok := result["currentIndex"]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: respuesta sin currentIndex")
|
||||
}
|
||||
currentIndex, ok := currentIndexRaw.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: currentIndex tipo inesperado: %T", currentIndexRaw)
|
||||
}
|
||||
|
||||
entriesRaw, ok := result["entries"]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: respuesta sin entries")
|
||||
}
|
||||
entries, ok := entriesRaw.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: entries tipo inesperado: %T", entriesRaw)
|
||||
}
|
||||
|
||||
idx := int(currentIndex) - 1
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("cdp nav back: ya en el inicio del historial")
|
||||
}
|
||||
if idx >= len(entries) {
|
||||
return fmt.Errorf("cdp nav back: indice %d fuera de rango (len=%d)", idx, len(entries))
|
||||
}
|
||||
|
||||
entry, ok := entries[idx].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: entrada[%d] tipo inesperado: %T", idx, entries[idx])
|
||||
}
|
||||
entryIDRaw, ok := entry["id"]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: entrada sin campo id")
|
||||
}
|
||||
entryIDFloat, ok := entryIDRaw.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav back: entry id tipo inesperado: %T", entryIDRaw)
|
||||
}
|
||||
entryID := int(entryIDFloat)
|
||||
|
||||
_, err = c.sendCDP("Page.navigateToHistoryEntry", map[string]any{"entryId": entryID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp nav back: navegar a entrada %d: %w", entryID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: cdp_nav_back_go_browser
|
||||
name: cdp_nav_back
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Retrocede una entrada en el historial de navegación de la pestaña activa via Page.getNavigationHistory + Page.navigateToHistoryEntry. Equivalente a pulsar el botón Atrás del navegador."
|
||||
tags: [cdp, browser, navigation, navegator]
|
||||
signature: "func CdpNavBack(c *CDPConn) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_nav_back.go"
|
||||
example: |
|
||||
conn, _ := browser.CdpConnect(9222)
|
||||
defer browser.CdpClose(conn, 0)
|
||||
_ = browser.CdpNavigate(conn, "https://example.com/paso1")
|
||||
_ = browser.CdpNavigate(conn, "https://example.com/paso2")
|
||||
// Volver a /paso1
|
||||
if err := browser.CdpNavBack(conn); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa obtenida de CdpConnect, apuntando a la pestaña que se quiere retroceder"
|
||||
output: "nil si navegó correctamente a la entrada anterior; error si ya estamos al inicio del historial, la conexión es nula o el cast de tipos falla (respuesta CDP malformada)"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := browser.CdpConnect(9222)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer browser.CdpClose(conn, 0)
|
||||
|
||||
_ = browser.CdpNavigate(conn, "https://metabase.local/dashboard/1")
|
||||
_ = browser.CdpNavigate(conn, "https://metabase.local/question/42")
|
||||
|
||||
// Volver al dashboard
|
||||
if err := browser.CdpNavBack(conn); err != nil {
|
||||
log.Printf("no se pudo retroceder: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un flujo de automatización navega por varias páginas y necesita volver atrás sin conocer la URL anterior. Útil en scraping de paginaciones o en flujos de formularios multipaso donde la URL destino no es predecible.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Navega dentro del historial de ESA pestaña CDP (sesión WS), no del perfil en disco ni del historial global de Chrome.
|
||||
- Si `currentIndex == 0` (primer elemento del historial), retorna error "ya en el inicio del historial" — no es un fallo de red, es estado válido.
|
||||
- Requiere que `Page` esté habilitado en la sesión; Chrome lo activa automáticamente con la mayoría de conexiones CDP, pero si usas una sesión muy restrictiva puede fallar.
|
||||
- No espera a que la página de destino cargue; encadenar con `CdpWaitLoad` o `CdpWaitIdle` si necesitas esperar la carga completa.
|
||||
@@ -0,0 +1,64 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CdpNavForward avanza una entrada en el historial de navegacion de la pestana activa.
|
||||
// Obtiene el historial via Page.getNavigationHistory, calcula el indice siguiente y
|
||||
// navega a esa entrada via Page.navigateToHistoryEntry.
|
||||
// Retorna error si ya estamos al final del historial (no hay entradas adelante).
|
||||
func CdpNavForward(c *CDPConn) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp nav forward: conexion nula")
|
||||
}
|
||||
|
||||
result, err := c.sendCDP("Page.getNavigationHistory", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp nav forward: obtener historial: %w", err)
|
||||
}
|
||||
|
||||
currentIndexRaw, ok := result["currentIndex"]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: respuesta sin currentIndex")
|
||||
}
|
||||
currentIndex, ok := currentIndexRaw.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: currentIndex tipo inesperado: %T", currentIndexRaw)
|
||||
}
|
||||
|
||||
entriesRaw, ok := result["entries"]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: respuesta sin entries")
|
||||
}
|
||||
entries, ok := entriesRaw.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: entries tipo inesperado: %T", entriesRaw)
|
||||
}
|
||||
|
||||
idx := int(currentIndex) + 1
|
||||
if idx >= len(entries) {
|
||||
return fmt.Errorf("cdp nav forward: ya en el final del historial")
|
||||
}
|
||||
|
||||
entry, ok := entries[idx].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: entrada[%d] tipo inesperado: %T", idx, entries[idx])
|
||||
}
|
||||
entryIDRaw, ok := entry["id"]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: entrada sin campo id")
|
||||
}
|
||||
entryIDFloat, ok := entryIDRaw.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp nav forward: entry id tipo inesperado: %T", entryIDRaw)
|
||||
}
|
||||
entryID := int(entryIDFloat)
|
||||
|
||||
_, err = c.sendCDP("Page.navigateToHistoryEntry", map[string]any{"entryId": entryID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp nav forward: navegar a entrada %d: %w", entryID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
id: cdp_nav_forward_go_browser
|
||||
name: cdp_nav_forward
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Avanza una entrada en el historial de navegación de la pestaña activa via Page.getNavigationHistory + Page.navigateToHistoryEntry. Equivalente a pulsar el botón Adelante del navegador."
|
||||
tags: [cdp, browser, navigation, navegator]
|
||||
signature: "func CdpNavForward(c *CDPConn) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_nav_forward.go"
|
||||
example: |
|
||||
conn, _ := browser.CdpConnect(9222)
|
||||
defer browser.CdpClose(conn, 0)
|
||||
_ = browser.CdpNavigate(conn, "https://example.com/paso1")
|
||||
_ = browser.CdpNavigate(conn, "https://example.com/paso2")
|
||||
_ = browser.CdpNavBack(conn) // volver a /paso1
|
||||
// Avanzar de nuevo a /paso2
|
||||
if err := browser.CdpNavForward(conn); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa obtenida de CdpConnect, apuntando a la pestaña que se quiere avanzar"
|
||||
output: "nil si navegó correctamente a la entrada siguiente; error si ya estamos al final del historial, la conexión es nula o el cast de tipos falla (respuesta CDP malformada)"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := browser.CdpConnect(9222)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer browser.CdpClose(conn, 0)
|
||||
|
||||
_ = browser.CdpNavigate(conn, "https://metabase.local/dashboard/1")
|
||||
_ = browser.CdpNavigate(conn, "https://metabase.local/question/42")
|
||||
_ = browser.CdpNavBack(conn) // vuelve a /dashboard/1
|
||||
|
||||
// Avanzar de nuevo a /question/42
|
||||
if err := browser.CdpNavForward(conn); err != nil {
|
||||
log.Printf("no se pudo avanzar: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un flujo de automatización ha retrocedido con `CdpNavBack` y necesita volver a avanzar sin conocer la URL destino. Útil para recorrer un historial de páginas hacia adelante y hacia atrás de forma programática, por ejemplo en herramientas de replay de sesiones.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Navega dentro del historial de ESA pestaña CDP (sesión WS), no del perfil en disco ni del historial global de Chrome.
|
||||
- Si `currentIndex` es el último elemento del historial (`currentIndex == len(entries) - 1`), retorna error "ya en el final del historial" — no es un fallo de red, es estado válido.
|
||||
- El historial se trunca cuando se navega a una URL nueva estando en una entrada intermedia: las entradas "adelante" desaparecen, igual que en un navegador real.
|
||||
- No espera a que la página de destino cargue; encadenar con `CdpWaitLoad` o `CdpWaitIdle` si necesitas esperar la carga completa.
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
)
|
||||
|
||||
// CdpNavigate navega a la URL indicada usando Page.navigate.
|
||||
// Espera a que la carga este confirmada via Page.loadEventFired antes de retornar.
|
||||
// El timeout de la navegacion es gestionado por Chrome internamente.
|
||||
// NO espera a que la pagina cargue: retorna en cuanto Chrome acepta la navegacion
|
||||
// (solo verifica que no haya errorText). Para esperar la carga real encadena
|
||||
// despues CdpWaitLoad (document.readyState) o CdpWaitIdle (red en reposo).
|
||||
func CdpNavigate(c *CDPConn, targetURL string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp navigate: conexion nula")
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package browser
|
||||
|
||||
import "fmt"
|
||||
|
||||
// pressKeyEntry define los atributos CDP de una tecla especial.
|
||||
type pressKeyEntry struct {
|
||||
vk int
|
||||
key string
|
||||
code string
|
||||
text string
|
||||
}
|
||||
|
||||
// pressKeyTable mapea nombres de tecla a sus atributos CDP.
|
||||
var pressKeyTable = map[string]pressKeyEntry{
|
||||
"Enter": {vk: 13, key: "Enter", code: "Enter", text: "\r"},
|
||||
"Tab": {vk: 9, key: "Tab", code: "Tab"},
|
||||
"Escape": {vk: 27, key: "Escape", code: "Escape"},
|
||||
"Backspace": {vk: 8, key: "Backspace", code: "Backspace"},
|
||||
"Delete": {vk: 46, key: "Delete", code: "Delete"},
|
||||
"ArrowUp": {vk: 38, key: "ArrowUp", code: "ArrowUp"},
|
||||
"ArrowDown": {vk: 40, key: "ArrowDown", code: "ArrowDown"},
|
||||
"ArrowLeft": {vk: 37, key: "ArrowLeft", code: "ArrowLeft"},
|
||||
"ArrowRight": {vk: 39, key: "ArrowRight", code: "ArrowRight"},
|
||||
"Home": {vk: 36, key: "Home", code: "Home"},
|
||||
"End": {vk: 35, key: "End", code: "End"},
|
||||
"PageUp": {vk: 33, key: "PageUp", code: "PageUp"},
|
||||
"PageDown": {vk: 34, key: "PageDown", code: "PageDown"},
|
||||
"Space": {vk: 32, key: " ", code: "Space", text: " "},
|
||||
}
|
||||
|
||||
// CdpPressKey pulsa una tecla especial por nombre usando Input.dispatchKeyEvent.
|
||||
// Soporta: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft,
|
||||
// ArrowRight, Home, End, PageUp, PageDown, Space.
|
||||
// Actua sobre el elemento con foco activo en la pagina.
|
||||
func CdpPressKey(c *CDPConn, keyName string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp press key: conexion nula")
|
||||
}
|
||||
|
||||
entry, ok := pressKeyTable[keyName]
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp press key: tecla no soportada: %s", keyName)
|
||||
}
|
||||
|
||||
down := map[string]any{
|
||||
"type": "keyDown",
|
||||
"windowsVirtualKeyCode": entry.vk,
|
||||
"key": entry.key,
|
||||
"code": entry.code,
|
||||
}
|
||||
if entry.text != "" {
|
||||
down["text"] = entry.text
|
||||
}
|
||||
if _, err := c.sendCDP("Input.dispatchKeyEvent", down); err != nil {
|
||||
return fmt.Errorf("cdp press key: keyDown %q: %w", keyName, err)
|
||||
}
|
||||
|
||||
up := map[string]any{
|
||||
"type": "keyUp",
|
||||
"windowsVirtualKeyCode": entry.vk,
|
||||
"key": entry.key,
|
||||
"code": entry.code,
|
||||
}
|
||||
if _, err := c.sendCDP("Input.dispatchKeyEvent", up); err != nil {
|
||||
return fmt.Errorf("cdp press key: keyUp %q: %w", keyName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: cdp_press_key_go_browser
|
||||
name: cdp_press_key
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
description: "Pulsa una tecla especial por nombre via Input.dispatchKeyEvent CDP (Enter, Tab, Escape, flechas, etc.) sobre el elemento con foco activo."
|
||||
tags: [cdp, browser, input, keyboard, navegator]
|
||||
signature: "func CdpPressKey(c *CDPConn, keyName string) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_press_key.go"
|
||||
example: |
|
||||
// Enfocar un input y pulsar Enter para enviar el formulario
|
||||
_ = CdpClick(c, "input[name='q']")
|
||||
_ = CdpTypeText(c, "golang")
|
||||
_ = CdpPressKey(c, "Enter")
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa obtenida con CdpConnect."
|
||||
- name: keyName
|
||||
desc: "Nombre de la tecla a pulsar. Valores soportados: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, Space."
|
||||
output: "nil si la tecla se despacho correctamente. Error si la conexion es nula, la tecla no esta en la tabla soportada, o CDP rechaza el evento."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := CdpConnect(9222)
|
||||
|
||||
// Enfocar campo de busqueda, escribir y enviar con Enter
|
||||
_ = CdpClick(conn, "input[name='q']")
|
||||
_ = CdpTypeText(conn, "golang generics")
|
||||
if err := CdpPressKey(conn, "Enter"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Navegar en un desplegable con flechas
|
||||
_ = CdpClick(conn, "#dropdown")
|
||||
_ = CdpPressKey(conn, "ArrowDown")
|
||||
_ = CdpPressKey(conn, "ArrowDown")
|
||||
_ = CdpPressKey(conn, "Enter")
|
||||
|
||||
// Cerrar un modal con Escape
|
||||
_ = CdpPressKey(conn, "Escape")
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar cuando necesites simular pulsaciones de teclas especiales sobre el elemento con foco: enviar formularios con Enter, navegar opciones con flechas, limpiar campos con Backspace/Delete, cerrar modales con Escape, o desplazarse con PageUp/PageDown. Para escribir texto normal usa CdpTypeText.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- La tecla actua sobre el elemento con foco activo. Llama a CdpClick primero para enfocar el elemento objetivo.
|
||||
- Teclas sin caracter imprimible (Tab, Escape, flechas, Home, End, PageUp, PageDown) no envian el campo "text" — Chrome lo requiere asi para distinguir navegacion de insercion.
|
||||
- Enter envia `text: "\r"` que es lo que Chrome espera para confirmar formularios y autocompletados.
|
||||
- Space envia `key: " "` y `text: " "` — funciona como barra espaciadora y como insercion de espacio en inputs.
|
||||
- Si la tecla que necesitas no esta en la tabla, la funcion retorna error explicito en vez de silencio.
|
||||
@@ -0,0 +1,120 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CdpStorageState agrupa cookies, localStorage y sessionStorage capturados de una
|
||||
// pestaña activa.
|
||||
type CdpStorageState struct {
|
||||
Cookies []map[string]any `json:"cookies"`
|
||||
LocalStorage map[string]string `json:"localStorage"`
|
||||
SessionStorage map[string]string `json:"sessionStorage"`
|
||||
}
|
||||
|
||||
// readWebStorage lee window.<store> (localStorage|sessionStorage) como mapa. Si el
|
||||
// origen no permite acceso (about:blank, chrome://) devuelve un mapa vacío.
|
||||
func readWebStorage(c *CDPConn, store string) map[string]string {
|
||||
raw, err := CdpEvaluate(c, "JSON.stringify(Object.assign({}, window."+store+"))")
|
||||
if err != nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
if raw == "" || raw == "undefined" || raw == "null" {
|
||||
return map[string]string{}
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// cookieDomainMatchesHost indica si una cookie con `domain` aplica al `host` dado.
|
||||
// Cubre el caso de dominios con punto inicial (".example.com") y subdominios.
|
||||
func cookieDomainMatchesHost(domain, host string) bool {
|
||||
if domain == "" || host == "" {
|
||||
return false
|
||||
}
|
||||
d := strings.TrimPrefix(domain, ".")
|
||||
return host == d || strings.HasSuffix(host, "."+d)
|
||||
}
|
||||
|
||||
// storageStateToMaps convierte []any (respuesta CDP) a []map[string]any.
|
||||
func storageStateToMaps(raw []any) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CdpSaveStorageState captura cookies y localStorage de la pagina actual y los
|
||||
// escribe como JSON a outPath. Permite restaurar la sesion autenticada en
|
||||
// ejecuciones posteriores sin repetir el login.
|
||||
func CdpSaveStorageState(c *CDPConn, outPath string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp save storage state: conexion nula")
|
||||
}
|
||||
if outPath == "" {
|
||||
return fmt.Errorf("cdp save storage state: outPath vacio")
|
||||
}
|
||||
|
||||
// Habilitar dominio Network para acceder a las cookies
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return fmt.Errorf("cdp save storage state: Network.enable: %w", err)
|
||||
}
|
||||
|
||||
// Obtener todas las cookies del perfil
|
||||
res, err := c.sendCDP("Network.getAllCookies", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp save storage state: getAllCookies: %w", err)
|
||||
}
|
||||
|
||||
var cookies []map[string]any
|
||||
if rawCookies, ok := res["cookies"].([]any); ok {
|
||||
cookies = storageStateToMaps(rawCookies)
|
||||
} else {
|
||||
cookies = []map[string]any{}
|
||||
}
|
||||
|
||||
// Filtrar al origen actual: Network.getAllCookies devuelve cookies de TODOS
|
||||
// los dominios del perfil. Para guardar "la sesión de ESTE sitio" solo
|
||||
// conservamos las que aplican al host cargado, evitando arrastrar cookies de
|
||||
// otros sitios visitados en la misma sesión del navegador.
|
||||
if host, herr := CdpEvaluate(c, "location.hostname"); herr == nil {
|
||||
host = strings.TrimSpace(host)
|
||||
if host != "" && host != "undefined" {
|
||||
filtered := make([]map[string]any, 0, len(cookies))
|
||||
for _, ck := range cookies {
|
||||
dom, _ := ck["domain"].(string)
|
||||
if cookieDomainMatchesHost(dom, host) {
|
||||
filtered = append(filtered, ck)
|
||||
}
|
||||
}
|
||||
cookies = filtered
|
||||
}
|
||||
}
|
||||
|
||||
// Capturar localStorage y sessionStorage del origen actualmente cargado.
|
||||
state := CdpStorageState{
|
||||
Cookies: cookies,
|
||||
LocalStorage: readWebStorage(c, "localStorage"),
|
||||
SessionStorage: readWebStorage(c, "sessionStorage"),
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp save storage state: marshal: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("cdp save storage state: escribir archivo: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: cdp_save_storage_state_go_browser
|
||||
name: cdp_save_storage_state
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
description: "Captura cookies y localStorage de la página activa y los serializa a un archivo JSON para restaurar la sesión sin repetir el login."
|
||||
tags: [cdp, browser, storage, session, cookies, localStorage, auth, navegator]
|
||||
signature: "func CdpSaveStorageState(c *CDPConn, outPath string) error"
|
||||
uses_functions:
|
||||
- cdp_evaluate_go_browser
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_save_storage_state.go"
|
||||
example: |
|
||||
conn, _ := CdpConnect(9222)
|
||||
defer CdpClose(conn)
|
||||
CdpNavigate(conn, "https://app.example.com")
|
||||
err := CdpSaveStorageState(conn, "/tmp/session.json")
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexión CDP activa apuntando a la pestaña con la sesión autenticada."
|
||||
- name: outPath
|
||||
desc: "Ruta del archivo JSON de salida donde se escribirá el estado (cookies + localStorage)."
|
||||
output: "nil si el archivo se escribió correctamente; error con contexto en caso de fallo de red, CDP o escritura."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, err := CdpConnect(9222)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer CdpClose(conn)
|
||||
|
||||
// Navegar y autenticarse manualmente o con scraping
|
||||
CdpNavigate(conn, "https://app.example.com/dashboard")
|
||||
|
||||
// Guardar estado de la sesión
|
||||
if err := CdpSaveStorageState(conn, "/tmp/session.json"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// /tmp/session.json contiene cookies + localStorage listos para restaurar
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras completar un login en el browser (manual o automatizado), antes de cerrar la sesión o como paso final del script de autenticación. En la próxima ejecución, llama a `CdpLoadStorageState` en vez de repetir el flujo de login.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **localStorage es por-origen**: solo captura el localStorage del origen actualmente cargado en la pestaña. Si necesitas preservar localStorage de múltiples dominios, guarda un estado por cada dominio navegado.
|
||||
- **Cookies globales del perfil**: `Network.getAllCookies` devuelve todas las cookies del perfil de Chrome, no solo las del origen activo. El JSON puede ser grande si el perfil tiene muchas cookies.
|
||||
- **Páginas especiales** (`about:blank`, `chrome://`, extensiones): `CdpEvaluate` sobre localStorage fallará; la función lo maneja devolviendo un mapa vacío de forma defensiva, así que no romperá — pero el localStorage quedará vacío en el JSON.
|
||||
- **Permisos**: el archivo se escribe con `0644`; asegúrate de que el directorio de destino existe antes de llamar a la función.
|
||||
@@ -0,0 +1,26 @@
|
||||
package browser
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CdpScroll desplaza la pagina via rueda del raton usando Input.dispatchMouseEvent.
|
||||
// deltaY positivo desplaza hacia abajo; deltaX positivo desplaza hacia la derecha.
|
||||
// El evento se despacha en las coordenadas (100, 100) del viewport, que
|
||||
// generalmente cae sobre el contenido principal de la pagina.
|
||||
func CdpScroll(c *CDPConn, deltaX, deltaY float64) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp scroll: conexion nula")
|
||||
}
|
||||
|
||||
params := map[string]any{
|
||||
"type": "mouseWheel",
|
||||
"x": 100.0,
|
||||
"y": 100.0,
|
||||
"deltaX": deltaX,
|
||||
"deltaY": deltaY,
|
||||
}
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", params); err != nil {
|
||||
return fmt.Errorf("cdp scroll: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: cdp_scroll_go_browser
|
||||
name: cdp_scroll
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
purity: impure
|
||||
version: 1.0.0
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
description: "Desplaza la pagina via rueda del raton con Input.dispatchMouseEvent type mouseWheel; imprescindible para scroll infinito en SPAs."
|
||||
tags: [cdp, browser, input, scroll, navegator]
|
||||
signature: "func CdpScroll(c *CDPConn, deltaX, deltaY float64) error"
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
file_path: "functions/browser/cdp_scroll.go"
|
||||
example: |
|
||||
// Scroll hacia abajo 800px en una SPA con feed infinito
|
||||
for i := 0; i < 5; i++ {
|
||||
_ = CdpScroll(c, 0, 800)
|
||||
_ = CdpWaitIdle(c, 1500)
|
||||
}
|
||||
params:
|
||||
- name: c
|
||||
desc: "Conexion CDP activa obtenida con CdpConnect."
|
||||
- name: deltaX
|
||||
desc: "Desplazamiento horizontal en pixeles. Positivo = derecha, negativo = izquierda. 0 para scroll solo vertical."
|
||||
- name: deltaY
|
||||
desc: "Desplazamiento vertical en pixeles. Positivo = hacia abajo, negativo = hacia arriba. Valores tipicos: 300-800 por paso."
|
||||
output: "nil si el evento de scroll se despacho correctamente. Error si la conexion es nula o CDP rechaza el evento."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
conn, _ := CdpConnect(9222)
|
||||
_ = CdpNavigate(conn, "https://news.ycombinator.com")
|
||||
_ = CdpWaitLoad(conn, 3000)
|
||||
|
||||
// Scroll hacia abajo en 5 pasos con pausa entre cada uno
|
||||
for i := 0; i < 5; i++ {
|
||||
if err := CdpScroll(conn, 0, 600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Esperar que la SPA cargue nuevo contenido
|
||||
_ = CdpWaitIdle(conn, 1500)
|
||||
}
|
||||
|
||||
// Volver al inicio
|
||||
_ = CdpScroll(conn, 0, -99999)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar para cargar contenido de scroll infinito en SPAs (Twitter, LinkedIn, feeds), para desplazarse hasta elementos fuera del viewport antes de interactuar con ellos, o para simular lectura humana de una pagina. Combinar con CdpWaitIdle entre scrolls para dar tiempo a que el framework cargue nuevo contenido.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El evento se despacha en las coordenadas fijas (100, 100) del viewport. Si la pagina tiene un panel lateral o header que ocupa esa zona, el scroll puede no afectar al contenedor principal. En ese caso, evaluar `window.scrollBy(deltaX, deltaY)` via CdpEvaluate como alternativa.
|
||||
- deltaY positivo = hacia abajo (igual que WheelEvent nativo del navegador).
|
||||
- Para SPAs con scroll infinito es imprescindible llamar CdpWaitIdle despues de cada CdpScroll; sin la pausa, los scrolls consecutivos llegan antes de que el framework procese el primero.
|
||||
- No hay garantia de que el scroll llegue al valor exacto de deltaY: el navegador puede aplicar aceleracion o limitar el desplazamiento al final del contenido.
|
||||
@@ -2,6 +2,7 @@ package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +14,17 @@ func CdpTypeText(c *CDPConn, text string) error {
|
||||
return fmt.Errorf("cdp type text: conexion nula")
|
||||
}
|
||||
|
||||
// Verificar que hay un campo editable enfocado. Sin foco, los caracteres se
|
||||
// pierden silenciosamente (van a document.body). Devolvemos error claro en vez
|
||||
// de "escribir a la nada".
|
||||
focus, ferr := CdpEvaluate(c, `(function(){var a=document.activeElement;if(!a)return 'none';var t=a.tagName.toLowerCase();return (t==='input'||t==='textarea'||t==='select'||a.isContentEditable)?'ok':t;})()`)
|
||||
if ferr != nil {
|
||||
return fmt.Errorf("cdp type text: verificar foco: %w", ferr)
|
||||
}
|
||||
if strings.TrimSpace(focus) != "ok" {
|
||||
return fmt.Errorf("cdp type text: no hay campo de texto enfocado (activeElement: %s); usa CdpClick sobre el input primero", strings.TrimSpace(focus))
|
||||
}
|
||||
|
||||
// keyDown (con `text`) ya inserta el caracter en el elemento focado en
|
||||
// Chrome — enviar ademas un evento "char" lo duplicaba en sitios que
|
||||
// reaccionan a `input` events (DuckDuckGo, Google, etc.). Patron
|
||||
|
||||
@@ -83,7 +83,15 @@ func defaultWindowsUserDataDir() (string, error) {
|
||||
}
|
||||
|
||||
// chromePathsLinux lista los binarios Linux-nativos de Chrome en orden de preferencia.
|
||||
// Las rutas absolutas a los binarios REALES van primero: saltan el wrapper
|
||||
// /usr/bin/chromium (un script que inyecta los flags de /etc/chromium.d/*, p.ej.
|
||||
// --user-data-dir y --remote-debugging-port globales que pisarian el aislamiento
|
||||
// del navegador del agente). Si no existen, se cae a los nombres de PATH — que
|
||||
// pueden resolver al wrapper, en cuyo caso el aislamiento depende de que nuestros
|
||||
// flags vayan al final (Chrome usa el ultimo --user-data-dir duplicado).
|
||||
var chromePathsLinux = []string{
|
||||
"/usr/lib/chromium/chromium",
|
||||
"/usr/lib/chromium-browser/chromium-browser",
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"google-chrome",
|
||||
|
||||
Reference in New Issue
Block a user