bb38eedfd1
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>
215 lines
6.7 KiB
Go
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")
|
|
}
|
|
}
|