# WeChat 交互服务 — 发送与跟踪 本目录提供从 **Mac** 和 **Pixel 手机** 两个端点与微信进行交互的能力:发送消息、实时跟踪收发消息。 底层依赖自研的 Android 模块 `HermesWeChatSendMsg`(位于 `wechat_plain/scripts/lsposed-wechat-sendmsg`): - **发送**:模块内的 `HermesAccessibilityService` 监听广播 `com.hermes.wechat.SEND_MSG`,触发后**纯 UI 自动化**——启动微信 → 搜索 displayName → 进入聊天 → 设置 EditText → 点发送按钮。这是当前唯一能把消息真正写入 message 表的路径。 - **跟踪**:通过 sqlcipher 轮询解密后的 `EnMicroMsg.db`(默认 key `e03ce81`),或备用方式 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. 模块安装 ```bash # 在 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(必须) ```bash # 启用 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 端使用 ### 发送消息 ```js 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); ``` ### 跟踪消息 ```js 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(); // 流式模式(更实时) ``` --- ## 手机端使用 ### 一键部署 ```bash cd server/services/wechat ./deploy-to-phone.sh /data/data/com.termux/files/home/hermes-wechat ``` ### 发送消息 ```js const { sendTextMessage, sendTextMessageBroadcast } = require('./android-wechat-sender'); // 方式1:ContentProvider(与 Mac API 一致) await sendTextMessage('filehelper', 'Hello from Pixel'); // 方式2:直接广播(更可靠) await sendTextMessageBroadcast('wxid_xxx', 'Test'); ``` ### 跟踪消息 ```js const { AndroidWechatTracker } = require('./android-wechat-tracker'); const tracker = new AndroidWechatTracker(); tracker.on('message', msg => console.log(msg)); tracker.startPolling(3000); ``` ### 运行测试 ```bash cd /data/data/com.termux/files/home/hermes-wechat node test-phone.js filehelper "hello from pixel" ``` --- ## 统一入口 ```js 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](../../wechat_plain/scripts/lsposed-wechat-sendmsg/README.md) — 模块编译、安装、Hook 点详情 - [SKILL.md](./SKILL.md) — hermes agent 快速上手指南