Files
fn_registry/apps/script_navegador/main.go
T
egutierrez bb38eedfd1 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>
2026-03-30 14:24:39 +02:00

215 lines
6.7 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"fn-registry/functions/infra"
)
func main() {
// Flags
scriptPath := flag.String("script", "", "Ruta al archivo YAML con el script de navegacion (obligatorio)")
port := flag.Int("port", 9222, "Puerto CDP de Chrome")
launch := flag.Bool("launch", false, "Lanzar Chrome nuevo en vez de conectarse a uno existente")
headless := flag.Bool("headless", false, "Lanzar Chrome en modo headless (requiere --launch)")
chromePath := flag.String("chrome-path", "", "Ruta al ejecutable de Chrome (ej: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe')")
userDataDir := flag.String("user-data-dir", "", "Directorio de perfil de Chrome (path WSL, se convierte a Windows automaticamente)")
keepOpen := flag.Bool("keep-open", false, "No cerrar Chrome al terminar")
abortOnError := flag.Bool("abort-on-error", false, "Abortar el script al primer error en cualquier paso")
flag.Parse()
if *scriptPath == "" {
fmt.Fprintln(os.Stderr, "error: --script es obligatorio")
flag.Usage()
os.Exit(1)
}
if err := run(*scriptPath, *port, *launch, *headless, *abortOnError, *userDataDir, *keepOpen, *chromePath); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func run(scriptPath string, port int, launch, headless, abortOnError bool, userDataDir string, keepOpen bool, chromePath string) error {
// 1. Cargar y validar el script YAML
script, err := LoadScript(scriptPath)
if err != nil {
return fmt.Errorf("cargar script: %w", err)
}
fmt.Printf("[script_navegador] script: %q (%d pasos)\n", script.Name, len(script.Steps))
// 2. Inicializar operations.db
appDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
// Fallback al directorio de trabajo
appDir, _ = os.Getwd()
}
// Si estamos corriendo con `go run .`, os.Args[0] es un tmp, usar cwd
if cwd, e := os.Getwd(); e == nil {
if _, e2 := os.Stat(filepath.Join(cwd, "app.md")); e2 == nil {
appDir = cwd
}
}
db, err := initOpsDB(appDir)
if err != nil {
// No es fatal: seguir sin operations.db, solo logear
fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudo inicializar operations.db: %v\n", err)
}
if db != nil {
defer db.Close()
}
// 3. Lanzar Chrome o conectarse al existente
var pid int
if launch {
// Convertir path WSL a Windows para chrome.exe
// Si empieza con / es un path Linux (WSL), convertir. Si empieza con letra:\ ya es Windows.
winDataDir := userDataDir
if winDataDir != "" && strings.HasPrefix(winDataDir, "/") {
out, err := exec.Command("wslpath", "-w", winDataDir).Output()
if err == nil {
winDataDir = strings.TrimSpace(string(out))
}
}
fmt.Printf("[chrome] lanzando Chrome en puerto %d (headless=%v, user-data-dir=%q)...\n", port, headless, winDataDir)
pid, err = infra.ChromeLaunch(infra.ChromeLaunchOpts{
Port: port,
Headless: headless,
UserDataDir: winDataDir,
ChromePath: chromePath,
})
if err != nil {
return fmt.Errorf("lanzar Chrome: %w", err)
}
fmt.Printf("[chrome] Chrome lanzado (pid=%d)\n", pid)
} else {
fmt.Printf("[chrome] conectando a Chrome en localhost:%d...\n", port)
}
// 4. Conectar CDP (con mirrored networking, localhost es compartido WSL<->Windows)
fmt.Printf("[cdp] conectando a localhost:%d...\n", port)
conn, err := infra.CdpConnect(port)
if err != nil {
// Si lanzamos Chrome, matar el proceso antes de salir
if pid > 0 {
_ = infra.CdpClose(nil, pid)
}
return fmt.Errorf("conectar CDP en localhost:%d: %w", port, err)
}
fmt.Printf("[cdp] conexion establecida\n")
// Asegurar cierre al salir (respetar --keep-open)
defer func() {
if keepOpen {
fmt.Printf("[cdp] cerrando conexion CDP (Chrome sigue abierto, pid=%d, puerto=%d)\n", pid, port)
// Solo cerrar la conexion WebSocket, no matar Chrome
if err := infra.CdpClose(conn, 0); err != nil {
fmt.Fprintf(os.Stderr, "[cdp] aviso al cerrar conexion: %v\n", err)
}
} else {
fmt.Printf("[cdp] cerrando conexion y limpiando recursos...\n")
if err := infra.CdpClose(conn, pid); err != nil {
fmt.Fprintf(os.Stderr, "[cdp] aviso al cerrar: %v\n", err)
}
}
}()
// 5. Registrar entities y relations en operations.db
var relationID string
if db != nil {
_, _, _, err := EnsureEntities(db, port, chromePath, userDataDir, script.Name, scriptPath)
if err != nil {
fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudieron crear entities: %v\n", err)
} else {
fmt.Printf("[ops] entities registradas\n")
}
relationID, err = EnsureRelations(db, "chrome_instance", "cdp_session", fmt.Sprintf("script_%s", script.Name))
if err != nil {
fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudieron crear relations: %v\n", err)
} else {
fmt.Printf("[ops] relations registradas\n")
}
}
// 6. Ejecutar el script
runner := NewRunner(conn, RunnerOpts{AbortOnError: abortOnError})
startedAt := time.Now()
fmt.Printf("[run] iniciando ejecucion: %s\n", startedAt.Format(time.RFC3339))
results, runErr := runner.Run(script)
endedAt := time.Now()
// 7. Imprimir resumen de pasos
printSummary(script, results, runErr, startedAt, endedAt)
// 8. Registrar execution y actualizar relation en operations.db
if db != nil {
execID, err := RecordRun(db, script, relationID, results, runErr, startedAt, endedAt)
if err != nil {
fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudo registrar ejecucion: %v\n", err)
} else {
fmt.Printf("[ops] ejecucion registrada en operations.db (id=%s)\n", execID[:8])
}
// Registrar cada paso como log
for _, r := range results {
if logErr := LogStep(db, execID, r); logErr != nil {
fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudo registrar log step[%d]: %v\n", r.Index, logErr)
}
}
// Actualizar relation status
if relationID != "" {
UpdateRelationAfterRun(db, relationID, runErr)
}
}
if runErr != nil {
return runErr
}
return nil
}
// printSummary imprime un resumen legible de la ejecucion.
func printSummary(script *Script, results []StepResult, runErr error, startedAt, endedAt time.Time) {
duration := endedAt.Sub(startedAt)
success := 0
for _, r := range results {
if r.Err == nil {
success++
}
}
fmt.Printf("\n--- Resumen: %q ---\n", script.Name)
fmt.Printf("Duracion: %v\n", duration.Round(time.Millisecond))
fmt.Printf("Pasos: %d/%d exitosos\n", success, len(results))
fmt.Println()
for _, r := range results {
status := "ok"
detail := ""
if r.Err != nil {
status = "ERROR"
detail = fmt.Sprintf(" -> %v", r.Err)
} else if r.Output != "" {
detail = fmt.Sprintf(" -> %q", r.Output)
}
fmt.Printf(" [%d] %-12s %s (%dms)%s\n",
r.Index, r.Action, status, r.Elapsed.Milliseconds(), detail)
}
if runErr != nil {
fmt.Printf("\nAbortado: %v\n", runErr)
} else {
fmt.Printf("\nScript completado.\n")
}
}