diff --git a/CLAUDE.md b/CLAUDE.md index beffa99..a4163a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,22 +6,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **lemonPCRM** — lemonAI 出品的 Local-First AI 个人人脉管理系统(Personal CRM)。以人为中心、以对话为链接,通过知识图谱管理微信、LinkedIn、X 等多平台社交关系,将每个联系人与其社媒动态、会议纪要、文件、照片全面关联。 -AI 对话与编排层基于 ACP 协议驱动本地 Claude CLI 进程,提供单 Agent 执行和 Master → Sub-Agent 多轮编排能力。Node.js + Express,无数据库依赖,数据完全本地化。 +AI 对话与编排层基于 ACP 协议驱动本地 Claude CLI 进程,提供单 Agent 执行和 Master → Sub-Agent 多轮编排能力。Node.js + Express,无数据库依赖(使用 Node 22.5+ 内置 `node:sqlite`),数据完全本地化。 ## 常用命令 ```bash -npm run setup # 首次初始化:cp .env.example .env + npm install +npm run setup # 首次初始化:cp .env.example .env + pnpm install npm start # 生产模式(node server/index.js) npm run dev # 开发模式(node --watch server/index.js,热重载) +./dev.sh # 杀掉端口占用进程 + npm run dev(推荐开发用) ``` -无测试框架,无 lint 脚本。 +无测试框架,无 lint 脚本。使用 pnpm workspaces(`packageManager: pnpm@10.28.1`)。 ## 架构 ### 技术栈 - **后端**:Node.js (≥18) + Express,pnpm workspace monorepo +- **数据库**:Node 22.5+ 内置 `node:sqlite`(`DatabaseSync`),位于 `server/db/lemon_crm.db` - **AI**:Claude CLI(本地子进程,`--dangerously-skip-permissions` 自动批准) - **文件解析**:mammoth(.docx)、pdf-parse(.pdf) @@ -31,13 +33,15 @@ npm run dev # 开发模式(node --watch server/index.js,热重载ï¼ |------|------| | `packages/acp-core/` | ACP 协议层:Protocol / Transport / Launcher 三层抽象 + ClaudeCliProtocol / AcpProtocol | | `packages/agent-core/` | Agent 运行时(ESM):目录驱动的 Agent、AgentBus、AgentRegistry、AgentScaffold | -| `server/index.js` | Express 入口(路由:`/api/master` + `/api/config` + `/api/health`) | +| `server/index.js` | Express 入口(路由 + 静态前端) | | `server/src/core/` | 编排引擎:orchestrator、master-session、agent-manager、session-pool、stream-executor、prompt-builder、parser、runtime-resolver | | `server/src/engine/` | 进程层:run-registry、command-queue、process-guard、idle-timer、failover-error | -| `server/src/tools/` | Master 工具:call-agent-tool、tool-registry | +| `server/src/tools/` | Master 工具:call-agent-tool、tool-registry、graph/(图谱 MCP 工具集) | | `server/src/standalone-entry.js` | 编程 API:`runMasterSession` / `runOrchestration` / `runSingleAgent` | -| `server/routes/master-chat.js` | SSE 流式对话路由(主入口) | -| `server/services/` | conversation-logger、file-parser、memory-service、claude-chat-service | +| `server/routes/` | API 路由:master-chat、config、graph、wechat、import、analysis、photo | +| `server/services/` | conversation-logger、file-parser、memory-service、claude-chat-service、wechat/、linkedin/、meeting/、photo/、x/ | +| `server/db/GraphStore.js` | 本地知识图谱存储层(SQLite):Person / Organization / Project / Event / Asset 节点 + KNOWS / BELONGS_TO / PARTICIPATED 等边 | +| `frontend/` | 静态前端(单 HTML 文件,由 server/index.js 托管) | | `server/config/backends.json` | ACP Launcher / Client 定义 | | `server/config/agents.json` | 静态 Agent 注册表(可选,也可由 `system/agents/` 目录发现) | @@ -51,6 +55,19 @@ npm run dev # 开发模式(node --watch server/index.js,热重载ï¼ | `DELETE /api/master/sessions/:id` | 关闭 session | | `GET/PUT /api/config/items/:key` | 读写配置文件 | | `GET /api/health` | 健康检查 | +| `GET /api/graph/nodes` | 列出图谱节点(?type=Person&limit=100) | +| `GET /api/graph/nodes/:id` | 查询单个节点(含边) | +| `POST /api/graph/nodes` | 创建/更新节点 | +| `GET /api/graph/search?q=关键词` | 全文搜索 | +| `POST /api/graph/edges` | 创建关系边 | +| `GET /api/graph/path?from=id&to=id` | 查询关系路径 | +| `POST /api/wechat/import-contacts` | 导入微信联系人到图谱 | +| `POST /api/wechat/import-chat` | 导入微信聊天记录 | +| `GET /api/wechat/contacts?q=搜索` | 查询微信联系人 | +| `GET /api/wechat/sessions` | 最近会话列表 | +| `POST /api/import/...` | 多平台数据导入 | +| `POST /api/analysis/...` | 图谱分析(关系链路、聚类等) | +| `POST /api/photo/...` | 照片服务(人脸聚类、向量检索) | ### 配置文件 @@ -102,11 +119,44 @@ Launcher (ProcessLauncher spawn / NoneLauncher 无操作) - 每个 Agent 的记忆存储在其目录下的 `memory/` 子目录,纯文件系统 - `AgentScaffold` 用于创建新 Agent 目录结构 +### 知识图谱(GraphStore) + +`server/db/GraphStore.js` 使用 `node:sqlite`(Node 22.5+ `DatabaseSync`),无需额外依赖。 + +- **节点类型**:Person、Organization、Project、Event、Asset +- **边类型**:KNOWS、BELONGS_TO、PARTICIPATED、CONTAINS、MENTIONED_IN、ASSOCIATED +- **分析状态**:`analysis_status` 字段跟踪节点是否已被 AI 分析(none / pending / done) +- **Master 工具**:`server/src/tools/graph/` 提供 `create_person`、`get_person`、`link_persons`、`search_graph`、`get_relationship_path` 等工具供 AI 调用 + +### 微信集成架构 + +`server/services/wechat/` 提供从 Mac 和 Pixel 双端点与微信交互的能力: + +- **发送消息(实际工作链路)**:`am broadcast com.hermes.wechat.SEND_MSG` → **`HermesAccessibilityService`**(lsposed-wechat-sendmsg 模块内)→ 启动微信 LauncherUI → 点搜索按钮 → 搜索 `toUserDisplayName`(**必须传 nickname**,如"火星人")→ 点搜索结果 → 找 `bkk` 容器内的 EditText 输入文本 → 点 `bql` 发送按钮。纯 UI 自动化,绕开所有反射 / hook 失败。 +- **消息跟踪**:`android-wechat-tracker-sqlcipher.js` 直接轮询解密后的 `EnMicroMsg.db`(SQLCipher) +- **自动回复**:`auto-reply.js` 结合 tracker + broadcast,在 Pixel 上独立运行 +- **数据导入**:`wechat-importer.js` / `wechat-reader.js` 将联系人和聊天记录导入知识图谱 + +> [!WARNING] 关于其他两条已废弃的发送路径 +> - **LSPosed 反射 API**(`HermesWeChatModule.sendTextMessageDirect`):监听同一广播但走 `u01.r1.a → m1.a()`。`Send result: true` 是**误报**——`builder.a()` 后主线程异步提交触发 null 异常被 swallow,消息不进 `message` 表。 +> - **Frida daemon 队列**(`android-wechat-sender-frida.js` + `wechat_plain/scripts/frida-daemon/frida_daemon.js`):Frida 17 + 微信 8.0.72 注入大体积 bundle 触发反调试,微信进程崩溃,已废弃。 +> 这两条路径的命名让人误解;真正能把消息推进 message 表的只有 A11y service。 + +> [!IMPORTANT] A11y service 启用方式 +> `enabled_accessibility_services` 系统设置易被关闭。检查与重新启用: +> ```bash +> adb shell "settings get secure enabled_accessibility_services" +> 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'" +> ``` + +详见 `server/services/wechat/README.md`。 + ## 注意事项 - `server/src/standalone-entry.js` 的 `loadConfig()` 优先读 `backends.json` + `agents.json`,无需数据库。 - `prompt-builder.js` 的 `getSystemStateXML()` 只返回时间戳(公司/OKR 层已移除)。 - Agent 目录内放 `SKILL.md` 文件会绕过协议封装,直接作为系统提示使用。 +- `server/index.js` 包含控制字符净化中间件:Agent 通过 curl 发送的 JSON body 可能含原始换行符,若解析失败会自动清理 `\x00-\x1F` 控制字符后重试。 > [!WARNING] (Agentic Self-Correction Loop Prevention) > **Master Agent 必读**:在 `Bash` tool 中使用含多重双引号的复杂命令(如 `curl -d '{"id":"lemonv"}'`)时,JSON 转义容易崩溃并陷入死循环。 diff --git a/server/services/wechat/README.md b/server/services/wechat/README.md index 9103577..1a77267 100644 --- a/server/services/wechat/README.md +++ b/server/services/wechat/README.md @@ -2,7 +2,12 @@ 本目录提供从 **Mac** 和 **Pixel 手机** 两个端点与微信进行交互的能力:发送消息、实时跟踪收发消息。 -底层依赖自研的 **LSPosed/Xposed 模块**(`HermesWeChatSendMsg`),通过 Hook 微信内部 API 实现消息发送,并拦截日志实现消息跟踪。 +底层依赖自研的 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 这一条路径。 --- @@ -57,17 +62,7 @@ ## 模块原理 -### Hook 点(WeChat 8.0.72) - -| 目标类 | 方法 | 作用 | -|--------|------|------| -| `ExtContentProviderBase` | `h()` → return 1 | 绕过 appId + UID + 签名校验 | -| `ExtContentProviderBase` | `b()` → return true | 绕过登录状态检查 | -| `AESUtil` | `b(long)` → 返回原值 | 跳过联系人 ID 加密 | -| `AESUtil` | `a(String)` → 返回原值 | 跳过联系人 ID 解密 | -| `MsgInfo` (f9) | `c1(String)` | 监控消息内容变更,记录日志 | - -### 消息发送调用链 +### 主路径:A11y UI 自动化(实际工作的) ``` 外部请求 @@ -77,40 +72,96 @@ └── am broadcast ────────────────────────────┘ │ ▼ - 微信主进程 BroadcastReceiver + HermesAccessibilityService (BIND_ACCESSIBILITY_SERVICE) │ - ┌────────────────────┴────────────────────┐ - │ │ - ▼ ▼ - sendTextMessage() sendTextMessageDirect() - (官方 ContentProvider) (反射内部 API) - │ │ - │ u01.r1.a(toUser) → Builder │ - │ builder.e(content) │ - │ builder.h(0) │ - │ builder.a() → u01.m1 │ - │ m1.a() // 必须在主线程调用 │ - │ │ - └─────────────────────────────────────────┘ + ▼ + 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 注入会触发反调试,进程崩溃。已不在生产使用。 + ### 跨进程通信 -模块代码运行在 **微信主进程**(通过 LSPosed 注入),ContentProvider 运行在 **模块 APK 自身进程**。两者通过 Android 广播通信: +`HermesAccessibilityService` 运行在**模块 APK 自身进程**,由系统的 AccessibilityManager 绑定。外部进程通过 Android 广播(`com.hermes.wechat.SEND_MSG`)触发: ``` -hermes / adb shell content → 模块 Provider 进程 → 发送广播 - │ - 微信主进程(注入的模块) - │ - 广播接收器 → NetSceneSendMsg - │ - 微信服务器 +hermes / adb shell content / am broadcast + │ + ▼ + Android 系统 broadcast manager + │ + ▼ + HermesAccessibilityService (模块进程) + │ + │ AccessibilityNodeInfo / performAction + ▼ + 微信主进程的 UI (作为前台被自动化点击) + │ + ▼ + 微信走自己正常的发送流程 → 服务器 ``` +A11y service 必须由用户在「设置 → 辅助功能」中手动启用,或者用 root 通过 `settings put secure enabled_accessibility_services` 启用。重启 / 系统更新 / 应用更新都可能导致它被关闭。 + --- ## 文件清单 @@ -119,10 +170,10 @@ hermes / adb shell content → 模块 Provider 进程 → 发送广播 |------|------|--------| | `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-sender-frida.js` | **主推** 发送消息(Frida daemon 队列) | Pixel | -| `android-wechat-sender.js` | 备用发送消息(am broadcast / content insert) | 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 | @@ -159,13 +210,22 @@ adb install app/build/outputs/apk/release/app-release.apk # 3. 强行停止微信,重新打开 ``` -### 3. 验证模块工作 +### 3. 启用 AccessibilityService(必须) ```bash -adb shell su -c "logcat -s HermesWX:D | grep 'Send broadcast receiver registered'" -# 应看到: [HermesWX] Send broadcast receiver registered +# 启用 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 端使用 @@ -298,10 +358,12 @@ tracker.startPolling(3000); | 现象 | 原因 | 解决 | |------|------|------| -| `content insert` 无响应 | 广播未注册 | 检查 logcat 中 `Send broadcast receiver registered` | -| `Send result: false` | 内部 API 调用失败 | 确认 WeChat 版本匹配,检查类名混淆 | -| `m1.a() return type` 异常 | 微信版本升级 | 重新反编译 APK,更新 `CLS_SEND_MSG_FACTORY` | -| 模块未出现在 Vector Manager | APK 未签名 | 重新签名安装 | +| 消息没进 `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 端 diff --git a/server/services/wechat/SKILL.md b/server/services/wechat/SKILL.md index fb5807a..6fec7ed 100644 --- a/server/services/wechat/SKILL.md +++ b/server/services/wechat/SKILL.md @@ -7,48 +7,73 @@ ## 环境要求 1. **Root 权限** — 必须已 root(Magisk 30.7+) -2. **LSPosed/Vector 模块** — HermesWeChatSendMsg 已启用,微信在作用域内 -3. **微信主进程运行中** +2. **HermesWeChatSendMsg 模块已安装**(lsposed-wechat-sendmsg APK) +3. **AccessibilityService 已启用** — 这是最容易踩的坑,详见下文 +4. **微信主进程运行中且在前台**(广播触发时 termux/其他 app 在前台会导致 A11y service 拿到错误的 root) + +### 启用 / 验证 AccessibilityService + +```bash +# 启用 +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'" + +# 验证 +adb shell "settings get secure enabled_accessibility_services" +# 应输出: com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService + +adb shell "dumpsys accessibility | grep Hermes" +# 应看到 Bound services 和 Enabled services 都包含 HermesAccessibilityService +``` ## 文件位置 ``` server/services/wechat/ +├── android-wechat-sender.js # **主推** 发送(am broadcast → A11y service) ├── android-wechat-tracker-sqlcipher.js # **主推** 消息跟踪(sqlcipher 轮询 DB) -├── android-wechat-sender-frida.js # **主推** 消息发送(Frida daemon 队列) -├── android-wechat-sender.js # 备用发送(am broadcast / content insert) ├── android-wechat-tracker.js # 备用跟踪(tail -f LSPosed 日志) +├── android-wechat-sender-frida.js # ⚠️ 已废弃(Frida daemon 队列) ├── auto-reply.js # 自动回复主入口 └── wechat-reader.js # 数据库读取 ``` ## 发送消息 -### 方式1:Frida Daemon(推荐) - -需要 frida-server 运行且 frida_daemon.js 已注入微信进程: +### 唯一可用方式:am broadcast → A11y UI 自动化 ```js -const { sendTextMessageFrida } = require('./android-wechat-sender-frida'); +const { sendTextMessageBroadcast } = require('./android-wechat-sender'); -const r = await sendTextMessageFrida('wxid_xxxxxx', '你好,来自 hermes'); +// ⚠️ toUser = wxid(写入 message 表用),toUserDisplayName = WeChat nickname(A11y 搜索用,必须传) +const r = await sendTextMessageBroadcast('yibooo', '你好,来自 hermes', { toUserDisplayName: '火星人' }); console.log(r); // { ok: true } -// 写入 /data/local/tmp/frida_daemon/queue.jsonl,daemon 异步消费 ``` -### 方式2:ContentProvider / Broadcast(备用) +底层等价于: -```js -const { sendTextMessage, sendTextMessageBroadcast } = require('./android-wechat-sender'); +```bash +adb shell "am broadcast -a com.hermes.wechat.SEND_MSG \ + --es toUser yibooo \ + --es toUserDisplayName 火星人 \ + --es content '你好'" +``` -// ContentProvider(与 Mac 端 API 一致) -const r1 = await sendTextMessage('filehelper', '你好'); -console.log(r1); // { ok: true, uri: 'content://.../send/0' } +A11y service 收到后会:启动微信 → 搜索 `火星人` → 进入聊天 → 写文本 → 点发送按钮,全程约 5-8 秒。 -// 直接广播(绕过 Provider 进程) -const r2 = await sendTextMessageBroadcast('wxid_xxxxxx', '测试消息'); -console.log(r2); // { ok: true } -``` +### 关键约束 + +1. **必须传 `toUserDisplayName`**(WeChat nickname)。如果只传 `toUser` (wxid),A11y 会用 wxid 去搜——绝大多数情况搜不到,发送失败。 +2. **微信必须在前台**或至少不被其他高优先级 app(如全屏游戏)阻挡。如果广播触发时手机停在 termux/其他 app,A11y 的 `startActivity(LauncherUI)` 可能被忽略。 +3. **不要短时间内连发**。A11y 流程有 3500ms + 800ms + 800ms + 2500ms + 400ms 的多段 delay,单次约 8s。 + +### 已废弃路径(不要用) + +| 文件 / API | 状态 | +|----|----| +| `sendTextMessageFrida` (`android-wechat-sender-frida.js`) | Frida 17 + 微信 8.0.72 注入崩溃,daemon 起不来 | +| `sendTextMessage` ContentProvider 路径 | 触发的 LSPosed `HermesWeChatModule.sendTextMessageDirect` 返回 `true` 但消息不进 DB(主线程 NetScene 提交 NPE 被 swallow) | + +注:`sendTextMessage`(ContentProvider)和 `sendTextMessageBroadcast`(broadcast)**最终广播都被 A11y service 接收并执行**,所以从外部看上去 ContentProvider 路径也能工作——但只是因为 A11y 路径在后台兜住了。建议直接用 broadcast 接口,语义最清晰。 ### 批量发送 @@ -126,11 +151,12 @@ const msgs = await tracker.fetchRecent(20); // 仅 AndroidWechatTracker 支持 | 现象 | 原因 | 解决 | |------|------|------| -| `exit 1: permission denied` | 未 root 或 su 不可用 | 检查 Magisk root 状态 | -| `content: not found` | 不在 Android 环境 | 确保在 Termux 或 Debian 中用 `/system/bin/content` | -| 发送成功但对方没收到 | 广播被拦截或微信没在前台 | 确保微信主进程存活,检查 LSPosed 日志 | -| 跟踪不到消息 | 日志级别不够 | 在 Vector Manager → 日志 中设置详细级别 | -| `am broadcast` 提示 `Background execution not allowed` | Android 10+ 后台限制 | 使用 `content insert` 方式替代 | +| 接口返回 `ok: true` 但 yibooo 没收到 / `message` 表没新行 | A11y service 没启用 | 见上文「启用 / 验证 AccessibilityService」 | +| A11y 日志 `Root package=com.termux` 后直接 reset | broadcast 触发时微信不在前台 | 先 `adb shell am start -n com.tencent.mm/.ui.LauncherUI` | +| `Search button not found` | 当前不是 LauncherUI(可能停在另一个聊天里) | A11y 内置了 relaunch 兜底,等下一次会自动恢复;或手动回到主界面 | +| `No clickable search result found` | nickname 错 / 不是好友 | 验证 `toUserDisplayName` 是 WeChat 里的实际昵称 | +| `[HermesWX] Send result: true` 出现在日志里但消息没发出 | LSPosed 反射路径的误报,**正常现象** | 看 `HermesA11y:*` 日志才反映真实情况 | +| 跟踪不到消息 | SQLCipher key 失效 / DB 路径变了 | 重新捕获 key(见 `wechat_plain/scripts/README_Android.md`) | ## 与 Mac 端区别 diff --git a/server/services/wechat/android-wechat-sender-frida.js b/server/services/wechat/android-wechat-sender-frida.js index 42dbd3d..f64853f 100644 --- a/server/services/wechat/android-wechat-sender-frida.js +++ b/server/services/wechat/android-wechat-sender-frida.js @@ -1,21 +1,20 @@ /** - * android-wechat-sender-frida.js — 基于 Frida Daemon 的微信消息发送 + * android-wechat-sender-frida.js — ⚠️ 已废弃,保留仅供参考 * - * 原理:写入队列文件 /data/local/tmp/frida_daemon/queue.jsonl - * 由常驻微信进程的 Frida daemon 读取并发送 + * 原设计:写入队列文件由常驻微信进程的 Frida daemon 读取并发送。 * - * 前提: - * 1. frida-server 运行中 - * 2. frida_daemon.js 已注入微信进程(通过 Mac 端 frida CLI) - * 3. 队列目录 /data/local/tmp/frida_daemon/ 存在且可写 + * 实测在 WeChat 8.0.72 + Frida 17 上 daemon 注入会触发微信反调试, + * 进程崩溃。当前生产路径走 am broadcast → HermesAccessibilityService UI 自动化, + * 见 android-wechat-sender.js (sendTextMessageBroadcast)。 * - * 注意:发送为 fire-and-forget,daemon 会异步处理队列。 - * 如需确认送达,可通过 sqlcipher 查询 message 表。 + * 不要在新代码中调用本模块的导出函数。 */ const fs = require('fs'); -const QUEUE_DIR = '/data/local/tmp/frida_daemon'; +// 所有 Pixel 端文件统一放在 Termux 可访问目录,避免 /data/local/tmp 权限问题 +const HOME_DIR = '/data/data/com.termux/files/home/hermes-wechat'; +const QUEUE_DIR = HOME_DIR + '/.frida_queue'; const QUEUE_FILE = QUEUE_DIR + '/queue.jsonl'; function ensureDir() { diff --git a/wechat_plain/scripts/local-testkit/docs/HERMES_WECHAT_ARCHITECTURE.md b/wechat_plain/scripts/local-testkit/docs/HERMES_WECHAT_ARCHITECTURE.md index d3f9ebc..613fd3e 100644 --- a/wechat_plain/scripts/local-testkit/docs/HERMES_WECHAT_ARCHITECTURE.md +++ b/wechat_plain/scripts/local-testkit/docs/HERMES_WECHAT_ARCHITECTURE.md @@ -23,7 +23,7 @@ - **hermes-agent** (lemonPCRM wrapper) 是独立的 OpenAI-compatible AI 服务,负责 LLM 推理 - 两者通过 HTTP API (`POST /v1/chat/completions`) 通信 - **SQLCipher DB 轮询** 替代 LSPosed 日志 tail,更稳定可靠 -- **Frida Daemon** 替代 am broadcast,绕过 Android 10+ 后台限制 +- **`am broadcast` → HermesAccessibilityService UI 自动化** 是当前唯一稳定可用的发送方式(Frida 注入 / LSPosed 反射 NetSceneSendMsg 都在 WeChat 8.0.72 上失败) --- @@ -49,16 +49,18 @@ │ │ ┌─────────┴─────────────────────────────────────────────────────────┐ │ │ │ │ │ auto-reply.js (Node.js) │ │ │ │ │ │ ┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ -│ │ │ │ SQLCipher │→ │ 新消息 │→ │ HTTP │→ │ Frida Queue │ │ │ │ -│ │ │ │ DB 轮询 │ │ 检测 │ │ hermes- │ │ /tmp/frida_ │ │ │ │ -│ │ │ │ │ │ │ │ agent │ │ daemon/ │ │ │ │ +│ │ │ │ SQLCipher │→ │ 新消息 │→ │ HTTP │→ │ am broadcast │ │ │ │ +│ │ │ │ DB 轮询 │ │ 检测 │ │ hermes- │ │ SEND_MSG │ │ │ │ +│ │ │ │ │ │ │ │ agent │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ │ │ │ │ ↑ │ │ │ │ │ │ │ │ │ │ │ │ -│ │ │ ┌──────────────┐ ┌──────────┴─────┐ │ │ │ -│ │ │ │ sqlcipher │ │ Frida Daemon │ │ │ │ -│ │ │ │ 直接查询 │ │ (注入微信进程) │ │ │ │ -│ │ │ └──────────────┘ └────────────────┘ │ │ │ +│ │ │ ┌──────────────┐ ┌──────────────┴───────┐ │ │ │ +│ │ │ │ sqlcipher │ │ HermesA11yService │ │ │ │ +│ │ │ │ 直接查询 │ │ (lsposed-wechat- │ │ │ │ +│ │ │ │ │ │ sendmsg 模块进程) │ │ │ │ +│ │ │ │ │ │ UI 自动化点击微信 │ │ │ │ +│ │ │ └──────────────┘ └──────────────────────┘ │ │ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ ↑ │ │ │ │ │ │ │ @@ -81,8 +83,9 @@ │ │ │ ~/.termux/boot/start-hermes.sh │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ Frida Server (可选,用于注入 daemon) │ │ │ -│ │ │ /data/local/tmp/frida-server │ │ │ +│ │ │ HermesWeChatSendMsg APK (含 A11y service) │ │ │ +│ │ │ /data/app/com.hermes.wechat.xposed-*/base.apk │ │ │ +│ │ │ 必须在 设置 → 辅助功能 中启用 HermesAccessibilityService │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ │ ↑ │ │ @@ -134,7 +137,7 @@ 8. auto-reply.js 提取 content 作为回复文本 │ ▼ -9. 写入 /data/local/tmp/frida_daemon/queue.jsonl +9. 写入 /data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl │ ▼ 10. Frida Daemon(注入微信进程)读取队列,调用微信内部 API 发送消息 @@ -296,7 +299,7 @@ Frida Daemon 注入微信主进程,通过 Java.perform 调用微信内部 APIï ``` auto-reply.js │ - ├── 写入队列文件 /data/local/tmp/frida_daemon/queue.jsonl + ├── 写入队列文件 /data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl │ {"talker": "filehelper", "content": "回复内容", "id": "msg_xxx"} │ ▼ @@ -346,8 +349,8 @@ Java.perform → 调用微信内部发送 API #!/bin/bash # 启动 frida-server -if [ -f "/data/local/tmp/frida-server" ]; then - su -c "/data/local/tmp/frida-server &" +if [ -f "/data/data/com.termux/files/home/hermes-wechat/bin/frida-server" ]; then + su -c "/data/data/com.termux/files/home/hermes-wechat/bin/frida-server &" fi # 启动微信(如果未运行) @@ -501,7 +504,7 @@ curl http://127.0.0.1:8642/v1/chat/completions \ ```bash # 1. 确保 frida-server 运行 -su -c "/data/local/tmp/frida-server &" +su -c "/data/data/com.termux/files/home/hermes-wechat/bin/frida-server &" # 2. 注入 daemon 到微信进程(从 Mac 或手机端) frida -U -n com.tencent.mm -l frida_daemon.js @@ -557,7 +560,7 @@ chmod +x ~/.termux/boot/start-hermes.sh **A**: 1. 确认 frida-server 运行中:`ps aux | grep frida-server` 2. 确认 frida_daemon.js 已注入微信进程 -3. 检查队列文件权限:`ls -la /data/local/tmp/frida_daemon/queue.jsonl` +3. 检查队列文件权限:`ls -la /data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl` 4. 备用方案:回退到 `sendTextMessageBroadcast()` ### Q: 息屏后完全失效 diff --git a/wechat_plain/scripts/local-testkit/docs/README.md b/wechat_plain/scripts/local-testkit/docs/README.md index a676739..2452a89 100644 --- a/wechat_plain/scripts/local-testkit/docs/README.md +++ b/wechat_plain/scripts/local-testkit/docs/README.md @@ -47,28 +47,41 @@ sqlcipher 直接查询 EnMicroMsg.db └─────────────────────────┘ └─────────────────────────┘ │ ▼ -┌─────────────────────────┐ ┌─────────────────────────┐ -│ 微信在前台? │ No →│ wakeWechat() 点亮+解锁 │ -└─────────────────────────┘ │ + am start 启动微信 │ - │Yes └─────────────────────────┘ - ▼ ┌─────────────────────────┐ -│ sendTextMessageFrida │────→ 写入 /data/local/tmp/frida_daemon/queue.jsonl -│ (android-wechat-sender-frida) │ -└─────────────────────────┘ +│ 微信在前台? │ No → am start LauncherUI +└─────────────────────────┘ + (息屏时) wakeWechat() + │Yes + ▼ +┌──────────────────────────────────────────────────────────┐ +│ am broadcast -a com.hermes.wechat.SEND_MSG │ +│ --es toUser │ +│ --es toUserDisplayName ← 必传! │ +│ --es content │ +└──────────────────────────────────────────────────────────┘ │ ▼ -┌─────────────────────────┐ -│ Frida Daemon (注入微信) │────→ Java.perform → 调用微信内部发送 API -└─────────────────────────┘ +┌──────────────────────────────────────────────────────────┐ +│ HermesAccessibilityService (lsposed-wechat-sendmsg 模块) │ +│ 1. startActivity(LauncherUI, CLEAR_TASK) │ +│ 2. 点搜索按钮 (id=jha) │ +│ 3. ACTION_SET_TEXT 输入 nickname → 点搜索结果 │ +│ 4. 进入聊天后 ACTION_SET_TEXT 写入 content │ +│ 5. 点发送按钮 (id=bql) │ +└──────────────────────────────────────────────────────────┘ + │ + ▼ + 微信走自己正常的发送流程 → 服务器 → message 表 isSend=1 ``` **优势**: -- 不依赖 LSPosed hook(WeChat 8.0.72 上 Xposed hook 不稳定) +- 不依赖反射 hook 微信内部 NetSceneSendMsg(在 WeChat 8.0.72 上主线程提交持续 NPE 失败) +- 不依赖 Frida 注入(Frida 17 + 微信 8.0.72 注入会触发反调试 / 进程崩溃) - sqlcipher 直接查询,数据完整(含群聊名称、发送者等) -- Frida 发送比 am broadcast 更可靠(绕过 Android 10+ 后台限制) +- 走 UI 是慢一点(~8s/条)但**唯一稳定可用** - 通过 hermes-agent API 享受完整的 Agent 编排、工具调用、记忆能力 +> ⚠️ **历史遗留命名警告**:`android-wechat-sender-frida.js` 中的 `sendTextMessageFrida` 写入 `.frida_queue/queue.jsonl` 的设计**已废弃**。同样地,README 早期版本把 Frida daemon 描述为主路径——那是错的。所有发送最终都走 `am broadcast` → A11y service。 + ### 备用流程:Python Daemon `hermes_wechat_daemon.py` 作为备用通道,当 Node.js plugin 不可用时运行: @@ -174,16 +187,22 @@ curl http://127.0.0.1:8642/v1/chat/completions -X POST \ -d '{"model":"hermes-agent","messages":[{"role":"user","content":"hi"}]}' ``` -### 4. 启动 Frida Daemon(发送端依赖) +### 4. 启用 HermesAccessibilityService(发送端依赖) + +A11y service 是消息真正能发出去的唯一路径——务必确认启用: ```bash -# 确保 frida-server 在运行 -su -c "/data/local/tmp/frida-server &" +# 启用 +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'" -# 注入 daemon 到微信进程(从 Mac 端执行) -frida -U -n com.tencent.mm -l frida_daemon.js +# 验证 +adb shell "settings get secure enabled_accessibility_services" +adb shell "dumpsys accessibility | grep -A3 Hermes" +# 应看到: Bound services / Enabled services 都包含 HermesAccessibilityService ``` +`enabled_accessibility_services` 在系统重启 / 微信或模块更新 / 用户在设置里关闭后会变成 `null`,发送会静默失败——把这个检查写进保活脚本里。 + ### 5. 启动 auto-reply plugin(推荐主流程) ```bash @@ -256,11 +275,13 @@ python3 hermes_wechat_daemon.py --daemon | 问题 | 排查步骤 | |------|---------| +| 发送接口返回 ok 但 yibooo 没收到 / `message` 表没新行 | **最常见**:A11y service 未启用。`adb shell "settings get secure enabled_accessibility_services"` 若为 `null` 即是。重新启用见上文 | +| A11y 日志 `Root package=com.termux` 后退出 | 广播触发时微信不在前台。先 `am start -n com.tencent.mm/.ui.LauncherUI` 再发 broadcast | +| A11y 日志 `Search button not found` | 当前页面不是主界面 / nickname 错。检查 `toUserDisplayName` 必须是好友的 WeChat 昵称(如"火星人"),不是 wxid | +| `[HermesWX] Send result: true` 但 DB 没新消息 | LSPosed 反射路径的**误报**,是已知 bug。看 `HermesA11y:*` 日志才反映真实状态 | | auto-reply 收不到消息 | 检查 sqlcipher 能否查询:`su -c "sqlcipher /data/.../EnMicroMsg.db 'PRAGMA key=\\\"e03ce81\\\"; SELECT COUNT(*) FROM message;'"` | | hermes-agent API 返回空 | 检查 hermes-agent 是否运行:`curl http://127.0.0.1:8642/v1/chat/completions ...` | | 息屏后完全失效 | 确认电池优化已关闭;检查 `keepalive.sh` 是否运行;检查通知权限 | -| 发送失败(Frida) | 确认 frida-server 运行中;确认 frida_daemon.js 已注入微信进程;检查队列文件 `/data/local/tmp/frida_daemon/queue.jsonl` | -| 发送失败(broadcast 备用) | 检查微信是否在前台;检查 LSPosed 模块是否启用 | | Python daemon 查询不到新消息 | 检查微信是否在前台;检查 `sqlcipher_key`;确认 WAL 已复制 | | Claude CLI 报错 `root/sudo privileges` | hermes 必须以 proot 内 **ubuntu 用户** 运行,不能是 root | diff --git a/wechat_plain/scripts/lsposed-wechat-sendmsg/README.md b/wechat_plain/scripts/lsposed-wechat-sendmsg/README.md index 9256ee8..ae33550 100644 --- a/wechat_plain/scripts/lsposed-wechat-sendmsg/README.md +++ b/wechat_plain/scripts/lsposed-wechat-sendmsg/README.md @@ -1,13 +1,15 @@ -# Hermes WeChat SendMsg — LSPosed/Xposed Module +# Hermes WeChat SendMsg — LSPosed Module + AccessibilityService -LSPosed 模块,目标 WeChat 8.0.72,用于绕过 ContentProvider 权限校验并暴露消息发送接口。 +针对 WeChat 8.0.72 的 Android 模块,包含两套发送实现: + +- ✅ **`HermesAccessibilityService`(生产路径)** — 监听广播 `com.hermes.wechat.SEND_MSG`,纯 UI 自动化:搜索联系人 → 输入文本 → 点发送按钮。**当前唯一稳定可用的发送方式**。 +- ❌ **`HermesWeChatModule`(已废弃,保留代码作记录)** — LSPosed 入口,hook `ExtContentProviderBase` / `AESUtil` 后反射调用 `u01.r1.a → m1.a()`。在 WeChat 8.0.72 上主线程 NetScene 提交持续 NPE 失败,`Send result: true` 是误报,消息不进 `message` 表。 ## 功能 -1. **Auth Bypass** — Hook `ExtContentProviderBase.h()` 和 `b()`,使任意调用者通过权限校验 -2. **AESUtil Bypass** — 跳过联系人 ID 的加密/解密,直接使用明文 wxid -3. **消息发送** — 外部应用通过 ContentProvider 写入发送请求,模块在 WeChat 主进程内通过广播接收并调用内部 NetSceneSendMsg API 发送 -4. **消息监控** — Hook `MsgInfo.setContent()`,将收发消息实时记录到日志 +1. **A11y 自动化发送(主路径)** — `HermesAccessibilityService` 模拟用户在微信主界面搜索联系人、点进聊天、写文本、点发送按钮 +2. **Auth Bypass / AESUtil / MsgInfo hook(历史遗留)** — `HermesWeChatModule` 内的 LSPosed hook 对生产路径没有影响,可选保留 +3. **ContentProvider 暴露** — `HermesWeChatProvider` 把 `content insert` 翻译成广播,主要给非 root 的调用方用 ## 项目结构 @@ -57,32 +59,57 @@ $ANDROID_HOME/build-tools/34.0.0/apksigner sign --ks hermes-debug.keystore --ks- ## 安装 -### 前提:LSPosed 框架(Zygisk 版) +### A11y 路径(生产,必做) + +1. 安装本模块 APK:`adb install app/build/outputs/apk/release/app-release.apk` +2. **启用 AccessibilityService**(关键,最容易踩的坑): + ```bash + 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'" + ``` + 或手动:设置 → 辅助功能 / 无障碍 → Hermes WeChat SendMsg → 启用 +3. 验证: + ```bash + adb shell "dumpsys accessibility | grep -A3 Hermes" + # 应看到 Bound services / Enabled services 都包含 HermesAccessibilityService + ``` + +A11y service 会在重启、微信更新、本模块更新、用户在设置里关闭后失效,发送将静默失败——把启用检查纳入保活脚本。 + +### LSPosed 模块路径(可选,目前无功能贡献) -1. 确保 Magisk 已安装且 **Zygisk 已启用**(Magisk 设置 → Zygisk → 开) -2. 下载 [LSPosed Zygisk 版](https://github.com/JingMatrix/LSPosed/releases)(或 Vector 2.0+) -3. 在 Magisk 中安装 LSPosed 模块,重启手机 -4. 安装本模块 APK:`adb install app/build/outputs/apk/release/app-release.apk` -5. 打开 LSPosed Manager → 模块 → 启用 HermesWeChatSendMsg -6. **作用域**中选择 **微信(com.tencent.mm)** -7. 强行停止微信,重新打开 +如果同时希望 LSPosed hook 生效(虽然反射发送已坏,但 hook 本身对 A11y 没影响): + +1. 确保 Magisk 已安装且 **Zygisk 已启用** +2. 安装 [LSPosed Zygisk 版](https://github.com/JingMatrix/LSPosed/releases)(或 Vector 2.0+) +3. LSPosed Manager → 模块 → 启用 HermesWeChatSendMsg → 作用域选择 **微信(com.tencent.mm)** +4. 强行停止微信,重新打开 ## 使用 -模块安装并启用后,可通过 `adb shell content` 命令与模块交互。 +### 发送文本消息(推荐方式:直接广播) + +```bash +adb shell "am broadcast -a com.hermes.wechat.SEND_MSG \ + --es toUser yibooo \ + --es toUserDisplayName 火星人 \ + --es content '你好,这是测试消息'" +``` + +字段说明: +- `toUser` — 接收者 wxid(如 `wxid_xxxxxx`、`yibooo`、`filehelper`)。**注意:A11y 流程不直接用这个值发送,但留着用于校验** +- `toUserDisplayName` — **必须传**:WeChat 里的 nickname(如 `火星人`、`文件传输助手`)。A11y 用它来在搜索框里输入并定位联系人。如果只传 wxid 通常搜不到 +- `content` — 消息内容 -### 发送文本消息 +### 备用:ContentProvider insert ```bash adb shell content insert \ --uri content://com.hermes.wechat.xposed.provider/send \ - --bind toUser:s:filehelper \ - --bind content:s:"你好,这是测试消息" + --bind toUser:s:yibooo \ + --bind content:s:"你好" ``` -字段说明: -- `toUser` — 接收者 wxid(如 `wxid_xxxxxx`、`filehelper`)或群聊 ID -- `content` — 消息内容 +这条路径会**双广播**(同时触发 LSPosed 反射 + A11y),LSPosed 那条会失败但不影响最终结果;A11y 这条把消息真正发出去。但因为 `content insert` 不传 `toUserDisplayName`,A11y 会用 toUser 当 nickname 去搜,搜不到的情况下发送失败——所以推荐用 `am broadcast` 显式传 displayName。 ### 查询发送状态 @@ -96,31 +123,63 @@ adb shell content query \ ### 查看模块日志 ```bash -adb logcat -s HermesWX:D LSPosed-Bridge:D | grep HermesWX -``` +# A11y 路径(看这个判断实际是否发送) +adb logcat -s HermesA11y:* -或查看 Vector/LSPosed 日志: -```bash +# LSPosed 路径(已废弃,仅供参考) adb shell su -c "cat /data/adb/lspd/log/verbose_*.log | grep HermesWX | tail -20" ``` -## 技术细节 +正常 A11y 成功日志: +``` +HermesA11y: Received send request: to=yibooo searchName=火星人 content=... +HermesA11y: Root package=com.tencent.mm +HermesA11y: Already in correct chat, typing directly (或 "Not in chat, starting search") +HermesA11y: Setting chat text: ... +HermesA11y: Clicking send button (by ID) +HermesA11y: State reset +``` -### 跨进程通信 +## 技术细节 -模块代码运行在 **WeChat 主进程** 中(通过 LSPosed 注入),ContentProvider 运行在 **模块 APK 自身进程** 中。两者通过 Android 广播通信: +### 进程拓扑 ``` -hermes / adb shell content → 模块 Provider 进程 → 发送广播 (com.hermes.wechat.SEND_MSG) - ↓ - WeChat 主进程(注入的模块) - ↓ - 广播接收器 → NetSceneSendMsg 内部 API - ↓ - 微信服务器 +外部调用方(adb / hermes / shell) + │ + │ am broadcast com.hermes.wechat.SEND_MSG + ▼ +Android system broadcast manager + │ + ├──────────────────────────┐ + │ │ + ▼ ▼ +HermesAccessibilityService HermesWeChatModule (LSPosed) +(模块 APK 进程) (注入在微信主进程) + │ │ + │ AccessibilityNodeInfo │ 反射调用 u01.r1.a → m1.a() + ▼ ▼ +微信主进程 UI(前台) 【已坏:主线程 NPE 被 swallow】 + │ + ▼ +微信走自己的发送流程 → 服务器 → message 表 ``` -### Hook 点 +两个广播 receiver **并行触发**,A11y 那条成功、LSPosed 那条静默失败。整个生产路径只依赖 A11y。 + +### A11y 用到的 UI 节点(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` | + +微信版本升级后这些 ID 会变化,需要重新 uiautomator dump 验证。 + +### LSPosed Hook 点(历史记录,目前无生产作用) | 目标类 | 方法 | 作用 | |--------|------|------| @@ -130,25 +189,18 @@ hermes / adb shell content → 模块 Provider 进程 → 发送广播 (com. | `AESUtil` | `a(String)` → 返回原值 | 跳过解密 | | `MsgInfo` (f9) | `c1(String)` | 监控消息内容变更,记录日志 | -### 内部发消息调用链 - -``` -u01.r1.a(String toUser) → Builder - Builder.e(String content) → 设置文本内容 - Builder.h(int type) → 设置联系人类型 (0=普通) - Builder.a() → u01.m1 (Result) - m1.a() → 提交网络请求(必须在主线程调用) -``` - -类名 `u01.*` 随 WeChat 版本变化,需通过 smali 分析确认。 +LSPosed 反射发送的调用链 `u01.r1.a → builder.e → builder.h(0) → builder.a() → m1.a()` 在 WeChat 8.0.72 上 `m1.a()` 主线程提交触发 NPE 失败。已确认无法修复(怀疑 WeChat 在 NetScene 链路加了反作弊校验)。代码仍保留在 `HermesWeChatModule.java` 中作为反编译记录。 ## 故障排查 | 现象 | 可能原因 | 解决 | |------|---------|------| -| `content insert` 无响应 | 广播未注册(WeChat 未启动或模块未启用) | 检查 logcat 中 `Send broadcast receiver registered` | -| `Send result: false` | 内部 API 调用失败(类名不匹配或不在主线程) | 检查日志中的具体错误,确认 WeChat 版本 | -| `m1.a() return type` 异常 | WeChat 版本升级后混淆类名变化 | 重新反编译 APK,更新 `CLS_SEND_MSG_FACTORY` 等常量 | +| `am broadcast` 返回 0 但 yibooo 没收到 / message 表无新行 | **最常见**:A11y service 未启用 | `adb shell "settings get secure enabled_accessibility_services"`,为 `null` 用上文命令重新启用 | +| HermesA11y 日志 `Root package=com.termux` 后退出 | 广播触发时微信不在前台 | 先 `adb shell am start -n com.tencent.mm/.ui.LauncherUI` 再发 broadcast | +| `Search button not found` | 当前不是 LauncherUI / nickname 错 | 确认 `toUserDisplayName` 是 WeChat 里实际的昵称 | +| `No clickable search result found` | 搜不到该 nickname | 在微信里手动搜一下验证 | +| `[HermesWX] Send result: true` 但 message 表没新行 | LSPosed 反射路径的**误报**(已知 bug) | 忽略 LSPosed 日志,看 HermesA11y 日志判断真实状态 | +| 微信 UI 节点 ID 找不到(升级后) | WeChat 版本变化导致 resource-id 变 | `adb shell uiautomator dump` 后重新核对 | | 模块未出现在 LSPosed Manager | APK 未正确安装或签名问题 | 重新安装模块 APK | ## 安全提示