Repository navigation
Releases: fanyuantaier/wechatauto-replica
Release list
Release v1.2.8.0
v1.2.8.0 (2026-10-23)
新增「群成员加好友」,顺手修了 open_chat 搜错框的老问题,并适配微信 4.1.15.56 的图片气泡类名。
新增:群成员加好友
wx.AddFriendFromGroup("群名", "小明") / wechatauto 加好友 群名 成员名。
界面路径:打开群 → 右上角「聊天信息」→「搜索群成员」→ 点结果卡片 →「添加到通讯录」→ 提交申请。
- 搜索结果按屏幕位置取第一行,不按控件树顺序。 一个名字可能命中多行,而 Qt 回收列表的子节点顺序和肉眼看到的顺序不一致;旧写法会打开最下面那张卡。现在按
BoundingRectangle.top排序后取最上面一行。 - 「添加到通讯录」同时是好友判据:非好友的资料卡只有这一颗按钮;已是好友的卡片是「发消息 / 语音聊天 / 视频聊天」,不会给通讯录里已有的人重复发申请。
- 好友申请框不写死类名:点完「添加到通讯录」后按「新出现的顶层窗 + 提交按钮」定位。申请框是
mmui::VerifyFriendWindow(标题「申请添加朋友」),提交按钮叫「确定」而不是「发送」,两个文案都认;发完在general.db:FMessageTable新增一行is_sender_=1、scene_=14的记录,可独立回读确认。 dry_run=True/--dry-run只走到资料卡,一颗按钮都不点。
修复
open_chat 会把词打进错误的「搜索」框
4.1.15 树里最多同时有三颗 Name='搜索' 的输入框(会话搜索、聊天内搜索、群成员搜索),旧代码取树里第一颗;开着会话或群信息面板时搜索词落进聊天内搜索,会话自然打不开。现在按祖先类名优先会话搜索框;遍历时撞上微信重绘产生的失效元素(COMError)也不再打断 open_chat。客户端停在朋友圈页时,常规导航栏点击会被透明渲染层吃掉,追加一次直接点击「微信」tab 的兜底(仍走拟人随机落点 + 曲线轨迹)。
微信 4.1.15.56 图片气泡类名适配
4.1.15.56 起图片气泡改用独立类名 mmui::ChatImageItemView,旧版 mmui::ChatBubbleReferItemView 也继续认——否则 MediaDownloader 会把图片行整片判成「看不见」,行对齐/取原件报「可视区里没有图片气泡」。两个类名都支持,跨版本可用;tools/selftest.py 已加回归。
验证
新增 30 项离线自检(取最上面一行、已是好友、dry-run、申请框缺失/无留言框/无提交按钮),全部离线套件绿;真机对 1 名非好友群成员完整发出好友申请,并在 FMessageTable 回读确认。
1.2.6.1:重要修复 — 发送者按消息所在分片的 Name2Id 解析
重要修复:发送者名称错位(issue #34)
-
⚠️ 重要修复:群消息的发送者会被换成无关联系人——real_sender_id必须按「消息所在分片」的Name2Id解析。(issue #34)- 现象(实际使用中人工发现):读取消息时发送者名称错位,显示成无关的联系人或群名称。
- 成因一(编号空间用错了表):
real_sender_id是消息所在分片message_N.db里
Name2Id的 rowid,而代码一直拿message_resource.db的SenderName2Id去解析。
这两张表的编号互不相干,同一个数字解析成的就是另一个人。 - 成因二(「编号 2 就是自己」不成立):本机自己账号在
message_0/1/2.db里的 rowid
实测是 2/4/1,写死== 2只在其中那一片成立;1.2.4 为了绕开它加的
「查不到就兜底按 1」在另外两片上同样必错。 - 判据(不依赖任何 id 表):群聊文本行的正文里,微信自己写了
发送者:前缀。本机 12 个群
3662 条可比对的行——按分片解析 3662 条全对,按旧的全局表 0 条对(3656 条指到别人身上、
6 条落空)。文件传输助手 856 行全是这台机器发的:新口径 855 行给出本机 wxid(另 1 行的
发送者确实就是filehelper本身),旧口径会把其中 639 行说成某个常联系的好友或一个群。 - 影响范围:v1.0.0 ~ v1.2.6 全部版本(按错表解析的
_sender_id_index()在初始提交里就有,
最早带标签的 1.0.3 已含)。可见面随版本变宽:export_history的sender_name一直受影响;
get_messages/ 监听回调里的sender_username自 v1.1.8 起走这条解析;群消息
「非文本也能认出人」自 v1.2.4.1 起也是它。 - 修法:查询时把每个分片自己的
Name2Id一并取出(_name2id_map()/_shard_maps()),
_msg_row_to_dict、get_message_row和导出的_resolve_sender都按该行的shard查本片映射;
删掉_sender_id_index();新增公开口WeChatDB.sender_username(shard, sender_id);
wx._db_row_to_message的方向判定与MediaDownloader._sent_by_self改成
「解析出来的 username 等不等于本机 wxid」,去掉sender_id == 2和「兜底按 1」。
解析不到就返回空串(未知),不回落到别的库去猜——这正是报告的建议。 - 行为变化:
sender_username以前几乎每行都有值(其中很多是错的),现在本机 60243 行里约
0.2%(129 行,集中在同一个分片) 是空串;同一个sender_id在不同分片可以解析成不同的
人(这才是正确行为);解析不到时_sent_by_self按「不是本机发的」处理并打一条 debug。 - 判据:新增
tools/test_sender_namespace.py36 项——两片库的Name2Id对同一个编号刻意
给出不同的人、行还故意撞号,所以这些断言只有「按行所在分片解析」才可能全绿;带反向验
(把命名空间退回「两片共用一张全局表」,sid=2立刻变成同一个陌生人);真机段要求文件传输
助手的发送者 ⊆ {本机,filehelper} 且 ≥95% 是本机,群聊按分片解析与正文前缀判据的不一致数
必须为 0。
-
其余内容同 v1.2.6(跨分片消息的
shard可寻址 + 分片读失败不再被静默当成「这个分片没消息」)。
升级:pip install -i https://pypi.org/simple --upgrade wechatauto-replica
1.2.6:跨分片消息可寻址(shard)+ 分片读失败不再静默
重要修复:跨分片消息的可寻址性(issue #20)
-
⚠️ 重要修复:跨分片会话里(会话, local_id)不唯一,按号回读会拿到别的分片的另一条消息。(issue #20)- 背景:微信把一个会话的历史横向拆进多个
message_N.db(分片),而local_id是
每个分片各自从 1 开始计数的。合并读取(v1.2.2 起)之后,(user, local_id)就不再是唯一键了。 - 本机实测:200 个会话表中 54 个跨多片;20008 组
(会话, local_id)同时命中多个分片,
其中 10477 组连local_type都相同而正文/图片不同(抽样里光图片就有 35 组同号但 md5 不同)。 - 后果:
get_message_row()以前不打任何日志,直接取sort_seq最新的那一条。语音/图片/文件/视频
下载全以它为入口,于是表现为"按 A 那条的号,下到了 B 那张图"。 - 修法:消息行统一带上
shard(分片库文件名),合并视图里(local_id, shard)才是唯一键;
get_message_row(user, local_id, local_type=…, shard=…)可以钉死某一片;不指定shard而命中
多片时先告警再按旧口径取最新;shard写错返回None并告警,不瞎猜。 - 顺带铺开:
list_voice_status()/list_image_status()的行也带shard;image_status/
download_image/download_image_original/download_voice/download_video/
download_file全部接受shard,download_media()分发时自动带上候选行自己的shard。 - 兼容性:只给返回 dict 增加
shard键、给方法增加可选关键字参数,旧调用形状一字不变。 - 判据:新增
tools/test_db_shard.py46 项——双分片临时库夹具验合并读取、shard寻址、
新加的? AS shard不挤掉 WHERE 参数、以及分片读失败的可见性;真机部分逐片回读一个真实的
「同号 + 同类型 + 跨分片」行(不带shard拿到的是最新那片,带上之后各拿本片那一行),
并核对get_messages()条数 = 各分片count(*)之和、覆盖分片数 = 该会话的分片数。
- 背景:微信把一个会话的历史横向拆进多个
-
修复:某个分片查询报
database disk image is malformed时被当成"这个分片没有消息"。
损坏现在抛给_run_msg_query走解密缓存重建重试;其它错误仍跳过该片,但会点名是哪个分片
(同一条只说一次,轮询里不刷屏)。这是"读不全"的另一个静默来源——少了一半还返回成功。 -
删除:
_find_msg_table()/_msg_conn()两个单分片桩函数(零调用者,docstring 写着
"只返回第一个命中分片")。issue #20 里那份逐行对照就是照着它们断定get_messages()只读
一片——那段代码自 v1.2.2 起已经没人调用了,但墓碑还挂在文件里。 -
其余内容同 v1.2.5.1(微信数据目录设在盘符根时的自动检测修复、回车前回读校验、
WxParam.ENABLE_OCR总开关、通讯录标签 + 右键转发)。
升级:pip install -i https://pypi.org/simple --upgrade wechatauto-replica
1.2.5.1:重要修复 — 微信数据目录在盘符根时自动检测依赖启动目录
重要修复(issue #32)
⚠️ 重要修复:微信数据目录设在盘符根(配置里写d:\)时,自动检测开始依赖启动目录。(issue #32)- 现象:同一个 Python、同一份依赖、同一个微信配置,只换个工作目录就从"能读"变成
RuntimeError: 未找到微信数据库目录,请通过 db_dir 参数手动指定。 - 原因:
_locate_account_root()用rstrip("\/")去掉尾分隔符,把d:\变成了d:;
而d:在 Windows 上是盘符相对路径,os.path.join('d:', 'xwechat_files')得到
d:xwechat_files,会按该盘自己的当前目录解析(不是进程工作目录,更反直觉)。 - 影响范围:v1.0.0 ~ v1.2.5 全部版本(这行代码从初始提交就是现在这个写法,不是回归)。
- 为什么多数人没遇到:需要两个条件同时成立——① 存储位置选成了裸盘根(选
E:\xwechat_files
这类"盘符+子目录"的,rstrip 什么都不改);② 恰好在那个盘的非根目录里工作(数据放 E 盘
而一直在 C 盘跑 Python 的人,E 盘的该盘当前目录还停在E:\,畸形路径反而解析正确)。
所以它表现为"我没改任何东西,昨天能读今天读不到"。 - 修法:新增
_abs_root(),用os.path.normpath保留盘根语义,并把只写盘符没写分隔符的
输入(d:)补成d:\;_locate_account_root()与_extract_path_from_config()里三处
同族写法全部改用它,候选保证是绝对路径。UNC 根不受影响。 - 暂时不能升级的绕法:给
WeChatDB(db_dir=...)显式传账号目录的父目录(绝对路径),
或不要把微信存储位置设在裸盘根。 - 判据:新增
tools/test_db_path_root.py40 项——假文件系统下以d:\为 root 必须定位到
d:\xwechat_files(含反向验:把实现换回旧rstrip,同一用例必须返回None,证明断言真能
咬住回归);A–F 与 Z 六个盘符逐个验规范行为;本机真实盘上做「该盘当前目录漂移」对照,
证明旧写法在两个启动目录下解析到不同位置;本机真库在 4 个启动目录下结果一致。
- 现象:同一个 Python、同一份依赖、同一个微信配置,只换个工作目录就从"能读"变成
- 其余内容同 v1.2.5(通讯录标签 + 右键转发、发送前回读校验、
WxParam.ENABLE_OCR总开关、
会话名占位符尾巴、置顶/前台锁/剪贴板三处静默失效)。
升级:pip install -i https://pypi.org/simple --upgrade wechatauto-replica
1.2.5:通讯录标签 + 右键转发 + 发送前回读校验
本版要点
- 新增:通讯录标签 + 右键转发。 走法按 4.1.15.13 真机实测的锚点实现(通讯录 → 通讯录管理 → 标签)。
WeChat增 12 个方法:ListLabels(prefer='db'/'ui')/CreateLabel/RenameLabel/AddLabelMembers/RemoveLabelMembers/DeleteLabel/LabelMembers/SendToLabel/ForwardMessage/ProbeLabels;wxid自动换成界面显示名,RenameLabel只改名字不动成员和label_id。- 先预演再真发:
SendToLabel(text, label, dry_run=True)只解析收件人、一次界面都不碰,limit=1先试一个人;WeChatDB().list_labels()是纯读库。ProbeLabels只导航不写,报断在哪一步。 - CLI 加
labels/forward两个子命令和菜单两项(9标签、10转发),中英都收(标签/打标签、转发/转给)。指南新增 §10.5 通讯录标签、§10.6 右键转发。
- 新增:发送前回读校验
WxParam.SEND_CONTENT_RATIO(默认 0.6,<=0关闭)。Ctrl+V有可能整个落空(微信不在前台、焦点没落到输入框上),这时按键照样打下去,发出去的是空消息或上一轮残留的内容——老代码还把那个异常吞了、照样返回成功。现在 UIA 主路径与 OCR/坐标路径两条都在回车前回读输入框比对,判不达标就不回车、清空输入框、返回失败让上层重试,并且不占用rhythm的写动作额度。- 阈值为什么不照抄更高的 0.9:真机实测一条带表情码的正常消息只有 0.884,8 字中文差一个字是 0.875——定 0.9 会把好消息拦死;而读回空是 0.0、残留内容是 0.11,0.6 都拦得住。
- 表情码口径(实测):
[微笑]在输入框里是 1 个U+FFFC,落库正文才是原样[微笑](发送时才转)。比对前两侧折算,折算后 1.0。代价:折算后[微笑]与[发怒]不可分辨,表情码打错不会被这道闸拦下。
- 新增:
WxParam.ENABLE_OCR(默认True,行为不变)。 OCR 识别不准时一键停用。总闸放在引擎入口ScreenOCR.recognize()(ocr()/ocr_zoomed()额外在截图前短路,连抓屏都省掉)——调用点散在guia/moment/uia_driver三个模块共 20 处,逐处加开关迟早漏,所以另加一条静态断言锁住「全库只有guia.py直接引用OcrEngine」。关掉后能走 UIA 的改走 UIA,没有 UIA 等价物的操作明确失败而不是盲点坐标:侧栏查找、搜索下拉点选、「发送」按钮兜底、朋友圈元素、转发/多选菜单。像素探针类(输入框定位、面板非空白、高亮行检测)不是 OCR,照常工作。 - 新增:前台收尾 API。
ensure_visible(keep_topmost=…)参数化;_minimize_blockers()现在给被它最小化的遮挡窗口记账,新增restore_blockers()(只还原自己按下去的那些,且用不抢前台的方式)和release_foreground()(取消置顶 + 还原遮挡)。 - 修复:
current_chat()一直带着输入框的占位符尾巴。 微信把「会话名 + 占位符」拼在控件Name里(本机实测「文件传输助手」→文件传输助手按住鼠标 语音输入文字),于是全库current_chat() == who一次都没成立过:会话明明已经打开,open_chat照样返回 False,发送被推去走慢且更易错的坐标路径,连续发送的快速路径也永不触发。现在剥掉已知尾巴;未收录的尾巴原样返回(行为同旧版)。 - 修复:置顶在 64 位 Python 下从来没生效过。
SetWindowPos(hwnd, -1, …)把-1当 32 位整数塞进HWND参数,调用直接返回 0(last_error 1400),改用ctypes.c_void_p(-1)/c_void_p(-2)。⚠️ 行为变化:置顶现在真的会生效——ensure_visible()默认keep_topmost=True,批量发送期间微信会压在桌面上所有窗口之上,收尾请调release_foreground()。 - 修复:前台锁设成 0 之后不再还原。
SPI_SETFOREGROUNDLOCKTIMEOUT是全机设置,一直留着 0 会削弱所有程序的前台保护(不只微信)。现在先读原值、结束时恢复;顺带发现数值必须放在pvParam才写得进去(放uiParam直接返回 FALSE、值不变)。 - 修复:剪贴板被别的进程占用时异常冒出
send_msg变成 traceback(真机撞到过一次PyperclipWindowsException: Error calling OpenClipboard)。set_clipboard()改为返回bool并兜住异常,调用方按「这次输入失败」重试或明确失败。 - 改进:
_chat_is_open改为 UIA 优先、OCR 兜底——UIA 命中时不再多花一次放大 OCR 和一次截图。 - 兼容性:接口签名一字未改;
set_clipboard()返回值从None变成bool,调用点判据写的是is False而不是not …,所以覆盖它、仍返回None的老代码不会被误判成失败。无新增依赖。 - 回归:
tools/selftest.py496 项 0 失败。新增 4 个离线套件——test_send_verify21、test_ocr_send_gate26、test_ocr_switch21、test_chat_identity21;标签/转发沿用test_labels98、test_forward59。真机:UIA 路径与 UIA-only(ENABLE_OCR=False)各发一条,数据库回读逐字一致;模拟「送给空气」→ 读回''、相似度 0.000、一次都没回车;置顶设置与解除闭环真机验过。 - 未测(如实说明):① 其它微信版本的占位符尾巴(未命中就退回旧行为,不会崩);②
ENABLE_OCR=False时朋友圈 / 转发的实际降级表现,离线只证明了「不会去调 OCR」;③SendToLabel的批量真发(--go),会发给别人,没跑过;④ 闸持续判不符时send_msg要跑满 3 次尝试(实测 56.9s)才失败,没做提前短路;⑤bring_to_front中途抛异常时前台锁不恢复(包try/finally要给整段循环改缩进,风险大于收益)。
安装:pip install -i https://pypi.org/simple --upgrade wechatauto-replica
1.2.4.4
微信 4.x 自动化库 wechatauto-replica 1.2.4.4:回应 issue #31——装完直接敲 wechatauto 就能用,不用再记命令和参数。
pip install -U wechatauto-replica==1.2.4.4
wechatauto
v1.2.4.4(2026-10-03)
- 新增:一条命令的入口
wechatauto/python -m wechatauto(issue #31「能不能把代码调用搞简单一点?太麻烦了真的不会,比如一条命令」)。原有 Python 接口一字未改——命令行只是WeChatDB/MediaDownloader/guia.quick_send/MomentDB的调用方,不想用它的人照旧写 Python。- 只记一条:装完直接敲
wechatauto(不带任何参数)。它先做一次体检(账号 / 数据库密钥 / 图片密钥 / 控件树——哪一项是 0 就直说下一步干什么),再给编号菜单:看消息 / 导出 / 下载图片 / 发消息 / 监听 / 朋友圈 / 列会话 / 体检。挑会话时列出最近 10 个(昵称 + 未读数)按序号选,不需要知道username长什么样。菜单只在终端里进;脚本/管道里python -m wechatauto照旧打帮助退 2,不会把 CI 挂在input()上。 - 子命令中英都认:
消息=messages、导出=export、图片=images、发=send、听=listen、体检=doctor、会话=sessions、朋友圈=moments。只是一层查表翻译,参数定义仍然只有 argparse 那一份。 - 它替新手吸收的三个坑,正是以前「麻烦」的来源:① 读库要
username、驱动界面要显示名——resolve_chat()让昵称/备注/wxid/群号随便传,同名多人时明确要你用 wxid 而不随便挑一个(把 wxid 填进微信搜索框是「搜不到 → 白等几十秒 → 返回一个None」的头号原因);② 下载图片前先确认图片 AES 密钥,缺了会说清怎么办,而不是一排None看着像库坏了;③ Windows 控制台默认 GBK,中文正文直接UnicodeEncodeError——入口第一件事把 stdout/stderr 重设 UTF-8,export永远写 UTF-8 文件。 - 显示层两条:视频/文件/引用这类正文是整段 XML,刮成
[非文本正文] playlength=151 length=4211669一行提示(要看原文用--json);--type中英文都收(库里类型名是中文,只收中文会让人以为「这个会话没有图片」)。 pyproject.toml加了[project.scripts],装完就有wechatauto这个命令,不必敲python -m。发消息默认带--verify(发完自己回读数据库确认)。
- 只记一条:装完直接敲
- 未测(如实说明):菜单的真实按键路径——自动化里只能用脚本喂输入,真键盘那一路没跑过;
send子命令的真机发送(会驱动窗口,需要真人客户端和rhythm闸)。其余都有离线回归:tools/selftest.py全量 490 项 0 失败,其中cli组 41 项(含「cli 顶部不许 import 界面栈」这条——把guia提到顶层它就会报红,反向验过)。
1.2.4.3
微信 4.x 自动化库 wechatauto-replica 1.2.4.3:原图路径按用户反馈重做,窄窗口(竖屏)下的两处点击一并修掉。
pip install -U wechatauto-replica==1.2.4.3
v1.2.4.3(2026-10-01)
- 修复:「我发的图片提示找不到原图」/「点开了还在瞎点,最后报失败」/「下到的还是预览图」——同一批反馈,四个互相独立的原因,没一个是解密问题:
- 「是不是我发的」以前写死
real_sender_id == 2。real_sender_id是message_resource.db里SenderName2Id的 rowid,本机账号落在哪个 rowid 每台机器不一样——那个 2 是 1.1.8 从别人机器上带来的取值。本机实测自己是 1(文件传输助手 400 条全是 1),2 反而是某个常联系的好友(图片消息 512 条)。按常数判的代价很直接:自己发的图被当成别人发的,先去点左边,永远点不中气泡。现在按「解析出来等不等于本机 wxid」判,索引查不到时才兜底。 download_image_original()从此只认原件_h.dat,本机没有就一定驱动界面去下载。 中间那一档.dat不再当成果:它是「微信下发的那一份」,可能是完整图也可能本身就是预览版——实机例:一条.dat44,002 字节、预览图 2,961 字节,那张仍是预览图。本机三档里没有任何一档能证明「这张不是预览版」,所以也不再假装有has_full这种答案(改成has_mid:有没有.dat这一份是事实,它是不是完整图本机答不了)。只想「本机有什么就交什么」的用download_image(tier='full'/'best'),那条路一次界面都不碰。- 微信写盘比我们的判定晚:「报错说取不到,下一轮同一句又说已在本地」和「明明已经拿到了还在一张一张瞎点,最后报下载失败」是同一个因。现在每个决策点(进门 / 点下一张之前 / 等档超时之后 / 判失败之前)都回查一次本机,查到就交文件并停止后续界面动作。
- 预览窗那颗下载键弹的是 Windows 通用保存对话框(标题是「保存」而不是「另存为」),而且第一遍「保存」写出的可能就是当前显示的那幅预览图(用户实测「UI 点下载存下来的仍然是预览图,要重新下载才得到原图」)。现在按本机档位核产物尺寸,判为预览版就先点「图片原始大小」再存一遍,几遍里取最大的那份;挑按钮会再验
ControlTypeName真是按钮(那颗「选择文件格式」下拉框在 UIA 里的 Name 也叫「保存」,实机点到过一次),点完要确认对话框真的关掉(没关就是没点中),任何失败路径都会点「取消」把模态框收掉。
- 「是不是我发的」以前写死
- 新增:目标图片不在可视区时按数据库算出的行差滚过去(
download_image_original(..., scroll=True, max_scrolls=6))。「哪一行才是这条图」用可视区 UIA 行序列和数据库「文本/图片」序列滑窗对齐来认,三道门槛缺一不可:至少两条文本锚点且吻合度 ≥0.6(可视区只剩一行时「吻合度 1.00」是假高分)、并列最优偏移必须给出唯一答案、对齐结果那一行在 UIA 里确实是图片行。每格滚几行是滚完看实际推进修正出来的,不预设 DPI/窗口高度常量。滚动的硬前提是拿得到微信进程 id——滚轮是按光标出队那一刻的位置投递的,落点无法校验时可能打进压在微信上面的别的程序,所以拿不到 pid 就退化成「只认当前屏、绝不滚」并警告。滚不动报scroll-stuck、滚满报scroll-limit、认不出退回「数它下面压了几张更新的图」,再不行逐个试——不会无限滚、不会一直瞎点。 - 修复两处窄窗口(竖屏)下的点击:
- 点一次导航栏「微信」栏退不出会话。 窄窗口是单栏布局,开着会话时
session_list和搜索框整片不在树里,第一下只把主窗带回聊天页、会话还开着,要再点一次才退出。只点一下的旧写法让紧接着找搜索框的open_chat()必然落空——报出来就是「会话打不开」。现在back_to_chat_tab()改成点一下 → 查聊天页那一片出来没有 → 没出来且还有额度就再点一次(max_clicks=2);本来就出来就一点都不点(那一下正好把用户正在看的会话切走)。 - 点不中图片气泡。 气泡位置不随行宽等比缩放(头像列加边距是固定像素):宽窗口行宽 2598 时气泡在行左 +44..+632,「行宽 12%」= +311 命中;竖屏行宽 720 时气泡在 +141..+459,同样算法只有 +86,落在底色上。改成「离发送方那一侧边缘 300px 的绝对锚点 → 旧的 12%」两个候选,宽窗口里两者重合、行为与已发布版本逐字一致。
- 顺带两条同一路径上的形状:预览窗有两种(桌面直接子节点就是
mmui::PreviewWindow,或顶层是Qt51514QWindowIcon标题「图片和视频」而mmui::PreviewWindow在里面一层,只按顶层筛第二种永远找不到);那颗缩放键的Name会随显示状态在「图片原始大小」⇄「图片适应窗口大小」之间变,只写死一个名字找不到就提前返回,于是「保存」兜底根本没机会跑。
- 点一次导航栏「微信」栏退不出会话。 窄窗口是单栏布局,开着会话时
- 修复:
demo_original_image.py里那道已经删掉的判据还留着——它用> 100KB当「原图已经下载」的前置判断,会把压缩版报成原图。 - 本轮仍未做真机复验(如实说明):点气泡 → 预览窗 → 按钮 →
_h.dat这一段需要真人客户端和rhythm闸;竖屏自发图那一侧的气泡区间目前只有镜像推算。它上面的所有逻辑(档位判据、认行与对齐、滚动决策、对话框点选、点击点几何)都有离线回归。 - 回归覆盖:
tools/selftest.py全量 449 项 0 失败(image组 196、tree组 25)。纹身验证:media.py69 刀 +uia_driver.py6 刀,本轮实跑 15 刀全部报红(把_harvest放宽到接受.dat→ 5 红;自发图又提前返回 → 1 红;has_mid恒真 → 1 红;等档等的是.dat→ 1 红;进门那次查盘取消 → 1 红;气泡点击退回纯百分比 → 2 红;去掉行宽上限 → 1 红(极窄行会点到 x=0);不分发送方 → 5 红;候选不去重 → 1 红;导航栏补点那一组 6 刀各 1–5 红)。
1.2.4.2 图片分三档 + 「控件树拿不到」给出可定位的原因
这一版解决什么 / What this fixes
1) 「无法下载原图,只能下载缩略图」
一条图片消息在本机最多有三档:<md5>_t.dat 缩略图、<md5>.dat 微信默认下发的压缩版、
<md5>_h.dat 真原图(只有在微信里点过「查看原图 / 图片原始大小」才会落盘)。反馈里
「只要原图,先不用缩略图」之后一轮一轮不收敛的重试,是四个各自独立的坑叠在一起,
没一个是解密问题:
download_image()压根不看_h.dat;而且拿到压缩版时文件名和原图一模一样
(<user>_<local_id>.jpg),调用方只能靠文件大小猜自己拿到了哪一档。download_image_original()用getsize(_h.dat) > 102400判「原图下好了没」。本机 705 个
_h.dat实测 min=575B、p10=4.3KB、p50=91.5KB、p75=563KB、max=18MB——51.3% 的真原图
被这条绝对门槛当成「还没下载」拒掉了。- 同一个函数的
timeout参数是摆设:点完固定sleep(3)取一次,慢机上文件还在长就判失败。 - 档位报告也在撒谎:图片的本地文件名来自正文里的 32 位 hex,而通用的消息查询不带
packed_info_data,于是每一行都被判成「不是图片」。
out = md.download_image(user, local_id, tier="original") # 本机没有原图 → None,不悄悄降级
out = md.download_image(user, local_id, tier="best") # 原图 > 压缩 > 缩略图,档位标进文件名
out = md.download_image(user, local_id) # 不传 tier = 与旧版逐字一致
md.image_status(user, local_id) # 单条:有哪几档 / 最高一档 / reason
md.list_image_status(user, limit=300) # 整个会话一次报清(消息表只查一次,同 md5 只扫一次盘)
MediaDownloader.original_ready(h_size, mid_size) # 判据公开,调用方可用同一条规则reason 取 ok / mid_only / only_thumbnail / original_partial(有 _h.dat 但是空壳或
比压缩版还小,即下载中断)/ no_local_copy / no_md5 / no_message_row。原图判据改成相对的:
非空、≥1KB、且不比压缩版小 10% 以上。
download_image_original() 现在:本机已有合格 _h.dat 时直接解密落盘,一步界面都不碰;
要动界面时按「这条图片下面压了几张更新的图」把最可能的气泡排在第一个试,每点一张都回来核对
md5(点错不可能冒充成功),等待改成轮询到「可用且大小不再变化」或到 timeout;
预览窗里没有「图片原始大小」按钮时单独警告并指向 tier='best',不再和「下载没完成」混成同一种失败。
实测(只读):211 个会话 4271 条图片消息 → mid_only 2210、only_thumbnail 1428、
ok 626、original_partial 5、no_local_copy 2。大约 7 条里只有 1 条本机真有原图,
所以这类反馈多数是微信的存储策略;先查状态再决定要不要触发下载,能省掉一整轮无效重试。
2) 「控件树被屏蔽」现在会说清原因
mmui 控件树只有在 Weixin.dll 里那个字节被翻开之后才存在,而微信每次重启、升级、重新登录
都会把它清 0。库会热写它,但写不成时以前是静默返回空树:_wechat_hwnds() 把「取不到
Weixin.dll 模块」的窗口全部过滤掉,ensure_materialized() 遇到空列表直接 return False,
ensure_window() 悄悄改走「设读屏标志 + wake」——一句日志都不打。而「窗口全被过滤光」
恰恰是 32 位解释器枚举 64 位进程模块的特征(TH32CS_SNAPMODULE 从 32 位打 64 位只会得到
ERROR_PARTIAL_COPY),用户看不到任何线索,只能描述成「被屏蔽」。
现在按场景给四种可执行的说法,一个进程每种只警告一次(长驻监听每轮都扫,刷屏比不说更糟):
没扫到可见窗口(没启动/没登录/在托盘)、扫到但读不到该进程模块(32 位 → 换 64 位 Python;
64 位 → 指向权限与安全软件白名单)、pywin32 缺失,以及有窗口正常留下时完全不打扰。
过滤逻辑与返回值一字未改,老调用方不受影响。
验证 / Verification
- 自检 283 项 0 失败(纯离线:临时目录 + 假 db + 假
win32gui,不碰真实客户端)。本版新增
image组 39 项(90% 线的两侧、七种reason、五种tier路由与文件名、不许悄悄降级、
气泡排序、轮询等待、db.get_image_rows()字段对齐、同 md5 不重复扫盘)与tree组 16 项
(扫描计数、四种文案、健康场景零输出、去重、两条真实路径确实都过了诊断)。 - 变异验证七刀都会咬:
>102400绝对门槛放回去 → 5 红;去掉比例判据 → 2 红;原图文件名不标
_h→ 2 红;排序不看「比这条新几张」→ 3 红;tier='original'允许悄悄降级 → 1 红;
同一 md5 重复扫盘 → 1 红;去掉ensure_materialized的诊断调用 → 2 红。 - 构建在干净的 worktree(
c030e8f)里完成,逐文件核对包内moment.py与提交版一致,
确认不含工作区里其它未提交改动。
已知边界 / Known limits
- 本轮没有跑真机界面路线:点气泡 → 预览窗 → 「图片原始大小」需要真人客户端,且按 9/21 之后的
约定必须走rhythm拟人节奏,因此那一段是未测;它上面的判据、路由、等待、排序与消息库
查询都由离线回归覆盖。有实机环境的欢迎回报结果。 - 原图只能在微信里点开才会落盘,本库不会替你点(
download_image_original()是唯一会点的那条,
它是显式触发下载的写动作)。 - 可视区里有多张图时按「下面压了几张更新的图」排序后逐个试,不做自动滚动——滚动一条消息会
改变整个虚拟列表的命中面,误点别的图比不点更糟。需要 scroll-to-message 的话可以提 issue。 - 「热写字节成功但控件树仍空」的场景本版没做主动复检(
get_accessibility_features()里那个值
是写进去的副本,不能自证)。真遇到请带Weixin.dll版本 + 日志开 issue。
安装 / Install
pip install -U wechatauto-replica文档见 GUIDE.md §6.2.2(图片三档与 reason 对照表)、§6.3(群聊图片)、§11 Q3、
§12 API 速查表,以及 Q8(控件树被屏蔽)。
1.2.4.1 群消息发送者身份 + 语音取不到的原因
这一版解决什么 / What this fixes
1) 群消息除了文本,都看不出是谁发的
real_sender_id 是数字 rowid,库早就通过 message_resource.db 的 SenderName2Id 把它换成了
真 wxid(放在 sender_username 里),但转换结果从没传到 Message 上,msg.wxid 存的还是那个数字,
唯一能用的路子是从文本正文里刮 wxid_xxx:\n 前缀——图片/语音/文件/表情全是匿名的。
def on_msg(msg, chat):
msg.sender_wxid # 真 wxid(非文本消息也有)
msg.sender # 备注或昵称;解析不到退回 wxid,再退回会话名
msg.wxid # 自己发的 = 真实 wxid(不再是常量 2)
chat.GetGroupMembers() # 群成员表:群里不在你通讯录的人只有这里能给名字
db.nickname_map() # 带缓存的 wxid→名字,批量换算用它实测 8 个真实群 / 1249 条历史消息:带发送者身份 95.8%,非文本消息 0% → 99.6%(451/453)。
剩下 4.2% 是微信自己没留身份(SenderName2Id 只有 655 条、不含那些 id,抽查 20 条的 source
里也没有任何用户名标签)——造不出来的就不造,sender_wxid 保持空串。
顺手删掉一条陈旧兜底:拿数字 rowid 去查 contact.username(永远查不到,还把数字冒充成用户名)。
2) 语音「识别不全」的真相:不是查找键错(issue #20「26 条只识别 19 条」)
先把查找键验了一遍:独立实现与库在 958/958 条语音上逐条一致,「svr_id 存在但挂在别的
chat_name_id 下」为 0 条。真因是微信只在界面上播放/接收过之后才把 voice_data 写进
media_*.db,而 download_voice() 一律返回 None,调用方分不清「本地没有」和「库读坏了」。
判据其实一直躺在消息表里:download_status。975 条语音的交叉表——
ds=1(515) 与 ds=5(400) 全部有音频,ds=0 的 60 条全部确实没有,一一对应,无一例外。
for v in md.list_voice_status("wxid_xxx"):
v['available'], v['reason'], v['bytes'], v['self_sent']
md.voice_status("wxid_xxx", local_id) # 单条reason:ok / audio_not_downloaded(本地确实没有,去微信里播放一次就会落盘)/
audio_missing_from_media_db(状态说该有却没有,这种才值得开 issue)/
session_not_in_media_index / no_server_id / no_voice_row。
本机可用率 93.7%(898 可取 / 54 未落盘 / 6 会话无 media 索引)。download_voice() 签名与行为不变。
3) 一处需要知道的行为变更
群里 msg.sender 以前是正文里刮出来的原始 wxid_xxx,现在是备注/昵称;要原始 id 请读
msg.sender_wxid。
验证 / Verification
- 新 API 与独立复算在 958 条语音上逐条一致;抽一条
audio_not_downloaded实测确实下载不到。 - 自检 226 项 0 失败:新增
sender组 22 项、voice组 28 项(后者基于真实的临时
SQLite 消息库与 media 库)。变异验证:身份解析退回老写法 → 5 项红;假数字兜底放回去 →
源码检查红;语音查询的缺列退化改回通用continue→ 「不丢行」红(只回来 0 条)并崩组。 - 构建在一个干净的 worktree(
c350cd6)里完成,确认包里不含工作区中其它未提交改动。
已知边界 / Known limits
那 6% 补不齐:只能在界面里把语音播放一遍,微信才会落盘(属于驱动真实客户端的写动作,本库没有自动化它)。
另外「自己发出的语音本地是否留音频」在本机数据不足(958 条里只有 2 条自发言),没有下结论;
list_voice_status 已把 self_sent 标出来,欢迎有数据的机器帮忙分自己/对方统计。
安装 / Install
pip install -U wechatauto-replica1.2.4 微信 4.1.15.13 适配 + 朋友圈滚动/点赞/评论修好 + 全局监听补齐
这一版解决什么 / What this fixes
1) 适配微信 4.1.15.13(客户端会自动升级,旧版照常可用)
三件事叠在一起,从外面看都长成「版本不兼容」:
- 搜索入口改成默认收起。原来输入框的位置现在只有一个叫「搜索」的
mmui::XButton
(实测[314,84,370,140]),点了之后mmui::XValidatorTextEdit才出现。
_search_box(expand=True)会先点一次再等输入框;search_box_rect()在收起状态下仍给得出
坐标(返回按钮矩形,作为只读锚点,故意不点——取坐标不该有副作用)。输入框本来就常驻的
老版本一步都不多点。 - gate RVA 挪到
0xb135c38,已进版本表,常用路径不必再跑向量化扫描。 - 进朋友圈需要显式唤醒
mmui树。没物化时报的是「找不到导航按钮」,现在会先查就绪状态、
调ensure_materialized(timeout=6.0),并在唤醒后重新锚定根控件(旧句柄指向唤醒前的空壳)。
2) 朋友圈滚动其实从来没滚过——find_moment 只是看起来能用
_send_scroll() 把 MOVE 和 WHEEL 两个 SendInput 连着发,中间零停顿。滚轮事件是在
出队那一刻按光标位置投递的,于是它落在旧坐标、也就是完全错误的窗口上;而且微信不是前台窗口时
mmui 时间线根本不接收滚轮。现在先用 SetCursorPos + GetCursorPos 轮询确认光标落位(最多约 0.4 秒)、
再停 0.3 秒,滚动前把窗口提到前台。实机:find_moment 连跑两次都命中(51.4s / 14.2s),后来 17.2s;
改之前是碰运气,运气好时看起来像时序抖动。
3) 点赞 / 评论合并成一条实现,老评论窗口补上节流
之前有两条并行路线,而真正跑通的那条(定位 「…」 → 点浮层里的 赞/评论)并不是 LikeMoment /
CommentMoment 走的路。现在 Like / Comment 先走浮层、失败才退回老的反向菜单,
LikeMoment / CommentMoment 改为委托,不再各留一份副本。老评论窗口(MomentCommentDialog.send)
上一版的节流把它漏了,现在补上,且放在所有前置校验之后(校验没过不占用写动作额度)。
教训:一个功能两份实现,被审过的那份往往不是正在跑的那份。
4) find_moment 不再交出一条点不动的 cell(BoundingRectangle 是 (0,0,0,0))
cell 滚出视口后 UIA 会回收句柄,回收后的节点矩形全是 0,在它上面做任何坐标动作都抛
Can not move cursor——表现成「明明定位成功了,却报未能打开朋友圈操作菜单」。现在命中后先验再返回:
同屏用「昵称 + 正文前缀 + 时间」重新认领同一条并原地换 control(调用方手里的对象继续有效),
同屏没有可用分身就先滚回视野再试一次。认领刻意保守:绝不认领同一个人的另一条(那等于给别的动态
点赞),既没正文又没时间的只剩昵称直接拒绝;正文按「前 10 字互含」比对而不是精确签名,因为 UIA 摘要
会截断正文、DB 校正过的正文反而更长,精确签名会连自己那条都认不出。
5) 全局监听 AddListenAll() 真的覆盖所有会话了
全局回调过去是按会话注册的,任何已经单独 AddListenChat 过的会话都被跳过、永远收不到全局回调。
现在按回调去重,轮询中新出现的会话自动纳管,交给回调的伪会话可用了(nickname 能取到、chat 懒建、
SendMsg 能发),监听器重启后回调会重新挂上,RemoveListenAll() 也真的摘得掉。文档见 GUIDE.md §4.3。
另外两个真实缺陷:并发——两个线程同时打开同一份库会踩掉同一个 dst.tmp 中间产物,从监听线程里抛
FileNotFoundError;现在按库加构建锁 + 中间文件名带进程号/线程号 + 清理超过 600 秒的残留。
分页入参——get_messages(limit=0) 会返回一整页、offset=-1 会返回最后一条(Python 切片行为),
本该报错的调用拿到一个看似合理的答案;现在 limit<=0 或 offset<0 一律返回 []。
实机复验 / Live verification
真实客户端、calm 节奏档、逐项拉开间隔:向文件传输助手发送并逐字回读通过两次(新 sort_seq 精确命中);
back_to_chat_tab() + open_chat() 端到端通过(整窗像素变化 7.2% / 8.3%,作为「页面确实切了」的客观判据);
朋友圈定位 → 点赞 → 评论,界面与本地库两侧都确认到位。
离线自检 175 项 0 失败(本项修复之前同一条命令读到 147 项;sessions / messages 两组会随账号数据多几项少几项):
新增 moment 组 30 项(假控件和真 MomentItem 实例各跑
一遍,不碰微信也不点击),外加 click(8)、listen(7)两组。两处变异验证确认新组真的会咬:
_rect_usable 改成永远返回 True → 15 项红;认领时只比昵称 → 5 项红,红的正是「不许认领另一条」那几条。
已知代价 / Known cost
AddListenAll 在 200 会话左右的账号上注册约 11 秒、每轮轮询约 10 秒(每个会话都要打开再读)。本轮未优化,
它同时是 issue #25「IO 偏高」报告的头号嫌疑。导航栏坐标点击仍待查(说法已在 1.2.3 更正过,本轮没碰)。
安装 / Install
pip install -U wechatauto-replica