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

285 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""调用 jiangchang_desktop_wake(只调不复制注入逻辑)。
- 主窗 WeWorkWindow → ``ensure_ui_ready("wecom")``
- 搜索浮层 SearchResultWindow2 → ``ensure_ui_ready("wecom_search")``
未安装 kit 时降级为跳过,不阻断采集。
Warm cache key = profile + hwnd(经 WeCom Adapter),避免主窗 warm
状态错误代表 SearchResultWindow2 已暖。
"""
from __future__ import annotations
import time
from typing import Any, Optional
# A successful provider probe remains valid for the lifetime of a normal
# recording flow. The old 45 second TTL made long desktop flows re-run an
# expensive WeCom wake in the middle of recording and produced minute-late
# evidence.
_CACHE_TTL_SEC = 600.0
# cache_key -> cache (profile:hwnd:N)
_caches: dict[str, dict[str, Any]] = {}
_PROFILE_WECOM = "wecom"
_PROFILE_WECOM_SEARCH = "wecom_search"
_THRESHOLDS = {
_PROFILE_WECOM: 80,
_PROFILE_WECOM_SEARCH: 20,
}
def _wecom_adapter():
try:
from adapters.wecom import wecom_adapter
return wecom_adapter
except Exception:
try:
from .adapters.wecom import wecom_adapter # type: ignore
return wecom_adapter
except Exception:
return None
def resolve_wake_profile(*, class_name: str = "", title: str = "") -> Optional[str]:
"""返回应调用的 kit profile id;非企微相关则 None。"""
ad = _wecom_adapter()
if ad is not None:
return ad.wake_profile(class_name=class_name, title=title)
# Compatibility fallback if adapter import fails
cls = (class_name or "").strip()
title = title or ""
if cls == "SearchResultWindow2":
return _PROFILE_WECOM_SEARCH
if cls == "WeWorkWindow":
return _PROFILE_WECOM
if "WeWork" in cls and ("企业微信" in title or "WeCom" in title or "wecom" in title.lower()):
return _PROFILE_WECOM
return None
def looks_like_wecom(*, class_name: str = "", title: str = "") -> bool:
return resolve_wake_profile(class_name=class_name, title=title) is not None
def kit_available() -> bool:
try:
from jiangchang_desktop_wake import ensure_ui_ready # noqa: F401
return True
except ImportError:
return False
def _cache_key(profile: str, hwnd: Optional[int]) -> str:
ad = _wecom_adapter()
if ad is not None:
return ad.cache_key(profile=profile, hwnd=int(hwnd or 0))
return f"{profile}:hwnd:{int(hwnd or 0)}"
def auto_ensure_if_wecom(
*,
class_name: str = "",
title: str = "",
hwnd: Optional[int] = None,
force: bool = False,
findall_hint: Optional[int] = None,
) -> dict[str, Any]:
"""若目标像企微主窗/搜索浮层则静默 ensure;否则跳过。"""
profile = resolve_wake_profile(class_name=class_name, title=title)
if profile is None:
return {
"applicable": False,
"skipped": True,
"reason": "not_wecom",
"available": kit_available(),
}
ad = _wecom_adapter()
warm_min = int(ad.warm_threshold(profile) if ad else _THRESHOLDS.get(profile, 80))
key = _cache_key(profile, hwnd)
cache = _caches.setdefault(
key, {"t": 0.0, "warm": False, "hwnd": int(hwnd or 0), "findall": 0, "profile": profile}
)
now = time.monotonic()
hint = int(findall_hint) if findall_hint is not None else -1
# Require same hwnd in cache — main window warm must not cover search popup.
cached_hwnd = int(cache.get("hwnd") or 0)
same_window = (not hwnd) or (cached_hwnd == int(hwnd))
if (
not force
and same_window
and cache.get("warm")
and (now - float(cache.get("t") or 0.0)) < _CACHE_TTL_SEC
):
return {
"applicable": True,
"available": True,
"skipped": True,
"reason": "cache_warm",
"ok": True,
"warm": True,
"wake_applied": False,
"findall": int(cache.get("findall") or 0),
"hwnd": int(cache.get("hwnd") or 0),
"profile": profile,
"cache_key": key,
"observed_findall_hint": hint,
}
# A direct probe is authoritative. Do not run an invasive wake merely
# because this process did not create the cache entry itself.
if not force and hint >= warm_min:
cache.update(
{"t": now, "warm": True, "hwnd": int(hwnd or 0), "findall": hint}
)
return {
"applicable": True,
"available": kit_available(),
"skipped": True,
"reason": "probe_warm",
"ok": True,
"warm": True,
"wake_applied": False,
"findall": hint,
"hwnd": int(hwnd or 0),
"profile": profile,
"cache_key": key,
}
try:
from jiangchang_desktop_wake import ensure_ui_ready
from jiangchang_desktop_wake.errors import DesktopWakeError
except ImportError:
return {
"applicable": True,
"available": False,
"skipped": True,
"reason": "kit_not_installed",
"ok": False,
"profile": profile,
"hint": "请安装 jiangchang-platform-kit>=1.2.6(含 wecom_search)后再采企微",
}
t0 = time.perf_counter()
try:
result = ensure_ui_ready(
profile,
hwnd=int(hwnd) if hwnd else None,
raise_on_cold=True,
)
except DesktopWakeError as exc:
raw = (getattr(exc, "code", None) or "").strip()
code = raw[6:] if raw.startswith("ERROR:") else (raw or "WAKE_FAILED")
return {
"applicable": True,
"available": True,
"skipped": False,
"ok": False,
"warm": False,
"wake_applied": False,
"error_code": code,
"message": str(getattr(exc, "message", None) or exc),
"elapsed_ms": int(round((time.perf_counter() - t0) * 1000)),
"profile": profile,
}
except Exception as exc:
return {
"applicable": True,
"available": True,
"skipped": False,
"ok": False,
"warm": False,
"error_code": "ENSURE_EXCEPTION",
"message": f"{type(exc).__name__}: {exc}",
"elapsed_ms": int(round((time.perf_counter() - t0) * 1000)),
"profile": profile,
}
after = result.probe_after
findall = int(after.findall_count) if after is not None else 0
report = {
"applicable": True,
"available": True,
"skipped": False,
"ok": bool(result.warm),
"warm": bool(result.warm),
"wake_applied": bool(result.wake_applied),
"findall": findall,
"hwnd": int(result.hwnd) if result.hwnd else 0,
"elapsed_ms": int(round((time.perf_counter() - t0) * 1000)),
"profile": profile,
"markers_found": list(after.markers_found) if after is not None else [],
}
report["cache_key"] = key
if result.warm:
# Persist under result hwnd so main≠search caches stay separate.
final_key = _cache_key(profile, report["hwnd"] or hwnd)
final_cache = _caches.setdefault(
final_key,
{"t": 0.0, "warm": False, "hwnd": int(report["hwnd"] or 0), "findall": 0, "profile": profile},
)
final_cache["t"] = now
final_cache["warm"] = True
final_cache["hwnd"] = int(report["hwnd"] or hwnd or 0)
final_cache["findall"] = findall
report["cache_key"] = final_key
else:
cache["warm"] = False
return report
def note_provider_warm(
*, class_name: str = "", title: str = "", hwnd: int = 0, findall: int = 0
) -> bool:
"""Pin a proven-warm provider so recorder evidence never re-wakes it."""
profile = resolve_wake_profile(class_name=class_name, title=title)
adapter = _wecom_adapter()
threshold = int(
adapter.warm_threshold(profile)
if adapter and profile
else _THRESHOLDS.get(profile or "", 80)
)
count = int(findall or 0)
if not profile or count < threshold:
return False
key = _cache_key(profile, hwnd)
_caches[key] = {
"t": time.monotonic(),
"warm": True,
"hwnd": int(hwnd or 0),
"findall": count,
"profile": profile,
}
return True
def format_kit_wake_line(report: Optional[dict[str, Any]]) -> str:
if not report:
return "kit_wake: (none)"
if not report.get("applicable"):
return "kit_wake: n/a (非企微)"
if report.get("reason") == "kit_not_installed":
return "kit_wake: 未安装 platform-kit(企微可能冷树)"
prof = report.get("profile") or "?"
if report.get("skipped") and report.get("reason") == "cache_warm":
return (
f"kit_wake[{prof}]: cache warm=True findall={report.get('findall')} "
f"hwnd={report.get('hwnd')}"
)
if report.get("ok"):
return (
f"kit_wake[{prof}]: warm={report.get('warm')} "
f"wake_applied={report.get('wake_applied')} "
f"findall={report.get('findall')} elapsed_ms={report.get('elapsed_ms')}"
)
return (
f"kit_wake[{prof}]: FAILED code={report.get('error_code')} "
f"msg={report.get('message')}"
)