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:
@@ -508,6 +508,70 @@ def prefer_last_active_popup(info: WindowInfo) -> WindowInfo:
|
||||
return info
|
||||
|
||||
|
||||
def window_info_under_cursor(
|
||||
*,
|
||||
exclude_hwnds: Optional[set[int]] = None,
|
||||
) -> Optional[WindowInfo]:
|
||||
"""光标下的顶层窗(不 Activate、不点击)。光标在 Inspector 上则返回 None。"""
|
||||
exclude = {int(h) for h in (exclude_hwnds or set()) if h}
|
||||
exclude |= {get_toplevel_hwnd(h) for h in list(exclude)}
|
||||
pt = wintypes.POINT()
|
||||
if not user32.GetCursorPos(ctypes.byref(pt)):
|
||||
return None
|
||||
hwnd = int(user32.WindowFromPoint(pt) or 0)
|
||||
if not hwnd:
|
||||
return None
|
||||
top = get_toplevel_hwnd(hwnd)
|
||||
if not top or top in exclude:
|
||||
return None
|
||||
if not user32.IsWindowVisible(top) or user32.IsIconic(top) or _is_cloaked(top):
|
||||
return None
|
||||
try:
|
||||
info = window_info_from_hwnd(top)
|
||||
except Exception:
|
||||
return None
|
||||
if is_inspector_window(info):
|
||||
return None
|
||||
if "UI Inspector" in (info.title or "") or "OpenClaw UI Inspector" in (info.title or ""):
|
||||
return None
|
||||
return info
|
||||
|
||||
|
||||
def find_wecom_search_result_window(
|
||||
*,
|
||||
pid: Optional[int] = None,
|
||||
exclude_hwnds: Optional[set[int]] = None,
|
||||
) -> Optional[WindowInfo]:
|
||||
"""同进程可见的 SearchResultWindow2(搜索联系人浮层),不要求前台焦点。"""
|
||||
exclude = {int(h) for h in (exclude_hwnds or set()) if h}
|
||||
exclude |= {get_toplevel_hwnd(h) for h in list(exclude)}
|
||||
best: Optional[WindowInfo] = None
|
||||
best_area = -1
|
||||
for hwnd in iter_toplevel_hwnds_zorder():
|
||||
if hwnd in exclude:
|
||||
continue
|
||||
if not user32.IsWindowVisible(hwnd) or user32.IsIconic(hwnd) or _is_cloaked(hwnd):
|
||||
continue
|
||||
cls_buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, cls_buf, 256)
|
||||
if (cls_buf.value or "") != "SearchResultWindow2":
|
||||
continue
|
||||
try:
|
||||
info = window_info_from_hwnd(hwnd)
|
||||
except Exception:
|
||||
continue
|
||||
if pid is not None and int(info.pid) != int(pid):
|
||||
continue
|
||||
rect = _window_rect(hwnd) or {}
|
||||
area = int(rect.get("width") or 0) * int(rect.get("height") or 0)
|
||||
if area < 40 * 40:
|
||||
continue
|
||||
if area > best_area:
|
||||
best_area = area
|
||||
best = info
|
||||
return best
|
||||
|
||||
|
||||
def foreground_toplevel_hwnd(*, exclude_hwnds: Optional[set[int]] = None) -> Optional[int]:
|
||||
"""True foreground top-level hwnd (GetForegroundWindow), if capturable."""
|
||||
exclude = {int(h) for h in (exclude_hwnds or set()) if h}
|
||||
@@ -530,16 +594,38 @@ def find_capture_target(
|
||||
*,
|
||||
exclude_hwnds: Optional[set[int]] = None,
|
||||
prefer_pid: Optional[int] = None,
|
||||
under_cursor: bool = False,
|
||||
prefer_wecom_search_popup: bool = True,
|
||||
) -> tuple[WindowInfo, list[WindowInfo]]:
|
||||
"""
|
||||
Pick the frontmost capturable window.
|
||||
Pick a capturable top-level window.
|
||||
|
||||
Priority:
|
||||
1) GetForegroundWindow (true frontmost), if capturable
|
||||
Default (under_cursor=False) keeps legacy behaviour:
|
||||
1) GetForegroundWindow, if capturable
|
||||
2) else Z-order top capturable candidate
|
||||
Optional prefer_pid only filters when sticky (same front app pid);
|
||||
never jumps to another app just because it is larger.
|
||||
3) prefer_last_active_popup (same pid)
|
||||
|
||||
Optional:
|
||||
- under_cursor: 光标下顶层窗优先(不点击、不抢焦点);失败则回退前台逻辑
|
||||
- prefer_wecom_search_popup: 若候选为企微主窗且存在 SearchResultWindow2,改采浮层
|
||||
"""
|
||||
info, cands, _meta = resolve_capture_target(
|
||||
exclude_hwnds=exclude_hwnds,
|
||||
prefer_pid=prefer_pid,
|
||||
under_cursor=under_cursor,
|
||||
prefer_wecom_search_popup=prefer_wecom_search_popup,
|
||||
)
|
||||
return info, cands
|
||||
|
||||
|
||||
def resolve_capture_target(
|
||||
*,
|
||||
exclude_hwnds: Optional[set[int]] = None,
|
||||
prefer_pid: Optional[int] = None,
|
||||
under_cursor: bool = False,
|
||||
prefer_wecom_search_popup: bool = True,
|
||||
) -> tuple[WindowInfo, list[WindowInfo], dict[str, Any]]:
|
||||
"""与 find_capture_target 相同,额外返回 selection meta(strategy 链)。"""
|
||||
candidates = list_capturable_candidates(exclude_hwnds=exclude_hwnds)
|
||||
if not candidates:
|
||||
raise RuntimeError(
|
||||
@@ -548,21 +634,31 @@ def find_capture_target(
|
||||
|
||||
by_hwnd = {int(c.hwnd): c for c in candidates}
|
||||
chosen: Optional[WindowInfo] = None
|
||||
strategy_parts: list[str] = []
|
||||
|
||||
fg = foreground_toplevel_hwnd(exclude_hwnds=exclude_hwnds)
|
||||
if fg and fg in by_hwnd:
|
||||
# Frontmost always wins — never replace with a larger/sticky other app.
|
||||
chosen = by_hwnd[fg]
|
||||
elif fg:
|
||||
# Foreground exists but filtered from candidates — still try to describe it.
|
||||
try:
|
||||
info = window_info_from_hwnd(fg)
|
||||
if not is_inspector_window(info) and not (
|
||||
is_system_chrome_window(info) and not (info.title or "").strip()
|
||||
):
|
||||
chosen = info
|
||||
except Exception:
|
||||
chosen = None
|
||||
if under_cursor:
|
||||
cur = window_info_under_cursor(exclude_hwnds=exclude_hwnds)
|
||||
if cur is not None:
|
||||
chosen = cur
|
||||
strategy_parts.append("under_cursor")
|
||||
|
||||
if chosen is None:
|
||||
fg = foreground_toplevel_hwnd(exclude_hwnds=exclude_hwnds)
|
||||
if fg and fg in by_hwnd:
|
||||
# Frontmost always wins — never replace with a larger/sticky other app.
|
||||
chosen = by_hwnd[fg]
|
||||
strategy_parts.append("foreground")
|
||||
elif fg:
|
||||
# Foreground exists but filtered from candidates — still try to describe it.
|
||||
try:
|
||||
info = window_info_from_hwnd(fg)
|
||||
if not is_inspector_window(info) and not (
|
||||
is_system_chrome_window(info) and not (info.title or "").strip()
|
||||
):
|
||||
chosen = info
|
||||
strategy_parts.append("foreground_raw")
|
||||
except Exception:
|
||||
chosen = None
|
||||
|
||||
if chosen is None:
|
||||
# No usable foreground: Z-order front. prefer_pid only as soft filter here.
|
||||
@@ -570,15 +666,35 @@ def find_capture_target(
|
||||
for c in candidates:
|
||||
if int(c.pid) == int(prefer_pid):
|
||||
chosen = c
|
||||
strategy_parts.append("sticky_pid")
|
||||
break
|
||||
if chosen is None:
|
||||
chosen = candidates[0]
|
||||
strategy_parts.append("zorder")
|
||||
|
||||
# Owned popup of the *same* front window only (e.g. search panel), never another app
|
||||
popup = prefer_last_active_popup(chosen)
|
||||
if int(popup.pid) == int(chosen.pid):
|
||||
if int(popup.pid) == int(chosen.pid) and int(popup.hwnd) != int(chosen.hwnd):
|
||||
chosen = popup
|
||||
return chosen, candidates
|
||||
strategy_parts.append("last_active_popup")
|
||||
|
||||
# 企微:主窗为前台时,若搜索浮层仍打开,优先采浮层(无需点击进会话)
|
||||
if prefer_wecom_search_popup and (chosen.class_name or "") == "WeWorkWindow":
|
||||
search_win = find_wecom_search_result_window(
|
||||
pid=int(chosen.pid), exclude_hwnds=exclude_hwnds
|
||||
)
|
||||
if search_win is not None:
|
||||
chosen = search_win
|
||||
strategy_parts.append("wecom_search_popup")
|
||||
|
||||
meta = {
|
||||
"strategy": "+".join(strategy_parts) if strategy_parts else "unknown",
|
||||
"under_cursor": bool(under_cursor),
|
||||
"prefer_wecom_search_popup": bool(prefer_wecom_search_popup),
|
||||
"chosen_class": chosen.class_name,
|
||||
"chosen_hwnd": int(chosen.hwnd),
|
||||
}
|
||||
return chosen, candidates, meta
|
||||
|
||||
|
||||
def wait_frontmost_target(
|
||||
@@ -632,8 +748,6 @@ def prepare_frontmost_for_capture(
|
||||
|
||||
企微:先静默调用 kit ``ensure_ui_ready("wecom")``(与技能同路径),再轻量 poke。
|
||||
"""
|
||||
import kit_wake
|
||||
|
||||
info, cands, wait_meta = wait_frontmost_target(
|
||||
exclude_hwnds=exclude_hwnds, timeout_sec=timeout_sec
|
||||
)
|
||||
@@ -642,28 +756,29 @@ def prepare_frontmost_for_capture(
|
||||
# Keep operator size/position; only restore if minimized.
|
||||
act = activate_main_hwnd(info.hwnd, maximize=False)
|
||||
|
||||
# Product adapters may need to make their accessibility provider ready
|
||||
# before the operator starts typing. Running this on the app's existing
|
||||
# startup worker keeps Tk responsive and prevents the first input
|
||||
# transaction from racing a 10-20 second background warmup.
|
||||
import kit_wake
|
||||
|
||||
kit_report = kit_wake.auto_ensure_if_wecom(
|
||||
class_name=info.class_name or "",
|
||||
title=info.title or "",
|
||||
hwnd=info.hwnd,
|
||||
force=True,
|
||||
)
|
||||
findall = kit_report.get("findall") if kit_report.get("ok") else None
|
||||
provider_cold = bool(kit_report.get("applicable") and not kit_report.get("ok"))
|
||||
|
||||
warm = warmup_uia_provider(
|
||||
info.hwnd, interact_window=False, allow_foreground_nudge=False
|
||||
)
|
||||
n = int(warm.get("findall_after") or findall_count_for_hwnd(info.hwnd) or 0)
|
||||
if kit_report.get("ok") and int(kit_report.get("findall") or 0) > n:
|
||||
n = int(kit_report["findall"])
|
||||
return {
|
||||
"ready": True, # frontmost resolved => ready to capture that window
|
||||
"window": info.to_dict(),
|
||||
"candidates": len(cands),
|
||||
"findall": n,
|
||||
"uia_sparse": n < 8,
|
||||
"provider_cold": is_provider_cold(info.hwnd, findall=n),
|
||||
"findall": findall,
|
||||
"uia_sparse": bool(provider_cold),
|
||||
"provider_cold": provider_cold,
|
||||
"activate": act,
|
||||
"warmup": warm,
|
||||
"warmup": {"mode": "startup_adapter" if kit_report.get("applicable") else "deferred"},
|
||||
"kit_wake": kit_report,
|
||||
"wait": wait_meta,
|
||||
"strategy": "foreground_only",
|
||||
@@ -933,6 +1048,7 @@ def dump_window_tree(
|
||||
warmup_mode:
|
||||
- full: MSAA/UIA poke on target; restore only if minimized
|
||||
- light: ElementFromHandle poke only (no ShowWindow)
|
||||
- probe: light poke + FindAll, but never invoke a blocking product wake
|
||||
- off: no provider warmup
|
||||
Child / same-PID merges never call ShowWindow (light poke at most).
|
||||
"""
|
||||
@@ -942,7 +1058,7 @@ def dump_window_tree(
|
||||
mode = (warmup_mode or "full").lower()
|
||||
if not warmup:
|
||||
mode = "off"
|
||||
if mode not in {"full", "light", "off"}:
|
||||
if mode not in {"full", "light", "probe", "off"}:
|
||||
mode = "full"
|
||||
|
||||
kit_report: dict[str, Any] = {"applicable": False, "skipped": True, "reason": "not_checked"}
|
||||
@@ -950,7 +1066,11 @@ def dump_window_tree(
|
||||
winfo_for_kit = window_info_from_hwnd(hwnd)
|
||||
except Exception:
|
||||
winfo_for_kit = None
|
||||
if winfo_for_kit is not None:
|
||||
# 搜索浮层稀疏时勿合并主窗会话列表,否则验收会误判「已有数据」
|
||||
allow_same_pid_merge = bool(merge_same_pid_if_sparse)
|
||||
if winfo_for_kit is not None and (winfo_for_kit.class_name or "") == "SearchResultWindow2":
|
||||
allow_same_pid_merge = False
|
||||
if winfo_for_kit is not None and mode not in {"off", "probe"}:
|
||||
hint = findall_count_for_hwnd(hwnd)
|
||||
kit_report = kit_wake.auto_ensure_if_wecom(
|
||||
class_name=winfo_for_kit.class_name or "",
|
||||
@@ -977,7 +1097,7 @@ def dump_window_tree(
|
||||
warmup_report = warmup_uia_provider(
|
||||
hwnd, interact_window=True, allow_foreground_nudge=True
|
||||
)
|
||||
elif mode == "light":
|
||||
elif mode in {"light", "probe"}:
|
||||
warmup_report = poke_uia_provider(hwnd)
|
||||
|
||||
def _run_dump() -> tuple[dict[str, Any], list[dict[str, Any]], dict[str, Any]]:
|
||||
@@ -1035,7 +1155,7 @@ def dump_window_tree(
|
||||
t = dict(t)
|
||||
t["children"] = children_nodes
|
||||
|
||||
if use_findall:
|
||||
if use_findall and mode != "off":
|
||||
found = collect_findall_flat(root_ctrl, max_nodes=max_nodes)
|
||||
st["findall_count"] = len(found)
|
||||
existing = {r.get("path_hint") for r in idx}
|
||||
@@ -1047,6 +1167,13 @@ def dump_window_tree(
|
||||
return t, idx, st
|
||||
|
||||
tree, index, stats = _run_dump()
|
||||
if winfo_for_kit is not None:
|
||||
kit_wake.note_provider_warm(
|
||||
class_name=winfo_for_kit.class_name or "",
|
||||
title=winfo_for_kit.title or "",
|
||||
hwnd=hwnd,
|
||||
findall=int(stats.get("findall_count") or 0),
|
||||
)
|
||||
stats["warmup"] = warmup_report
|
||||
stats["warmup_mode"] = mode
|
||||
stats["kit_wake"] = kit_report
|
||||
@@ -1061,7 +1188,7 @@ def dump_window_tree(
|
||||
)
|
||||
|
||||
# 仍冷且目标为企微:强制再 ensure 一次(绕过短时 cache)后重 dump
|
||||
if stats["provider_cold"] and winfo_for_kit is not None and kit_wake.looks_like_wecom(
|
||||
if mode not in {"off", "probe"} and stats["provider_cold"] and winfo_for_kit is not None and kit_wake.looks_like_wecom(
|
||||
class_name=winfo_for_kit.class_name or "",
|
||||
title=winfo_for_kit.title or "",
|
||||
):
|
||||
@@ -1117,7 +1244,9 @@ def dump_window_tree(
|
||||
|
||||
# Sparse fallback: light poke + FindAll on a few large same-pid top-levels.
|
||||
# Includes WeWorkWindow when current target is a thin popup.
|
||||
if merge_same_pid_if_sparse and stats["uia_sparse"]:
|
||||
# Never merge into SearchResultWindow2 (see allow_same_pid_merge).
|
||||
stats["merge_same_pid_allowed"] = allow_same_pid_merge
|
||||
if allow_same_pid_merge and stats["uia_sparse"]:
|
||||
try:
|
||||
info = window_info_from_hwnd(hwnd)
|
||||
merged_n = 0
|
||||
@@ -1329,15 +1458,20 @@ def format_tree_text(tree: dict[str, Any], *, max_lines: int = 2000) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Back-compat alias used by older app versions
|
||||
def resolve_capture_target(
|
||||
# Back-compat: older callers expected a single WindowInfo (foreground only).
|
||||
# Do NOT name this resolve_capture_target — that name is the full 3-tuple API above.
|
||||
def resolve_frontmost_window(
|
||||
*,
|
||||
self_hwnd: int = 0,
|
||||
poll_sec: float = 0.15,
|
||||
wait_sec: float = 2.0,
|
||||
) -> WindowInfo:
|
||||
_ = poll_sec, wait_sec
|
||||
info, _cands = find_capture_target(exclude_hwnds={int(self_hwnd)} if self_hwnd else None)
|
||||
info, _cands = find_capture_target(
|
||||
exclude_hwnds={int(self_hwnd)} if self_hwnd else None,
|
||||
under_cursor=False,
|
||||
prefer_wecom_search_popup=False,
|
||||
)
|
||||
return info
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user