"""调用 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')}" )