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:
@@ -0,0 +1,214 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user