package browser import ( "context" "encoding/json" "fmt" ) // AccessibilityOptions opciones para obtener el árbol de accesibilidad type AccessibilityOptions struct { Depth int // Profundidad máxima del árbol (0 = ilimitado) FilterRoles []string // Roles a incluir (ej: ["button", "link", "heading"]) } // DefaultAccessibilityOptions retorna opciones por defecto func DefaultAccessibilityOptions() *AccessibilityOptions { return &AccessibilityOptions{ Depth: 0, // Sin límite FilterRoles: nil, } } // AXTree representa el árbol de accesibilidad completo type AXTree struct { Nodes []AXNode `json:"nodes"` } // AXNode representa un nodo en el árbol de accesibilidad type AXNode struct { NodeID string `json:"nodeId"` Role string `json:"role"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Value interface{} `json:"value,omitempty"` Properties []AXProperty `json:"properties,omitempty"` ChildIDs []string `json:"childIds,omitempty"` BackendDOMNodeId int `json:"backendDOMNodeId,omitempty"` Ignored bool `json:"ignored,omitempty"` } // AXProperty representa una propiedad de accesibilidad type AXProperty struct { Name string `json:"name"` Value interface{} `json:"value"` } // GetAccessibilityTree obtiene el árbol de accesibilidad de la página func (b *Browser) GetAccessibilityTree(ctx context.Context, opts *AccessibilityOptions) (*AXTree, error) { if opts == nil { opts = DefaultAccessibilityOptions() } // 1. Habilitar el dominio Accessibility if err := b.enableAccessibility(ctx); err != nil { return nil, fmt.Errorf("error enabling accessibility: %w", err) } // 2. Obtener el árbol completo result, err := b.cdpClient.SendCommand(ctx, "Accessibility.getFullAXTree", map[string]interface{}{ "depth": opts.Depth, }) if err != nil { return nil, fmt.Errorf("error getting accessibility tree: %w", err) } // 3. Parsear el resultado var axTree AXTree if nodesData, ok := result["nodes"].([]interface{}); ok { for _, nodeData := range nodesData { if nodeMap, ok := nodeData.(map[string]interface{}); ok { node := parseAXNode(nodeMap) // Filtrar por roles si se especificó if len(opts.FilterRoles) > 0 { if !contains(opts.FilterRoles, node.Role) { continue } } axTree.Nodes = append(axTree.Nodes, node) } } } return &axTree, nil } // GetAccessibilitySnapshot obtiene un snapshot simplificado del árbol de accesibilidad // más rápido y fácil de usar que GetAccessibilityTree func (b *Browser) GetAccessibilitySnapshot(ctx context.Context) (*AXTree, error) { // Habilitar accessibility if err := b.enableAccessibility(ctx); err != nil { return nil, fmt.Errorf("error enabling accessibility: %w", err) } // Obtener snapshot result, err := b.cdpClient.SendCommand(ctx, "Accessibility.getFullAXTree", map[string]interface{}{ "max_depth": 20, // Límite razonable }) if err != nil { return nil, fmt.Errorf("error getting snapshot: %w", err) } var axTree AXTree if nodesData, ok := result["nodes"].([]interface{}); ok { for _, nodeData := range nodesData { if nodeMap, ok := nodeData.(map[string]interface{}); ok { axTree.Nodes = append(axTree.Nodes, parseAXNode(nodeMap)) } } } return &axTree, nil } // FindInteractiveElements encuentra todos los elementos interactuables // (botones, links, inputs, etc.) func (b *Browser) FindInteractiveElements(ctx context.Context) ([]AXNode, error) { interactiveRoles := []string{ "button", "link", "textbox", "searchbox", "combobox", "checkbox", "radio", "slider", "spinbutton", "tab", "menuitem", "menuitemcheckbox", "menuitemradio", } opts := &AccessibilityOptions{ FilterRoles: interactiveRoles, } tree, err := b.GetAccessibilityTree(ctx, opts) if err != nil { return nil, err } return tree.Nodes, nil } // GetAccessibilitySummary genera un resumen textual del árbol de accesibilidad // ideal para LLMs func (b *Browser) GetAccessibilitySummary(ctx context.Context) (string, error) { tree, err := b.GetAccessibilitySnapshot(ctx) if err != nil { return "", err } summary := "=== Page Accessibility Structure ===\n\n" // Agrupar por rol roleGroups := make(map[string][]AXNode) for _, node := range tree.Nodes { if !node.Ignored && node.Role != "" { roleGroups[node.Role] = append(roleGroups[node.Role], node) } } // Generar resumen por rol for role, nodes := range roleGroups { summary += fmt.Sprintf("## %s (%d)\n", role, len(nodes)) for i, node := range nodes { if i >= 10 { summary += fmt.Sprintf(" ... and %d more\n", len(nodes)-10) break } if node.Name != "" { summary += fmt.Sprintf(" - %s\n", node.Name) } else if node.Description != "" { summary += fmt.Sprintf(" - %s\n", node.Description) } } summary += "\n" } return summary, nil } // enableAccessibility habilita el dominio Accessibility de CDP func (b *Browser) enableAccessibility(ctx context.Context) error { _, err := b.cdpClient.SendCommand(ctx, "Accessibility.enable", nil) return err } // parseAXNode parsea un nodo del árbol de accesibilidad desde el formato CDP func parseAXNode(data map[string]interface{}) AXNode { node := AXNode{} if nodeID, ok := data["nodeId"].(string); ok { node.NodeID = nodeID } if role, ok := data["role"].(map[string]interface{}); ok { if roleValue, ok := role["value"].(string); ok { node.Role = roleValue } } if name, ok := data["name"].(map[string]interface{}); ok { if nameValue, ok := name["value"].(string); ok { node.Name = nameValue } } if description, ok := data["description"].(map[string]interface{}); ok { if descValue, ok := description["value"].(string); ok { node.Description = descValue } } if value, ok := data["value"].(map[string]interface{}); ok { if val, ok := value["value"]; ok { node.Value = val } } if properties, ok := data["properties"].([]interface{}); ok { for _, prop := range properties { if propMap, ok := prop.(map[string]interface{}); ok { property := AXProperty{} if name, ok := propMap["name"].(string); ok { property.Name = name } if value, ok := propMap["value"].(map[string]interface{}); ok { if val, ok := value["value"]; ok { property.Value = val } } node.Properties = append(node.Properties, property) } } } if childIDs, ok := data["childIds"].([]interface{}); ok { for _, childID := range childIDs { if id, ok := childID.(string); ok { node.ChildIDs = append(node.ChildIDs, id) } } } if backendID, ok := data["backendDOMNodeId"].(float64); ok { node.BackendDOMNodeId = int(backendID) } if ignored, ok := data["ignored"].(bool); ok { node.Ignored = ignored } return node } // ToJSON serializa el árbol de accesibilidad a JSON func (tree *AXTree) ToJSON() (string, error) { bytes, err := json.MarshalIndent(tree, "", " ") if err != nil { return "", err } return string(bytes), nil } // contains verifica si un slice contiene un string func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false }