"""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}