package browser import ( "context" "encoding/json" "fmt" "sync" ) // Tab representa un tab del navegador type Tab struct { ID string URL string Title string Type string // "page" | "background_page" | ... Attached bool } // tabHandler almacena handlers para eventos de tabs type tabHandler struct { onCreate func(*Tab) } var ( tabHandlers = &tabHandler{} tabMutex sync.RWMutex ) // GetTabs obtiene todos los tabs abiertos func (b *Browser) GetTabs(ctx context.Context) ([]*Tab, error) { var result struct { TargetInfos []struct { TargetID string `json:"targetId"` Type string `json:"type"` Title string `json:"title"` URL string `json:"url"` Attached bool `json:"attached"` } `json:"targetInfos"` } if err := b.cdpClient.Execute(ctx, "Target.getTargets", nil, &result); err != nil { return nil, fmt.Errorf("failed to get targets: %w", err) } var tabs []*Tab for _, info := range result.TargetInfos { if info.Type == "page" { tabs = append(tabs, &Tab{ ID: info.TargetID, URL: info.URL, Title: info.Title, Type: info.Type, Attached: info.Attached, }) } } return tabs, nil } // NewTab crea un nuevo tab y retorna su ID func (b *Browser) NewTab(ctx context.Context, url string) (string, error) { var result struct { TargetID string `json:"targetId"` } params := map[string]interface{}{ "url": url, } if err := b.cdpClient.Execute(ctx, "Target.createTarget", params, &result); err != nil { return "", fmt.Errorf("failed to create tab: %w", err) } return result.TargetID, nil } // CloseTab cierra un tab específico func (b *Browser) CloseTab(ctx context.Context, tabID string) error { params := map[string]interface{}{ "targetId": tabID, } var result struct { Success bool `json:"success"` } if err := b.cdpClient.Execute(ctx, "Target.closeTarget", params, &result); err != nil { return fmt.Errorf("failed to close tab: %w", err) } if !result.Success { return fmt.Errorf("failed to close tab: CDP returned success=false") } return nil } // SwitchToTab cambia el foco a un tab específico func (b *Browser) SwitchToTab(ctx context.Context, tabID string) error { // Activar tab activateParams := map[string]interface{}{ "targetId": tabID, } if err := b.cdpClient.Execute(ctx, "Target.activateTarget", activateParams, nil); err != nil { return fmt.Errorf("failed to activate tab: %w", err) } // Attach al tab si no está attached attachParams := map[string]interface{}{ "targetId": tabID, "flatten": true, } var attachResult struct { SessionID string `json:"sessionId"` } if err := b.cdpClient.Execute(ctx, "Target.attachToTarget", attachParams, &attachResult); err != nil { // Puede que ya esté attached, continuar } // Actualizar targetID actual del browser b.targetID = tabID return nil } // GetCurrentTab obtiene el tab actual func (b *Browser) GetCurrentTab(ctx context.Context) (*Tab, error) { tabs, err := b.GetTabs(ctx) if err != nil { return nil, err } // Buscar el tab con el targetID actual for _, tab := range tabs { if tab.ID == b.targetID { return tab, nil } } // Si no encontramos, retornar el primero if len(tabs) > 0 { return tabs[0], nil } return nil, fmt.Errorf("no tabs found") } // WaitForNewTab espera a que se abra un nuevo tab y lo retorna func (b *Browser) WaitForNewTab(ctx context.Context, action func()) (*Tab, error) { // Obtener tabs actuales currentTabs, err := b.GetTabs(ctx) if err != nil { return nil, err } currentIDs := make(map[string]bool) for _, tab := range currentTabs { currentIDs[tab.ID] = true } // Canal para recibir nuevo tab newTabChan := make(chan *Tab, 1) // Registrar listener temporal para nuevos tabs b.cdpClient.On("Target.targetCreated", func(params json.RawMessage) { var event struct { TargetInfo struct { TargetID string `json:"targetId"` Type string `json:"type"` Title string `json:"title"` URL string `json:"url"` } `json:"targetInfo"` } if err := json.Unmarshal(params, &event); err != nil { return } // Solo procesar tabs de tipo "page" if event.TargetInfo.Type == "page" { // Verificar que es un tab nuevo if !currentIDs[event.TargetInfo.TargetID] { newTab := &Tab{ ID: event.TargetInfo.TargetID, URL: event.TargetInfo.URL, Title: event.TargetInfo.Title, Type: event.TargetInfo.Type, } select { case newTabChan <- newTab: default: } } } }) // Ejecutar acción que abrirá el tab if action != nil { action() } // Esperar nuevo tab select { case newTab := <-newTabChan: return newTab, nil case <-ctx.Done(): return nil, fmt.Errorf("timeout waiting for new tab: %w", ctx.Err()) } } // OnTabCreated registra callback para cuando se crea un nuevo tab func (b *Browser) OnTabCreated(handler func(*Tab)) error { tabMutex.Lock() defer tabMutex.Unlock() tabHandlers.onCreate = handler // Registrar listener de eventos b.cdpClient.On("Target.targetCreated", func(params json.RawMessage) { var event struct { TargetInfo struct { TargetID string `json:"targetId"` Type string `json:"type"` Title string `json:"title"` URL string `json:"url"` } `json:"targetInfo"` } if err := json.Unmarshal(params, &event); err != nil { return } if event.TargetInfo.Type == "page" { tab := &Tab{ ID: event.TargetInfo.TargetID, URL: event.TargetInfo.URL, Title: event.TargetInfo.Title, Type: event.TargetInfo.Type, } tabMutex.RLock() if tabHandlers.onCreate != nil { tabHandlers.onCreate(tab) } tabMutex.RUnlock() } }) return nil } // CloseOtherTabs cierra todos los tabs excepto el actual func (b *Browser) CloseOtherTabs(ctx context.Context) error { currentTab, err := b.GetCurrentTab(ctx) if err != nil { return err } tabs, err := b.GetTabs(ctx) if err != nil { return err } for _, tab := range tabs { if tab.ID != currentTab.ID { if err := b.CloseTab(ctx, tab.ID); err != nil { // Continuar cerrando otros tabs incluso si uno falla continue } } } return nil } // GetTabByURL busca un tab por URL (coincidencia parcial) func (b *Browser) GetTabByURL(ctx context.Context, urlPattern string) (*Tab, error) { tabs, err := b.GetTabs(ctx) if err != nil { return nil, err } for _, tab := range tabs { if containsString(tab.URL, urlPattern) { return tab, nil } } return nil, fmt.Errorf("no tab found with URL pattern: %s", urlPattern) } // GetTabByTitle busca un tab por título (coincidencia parcial) func (b *Browser) GetTabByTitle(ctx context.Context, titlePattern string) (*Tab, error) { tabs, err := b.GetTabs(ctx) if err != nil { return nil, err } for _, tab := range tabs { if containsString(tab.Title, titlePattern) { return tab, nil } } return nil, fmt.Errorf("no tab found with title pattern: %s", titlePattern) }