wechat-screen-off-send-attempts.md 11.7 KB

WeChat 息屏自动发送方案汇总

设备:Pixel 10 Pro Fold (Android 15) 微信:8.0.72 目标:息屏状态下成功发送消息给 yibooo 时间:2026-05-23


一、整体架构(当前部署)

息屏检测                消息生成               消息发送
---------->  三种方案并行尝试  ---------->

1. sqlcipher DB 轮询 ←── 检测新消息 ──→ hermes API ←── 生成 AI 回复
2. termux-notification-list(息屏兜底唤醒)
3. LSPosed hook(WCDB insert 监控)

当前部署路径:/data/data/com.termux/files/home/hermes-wechat/


二、已尝试的发送方案(5 种)

方案 1:LSPosed Xposed 模块 + am broadcast

原理:

  • 安装 LSPosed 模块到 WeChat 进程
  • 模块注册 BroadcastReceiver,监听 com.hermes.wechat.SEND_MSG
  • 收到广播后,反射调用微信内部 API:u01.r1.a(talker) → q1.e(content) → q1.a() → m1.a()
  • 将 NetScene 提交到主线程执行

代码位置:

  • Java 源码:wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/HermesWeChatModule.java
  • Smali(已编译):/tmp/lsposed-mod/smali/com/hermes/wechat/xposed/HermesWeChatModule.smali
  • 签名 APK:/tmp/lsposed-mod/dist/lsposed-wechat-sendmsg.apk

已做的 bypass:

  • ExtContentProviderBase.h() → return 1(auth bypass)
  • ExtContentProviderBase.b() → return true(login bypass)
  • AESUtil.b() / AESUtil.a() 跳过加密
  • NetScene submit 改为 Handler(Looper.getMainLooper()).post() 主线程执行

测试结果:

  • 亮屏:am broadcast 可以执行,但消息未成功写入 DB(怀疑模块未正确 hook 或 m1.a() 不是真正的发送方法)
  • 息屏:am broadcast 可能被系统拦截,未验证
  • 状态:❌ 未成功

失败原因分析:

  1. 模块重新签名安装后,LSPosed 数据库中路径未更新,可能未正确加载
  2. u01.m1.a() 返回 true 但无 DB 写入,说明 a() 可能不是真正的提交方法
  3. Android 15 后台广播限制可能阻止息屏时的 am broadcast

方案 2:Frida 直接调用微信内部 API

原理:

  • Frida attach 到 WeChat 主进程,直接执行 JavaScript 调用微信内部类
  • 调用链:u01.r1.a("yibooo") → q1.e("content") → q1.a() → m1.a()
  • 使用 Java.scheduleOnMainThread() 确保在主线程执行

Frida 脚本:

  • /tmp/send_msg3.js — 基本发送脚本(主线程提交)
  • /tmp/send_msg4.js — 测试 q1.b() 作为替代发送方法(尚未运行)
  • /tmp/inspect_all.js — 类方法枚举工具

已发现的关键类签名:

u01.r1.a(java.lang.String) → u01.q1          # 创建 builder
u01.q1.e(java.lang.String) → u01.q1          # 设置消息内容(链式)
u01.q1.g(java.lang.String) → u01.q1          # 可能是设置 talker
u01.q1.h(int) → u01.q1                       # 可能是设置 type
u01.q1.a() → u01.m1                          # build NetScene
u01.q1.b() → void                            # ??? 可能是真正发送
u01.q1.d(u01.q1, qw5.l, int, Object) → void  # 静态方法,可能是 dispatch
u01.m1.a() → boolean                         # 返回 true 但无实际效果

测试结果:

  • m1.a() 在主线程执行,返回 true,但 DB 中没有新的 isSend=1 记录
  • msgId 846 之后无新消息写入
  • 状态:❌ 亮屏也未成功

关键疑点: u01.m1.a() 可能不是 doScene()/dispatch(),而是:

  • 预检查/校验方法
  • 状态查询方法
  • 需要配合其他参数才能触发真正的发送

下一步待验证:

  1. u01.q1.b() — 返回 void,更像执行方法
  2. u01.q1.d() — 静态方法,参数 (q1, l, int, Object) 可能是 dispatcher
  3. hook u01.m1.a() 内部实现,追踪它到底调用了什么
  4. hook WCDB insert(),确认调用 API 后是否有任何 DB 写入尝试

