chore: auto-commit (97 archivos)
- .claude/CLAUDE.md - .claude/agents/fn-recopilador/SKILL.md - .claude/rules/INDEX.md - .claude/rules/cpp_apps.md - bash/functions/infra/build_cpp_windows.sh - cpp/CMakeLists.txt - cpp/PATTERNS.md - cpp/framework/app_base.cpp - cpp/framework/app_base.h - dev/issues/README.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// CdpTab representa una pestaña/target devuelta por el endpoint /json de CDP.
|
||||
// Campos publicos para que apps consumidoras puedan filtrar/render.
|
||||
type CdpTab struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "page", "iframe", "service_worker", ...
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
|
||||
DevtoolsFrontendURL string `json:"devtoolsFrontendUrl,omitempty"`
|
||||
}
|
||||
|
||||
// CdpListTabs llama GET http://{host}:{port}/json y retorna la lista de
|
||||
// targets. Sin filtrar por tipo — el caller decide si se queda solo con
|
||||
// type=="page", incluye iframes, etc.
|
||||
//
|
||||
// host vacio = "localhost". No requiere websocket (CDP expone /json en HTTP).
|
||||
func CdpListTabs(host string, port int) ([]CdpTab, error) {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s:%d/json", host, port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdp list tabs: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("cdp list tabs: status %d", resp.StatusCode)
|
||||
}
|
||||
var tabs []CdpTab
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tabs); err != nil {
|
||||
return nil, fmt.Errorf("cdp list tabs: decode: %w", err)
|
||||
}
|
||||
return tabs, nil
|
||||
}
|
||||
|
||||
// CdpNewTab abre una pestaña nueva via PUT /json/new?<startURL>. Si startURL
|
||||
// es vacio Chrome abre about:blank. Retorna el CdpTab recien creado.
|
||||
//
|
||||
// Nota: desde Chrome 126 /json/new requiere PUT (no GET). Mantenemos el
|
||||
// fallback a GET por compatibilidad con builds antiguos.
|
||||
func CdpNewTab(host string, port int, startURL string) (CdpTab, error) {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
endpoint := fmt.Sprintf("http://%s:%d/json/new", host, port)
|
||||
if startURL != "" {
|
||||
endpoint += "?" + url.QueryEscape(startURL)
|
||||
}
|
||||
|
||||
tryRequest := func(method string) (CdpTab, error) {
|
||||
var out CdpTab
|
||||
req, err := http.NewRequest(method, endpoint, nil)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return out, fmt.Errorf("status %d", resp.StatusCode)
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return out, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
tab, err := tryRequest(http.MethodPut)
|
||||
if err == nil && tab.ID != "" {
|
||||
return tab, nil
|
||||
}
|
||||
// Fallback GET (Chrome < 126).
|
||||
tab, err2 := tryRequest(http.MethodGet)
|
||||
if err2 == nil && tab.ID != "" {
|
||||
return tab, nil
|
||||
}
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return CdpTab{}, fmt.Errorf("cdp new tab: %w", err)
|
||||
}
|
||||
|
||||
// CdpCloseTab cierra una pestaña por su ID via GET /json/close/<id>.
|
||||
// Util complemento — incluido aqui porque comparte estructura HTTP /json.
|
||||
func CdpCloseTab(host string, port int, tabID string) error {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if tabID == "" {
|
||||
return fmt.Errorf("cdp close tab: tabID vacio")
|
||||
}
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s:%d/json/close/%s", host, port, url.PathEscape(tabID)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp close tab: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("cdp close tab: status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CdpActivateTab pone la pestaña en foreground (focus) via /json/activate/<id>.
|
||||
func CdpActivateTab(host string, port int, tabID string) error {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if tabID == "" {
|
||||
return fmt.Errorf("cdp activate tab: tabID vacio")
|
||||
}
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s:%d/json/activate/%s", host, port, url.PathEscape(tabID)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp activate tab: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("cdp activate tab: status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user