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>
127 lines
4.3 KiB
Python
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}
|