初步搞定 很酷
This commit is contained in:
parent
90881c4191
commit
c835182a44
@ -40,7 +40,8 @@ wechat_clicker/
|
|||||||
- "..."按钮搜索限制在预览区域(独立窗口或主窗口 x>200),排除侧边栏
|
- "..."按钮搜索限制在预览区域(独立窗口或主窗口 x>200),排除侧边栏
|
||||||
- 媒体去重基于消息列表**子元素索引**(child index),同一元素跨滚动轮次只处理一次
|
- 媒体去重基于消息列表**子元素索引**(child index),同一元素跨滚动轮次只处理一次
|
||||||
- 可见性检查基于元素**中心点**是否在消息列表可见区域内(30px margin)
|
- 可见性检查基于元素**中心点**是否在消息列表可见区域内(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)
|
||||||
- 运行时会占用微信前台操作
|
- 运行时会占用微信前台操作
|
||||||
- 建议在专用电脑上运行
|
- 建议在专用电脑上运行
|
||||||
|
|||||||
11
project.md
11
project.md
@ -53,9 +53,18 @@
|
|||||||
- [x] **已验证: 完整图片处理流程** — 蜗牛聊天 4 张不同尺寸图片(289px、155px、180px)全部成功:缩略图→预览窗口→...按钮→使用预览打开→关闭 Preview→恢复
|
- [x] **已验证: 完整图片处理流程** — 蜗牛聊天 4 张不同尺寸图片(289px、155px、180px)全部成功:缩略图→预览窗口→...按钮→使用预览打开→关闭 Preview→恢复
|
||||||
- [x] **已验证: 带滚动的媒体处理** — scroll_to_bottom 正确到达最新消息,向上滚动正确加载历史,去重正确(processed_indices=[45,47,49,50])
|
- [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 自动化流程
|
- [ ] 有未读图片消息时的完整 --once 自动化流程
|
||||||
- [ ] 长时间运行稳定性测试
|
- [ ] 长时间运行稳定性测试
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ApplicationServices import (
|
from ApplicationServices import (
|
||||||
@ -16,6 +15,7 @@ from ApplicationServices import (
|
|||||||
AXUIElementCopyAttributeNames,
|
AXUIElementCopyAttributeNames,
|
||||||
AXUIElementCopyActionNames,
|
AXUIElementCopyActionNames,
|
||||||
AXUIElementPerformAction,
|
AXUIElementPerformAction,
|
||||||
|
AXUIElementSetAttributeValue,
|
||||||
)
|
)
|
||||||
from Cocoa import (
|
from Cocoa import (
|
||||||
NSRunningApplication,
|
NSRunningApplication,
|
||||||
@ -108,21 +108,57 @@ class AXBridge:
|
|||||||
apps = NSRunningApplication.runningApplicationsWithBundleIdentifier_(bundle_id)
|
apps = NSRunningApplication.runningApplicationsWithBundleIdentifier_(bundle_id)
|
||||||
return apps is not None and len(apps) > 0
|
return apps is not None and len(apps) > 0
|
||||||
|
|
||||||
def unminimize_app(self, app_name: str) -> bool:
|
def unminimize_app(self, bundle_id: str) -> bool:
|
||||||
"""通过 AppleScript 取消应用窗口的最小化状态。"""
|
"""取消应用窗口的最小化/隐藏状态,多策略依次尝试。"""
|
||||||
script = (
|
apps = NSRunningApplication.runningApplicationsWithBundleIdentifier_(bundle_id)
|
||||||
f'tell application "{app_name}" to '
|
if not apps or len(apps) == 0:
|
||||||
f'set miniaturized of every window to false'
|
logger.error(f"未找到运行中的应用: {bundle_id}")
|
||||||
)
|
return False
|
||||||
try:
|
|
||||||
subprocess.run(
|
app = apps[0]
|
||||||
["osascript", "-e", script],
|
app_name = app.localizedName() or "WeChat"
|
||||||
capture_output=True, timeout=5
|
|
||||||
)
|
# 策略1: NSRunningApplication unhide(处理 Cmd+H 隐藏)
|
||||||
logger.info(f"已取消 {app_name} 窗口最小化")
|
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
|
return True
|
||||||
|
|
||||||
|
def is_window_minimized(self, window) -> bool:
|
||||||
|
"""检查窗口是否处于最小化状态。"""
|
||||||
|
try:
|
||||||
|
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:
|
except Exception as e:
|
||||||
logger.error(f"取消最小化失败: {e}")
|
logger.error(f"设置 AXMinimized 失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
|||||||
@ -58,22 +58,32 @@ class StateMachine:
|
|||||||
if self.ax.is_app_running(self.ui.bundle_id):
|
if self.ax.is_app_running(self.ui.bundle_id):
|
||||||
self._current_state = UIState.WECHAT_MINIMIZED
|
self._current_state = UIState.WECHAT_MINIMIZED
|
||||||
self._conversation_name = None
|
self._conversation_name = None
|
||||||
logger.debug("微信在运行但无可见窗口(可能已最小化)")
|
logger.debug("微信在运行但无可见窗口(可能已最小化或隐藏)")
|
||||||
else:
|
else:
|
||||||
self._current_state = UIState.WECHAT_NOT_RUNNING
|
self._current_state = UIState.WECHAT_NOT_RUNNING
|
||||||
self._conversation_name = None
|
self._conversation_name = None
|
||||||
logger.debug("微信未运行")
|
logger.debug("微信未运行")
|
||||||
return self._current_state
|
return self._current_state
|
||||||
|
|
||||||
# 分析窗口
|
# 检查主窗口是否最小化(AXWindows 会包含最小化的窗口)
|
||||||
main_window = None
|
main_window = None
|
||||||
|
all_minimized = True
|
||||||
other_windows = []
|
other_windows = []
|
||||||
for win in windows:
|
for win in windows:
|
||||||
title = self.ax.get_title(win)
|
title = self.ax.get_title(win)
|
||||||
|
is_mini = self.ax.is_window_minimized(win)
|
||||||
if title == "微信":
|
if title == "微信":
|
||||||
main_window = win
|
main_window = win
|
||||||
elif title:
|
elif title:
|
||||||
other_windows.append((win, 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:
|
if main_window is None:
|
||||||
self._current_state = UIState.UNKNOWN
|
self._current_state = UIState.UNKNOWN
|
||||||
|
|||||||
@ -76,28 +76,40 @@ class WeChatUI:
|
|||||||
"""确保微信在最前台。"""
|
"""确保微信在最前台。"""
|
||||||
return self.ax.bring_to_front(self.bundle_id)
|
return self.ax.bring_to_front(self.bundle_id)
|
||||||
|
|
||||||
def ensure_wechat_visible(self) -> bool:
|
def ensure_wechat_visible(self, max_retries: int = 3) -> bool:
|
||||||
"""确保微信窗口可见(取消最小化并带到前台)。"""
|
"""确保微信窗口可见(取消最小化/隐藏并带到前台)。
|
||||||
windows = self.get_all_windows()
|
|
||||||
if windows:
|
|
||||||
return self.ensure_wechat_frontmost()
|
|
||||||
|
|
||||||
|
多次重试,每次重试后验证主窗口是否真正可见(非最小化)。
|
||||||
|
"""
|
||||||
if not self.ax.is_app_running(self.bundle_id):
|
if not self.ax.is_app_running(self.bundle_id):
|
||||||
logger.error("微信未运行")
|
logger.error("微信未运行")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info("微信窗口不可见(可能已最小化),尝试恢复...")
|
# 先检查是否已经可见且非最小化
|
||||||
self.ax.unminimize_app("WeChat")
|
main_win = self.get_main_window()
|
||||||
time.sleep(0.5)
|
if main_win is not None and not self.ax.is_window_minimized(main_win):
|
||||||
self.ax.bring_to_front(self.bundle_id)
|
self.ensure_wechat_frontmost()
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
windows = self.get_all_windows()
|
|
||||||
if windows:
|
|
||||||
logger.info("微信窗口已恢复可见")
|
|
||||||
return True
|
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
|
return False
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user