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>
120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
"""ActionRecord + ActionFlow models."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Any, Optional
|
|
|
|
SCHEMA_ACTION = "openclaw.ui_inspector.action.v1"
|
|
SCHEMA_ACTION_FLOW = "openclaw.ui_inspector.action_flow.v1"
|
|
|
|
# Full vocabulary — capability flags live elsewhere for unimplemented types.
|
|
ACTION_TYPES: tuple[str, ...] = (
|
|
"click",
|
|
"double_click",
|
|
"right_click",
|
|
"fill",
|
|
"type",
|
|
"clear",
|
|
"press_key",
|
|
"hotkey",
|
|
"select",
|
|
"toggle",
|
|
"hover",
|
|
"scroll",
|
|
"drag_drop",
|
|
"focus",
|
|
"window_switch",
|
|
"navigation",
|
|
"checkpoint",
|
|
"wait",
|
|
"verify",
|
|
"unknown",
|
|
)
|
|
|
|
# Capability status for this Inspector release.
|
|
ACTION_CAPABILITY: dict[str, str] = {
|
|
"click": "implemented",
|
|
"double_click": "schema_only",
|
|
"right_click": "schema_only",
|
|
"fill": "implemented",
|
|
"type": "implemented", # aggregated into fill/type
|
|
"clear": "schema_only",
|
|
"press_key": "implemented",
|
|
"hotkey": "schema_only",
|
|
"select": "schema_only",
|
|
"toggle": "schema_only",
|
|
"hover": "schema_only",
|
|
"scroll": "schema_only",
|
|
"drag_drop": "schema_only",
|
|
"focus": "schema_only",
|
|
"window_switch": "schema_only",
|
|
"navigation": "partial",
|
|
"checkpoint": "implemented",
|
|
"wait": "schema_only",
|
|
"verify": "schema_only",
|
|
"unknown": "fallback",
|
|
}
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now().astimezone().isoformat()
|
|
|
|
|
|
def _new_id(prefix: str = "act") -> str:
|
|
return f"{prefix}_{uuid.uuid4().hex[:12]}"
|
|
|
|
|
|
@dataclass
|
|
class ActionRecord:
|
|
schema: str = SCHEMA_ACTION
|
|
id: str = field(default_factory=lambda: _new_id("act"))
|
|
sequence: int = 0
|
|
action_type: str = "unknown"
|
|
started_at: str = field(default_factory=_now_iso)
|
|
ended_at: Optional[str] = None
|
|
surface: str = "windows"
|
|
target_element_id: Optional[str] = None
|
|
target_snapshot: dict[str, Any] = field(default_factory=dict)
|
|
input: dict[str, Any] = field(default_factory=dict)
|
|
sensitive: bool = False
|
|
before_state_id: Optional[str] = None
|
|
after_state_id: Optional[str] = None
|
|
before_observation_id: Optional[str] = None
|
|
after_observation_id: Optional[str] = None
|
|
observed_changes: list[dict[str, Any]] = field(default_factory=list)
|
|
proposed_postconditions: list[dict[str, Any]] = field(default_factory=list)
|
|
status: str = "ok" # ok | partial | failed | skipped
|
|
confidence: float = 0.0
|
|
evidence: dict[str, Any] = field(default_factory=dict)
|
|
raw_event: dict[str, Any] = field(default_factory=dict)
|
|
raw_event_ids: list[str] = field(default_factory=list)
|
|
capability: str = ""
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.action_type not in ACTION_TYPES:
|
|
self.action_type = "unknown"
|
|
if not self.capability:
|
|
self.capability = ACTION_CAPABILITY.get(self.action_type, "schema_only")
|
|
|
|
|
|
@dataclass
|
|
class ActionFlow:
|
|
schema: str = SCHEMA_ACTION_FLOW
|
|
flow_id: str = ""
|
|
flow_name: str = ""
|
|
created_at: str = field(default_factory=_now_iso)
|
|
ended_at: Optional[str] = None
|
|
states: list[str] = field(default_factory=list) # state ids in order seen
|
|
observations: list[str] = field(default_factory=list) # every concrete capture
|
|
actions: list[str] = field(default_factory=list) # action ids in sequence
|
|
elements: list[str] = field(default_factory=list) # element ids
|
|
entry_state_id: Optional[str] = None
|
|
final_state_id: Optional[str] = None
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
recording_mode: str = "auto" # auto | guided | checkpoint | legacy_interval
|
|
surface: str = ""
|
|
status: str = "open"
|