package main import ( "strings" "sync" "fn-registry/functions/browser" ) // connPool reusa conexiones CDP entre invocaciones de tools. Clave = puerto CDP. // Una conexión = una sesión viva a una tab "page". Mantenerla evita pagar el // handshake WebSocket en cada tool y preserva estado (event handlers, contexto). type connPool struct { mu sync.Mutex conns map[int]*browser.CDPConn cancels map[int]func() // cancels de handlers persistentes (handle_dialog) dialogLogs map[int]*browser.DialogLog // log de diálogos auto-respondidos por puerto } func newConnPool() *connPool { return &connPool{ conns: map[int]*browser.CDPConn{}, cancels: map[int]func(){}, dialogLogs: map[int]*browser.DialogLog{}, } } func (p *connPool) get(port int) (*browser.CDPConn, error) { p.mu.Lock() defer p.mu.Unlock() if c, ok := p.conns[port]; ok && c != nil { return c, nil } c, err := browser.CdpConnect(port) if err != nil { return nil, err } p.conns[port] = c return c, nil } func (p *connPool) drop(port int) { p.mu.Lock() defer p.mu.Unlock() if cancel, ok := p.cancels[port]; ok && cancel != nil { cancel() delete(p.cancels, port) } delete(p.dialogLogs, port) if c, ok := p.conns[port]; ok && c != nil { // pid=0: cerrar solo el WebSocket, sin matar Chrome (el navegador sigue // vivo; solo soltamos la sesión pooled). _ = browser.CdpClose(c, 0) delete(p.conns, port) } } // connectTarget descarta la conexión actual del puerto y reconecta a un target // determinista (por id o substring de URL). Asegura que el agente opera sobre una // pestaña conocida y no sobre "la primera al azar". func (p *connPool) connectTarget(port int, match string) (*browser.CDPConn, error) { p.drop(port) c, err := browser.CdpConnectTarget("localhost", port, match) if err != nil { return nil, err } p.mu.Lock() p.conns[port] = c p.mu.Unlock() return c, nil } // setDialog guarda el cancel y el DialogLog del auto-handler de diálogos del // puerto. Si ya había uno armado, lo cancela primero. func (p *connPool) setDialog(port int, cancel func(), dlog *browser.DialogLog) { p.mu.Lock() defer p.mu.Unlock() if old := p.cancels[port]; old != nil { old() } p.cancels[port] = cancel p.dialogLogs[port] = dlog } // dialogSnapshot devuelve el estado del log de diálogos del puerto (0,"","" si // no hay handler armado). func (p *connPool) dialogSnapshot(port int) (int, string, string) { p.mu.Lock() defer p.mu.Unlock() if dl := p.dialogLogs[port]; dl != nil { return dl.Snapshot() } return 0, "", "" } func (p *connPool) closeAll() { p.mu.Lock() defer p.mu.Unlock() for port, c := range p.conns { if cancel := p.cancels[port]; cancel != nil { cancel() } if c != nil { _ = browser.CdpClose(c, 0) } } p.conns = map[int]*browser.CDPConn{} p.cancels = map[int]func(){} p.dialogLogs = map[int]*browser.DialogLog{} } // isConnErr reconoce errores de conexión CDP muerta para reintentar UNA vez. func isConnErr(err error) bool { s := err.Error() return strings.Contains(s, "connection close") || strings.Contains(s, "broken pipe") || strings.Contains(s, "use of closed") || strings.Contains(s, "ws read") || strings.Contains(s, "EOF") }