From 6a96111db6928ec89ef1e20d5fd001033953665e Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Sat, 25 Jul 2026 15:02:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DQQ=E8=AF=BB=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=E6=B6=88=E6=81=AF+=E5=BE=AE=E4=BF=A1=E5=89=8D=E5=8F=B0?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E5=A4=B1=E8=B4=A5(=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E5=9C=A8=E5=89=8D=E5=8F=B0=E4=BB=8D=E6=8A=A5=E9=94=99)\n\n?= =?UTF-8?q?=E9=97=AE=E9=A2=981:=20QQ(TIM)=E8=AF=BB=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=E6=B6=88=E6=81=AF\n=20=20=E6=A0=B9=E5=9B=A0:=20=5Ffocus=5Fmess?= =?UTF-8?q?age=5Farea=E5=9C=A8=E7=AA=97=E5=8F=A3=E6=9C=AA=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=E5=89=8DSendKeys({Esc})=E5=8F=91=E7=BB=99=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=AA=97=E5=8F=A3\n=20=20=E7=84=B6=E5=90=8E=5Fread=5F?= =?UTF-8?q?via=5Fclipboard=5Fonce=E5=8F=AA=E7=94=A8{Esc}=E6=9C=AA=E8=83=BD?= =?UTF-8?q?=E8=81=9A=E7=84=A6=E6=B6=88=E6=81=AF=E6=98=BE=E7=A4=BA=E5=8C=BA?= =?UTF-8?q?\n=20=20TIM=E4=B8=AD{Esc}=E5=8F=AA=E9=80=80=E5=87=BA=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=88=B0=E4=BC=9A=E8=AF=9D=E5=88=97=E8=A1=A8,?= =?UTF-8?q?=E9=9D=9E=E6=B6=88=E6=81=AF=E5=8C=BA=E5=9F=9F\n=20=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D:=20=E5=88=A0=5Ffocus=5Fmessage=5Farea(=E6=95=B4?= =?UTF-8?q?=E4=B8=AA=E6=96=B9=E6=B3=95=E7=AA=97=E5=8F=A3=E6=9C=AA=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=E6=97=B6=E6=97=A0=E6=84=8F=E4=B9=89)\n=20=20=5Fread?= =?UTF-8?q?=5Fvia=5Fclipboard=5Fonce=E4=B8=ADTIM=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=94=B9=E4=B8=BA:\n=20=20{Esc}{Esc}=E9=80=80=E5=87=BA?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E2=86=92{Down}=E9=80=89=E4=B8=AD=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E2=86=92{Enter}=E6=89=93=E5=BC=80(=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8C=BA)\n=20=20=E6=AD=A4=E6=97=B6Ctrl+A?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E6=B6=88=E6=81=AF=E5=86=85=E5=AE=B9=E8=80=8C?= =?UTF-8?q?=E9=9D=9E=E4=BC=9A=E8=AF=9D=E5=88=97=E8=A1=A8\n\n=E9=97=AE?= =?UTF-8?q?=E9=A2=982:=20=E5=BE=AE=E4=BF=A1=E5=B7=B2=E5=89=8D=E7=BD=AE?= =?UTF-8?q?=E4=BB=8D=E6=8A=A5=E7=9B=AE=E6=A0=87=E7=AA=97=E5=8F=A3=E4=B8=8D?= =?UTF-8?q?=E5=9C=A8=E5=89=8D=E5=8F=B0\n=20=20=E6=A0=B9=E5=9B=A0:=20=5Fcac?= =?UTF-8?q?hed=5Fhwnd=E5=AD=98=E5=82=A8UIA=20NativeWindowHandle(=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E6=98=AF=E5=AD=90=E7=AA=97=E5=8F=A3=E5=8F=A5=E6=9F=84?= =?UTF-8?q?)\n=20=20=E8=80=8CGetForegroundWindow()=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E9=A1=B6=E5=B1=82=E7=AA=97=E5=8F=A3=E5=8F=A5=E6=9F=84=E2=86=92?= =?UTF-8?q?hwnd=E4=B8=8D=E5=8C=B9=E9=85=8D\n=20=20=E4=BF=AE=E5=A4=8D:=20?= =?UTF-8?q?=5Fensure=5Fhwnd=E5=AD=98=E5=82=A8=E6=97=B6=E5=8A=A0GetAncestor?= =?UTF-8?q?(hwnd,GA=5FROOT)=E7=A1=AE=E4=BF=9D=E5=AD=98=E9=A1=B6=E5=B1=82?= =?UTF-8?q?=E5=8F=A5=E6=9F=84\n=20=20=5Fensure=5Fforeground=E6=94=B9?= =?UTF-8?q?=E7=94=A8root=5Fhwnd=E5=88=A4=E6=96=AD\n=20=20=E5=8A=A0SwitchTo?= =?UTF-8?q?ThisWindow=20fallback(=E6=9D=83=E9=99=90=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E6=AF=94SetForegroundWindow=E4=BD=8E)\n=20=20\n=E5=85=B6?= =?UTF-8?q?=E4=BB=96:=20.gitignore=E5=8A=A0tim=5Fdump.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + monitor.py | 102 ++++++++++++++++++----------------------------------- 2 files changed, 36 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index f94d0c8..ef2a243 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ local_config.json # Debug dumps .debug/ +tim_dump.txt # Logs *.log diff --git a/monitor.py b/monitor.py index 08a06f5..e0e1bca 100644 --- a/monitor.py +++ b/monitor.py @@ -138,9 +138,11 @@ class MessageMonitor: rect = w.BoundingRectangle if rect.width() > 200 and rect.height() > 100: h = w.NativeWindowHandle - self._cached_hwnd[platform] = h - logger.info(f"[{platform}] 窗口已定位: {name} handle={h}") - return h + # 确保存顶层窗口句柄(与 GetForegroundWindow 返回值一致) + top = win32gui.GetAncestor(h, win32con.GA_ROOT) + self._cached_hwnd[platform] = top + logger.info(f"[{platform}] 窗口已定位: {name} handle={top}") + return top except Exception: pass elif platform == "qq": @@ -151,10 +153,11 @@ class MessageMonitor: rect = w.BoundingRectangle if rect.width() > 200 and rect.height() > 100: h = w.NativeWindowHandle - self._cached_hwnd[platform] = h + top = win32gui.GetAncestor(h, win32con.GA_ROOT) + self._cached_hwnd[platform] = top self._app_variant[platform] = "qq_nt" if name == "QQ" else "tim" - logger.info(f"[{platform}] 窗口已定位(名称): {name}") - return h + logger.info(f"[{platform}] 窗口已定位(名称): {name} top={top}") + return top except Exception: pass # QQ NT/TIM → TXGuiFoundation 类名 @@ -166,12 +169,12 @@ class MessageMonitor: rect = child.BoundingRectangle if rect.width() > 200 and rect.height() > 100: h = child.NativeWindowHandle - # 判断变体:名称含"TIM"则为tim,否则qq_nt + top = win32gui.GetAncestor(h, win32con.GA_ROOT) cname = str(child.Name or "") self._app_variant[platform] = "tim" if "TIM" in cname.upper() else "qq_nt" - self._cached_hwnd[platform] = h - logger.info(f"[{platform}] 窗口已定位(TXGuiFoundation): {child.Name}") - return h + self._cached_hwnd[platform] = top + logger.info(f"[{platform}] 窗口已定位(TXGuiFoundation): {child.Name} top={top}") + return top except Exception: pass except Exception: @@ -234,66 +237,27 @@ class MessageMonitor: except Exception as e: logger.debug(f"[{platform}] 滚动到底失败: {e}") - def _focus_message_area(self, platform: str) -> bool: - """聚焦消息区:Esc清弹窗 → 点击会话列表选对话 → 点击消息区聚焦。 - - 两步点击确保焦点落在消息显示区域而非联系人/会话列表。 - SendMessage 同步发送(Electron 响应同步消息)。 - 不移动物理鼠标。 - """ - hwnd = self._cached_hwnd.get(platform) - if not hwnd: - return False - try: - rect = win32gui.GetWindowRect(hwnd) - if not rect: - return False - left, top, right, bottom = rect - w = right - left - h = bottom - top - if w < 100 or h < 100: - return False - - # 先从前景(_read_via_clipboard_once 已做,但再确保) - # Esc 退出搜索/弹出框,恢复默认焦点 - auto.SendKeys('{Esc}') - time.sleep(0.15) - auto.SendKeys('{Esc}') - time.sleep(0.15) - - # 1. 点击会话列表(左 ~15%)选中一个对话 - x1 = int(w * 0.15) - y1 = int(h * 0.35) - lp1 = win32api.MAKELONG(x1, y1) - win32gui.SendMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lp1) - win32gui.SendMessage(hwnd, win32con.WM_LBUTTONUP, 0, lp1) - time.sleep(0.3) - - # 2. 点击消息显示区(中右 ~60%)聚焦消息列表 - x2 = int(w * 0.60) - y2 = int(h * 0.40) - lp2 = win32api.MAKELONG(x2, y2) - win32gui.SendMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lp2) - win32gui.SendMessage(hwnd, win32con.WM_LBUTTONUP, 0, lp2) - time.sleep(0.3) - - logger.debug(f"[{platform}] 已聚焦消息区 (conv={x1},{y1} msg={x2},{y2})") - return True - except Exception as e: - logger.debug(f"[{platform}] 聚焦消息区失败: {e}") - return False - def _ensure_foreground(self, hwnd, max_retries=3) -> bool: """确保窗口在前台,重试多次。失败返回 False,不强行发送。""" + # 确保用顶层窗口句柄(与 GetForegroundWindow 返回值一致) + root_hwnd = win32gui.GetAncestor(hwnd, win32con.GA_ROOT) for i in range(max_retries): - self._attach_set_foreground(hwnd) + self._attach_set_foreground(root_hwnd) time.sleep(0.2) try: fg = win32gui.GetForegroundWindow() - if fg == hwnd: + if fg == root_hwnd: return True - # 目标窗口是前台窗口的子窗口也可 - if fg and win32gui.IsChild(hwnd, fg): + if fg and win32gui.IsChild(root_hwnd, fg): + return True + except Exception: + pass + # fallback: SwitchToThisWindow(比 SetForegroundWindow 权限要求低) + try: + ctypes.windll.user32.SwitchToThisWindow(root_hwnd, True) + time.sleep(0.15) + fg = win32gui.GetForegroundWindow() + if fg == root_hwnd or (fg and win32gui.IsChild(root_hwnd, fg)): return True except Exception: pass @@ -388,13 +352,18 @@ class MessageMonitor: if not self._ensure_foreground(hwnd, max_retries=2): return [] - # TIM/QQ NT: Esc 退出搜索/弹框,确保焦点在消息区域 + # TIM: 键盘导航到消息区域 variant = self._app_variant.get(platform, "") if variant == "tim": auto.SendKeys('{Esc}') - time.sleep(0.15) + time.sleep(0.1) auto.SendKeys('{Esc}') + time.sleep(0.1) + # 确保选中一个对话并打开(加载右侧消息) + auto.SendKeys('{Down}') time.sleep(0.15) + auto.SendKeys('{Enter}') + time.sleep(0.5) # 等消息加载 # Ctrl+A (全选) auto.SendKeys('{Ctrl}a') @@ -590,8 +559,7 @@ class MessageMonitor: texts = self._read_message_list(window) if texts: return texts - # TIM / QQ NT UIA 缓存失效: 聚焦消息区 → 剪贴板 - self._focus_message_area(platform) + # TIM / QQ NT UIA 缓存失效: 剪贴板法(内部含前置+键盘聚焦) texts = self._read_via_clipboard(platform) if texts: return texts