# browser/accessibility.py import json from playwright.async_api import Page _SKIP_PROPERTIES = { "description", "value", "keyshortcuts", "roledescription", "valuetext", "disabled", "expanded", "focused", "modal", "multiline", "multiselectable", "readonly", "required", "selected" } def compress_ax_node(node: dict) -> dict | None: if "ignored" in node and node["ignored"]: return None compressed = { "id": node.get("nodeId", ""), "role": node.get("role", ""), "name": node.get("name", ""), } if "children" in node: children = [] for child in node["children"]: comp_child = compress_ax_node(child) if comp_child: children.append(comp_child) if children: compressed["children"] = children if node.get("role") in ("none", "generic", "StaticText") and not node.get("name"): if "children" not in compressed: return None return compressed async def extract_ax_tree(page: Page) -> str: """ Gibt den komprimierten Accessibility Tree (ohne rohes HTML) zurück. Schützt vor Prompt Injections und spart Token. """ client = await page.context.new_cdp_session(page) response = await client.send("Accessibility.getFullAXTree") nodes = response.get("nodes", []) if not nodes: return "{}" root_node = nodes[0] # Wir iterieren durch die flache Liste und bauen den Baum zusammen # Für diese vereinfachte Implementierung geben wir vorerst die gefilterte Liste zurück # Eine echte Rekursion auf der Basis der 'childIds' wäre noch robuster. simplified_nodes = [] for n in nodes: if n.get("role") not in ("generic", "StaticText") or n.get("name"): if not n.get("ignored"): simplified_nodes.append({ "id": n.get("nodeId"), "role": n.get("role"), "name": n.get("name", {}).get("value", "") }) return json.dumps(simplified_nodes, ensure_ascii=False) async def extract_dom_text(page: Page) -> str: """ Token-sparende DOM Extraktion (inspiriert vom Antigravity Mobile Browser). Filtert SCRIPT, STYLE und generiert eine textuelle Repräsentation. """ js = r''' (function(){ const selector = [ "a", "button", "input", "textarea", "select", "iframe", "[role=button]", "[role=checkbox]", "[role=link]", "[contenteditable=true]", "[tabindex]" ].join(","); const candidates = Array.from(document.querySelectorAll(selector)); const visible = candidates.filter(el => { const style = getComputedStyle(el); const rect = el.getBoundingClientRect(); return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0; }).slice(0, 80); window.__gravityAxNextId = window.__gravityAxNextId || 0; const elements = visible.map((el) => { if (!el.getAttribute("data-ax-node-id")) { el.setAttribute( "data-ax-node-id", "ax-" + window.__gravityAxNextId++ ); } const id = el.getAttribute("data-ax-node-id"); const name = ( el.getAttribute("aria-label") || el.getAttribute("title") || el.placeholder || el.name || el.value || el.textContent || "" ).replace(/\s+/g, " ").trim().substring(0, 120); const item = { id, tag: el.tagName.toLowerCase(), role: el.getAttribute("role") || "", name }; if (el.type) item.type = el.type; if (el.checked !== undefined) item.checked = Boolean(el.checked); if (el.disabled !== undefined) item.disabled = Boolean(el.disabled); if (el.href) item.href = String(el.href).substring(0, 180); return item; }); const pageText = (document.body.innerText || "") .replace(/\s+/g, " ").trim().substring(0, 2500); return JSON.stringify({ url: location.href, title: document.title, elements, text: pageText }); })() ''' try: text = await page.evaluate(js) return text if text else "" except Exception: return ""