feat: app script_navegador y dashboard Metabase

App Go para ejecutar scripts de navegación automatizada usando las
funciones CDP del registry. Incluye script de creación de dashboard
en Metabase para monitoreo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 14:24:39 +02:00
parent 9ed0f2e16f
commit 3c250a9252
15 changed files with 1322 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
package main
import (
"fmt"
"time"
"fn-registry/functions/infra"
)
// StepResult es el resultado de ejecutar un paso.
type StepResult struct {
Index int
Action string
Output string // resultado de evaluate/get_html, path de screenshot, etc.
Err error
Elapsed time.Duration
}
// RunnerOpts configura la ejecucion del runner.
type RunnerOpts struct {
AbortOnError bool
}
// Runner ejecuta los pasos de un Script sobre una conexion CDP activa.
type Runner struct {
conn *infra.CDPConn
opts RunnerOpts
}
// NewRunner crea un Runner con la conexion CDP dada.
func NewRunner(conn *infra.CDPConn, opts RunnerOpts) *Runner {
return &Runner{conn: conn, opts: opts}
}
// Run ejecuta todos los pasos del script y retorna los resultados de cada paso.
// Siempre retorna todos los resultados procesados hasta el momento, incluso si aborta.
func (r *Runner) Run(script *Script) ([]StepResult, error) {
results := make([]StepResult, 0, len(script.Steps))
for i, step := range script.Steps {
start := time.Now()
output, err := r.runStep(step)
elapsed := time.Since(start)
res := StepResult{
Index: i,
Action: step.Action,
Output: output,
Err: err,
Elapsed: elapsed,
}
results = append(results, res)
if err != nil {
if step.ContinueOnError {
// Continuar con el siguiente paso aunque este fallo
continue
}
if r.opts.AbortOnError {
return results, fmt.Errorf("step[%d] %s: %w", i, step.Action, err)
}
// Por defecto: abortar si el paso fallo y no tiene continue_on_error
return results, fmt.Errorf("step[%d] %s: %w", i, step.Action, err)
}
}
return results, nil
}
// runStep ejecuta un paso individual y retorna su output y error.
func (r *Runner) runStep(step Step) (string, error) {
switch step.Action {
case "navigate":
if err := infra.CdpNavigate(r.conn, step.URL); err != nil {
return "", err
}
// Esperar a que la página cargue completamente
timeout := time.Duration(step.TimeoutMs) * time.Millisecond
if timeout <= 0 {
timeout = 15 * time.Second
}
return "", infra.CdpWaitLoad(r.conn, timeout)
case "wait_load":
timeout := time.Duration(step.TimeoutMs) * time.Millisecond
if timeout <= 0 {
timeout = 15 * time.Second
}
return "", infra.CdpWaitLoad(r.conn, timeout)
case "wait":
timeout := time.Duration(step.TimeoutMs) * time.Millisecond
if timeout <= 0 {
timeout = 10 * time.Second
}
return "", infra.CdpWaitElement(r.conn, step.Selector, timeout)
case "click":
return "", infra.CdpClick(r.conn, step.Selector)
case "type":
// Hacer click primero para enfocar el elemento
if err := infra.CdpClick(r.conn, step.Selector); err != nil {
return "", fmt.Errorf("enfocar elemento para type: %w", err)
}
return "", infra.CdpTypeText(r.conn, step.Text)
case "screenshot":
opts := infra.CdpScreenshotOpts{
FullPage: step.FullPage,
Format: "png",
}
if err := infra.CdpScreenshot(r.conn, step.Path, opts); err != nil {
return "", err
}
return step.Path, nil
case "evaluate":
result, err := infra.CdpEvaluate(r.conn, step.Expr)
if err != nil {
return "", err
}
return result, nil
case "get_html":
html, err := infra.CdpGetHTML(r.conn)
if err != nil {
return "", err
}
// Truncar para el log (el HTML puede ser muy largo)
if len(html) > 200 {
return html[:200] + "...", nil
}
return html, nil
case "sleep":
time.Sleep(time.Duration(step.Ms) * time.Millisecond)
return fmt.Sprintf("slept %dms", step.Ms), nil
default:
return "", fmt.Errorf("accion desconocida: %q", step.Action)
}
}