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

176 lines
5.3 KiB
Python

"""UIState model + stable business fingerprint."""
from __future__ import annotations
import hashlib
import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional
SCHEMA_STATE = "openclaw.ui_inspector.state.v1"
# Keys ignored when hashing business identity.
_FINGERPRINT_EXCLUDE_TOP = {
"captured_at",
"id",
"previous_state_id",
"tree_path",
"index_path",
"locatable_path",
"screenshot_path",
"duplicate_of",
"observation_count",
"change_reason",
}
_SESSION_KEYS = {
"pid",
"hwnd",
"process_id",
"thread_id",
"elapsed_ms",
"dump_ms",
"warmup_ms",
"timings",
"screenshot",
"screen_path",
}
def _now_iso() -> str:
return datetime.now().astimezone().isoformat()
def _new_id(prefix: str = "st") -> str:
return f"{prefix}_{uuid.uuid4().hex[:12]}"
def _strip_session(obj: Any) -> Any:
if isinstance(obj, dict):
out: dict[str, Any] = {}
for k, v in obj.items():
lk = str(k).lower()
if lk in _SESSION_KEYS or lk.endswith("_ms") or lk.endswith("_path"):
continue
if lk in {"left", "top", "right", "bottom", "x", "y", "width", "height"}:
# Absolute coordinates are not primary identity.
continue
if lk == "rect" or lk == "bounds" or lk == "bounding_rectangle":
continue
out[str(k)] = _strip_session(v)
return out
if isinstance(obj, list):
return [_strip_session(x) for x in obj]
return obj
def _locatable_signature(rows: list[dict[str, Any]], *, limit: int = 120) -> list[dict[str, Any]]:
"""Compact visible-business signature for fingerprinting."""
sig: list[dict[str, Any]] = []
for r in rows[:limit]:
if not isinstance(r, dict):
continue
entry = {
"control_type": r.get("control_type") or r.get("role") or "",
"name": r.get("name") or "",
"value": r.get("value") or r.get("text") or "",
"automation_id": r.get("automation_id") or r.get("test_id") or "",
"class_name": r.get("class_name") or "",
}
# Drop empty noise rows
if not any(str(v).strip() for v in entry.values()):
continue
sig.append(entry)
# Stable order
sig.sort(
key=lambda e: (
str(e.get("control_type") or ""),
str(e.get("automation_id") or ""),
str(e.get("name") or ""),
str(e.get("value") or ""),
)
)
return sig
def compute_state_fingerprint(
*,
surface: str,
driver: str = "",
url: str = "",
page_title: str = "",
window: Optional[dict[str, Any]] = None,
locatable: Optional[list[dict[str, Any]]] = None,
app: Optional[dict[str, Any]] = None,
extra_business: Optional[dict[str, Any]] = None,
) -> str:
"""
SHA-256 of stable business identity.
Excludes timestamps, pid/hwnd, screenshot paths, absolute coords, timings.
"""
win = dict(window or {})
app_d = dict(app or {})
payload = {
"surface": surface or "",
"driver": driver or "",
"url": url or win.get("url") or app_d.get("url") or "",
"page_title": page_title or "",
"window": {
"title": win.get("title") or app_d.get("title") or "",
"class_name": win.get("class_name") or app_d.get("class_name") or "",
"process_name": win.get("process_name") or app_d.get("process_name") or "",
},
"locatable": _locatable_signature(list(locatable or [])),
"extra": _strip_session(extra_business or {}),
}
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
@dataclass
class UIState:
schema: str = SCHEMA_STATE
id: str = field(default_factory=lambda: _new_id("st"))
surface: str = "windows"
driver: str = ""
captured_at: str = field(default_factory=_now_iso)
fingerprint: str = ""
previous_state_id: Optional[str] = None
change_reason: str = ""
app: dict[str, Any] = field(default_factory=dict)
window: dict[str, Any] = field(default_factory=dict)
url: str = ""
page_title: str = ""
tree_path: str = ""
index_path: str = ""
locatable_path: str = ""
screenshot_path: str = ""
stats: dict[str, Any] = field(default_factory=dict)
selection: dict[str, Any] = field(default_factory=dict)
duplicate_of: Optional[str] = None
observation_count: int = 1
may_contain_sensitive_data: bool = False
# In-memory locatable used for fingerprint/diff (not always persisted as field).
locatable: list[dict[str, Any]] = field(default_factory=list, repr=False)
def ensure_fingerprint(self) -> str:
if self.fingerprint:
return self.fingerprint
self.fingerprint = compute_state_fingerprint(
surface=self.surface,
driver=self.driver,
url=self.url,
page_title=self.page_title,
window=self.window,
locatable=self.locatable,
app=self.app,
)
return self.fingerprint
def bump_observation(self) -> None:
self.observation_count = int(self.observation_count or 0) + 1
self.captured_at = _now_iso()