初步搞定 很酷

This commit is contained in:
cris 2026-04-23 19:23:59 +08:00
parent 90881c4191
commit c835182a44
5 changed files with 102 additions and 34 deletions

View File

@ -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
- 运行时会占用微信前台操作
- 建议在专用电脑上运行

View File

@ -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] **多策略窗口恢复** — unhideCmd+H隐藏→ AXMinimized=False最小化→ activate前台覆盖所有不可见场景
- [x] **恢复重试与验证**`ensure_wechat_visible` 增加 3 次重试循环,每次验证主窗口 AXMinimized=False 后才返回成功
- [x] **已验证** — 两次完整测试通过:最小化微信→运行 `--once --debug`→自动恢复→正常扫描处理
### 待验证
- [ ] 验证最小化窗口自动恢复功能(需手动最小化微信窗口测试)
- [x] ~~验证最小化窗口自动恢复功能(需手动最小化微信窗口测试)~~
- [ ] 有未读图片消息时的完整 --once 自动化流程
- [ ] 长时间运行稳定性测试

View File

@ -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
# ----------------------------------------------------------------

View File

@ -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

View File

@ -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
# ----------------------------------------------------------------