feat(browser): auto-commit con 60 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,75 +3,119 @@ package browser
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CdpEvalInFrame ejecuta una expresion JavaScript en el contexto aislado de un iframe
|
||||
// especifico usando Page.createIsolatedWorld + Runtime.evaluate con el contextId del frame.
|
||||
// Retorna el resultado serializado como string.
|
||||
func CdpEvalInFrame(c *CDPConn, frameID, expression string) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: conexion nula")
|
||||
}
|
||||
if frameID == "" {
|
||||
return "", fmt.Errorf("cdp eval in frame: frameID vacio")
|
||||
}
|
||||
// frameCtxCache mapea frameID -> executionContextId del isolated world creado
|
||||
// para ese frame. Evita pagar Page.createIsolatedWorld en cada CdpEvalInFrame.
|
||||
// Es puro y testeable de forma aislada (su propio mutex, sin tocar CDP).
|
||||
type frameCtxCache struct {
|
||||
mu sync.Mutex
|
||||
m map[string]int
|
||||
}
|
||||
|
||||
// Page.enable es idempotente; necesario antes de crear mundos aislados
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: Page.enable: %w", err)
|
||||
}
|
||||
func newFrameCtxCache() *frameCtxCache {
|
||||
return &frameCtxCache{m: map[string]int{}}
|
||||
}
|
||||
|
||||
// Crear un mundo aislado en el frame indicado para no contaminar su contexto JS
|
||||
func (f *frameCtxCache) get(frameID string) (int, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
id, ok := f.m[frameID]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func (f *frameCtxCache) set(frameID string, ctxID int) {
|
||||
f.mu.Lock()
|
||||
f.m[frameID] = ctxID
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *frameCtxCache) invalidate(frameID string) {
|
||||
f.mu.Lock()
|
||||
delete(f.m, frameID)
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// isStaleContextError reconoce el error de CDP cuando un executionContextId
|
||||
// cacheado ya no existe (el frame recargó/navegó y su isolated world murió). Es
|
||||
// puro: decide a partir del texto del error. Permite reintentar recreando el
|
||||
// mundo en vez de fallar.
|
||||
func isStaleContextError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s := err.Error()
|
||||
return strings.Contains(s, "Cannot find context") ||
|
||||
strings.Contains(s, "context with specified id") ||
|
||||
strings.Contains(s, "Execution context was destroyed") ||
|
||||
strings.Contains(s, "uniqueContextId")
|
||||
}
|
||||
|
||||
// frameCtxCacheLazy devuelve el cache de contextos del frame de esta conexion,
|
||||
// inicializandolo en el primer uso. El mutex de CDPConn solo protege este
|
||||
// lazy-init del puntero.
|
||||
func (c *CDPConn) frameCtxCacheLazy() *frameCtxCache {
|
||||
c.frameCtxMu.Lock()
|
||||
defer c.frameCtxMu.Unlock()
|
||||
if c.frameCtx == nil {
|
||||
c.frameCtx = newFrameCtxCache()
|
||||
}
|
||||
return c.frameCtx
|
||||
}
|
||||
|
||||
// createIsolatedWorld crea un mundo aislado en el frame y devuelve su
|
||||
// executionContextId.
|
||||
func createIsolatedWorld(c *CDPConn, frameID string) (int, error) {
|
||||
ctxRes, err := c.sendCDP("Page.createIsolatedWorld", map[string]any{
|
||||
"frameId": frameID,
|
||||
"worldName": "fn_registry_isolated",
|
||||
"grantUniveralAccess": false,
|
||||
"frameId": frameID,
|
||||
"worldName": "fn_registry_isolated",
|
||||
"grantUniversalAccess": false,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: createIsolatedWorld: %w", err)
|
||||
return 0, fmt.Errorf("createIsolatedWorld: %w", err)
|
||||
}
|
||||
|
||||
ctxIDRaw, ok := ctxRes["executionContextId"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cdp eval in frame: executionContextId no encontrado en respuesta")
|
||||
return 0, fmt.Errorf("createIsolatedWorld: executionContextId no encontrado en respuesta")
|
||||
}
|
||||
ctxID, ok := ctxIDRaw.(float64)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cdp eval in frame: executionContextId tipo inesperado: %T", ctxIDRaw)
|
||||
return 0, fmt.Errorf("createIsolatedWorld: executionContextId tipo inesperado: %T", ctxIDRaw)
|
||||
}
|
||||
return int(ctxID), nil
|
||||
}
|
||||
|
||||
// Evaluar la expresion en el contexto aislado del frame
|
||||
// evalInFrameContext ejecuta la expresion en el executionContextId dado y
|
||||
// serializa el resultado como string (mismo patron que CdpEvaluate).
|
||||
func evalInFrameContext(c *CDPConn, ctxID int, frameID, expression string) (string, error) {
|
||||
evRes, err := c.sendCDP("Runtime.evaluate", map[string]any{
|
||||
"expression": expression,
|
||||
"contextId": int(ctxID),
|
||||
"contextId": ctxID,
|
||||
"returnByValue": true,
|
||||
"awaitPromise": true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: Runtime.evaluate: %w", err)
|
||||
return "", fmt.Errorf("Runtime.evaluate: %w", err)
|
||||
}
|
||||
|
||||
// Verificar excepcion JS
|
||||
if exc, ok := evRes["exceptionDetails"]; ok && exc != nil {
|
||||
excMap, _ := exc.(map[string]any)
|
||||
text, _ := excMap["text"].(string)
|
||||
return "", fmt.Errorf("cdp eval in frame: excepcion JS en frame %q: %s", frameID, text)
|
||||
return "", fmt.Errorf("excepcion JS en frame %q: %s", frameID, text)
|
||||
}
|
||||
|
||||
// Extraer valor del resultado (mismo patron que CdpEvaluate)
|
||||
resVal, ok := evRes["result"].(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cdp eval in frame: resultado inesperado: %v", evRes)
|
||||
return "", fmt.Errorf("resultado inesperado: %v", evRes)
|
||||
}
|
||||
|
||||
value, ok := resVal["value"]
|
||||
if !ok {
|
||||
// undefined u otro tipo no serializable
|
||||
typ, _ := resVal["type"].(string)
|
||||
return typ, nil
|
||||
}
|
||||
|
||||
// Strings tal cual; objetos/arrays JS a JSON real (no la repr de Go de "%v").
|
||||
if s, ok := value.(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
@@ -81,3 +125,52 @@ func CdpEvalInFrame(c *CDPConn, frameID, expression string) (string, error) {
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// CdpEvalInFrame ejecuta una expresion JavaScript en el contexto aislado de un iframe
|
||||
// especifico usando Page.createIsolatedWorld + Runtime.evaluate con el contextId del frame.
|
||||
// Retorna el resultado serializado como string.
|
||||
//
|
||||
// Cachea el executionContextId por frameID en la conexion: la primera llamada
|
||||
// crea el mundo aislado, las siguientes lo reutilizan. Si el contexto cacheado
|
||||
// caducó (el frame navegó/recargó), recrea el mundo una vez y reintenta.
|
||||
func CdpEvalInFrame(c *CDPConn, frameID, expression string) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: conexion nula")
|
||||
}
|
||||
if frameID == "" {
|
||||
return "", fmt.Errorf("cdp eval in frame: frameID vacio")
|
||||
}
|
||||
|
||||
// Page.enable es idempotente; necesario antes de crear mundos aislados.
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: Page.enable: %w", err)
|
||||
}
|
||||
|
||||
cache := c.frameCtxCacheLazy()
|
||||
|
||||
ctxID, cached := cache.get(frameID)
|
||||
if !cached {
|
||||
newID, err := createIsolatedWorld(c, frameID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: %w", err)
|
||||
}
|
||||
ctxID = newID
|
||||
cache.set(frameID, ctxID)
|
||||
}
|
||||
|
||||
out, evErr := evalInFrameContext(c, ctxID, frameID, expression)
|
||||
if evErr != nil && cached && isStaleContextError(evErr) {
|
||||
// El contexto cacheado murió (frame recargó). Recrear una vez.
|
||||
cache.invalidate(frameID)
|
||||
newID, err := createIsolatedWorld(c, frameID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: %w", err)
|
||||
}
|
||||
cache.set(frameID, newID)
|
||||
out, evErr = evalInFrameContext(c, newID, frameID, expression)
|
||||
}
|
||||
if evErr != nil {
|
||||
return "", fmt.Errorf("cdp eval in frame: %w", evErr)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user