From c835182a44bb7a1c5575be9aba846030d85b51bb Mon Sep 17 00:00:00 2001 From: cris Date: Thu, 23 Apr 2026 19:23:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=90=9E=E5=AE=9A=20?= =?UTF-8?q?=E5=BE=88=E9=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 5 +-- project.md | 11 +++++- wechat_clicker/ax_bridge.py | 64 +++++++++++++++++++++++++-------- wechat_clicker/state_machine.py | 14 ++++++-- wechat_clicker/wechat_ui.py | 42 ++++++++++++++-------- 5 files changed, 102 insertions(+), 34 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a6b6b43..6d3878d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,8 @@ wechat_clicker/ - "..."按钮搜索限制在预览区域(独立窗口或主窗口 x>200),排除侧边栏 - 媒体去重基于消息列表**子元素索引**(child index),同一元素跨滚动轮次只处理一次 - 可见性检查基于元素**中心点**是否在消息列表可见区域内(30px margin) -- 状态检测基于窗口计数:1 窗口=聊天列表,2 窗口=有预览/会话打开 +- 状态检测基于窗口计数 + **AXMinimized 属性**:区分窗口存在但最小化 vs 真正可见 +- **窗口恢复策略**:NSRunningApplication.unhide(隐藏)→ AX API 设置 AXMinimized=False(最小化)→ activate(前台);AppleScript 不可靠(微信不支持 miniaturized 属性) ## 使用方法 @@ -69,6 +70,6 @@ python main.py --debug # 详细日志 ## 注意事项 -- 微信窗口最小化时工具会自动尝试恢复(通过 AppleScript 取消最小化) +- 微信窗口最小化或隐藏时工具会自动恢复(通过 AX API 设置 AXMinimized=False + NSRunningApplication activate) - 运行时会占用微信前台操作 - 建议在专用电脑上运行 diff --git a/project.md b/project.md index 4016fee..32f2c73 100644 --- a/project.md +++ b/project.md @@ -53,9 +53,18 @@ - [x] **已验证: 完整图片处理流程** — 蜗牛聊天 4 张不同尺寸图片(289px、155px、180px)全部成功:缩略图→预览窗口→...按钮→使用预览打开→关闭 Preview→恢复 - [x] **已验证: 带滚动的媒体处理** — scroll_to_bottom 正确到达最新消息,向上滚动正确加载历史,去重正确(processed_indices=[45,47,49,50]) +### v0.6.0 最小化窗口恢复修复 (2026/04/23) + +- [x] **修复: 最小化时恢复失败(根本原因)** — 原 AppleScript `tell application "WeChat"` 方案存在两个致命问题:(1) 微信的 localizedName 是"微信"不是"WeChat",AppleScript 找错应用或静默失败导致打开了别的窗口;(2) 微信不支持标准 AppleScript `miniaturized` 属性(返回 error -10006) +- [x] **修复: 最小化检测遗漏** — 微信窗口最小化后 AXWindows 仍返回该窗口(AXMinimized=True),原 `detect_state` 和 `ensure_wechat_visible` 未检查此属性,误判窗口已可见 +- [x] **改用 AX API 恢复窗口** — 通过 `AXUIElementSetAttributeValue(window, "AXMinimized", False)` 取消最小化,比 AppleScript 更可靠 +- [x] **多策略窗口恢复** — unhide(Cmd+H隐藏)→ AXMinimized=False(最小化)→ activate(前台),覆盖所有不可见场景 +- [x] **恢复重试与验证** — `ensure_wechat_visible` 增加 3 次重试循环,每次验证主窗口 AXMinimized=False 后才返回成功 +- [x] **已验证** — 两次完整测试通过:最小化微信→运行 `--once --debug`→自动恢复→正常扫描处理 + ### 待验证 -- [ ] 验证最小化窗口自动恢复功能(需手动最小化微信窗口测试) +- [x] ~~验证最小化窗口自动恢复功能(需手动最小化微信窗口测试)~~ - [ ] 有未读图片消息时的完整 --once 自动化流程 - [ ] 长时间运行稳定性测试 diff --git a/wechat_clicker/ax_bridge.py b/wechat_clicker/ax_bridge.py index 5b78fc8..5e64778 100644 --- a/wechat_clicker/ax_bridge.py +++ b/wechat_clicker/ax_bridge.py @@ -5,7 +5,6 @@ import logging import re -import subprocess import time from ApplicationServices import ( @@ -16,6 +15,7 @@ from ApplicationServices import ( AXUIElementCopyAttributeNames, AXUIElementCopyActionNames, AXUIElementPerformAction, + AXUIElementSetAttributeValue, ) from Cocoa import ( NSRunningApplication, @@ -108,21 +108,57 @@ class AXBridge: apps = NSRunningApplication.runningApplicationsWithBundleIdentifier_(bundle_id) return apps is not None and len(apps) > 0 - def unminimize_app(self, app_name: str) -> bool: - """通过 AppleScript 取消应用窗口的最小化状态。""" - script = ( - f'tell application "{app_name}" to ' - f'set miniaturized of every window to false' - ) + def unminimize_app(self, bundle_id: str) -> bool: + """取消应用窗口的最小化/隐藏状态,多策略依次尝试。""" + apps = NSRunningApplication.runningApplicationsWithBundleIdentifier_(bundle_id) + if not apps or len(apps) == 0: + logger.error(f"未找到运行中的应用: {bundle_id}") + return False + + app = apps[0] + app_name = app.localizedName() or "WeChat" + + # 策略1: NSRunningApplication unhide(处理 Cmd+H 隐藏) + if app.isHidden(): + logger.info(f"{app_name} 处于隐藏状态,调用 unhide") + app.unhide() + time.sleep(0.3) + + # 策略2: 通过 AX API 取消所有窗口的最小化(最可靠的方式) + app_ref = self.get_app_ref(bundle_id) + if app_ref: + windows = self.get_windows(app_ref) + for win in windows: + if self.is_window_minimized(win): + title = self.get_title(win) or "?" + logger.info(f"通过 AX API 取消窗口最小化: {title}") + self.set_window_minimized(win, False) + time.sleep(0.5) + + # 策略3: activate 带到前台 + app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps) + time.sleep(0.3) + + logger.info(f"窗口恢复流程完成: {app_name}") + return True + + def is_window_minimized(self, window) -> bool: + """检查窗口是否处于最小化状态。""" try: - subprocess.run( - ["osascript", "-e", script], - capture_output=True, timeout=5 - ) - logger.info(f"已取消 {app_name} 窗口最小化") - return True + err, value = AXUIElementCopyAttributeValue(window, "AXMinimized", None) + if err == kAXErrorSuccess: + return bool(value) + except Exception: + pass + return False + + def set_window_minimized(self, window, minimized: bool) -> bool: + """通过 AX API 设置窗口最小化状态。""" + try: + err = AXUIElementSetAttributeValue(window, "AXMinimized", minimized) + return err == kAXErrorSuccess except Exception as e: - logger.error(f"取消最小化失败: {e}") + logger.error(f"设置 AXMinimized 失败: {e}") return False # ---------------------------------------------------------------- diff --git a/wechat_clicker/state_machine.py b/wechat_clicker/state_machine.py index 43ab16e..3d7c61d 100644 --- a/wechat_clicker/state_machine.py +++ b/wechat_clicker/state_machine.py @@ -58,22 +58,32 @@ class StateMachine: if self.ax.is_app_running(self.ui.bundle_id): self._current_state = UIState.WECHAT_MINIMIZED self._conversation_name = None - logger.debug("微信在运行但无可见窗口(可能已最小化)") + logger.debug("微信在运行但无可见窗口(可能已最小化或隐藏)") else: self._current_state = UIState.WECHAT_NOT_RUNNING self._conversation_name = None logger.debug("微信未运行") return self._current_state - # 分析窗口 + # 检查主窗口是否最小化(AXWindows 会包含最小化的窗口) main_window = None + all_minimized = True other_windows = [] for win in windows: title = self.ax.get_title(win) + is_mini = self.ax.is_window_minimized(win) if title == "微信": main_window = win elif title: other_windows.append((win, title)) + if not is_mini: + all_minimized = False + + if all_minimized: + self._current_state = UIState.WECHAT_MINIMIZED + self._conversation_name = None + logger.debug("微信所有窗口均已最小化") + return self._current_state if main_window is None: self._current_state = UIState.UNKNOWN diff --git a/wechat_clicker/wechat_ui.py b/wechat_clicker/wechat_ui.py index 5f1c1c7..af157b3 100644 --- a/wechat_clicker/wechat_ui.py +++ b/wechat_clicker/wechat_ui.py @@ -76,28 +76,40 @@ class WeChatUI: """确保微信在最前台。""" return self.ax.bring_to_front(self.bundle_id) - def ensure_wechat_visible(self) -> bool: - """确保微信窗口可见(取消最小化并带到前台)。""" - windows = self.get_all_windows() - if windows: - return self.ensure_wechat_frontmost() + def ensure_wechat_visible(self, max_retries: int = 3) -> bool: + """确保微信窗口可见(取消最小化/隐藏并带到前台)。 + 多次重试,每次重试后验证主窗口是否真正可见(非最小化)。 + """ if not self.ax.is_app_running(self.bundle_id): logger.error("微信未运行") return False - logger.info("微信窗口不可见(可能已最小化),尝试恢复...") - self.ax.unminimize_app("WeChat") - time.sleep(0.5) - self.ax.bring_to_front(self.bundle_id) - time.sleep(0.5) - - windows = self.get_all_windows() - if windows: - logger.info("微信窗口已恢复可见") + # 先检查是否已经可见且非最小化 + main_win = self.get_main_window() + if main_win is not None and not self.ax.is_window_minimized(main_win): + self.ensure_wechat_frontmost() return True - logger.error("微信窗口恢复失败") + for attempt in range(1, max_retries + 1): + logger.info(f"微信窗口不可见或已最小化,恢复尝试 {attempt}/{max_retries}...") + + self.ax.unminimize_app(self.bundle_id) + time.sleep(1.0) + + # 清除缓存的 app_ref,强制重新获取 + self.invalidate_app_ref() + + main_win = self.get_main_window() + if main_win is not None and not self.ax.is_window_minimized(main_win): + logger.info("微信主窗口已恢复可见") + self.ensure_wechat_frontmost() + return True + + logger.warning(f"恢复尝试 {attempt} 后窗口仍不可见,等待后重试...") + time.sleep(1.0) + + logger.error(f"经过 {max_retries} 次尝试,微信窗口恢复失败") return False # ----------------------------------------------------------------