README.md
WeChat 交互服务 — 发送与跟踪
本目录提供从 Mac 和 Pixel 手机 两个端点与微信进行交互的能力:发送消息、实时跟踪收发消息。
底层依赖自研的 Android 模块 HermesWeChatSendMsg(位于 wechat_plain/scripts/lsposed-wechat-sendmsg):
- 发送:模块内的
HermesAccessibilityService监听广播com.hermes.wechat.SEND_MSG,触发后纯 UI 自动化——启动微信 → 搜索 displayName → 进入聊天 → 设置 EditText → 点发送按钮。这是当前唯一能把消息真正写入 message 表的路径。 - 跟踪:通过 sqlcipher 轮询解密后的
EnMicroMsg.db(默认 keye03ce81),或备用方式 tail LSPosed 日志。
⚠️ 模块同名的 LSPosed 入口类
HermesWeChatModule(反射 NetSceneSendMsg)以及 Frida daemon 队列(android-wechat-sender-frida.js)已确认无法把消息真正发出去——前者Send result: true是误报、后者注入会触发微信反调试崩溃。所有 sender 调用最终都汇聚到am broadcast→ A11y service 这一条路径。
整体架构
┌─────────────────────────────────────────────────────────────────────┐
│ 用户 / hermes agent │
│ │
│ Mac 端 │ Pixel 手机端 (Termux/Debian) │
│ ┌──────────────────────┐ │ ┌──────────────────────────┐ │
│ │ wechat-sender.js │ │ │ android-wechat-sender.js │ │
│ │ wechat-tracker.js │ adb │ │ android-wechat-tracker.js│ │
│ └──────────┬───────────┘──────┘ └────────────┬─────────────┘ │
│ │ │ │
│ │ adb shell su -c │ su -c │
│ │ "content insert ..." │ "content ..." │
│ │ "cat /data/adb/lspd/..." │ "tail -f ..." │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Pixel 10 Pro Fold (Rooted) │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ HermesWeChatSendMsg (LSPosed/Vector Module) │ │ │
│ │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │
│ │ │ │ ContentProvider│─────▶│ Broadcast Intent │ │ │ │
│ │ │ │ (Provider) │ │ com.hermes.wechat│ │ │ │
│ │ │ └──────────────┘ │ .SEND_MSG │ │ │ │
│ │ │ │ └────────┬───────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ 微信主进程 (com.tencent.mm) │ │ │ │
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Hook 点: │ │ │ │ │
│ │ │ │ │ • ExtContentProviderBase.h() → 1 │ │ │ │ │
│ │ │ │ │ • ExtContentProviderBase.b() → true│ │ │ │ │
│ │ │ │ │ • AESUtil.b/a → 明文 wxid │ │ │ │ │
│ │ │ │ │ • MsgInfo.c1() → 消息日志 │ │ │ │ │
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ 广播接收器 → NetSceneSendMsg │ │ │ │ │
│ │ │ │ │ u01.r1.a() → Builder → m1.a() │ │ │ │ │
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
模块原理
主路径:A11y UI 自动化(实际工作的)
外部请求
│
├── content insert ──▶ ContentProvider ──▶ Broadcast (com.hermes.wechat.SEND_MSG)
│ │
└── am broadcast ────────────────────────────┘
│
▼
HermesAccessibilityService (BIND_ACCESSIBILITY_SERVICE)
│
▼
1. startActivity(LauncherUI, FLAG_ACTIVITY_CLEAR_TASK)
│
▼
2. 等 3.5s 后判断是否已在目标聊天页
(id=obn 的标题 == toUserDisplayName?)
│
┌─────────────否─────────┴────────────是─────────┐
▼ ▼
3a. 点击搜索按钮 (id=jha) 直接到步骤 5
3b. 在搜索框 (id=d98) ACTION_SET_TEXT
= toUserDisplayName (微信 nickname,必须是
"火星人" 而不是 "yibooo")
3c. 在搜索结果中找匹配文本的 clickable ancestor 并点击
│
▼
4. 等待聊天界面加载 (id=bkk 容器存在)
│
▼
5. 在 bkk 内的 EditText ACTION_SET_TEXT = content
│
▼
6. 点击发送按钮 (id=bql,或 fallback 找文本 "发送")
│
▼
微信走自己正常的发送流程 → 服务器 → message 表 isSend=1
关键 UI 节点 ID(WeChat 8.0.72):
| 用途 | resource-id |
|---|---|
| 主界面搜索按钮 | com.tencent.mm:id/jha |
| 搜索输入框 | com.tencent.mm:id/d98 |
| 聊天输入框容器(FrameLayout,内含 EditText) | com.tencent.mm:id/bkk |
| 发送按钮 | com.tencent.mm:id/bql |
| 聊天页标题(验证进入了正确的会话) | com.tencent.mm:id/obn |
代码位置:wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java
已废弃路径 1:LSPosed 反射 NetSceneSendMsg
模块同名入口类 HermesWeChatModule 通过 hook 反射调用:
| 目标类 | 方法 | 作用 |
|---|---|---|
ExtContentProviderBase |
h() → return 1 |
绕过 appId + UID + 签名校验 |
ExtContentProviderBase |
b() → return true |
绕过登录状态检查 |
AESUtil |
b(long) → 返回原值 |
跳过联系人 ID 加密 |
AESUtil |
a(String) → 返回原值 |
跳过联系人 ID 解密 |
MsgInfo (f9) |
c1(String) |
监控消息内容变更,记录日志 |
调用链 u01.r1.a(toUser) → builder.e(content) → builder.a() → m1.a()。sendTextMessageDirect 同步返回 true 然后在主线程异步提交 NetScene——但目前主线程提交触发 NullPointerException 被 swallow,消息不进 message 表。日志特征:
[HermesWX] Send result: true
[HermesWX] NetScene submit on main thread failed: null
注:这些 hook(auth bypass / AESUtil)对 A11y 路径没有影响,是否保留无关紧要。
已废弃路径 2:Frida daemon 队列
android-wechat-sender-frida.js 写入 ~/.frida_queue/queue.jsonl,由常驻微信进程的 frida daemon 读取并反射调用 u01.r1.a。Frida 17 + 微信 8.0.72 注入会触发反调试,进程崩溃。已不在生产使用。
跨进程通信
HermesAccessibilityService 运行在模块 APK 自身进程,由系统的 AccessibilityManager 绑定。外部进程通过 Android 广播(com.hermes.wechat.SEND_MSG)触发:
hermes / adb shell content / am broadcast
│
▼
Android 系统 broadcast manager
│
▼
HermesAccessibilityService (模块进程)
│
│ AccessibilityNodeInfo / performAction
▼
微信主进程的 UI (作为前台被自动化点击)
│
▼
微信走自己正常的发送流程 → 服务器
A11y service 必须由用户在「设置 → 辅助功能」中手动启用,或者用 root 通过 settings put secure enabled_accessibility_services 启用。重启 / 系统更新 / 应用更新都可能导致它被关闭。
文件清单
| 文件 | 用途 | 运行端 |
|---|---|---|
wechat-sender.js |
发送消息(adb) | Mac |
wechat-tracker.js |
跟踪消息(adb) | Mac |
android-wechat-sender.js |
主推 发送消息(am broadcast → A11y service UI 自动化) |
Pixel |
android-wechat-tracker-sqlcipher.js |
主推 跟踪消息(sqlcipher 轮询 DB) | Pixel |
android-wechat-tracker.js |
备用跟踪消息(tail -f LSPosed 日志) | Pixel |
android-wechat-sender-frida.js |
⚠️ 已废弃:Frida daemon 队列。Frida 17 + 微信 8.0.72 注入会崩溃,未在生产使用 | Pixel |
auto-reply.js |
Pixel 自动回复 plugin(hermes-agent API) | Pixel |
index.js |
统一入口,自动检测环境 | 通用 |
test-phone.js |
手机端一键测试 | Pixel |
deploy-to-phone.sh |
推送到手机 | Mac |
SKILL.md |
hermes agent skill 文档 | 通用 |
wechat-reader.js |
读取解密后的 SQLite DB | Mac |
wechat-importer.js |
导入联系人到知识图谱 | Mac |
前置条件
1. 手机环境
- Pixel 10 Pro Fold(或其他已 root Android 设备)
- Magisk 30.7+,Zygisk 已启用
- LSPosed/Vector 2.0+ 框架已安装
- 微信 8.0.72
- Termux 或 Debian chroot(hermes agent 运行环境)
2. 模块安装
# 在 Mac 上编译(详见 lsposed-wechat-sendmsg/README.md)
cd lsposed-wechat-sendmsg
./gradlew assembleRelease
# 签名并安装
adb install app/build/outputs/apk/release/app-release.apk
# 在手机上:
# 1. 打开 Vector Manager → 模块 → 启用 HermesWeChatSendMsg
# 2. 作用域中选择「微信(com.tencent.mm)」
# 3. 强行停止微信,重新打开
3. 启用 AccessibilityService(必须)
# 启用 A11y service
adb shell "su -c 'settings put secure enabled_accessibility_services com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService; settings put secure accessibility_enabled 1'"
# 验证已 bind
adb shell "dumpsys accessibility | grep -A3 Hermes"
# 应看到: Enabled services:{{com.hermes.wechat.xposed/.../HermesAccessibilityService}}
# 验证模块(可选,对 A11y 路径不是必需)
adb shell "su -c \"logcat -s HermesA11y:* -d -t 30\""
A11y service 一旦关闭(重启 / 系统更新 / 用户在设置中关闭),发送就会失效。enabled_accessibility_services = null 是最常见的故障原因。
Mac 端使用
发送消息
const { sendTextMessage, sendBatch } = require('./wechat-sender');
// 单条
const r = await sendTextMessage('filehelper', 'Hello from Mac');
console.log(r); // { ok: true, uri: 'content://.../send/0' }
// 批量
await sendBatch([
{ toUser: 'filehelper', content: 'msg1' },
{ toUser: 'wxid_xxx', content: 'msg2' },
], 2000);
跟踪消息
const { WechatTracker } = require('./wechat-tracker');
const tracker = new WechatTracker();
tracker.on('message', msg => {
console.log(`[${msg.type}] ${msg.talker}: ${msg.content}`);
});
tracker.startPolling(3000); // 轮询模式
// tracker.startStream(); // 流式模式(更实时)
手机端使用
一键部署
cd server/services/wechat
./deploy-to-phone.sh /data/data/com.termux/files/home/hermes-wechat
发送消息
const { sendTextMessage, sendTextMessageBroadcast } = require('./android-wechat-sender');
// 方式1:ContentProvider(与 Mac API 一致)
await sendTextMessage('filehelper', 'Hello from Pixel');
// 方式2:直接广播(更可靠)
await sendTextMessageBroadcast('wxid_xxx', 'Test');
跟踪消息
const { AndroidWechatTracker } = require('./android-wechat-tracker');
const tracker = new AndroidWechatTracker();
tracker.on('message', msg => console.log(msg));
tracker.startPolling(3000);
运行测试
cd /data/data/com.termux/files/home/hermes-wechat
node test-phone.js filehelper "hello from pixel"
统一入口
const wechat = require('./index');
// 自动检测环境:Mac 用 adb,Android 用 su
console.log(wechat.platform); // 'mac' | 'android'
// 发送
await wechat.sendTextMessage('filehelper', 'test');
// 跟踪
const tracker = new wechat.WechatTracker();
tracker.startPolling(3000);
API 参考
发送
| 函数 | 参数 | 返回值 | 说明 |
|---|---|---|---|
sendTextMessage(toUser, content) |
string, string | {ok, uri, error} |
通过 ContentProvider |
sendTextMessageBroadcast(toUser, content) |
string, string | {ok, error} |
直接广播 |
queryStatus() |
— | {status, error} |
查询发送结果 |
sendBatch(messages, delayMs) |
Array, number | Array | 批量发送 |
跟踪
| 方法 | 说明 |
|---|---|
fetchRecent(n) |
拉取最近 N 条 |
startPolling(ms) |
轮询模式 |
startStream() |
流式模式(tail -f) |
stopPolling() / stopStream() |
停止 |
事件
| 事件名 | 数据 |
|---|---|
message |
{type, talker, msgType, msgId, content, raw, timestamp} |
sendResult |
{success, raw} |
sendRequest |
{toUser, raw} |
error |
Error |
close |
— |
故障排查
模块层面
| 现象 | 原因 | 解决 |
|---|---|---|
消息没进 message 表 |
A11y service 未启用 | adb shell "settings get secure enabled_accessibility_services",为 null 就用上面的命令重新启用 |
A11y 日志显示 Root package=com.termux 然后退出 |
广播触发时微信不在前台,启动 intent 被前台 termux 抢占 | broadcast 前先 adb shell am start -n com.tencent.mm/.ui.LauncherUI |
Search button not found |
displayName 字段空 / 错误,或当前页面不是 LauncherUI | broadcast 必须传 --es toUserDisplayName,且值必须是 WeChat nickname(如 火星人),不能是 wxid |
No clickable search result found |
nickname 写错 / 联系人不在好友列表 | 在微信里直接搜索这个 nickname 验证是否能找到 |
Send result: true 但消息没发出去 |
这是 LSPosed 反射路径的误报,正常 | 看 A11y 日志而不是 HermesWX 日志 |
| 模块未出现在 LSPosed Manager | APK 未签名 | 重新签名安装 |
Mac 端
| 现象 | 原因 | 解决 |
|---|---|---|
adb: no devices |
手机未连接 | 检查 USB 调试和 adb 授权 |
adb exit 255 |
未 root | 确认 Magisk 已授权 adb shell 的 su |
手机端
| 现象 | 原因 | 解决 |
|---|---|---|
exit 1: permission denied |
未 root | 检查 Magisk root 状态 |
content: not found |
不在 Android 环境 | 确保在 Termux 或指定 /system/bin/content |
| 发送成功但对方没收到 | 微信没在前台 | 确保微信主进程存活 |
| 跟踪不到消息 | 日志级别不够 | Vector Manager → 日志 → 设置详细级别 |
Background execution not allowed |
Android 10+ 限制 | 使用 content insert 替代 am broadcast |
安全提示
- 本模块仅用于个人设备上的自动化测试和学习研究
- 微信账号存在被封风险,建议在测试用小号上操作
- 不要在群聊中高频发送消息,避免触发风控
相关文档
- LSPosed 模块 README — 模块编译、安装、Hook 点详情
- SKILL.md — hermes agent 快速上手指南