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

18 KiB
Raw Permalink Blame History

项目规划WeChat 消息自动点击器

项目目标

创建一个长期稳定运行的自动化工具,自动点击微信桌面端中未读消息的图片和文件,触发微信下载原始文件到本地目录,配合信息收集系统完成数据入库流程。

项目状态

已完成 (v0.1.0 - 2026/04/22)

  • 项目结构搭建
  • 配置系统YAML 配置文件、默认值、白/黑名单)
  • AXUIElement 底层封装ax_bridge.py
  • 微信 UI 导航wechat_ui.py— 聊天列表解析、消息类型分类
  • UI 状态机state_machine.py— 基于窗口计数的状态检测与恢复
  • 拟人行为引擎human_like.py— 高斯分布延迟、长休息、工作时间
  • 主自动化逻辑automator.py— 完整扫描-点击-关闭循环
  • 入口脚本main.py— 参数解析、信号处理
  • 调试工具(--dump-ui, --dry-run

v0.2.0 修复与增强 (2026/04/22)

  • 修复: 聊天项点击失败 — AXStaticText 无 AXPress 动作,改用 CGEvent 鼠标坐标点击
  • 修复: AXValue 位置解包 — get_position/get_size 改用 AXValueGetValue 正确提取 CGPoint/CGSize
  • 新增: 消息列表滚动 — 进入聊天后向上滚动 5 轮,加载更多历史媒体消息
  • 新增: 图片"使用预览打开"流程 — 点击大图 → 点"..." → "使用预览打开" → 关闭 Preview.app确保原始文件保存到本地
  • 新增: 文件直接点击下载 — 点击文件气泡触发微信下载
  • 新增: Preview.app 管理 — state_machine 添加检测与关闭 macOS Preview.app 的能力
  • 新增: 鼠标双击、滚轮滚动 — ax_bridge 扩展 CGEvent 操作

v0.3.0 修复与调试增强 (2026/04/23)

  • 修复: 滚动误点击_scroll_at() 原先用 mouseDown/mouseUp 聚焦导致误点侧边栏按钮,改用 kCGEventMouseMoved 仅移动鼠标不触发点击
  • 修复: 未跳转最新消息 — 进入聊天后先 scroll_to_bottom() 滚到消息列表底部(最新消息),再向上滚动加载历史
  • 修复: "..."按钮误匹配_find_more_button_in_preview() 限制搜索范围,主窗口中只搜索 x>200 区域(排除左侧侧边栏),增加位置日志
  • 修复: 重复点击同一图片 — 用消息列表子元素索引(child index)做去重processed_indices 跟踪已处理元素,跨滚动轮次不重复
  • 修复: 裁剪图片点击无效 — 只处理完全在消息列表可见区域内的媒体(上下各 30px margin跳过部分被裁剪的元素
  • 增强: 全链路调试日志 — click_at_element 记录坐标/角色/标题get_media_messages 输出每个媒体详情find_menu_item 记录搜索过程,状态检测记录所有窗口标题

v0.4.0 诊断工具与窗口恢复 (2026/04/23)

  • 新增: --dump-ui --chat 诊断工具python main.py --dump-ui --chat "聊天名" 进入指定聊天,滚到底部,以 max_depth=10 dump 每个消息子元素的完整 AX 树含索引、position、actions、identifier用于诊断不同图片元素结构差异
  • 新增: dump_element 增强 — 输出 AXPosition、AXActions、AXIdentifier 字段
  • 新增: 微信窗口最小化自动恢复 — 新增 WECHAT_MINIMIZED 状态detect_state 区分"未运行"和"已最小化",通过 AppleScript 自动取消最小化并恢复前台
  • 增强: verify_setup 自动恢复 — 启动时找不到主窗口先尝试恢复最小化,再判断失败

v0.5.0 图片点击与滚动修复 (2026/04/23)

  • 修复: 图片点击不生效(根本原因) — AXStaticText 覆盖整行575px宽实际图片气泡仅在左侧 80~170px 区域。改用 click_at_element_offset(x=120, y=height/2) 命中实际缩略图区域(通过逐像素探测确认可点击边界)
  • 修复: 滚动方向反转 — macOS CGEvent ScrollWheel 正值=向上滚(看旧消息),负值=向下滚(看新消息)。scroll_to_bottom 从 +20 改为 -20历史加载滚动从 -10 改为 +10
  • 修复: 可见性判断过严 — 原先要求元素完全在可见区域内30px margin289px 高图片经常被判定裁剪。改为只检查元素中心点是否在可见区域内
  • 新增: click_at_element_offset — ax_bridge 新增带偏移量的点击方法,文件/视频点击也使用偏移量
  • 已验证: 完整图片处理流程 — 蜗牛聊天 4 张不同尺寸图片289px、155px、180px全部成功缩略图→预览窗口→...按钮→使用预览打开→关闭 Preview→恢复
  • 已验证: 带滚动的媒体处理 — scroll_to_bottom 正确到达最新消息向上滚动正确加载历史去重正确processed_indices=[45,47,49,50]

v0.6.0 最小化窗口恢复修复 (2026/04/23)

  • 修复: 最小化时恢复失败(根本原因) — 原 AppleScript tell application "WeChat" 方案存在两个致命问题:(1) 微信的 localizedName 是"微信"不是"WeChat"AppleScript 找错应用或静默失败导致打开了别的窗口;(2) 微信不支持标准 AppleScript miniaturized 属性(返回 error -10006
  • 修复: 最小化检测遗漏 — 微信窗口最小化后 AXWindows 仍返回该窗口AXMinimized=Truedetect_stateensure_wechat_visible 未检查此属性,误判窗口已可见
  • 改用 AX API 恢复窗口 — 通过 AXUIElementSetAttributeValue(window, "AXMinimized", False) 取消最小化,比 AppleScript 更可靠
  • 多策略窗口恢复 — unhideCmd+H隐藏→ AXMinimized=False最小化→ activate前台覆盖所有不可见场景
  • 恢复重试与验证ensure_wechat_visible 增加 3 次重试循环,每次验证主窗口 AXMinimized=False 后才返回成功
  • 已验证 — 两次完整测试通过:最小化微信→运行 --once --debug→自动恢复→正常扫描处理

v0.7.0 默认只点击图片 (2026/04/23)

  • 配置变更: click_files 默认关闭media.click_files 默认值从 true 改为 false,默认只点击图片不点击文件。需要点击文件时在 config.yaml 中设置 click_files: true

v0.8.0 提高图片可靠性 — 减少遗漏 (2026/04/24)

  • 修复: 去重 bug — 原用绝对子元素索引去重,滚动后索引偏移导致图片被错误跳过。改用反向索引(距数组末尾距离),滚动时新元素从头部插入不影响已有元素
  • 新增: 失败重试 — 图片点击失败不再永久跳过,最多重试 2 次后才标记为已处理
  • 改进: 移除聊天数量上限max_chats_per_scan 默认改为 0不限制几十个群全部处理
  • 改进: 忙时不 sleep — 处理完有未读聊天后立即重扫,不等待 scan interval只有无未读时才 sleep 15s
  • 改进: 动态滚动轮数 — 根据聊天未读数动态计算滚动轮数(max(5, unread//3),上限 30大群覆盖更深
  • 新增: 尾部复检 — 滚动处理完成后回到底部复检,捕获处理期间新到达的消息
  • 改进: 忙时不休息 — 当全局未读 > 5 时跳过随机休息检查
  • 改进: 缩短延迟范围 — 所有延迟缩短约 50-60%,提高处理效率
  • 增强: 日志 — 记录被过滤掉的聊天名、每周期输出摘要(全局未读/处理聊天数/过滤数/点击媒体数/耗时)

v0.9.0 锚点+箭头导航重构图片处理 (2026/04/24)

  • 重构: 图片处理改为锚点+箭头导航 — 不再逐个查找图片元素、滚动、去重。改为:找最底部可见图片(锚点)→ 点击进入预览 → 左箭头 i 次到达第 i 张 → "..."→"使用预览打开"→ 关闭 Preview → 重复。彻底绕过滚动覆盖不足、可见性判断、去重 bug 等问题
  • 新增: ax_bridge 左箭头键支持kVK_LeftArrow 常量和 send_left_arrow() 方法
  • 新增: _find_anchor_image — 在消息列表底部查找最后一个可见图片元素作为导航锚点
  • 新增: _process_images_with_arrow — 锚点+箭头循环核心逻辑,每张图片:点击锚点 → 左箭头 N 次 → 预览流程
  • 新增: _do_preview_and_close — 从旧 _click_image 提取的预览流程(找"..."→ "使用预览打开"→ 关闭 Preview
  • 新增: _process_visible_files — 文件/视频独立处理(不走箭头导航,直接点击可见元素)
  • 删除: 旧图片逐个处理逻辑_click_image_process_visible_media 中的图片分支、processed_indices/retry_counts 去重、滚动上翻 N 轮、尾部复检
  • 已验证: 锚点+箭头导航 — 测试脚本 test_arrow_nav.py 验证 5/5 张图片全部成功,平均 10.4s/张

v0.9.1 锚点+箭头导航稳定性增强 (2026/04/24)

  • 新增: 重锚机制Re-anchor — 连续 3 次失败(预览未打开或预览流程失败)自动触发重锚:滚到底部 → 重新找锚点 → i 归零重新开始。最多重锚 2 次,超过则放弃当前聊天
  • 新增: _scroll_and_find_anchor — 合并滚动+锚点查找为单一方法,供初始化和重锚复用
  • 改进: _process_images_with_arrow 重写 — 改为接收 msg_list 参数,内部管理锚点生命周期(查找、使用、失效检测、重锚)
  • 新增: 每聊天图片处理上限MAX_IMAGES_PER_CHAT = 30防止单个高活跃群聊垄断处理时间30张×10s = 5分钟/聊天)
  • 简化: _process_media_with_scroll — 不再负责滚动和锚点查找(移入 _process_images_with_arrow 内部),代码更清晰

v0.9.2 UNKNOWN 状态恢复加固 (2026/04/24)

  • 修复: UNKNOWN 状态长时间无法恢复 — 原恢复逻辑只做 activate + 点侧边栏,遇到弹窗/多余窗口无法清理。改为先 Escape + Cmd+W 关闭可能的弹窗,再 activate + 点侧边栏
  • 改进: 恢复失败指数退避 — 外层循环连续恢复失败时退避等待10s → 20s → 40s → 80s → 120s封顶避免每 13 秒重复无效尝试。恢复成功后计数器归零

v0.9.3 窗口关闭恢复 + 长休息优化 (2026/04/24-25)

  • 修复: 窗口被关闭(非最小化)时无法恢复 — 微信进程在运行但 AXWindows=0窗口被关闭而非最小化原 unhide/AXMinimized=False/activate 均无效。新增策略:检测到 0 窗口时通过 subprocess.run(["open", "-b", bundle_id]) 重新打开微信窗口
  • 窗口恢复策略升级为 4 层 — unhide隐藏→ 检测 0 窗口时 open -b 重开(关闭)→ AXMinimized=False最小化→ activate前台
  • 修复: 无可处理聊天时频繁触发长休息should_take_break() 原先在过滤聊天之前执行,当只有黑名单聊天(如腾讯新闻)有未读时,每 15s 周期仍有 5% 概率触发 30-120s 长休息。改为只在有实际可处理的聊天时才触发长休息
  • 黑名单扩充 — 默认黑名单新增"服务号"、"订阅号"、"文件传输助手",防止进入非聊天界面导致状态异常

v0.9.4 窗口恢复死循环修复 (2026/04/30)

  • 修复(关键): UNKNOWN 状态 Cmd+W 导致恶性循环 — 原 UNKNOWN 状态恢复会发 Cmd+W 关闭窗口,而 open -b 刚重开的窗口标题可能暂未注册为"微信"被识别为 UNKNOWN → Cmd+W 关掉 → 又变成 0 窗口 → 再 open -b → 无限循环(实测连续 787 次/26 小时)。去掉 UNKNOWN 状态下的 Cmd+W只保留安全的 Escape
  • 新增: WECHAT_ABNORMAL 状态 — 区分"有窗口但无'微信'标题"可能窗口正在加载或登录界面和完全未知状态ABNORMAL 状态下等待窗口就绪而非尝试关闭
  • 修复: unminimize_app open -b 后不验证 — 原 open -b 后不重新读取窗口列表,始终返回 True。改为 polling 等待(最多 5s窗口出现失败返回 False
  • 新增: ensure_wechat_visible 二次确认 — 找到主窗口后等 0.5s 再确认一次,防止瞬时窗口骗过检测
  • 新增: 诊断日志增强 — get_main_window 失败时记录实际存在的窗口标题open -b 后记录新出现的窗口标题
  • 新增: 连续失败上限 + 系统通知 — 连续恢复失败 ≥15 次后发送 macOS 系统通知提醒人工介入,暂停 600s之前无上限

v1.0.0 视频处理支持 (2026/04/30)

  • 新增: 视频纳入锚点+箭头导航 — 锚点查找同时匹配图片(title=="图片")和视频(title.startswith("视频")),箭头导航中自动检测当前预览类型
  • 新增: 视频类型三层检测 — (1) 有"查看原视频"→未下载视频,点击+等10s(2) 无"查看原视频"但有"原视频"→已下载视频等1s后Escape跳过(3) 都没有→图片,走"..."→"使用预览打开"
  • 新增: _find_view_original_video_button — 在预览窗口中搜索"查看原视频"按钮(先搜独立窗口,再搜主窗口右侧 x>200
  • 新增: _is_downloaded_video_preview — 检测"原视频"按钮判断已下载视频
  • 新增: _do_video_download_and_close — 点击"查看原视频" → 等待 10s → 关闭额外窗口 → Escape 退出预览
  • 重构: _process_images_with_arrow_process_media_with_arrow — 支持图片/视频混合处理
  • 重构: _find_anchor_image_find_anchor_media — 锚点查找同时匹配图片和视频
  • 重构: _process_visible_files 精简 — 视频已移入箭头导航,此方法只处理文件
  • 配置变更: click_videos 默认开启media.click_videos 默认值从 false 改为 true

v1.0.1 窗口恢复加固 + 视频首次点击兜底 (2026/05/01-06)

  • 修复: 多个"微信"窗口导致 UNKNOWN 状态 — 视频迷你播放器249x151小浮窗也叫"微信",导致状态机无法识别。改为取面积最大的"微信"窗口为主窗口,其余归入 other_windows
  • 修复: UNKNOWN/ABNORMAL 恢复不关闭多余窗口 — 新增 _close_non_main_windows 方法,恢复时先关闭所有非主窗口(含空标题遗留预览、小浮窗),再重试状态检测
  • 修复: get_main_window 多窗口误选 — 多个"微信"窗口时按面积排序取最大,避免选中迷你播放器
  • 新增: 视频首次点击兜底 — 视频首次点击微信可能不弹出独立预览窗口先下载。等待5s无窗口后检查是否有"查看原视频"按钮(内嵌预览),有则直接处理,避免误判为失败
  • 新增: 预览窗口检测重试 — 点击锚点后2s无窗口再等3s重试应对视频加载慢的情况
  • 配置变更: 工作时间 — 改为 7:00-次日1:00非工作时间 1:00-7:00支持跨午夜时间判断
  • 配置变更: 日志级别 — 默认改为 DEBUG便于问题定位

待验证

  • 验证最小化窗口自动恢复功能(需手动最小化微信窗口测试)
  • 有未读图片消息时的完整 --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+