Files
skill-template/tools/ui_inspector/locators/windows.py
chendelian 633ecff4e2 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>
2026-08-07 11:19:14 +08:00

181 lines
5.7 KiB
Python

"""Windows UIA locator compiler (Name ≠ Value; no coordinate primary)."""
from __future__ import annotations
from typing import Any, Optional
from models.locator import LocatorCandidate
from .base import count_matches, rank_and_select_primary
def _expr(strategy: str, props: dict[str, Any]) -> str:
parts = [f"{k}={v!r}" for k, v in props.items() if v not in (None, "")]
return f"{strategy}:" + ",".join(parts)
def compile_windows_locators(
element: dict[str, Any],
*,
rows: Optional[list[dict[str, Any]]] = None,
window_scope: Optional[dict[str, Any]] = None,
) -> list[LocatorCandidate]:
"""
Priority:
1. Window scope + AutomationId + ControlType
2. Window scope + stable Name + ControlType
3. Window scope + Name + Value + ControlType
4. Business parent + child attrs
5. Visible Value + ControlType
6. Relative ancestor scope
7. Index with business reason (low score)
8. BoundingRectangle evidence only
"""
el = element or {}
rows = list(rows or [])
win = dict(window_scope or el.get("window_scope") or {})
scope = {
"window_class": win.get("class_name") or win.get("window_class") or "",
"window_title": win.get("title") or win.get("window_title") or "",
"process_name": win.get("process_name") or "",
}
cands: list[LocatorCandidate] = []
aid = str(el.get("automation_id") or "").strip()
name = str(el.get("name") or "").strip()
value = str(el.get("value") or "").strip()
ctype = str(el.get("control_type") or el.get("role") or "").strip()
class_name = str(el.get("class_name") or "").strip()
parent_chain = list(el.get("parent_chain") or [])
def add(
strategy: str,
props: dict[str, Any],
score: float,
reason: str,
*,
uses_index: bool = False,
uses_coordinates: bool = False,
) -> None:
if not props or not any(str(v).strip() for v in props.values() if not isinstance(v, (dict, list))):
return
c = LocatorCandidate(
strategy=strategy,
scope=dict(scope),
properties=dict(props),
expression=_expr(strategy, props),
stability_score=score,
uses_index=uses_index,
uses_coordinates=uses_coordinates,
reasons=[reason],
)
if rows:
c.mark_counts(count_matches(rows, properties=props, scope=scope))
c.validation = "snapshot_only"
elif uses_coordinates:
c.production_eligible = False
c.unique = False
c.ambiguous = True
cands.append(c)
if aid and ctype:
add(
"window_automation_id_type",
{"automation_id": aid, "control_type": ctype},
0.96,
"automation_id_control_type",
)
elif aid:
add("window_automation_id", {"automation_id": aid}, 0.9, "automation_id")
if name and ctype and name.lower() not in {"item", "pane", "unknown"}:
add(
"window_name_type",
{"name": name, "control_type": ctype},
0.88,
"stable_name_control_type",
)
if name and value and ctype:
add(
"window_name_value_type",
{"name": name, "value": value, "control_type": ctype},
0.86,
"name_value_control_type",
)
# Business parent + child
if parent_chain:
parent = parent_chain[0] if isinstance(parent_chain[0], dict) else {}
parent_name = str(parent.get("name") or parent.get("automation_id") or "").strip()
if parent_name and (aid or name or value):
props: dict[str, Any] = {"parent_name": parent_name}
if aid:
props["automation_id"] = aid
if name:
props["name"] = name
if value:
props["value"] = value
if ctype:
props["control_type"] = ctype
add("parent_scoped", props, 0.82, "business_parent_scope")
if value and ctype and len(value) <= 64:
add(
"value_type",
{"value": value, "control_type": ctype},
0.78,
"visible_value_control_type",
)
if class_name and ctype:
add(
"class_type",
{"class_name": class_name, "control_type": ctype},
0.55,
"class_name_control_type",
)
# Index only with explicit business reason — low score, not auto primary
idx = el.get("index")
if idx is not None and (aid or name):
props = {"index": int(idx)}
if aid:
props["automation_id"] = aid
if name:
props["name"] = name
if ctype:
props["control_type"] = ctype
add(
"indexed",
props,
0.35,
"index_requires_business_justification",
uses_index=True,
)
bounds = el.get("bounds") or el.get("rect") or el.get("bounding_rectangle")
if bounds:
c = LocatorCandidate(
strategy="bounding_rectangle",
scope=dict(scope),
properties={"bounds": bounds},
expression="coordinates:(evidence_only)",
stability_score=0.05,
uses_coordinates=True,
production_eligible=False,
unique=False,
ambiguous=True,
reasons=["coordinates_never_primary"],
)
cands.append(c)
for c in cands:
if c.actual_count is not None:
c.mark_counts(int(c.actual_count), expected=int(c.expected_count or 1))
if c.uses_coordinates:
c.production_eligible = False
rank_and_select_primary(cands)
return cands