Files
skill-template/tools/ui_inspector/models/element.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

127 lines
4.3 KiB
Python

"""ElementRef model for Element Catalog."""
from __future__ import annotations
import hashlib
import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Optional
from .locator import LocatorCandidate
SCHEMA_ELEMENT = "openclaw.ui_inspector.element.v1"
SCHEMA_CATALOG = "openclaw.ui_inspector.element_catalog.v1"
def _now_iso() -> str:
return datetime.now().astimezone().isoformat()
def _new_id(prefix: str = "el") -> str:
return f"{prefix}_{uuid.uuid4().hex[:12]}"
def element_fingerprint(
*,
surface: str,
role: str = "",
name: str = "",
automation_id: str = "",
class_name: str = "",
value: str = "",
window_scope: Optional[dict[str, Any]] = None,
dom_fingerprint: str = "",
page_scope: str = "",
contenteditable: bool = False,
) -> str:
payload = {
"surface": surface or "",
"role": role or "",
"name": "" if contenteditable else (name or ""),
"automation_id": automation_id or "",
"class_name": class_name or "",
# Value is identity only when short/stable-looking (not long free text).
"value": "" if contenteditable else ((value or "") if len(value or "") <= 64 else ""),
"dom_fingerprint": dom_fingerprint or "",
"page_scope": page_scope or "",
"window_scope": {
"class_name": (window_scope or {}).get("class_name") or "",
"title": (window_scope or {}).get("title") or "",
},
}
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
@dataclass
class ElementRef:
schema: str = SCHEMA_ELEMENT
id: str = field(default_factory=lambda: _new_id("el"))
logical_name: str = ""
surface: str = "windows"
backend: str = ""
app: dict[str, Any] = field(default_factory=dict)
window_scope: dict[str, Any] = field(default_factory=dict)
element_scope: dict[str, Any] = field(default_factory=dict)
role: str = ""
control_type: str = ""
name: str = ""
value: str = ""
automation_id: str = ""
class_name: str = ""
framework_id: str = ""
states: dict[str, Any] = field(default_factory=dict)
bounds: dict[str, Any] = field(default_factory=dict)
actions: list[str] = field(default_factory=list)
parent_chain: list[dict[str, Any]] = field(default_factory=list)
raw: dict[str, Any] = field(default_factory=dict)
fingerprint: str = ""
sensitive: bool = False
locator_candidates: list[LocatorCandidate] = field(default_factory=list)
primary_locator: Optional[LocatorCandidate] = None
fallback_locators: list[LocatorCandidate] = field(default_factory=list)
expected_count: int = 1
verified_count: Optional[int] = None
verification_level: str = "none"
stability_score: float = 0.0
verified_at: Optional[str] = None
first_seen: str = field(default_factory=_now_iso)
last_seen: str = field(default_factory=_now_iso)
observation_count: int = 1
locator_history: list[dict[str, Any]] = field(default_factory=list)
parameterized: dict[str, str] = field(default_factory=dict)
def ensure_fingerprint(self) -> str:
if self.fingerprint:
return self.fingerprint
self.fingerprint = element_fingerprint(
surface=self.surface,
role=self.role or self.control_type,
name=self.name,
automation_id=self.automation_id,
class_name=self.class_name,
value=self.value,
window_scope=self.window_scope,
dom_fingerprint=str(self.raw.get("fingerprint") or self.raw.get("css") or ""),
page_scope=str(self.element_scope.get("page_url") or ""),
contenteditable=bool(self.raw.get("contenteditable")),
)
return self.fingerprint
def touch(self) -> None:
self.last_seen = _now_iso()
self.observation_count = int(self.observation_count or 0) + 1
@dataclass
class ElementCatalog:
schema: str = SCHEMA_CATALOG
flow_id: str = ""
updated_at: str = field(default_factory=_now_iso)
elements: list[ElementRef] = field(default_factory=list)
def by_logical_name(self) -> dict[str, ElementRef]:
return {e.logical_name: e for e in self.elements if e.logical_name}