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>
285 lines
9.2 KiB
Python
285 lines
9.2 KiB
Python
"""调用 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')}"
|
||
)
|