Initial commit: navegator - Chrome CDP automation for LLMs
Tests / Lint (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / E2E Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled

Add complete navegator system for stealthy browser automation:
- CDP client with WebSocket communication
- Browser API with navigation, storage, network, runtime
- Stealth flags and anti-detection scripts
- Persistent profile support
- Examples and comprehensive documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-24 23:33:07 +01:00
commit 3253828fef
36 changed files with 8116 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"time"
"navegator/pkg/browser"
)
// Resultado representa un resultado de búsqueda
type Resultado struct {
Titulo string `json:"titulo"`
URL string `json:"url"`
Descripcion string `json:"descripcion"`
}
func main() {
// Definir flags/parámetros
query := flag.String("q", "", "Consulta de búsqueda (requerido)")
maxResults := flag.Int("n", 10, "Número máximo de resultados (default: 10)")
headless := flag.Bool("headless", true, "Modo headless (default: true)")
outputJSON := flag.String("output", "", "Guardar resultados en archivo JSON")
profileName := flag.String("profile", "search-bot", "Nombre del perfil a usar")
flag.Parse()
// Validar que se proporcionó la consulta
if *query == "" {
fmt.Println("Error: debes proporcionar una consulta con -q")
fmt.Println("\nEjemplo:")
fmt.Println(" ./buscar -q \"golang tutorial\" -n 20")
fmt.Println("\nOpciones:")
flag.PrintDefaults()
os.Exit(1)
}
ctx := context.Background()
// Configurar navegador
currentDir, _ := os.Getwd()
profilesDir := filepath.Join(currentDir, "perfiles")
config := browser.DefaultConfig()
config.ProfilesBaseDir = profilesDir
config.ProfileName = *profileName
config.StealthFlags.Headless = *headless
config.StealthFlags.WindowSize = [2]int{1280, 720}
log.Printf("🔍 Buscando: %s", *query)
log.Printf("📊 Máximo de resultados: %d", *maxResults)
log.Printf("👤 Usando perfil: %s", *profileName)
// Lanzar navegador
b, err := browser.Launch(ctx, config)
if err != nil {
log.Fatalf("❌ Error al lanzar navegador: %v", err)
}
defer b.Close()
// Navegar a DuckDuckGo (más amigable para bots que Google)
searchURL := fmt.Sprintf("https://duckduckgo.com/?q=%s", *query)
log.Println("🌐 Navegando a DuckDuckGo...")
if err := b.Navigate(ctx, searchURL, nil); err != nil {
log.Fatalf("❌ Error al navegar: %v", err)
}
// Esperar a que carguen los resultados
time.Sleep(3 * time.Second)
log.Println("📥 Extrayendo resultados...")
// Script para extraer resultados
extractScript := fmt.Sprintf(`
(() => {
const results = [];
const maxResults = %d;
// DuckDuckGo usa article con data-testid="result"
const items = document.querySelectorAll('article[data-testid="result"]');
for (let i = 0; i < Math.min(items.length, maxResults); i++) {
const item = items[i];
// Título
const titleEl = item.querySelector('h2 a');
const titulo = titleEl ? titleEl.textContent : '';
const url = titleEl ? titleEl.href : '';
// Descripción
const descEl = item.querySelector('[data-result="snippet"]');
const descripcion = descEl ? descEl.textContent : '';
if (titulo && url) {
results.push({
titulo: titulo.trim(),
url: url,
descripcion: descripcion.trim()
});
}
}
return results;
})()
`, *maxResults)
result, err := b.Evaluate(ctx, extractScript)
if err != nil {
log.Fatalf("❌ Error al extraer resultados: %v", err)
}
// Parsear resultados
resultadosJSON, err := json.Marshal(result.Value)
if err != nil {
log.Fatalf("❌ Error al parsear resultados: %v", err)
}
var resultados []Resultado
if err := json.Unmarshal(resultadosJSON, &resultados); err != nil {
log.Fatalf("❌ Error al deserializar: %v", err)
}
// Mostrar resultados
log.Printf("\n✅ Encontrados %d resultados:\n", len(resultados))
for i, r := range resultados {
fmt.Printf("\n%d. %s\n", i+1, r.Titulo)
fmt.Printf(" 🔗 %s\n", r.URL)
if r.Descripcion != "" {
fmt.Printf(" 📝 %s\n", r.Descripcion)
}
}
// Guardar en JSON si se especificó
if *outputJSON != "" {
data, _ := json.MarshalIndent(resultados, "", " ")
if err := os.WriteFile(*outputJSON, data, 0644); err != nil {
log.Printf("⚠️ Error al guardar JSON: %v", err)
} else {
log.Printf("\n💾 Resultados guardados en: %s", *outputJSON)
}
}
log.Println("\n✨ Búsqueda completada!")
}
+166
View File
@@ -0,0 +1,166 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"time"
"navegator/pkg/browser"
)
// Resultado representa un resultado de búsqueda
type Resultado struct {
Titulo string `json:"titulo"`
URL string `json:"url"`
Descripcion string `json:"descripcion"`
}
func main() {
// Definir flags/parámetros
query := flag.String("q", "", "Consulta de búsqueda (requerido)")
maxResults := flag.Int("n", 10, "Número máximo de resultados (default: 10)")
headless := flag.Bool("headless", true, "Modo headless (default: true)")
outputJSON := flag.String("output", "", "Guardar resultados en archivo JSON")
profileName := flag.String("profile", "search-bot", "Nombre del perfil a usar")
// NUEVO: Directorio de perfiles compartido
profilesDir := flag.String("profiles-dir", "", "Directorio de perfiles (default: ~/.navegator/profiles)")
flag.Parse()
// Validar que se proporcionó la consulta
if *query == "" {
fmt.Println("Error: debes proporcionar una consulta con -q")
fmt.Println("\nEjemplo:")
fmt.Println(" ./buscar -q \"golang tutorial\" -n 20")
fmt.Println("\nOpciones:")
flag.PrintDefaults()
os.Exit(1)
}
ctx := context.Background()
// Determinar directorio de perfiles
var perfilesPath string
if *profilesDir != "" {
perfilesPath = *profilesDir
} else {
// Default: ~/.navegator/profiles
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("❌ Error al obtener home dir: %v", err)
}
perfilesPath = filepath.Join(homeDir, ".navegator", "profiles")
}
// Crear directorio si no existe
if err := os.MkdirAll(perfilesPath, 0755); err != nil {
log.Fatalf("❌ Error al crear directorio de perfiles: %v", err)
}
config := browser.DefaultConfig()
config.ProfilesBaseDir = perfilesPath
config.ProfileName = *profileName
config.StealthFlags.Headless = *headless
config.StealthFlags.WindowSize = [2]int{1280, 720}
log.Printf("🔍 Buscando: %s", *query)
log.Printf("📊 Máximo de resultados: %d", *maxResults)
log.Printf("👤 Usando perfil: %s", *profileName)
log.Printf("📂 Perfiles en: %s", perfilesPath)
// Lanzar navegador
b, err := browser.Launch(ctx, config)
if err != nil {
log.Fatalf("❌ Error al lanzar navegador: %v", err)
}
defer b.Close()
// Navegar a DuckDuckGo
searchURL := fmt.Sprintf("https://duckduckgo.com/?q=%s", *query)
log.Println("🌐 Navegando a DuckDuckGo...")
if err := b.Navigate(ctx, searchURL, nil); err != nil {
log.Fatalf("❌ Error al navegar: %v", err)
}
// Esperar a que carguen los resultados
time.Sleep(3 * time.Second)
log.Println("📥 Extrayendo resultados...")
// Script para extraer resultados
extractScript := fmt.Sprintf(`
(() => {
const results = [];
const maxResults = %d;
const items = document.querySelectorAll('article[data-testid="result"]');
for (let i = 0; i < Math.min(items.length, maxResults); i++) {
const item = items[i];
const titleEl = item.querySelector('h2 a');
const titulo = titleEl ? titleEl.textContent : '';
const url = titleEl ? titleEl.href : '';
const descEl = item.querySelector('[data-result="snippet"]');
const descripcion = descEl ? descEl.textContent : '';
if (titulo && url) {
results.push({
titulo: titulo.trim(),
url: url,
descripcion: descripcion.trim()
});
}
}
return results;
})()
`, *maxResults)
result, err := b.Evaluate(ctx, extractScript)
if err != nil {
log.Fatalf("❌ Error al extraer resultados: %v", err)
}
// Parsear resultados
resultadosJSON, err := json.Marshal(result.Value)
if err != nil {
log.Fatalf("❌ Error al parsear resultados: %v", err)
}
var resultados []Resultado
if err := json.Unmarshal(resultadosJSON, &resultados); err != nil {
log.Fatalf("❌ Error al deserializar: %v", err)
}
// Mostrar resultados
log.Printf("\n✅ Encontrados %d resultados:\n", len(resultados))
for i, r := range resultados {
fmt.Printf("\n%d. %s\n", i+1, r.Titulo)
fmt.Printf(" 🔗 %s\n", r.URL)
if r.Descripcion != "" {
fmt.Printf(" 📝 %s\n", r.Descripcion)
}
}
// Guardar en JSON si se especificó
if *outputJSON != "" {
data, _ := json.MarshalIndent(resultados, "", " ")
if err := os.WriteFile(*outputJSON, data, 0644); err != nil {
log.Printf("⚠️ Error al guardar JSON: %v", err)
} else {
log.Printf("\n💾 Resultados guardados en: %s", *outputJSON)
}
}
log.Println("\n✨ Búsqueda completada!")
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"time"
"navegator/pkg/browser"
)
func main() {
// Parámetros
url := flag.String("url", "", "URL a visitar (requerido)")
profile := flag.String("profile", "user-default", "Perfil de navegador a usar")
headless := flag.Bool("headless", false, "Modo headless (default: false para ver la navegación)")
duration := flag.Int("duration", 10, "Segundos que mantener abierto el navegador")
click := flag.String("click", "", "Selector CSS para hacer click (opcional)")
type_ := flag.String("type", "", "Selector CSS donde escribir (opcional)")
text := flag.String("text", "", "Texto a escribir (requiere -type)")
flag.Parse()
if *url == "" {
fmt.Println("Error: debes proporcionar una URL con -url")
fmt.Println("\nEjemplo básico:")
fmt.Println(" ./navegar -url https://example.com -profile usuario1")
fmt.Println("\nEjemplo con interacción:")
fmt.Println(" ./navegar -url https://example.com -click 'a[href]' -duration 30")
fmt.Println("\nEjemplo con formulario:")
fmt.Println(" ./navegar -url https://httpbin.org/forms/post -type 'input[name=\"custname\"]' -text 'Juan Pérez'")
fmt.Println("\nOpciones:")
flag.PrintDefaults()
os.Exit(1)
}
ctx := context.Background()
// Configurar navegador
currentDir, _ := os.Getwd()
profilesDir := filepath.Join(currentDir, "perfiles")
config := browser.DefaultConfig()
config.ProfilesBaseDir = profilesDir
config.ProfileName = *profile
config.StealthFlags.Headless = *headless
config.StealthFlags.WindowSize = [2]int{1280, 720}
log.Printf("🌐 Navegando: %s", *url)
log.Printf("👤 Perfil: %s", *profile)
log.Printf("⏱️ Duración: %d segundos", *duration)
// Lanzar navegador
b, err := browser.Launch(ctx, config)
if err != nil {
log.Fatalf("❌ Error: %v", err)
}
defer b.Close()
// Iniciar recording
recordingFile := filepath.Join(currentDir, fmt.Sprintf("recording_%s.log", *profile))
if err := b.StartRecording(recordingFile); err != nil {
log.Printf("⚠️ Recording desactivado: %v", err)
} else {
log.Printf("📝 Recording: %s", recordingFile)
}
b.AddComment(fmt.Sprintf("=== Sesión de %s ===", *profile))
// Navegar
opts := browser.DefaultNavigateOptions()
opts.Timeout = 30 * time.Second
if err := b.Navigate(ctx, *url, opts); err != nil {
log.Printf("⚠️ Advertencia: %v", err)
} else {
log.Println("✅ Página cargada")
}
time.Sleep(2 * time.Second)
// Click si se especificó
if *click != "" {
b.AddComment(fmt.Sprintf("Click en: %s", *click))
log.Printf("🖱️ Haciendo click en: %s", *click)
if err := b.Click(ctx, *click); err != nil {
log.Printf("⚠️ Error al hacer click: %v", err)
} else {
log.Println("✅ Click realizado")
time.Sleep(2 * time.Second)
}
}
// Type si se especificó
if *type_ != "" && *text != "" {
b.AddComment(fmt.Sprintf("Escribiendo en: %s", *type_))
log.Printf("⌨️ Escribiendo '%s' en: %s", *text, *type_)
if err := b.Type(ctx, *type_, *text, nil); err != nil {
log.Printf("⚠️ Error al escribir: %v", err)
} else {
log.Println("✅ Texto escrito")
time.Sleep(2 * time.Second)
}
}
// Obtener información de la página
title, _ := b.Evaluate(ctx, "document.title")
log.Printf("📄 Título: %v", title.Value)
currentURL, _ := b.Evaluate(ctx, "window.location.href")
log.Printf("🔗 URL actual: %v", currentURL.Value)
// Mantener navegador abierto
log.Printf("\n⏳ Manteniendo navegador abierto por %d segundos...", *duration)
time.Sleep(time.Duration(*duration) * time.Second)
b.AddComment("Sesión finalizada")
log.Println("✨ Completado!")
}
+78
View File
@@ -0,0 +1,78 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"navegator/pkg/browser"
)
func main() {
// Parámetros
url := flag.String("url", "", "URL a capturar (requerido)")
output := flag.String("o", "screenshot.png", "Archivo de salida")
profile := flag.String("profile", "screenshot-bot", "Perfil de navegador a usar")
headless := flag.Bool("headless", true, "Modo headless")
fullPage := flag.Bool("full", false, "Captura página completa")
width := flag.Int("width", 1280, "Ancho de ventana")
height := flag.Int("height", 720, "Alto de ventana")
flag.Parse()
if *url == "" {
fmt.Println("Error: debes proporcionar una URL con -url")
fmt.Println("\nEjemplo:")
fmt.Println(" ./screenshot -url https://example.com -o captura.png")
fmt.Println("\nOpciones:")
flag.PrintDefaults()
os.Exit(1)
}
ctx := context.Background()
// Configurar navegador
currentDir, _ := os.Getwd()
profilesDir := filepath.Join(currentDir, "perfiles")
config := browser.DefaultConfig()
config.ProfilesBaseDir = profilesDir
config.ProfileName = *profile
config.StealthFlags.Headless = *headless
config.StealthFlags.WindowSize = [2]int{*width, *height}
log.Printf("📸 Capturando: %s", *url)
log.Printf("👤 Usando perfil: %s", *profile)
// Lanzar navegador
b, err := browser.Launch(ctx, config)
if err != nil {
log.Fatalf("❌ Error: %v", err)
}
defer b.Close()
// Navegar
opts := browser.DefaultNavigateOptions()
opts.Timeout = 15 * 1000000000 // 15 segundos
if err := b.Navigate(ctx, *url, opts); err != nil {
log.Printf("⚠️ Timeout en navegación, pero continuando...")
}
// Tomar screenshot
log.Println("📷 Capturando pantalla...")
screenshot, err := b.Screenshot(ctx, *fullPage)
if err != nil {
log.Fatalf("❌ Error al capturar: %v", err)
}
// Guardar
if err := os.WriteFile(*output, screenshot, 0644); err != nil {
log.Fatalf("❌ Error al guardar: %v", err)
}
log.Printf("✅ Screenshot guardado: %s (%d bytes)", *output, len(screenshot))
}