chore: archive tools/ui_inspector snapshot before migration removal.

Keep the last in-template copy on main so history can restore it; product continues in jiangchang-ui-recorder.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 11:19:14 +08:00
parent 46bc573777
commit 633ecff4e2
92 changed files with 13704 additions and 288 deletions

View File

@@ -0,0 +1,7 @@
"""Surface-independent decision policies."""
from .element_identity import element_identity_key
from .target_scoring import resolve_browser_target
__all__ = ["element_identity_key", "resolve_browser_target"]

View File

@@ -0,0 +1,62 @@
"""Stable element identity independent of transient value/focus classes."""
from __future__ import annotations
import hashlib
import json
import re
from typing import Any
from urllib.parse import urlsplit
def _origin(url: str) -> str:
try:
parts = urlsplit(url or "")
return f"{parts.scheme}://{parts.netloc}" if parts.netloc else ""
except Exception:
return ""
def _structural_css(css: str) -> str:
# Classes frequently encode focus/hover/build hashes. Keep tag depth and
# stable IDs; that distinguishes nested anonymous nodes without freezing a
# generated class list into identity.
parts = []
for raw in (css or "").split(">"):
part = raw.strip()
tag_match = re.match(r"^[A-Za-z][\w-]*", part)
id_match = re.search(r"#[A-Za-z0-9_-]+", part)
tag = tag_match.group(0).lower() if tag_match else "*"
parts.append(tag + (id_match.group(0) if id_match else ""))
return ">".join(parts)
def element_identity_key(
element: dict[str, Any], *, surface: str, page_url: str = "", frame_name: str = ""
) -> str:
el = dict(element or {})
role = str(el.get("control_type") or el.get("role") or el.get("tag") or "")
anchor = ""
for key in (
"test_id", "automation_id", "id", "name_attr", "href",
"placeholder", "aria_label", "label",
):
value = str(el.get(key) or "").strip()
if value:
anchor = f"{key}:{value}"
break
if not anchor and not el.get("contenteditable"):
name = str(el.get("semantic_name") or el.get("name") or "").strip()
if name and len(name) <= 96:
anchor = f"name:{name}"
if not anchor:
anchor = f"structure:{_structural_css(str(el.get('css') or ''))}"
payload = {
"surface": surface or "",
"origin": _origin(page_url) if surface == "browser" else "",
"frame": frame_name or "",
"role": role.lower(),
"anchor": anchor,
}
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode("utf-8")).hexdigest()

View File

@@ -0,0 +1,145 @@
"""Distance- and geometry-aware target selection.
The resolver never relies on a product selector. It ranks the composed path
using native semantics, explicit accessibility metadata, distance from the hit
node, and geometry. Huge ancestors are evidence containers, not controls.
"""
from __future__ import annotations
import re
from typing import Any, Iterable
_NATIVE_TAGS = {"button", "a", "input", "textarea", "select", "summary", "label"}
_INTERACTIVE_ROLES = {
"button", "link", "textbox", "combobox", "checkbox", "radio",
"menuitem", "option", "tab", "switch", "slider", "spinbutton",
}
_ACTION_CLASS_TOKENS = {
"button", "btn", "submit", "send", "publish", "post", "play",
"pause", "action", "click", "toggle", "menu", "option", "tab",
}
_GRAPHIC_LEAF_TAGS = {"path", "use", "g", "svg"}
def _tokens(value: str) -> set[str]:
value = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", value or "")
return {x.lower() for x in re.split(r"[^A-Za-z0-9]+", value) if x}
def _area(rect: dict[str, Any]) -> float:
try:
return max(0.0, float(rect.get("width") or 0)) * max(
0.0, float(rect.get("height") or 0)
)
except (TypeError, ValueError):
return 0.0
def _contains(
rect: dict[str, Any], x: int | None, y: int | None, *, slop: float = 12.0
) -> bool:
if x is None or y is None or not rect:
return True
try:
return (
float(rect.get("left")) - slop <= x <= float(rect.get("right")) + slop
and float(rect.get("top")) - slop <= y <= float(rect.get("bottom")) + slop
)
except (TypeError, ValueError):
return True
def resolve_browser_target(
hit_target: dict[str, Any],
candidates: Iterable[dict[str, Any]],
*,
x: int | None = None,
y: int | None = None,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
"""Return the best actionable node and an auditable scored candidate list."""
chain = [dict(item or {}) for item in candidates if isinstance(item, dict)]
if not chain:
chain = [dict(hit_target or {})]
hit = dict(hit_target or chain[0] or {})
hit_area = max(1.0, _area(hit.get("rect") or {}))
scored: list[dict[str, Any]] = []
for depth, item in enumerate(chain):
tag = str(item.get("tag") or "").lower()
role = str(item.get("role") or "").lower()
explicit_role = str(item.get("explicit_role") or "").lower()
rect = dict(item.get("rect") or {})
area = _area(rect)
score = 24.0 if depth == 0 else 0.0
reasons: list[str] = [f"depth={depth}"]
if tag in _NATIVE_TAGS:
score = max(score, 100.0)
reasons.append("native_control")
if item.get("contenteditable_attr"):
score = max(score, 99.0)
reasons.append("editable_root")
elif item.get("contenteditable"):
score = max(score, 82.0)
reasons.append("editable_descendant")
if explicit_role in _INTERACTIVE_ROLES:
score = max(score, 97.0)
reasons.append("explicit_interactive_role")
elif role in _INTERACTIVE_ROLES:
score = max(score, 88.0)
reasons.append("computed_interactive_role")
if item.get("test_id") or item.get("aria_label"):
score = max(score, 91.0)
reasons.append("stable_semantic_attribute")
if item.get("tabindex") not in (None, ""):
score = max(score, 76.0)
reasons.append("keyboard_focusable")
if _tokens(str(item.get("class_name") or "")) & _ACTION_CLASS_TOKENS:
score = max(score, 82.0)
reasons.append("action_class_token")
if str(item.get("cursor") or "").lower() == "pointer":
score = max(score, 68.0)
reasons.append("pointer_cursor")
# Target selection and locator quality are one decision. Event-time
# counts are captured before a click can navigate or mutate the DOM.
counts = [
item.get("stable_css_count"),
item.get("href_count"),
item.get("css_count"),
]
known_counts = [int(value) for value in counts if value is not None]
if 1 in known_counts:
score += 22.0
reasons.append("event_time_unique_locator")
elif known_counts and min(known_counts) > 1:
score -= 18.0
reasons.append("event_time_ambiguous_locator")
if tag in _GRAPHIC_LEAF_TAGS:
score -= 28.0
reasons.append("graphic_leaf")
elif (
str(item.get("cursor") or "").lower() == "pointer"
and 20.0 <= float(rect.get("width") or 0.0) <= 96.0
and 20.0 <= float(rect.get("height") or 0.0) <= 96.0
):
score += 14.0
reasons.append("compact_clickable_wrapper")
score -= depth * 3.5
if not _contains(rect, x, y):
score -= 100.0
reasons.append("outside_pointer")
if area and area > max(hit_area * 25.0, 200_000.0):
score -= 70.0
reasons.append("oversized_ancestor")
elif area and area > max(hit_area * 8.0, 60_000.0):
score -= 25.0
reasons.append("large_ancestor")
scored.append({"depth": depth, "score": round(score, 3), "reasons": reasons, "target": item})
winner = max(scored, key=lambda row: (row["score"], -row["depth"]))
return dict(winner["target"]), scored