方案 3:input text + input keyevent 66 模拟键盘输入

原理:

  • 使用 Android input text "消息内容" 向当前焦点窗口输入文本
  • input keyevent 66 模拟回车发送

测试结果:

  • 亮屏:简单英文数字可用,某些字符导致 NullPointerException
  • 息屏:无法输入到没有焦点的前台窗口
  • 状态:❌ 不适用

失败原因:

  1. 微信聊天窗口需要是前台焦点才能接收 input text
  2. 息屏状态下系统不会分发输入事件到应用
  3. 特殊字符(中文、标点)容易崩溃

方案 4:直接启动 ChattingUI (am start -n)

原理:

  • am start -n com.tencent.mm/.ui.chatting.ChattingUI 直接打开聊天窗口
  • 然后配合 input text 发送

测试结果:

  • am start 抛出 SecurityException: Not exported
  • ChattingUI 未在 manifest 中声明 exported=true
  • 状态:❌ 无法启动

方案 5:content insert → ContentProvider

原理:

  • 通过 content insert --uri content://com.hermes.wechat.xposed.provider/send 发送
  • 绕过了广播,直接走 ContentProvider

代码位置:android-wechat-sender.js::sendTextMessage()

测试结果:

  • 未实际测试(依赖 LSPosed 模块的 ContentProvider)
  • 模块的 Provider 可能需要同样 bypass auth
  • 状态:❓ 未测试

三、消息检测方案(2 种,均可用)

检测 1:sqlcipher DB 轮询 ✅

原理:

  • 直接用 sqlcipher 打开 EnMicroMsg.db(key: e03ce81)
  • 轮询 message 表,查 msgId > lastMsgId AND isSend = 0

代码位置:android-wechat-tracker-sqlcipher.js

已修复的问题:

  • temp 文件路径从 /tmp/ 改为 /data/data/com.termux/files/usr/tmp/(Termux 无 /tmp 权限)

状态:✅ 工作正常,可检测新消息

检测 2:termux-notification-list 息屏兜底 ✅

原理:

  • 每 3 秒执行 termux-notification-list
  • 过滤 packageName === 'com.tencent.mm' 的通知
  • 检测到通知后 → input keyevent 26 点亮屏幕 → am force-stop + am start 重启微信 → 等待前台确认

代码位置:auto-reply.js::pollNotifications()

已修复的问题:

  • pkgName → packageName 字段名修正
  • 添加 input keyevent 26 唤醒屏幕(息屏时 am start 不执行)
  • 添加 ensureWechatForeground() 轮询确认前台 + 模拟按键保持活跃

状态:✅ 检测+唤醒工作正常,但发送卡住了


四、当前代码文件状态

文件 位置 状态 说明
auto-reply.js server/services/wechat/ ✅ 已部署 主逻辑,通知兜底+sqlcipher轮询+hermes调用,但发送调用的是 sendTextMessageBroadcast()
android-wechat-tracker-sqlcipher.js server/services/wechat/ ✅ 已部署 sqlcipher DB 轮询,检测新消息正常
android-wechat-sender.js server/services/wechat/ ✅ 已部署 提供 sendTextMessageBroadcast() 和 sendTextMessage(),但底层依赖 LSPosed 模块
HermesWeChatModule.java wechat_plain/scripts/lsposed-wechat-sendmsg/... ✅ 已修改 主线程提交 NetScene,但 APK 重新签名后 LSPosed 可能未正确加载
lsposed-wechat-sendmsg.apk /tmp/lsposed-mod/dist/ ⚠️ 已编译 新签名,LSPosed DB 中路径可能未更新
Frida 脚本 /tmp/send_msg*.js ✅ 可用 send_msg3.js 亮屏测试未成功,send_msg4.js 待测试 q1.b()

五、核心卡点总结

卡点 1:为什么 u01.m1.a() 返回 true 但消息没发出去?

现象:

  • Frida 调用 netScene.a() 返回 true
  • DB 中没有新的 isSend=1 记录
  • 没有异常抛出

假设:

  1. a() 不是 doScene(),而是其他方法(如 check() / preSend())
  2. 真正的发送方法是 u01.q1.b() 或 u01.q1.d()
  3. NetScene 需要额外的初始化(如 MsgInfo 的某些字段未设置)
  4. 微信在息屏/后台时 NetSceneDispatcher 被冻结,即使调用也不执行

