feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 534 KiB |
+514
@@ -0,0 +1,514 @@
|
||||
# wechatauto 技术文档
|
||||
|
||||
> 版本:1.0.6 | 适用:微信 4.x Windows 客户端 | 语言:Python 3.9+
|
||||
> 本文档描述 wechatauto 的设计原理、内部实现与扩展方式,面向二次开发与排障。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
wechatauto 是上游 [wxauto](https://wxauto.org) 的复刻版,面向**微信 4.x
|
||||
Windows 客户端**的自动化库。它**不是网页版**,直接操作本机客户端。
|
||||
|
||||
与老版本微信(3.x,暴露 UIAutomation 无障碍树)不同,微信 4.1.12+ 的聊天
|
||||
区域采用**自绘渲染**(`MMUIRenderSubWindow*`,不同版本后缀不同,如
|
||||
`MMUIRenderSubWindowHW` / `MMUIRenderSubWindow`),对 UIAutomation / MSAA 完全
|
||||
不暴露内容。因此本项目拆成两条技术路线:
|
||||
|
||||
| 路线 | 模块 | 用途 |
|
||||
| ---- | ---- | ---- |
|
||||
| 本地数据库解密 | `wechatauto/db.py` | 读取消息、监听、历史导出、会话/联系人 |
|
||||
| 坐标 + OCR | `wechatauto/guia.py` | 发送文本/文件/图片、回复、@成员 |
|
||||
|
||||
外加两个独立子系统:媒体下载(`media.py`)、朋友圈读取(`moment.py`)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体架构
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ wechatauto │
|
||||
├──────────────┬─────────────┬───────────────┤
|
||||
读取/监听/导出 │ wechatauto │ wechatauto │ wechatauto │ 发送/回复/@
|
||||
┌──────────────┐ │ db.py │ media.py │ guia.py │ ┌────────────┐
|
||||
│ Listener │ │ (SQLCipher4) │ (媒体解密) │ (坐标+OCR) │ │ WeChatGUI │
|
||||
│ MomentDB │ └──────────────┴─────────────┴───────────────┘ └────────────┘
|
||||
│ msgs/ ui/ │ │ │ │
|
||||
└──────┬───────┘ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
contact/session/message_*.db msg\attach\*.dat 屏幕截取 + Win32 微信主窗口
|
||||
sns.db(SQLCipher 4) media_0.db 用户输入(SendInput) (MMUIRender…)
|
||||
```
|
||||
|
||||
### 2.1 模块职责
|
||||
|
||||
| 模块 | 职责 |
|
||||
| ---- | ---- |
|
||||
| `db.py` | 微信数据库密钥提取、SQLCipher 4 解密、WAL 增量合并、消息查询/监听/导出、会话/联系人 |
|
||||
| `guia.py` | 坐标 + OCR 发送:窗口定位、会话点击、输入框探测、剪贴板粘贴、发送按钮、发送后 DB 验证 |
|
||||
| `media.py` | 图片 v2 解密(AES+XOR)、语音/视频/文件下载、剪贴板 CF_HDROP |
|
||||
| `moment.py` | 朋友圈时间线读取、媒体下载(`MomentDB` 数据库路线) |
|
||||
| `wx.py` | 高层 API 入口(`WeChat`/`Chat`/`Listener`),内部按能力分派到 DB 或 GUI |
|
||||
| `msgs/` | 消息对象模型(文本/图片/语音/引用/系统消息等) |
|
||||
| `ui/` | 老版 UIAutomation 控件层(微信 3.x 遗留,4.x 受限,保留兼容) |
|
||||
| `utils/` | `win32.py`(窗口/剪贴板/进程)、`lock.py`(全局 UI 锁)、`tools.py` |
|
||||
| `param.py` | `WxParam`/`WxResponse`/`PROJECT_NAME` 等公共参数与返回类型 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据库读取子系统(`wechatauto/db.py`)
|
||||
|
||||
### 3.1 数据存储布局
|
||||
|
||||
微信 4.x 数据位于登录目录下。**不同电脑/账号的目录位置不同**,由
|
||||
`auto_detect_db_dir()` 自动定位,探测链(`db.py`):
|
||||
|
||||
1. **微信配置文件**:扫描 `%APPDATA%`/`%LOCALAPPDATA%` 下
|
||||
`Tencent\xwechat`、`Tencent\WeChat` 等配置目录内的**所有文件**,内容支持
|
||||
JSON(`dataDir`/`fileSavePath` 等字段)、纯路径、或任意文本中提取第一个
|
||||
Windows 路径(`_extract_path_from_config`);
|
||||
2. **注册表**:读 `HKCU\Software\Tencent\xwechat` 等键下含
|
||||
path/dir/save 的值(用户自定义保存位置时的补充来源);
|
||||
3. **常见默认目录**:`Documents`、用户主目录兜底。
|
||||
|
||||
定位结果统一经 `_locate_account_root()` 归一化:兼容
|
||||
`<root>/xwechat_files/<wxid>_xxxx/db_storage` 与
|
||||
`<root>/<wxid>_xxxx/db_storage` 两种布局,返回包含 `wxid_*` 账号目录的
|
||||
父目录(即 `WeChatDB.db_dir`)。仍找不到时可手动传入 `db_dir=` 指定。
|
||||
|
||||
典型布局:
|
||||
|
||||
```
|
||||
D:\微信文件\xwechat_files\<wxid>_xxxx\db_storage\
|
||||
├── contact\contact.db 联系人(昵称、备注、别名)
|
||||
├── session\session.db 会话列表(未读数、摘要、排序时间戳)
|
||||
├── message\message_0..4.db 聊天消息(按会话分表 Msg_<md5>,跨库分片)
|
||||
├── message\media_0.db 语音(VoiceInfo.voice_data,SILK 二进制)
|
||||
├── message\message_resource.db 文件原名(MessageResourceDetail.packed_info)
|
||||
├── sns\sns.db 朋友圈(SnsTimeLine、SnsDataItem XML)
|
||||
└── ...
|
||||
```
|
||||
|
||||
每个数据库都是 **SQLCipher 4** 加密的 SQLite:页大小 4096、
|
||||
`PBKDF2-HMAC-SHA512`(加密密钥迭代 256000 次),每页 IV(16) + HMAC(80)。
|
||||
|
||||
### 3.2 密钥提取(进程内存只读扫描)
|
||||
|
||||
每个数据库有**独立的 32 字节密钥**,运行时保存在微信进程
|
||||
`com.Tencent.WCDB.Config.Cipher` 配置对象中。`extract_keys()` 流程:
|
||||
|
||||
1. 用 `psutil` 枚举 `Weixin.exe` 进程,取可读内存区域;
|
||||
2. 定位字符串 `com.Tencent.WCDB.Config.Cipher` 的内存地址;
|
||||
3. 从字符串地址回溯配置对象(`[ptr][len]` 结构),读取数据块;
|
||||
4. 数据块与固定掩码(`CONFIG_XOR_MASK`)逐字节异或,得到明文配置:
|
||||
`x'<64位hex密钥><32位hex盐>'`;
|
||||
5. 用 SQLCipher 4 的 HMAC 校验规则验证候选密钥(取某库首页试解);
|
||||
6. 通过后写入 `%TEMP%\wechatauto_db\<账号>\keys.json` 缓存,
|
||||
之后进程内复用(`_cached_db`),无需每次重新扫描。
|
||||
|
||||
### 3.3 数据库解密
|
||||
|
||||
- SQLCipher 4,`PAGE_SZ=4096`,页尾 `RESERVE_SZ=80` 保留字节
|
||||
(IV 16 + HMAC 64);
|
||||
- 解密结果按页写入临时工作目录;通过比对源文件 mtime/size 的 stamp
|
||||
(`STAMP_VERSION=2`,含缓存格式版本号)判定是否可复用缓存;
|
||||
- 首次解密 contact.db 约 6s,之后全部秒级;
|
||||
- 每次打开连接前用 `PRAGMA integrity_check` 兜底,损坏时自动全量重建。
|
||||
|
||||
### 3.4 WAL 增量合并(帧盐校验)
|
||||
|
||||
微信 `-wal` 是**预分配文件**:checkpoint 时 WAL 头 salt+1 并清零写游标,
|
||||
但**旧世代帧仍留在文件中**。若合并时不过滤帧盐,会把过期页覆盖进主库,
|
||||
导致 `database disk image is malformed`。修复方案(`_merge_wal`):
|
||||
|
||||
1. 读取 WAL 头 `salt1/salt2`;
|
||||
2. 仅合并 **salt 与当前 WAL 头一致**的帧,旧世代帧直接跳过;
|
||||
3. 合并结果 `PRAGMA integrity_check` 校验,失败自动回退全量重建;
|
||||
4. 缓存 stamp 版本升级到 2,旧格式缓存强制重建。
|
||||
|
||||
### 3.5 消息分片与查询
|
||||
|
||||
- 会话名 → `Md5(会话微信号)` → 表名 `Msg_<md5>`;
|
||||
- 同一会话可能分片在多个 `message_*.db`,查询时按 `sort_seq` 合并排序;
|
||||
- 关键列:
|
||||
- `local_type` → 消息类型(见 `MSG_TYPE_NAMES`);
|
||||
- `real_sender_id`:`2` 表示自己,其他为数字 id,可经
|
||||
`message_resource.SenderName2Id` 反查微信号;
|
||||
- `server_id`:服务端消息 id;
|
||||
- `packed_info_data`:图片/视频 md5 等二进制载荷;
|
||||
- `sort_seq`:会话内单调递增序号(增量游标用);
|
||||
- `get_messages(user, limit, offset)` 倒序取最近消息;
|
||||
- `get_message_row(user, local_id)` 取含 `packed_info` 的原始行(媒体用);
|
||||
- `get_new_messages(user, since_seq)` 返回 `sort_seq > since_seq` 的增量,
|
||||
升序排列。
|
||||
|
||||
### 3.6 增量消息与水印
|
||||
|
||||
`Listener` 每个被监听会话维护一个 `watermark`(最近一次 `sort_seq`)。
|
||||
轮询时以水印为游标取增量,成功后把水印推进到 `msgs[-1]["sort_seq"]`,
|
||||
保证不重不漏。回调签名 `callback(msg: dict, listener)`。
|
||||
|
||||
### 3.7 会话 / 联系人 / 导出
|
||||
|
||||
- `get_sessions(limit)`:读 `session.db` 的 `SessionTable`
|
||||
(`username/unread/summary/last_time/last_sender`);
|
||||
- `search_contact(keyword)`:对 contact.db 按 昵称/备注/微信号/别名 模糊匹配;
|
||||
- `get_nickname(user)`:微信号 → 显示昵称(内部维护昵称/发送者索引);
|
||||
- `export_history(out, fmt, users, limit_per_chat)`:全量导出
|
||||
JSON / SQLite,支持指定会话与条数上限。
|
||||
|
||||
---
|
||||
|
||||
## 4. 媒体下载子系统(`wechatauto/media.py`)
|
||||
|
||||
### 4.1 媒体存储
|
||||
|
||||
| 媒体 | 位置 | 说明 |
|
||||
| ---- | ---- | ---- |
|
||||
| 图片 | `msg\attach\<会话md5>\<YYYY-MM>\Img\<md5>.dat` | v2 加密 |
|
||||
| 语音 | `media_0.db` → `VoiceInfo.voice_data` | SILK 明文 BLOB |
|
||||
| 文件 | `msg\file\<YYYY-MM>\<原文件名>` | 原名来自 message_resource |
|
||||
| 视频 | `msg\video\<YYYY-MM>\<id>.mp4` | 仅本地落盘时可用 |
|
||||
|
||||
### 4.2 图片 v2 格式
|
||||
|
||||
```
|
||||
[6B 签名 07 08 56 32 08 07] [4B aes_size LE] [4B xor_size LE]
|
||||
+ AES-ECB 密文段 + 明文段 + 异或段
|
||||
```
|
||||
|
||||
### 4.3 AES 密钥内存反测
|
||||
|
||||
图片 AES 密钥是 16 字节 ASCII、账户级稳定,但**仅在微信查看图片时驻留进程
|
||||
内存**。`detect_image_key()` 的流程:
|
||||
|
||||
1. 优先读缓存 `image_keys.json`;
|
||||
2. 否则对 `Weixin.exe` 内存做定向扫描,对每个候选 16 字节串用 AES-ECB
|
||||
解密图片首块,校验 JPEG/PNG 魔数,命中即认为有效;
|
||||
3. 命中后持久化;也可用 `image_key=` 显式注入。
|
||||
|
||||
### 4.4 XOR 密钥推导
|
||||
|
||||
异或段密钥是单字节,从同图缩略图 `<md5>_t.dat` 尾部 JPEG 结束标记
|
||||
`FF D9` 反推:`key = tail[0] ^ 0xFF`。
|
||||
|
||||
### 4.5 下载分发
|
||||
|
||||
`download_media(user, local_id)` 按 `local_type` 自动分发到
|
||||
`download_image/_voice/_video/_file`,输出到 `DEFAULT_SAVE_PATH`
|
||||
(`~/Documents/wechatauto_media`)或自定义 `save_dir`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 朋友圈子系统(`wechatauto/moment.py`)
|
||||
|
||||
`MomentDB` 直接读 `sns.db`:
|
||||
|
||||
- `get_moments(limit, offset, username)`:时间线(最新在前,3382 条全量可读);
|
||||
- `get_moment(tid)` / `get_my_moments(limit)`:单条 / 我的动态;
|
||||
- 文本、图片 md5、点赞、评论均由 `SnsDataItem` XML 解析得到;
|
||||
- `find_local_media(md5, kind)` / `download_media(media, save_dir)`:
|
||||
本地缓存(`Sns\Img` / `Sns\Video`)优先,否则 URL 拉取。
|
||||
|
||||
> 发表朋友圈已舍弃(4.x 自绘界面不可靠);点赞/评论仅在旧 UIA 类中
|
||||
> 保留,4.x 下不可用。
|
||||
|
||||
---
|
||||
|
||||
## 6. 发送子系统(`wechatauto/guia.py`)
|
||||
|
||||
发送走「屏幕坐标 + 本地 OCR」,全部坐标基于**渲染子窗口相对坐标**,
|
||||
运行时换算为屏幕绝对坐标,保证跨 DPI / 分辨率 / 窗口尺寸一致。
|
||||
|
||||
### 6.1 窗口定位与布局
|
||||
|
||||
- 主窗口:**多特征兜底定位**(防 Qt 升级改名)。类名前缀
|
||||
`Qt51514QWindowIcon` 只是「软条件」之一,`_find_main_window()` 对全部
|
||||
可见顶层窗口按「标题含微信 / 类名前缀 / 进程名 `weixin.exe` / 可见 /
|
||||
尺寸 ≥ 800px」加权评分,取最高分;完全找不到时退回按标题精确查找。
|
||||
- 渲染子窗口:类名前缀 `MMUIRenderSubWindow`(自绘内容所在;不同版本
|
||||
后缀不同,如 `MMUIRenderSubWindowHW` / `MMUIRenderSubWindow`,
|
||||
`_find_render_window()` 按前缀匹配并取面积最大者);找不到渲染子窗口
|
||||
时(Qt 改版等)直接**回退用主窗口矩形计算坐标**,不中断使用。
|
||||
- `_update_render_rect()` 读 `GetWindowRect` 得到渲染区,`_update_layout()`
|
||||
按比例换算各布局常量:
|
||||
- `SIDEBAR_RATIO=0.22` → 侧栏宽度 / 渲染区宽度(默认基线);
|
||||
- `SEARCH_BOX_RATIO` → 左侧搜索框;
|
||||
- `SEND_BUTTON_RATIO` → 右下角发送按钮检索区;
|
||||
- 所有像素坐标均改为比例计算,避免硬编码失效。
|
||||
|
||||
### 6.1.1 布局动态校准(防 DPI/布局漂移)
|
||||
|
||||
不同机器(DPI 缩放 / 窗口尺寸 / 微信版本)下,固定比例常量可能漂移。
|
||||
`WeChatGUI` 提供「跑一次永久兼容」的校准机制:
|
||||
|
||||
1. **锚点实测**:`calibrate_layout()` 用 OCR 检测「搜索」占位文本
|
||||
(文本中心 ≈ 侧栏宽 0.28)反推侧栏宽度;在右下角 OCR 找「发送」
|
||||
按钮反推其检索区比例。结果都钳制在合理范围(侧栏比例限
|
||||
`[0.14, 0.30]`),**检测失败一律回退默认常量**,宁可不动不误配;
|
||||
2. **按机器缓存**:结果写入 `~/.wechatauto/layout-<主机名>_<分辨率>.json`
|
||||
(含当时的 `render_w/h`);再次运行时 `_load_layout()` 自动加载,
|
||||
与当前窗口尺寸差异 >15% 视为不匹配,自动重新校准;
|
||||
3. **异常自动重校准**:`get_input_box()` 探测连续 6 次失败时,自动触发
|
||||
`calibrate_layout()` 并再次探测(每会话一次),应对运行中途布局漂移;
|
||||
4. **显式强制**:`WeChatGUI(calibrate=True)` 强制重新校准;也可在任何
|
||||
时候调用 `wx.calibrate_layout()`。
|
||||
|
||||
> 说明:微信 4.x 侧栏背景与消息区同为纯白,**像素色差检测不可靠**
|
||||
> (实测边界两侧均为 255,唯一稳定锚点是搜索框 OCR 文本)。
|
||||
|
||||
### 6.2 会话定位与点击(防 toggle 关闭)
|
||||
|
||||
`open_chat()` 优先点侧栏(最可靠),失败走搜索框回退。流程:
|
||||
|
||||
1. `find_session(name)`:OCR 侧栏,按名称匹配返回会话行位置;
|
||||
2. **活动行检测** `_row_is_active(rel_y)`:微信活动会话行背景为绿色
|
||||
`(21,172,112)`,普通行浅灰 `(238,238,240)`。采样行内横向条带统计
|
||||
"g 显著大于 r/b" 的绿色像素数,超过阈值判定为已打开;
|
||||
3. 若行已是活动行(会话已打开),**跳过点击**——因为再点一次会
|
||||
toggle 关闭会话(这是二次发送失败的历史根因);
|
||||
4. 否则点击并 `_chat_open_confirmed()`:轮询标题 OCR 命中,或
|
||||
`_pane_has_content()` 判定消息区已渲染非空白;
|
||||
5. 侧栏确认失败 → `_search_chat(name)` 搜索框兜底,命中独立聊天窗则
|
||||
`use_window` 切换 GUI 目标。
|
||||
|
||||
### 6.3 输入框定位与文本输入
|
||||
|
||||
- `get_input_box()`:在输入区按比例扫描白色矩形带(`_probe_input_box`);
|
||||
- `focus_input()`:点击输入框聚焦;
|
||||
- `input_text()`:文字以 **剪贴板 + Ctrl+V** 注入(绕过中文输入法拦截),
|
||||
失败时回退拼音组合(`pypinyin` + SendInput 大写字母键)。
|
||||
|
||||
### 6.4 发送与验证
|
||||
|
||||
- `click_send()`:OCR 定位发送按钮(`发送`/`Enter` 图标)点击,
|
||||
找不到则直接回车;
|
||||
- `verify=True` 时用 `WeChatDB` 读回确认:`send_msg` 轮询
|
||||
`_verify_sent()`(新消息类型 + 内容匹配),`send_file/send_image` 用
|
||||
`_verify_attachment_sent()`(`sort_seq` 基线 + 类型/文件名/`packed_info`
|
||||
匹配,图片按类型+时序)。
|
||||
- DB 实例经 `_get_db()` 缓存复用,避免每次发送验证都重新解析数据库
|
||||
(这是历史慢 2.5 分钟问题的根因之一)。
|
||||
|
||||
### 6.5 文件 / 图片发送(CF_HDROP)
|
||||
|
||||
文件/图片通过 **剪贴板 CF_HDROP + Ctrl+V** 插入草稿再回车发送,绕开自绘
|
||||
「+ 菜单」定位。`copy_files_to_clipboard(paths)` 写入文件列表剪贴板,
|
||||
粘贴后 `click_send()`。
|
||||
|
||||
### 6.6 回复与艾特
|
||||
|
||||
- `reply_msg(text, who, target_text, verify)`:悬停最近一条消息
|
||||
(`_last_message_y` 估算)→ OCR 悬停工具栏找「回复」→ 点击 →
|
||||
`input_text` → 发送;找不到工具栏回退右键菜单;
|
||||
- `at_member(member, text, who, verify)`:聚焦输入框 → 键入 `@` →
|
||||
OCR 成员选择弹层定位成员名 → 点击 → 输入正文 → 发送;
|
||||
- 两者均可用 `verify=True` 走 DB 确认。
|
||||
|
||||
### 6.7 关键工程修复记录
|
||||
|
||||
| 问题 | 根因 | 修复 |
|
||||
| ---- | ---- | ---- |
|
||||
| 二次发送失败/2.5min 卡死 | 点击已打开会话被 toggle 关闭 | `_row_is_active` 活动行高亮检测,跳过点击 |
|
||||
| 发送验证每次重新解析 DB | `WeChatDB()` 每次新建、重复解库 | `_get_db()` 缓存实例 |
|
||||
| 窗口尺寸变化布局失效 | 硬编码像素坐标 | 全量改按比例布局 |
|
||||
| `Listener.stop()` 崩溃(v1.0.3) | `_run/_poll_once` 用 `sys.stderr` 却未 `import sys` | db.py 补 `import sys` |
|
||||
| 部分文本消息显示为 `[文本]`/空(v1.0.3) | 微信 4.x 文本消息 content 为「容器头+明文+填充」结构,`_friendly_content` 无法解码 | 新增 `_extract_text_from_blob` 还原明文,数据库与 bot 均可见真实内容 |
|
||||
| 动画表情被落盘为打不开的伪 `.gif`(v1.0.3) | 解密后为 `wxgf` 容器(微信动画表情私有格式),按魔数误判为 GIF | `download_image` 对 `wxgf` 直接返回 `None`,不落盘 |
|
||||
| 搜索联系人点错「网络搜索」(v1.0.3) | OCR 未按视觉顺序排序,且未过滤「搜索网络结果」节标题 | `_search_chat` 按 y 排序并跳过网络搜索节标题,取第一条联系人 |
|
||||
| 表情截图截到自己发的消息(v1.0.3) | `capture()` 固定取「最底部一条」,截图前自己发消息时底部是自己的气泡 | 按消息方向(`msg.attr`)裁剪:自己消息用消息分隔空白定位、对方消息用头像锚点定位,阈值自适应截图尺寸 |
|
||||
| 逐条发送都要重新点击对话框(v1.0.3) | `send_msg` 每次对目标会话都走完整 `open_chat`(重扫侧栏+点击) | 记录 `_current_chat`,目标会话已打开时跳过 `open_chat` 直接输入发送 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 消息监听(`Listener`)
|
||||
|
||||
```python
|
||||
from wechatauto import WeChatDB
|
||||
from wechatauto.db import Listener
|
||||
|
||||
db = WeChatDB()
|
||||
lst = Listener(db, interval=1.0) # 轮询间隔(秒)
|
||||
lst.add_listener("filehelper", cb) # cb(msg: dict, listener)
|
||||
lst.start() # 后台守护线程
|
||||
lst.stop() # 优雅停止
|
||||
```
|
||||
|
||||
- 每个会话独立水印,`watermark` 属性可读写(支持断点续跑);
|
||||
- 单次轮询/回调异常不终止监听,写 `sys.stderr` 后继续;
|
||||
- 数据来源是解密后的本地库,与客户端在线状态无关;
|
||||
- **回调并发模型(v1.0.2)**:回调在独立工作线程中执行。每个被监听会话
|
||||
对应一条**串行**工作线程:同一会话内消息按序处理、不同会话间并行;
|
||||
轮询线程只负责读取数据库并分派任务,不会被慢回调(AI 调用 / 图片识别等)
|
||||
阻塞。`stop()` 会向工作队列投递停止信号并等待线程退出。
|
||||
|
||||
---
|
||||
|
||||
## 8. 消息模型与 UIA 兼容层
|
||||
|
||||
- `msgs/`:`BaseMessage` 及子类(`TextMessage/ImageMessage/VideoMessage/
|
||||
VoiceMessage/FileMessage/QuoteMessage/LinkMessage/LocationMessage/
|
||||
PersonalCardMessage/EmojiMessage/SystemMessage`),`parse_msg()` 统一解析;
|
||||
- `EmojiMessage`(`type='emotion'`,v1.0.2 新增):"动画表情"消息专属类型,
|
||||
`FriendEmojiMessage` / `SelfEmojiMessage` 按收发方向区分。微信 4.x 表情
|
||||
content 在数据库中为加密数据,`capture()` 采用「打开会话 → 滚动到底 →
|
||||
截取消息区 → 自动裁剪最后一条消息气泡」的屏幕截图方案返回图片路径,
|
||||
供上层 AI 视觉识别使用(示例见 `demo_emoji_capture.py`)。
|
||||
|
||||
定位实现(`msgs/mtype.py`):
|
||||
- 微信 4.x 主窗口为 Qt 自绘渲染(`Qt51514QWindowIcon`),不暴露 UIA 子树,
|
||||
且 DB 模式消息的 control 是伪控件(`_DBMessageControl.BoundingRectangle`
|
||||
恒为 0),无法用控件坐标定位,因此**全部基于屏幕像素分析**;
|
||||
- `_pane_capture_img()` 截取消息区全宽画面(渲染顶部到输入框);
|
||||
- 自己发的消息(`attr='self'`):`_crop_bottom_message()` 用「消息分隔
|
||||
空白」定位消息顶部,空白阈值自适应截图高度(约消息区高度的 2.5%),
|
||||
跨分辨率/DPI 保持一致;
|
||||
- 对方发的消息(`attr='friend'`):`_crop_by_avatar()` 优先用左侧头像
|
||||
圆形彩色块的顶部锚定消息顶部(特征跨分辨率稳定),失败回退
|
||||
`_crop_bottom_message()`;
|
||||
- 结果再经 `_content_bbox_crop()` 裁掉空白边缘后保存;
|
||||
- 会话已打开时跳过 `open_chat`(避免刷新消息列表/切换窗口),并记录
|
||||
`[CAP]` 诊断日志与 `~/pane_diag_raw.png` 原图便于跨电脑排查;
|
||||
- `msgs/mtype.py`:`WxParam.MSG_TYPE_*` 常量到类型类的映射;
|
||||
- `ui/`:`BaseUISubWnd/Component/Main/NavigationBox/SessionBox/ChatBox`,
|
||||
老版 UIA 控件层,微信 4.x 下受限,保留兼容;
|
||||
- `wx.py`:`WeChat/Chat/Listener` 高层入口,内部按能力分派:
|
||||
聊天区读取/监听走 DB,发送走 GUI(`guia.WeChatGUI`)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 工具层(`utils/`)
|
||||
|
||||
- `win32.py`:`find_all_windows_from_root`(遍历子窗口)、
|
||||
`GetPathByHwnd`(进程路径)、`SetClipboardText`;
|
||||
- `lock.py`:`LockManager` + `uilock` 全局互斥锁,避免多线程并发操作
|
||||
同一微信窗口;
|
||||
- `tools.py`:杂项工具函数。
|
||||
|
||||
---
|
||||
|
||||
## 10. API 参考
|
||||
|
||||
### `wechatauto/__init__.py` 顶层导出
|
||||
|
||||
`WeChat`、`Chat`、`Listener`、`WeChatDB`、`auto_detect_db_dir`、
|
||||
`list_accounts`、`MediaDownloader`、`WeChatGUI`、`quick_send`、
|
||||
`quick_send_file`、`quick_send_image`、`quick_reply`、`WinInput`、
|
||||
`ScreenOCR`、`WxParam`、`WxResponse`、`wxlog`、`Moment`、`MomentDB`、
|
||||
`LockManager`、`uilock`、异常类、消息模型、`parse_msg`、`PROJECT_NAME`。
|
||||
|
||||
### `WeChatDB`
|
||||
|
||||
| 方法 | 说明 |
|
||||
| ---- | ---- |
|
||||
| `get_self_info()` | 当前账号(username / nick_name / remark) |
|
||||
| `get_sessions(limit=100)` | 会话列表 |
|
||||
| `search_contact(keyword)` | 搜索联系人 |
|
||||
| `get_messages(user, limit, offset)` | 会话消息 |
|
||||
| `get_message_row(user, local_id)` | 原始行(媒体用) |
|
||||
| `get_new_messages(user, since_seq)` | 增量消息 |
|
||||
| `get_nickname(user)` | 微信号 → 昵称 |
|
||||
| `list_message_chats()` | 含消息的会话 |
|
||||
| `export_history(out, fmt, ...)` | 导出 JSON/SQLite |
|
||||
| `extract_keys()` | 手动密钥提取 |
|
||||
|
||||
### `MediaDownloader`
|
||||
|
||||
`detect_image_key(refresh)`、`decrypt_image(dat_path)`、
|
||||
`download_media(user, local_id)`、`download_image/_voice/_video/_file`、
|
||||
`copy_files_to_clipboard(paths)`。
|
||||
|
||||
### `MomentDB`
|
||||
|
||||
`get_moments(limit, offset, username)`、`get_moment(tid)`、
|
||||
`get_my_moments(limit)`、`find_local_media(md5, kind)`、
|
||||
`download_media(media, save_dir)`。
|
||||
|
||||
### `WeChatGUI`
|
||||
|
||||
`send_msg`、`send_file`、`send_image`、`reply_msg`、`at_member`、
|
||||
`open_chat`、`focus_input`、`bring_to_front`、`get_sessions`、
|
||||
`get_input_box`;一行式 `quick_send` 等。
|
||||
|
||||
---
|
||||
|
||||
## 11. 目录结构
|
||||
|
||||
```
|
||||
wechatauto/
|
||||
├── wechatauto/
|
||||
│ ├── __init__.py 公共导出
|
||||
│ ├── wx.py 高层 API(UIA 受限,分派 DB/GUI)
|
||||
│ ├── guia.py ★ 坐标+OCR 发送模块
|
||||
│ ├── db.py ★ 数据库解密/监听/导出
|
||||
│ ├── media.py ★ 媒体下载(图片 v2 解密)
|
||||
│ ├── moment.py ★ 朋友圈(MomentDB)
|
||||
│ ├── param.py / logger.py / languages.py / exceptions.py
|
||||
│ ├── ui/ msgs/ utils/ uia/
|
||||
│ └── py.typed
|
||||
├── demo.py UI 自动化示例(4.x 受限)
|
||||
├── demo_db.py ★ 数据库读取示例
|
||||
├── demo_guia.py ★ 坐标+OCR 发送示例
|
||||
├── demo_listen.py ★ 实时消息监听示例
|
||||
├── demo_reply_at.py ★ 回复/@ 成员实测示例
|
||||
├── demo_emoji_capture.py ★ 表情消息截图示例
|
||||
├── README.md
|
||||
├── docs/技术文档.md 本文档
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 已知限制
|
||||
|
||||
1. **需要微信登录**:数据库密钥存于进程内存,首次需微信运行中(提取后缓存);
|
||||
重新登录密钥变化需重新提取(自动校验失败重扫);
|
||||
2. **图片 AES 密钥瞬态**:仅在微信查看图片时驻留内存,命中后持久化
|
||||
`image_keys.json`,也可 `image_key=` 显式注入;
|
||||
3. **发送依赖桌面**:锁屏/会话断开时 `desktop_available()` 返回 False;
|
||||
回复/艾特依赖 OCR 工具栏/弹层,弱网或弹层排版变化时可能失败;
|
||||
4. **视频仅本地可用**:视频 mp4 未落盘时返回 None;
|
||||
5. **发朋友圈已舍弃**;点赞/评论在 4.x 下不可用;
|
||||
6. **UI 自动化受限**:4.1.12+ 聊天区自绘,UIA 路线不可用。
|
||||
|
||||
---
|
||||
|
||||
## 13. 扩展开发指南
|
||||
|
||||
### 新增一个发送类型
|
||||
|
||||
1. 在 `guia.py` 仿照 `send_file`/`send_image` 实现原语(定位 → 注入 →
|
||||
发送 → 可选验证);
|
||||
2. 需要验证时,用 `_target_seq()` 取基线、`_verify_attachment_sent()`
|
||||
轮询 DB;
|
||||
3. 在 `__init__.py` 导出,并加一行式 `quick_*` 封装。
|
||||
|
||||
### 新增一种媒体格式
|
||||
|
||||
1. 在 `media.py` 参照 `_decrypt_v2` 实现解码原语;
|
||||
2. 在 `download_media()` 分发处按 `local_type` 接入;
|
||||
3. 按需在 `_img_md5`/`_find_dat` 补充定位逻辑。
|
||||
|
||||
### 新增数据库表
|
||||
|
||||
在 `db.py` 的 `_collect_db_files()` 注册新库,用 `_open(rel)` 打开连接,
|
||||
复用 `_check_merged` 的 WAL 合并与 integrity 兜底。
|
||||
|
||||
### 布局常量校准
|
||||
|
||||
微信更新导致 OCR 定位失效时,优先检查 `guia.py` 顶部 `*_RATIO` 常量与
|
||||
`_update_layout()`,而非改硬编码坐标。跨机器/DPI 的布局漂移走自动校准:
|
||||
删掉 `~/.wechatauto/layout-<机器>.json` 后重启(或
|
||||
`WeChatGUI(calibrate=True)` / 调用 `calibrate_layout()`)会重新实测并
|
||||
缓存「搜索/发送」锚点比例。
|
||||
|
||||
---
|
||||
|
||||
## 14. 路线图
|
||||
|
||||
1. 回复/艾特的跨版本稳定性校准(当前依赖 OCR 工具栏/弹层位置);
|
||||
2. 视频消息下载增强(确认 4.x 视频落盘位置);
|
||||
3. 导出/首扫并行化,密钥内存扫描增量缓存;
|
||||
4. 支持更多消息类型(小程序、转账、位置等)友好显示(表情消息已在 v1.0.2
|
||||
支持)。
|
||||
Reference in New Issue
Block a user