Files
fn_registry/functions/browser/cdp_collect_console.go
T
Egutierrez 9798aed2cf feat(browser): cdp_collect_console + cdp_print_pdf + cdp_select_option + cdp_set_file_input
Cuatro primitivas CDP nuevas para el dominio browser, base de nuevas tools del
browser_mcp:
- cdp_collect_console: snapshot temporal de console + exceptions + log entries
- cdp_print_pdf: Page.printToPDF -> []byte
- cdp_select_option: selecciona <option> en un <select> y dispara input/change
- cdp_set_file_input: sube archivos a un <input type=file> via DOM.setFileInputFiles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:21:46 +02:00

219 lines
7.0 KiB
Go

package browser
import (
"encoding/json"
"fmt"
"strings"
"sync"
"time"
)
// ConsoleEntry es una entrada del log de consola/diagnostico capturada via CDP
// durante una ventana temporal. Type clasifica el origen:
// - "log"/"info"/"warn"/"error"/"debug" — Runtime.consoleAPICalled (console.*)
// - "exception" — Runtime.exceptionThrown (errores JS no capturados)
// - el level de Log.entryAdded ("verbose"/"info"/"warning"/"error") para
// avisos del propio navegador (network, security, deprecaciones...)
type ConsoleEntry struct {
Type string `json:"type"` // log|info|warn|warning|error|debug|exception|verbose
Text string `json:"text"` // mensaje legible (args concatenados / descripcion + stack)
URL string `json:"url"` // URL del script o recurso, si Chrome lo informa
Line int `json:"line"` // numero de linea (1-based), 0 si desconocido
Timestamp float64 `json:"timestamp"` // CDP timestamp (monotonic seconds) o wall time
}
// CdpCollectConsole habilita los dominios Runtime y Log en la conexion, se
// suscribe a los eventos de consola/excepcion/log del navegador y acumula todo
// lo que ocurra durante `durationMs` milisegundos. Es un SNAPSHOT temporal:
// captura solo lo emitido dentro de la ventana, no el historico previo de la
// pagina. Si durationMs <= 0 usa 1500ms por defecto.
//
// Eventos capturados y como se mapean a ConsoleEntry.Type:
// - Runtime.consoleAPICalled -> el `type` del evento (log/info/warning/error/...)
// - Runtime.exceptionThrown -> "exception" (texto = descripcion + stack)
// - Log.entryAdded -> el `level` del entry (warning/error del browser)
//
// Robusta ante silencio: si no llega ningun evento devuelve un slice vacio
// (no nil, no error). La conexion debe estar abierta; la funcion no la cierra.
func CdpCollectConsole(c *CDPConn, durationMs int) ([]ConsoleEntry, error) {
if c == nil {
return nil, fmt.Errorf("cdp collect console: conexion nula")
}
if durationMs <= 0 {
durationMs = 1500
}
var (
mu sync.Mutex
entries = make([]ConsoleEntry, 0, 16)
)
// Helpers para extraer campos de map[string]any sin pelearse con cast.
str := func(m map[string]any, k string) string {
if v, ok := m[k]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
num := func(m map[string]any, k string) float64 {
if v, ok := m[k]; ok {
if f, ok := v.(float64); ok {
return f
}
}
return 0
}
// argToText convierte un RemoteObject de Runtime a una representacion legible.
// Para primitivas usa `value`; para objetos sin value cae a `description` o
// `unserializableValue`; ultimo recurso, el `type`.
argToText := func(arg map[string]any) string {
if v, ok := arg["value"]; ok && v != nil {
if s, ok := v.(string); ok {
return s
}
// objetos/arrays serializados por valor -> JSON real.
if b, err := json.Marshal(v); err == nil {
return string(b)
}
return fmt.Sprintf("%v", v)
}
if d := str(arg, "description"); d != "" {
return d
}
if u := str(arg, "unserializableValue"); u != "" {
return u
}
return str(arg, "type")
}
// --- Runtime.consoleAPICalled: console.log / info / warn / error / ... ---
cancel1 := c.OnEvent("Runtime.consoleAPICalled", func(_ string, p map[string]any) {
entry := ConsoleEntry{
Type: str(p, "type"),
Timestamp: num(p, "timestamp"),
}
// Concatenar los args a un texto legible separado por espacios.
if rawArgs, ok := p["args"].([]any); ok {
parts := make([]string, 0, len(rawArgs))
for _, ra := range rawArgs {
if am, ok := ra.(map[string]any); ok {
parts = append(parts, argToText(am))
}
}
entry.Text = strings.Join(parts, " ")
}
// stackTrace -> primer frame para URL/linea.
if st, ok := p["stackTrace"].(map[string]any); ok {
if frames, ok := st["callFrames"].([]any); ok && len(frames) > 0 {
if f0, ok := frames[0].(map[string]any); ok {
entry.URL = str(f0, "url")
// lineNumber es 0-based en CDP; +1 para ser 1-based legible.
if ln := int(num(f0, "lineNumber")); ln >= 0 {
entry.Line = ln + 1
}
}
}
}
mu.Lock()
entries = append(entries, entry)
mu.Unlock()
})
defer cancel1()
// --- Runtime.exceptionThrown: errores JS no capturados ---
cancel2 := c.OnEvent("Runtime.exceptionThrown", func(_ string, p map[string]any) {
entry := ConsoleEntry{
Type: "exception",
Timestamp: num(p, "timestamp"),
}
ed, _ := p["exceptionDetails"].(map[string]any)
if ed != nil {
// Texto base de la excepcion.
text := str(ed, "text")
// Si hay un objeto de excepcion con descripcion (stack completo), preferirlo.
if exc, ok := ed["exception"].(map[string]any); ok {
if desc := str(exc, "description"); desc != "" {
if text != "" && !strings.Contains(desc, text) {
text = text + ": " + desc
} else {
text = desc
}
}
}
entry.Text = text
entry.URL = str(ed, "url")
// lineNumber 0-based -> 1-based.
if ln := int(num(ed, "lineNumber")); ln >= 0 {
entry.Line = ln + 1
}
// stackTrace top frame como respaldo de URL/linea.
if entry.URL == "" {
if st, ok := ed["stackTrace"].(map[string]any); ok {
if frames, ok := st["callFrames"].([]any); ok && len(frames) > 0 {
if f0, ok := frames[0].(map[string]any); ok {
entry.URL = str(f0, "url")
if entry.Line == 0 {
if ln := int(num(f0, "lineNumber")); ln >= 0 {
entry.Line = ln + 1
}
}
}
}
}
}
}
if entry.Text == "" {
entry.Text = "uncaught exception"
}
mu.Lock()
entries = append(entries, entry)
mu.Unlock()
})
defer cancel2()
// --- Log.entryAdded: avisos del propio navegador (network, security...) ---
cancel3 := c.OnEvent("Log.entryAdded", func(_ string, p map[string]any) {
le, _ := p["entry"].(map[string]any)
if le == nil {
return
}
entry := ConsoleEntry{
Type: str(le, "level"), // verbose|info|warning|error
Text: str(le, "text"),
URL: str(le, "url"),
Line: int(num(le, "lineNumber")),
Timestamp: num(le, "timestamp"),
}
mu.Lock()
entries = append(entries, entry)
mu.Unlock()
})
defer cancel3()
// Habilitar dominios. Runtime.enable provoca un flush de consoleAPICalled
// rezagados; Log.enable abre el stream de avisos del navegador.
if _, err := c.sendCDP("Runtime.enable", nil); err != nil {
return nil, fmt.Errorf("cdp collect console: Runtime.enable: %w", err)
}
if _, err := c.sendCDP("Log.enable", nil); err != nil {
// Log.enable puede no estar disponible en algunos targets; no es fatal,
// seguimos capturando Runtime.*. Deshabilitar Runtime no hace falta.
_ = err
}
// No deshabilitamos Runtime al salir: otras funciones (ej. cdp_pick_element_js)
// dependen de consoleAPICalled. Solo cerramos Log que abrimos aqui.
defer c.sendCDP("Log.disable", nil)
// Ventana de captura.
time.Sleep(time.Duration(durationMs) * time.Millisecond)
mu.Lock()
out := make([]ConsoleEntry, len(entries))
copy(out, entries)
mu.Unlock()
return out, nil
}