待验证:

  • [ ] 测试 u01.q1.b() 是否能触发 DB 写入
  • [ ] 测试 u01.q1.d() 静态方法
  • [ ] hook u01.m1.a() 内部,看其调用链
  • [ ] hook WCDB insert,确认 API 调用后是否有任何 DB 操作

卡点 2:息屏状态下后台执行限制

现象:

  • Android 15 对后台 Activity 启动、后台广播有严格限制
  • am start 在息屏锁定时不会立即执行
  • 需要先 input keyevent 26 点亮屏幕

当前 workaround:

  • 检测到通知 → 唤醒屏幕 → force-stop + start 微信 → 轮询确认前台
  • 这个流程已验证可以工作

剩余风险:

  • 即使微信在前台,如果 NetSceneDispatcher 在息屏时被系统挂起,消息仍可能无法发送
  • 需要确认亮屏后微信网络层是否完全恢复

卡点 3:LSPosed 模块加载问题

现象:

  • 重新编译签名 APK 后安装
  • LSPosed 管理器中显示已启用,但日志中没有模块 hook 的输出
  • 可能原因:新签名导致 LSPosed 数据库中路径/签名不匹配

已尝试:

  • 直接修改 /data/adb/lspd/config/modules_config.db 更新路径
  • 重启 Zygote (setprop ctl.restart zygote) 和 lspd

状态:⚠️ 不确定模块是否真正加载


六、下一步建议(优先级排序)

P0:确认真正发送方法(Frida 验证)

用 Frida 在亮屏状态下测试以下方法,找到真正能触发 DB 写入的调用:

// 测试 1:q1.b()
var builder = r1.a("yibooo");
builder.e("test msg");
builder.b();  // 返回 void,可能是真正发送

// 测试 2:q1.d() 静态方法
q1.d(builder, null, 0, null);  // 参数猜测

// 测试 3:hook m1.a() 内部
// 看 m1.a() 到底调用了什么,追踪其调用链

P1:验证亮屏发送

只有在亮屏状态下能稳定发送,才有意义测试息屏。 当前亮屏都无法发送,说明调用链不正确。

P2:息屏发送验证

亮屏发送成功后:

  1. 息屏
  2. 等待 5 秒
  3. Frida 执行相同脚本
  4. 检查 DB 是否有写入

P3:修复 LSPosed 模块(备用方案)

如果 Frida 方案因息屏限制不可行,回到 LSPosed 模块:

  1. 确认模块正确加载(查看 LSPosed 日志)
  2. 修正 m1.a() 为正确的方法调用
  3. 验证息屏时广播是否能被接收

P4:集成到 auto-reply.js

发送方法确定后:

  1. 修改 auto-reply.js 中的发送调用
  2. 确保息屏唤醒流程与发送流程衔接
  3. 测试完整闭环:收到消息 → AI 回复 → 成功发送

七、附录:Frida 调试命令速查

# 连接设备
adb connect 192.168.x.x:5555

# 启动微信并 Frida 注入
frida -U -f com.tencent.mm -l /tmp/send_msg3.js --no-pause

# attach 到已运行的微信
frida -U -n "com.tencent.mm" -l /tmp/send_msg3.js

# 查看微信进程 PID
adb shell ps | grep com.tencent.mm

# 息屏/亮屏
adb shell input keyevent 26

# 查询 DB 最新发送消息
adb shell su -c "sqlcipher /data/data/.../EnMicroMsg.db 'PRAGMA key=e03ce81; SELECT msgId, talker, content FROM message WHERE isSend=1 ORDER BY msgId DESC LIMIT 3;'"

八、关键发现记录

发现 时间 影响
WeChat 8.0.72 使用 u01.r1/q1/m1 作为发送 API 2026-05-23 确认了调用链
m1.a() 返回 true 但无 DB 写入 2026-05-23 怀疑 a() 不是真正发送方法
termux-notification-list 字段是 packageName 非 pkgName 2026-05-23 修复了息屏检测
Termux 无法写 /tmp,需用 /data/data/com.termux/files/usr/tmp/ 2026-05-23 修复了 sqlcipher 查询
input keyevent 26 可在息屏时点亮屏幕 2026-05-23 实现了息屏唤醒
ChattingUI 未 exported,无法 am start 2026-05-23 排除了直接启动聊天窗口方案
u01.q1.b() 和 d() 可能是真正发送方法 2026-05-23 待验证