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:
126
tools/ui_inspector/models/element.py
Normal file
126
tools/ui_inspector/models/element.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user