89 lines
6.2 KiB
Markdown
89 lines
6.2 KiB
Markdown
# WeChat 消息自动点击器
|
||
|
||
自动点击微信桌面端未读消息中的图片、视频和文件,触发原始文件下载到本地目录。
|
||
|
||
## 项目背景
|
||
|
||
配合另一个信息收集项目使用:信息收集脚本负责将微信消息入库,但图片、视频和文件需要被点击预览后微信才会下载原始文件到本地。本工具自动完成这个"点击"操作。
|
||
|
||
## 技术架构
|
||
|
||
- **语言**: Python 3.11+
|
||
- **核心技术**: macOS Accessibility API (AXUIElement) via pyobjc
|
||
- **目标应用**: 微信桌面端 v4.1.9+ (bundle ID: com.tencent.xinWeChat)
|
||
- **运行平台**: macOS Sonoma 14+
|
||
|
||
### 模块结构
|
||
|
||
```
|
||
wechat_clicker/
|
||
├── ax_bridge.py # AXUIElement 底层封装(属性读取、鼠标点击、键盘事件、滚动)
|
||
├── wechat_ui.py # 微信 UI 导航(聊天列表、消息列表、预览界面元素查找)
|
||
├── state_machine.py # UI 状态机(窗口状态检测、恢复、Preview.app 管理)
|
||
├── automator.py # 主自动化逻辑(扫描→进入聊天→锚点+箭头导航点击图片/视频→点击文件→循环)
|
||
├── human_like.py # 拟人行为(高斯分布延迟、长休息、工作时间)
|
||
├── config.py # YAML 配置加载
|
||
└── logger_setup.py # 日志配置
|
||
```
|
||
|
||
### 关键设计决策
|
||
|
||
- 聊天列表项为 AXStaticText(无 AXPress),使用 **CGEvent 鼠标坐标点击**
|
||
- AXValue 位置/尺寸需用 AXValueGetValue 解包 CGPoint/CGSize
|
||
- 消息类型通过 title 内容判断:`"图片"` → 图片,`"视频..."` → 视频,`"文件\n..."` → 文件
|
||
- **图片/文件气泡偏移点击**:AXStaticText 覆盖整行(575px宽),实际气泡仅在左侧 ~80-170px 区域,使用 `click_at_element_offset(x=120, y=height/2)` 命中
|
||
- **图片+视频处理(锚点+箭头导航)**:滚到底部 → 找最底部可见图片或视频(锚点)→ 点击锚点进入预览 → 按 i 次左箭头到达第 i 个 → 检测类型(查找"查看原视频"按钮区分视频/图片)→ 图片:点"..."→"使用预览打开"→ 关闭 Preview.app;视频:点"查看原视频"→ 等 10s → Escape 退出 → 回到微信 → 再次点击锚点 → 左箭头 i+1 次 → ... 循环直到覆盖 `unread_count + overlap` 个
|
||
- **视频类型三层检测**:(1) 有"查看原视频"按钮 → 未下载视频,点击触发下载+等10s;(2) 无"查看原视频"但有"原视频" → 已下载视频,等1s后Escape跳过;(3) 都没有 → 图片,走"..."→"使用预览打开"流程
|
||
- **视频首次点击兜底**:视频首次点击可能不弹出独立预览窗口(微信先下载),等待5s后仍无窗口时,检查主窗口内是否有"查看原视频"按钮(内嵌预览),有则直接处理
|
||
- **锚点稳定性**:连续 3 次失败自动重锚(重新滚到底部 + 重找锚点 + i 归零),最多重锚 2 次;每聊天最多处理 30 个媒体(`MAX_IMAGES_PER_CHAT`),防止单聊天垄断处理时间
|
||
- 文件处理:直接点击触发下载(不走箭头导航)
|
||
- **滚动方向**:macOS CGEvent ScrollWheel **负值=向下滚**(看新消息),**正值=向上滚**(看旧消息)
|
||
- 进入聊天后滚到底部(负值),图片/视频通过箭头导航覆盖历史
|
||
- 滚动使用 **kCGEventMouseMoved + ScrollWheel**(不触发点击),避免误点 UI 元素
|
||
- "..."按钮搜索限制在预览区域(独立窗口或主窗口 x>200),排除侧边栏
|
||
- 可见性检查基于元素**中心点**是否在消息列表可见区域内(30px margin)
|
||
- **QuickTime Player 清理**:直接发送的视频(非文件方式)被点击后通过 QuickTime Player 打开,处理完毕后通过 bundle ID 检测 + activate + Cmd+W 关闭
|
||
- 状态检测基于窗口计数 + **AXMinimized 属性**:区分窗口存在但最小化 vs 真正可见
|
||
- **窗口恢复策略(4 层)**:NSRunningApplication.unhide(隐藏)→ 检测 0 窗口时 `open -b` 重新打开(窗口被关闭,带 polling 验证)→ AX API 设置 AXMinimized=False(最小化)→ activate(前台)
|
||
- **UNKNOWN/ABNORMAL 状态恢复**:先关闭所有非主窗口(含空标题遗留预览、小尺寸浮窗),再 Escape + activate + 点击侧边栏
|
||
- **主窗口识别**:多个"微信"窗口时取面积最大的为主窗口(解决视频迷你播放器等小浮窗干扰)
|
||
- **WECHAT_ABNORMAL 状态**:有窗口但标题非"微信"时(窗口正在加载/登录界面),先尝试关闭多余窗口再等待就绪
|
||
- **窗口恢复二次确认**:`ensure_wechat_visible` 找到主窗口后等 0.5s 二次确认,防止瞬时窗口骗过检测
|
||
- **连续恢复失败保护**:≥15 次连续失败后发送 macOS 系统通知 + 暂停 600s,避免无限空转;外层循环指数退避(10s → 20s → 40s → ... → 120s 封顶)
|
||
|
||
## 使用方法
|
||
|
||
```bash
|
||
# 前置条件
|
||
pip install -r requirements.txt
|
||
# 系统设置 > 隐私与安全 > 辅助功能 → 添加终端/Python
|
||
|
||
# 复制配置
|
||
cp config.example.yaml config.yaml
|
||
|
||
# 运行
|
||
python main.py # 持续运行
|
||
python main.py --once # 单次扫描
|
||
python main.py --dry-run # 只扫描不点击
|
||
python main.py --dump-ui # 输出 UI 元素树
|
||
python main.py --dump-ui --chat "聊天名" # 进入指定聊天 dump 消息元素树(诊断用)
|
||
python main.py --debug # 详细日志
|
||
```
|
||
|
||
## 配置重点
|
||
|
||
- `config.yaml` 中可设置扫描间隔、延迟范围、白/黑名单、工作时间、媒体类型开关
|
||
- `max_chats_per_scan: 0` 表示不限制,处理全部未读聊天(适合大量群聊场景)
|
||
- 默认点击图片和视频(`media.click_files: false`)
|
||
- 默认日志级别 DEBUG,便于问题定位
|
||
- 默认工作时间 7:00-次日1:00(非工作时间 1:00-7:00),支持跨午夜
|
||
- 默认黑名单包含微信系统账号和非聊天界面(腾讯新闻、微信支付、微信团队、服务号、订阅号、文件传输助手)
|
||
- 处理完有未读的聊天后立即重扫,不等待 scan interval;只有无未读时才 sleep
|
||
- 忙时(未读 > 5)自动跳过随机休息
|
||
|
||
## 注意事项
|
||
|
||
- 微信窗口最小化、隐藏或被关闭时工具均会自动恢复(unhide → open -b 重开窗口 → AXMinimized=False → activate)
|
||
- 运行时会占用微信前台操作
|
||
- 建议在专用电脑上运行
|