APP-Backend / Browser_Agent /browser /accessibility.py
Luca448's picture
Savestate: Fix Dockerfile and add Browser_Agent for HF build, bump version to 1.8.334
508316a
Raw History Blame Contribute Delete
4.53 kB
# 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 ""