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:
2026-08-07 11:19:14 +08:00
parent 46bc573777
commit 633ecff4e2
92 changed files with 13704 additions and 288 deletions

View File

@@ -0,0 +1,6 @@
"""Asynchronous evidence capture."""
from .coordinator import EvidenceCoordinator
__all__ = ["EvidenceCoordinator"]

View File

@@ -0,0 +1,255 @@
"""Coalesced background full-page evidence capture.
Semantic actions are committed immediately. Slow dumps are supplementary
evidence and are explicitly labelled eventual instead of being misrepresented
as an event-time after-state.
"""
from __future__ import annotations
import queue
import threading
import time
from dataclasses import dataclass
from typing import Any, Callable, Optional
@dataclass
class EvidenceRequest:
action_id: str
context: dict[str, Any]
requested_monotonic: float
class EvidenceCoordinator:
def __init__(
self,
manager: Any,
dump_fn: Callable[..., Any],
*,
coalesce_sec: float = 0.25,
on_dump: Optional[Callable[[Any], None]] = None,
) -> None:
self.manager = manager
self.dump_fn = dump_fn
self.coalesce_sec = coalesce_sec
self.on_dump = on_dump
self._queue: queue.Queue[EvidenceRequest | None] = queue.Queue(maxsize=512)
self._urgent_queue: queue.Queue[EvidenceRequest | None] = queue.Queue(maxsize=128)
self._thread: Optional[threading.Thread] = None
self._urgent_thread: Optional[threading.Thread] = None
self._stop = threading.Event()
self.dropped = 0
self.capture_count = 0
self.failure_count = 0
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._stop.clear()
self._thread = threading.Thread(target=self._run, name="ui-inspector-evidence", daemon=True)
self._thread.start()
if str(getattr(self.manager.action_flow, "surface", "")) == "windows":
self._urgent_thread = threading.Thread(
target=self._run_urgent,
name="ui-inspector-evidence-transient",
daemon=True,
)
self._urgent_thread.start()
def schedule(self, action: Any) -> None:
raw = dict(getattr(action, "raw_event", {}) or {})
meta = dict(raw.get("meta") or {})
context = {
"page_id": meta.get("page_id") or "",
"document_id": meta.get("document_id") or "",
"frame_id": meta.get("frame_id") or "",
"source_ts_ms": meta.get("source_ts_ms"),
"url": raw.get("url") or "",
}
scope = dict((raw.get("target") or {}).get("window_scope") or {})
context.update(
{
"target_hwnd": int(scope.get("hwnd") or 0),
"target_pid": int(scope.get("pid") or 0),
"target_class_name": str(scope.get("class_name") or ""),
"target_title": str(scope.get("title") or ""),
}
)
out_queue = (
self._urgent_queue
if context["target_class_name"] == "SearchResultWindow2"
else self._queue
)
try:
out_queue.put_nowait(EvidenceRequest(action.id, context, time.monotonic()))
except queue.Full:
self.dropped += 1
def prime(self) -> None:
"""Start one background enrichment without creating a semantic action."""
try:
self._queue.put_nowait(EvidenceRequest("", {}, time.monotonic()))
except queue.Full:
self.dropped += 1
def _dump(self, context: dict[str, Any]) -> Any:
kwargs = {
k: context.get(k)
for k in (
"page_id", "document_id", "frame_id", "target_hwnd",
"target_pid", "target_class_name", "target_title",
)
if context.get(k)
}
if kwargs:
try:
return self.dump_fn(**kwargs)
except TypeError as exc:
if "unexpected keyword" not in str(exc) and "positional" not in str(exc):
raise
return self.dump_fn()
def _run(self) -> None:
while not self._stop.is_set():
try:
first = self._queue.get(timeout=0.2)
except queue.Empty:
continue
if first is None:
self._queue.task_done()
self._stop.set()
break
batch = [first]
stop_after_batch = False
deadline = time.monotonic() + self.coalesce_sec
while time.monotonic() < deadline:
try:
item = self._queue.get(timeout=max(0.01, deadline - time.monotonic()))
except queue.Empty:
break
if item is None:
self._queue.task_done()
stop_after_batch = True
break
batch.append(item)
groups: dict[str, list[EvidenceRequest]] = {}
for item in batch:
key = str(item.context.get("page_id") or "active")
groups.setdefault(key, []).append(item)
for items in groups.values():
latest = items[-1]
try:
dump = self._dump(latest.context)
if self.on_dump is not None:
self.on_dump(dump)
self.capture_count += 1
# A desktop provider dump can take seconds. Actions that
# arrived while it was running are still earlier than this
# eventual observation, so fold them into the same result
# instead of immediately launching another identical dump.
if str(getattr(self.manager.action_flow, "surface", "")) == "windows":
while True:
try:
late = self._queue.get_nowait()
except queue.Empty:
break
if late is None:
self._queue.task_done()
stop_after_batch = True
break
batch.append(late)
items.append(late)
if not self._stop.is_set():
self.manager.attach_eventual_evidence(
[item.action_id for item in items], dump=dump,
requested_monotonic=min(item.requested_monotonic for item in items),
)
except Exception as exc:
self.failure_count += 1
if not self._stop.is_set():
self.manager.note_evidence_failure([item.action_id for item in items], str(exc))
for _item in batch:
self._queue.task_done()
if stop_after_batch:
self._stop.set()
def _run_urgent(self) -> None:
"""Capture transient native popups independently of a slow main dump."""
while not self._stop.is_set():
try:
first = self._urgent_queue.get(timeout=0.1)
except queue.Empty:
continue
if first is None:
self._urgent_queue.task_done()
break
batch = [first]
# A very short coalesce keeps double-clicks together without waiting
# long enough for a search-result popup to disappear.
deadline = time.monotonic() + 0.04
while time.monotonic() < deadline:
try:
item = self._urgent_queue.get_nowait()
except queue.Empty:
time.sleep(0.005)
continue
if item is None:
self._urgent_queue.task_done()
break
if item.context.get("target_hwnd") == first.context.get("target_hwnd"):
batch.append(item)
else:
# Preserve a different transient window for the next pass.
try:
self._urgent_queue.put_nowait(item)
except queue.Full:
self.dropped += 1
self._urgent_queue.task_done()
break
try:
dump = self._dump(batch[-1].context)
if self.on_dump is not None:
self.on_dump(dump)
self.capture_count += 1
if not self._stop.is_set():
self.manager.attach_eventual_evidence(
[item.action_id for item in batch],
dump=dump,
requested_monotonic=min(item.requested_monotonic for item in batch),
)
except Exception as exc:
self.failure_count += 1
if not self._stop.is_set():
self.manager.note_evidence_failure(
[item.action_id for item in batch], str(exc)
)
finally:
for _item in batch:
self._urgent_queue.task_done()
def stop(self, *, timeout: float = 35.0) -> None:
if not self._thread:
return
# Drain the transient-window lane first. A WeCom main-window dump can
# be stuck in an uninterruptible provider call for tens of seconds; it
# must not cancel the short-lived popup evidence when the user ends the
# flow immediately after selecting a result.
if self._urgent_thread:
try:
self._urgent_queue.put(None, timeout=1.0)
except queue.Full:
pass
self._urgent_thread.join(timeout=min(max(timeout, 3.0), 8.0))
try:
self._queue.put(None, timeout=2.0)
except queue.Full:
pass
self._thread.join(timeout=timeout)
if self._thread.is_alive():
# A provider dump may be uninterruptible. Prevent a late result
# from mutating a manager which is already being finalized.
self._stop.set()
self._thread = None
self._urgent_thread = None