fix: window activation bug - verify PID before using cached HWND

Root cause: _ensure_hwnd cached HWND without verifying the owning process.
When target app exits, OS reuses HWND value for another process, causing
activation of wrong window (e.g. browser/editor disguised as QQ/WeChat).

Changes:
- Add _validate_wechat_window() to verify process name == WeChat.exe
- _ensure_hwnd cache hit: verify HWND belongs to target process via PID
- TXGuiFoundation fallback: verify PID belongs to QQ.exe/TIM.exe
- _find_qq_window_fresh: same PID verification for TXGuiFoundation
- _ocr_read_wechat: validate window before OCR capture
- Raise min window size threshold from 200x100 to 300x200 everywhere
@
This commit is contained in:
youpaishidifu
2026-08-03 14:47:23 +08:00
parent eb5a260534
commit b07fc75d99
+76 -9
View File
@@ -684,7 +684,15 @@ class MessageMonitor:
try:
if hwnd:
if win32gui.IsWindow(hwnd):
return hwnd
# 验证窗口进程是否仍属于目标平台
if platform == "wechat":
if self._validate_wechat_window(hwnd):
return hwnd
elif platform == "qq":
if self._validate_qq_window(hwnd):
return hwnd
else:
return hwnd
except Exception:
pass
@@ -702,7 +710,7 @@ class MessageMonitor:
w = auto.WindowControl(Name=name)
if w.Exists(maxSearchSeconds=0.5):
rect = w.BoundingRectangle
if rect.width() > 200 and rect.height() > 100:
if rect.width() > 300 and rect.height() > 200:
h = w.NativeWindowHandle
# 确保存顶层窗口句柄(与 GetForegroundWindow 返回值一致)
top = win32gui.GetAncestor(h, win32con.GA_ROOT)
@@ -745,7 +753,7 @@ class MessageMonitor:
if not (win32gui.IsWindow(top) and win32gui.IsWindowVisible(top)):
continue
rect = win32gui.GetWindowRect(top)
if (rect[2] - rect[0]) > 200 and (rect[3] - rect[1]) > 100:
if (rect[2] - rect[0]) > 300 and (rect[3] - rect[1]) > 200:
self._cached_hwnd[platform] = top
self._cached_pid[platform] = pid
self._window_not_found_until.pop(platform, None)
@@ -781,7 +789,7 @@ class MessageMonitor:
if not (win32gui.IsWindow(top) and win32gui.IsWindowVisible(top)):
continue
rect = win32gui.GetWindowRect(top)
if (rect[2] - rect[0]) > 200 and (rect[3] - rect[1]) > 100:
if (rect[2] - rect[0]) > 300 and (rect[3] - rect[1]) > 200:
variant = "tim" if "TIM" in proc_name.upper() else "qq_nt"
self._app_variant[platform] = variant
self._cached_hwnd[platform] = top
@@ -799,15 +807,31 @@ class MessageMonitor:
try:
if child.ClassName == "TXGuiFoundation":
rect = child.BoundingRectangle
if rect.width() > 200 and rect.height() > 100:
if rect.width() > 300 and rect.height() > 200:
h = child.NativeWindowHandle
top = win32gui.GetAncestor(h, win32con.GA_ROOT)
pid = win32process.GetWindowThreadProcessId(top)[1]
# 验证进程是否属于 QQ/TIM
try:
hproc = win32api.OpenProcess(
0x0400 | 0x0010, # PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
False, pid
)
if hproc:
exe_path = win32process.GetModuleFileNameEx(hproc, 0)
win32api.CloseHandle(hproc)
exe_name = exe_path.split('\\')[-1].lower() if exe_path else ''
if exe_name not in ('qq.exe', 'tim.exe'):
continue # 不是QQ/TIM,跳过
else:
continue # 无法打开进程,跳过
except Exception:
continue # 验证失败,跳过
self._app_variant[platform] = "qq_nt"
self._cached_hwnd[platform] = top
self._cached_pid[platform] = pid
self._window_not_found_until.pop(platform, None)
logger.info(f"[{platform}] 窗口已定位(UIA TXGuiFoundation): top={top}")
logger.info(f"[{platform}] 窗口已定位(UIA TXGuiFoundation): top={top} pid={pid}")
return top
except Exception:
continue
@@ -873,7 +897,7 @@ class MessageMonitor:
w = auto.WindowControl(Name=name)
if w.Exists(maxSearchSeconds=0.2):
rect = w.BoundingRectangle
if rect.width() > 200 and rect.height() > 100:
if rect.width() > 300 and rect.height() > 200:
return w
except Exception:
pass
@@ -883,7 +907,25 @@ class MessageMonitor:
try:
if child.ClassName == "TXGuiFoundation":
rect = child.BoundingRectangle
if rect.width() > 200 and rect.height() > 100:
if rect.width() > 300 and rect.height() > 200:
# 验证进程是否属于 QQ/TIM
h = child.NativeWindowHandle
top = win32gui.GetAncestor(h, win32con.GA_ROOT)
pid = win32process.GetWindowThreadProcessId(top)[1]
try:
hproc = win32api.OpenProcess(
0x0400 | 0x0010, False, pid
)
if hproc:
exe_path = win32process.GetModuleFileNameEx(hproc, 0)
win32api.CloseHandle(hproc)
exe_name = exe_path.split('\\')[-1].lower() if exe_path else ''
if exe_name not in ('qq.exe', 'tim.exe'):
continue
else:
continue
except Exception:
continue
return child
except Exception:
pass
@@ -1226,6 +1268,31 @@ class MessageMonitor:
pass
return False
def _validate_wechat_window(self, hwnd: int) -> bool:
"""验证窗口是否确实是微信进程。"""
if not win32gui.IsWindow(hwnd):
return False
if not win32gui.IsWindowVisible(hwnd):
return False
if win32gui.IsIconic(hwnd):
return False
# 验证进程名
try:
_, pid = win32process.GetWindowThreadProcessId(hwnd)
hproc = win32api.OpenProcess(
0x0400 | 0x0010, # PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
False, pid
)
if hproc:
exe_path = win32process.GetModuleFileNameEx(hproc, 0)
win32api.CloseHandle(hproc)
exe_name = exe_path.split('\\')[-1].lower() if exe_path else ''
if exe_name in ('wechat.exe',):
return True
except Exception:
pass
return False
@staticmethod
def _fuzzy_duplicate(new_text: str, seen_set: set, threshold: float = 0.80) -> bool:
"""检查 new_text 是否与 seen_set 中已有文本相似度超阈值。
@@ -1945,7 +2012,7 @@ class MessageMonitor:
hwnd = self._cached_hwnd.get("wechat")
if not hwnd:
return []
if not win32gui.IsWindow(hwnd):
if not win32gui.IsWindow(hwnd) or not self._validate_wechat_window(hwnd):
self._cached_hwnd.pop("wechat", None)
self._cached_pid.pop("wechat", None)
return []