wechat_msg_clicker/project.md
2026-05-06 14:31:56 +08:00

214 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 项目规划WeChat 消息自动点击器
## 项目目标
创建一个长期稳定运行的自动化工具,自动点击微信桌面端中未读消息的图片和文件,触发微信下载原始文件到本地目录,配合信息收集系统完成数据入库流程。
## 项目状态
### 已完成 (v0.1.0 - 2026/04/22)
- [x] 项目结构搭建
- [x] 配置系统YAML 配置文件、默认值、白/黑名单)
- [x] AXUIElement 底层封装ax_bridge.py
- [x] 微信 UI 导航wechat_ui.py— 聊天列表解析、消息类型分类
- [x] UI 状态机state_machine.py— 基于窗口计数的状态检测与恢复
- [x] 拟人行为引擎human_like.py— 高斯分布延迟、长休息、工作时间
- [x] 主自动化逻辑automator.py— 完整扫描-点击-关闭循环
- [x] 入口脚本main.py— 参数解析、信号处理
- [x] 调试工具(--dump-ui, --dry-run
### v0.2.0 修复与增强 (2026/04/22)
- [x] **修复: 聊天项点击失败** — AXStaticText 无 AXPress 动作,改用 CGEvent 鼠标坐标点击
- [x] **修复: AXValue 位置解包** — get_position/get_size 改用 AXValueGetValue 正确提取 CGPoint/CGSize
- [x] **新增: 消息列表滚动** — 进入聊天后向上滚动 5 轮,加载更多历史媒体消息
- [x] **新增: 图片"使用预览打开"流程** — 点击大图 → 点"..." → "使用预览打开" → 关闭 Preview.app确保原始文件保存到本地
- [x] **新增: 文件直接点击下载** — 点击文件气泡触发微信下载
- [x] **新增: Preview.app 管理** — state_machine 添加检测与关闭 macOS Preview.app 的能力
- [x] **新增: 鼠标双击、滚轮滚动** — ax_bridge 扩展 CGEvent 操作
### v0.3.0 修复与调试增强 (2026/04/23)
- [x] **修复: 滚动误点击**`_scroll_at()` 原先用 mouseDown/mouseUp 聚焦导致误点侧边栏按钮,改用 kCGEventMouseMoved 仅移动鼠标不触发点击
- [x] **修复: 未跳转最新消息** — 进入聊天后先 `scroll_to_bottom()` 滚到消息列表底部(最新消息),再向上滚动加载历史
- [x] **修复: "..."按钮误匹配**`_find_more_button_in_preview()` 限制搜索范围,主窗口中只搜索 x>200 区域(排除左侧侧边栏),增加位置日志
- [x] **修复: 重复点击同一图片** — 用消息列表子元素索引(child index)做去重processed_indices 跟踪已处理元素,跨滚动轮次不重复
- [x] **修复: 裁剪图片点击无效** — 只处理完全在消息列表可见区域内的媒体(上下各 30px margin跳过部分被裁剪的元素
- [x] **增强: 全链路调试日志** — click_at_element 记录坐标/角色/标题get_media_messages 输出每个媒体详情find_menu_item 记录搜索过程,状态检测记录所有窗口标题
### v0.4.0 诊断工具与窗口恢复 (2026/04/23)
- [x] **新增: --dump-ui --chat 诊断工具**`python main.py --dump-ui --chat "聊天名"` 进入指定聊天,滚到底部,以 max_depth=10 dump 每个消息子元素的完整 AX 树含索引、position、actions、identifier用于诊断不同图片元素结构差异
- [x] **新增: dump_element 增强** — 输出 AXPosition、AXActions、AXIdentifier 字段
- [x] **新增: 微信窗口最小化自动恢复** — 新增 `WECHAT_MINIMIZED` 状态detect_state 区分"未运行"和"已最小化",通过 AppleScript 自动取消最小化并恢复前台
- [x] **增强: verify_setup 自动恢复** — 启动时找不到主窗口先尝试恢复最小化,再判断失败
### v0.5.0 图片点击与滚动修复 (2026/04/23)
- [x] **修复: 图片点击不生效(根本原因)** — AXStaticText 覆盖整行575px宽实际图片气泡仅在左侧 80~170px 区域。改用 `click_at_element_offset(x=120, y=height/2)` 命中实际缩略图区域(通过逐像素探测确认可点击边界)
- [x] **修复: 滚动方向反转** — macOS CGEvent ScrollWheel 正值=向上滚(看旧消息),负值=向下滚(看新消息)。`scroll_to_bottom` 从 +20 改为 -20历史加载滚动从 -10 改为 +10
- [x] **修复: 可见性判断过严** — 原先要求元素完全在可见区域内30px margin289px 高图片经常被判定裁剪。改为只检查元素**中心点**是否在可见区域内
- [x] **新增: click_at_element_offset** — ax_bridge 新增带偏移量的点击方法,文件/视频点击也使用偏移量
- [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`→自动恢复→正常扫描处理
### v0.7.0 默认只点击图片 (2026/04/23)
- [x] **配置变更: click_files 默认关闭**`media.click_files` 默认值从 `true` 改为 `false`,默认只点击图片不点击文件。需要点击文件时在 config.yaml 中设置 `click_files: true`
### v0.8.0 提高图片可靠性 — 减少遗漏 (2026/04/24)
- [x] **修复: 去重 bug** — 原用绝对子元素索引去重,滚动后索引偏移导致图片被错误跳过。改用反向索引(距数组末尾距离),滚动时新元素从头部插入不影响已有元素
- [x] **新增: 失败重试** — 图片点击失败不再永久跳过,最多重试 2 次后才标记为已处理
- [x] **改进: 移除聊天数量上限**`max_chats_per_scan` 默认改为 0不限制几十个群全部处理
- [x] **改进: 忙时不 sleep** — 处理完有未读聊天后立即重扫,不等待 scan interval只有无未读时才 sleep 15s
- [x] **改进: 动态滚动轮数** — 根据聊天未读数动态计算滚动轮数(`max(5, unread//3)`,上限 30大群覆盖更深
- [x] **新增: 尾部复检** — 滚动处理完成后回到底部复检,捕获处理期间新到达的消息
- [x] **改进: 忙时不休息** — 当全局未读 > 5 时跳过随机休息检查
- [x] **改进: 缩短延迟范围** — 所有延迟缩短约 50-60%,提高处理效率
- [x] **增强: 日志** — 记录被过滤掉的聊天名、每周期输出摘要(全局未读/处理聊天数/过滤数/点击媒体数/耗时)
### v0.9.0 锚点+箭头导航重构图片处理 (2026/04/24)
- [x] **重构: 图片处理改为锚点+箭头导航** — 不再逐个查找图片元素、滚动、去重。改为:找最底部可见图片(锚点)→ 点击进入预览 → 左箭头 i 次到达第 i 张 → "..."→"使用预览打开"→ 关闭 Preview → 重复。彻底绕过滚动覆盖不足、可见性判断、去重 bug 等问题
- [x] **新增: ax_bridge 左箭头键支持**`kVK_LeftArrow` 常量和 `send_left_arrow()` 方法
- [x] **新增: `_find_anchor_image`** — 在消息列表底部查找最后一个可见图片元素作为导航锚点
- [x] **新增: `_process_images_with_arrow`** — 锚点+箭头循环核心逻辑,每张图片:点击锚点 → 左箭头 N 次 → 预览流程
- [x] **新增: `_do_preview_and_close`** — 从旧 `_click_image` 提取的预览流程(找"..."→ "使用预览打开"→ 关闭 Preview
- [x] **新增: `_process_visible_files`** — 文件/视频独立处理(不走箭头导航,直接点击可见元素)
- [x] **删除: 旧图片逐个处理逻辑**`_click_image`、`_process_visible_media` 中的图片分支、`processed_indices`/`retry_counts` 去重、滚动上翻 N 轮、尾部复检
- [x] **已验证: 锚点+箭头导航** — 测试脚本 `test_arrow_nav.py` 验证 5/5 张图片全部成功,平均 10.4s/张
### v0.9.1 锚点+箭头导航稳定性增强 (2026/04/24)
- [x] **新增: 重锚机制Re-anchor** — 连续 3 次失败(预览未打开或预览流程失败)自动触发重锚:滚到底部 → 重新找锚点 → i 归零重新开始。最多重锚 2 次,超过则放弃当前聊天
- [x] **新增: `_scroll_and_find_anchor`** — 合并滚动+锚点查找为单一方法,供初始化和重锚复用
- [x] **改进: `_process_images_with_arrow` 重写** — 改为接收 `msg_list` 参数,内部管理锚点生命周期(查找、使用、失效检测、重锚)
- [x] **新增: 每聊天图片处理上限**`MAX_IMAGES_PER_CHAT = 30`防止单个高活跃群聊垄断处理时间30张×10s = 5分钟/聊天)
- [x] **简化: `_process_media_with_scroll`** — 不再负责滚动和锚点查找(移入 `_process_images_with_arrow` 内部),代码更清晰
### v0.9.2 UNKNOWN 状态恢复加固 (2026/04/24)
- [x] **修复: UNKNOWN 状态长时间无法恢复** — 原恢复逻辑只做 activate + 点侧边栏,遇到弹窗/多余窗口无法清理。改为先 Escape + Cmd+W 关闭可能的弹窗,再 activate + 点侧边栏
- [x] **改进: 恢复失败指数退避** — 外层循环连续恢复失败时退避等待10s → 20s → 40s → 80s → 120s封顶避免每 13 秒重复无效尝试。恢复成功后计数器归零
### v0.9.3 窗口关闭恢复 + 长休息优化 (2026/04/24-25)
- [x] **修复: 窗口被关闭(非最小化)时无法恢复** — 微信进程在运行但 AXWindows=0窗口被关闭而非最小化原 unhide/AXMinimized=False/activate 均无效。新增策略:检测到 0 窗口时通过 `subprocess.run(["open", "-b", bundle_id])` 重新打开微信窗口
- [x] **窗口恢复策略升级为 4 层** — unhide隐藏→ 检测 0 窗口时 open -b 重开(关闭)→ AXMinimized=False最小化→ activate前台
- [x] **修复: 无可处理聊天时频繁触发长休息**`should_take_break()` 原先在过滤聊天之前执行,当只有黑名单聊天(如腾讯新闻)有未读时,每 15s 周期仍有 5% 概率触发 30-120s 长休息。改为只在有实际可处理的聊天时才触发长休息
- [x] **黑名单扩充** — 默认黑名单新增"服务号"、"订阅号"、"文件传输助手",防止进入非聊天界面导致状态异常
### v0.9.4 窗口恢复死循环修复 (2026/04/30)
- [x] **修复(关键): UNKNOWN 状态 Cmd+W 导致恶性循环** — 原 UNKNOWN 状态恢复会发 Cmd+W 关闭窗口,而 open -b 刚重开的窗口标题可能暂未注册为"微信"被识别为 UNKNOWN → Cmd+W 关掉 → 又变成 0 窗口 → 再 open -b → 无限循环(实测连续 787 次/26 小时)。去掉 UNKNOWN 状态下的 Cmd+W只保留安全的 Escape
- [x] **新增: WECHAT_ABNORMAL 状态** — 区分"有窗口但无'微信'标题"可能窗口正在加载或登录界面和完全未知状态ABNORMAL 状态下等待窗口就绪而非尝试关闭
- [x] **修复: unminimize_app open -b 后不验证** — 原 `open -b` 后不重新读取窗口列表,始终返回 True。改为 polling 等待(最多 5s窗口出现失败返回 False
- [x] **新增: ensure_wechat_visible 二次确认** — 找到主窗口后等 0.5s 再确认一次,防止瞬时窗口骗过检测
- [x] **新增: 诊断日志增强** — get_main_window 失败时记录实际存在的窗口标题open -b 后记录新出现的窗口标题
- [x] **新增: 连续失败上限 + 系统通知** — 连续恢复失败 ≥15 次后发送 macOS 系统通知提醒人工介入,暂停 600s之前无上限
### v1.0.0 视频处理支持 (2026/04/30)
- [x] **新增: 视频纳入锚点+箭头导航** — 锚点查找同时匹配图片(`title=="图片"`)和视频(`title.startswith("视频")`),箭头导航中自动检测当前预览类型
- [x] **新增: 视频类型三层检测** — (1) 有"查看原视频"→未下载视频,点击+等10s(2) 无"查看原视频"但有"原视频"→已下载视频等1s后Escape跳过(3) 都没有→图片,走"..."→"使用预览打开"
- [x] **新增: `_find_view_original_video_button`** — 在预览窗口中搜索"查看原视频"按钮(先搜独立窗口,再搜主窗口右侧 x>200
- [x] **新增: `_is_downloaded_video_preview`** — 检测"原视频"按钮判断已下载视频
- [x] **新增: `_do_video_download_and_close`** — 点击"查看原视频" → 等待 10s → 关闭额外窗口 → Escape 退出预览
- [x] **重构: `_process_images_with_arrow` → `_process_media_with_arrow`** — 支持图片/视频混合处理
- [x] **重构: `_find_anchor_image` → `_find_anchor_media`** — 锚点查找同时匹配图片和视频
- [x] **重构: `_process_visible_files` 精简** — 视频已移入箭头导航,此方法只处理文件
- [x] **配置变更: click_videos 默认开启**`media.click_videos` 默认值从 `false` 改为 `true`
### v1.0.1 窗口恢复加固 + 视频首次点击兜底 (2026/05/01-06)
- [x] **修复: 多个"微信"窗口导致 UNKNOWN 状态** — 视频迷你播放器249x151小浮窗也叫"微信",导致状态机无法识别。改为取面积最大的"微信"窗口为主窗口,其余归入 other_windows
- [x] **修复: UNKNOWN/ABNORMAL 恢复不关闭多余窗口** — 新增 `_close_non_main_windows` 方法,恢复时先关闭所有非主窗口(含空标题遗留预览、小浮窗),再重试状态检测
- [x] **修复: `get_main_window` 多窗口误选** — 多个"微信"窗口时按面积排序取最大,避免选中迷你播放器
- [x] **新增: 视频首次点击兜底** — 视频首次点击微信可能不弹出独立预览窗口先下载。等待5s无窗口后检查是否有"查看原视频"按钮(内嵌预览),有则直接处理,避免误判为失败
- [x] **新增: 预览窗口检测重试** — 点击锚点后2s无窗口再等3s重试应对视频加载慢的情况
- [x] **配置变更: 工作时间** — 改为 7:00-次日1:00非工作时间 1:00-7:00支持跨午夜时间判断
- [x] **配置变更: 日志级别** — 默认改为 DEBUG便于问题定位
### 待验证
- [x] ~~验证最小化窗口自动恢复功能(需手动最小化微信窗口测试)~~
- [ ] 有未读图片消息时的完整 --once 自动化流程
- [ ] 长时间运行稳定性测试
### 未来可能的改进
- [ ] 聊天列表滚动支持(处理不在可见区域的聊天)
- [ ] 已处理消息去重(记录已点击的媒体,避免重复)
- [ ] 微信版本适配(检测 UI 结构变化并自动调整)
- [ ] 运行状态 Web 面板(远程监控)
- [ ] 与信息收集系统直接集成
## 技术细节
### 微信 UI 结构 (v4.1.9)
- 主窗口 "微信" 包含侧边栏 + 聊天列表
- 聊天列表:`AXList name="会话"`,子元素为 `AXStaticText`(无 AXPress 动作)
- 聊天项 AXIdentifier 格式:`session_item_聊天名`
- AXStaticText 的 AXPosition/AXSize 需通过 AXValueGetValue 解包
- 点击聊天项打开独立会话窗口
- 消息列表:`AXList name="消息"`
- 图片消息 title = "图片",文件消息 title 以 "文件\n" 开头
### 点击策略
- 聊天项和消息元素均使用 CGEvent 鼠标坐标点击AXStaticText 不支持 AXPress
- 计算元素中心坐标 = position + size/2
- 图片预览中的按钮优先尝试 click_at_element后备 AXPress
### 媒体下载流程(锚点+箭头导航,图片+视频统一)
1. 滚到消息列表底部(最新消息)
2. 找到最底部可见的图片或视频元素(锚点 N
3. 对 i = 0, 1, 2, ... media_count-1:
a. 点击锚点 N → 打开预览(等 2s无窗口再等 3s
b. 若无独立预览窗口但检测到"查看原视频"按钮 → 内嵌预览兜底处理
c. 按左箭头 i 次 → 到达媒体 N-i
d. 三层类型检测:
- 有"查看原视频" → 未下载视频 → 点击 + 等 10s + Escape
- 无"查看原视频"但有"原视频" → 已下载视频 → 等 1s + Escape
- 都没有 → 图片 → "..." → "使用预览打开" → 关闭 Preview.app
e. 回到微信,准备下一轮
4. media_count = min(max(5, unread_count) + 5, 30)(固定 overlap上限 30/聊天)
5. 连续 3 次失败自动重锚(滚到底部 + 重找锚点 + i 归零),最多重锚 2 次
### 调度策略
- 高斯分布随机延迟(非均匀)
- ±20% 扫描间隔抖动(无未读时生效)
- 忙时(未读 > 5跳过随机休息空闲时 5% 概率长休息
- 工作时间限制8:00-23:00
- 聊天数量不限制(`max_chats_per_scan: 0`
- 处理完有未读聊天后立即重扫
- 图片覆盖量 = min(max(5, unread_count) + 5, 30) (固定 overlap上限 30 张/聊天)
### 依赖
- pyobjc-framework-Quartz
- pyobjc-framework-ApplicationServices
- pyobjc-framework-Cocoa
- PyYAML
### 系统要求
- macOS Sonoma 14+
- Python 3.11+
- 辅助功能权限(终端/Python 需要授权)
- 微信桌面端 v4.1.9+