Commit ceb78d7fdf8ecee0aa19dfc2111da248401f4f88
1 parent
6da2b2d9
docs: 纠正微信发送链路文档 — A11y UI 自动化是唯一可用路径
通过 DB 实证与 LSPosed 日志对比定位到: - LSPosed `HermesWeChatModule.sendTextMessageDirect` 在 WeChat 8.0.72 上 返回 `Send result: true` 后,主线程异步 NetScene 提交触发 NPE 被 swallow, 消息**不进 message 表**。这是误报。 - Frida daemon 队列方案(`android-wechat-sender-frida.js`)在 Frida 17 + 微信 8.0.72 上注入会触发反调试,进程崩溃,无法生产使用。 - 实际可用路径是 `HermesAccessibilityService`(lsposed-wechat-sendmsg 模块内): 监听同一广播 `com.hermes.wechat.SEND_MSG`,纯 UI 自动化点击微信完成发送。 - A11y service 易被系统关闭(`enabled_accessibility_services = null`), 这是消息发送失败的最常见根因,docs 中加上启用 / 验证命令。 更新范围: - CLAUDE.md - server/services/wechat/README.md, SKILL.md - server/services/wechat/android-wechat-sender-frida.js(标注废弃) - wechat_plain/scripts/local-testkit/docs/README.md, HERMES_WECHAT_ARCHITECTURE.md - wechat_plain/scripts/lsposed-wechat-sendmsg/README.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing
7 changed files
with
385 additions
and
172 deletions
CLAUDE.md
| ... | ... | @@ -6,22 +6,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co |
| 6 | 6 | |
| 7 | 7 | **lemonPCRM** — lemonAI 出å“çš„ Local-First AI 个人人脉管ç†ç³»ç»Ÿï¼ˆPersonal CRM)。以人为ä¸å¿ƒã€ä»¥å¯¹è¯ä¸ºé“¾æŽ¥ï¼Œé€šè¿‡çŸ¥è¯†å›¾è°±ç®¡ç†å¾®ä¿¡ã€LinkedInã€X ç‰å¤šå¹³å°ç¤¾äº¤å…³ç³»ï¼Œå°†æ¯ä¸ªè”系人与其社媒动æ€ã€ä¼šè®®çºªè¦ã€æ–‡ä»¶ã€ç…§ç‰‡å…¨é¢å…³è”。 |
| 8 | 8 | |
| 9 | -AI 对è¯ä¸Žç¼–排层基于 ACP å议驱动本地 Claude CLI 进程,æä¾›å• Agent 执行和 Master → Sub-Agent 多轮编排能力。Node.js + Expressï¼Œæ— æ•°æ®åº“ä¾èµ–,数æ®å®Œå…¨æœ¬åœ°åŒ–。 | |
| 9 | +AI 对è¯ä¸Žç¼–排层基于 ACP å议驱动本地 Claude CLI 进程,æä¾›å• Agent 执行和 Master → Sub-Agent 多轮编排能力。Node.js + Expressï¼Œæ— æ•°æ®åº“ä¾èµ–(使用 Node 22.5+ 内置 `node:sqlite`),数æ®å®Œå…¨æœ¬åœ°åŒ–。 | |
| 10 | 10 | |
| 11 | 11 | ## 常用命令 |
| 12 | 12 | |
| 13 | 13 | ```bash |
| 14 | -npm run setup # 首次åˆå§‹åŒ–:cp .env.example .env + npm install | |
| 14 | +npm run setup # 首次åˆå§‹åŒ–:cp .env.example .env + pnpm install | |
| 15 | 15 | npm start # 生产模å¼ï¼ˆnode server/index.js) |
| 16 | 16 | npm run dev # 开呿¨¡å¼ï¼ˆnode --watch server/index.js,çƒé‡è½½ï¼‰ |
| 17 | +./dev.sh # æ€æŽ‰ç«¯å£å 用进程 + npm run dev(推èå¼€å‘用) | |
| 17 | 18 | ``` |
| 18 | 19 | |
| 19 | -æ— æµ‹è¯•æ¡†æž¶ï¼Œæ— lint 脚本。 | |
| 20 | +æ— æµ‹è¯•æ¡†æž¶ï¼Œæ— lint 脚本。使用 pnpm workspaces(`packageManager: pnpm@10.28.1`)。 | |
| 20 | 21 | |
| 21 | 22 | ## æž¶æž„ |
| 22 | 23 | |
| 23 | 24 | ### æŠ€æœ¯æ ˆ |
| 24 | 25 | - **åŽç«¯**:Node.js (≥18) + Express,pnpm workspace monorepo |
| 26 | +- **æ•°æ®åº“**:Node 22.5+ 内置 `node:sqlite`(`DatabaseSync`),ä½äºŽ `server/db/lemon_crm.db` | |
| 25 | 27 | - **AI**:Claude CLI(本地å进程,`--dangerously-skip-permissions` 自动批准) |
| 26 | 28 | - **文件解æž**:mammoth(.docx)ã€pdf-parse(.pdf) |
| 27 | 29 | |
| ... | ... | @@ -31,13 +33,15 @@ npm run dev # 开呿¨¡å¼ï¼ˆnode --watch server/index.js,çƒé‡è½½ï¼ |
| 31 | 33 | |------|------| |
| 32 | 34 | | `packages/acp-core/` | ACP å议层:Protocol / Transport / Launcher 三层抽象 + ClaudeCliProtocol / AcpProtocol | |
| 33 | 35 | | `packages/agent-core/` | Agent è¿è¡Œæ—¶ï¼ˆESM):目录驱动的 Agentã€AgentBusã€AgentRegistryã€AgentScaffold | |
| 34 | -| `server/index.js` | Express å…¥å£ï¼ˆè·¯ç”±ï¼š`/api/master` + `/api/config` + `/api/health`) | | |
| 36 | +| `server/index.js` | Express å…¥å£ï¼ˆè·¯ç”± + 陿€å‰ç«¯ï¼‰ | | |
| 35 | 37 | | `server/src/core/` | 编排引擎:orchestratorã€master-sessionã€agent-managerã€session-poolã€stream-executorã€prompt-builderã€parserã€runtime-resolver | |
| 36 | 38 | | `server/src/engine/` | 进程层:run-registryã€command-queueã€process-guardã€idle-timerã€failover-error | |
| 37 | -| `server/src/tools/` | Master 工具:call-agent-toolã€tool-registry | | |
| 39 | +| `server/src/tools/` | Master 工具:call-agent-toolã€tool-registryã€graph/(图谱 MCP 工具集) | | |
| 38 | 40 | | `server/src/standalone-entry.js` | 编程 API:`runMasterSession` / `runOrchestration` / `runSingleAgent` | |
| 39 | -| `server/routes/master-chat.js` | SSE æµå¼å¯¹è¯è·¯ç”±ï¼ˆä¸»å…¥å£ï¼‰ | | |
| 40 | -| `server/services/` | conversation-loggerã€file-parserã€memory-serviceã€claude-chat-service | | |
| 41 | +| `server/routes/` | API 路由:master-chatã€configã€graphã€wechatã€importã€analysisã€photo | | |
| 42 | +| `server/services/` | conversation-loggerã€file-parserã€memory-serviceã€claude-chat-serviceã€wechat/ã€linkedin/ã€meeting/ã€photo/ã€x/ | | |
| 43 | +| `server/db/GraphStore.js` | 本地知识图谱å˜å‚¨å±‚(SQLite):Person / Organization / Project / Event / Asset 节点 + KNOWS / BELONGS_TO / PARTICIPATED ç‰è¾¹ | | |
| 44 | +| `frontend/` | 陿€å‰ç«¯ï¼ˆå• HTML 文件,由 server/index.js 托管) | | |
| 41 | 45 | | `server/config/backends.json` | ACP Launcher / Client 定义 | |
| 42 | 46 | | `server/config/agents.json` | 陿€ Agent 注册表(å¯é€‰ï¼Œä¹Ÿå¯ç”± `system/agents/` 目录å‘现) | |
| 43 | 47 | |
| ... | ... | @@ -51,6 +55,19 @@ npm run dev # 开呿¨¡å¼ï¼ˆnode --watch server/index.js,çƒé‡è½½ï¼ |
| 51 | 55 | | `DELETE /api/master/sessions/:id` | å…³é— session | |
| 52 | 56 | | `GET/PUT /api/config/items/:key` | 读写é…置文件 | |
| 53 | 57 | | `GET /api/health` | å¥åº·æ£€æŸ¥ | |
| 58 | +| `GET /api/graph/nodes` | 列出图谱节点(?type=Person&limit=100) | | |
| 59 | +| `GET /api/graph/nodes/:id` | 查询å•个节点(å«è¾¹ï¼‰ | | |
| 60 | +| `POST /api/graph/nodes` | 创建/更新节点 | | |
| 61 | +| `GET /api/graph/search?q=关键è¯` | 全文æœç´¢ | | |
| 62 | +| `POST /api/graph/edges` | 创建关系边 | | |
| 63 | +| `GET /api/graph/path?from=id&to=id` | 查询关系路径 | | |
| 64 | +| `POST /api/wechat/import-contacts` | 导入微信è”系人到图谱 | | |
| 65 | +| `POST /api/wechat/import-chat` | 导入微信èŠå¤©è®°å½• | | |
| 66 | +| `GET /api/wechat/contacts?q=æœç´¢` | 查询微信è”系人 | | |
| 67 | +| `GET /api/wechat/sessions` | 最近会è¯åˆ—表 | | |
| 68 | +| `POST /api/import/...` | å¤šå¹³å°æ•°æ®å¯¼å…¥ | | |
| 69 | +| `POST /api/analysis/...` | 图谱分æžï¼ˆå…³ç³»é“¾è·¯ã€èšç±»ç‰ï¼‰ | | |
| 70 | +| `POST /api/photo/...` | 照片æœåŠ¡ï¼ˆäººè„¸èšç±»ã€å‘釿£€ç´¢ï¼‰ | | |
| 54 | 71 | |
| 55 | 72 | ### é…置文件 |
| 56 | 73 | |
| ... | ... | @@ -102,11 +119,44 @@ Launcher (ProcessLauncher spawn / NoneLauncher æ— æ“作) |
| 102 | 119 | - æ¯ä¸ª Agent 的记忆å˜å‚¨åœ¨å…¶ç›®å½•下的 `memory/` å目录,纯文件系统 |
| 103 | 120 | - `AgentScaffold` 用于创建新 Agent 目录结构 |
| 104 | 121 | |
| 122 | +### 知识图谱(GraphStore) | |
| 123 | + | |
| 124 | +`server/db/GraphStore.js` 使用 `node:sqlite`(Node 22.5+ `DatabaseSync`ï¼‰ï¼Œæ— éœ€é¢å¤–ä¾èµ–。 | |
| 125 | + | |
| 126 | +- **节点类型**:Personã€Organizationã€Projectã€Eventã€Asset | |
| 127 | +- **边类型**:KNOWSã€BELONGS_TOã€PARTICIPATEDã€CONTAINSã€MENTIONED_INã€ASSOCIATED | |
| 128 | +- **分æžçжæ€**:`analysis_status` å—æ®µè·Ÿè¸ªèŠ‚ç‚¹æ˜¯å¦å·²è¢« AI 分æžï¼ˆnone / pending / done) | |
| 129 | +- **Master 工具**:`server/src/tools/graph/` æä¾› `create_person`ã€`get_person`ã€`link_persons`ã€`search_graph`ã€`get_relationship_path` ç‰å·¥å…·ä¾› AI 调用 | |
| 130 | + | |
| 131 | +### å¾®ä¿¡é›†æˆæž¶æž„ | |
| 132 | + | |
| 133 | +`server/services/wechat/` æä¾›ä»Ž Mac å’Œ Pixel åŒç«¯ç‚¹ä¸Žå¾®ä¿¡äº¤äº’的能力: | |
| 134 | + | |
| 135 | +- **å‘逿¶ˆæ¯ï¼ˆå®žé™…工作链路)**:`am broadcast com.hermes.wechat.SEND_MSG` → **`HermesAccessibilityService`**(lsposed-wechat-sendmsg 模å—内)→ å¯åŠ¨å¾®ä¿¡ LauncherUI → 点æœç´¢æŒ‰é’® → æœç´¢ `toUserDisplayName`(**å¿…é¡»ä¼ nickname**,如"ç«æ˜Ÿäºº")→ 点æœç´¢ç»“æžœ → 找 `bkk` 容器内的 EditText 输入文本 → 点 `bql` å‘逿Œ‰é’®ã€‚纯 UI 自动化,绕开所有åå°„ / hook 失败。 | |
| 136 | +- **消æ¯è·Ÿè¸ª**:`android-wechat-tracker-sqlcipher.js` 直接轮询解密åŽçš„ `EnMicroMsg.db`(SQLCipher) | |
| 137 | +- **自动回å¤**:`auto-reply.js` ç»“åˆ tracker + broadcast,在 Pixel 上独立è¿è¡Œ | |
| 138 | +- **æ•°æ®å¯¼å…¥**:`wechat-importer.js` / `wechat-reader.js` å°†è”系人和èŠå¤©è®°å½•导入知识图谱 | |
| 139 | + | |
| 140 | +> [!WARNING] 关于其他两æ¡å·²åºŸå¼ƒçš„å‘é€è·¯å¾„ | |
| 141 | +> - **LSPosed åå°„ API**(`HermesWeChatModule.sendTextMessageDirect`):监å¬åŒä¸€å¹¿æ’但走 `u01.r1.a → m1.a()`。`Send result: true` 是**误报**——`builder.a()` åŽä¸»çº¿ç¨‹å¼‚æ¥æäº¤è§¦å‘ null 异常被 swallow,消æ¯ä¸è¿› `message` 表。 | |
| 142 | +> - **Frida daemon 队列**(`android-wechat-sender-frida.js` + `wechat_plain/scripts/frida-daemon/frida_daemon.js`):Frida 17 + 微信 8.0.72 注入大体积 bundle 触å‘å调试,微信进程崩溃,已废弃。 | |
| 143 | +> 这两æ¡è·¯å¾„的命å让人误解;真æ£èƒ½æŠŠæ¶ˆæ¯æŽ¨è¿› message è¡¨çš„åªæœ‰ A11y service。 | |
| 144 | + | |
| 145 | +> [!IMPORTANT] A11y service å¯ç”¨æ–¹å¼ | |
| 146 | +> `enabled_accessibility_services` 系统设置易被关é—ã€‚æ£€æŸ¥ä¸Žé‡æ–°å¯ç”¨ï¼š | |
| 147 | +> ```bash | |
| 148 | +> adb shell "settings get secure enabled_accessibility_services" | |
| 149 | +> 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'" | |
| 150 | +> ``` | |
| 151 | + | |
| 152 | +è¯¦è§ `server/services/wechat/README.md`。 | |
| 153 | + | |
| 105 | 154 | ## 注æ„事项 |
| 106 | 155 | |
| 107 | 156 | - `server/src/standalone-entry.js` çš„ `loadConfig()` 优先读 `backends.json` + `agents.json`ï¼Œæ— éœ€æ•°æ®åº“。 |
| 108 | 157 | - `prompt-builder.js` çš„ `getSystemStateXML()` åªè¿”回时间戳(公å¸/OKR 层已移除)。 |
| 109 | 158 | - Agent 目录内放 `SKILL.md` 文件会绕过åè®®å°è£…,直接作为系统æç¤ºä½¿ç”¨ã€‚ |
| 159 | +- `server/index.js` åŒ…å«æŽ§åˆ¶å—符净化ä¸é—´ä»¶ï¼šAgent 通过 curl å‘é€çš„ JSON body å¯èƒ½å«åŽŸå§‹æ¢è¡Œç¬¦ï¼Œè‹¥è§£æžå¤±è´¥ä¼šè‡ªåŠ¨æ¸…ç† `\x00-\x1F` 控制å—符åŽé‡è¯•。 | |
| 110 | 160 | |
| 111 | 161 | > [!WARNING] (Agentic Self-Correction Loop Prevention) |
| 112 | 162 | > **Master Agent 必读**:在 `Bash` tool ä¸ä½¿ç”¨å«å¤šé‡åŒå¼•å·çš„夿‚命令(如 `curl -d '{"id":"lemonv"}'`)时,JSON 转义容易崩溃并陷入æ»å¾ªçŽ¯ã€‚ | ... | ... |
server/services/wechat/README.md
| ... | ... | @@ -2,7 +2,12 @@ |
| 2 | 2 | |
| 3 | 3 | 本目录提供从 **Mac** 和 **Pixel 手机** 两个端点与微信进行交互的能力:发送消息、实时跟踪收发消息。 |
| 4 | 4 | |
| 5 | -底层依赖自研的 **LSPosed/Xposed 模块**(`HermesWeChatSendMsg`),通过 Hook 微信内部 API 实现消息发送,并拦截日志实现消息跟踪。 | |
| 5 | +底层依赖自研的 Android 模块 `HermesWeChatSendMsg`(位于 `wechat_plain/scripts/lsposed-wechat-sendmsg`): | |
| 6 | + | |
| 7 | +- **发送**:模块内的 `HermesAccessibilityService` 监听广播 `com.hermes.wechat.SEND_MSG`,触发后**纯 UI 自动化**——启动微信 → 搜索 displayName → 进入聊天 → 设置 EditText → 点发送按钮。这是当前唯一能把消息真正写入 message 表的路径。 | |
| 8 | +- **跟踪**:通过 sqlcipher 轮询解密后的 `EnMicroMsg.db`(默认 key `e03ce81`),或备用方式 tail LSPosed 日志。 | |
| 9 | + | |
| 10 | +> ⚠️ 模块同名的 LSPosed 入口类 `HermesWeChatModule`(反射 NetSceneSendMsg)以及 Frida daemon 队列(`android-wechat-sender-frida.js`)已确认**无法把消息真正发出去**——前者 `Send result: true` 是误报、后者注入会触发微信反调试崩溃。所有 sender 调用最终都汇聚到 `am broadcast` → A11y service 这一条路径。 | |
| 6 | 11 | |
| 7 | 12 | --- |
| 8 | 13 | |
| ... | ... | @@ -57,17 +62,7 @@ |
| 57 | 62 | |
| 58 | 63 | ## 模块原理 |
| 59 | 64 | |
| 60 | -### Hook 点(WeChat 8.0.72) | |
| 61 | - | |
| 62 | -| 目标类 | 方法 | 作用 | | |
| 63 | -|--------|------|------| | |
| 64 | -| `ExtContentProviderBase` | `h()` → return 1 | 绕过 appId + UID + 签名校验 | | |
| 65 | -| `ExtContentProviderBase` | `b()` → return true | 绕过登录状态检查 | | |
| 66 | -| `AESUtil` | `b(long)` → 返回原值 | 跳过联系人 ID 加密 | | |
| 67 | -| `AESUtil` | `a(String)` → 返回原值 | 跳过联系人 ID 解密 | | |
| 68 | -| `MsgInfo` (f9) | `c1(String)` | 监控消息内容变更,记录日志 | | |
| 69 | - | |
| 70 | -### 消息发送调用链 | |
| 65 | +### 主路径:A11y UI 自动化(实际工作的) | |
| 71 | 66 | |
| 72 | 67 | ``` |
| 73 | 68 | 外部请求 |
| ... | ... | @@ -77,40 +72,96 @@ |
| 77 | 72 | └── am broadcast ────────────────────────────┘ |
| 78 | 73 | │ |
| 79 | 74 | ▼ |
| 80 | - 微信主进程 BroadcastReceiver | |
| 75 | + HermesAccessibilityService (BIND_ACCESSIBILITY_SERVICE) | |
| 81 | 76 | │ |
| 82 | - ┌────────────────────┴────────────────────┐ | |
| 83 | - │ │ | |
| 84 | - ▼ ▼ | |
| 85 | - sendTextMessage() sendTextMessageDirect() | |
| 86 | - (官方 ContentProvider) (反射内部 API) | |
| 87 | - │ │ | |
| 88 | - │ u01.r1.a(toUser) → Builder │ | |
| 89 | - │ builder.e(content) │ | |
| 90 | - │ builder.h(0) │ | |
| 91 | - │ builder.a() → u01.m1 │ | |
| 92 | - │ m1.a() // 必须在主线程调用 │ | |
| 93 | - │ │ | |
| 94 | - └─────────────────────────────────────────┘ | |
| 77 | + ▼ | |
| 78 | + 1. startActivity(LauncherUI, FLAG_ACTIVITY_CLEAR_TASK) | |
| 95 | 79 | │ |
| 96 | 80 | ▼ |
| 97 | - 微信服务器 | |
| 81 | + 2. 等 3.5s 后判断是否已在目标聊天页 | |
| 82 | + (id=obn 的标题 == toUserDisplayName?) | |
| 83 | + │ | |
| 84 | + ┌─────────────否─────────┴────────────是─────────┐ | |
| 85 | + ▼ ▼ | |
| 86 | + 3a. 点击搜索按钮 (id=jha) 直接到步骤 5 | |
| 87 | + 3b. 在搜索框 (id=d98) ACTION_SET_TEXT | |
| 88 | + = toUserDisplayName (微信 nickname,必须是 | |
| 89 | + "火星人" 而不是 "yibooo") | |
| 90 | + 3c. 在搜索结果中找匹配文本的 clickable ancestor 并点击 | |
| 91 | + │ | |
| 92 | + ▼ | |
| 93 | + 4. 等待聊天界面加载 (id=bkk 容器存在) | |
| 94 | + │ | |
| 95 | + ▼ | |
| 96 | + 5. 在 bkk 内的 EditText ACTION_SET_TEXT = content | |
| 97 | + │ | |
| 98 | + ▼ | |
| 99 | + 6. 点击发送按钮 (id=bql,或 fallback 找文本 "发送") | |
| 100 | + │ | |
| 101 | + ▼ | |
| 102 | + 微信走自己正常的发送流程 → 服务器 → message 表 isSend=1 | |
| 98 | 103 | ``` |
| 99 | 104 | |
| 105 | +**关键 UI 节点 ID**(WeChat 8.0.72): | |
| 106 | + | |
| 107 | +| 用途 | resource-id | | |
| 108 | +|------|-------------| | |
| 109 | +| 主界面搜索按钮 | `com.tencent.mm:id/jha` | | |
| 110 | +| 搜索输入框 | `com.tencent.mm:id/d98` | | |
| 111 | +| 聊天输入框容器(FrameLayout,内含 EditText) | `com.tencent.mm:id/bkk` | | |
| 112 | +| 发送按钮 | `com.tencent.mm:id/bql` | | |
| 113 | +| 聊天页标题(验证进入了正确的会话) | `com.tencent.mm:id/obn` | | |
| 114 | + | |
| 115 | +代码位置:`wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java` | |
| 116 | + | |
| 117 | +### 已废弃路径 1:LSPosed 反射 NetSceneSendMsg | |
| 118 | + | |
| 119 | +模块同名入口类 `HermesWeChatModule` 通过 hook 反射调用: | |
| 120 | + | |
| 121 | +| 目标类 | 方法 | 作用 | | |
| 122 | +|--------|------|------| | |
| 123 | +| `ExtContentProviderBase` | `h()` → return 1 | 绕过 appId + UID + 签名校验 | | |
| 124 | +| `ExtContentProviderBase` | `b()` → return true | 绕过登录状态检查 | | |
| 125 | +| `AESUtil` | `b(long)` → 返回原值 | 跳过联系人 ID 加密 | | |
| 126 | +| `AESUtil` | `a(String)` → 返回原值 | 跳过联系人 ID 解密 | | |
| 127 | +| `MsgInfo` (f9) | `c1(String)` | 监控消息内容变更,记录日志 | | |
| 128 | + | |
| 129 | +调用链 `u01.r1.a(toUser) → builder.e(content) → builder.a() → m1.a()`。`sendTextMessageDirect` 同步返回 `true` 然后在主线程异步提交 NetScene——但目前主线程提交触发 `NullPointerException` 被 swallow,**消息不进 message 表**。日志特征: | |
| 130 | + | |
| 131 | +``` | |
| 132 | +[HermesWX] Send result: true | |
| 133 | +[HermesWX] NetScene submit on main thread failed: null | |
| 134 | +``` | |
| 135 | + | |
| 136 | +注:这些 hook(auth bypass / AESUtil)对 A11y 路径**没有影响**,是否保留无关紧要。 | |
| 137 | + | |
| 138 | +### 已废弃路径 2:Frida daemon 队列 | |
| 139 | + | |
| 140 | +`android-wechat-sender-frida.js` 写入 `~/.frida_queue/queue.jsonl`,由常驻微信进程的 frida daemon 读取并反射调用 `u01.r1.a`。Frida 17 + 微信 8.0.72 注入会触发反调试,进程崩溃。已不在生产使用。 | |
| 141 | + | |
| 100 | 142 | ### 跨进程通信 |
| 101 | 143 | |
| 102 | -模块代码运行在 **微信主进程**(通过 LSPosed 注入),ContentProvider 运行在 **模块 APK 自身进程**。两者通过 Android 广播通信: | |
| 144 | +`HermesAccessibilityService` 运行在**模块 APK 自身进程**,由系统的 AccessibilityManager 绑定。外部进程通过 Android 广播(`com.hermes.wechat.SEND_MSG`)触发: | |
| 103 | 145 | |
| 104 | 146 | ``` |
| 105 | -hermes / adb shell content → 模块 Provider 进程 → 发送广播 | |
| 106 | - │ | |
| 107 | - 微信主进程(注入的模块) | |
| 108 | - │ | |
| 109 | - 广播接收器 → NetSceneSendMsg | |
| 110 | - │ | |
| 111 | - 微信服务器 | |
| 147 | +hermes / adb shell content / am broadcast | |
| 148 | + │ | |
| 149 | + ▼ | |
| 150 | + Android 系统 broadcast manager | |
| 151 | + │ | |
| 152 | + ▼ | |
| 153 | + HermesAccessibilityService (模块进程) | |
| 154 | + │ | |
| 155 | + │ AccessibilityNodeInfo / performAction | |
| 156 | + ▼ | |
| 157 | + 微信主进程的 UI (作为前台被自动化点击) | |
| 158 | + │ | |
| 159 | + ▼ | |
| 160 | + 微信走自己正常的发送流程 → 服务器 | |
| 112 | 161 | ``` |
| 113 | 162 | |
| 163 | +A11y service 必须由用户在「设置 → 辅助功能」中手动启用,或者用 root 通过 `settings put secure enabled_accessibility_services` 启用。重启 / 系统更新 / 应用更新都可能导致它被关闭。 | |
| 164 | + | |
| 114 | 165 | --- |
| 115 | 166 | |
| 116 | 167 | ## 文件清单 |
| ... | ... | @@ -119,10 +170,10 @@ hermes / adb shell content → 模块 Provider 进程 → 发送广播 |
| 119 | 170 | |------|------|--------| |
| 120 | 171 | | `wechat-sender.js` | 发送消息(adb) | Mac | |
| 121 | 172 | | `wechat-tracker.js` | 跟踪消息(adb) | Mac | |
| 173 | +| `android-wechat-sender.js` | **主推** 发送消息(`am broadcast` → A11y service UI 自动化) | Pixel | | |
| 122 | 174 | | `android-wechat-tracker-sqlcipher.js` | **主推** 跟踪消息(sqlcipher 轮询 DB) | Pixel | |
| 123 | -| `android-wechat-sender-frida.js` | **主推** 发送消息(Frida daemon 队列) | Pixel | | |
| 124 | -| `android-wechat-sender.js` | 备用发送消息(am broadcast / content insert) | Pixel | | |
| 125 | 175 | | `android-wechat-tracker.js` | 备用跟踪消息(tail -f LSPosed 日志) | Pixel | |
| 176 | +| `android-wechat-sender-frida.js` | ⚠️ **已废弃**:Frida daemon 队列。Frida 17 + 微信 8.0.72 注入会崩溃,未在生产使用 | Pixel | | |
| 126 | 177 | | `auto-reply.js` | Pixel 自动回复 plugin(hermes-agent API) | Pixel | |
| 127 | 178 | | `index.js` | 统一入口,自动检测环境 | 通用 | |
| 128 | 179 | | `test-phone.js` | 手机端一键测试 | Pixel | |
| ... | ... | @@ -159,13 +210,22 @@ adb install app/build/outputs/apk/release/app-release.apk |
| 159 | 210 | # 3. 强行停止微信,重新打开 |
| 160 | 211 | ``` |
| 161 | 212 | |
| 162 | -### 3. 验证模块工作 | |
| 213 | +### 3. 启用 AccessibilityService(必须) | |
| 163 | 214 | |
| 164 | 215 | ```bash |
| 165 | -adb shell su -c "logcat -s HermesWX:D | grep 'Send broadcast receiver registered'" | |
| 166 | -# 应看到: [HermesWX] Send broadcast receiver registered | |
| 216 | +# 启用 A11y service | |
| 217 | +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'" | |
| 218 | + | |
| 219 | +# 验证已 bind | |
| 220 | +adb shell "dumpsys accessibility | grep -A3 Hermes" | |
| 221 | +# 应看到: Enabled services:{{com.hermes.wechat.xposed/.../HermesAccessibilityService}} | |
| 222 | + | |
| 223 | +# 验证模块(可选,对 A11y 路径不是必需) | |
| 224 | +adb shell "su -c \"logcat -s HermesA11y:* -d -t 30\"" | |
| 167 | 225 | ``` |
| 168 | 226 | |
| 227 | +A11y service 一旦关闭(重启 / 系统更新 / 用户在设置中关闭),发送就会失效。`enabled_accessibility_services = null` 是最常见的故障原因。 | |
| 228 | + | |
| 169 | 229 | --- |
| 170 | 230 | |
| 171 | 231 | ## Mac 端使用 |
| ... | ... | @@ -298,10 +358,12 @@ tracker.startPolling(3000); |
| 298 | 358 | |
| 299 | 359 | | 现象 | 原因 | 解决 | |
| 300 | 360 | |------|------|------| |
| 301 | -| `content insert` 无响应 | 广播未注册 | 检查 logcat 中 `Send broadcast receiver registered` | | |
| 302 | -| `Send result: false` | 内部 API 调用失败 | 确认 WeChat 版本匹配,检查类名混淆 | | |
| 303 | -| `m1.a() return type` 异常 | 微信版本升级 | 重新反编译 APK,更新 `CLS_SEND_MSG_FACTORY` | | |
| 304 | -| 模块未出现在 Vector Manager | APK 未签名 | 重新签名安装 | | |
| 361 | +| 消息没进 `message` 表 | A11y service 未启用 | `adb shell "settings get secure enabled_accessibility_services"`,为 `null` 就用上面的命令重新启用 | | |
| 362 | +| A11y 日志显示 `Root package=com.termux` 然后退出 | 广播触发时微信不在前台,启动 intent 被前台 termux 抢占 | broadcast 前先 `adb shell am start -n com.tencent.mm/.ui.LauncherUI` | | |
| 363 | +| `Search button not found` | displayName 字段空 / 错误,或当前页面不是 LauncherUI | broadcast 必须传 `--es toUserDisplayName`,且值必须是 WeChat **nickname**(如 `火星人`),不能是 wxid | | |
| 364 | +| `No clickable search result found` | nickname 写错 / 联系人不在好友列表 | 在微信里直接搜索这个 nickname 验证是否能找到 | | |
| 365 | +| `Send result: true` 但消息没发出去 | 这是 LSPosed 反射路径的**误报**,正常 | 看 A11y 日志而不是 HermesWX 日志 | | |
| 366 | +| 模块未出现在 LSPosed Manager | APK 未签名 | 重新签名安装 | | |
| 305 | 367 | |
| 306 | 368 | ### Mac 端 |
| 307 | 369 | ... | ... |
server/services/wechat/SKILL.md
| ... | ... | @@ -7,48 +7,73 @@ |
| 7 | 7 | ## 环境要求 |
| 8 | 8 | |
| 9 | 9 | 1. **Root 权限** — 必须已 root(Magisk 30.7+) |
| 10 | -2. **LSPosed/Vector 模块** — HermesWeChatSendMsg 已启用,微信在作用域内 | |
| 11 | -3. **微信主进程运行中** | |
| 10 | +2. **HermesWeChatSendMsg 模块已安装**(lsposed-wechat-sendmsg APK) | |
| 11 | +3. **AccessibilityService 已启用** — 这是最容易踩的坑,详见下文 | |
| 12 | +4. **微信主进程运行中且在前台**(广播触发时 termux/其他 app 在前台会导致 A11y service 拿到错误的 root) | |
| 13 | + | |
| 14 | +### 启用 / 验证 AccessibilityService | |
| 15 | + | |
| 16 | +```bash | |
| 17 | +# 启用 | |
| 18 | +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'" | |
| 19 | + | |
| 20 | +# 验证 | |
| 21 | +adb shell "settings get secure enabled_accessibility_services" | |
| 22 | +# 应输出: com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService | |
| 23 | + | |
| 24 | +adb shell "dumpsys accessibility | grep Hermes" | |
| 25 | +# 应看到 Bound services 和 Enabled services 都包含 HermesAccessibilityService | |
| 26 | +``` | |
| 12 | 27 | |
| 13 | 28 | ## 文件位置 |
| 14 | 29 | |
| 15 | 30 | ``` |
| 16 | 31 | server/services/wechat/ |
| 32 | +├── android-wechat-sender.js # **主推** 发送(am broadcast → A11y service) | |
| 17 | 33 | ├── android-wechat-tracker-sqlcipher.js # **主推** 消息跟踪(sqlcipher 轮询 DB) |
| 18 | -├── android-wechat-sender-frida.js # **主推** 消息发送(Frida daemon 队列) | |
| 19 | -├── android-wechat-sender.js # 备用发送(am broadcast / content insert) | |
| 20 | 34 | ├── android-wechat-tracker.js # 备用跟踪(tail -f LSPosed 日志) |
| 35 | +├── android-wechat-sender-frida.js # ⚠️ 已废弃(Frida daemon 队列) | |
| 21 | 36 | ├── auto-reply.js # 自动回复主入口 |
| 22 | 37 | └── wechat-reader.js # 数据库读取 |
| 23 | 38 | ``` |
| 24 | 39 | |
| 25 | 40 | ## 发送消息 |
| 26 | 41 | |
| 27 | -### 方式1:Frida Daemon(推荐) | |
| 28 | - | |
| 29 | -需要 frida-server 运行且 frida_daemon.js 已注入微信进程: | |
| 42 | +### 唯一可用方式:am broadcast → A11y UI 自动化 | |
| 30 | 43 | |
| 31 | 44 | ```js |
| 32 | -const { sendTextMessageFrida } = require('./android-wechat-sender-frida'); | |
| 45 | +const { sendTextMessageBroadcast } = require('./android-wechat-sender'); | |
| 33 | 46 | |
| 34 | -const r = await sendTextMessageFrida('wxid_xxxxxx', '你好,来自 hermes'); | |
| 47 | +// ⚠️ toUser = wxid(写入 message 表用),toUserDisplayName = WeChat nickname(A11y 搜索用,必须传) | |
| 48 | +const r = await sendTextMessageBroadcast('yibooo', '你好,来自 hermes', { toUserDisplayName: '火星人' }); | |
| 35 | 49 | console.log(r); // { ok: true } |
| 36 | -// 写入 /data/local/tmp/frida_daemon/queue.jsonl,daemon 异步消费 | |
| 37 | 50 | ``` |
| 38 | 51 | |
| 39 | -### 方式2:ContentProvider / Broadcast(备用) | |
| 52 | +底层等价于: | |
| 40 | 53 | |
| 41 | -```js | |
| 42 | -const { sendTextMessage, sendTextMessageBroadcast } = require('./android-wechat-sender'); | |
| 54 | +```bash | |
| 55 | +adb shell "am broadcast -a com.hermes.wechat.SEND_MSG \ | |
| 56 | + --es toUser yibooo \ | |
| 57 | + --es toUserDisplayName 火星人 \ | |
| 58 | + --es content '你好'" | |
| 59 | +``` | |
| 43 | 60 | |
| 44 | -// ContentProvider(与 Mac 端 API 一致) | |
| 45 | -const r1 = await sendTextMessage('filehelper', '你好'); | |
| 46 | -console.log(r1); // { ok: true, uri: 'content://.../send/0' } | |
| 61 | +A11y service 收到后会:启动微信 → 搜索 `火星人` → 进入聊天 → 写文本 → 点发送按钮,全程约 5-8 秒。 | |
| 47 | 62 | |
| 48 | -// 直接广播(绕过 Provider 进程) | |
| 49 | -const r2 = await sendTextMessageBroadcast('wxid_xxxxxx', '测试消息'); | |
| 50 | -console.log(r2); // { ok: true } | |
| 51 | -``` | |
| 63 | +### 关键约束 | |
| 64 | + | |
| 65 | +1. **必须传 `toUserDisplayName`**(WeChat nickname)。如果只传 `toUser` (wxid),A11y 会用 wxid 去搜——绝大多数情况搜不到,发送失败。 | |
| 66 | +2. **微信必须在前台**或至少不被其他高优先级 app(如全屏游戏)阻挡。如果广播触发时手机停在 termux/其他 app,A11y 的 `startActivity(LauncherUI)` 可能被忽略。 | |
| 67 | +3. **不要短时间内连发**。A11y 流程有 3500ms + 800ms + 800ms + 2500ms + 400ms 的多段 delay,单次约 8s。 | |
| 68 | + | |
| 69 | +### 已废弃路径(不要用) | |
| 70 | + | |
| 71 | +| 文件 / API | 状态 | | |
| 72 | +|----|----| | |
| 73 | +| `sendTextMessageFrida` (`android-wechat-sender-frida.js`) | Frida 17 + 微信 8.0.72 注入崩溃,daemon 起不来 | | |
| 74 | +| `sendTextMessage` ContentProvider 路径 | 触发的 LSPosed `HermesWeChatModule.sendTextMessageDirect` 返回 `true` 但消息不进 DB(主线程 NetScene 提交 NPE 被 swallow) | | |
| 75 | + | |
| 76 | +注:`sendTextMessage`(ContentProvider)和 `sendTextMessageBroadcast`(broadcast)**最终广播都被 A11y service 接收并执行**,所以从外部看上去 ContentProvider 路径也能工作——但只是因为 A11y 路径在后台兜住了。建议直接用 broadcast 接口,语义最清晰。 | |
| 52 | 77 | |
| 53 | 78 | ### 批量发送 |
| 54 | 79 | |
| ... | ... | @@ -126,11 +151,12 @@ const msgs = await tracker.fetchRecent(20); // 仅 AndroidWechatTracker 支持 |
| 126 | 151 | |
| 127 | 152 | | 现象 | 原因 | 解决 | |
| 128 | 153 | |------|------|------| |
| 129 | -| `exit 1: permission denied` | 未 root 或 su 不可用 | 检查 Magisk root 状态 | | |
| 130 | -| `content: not found` | 不在 Android 环境 | 确保在 Termux 或 Debian 中用 `/system/bin/content` | | |
| 131 | -| 发送成功但对方没收到 | 广播被拦截或微信没在前台 | 确保微信主进程存活,检查 LSPosed 日志 | | |
| 132 | -| 跟踪不到消息 | 日志级别不够 | 在 Vector Manager → 日志 中设置详细级别 | | |
| 133 | -| `am broadcast` 提示 `Background execution not allowed` | Android 10+ 后台限制 | 使用 `content insert` 方式替代 | | |
| 154 | +| 接口返回 `ok: true` 但 yibooo 没收到 / `message` 表没新行 | A11y service 没启用 | 见上文「启用 / 验证 AccessibilityService」 | | |
| 155 | +| A11y 日志 `Root package=com.termux` 后直接 reset | broadcast 触发时微信不在前台 | 先 `adb shell am start -n com.tencent.mm/.ui.LauncherUI` | | |
| 156 | +| `Search button not found` | 当前不是 LauncherUI(可能停在另一个聊天里) | A11y 内置了 relaunch 兜底,等下一次会自动恢复;或手动回到主界面 | | |
| 157 | +| `No clickable search result found` | nickname 错 / 不是好友 | 验证 `toUserDisplayName` 是 WeChat 里的实际昵称 | | |
| 158 | +| `[HermesWX] Send result: true` 出现在日志里但消息没发出 | LSPosed 反射路径的误报,**正常现象** | 看 `HermesA11y:*` 日志才反映真实情况 | | |
| 159 | +| 跟踪不到消息 | SQLCipher key 失效 / DB 路径变了 | 重新捕获 key(见 `wechat_plain/scripts/README_Android.md`) | | |
| 134 | 160 | |
| 135 | 161 | ## 与 Mac 端区别 |
| 136 | 162 | ... | ... |
server/services/wechat/android-wechat-sender-frida.js
| 1 | 1 | /** |
| 2 | - * android-wechat-sender-frida.js — 基于 Frida Daemon 的微信消息发送 | |
| 2 | + * android-wechat-sender-frida.js — ⚠️ 已废弃,保留仅供参考 | |
| 3 | 3 | * |
| 4 | - * 原理:写入队列文件 /data/local/tmp/frida_daemon/queue.jsonl | |
| 5 | - * 由常驻微信进程的 Frida daemon 读取并发送 | |
| 4 | + * 原设计:写入队列文件由常驻微信进程的 Frida daemon 读取并发送。 | |
| 6 | 5 | * |
| 7 | - * 前提: | |
| 8 | - * 1. frida-server 运行中 | |
| 9 | - * 2. frida_daemon.js 已注入微信进程(通过 Mac 端 frida CLI) | |
| 10 | - * 3. 队列目录 /data/local/tmp/frida_daemon/ 存在且可写 | |
| 6 | + * 实测在 WeChat 8.0.72 + Frida 17 上 daemon 注入会触发微信反调试, | |
| 7 | + * 进程崩溃。当前生产路径走 am broadcast → HermesAccessibilityService UI 自动化, | |
| 8 | + * 见 android-wechat-sender.js (sendTextMessageBroadcast)。 | |
| 11 | 9 | * |
| 12 | - * 注意:发送为 fire-and-forget,daemon 会异步处理队列。 | |
| 13 | - * 如需确认送达,可通过 sqlcipher 查询 message 表。 | |
| 10 | + * 不要在新代码中调用本模块的导出函数。 | |
| 14 | 11 | */ |
| 15 | 12 | |
| 16 | 13 | const fs = require('fs'); |
| 17 | 14 | |
| 18 | -const QUEUE_DIR = '/data/local/tmp/frida_daemon'; | |
| 15 | +// 所有 Pixel 端文件统一放在 Termux 可访问目录,避免 /data/local/tmp 权限问题 | |
| 16 | +const HOME_DIR = '/data/data/com.termux/files/home/hermes-wechat'; | |
| 17 | +const QUEUE_DIR = HOME_DIR + '/.frida_queue'; | |
| 19 | 18 | const QUEUE_FILE = QUEUE_DIR + '/queue.jsonl'; |
| 20 | 19 | |
| 21 | 20 | function ensureDir() { | ... | ... |
wechat_plain/scripts/local-testkit/docs/HERMES_WECHAT_ARCHITECTURE.md
| ... | ... | @@ -23,7 +23,7 @@ |
| 23 | 23 | - **hermes-agent** (lemonPCRM wrapper) 是独立的 OpenAI-compatible AI 服务,负责 LLM 推理 |
| 24 | 24 | - 两者通过 HTTP API (`POST /v1/chat/completions`) 通信 |
| 25 | 25 | - **SQLCipher DB 轮询** 替代 LSPosed 日志 tail,更稳定可靠 |
| 26 | -- **Frida Daemon** 替代 am broadcast,绕过 Android 10+ 后台限制 | |
| 26 | +- **`am broadcast` → HermesAccessibilityService UI 自动化** 是当前唯一稳定可用的发送方式(Frida 注入 / LSPosed 反射 NetSceneSendMsg 都在 WeChat 8.0.72 上失败) | |
| 27 | 27 | |
| 28 | 28 | --- |
| 29 | 29 | |
| ... | ... | @@ -49,16 +49,18 @@ |
| 49 | 49 | │ │ ┌─────────┴─────────────────────────────────────────────────────────┐ │ │ |
| 50 | 50 | │ │ │ auto-reply.js (Node.js) │ │ │ |
| 51 | 51 | │ │ │ ┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ |
| 52 | -│ │ │ │ SQLCipher │→ │ 新消息 │→ │ HTTP │→ │ Frida Queue │ │ │ │ | |
| 53 | -│ │ │ │ DB 轮询 │ │ 检测 │ │ hermes- │ │ /tmp/frida_ │ │ │ │ | |
| 54 | -│ │ │ │ │ │ │ │ agent │ │ daemon/ │ │ │ │ | |
| 52 | +│ │ │ │ SQLCipher │→ │ 新消息 │→ │ HTTP │→ │ am broadcast │ │ │ │ | |
| 53 | +│ │ │ │ DB 轮询 │ │ 检测 │ │ hermes- │ │ SEND_MSG │ │ │ │ | |
| 54 | +│ │ │ │ │ │ │ │ agent │ │ │ │ │ │ | |
| 55 | 55 | │ │ │ └──────────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ │ |
| 56 | 56 | │ │ │ ↑ │ │ │ │ |
| 57 | 57 | │ │ │ │ │ │ │ │ |
| 58 | -│ │ │ ┌──────────────┐ ┌──────────┴─────┐ │ │ │ | |
| 59 | -│ │ │ │ sqlcipher │ │ Frida Daemon │ │ │ │ | |
| 60 | -│ │ │ │ 直接查询 │ │ (注入微信进程) │ │ │ │ | |
| 61 | -│ │ │ └──────────────┘ └────────────────┘ │ │ │ | |
| 58 | +│ │ │ ┌──────────────┐ ┌──────────────┴───────┐ │ │ │ | |
| 59 | +│ │ │ │ sqlcipher │ │ HermesA11yService │ │ │ │ | |
| 60 | +│ │ │ │ 直接查询 │ │ (lsposed-wechat- │ │ │ │ | |
| 61 | +│ │ │ │ │ │ sendmsg 模块进程) │ │ │ │ | |
| 62 | +│ │ │ │ │ │ UI 自动化点击微信 │ │ │ │ | |
| 63 | +│ │ │ └──────────────┘ └──────────────────────┘ │ │ │ | |
| 62 | 64 | │ │ └──────────────────────────────────────────────────────────────────┘ │ │ |
| 63 | 65 | │ │ ↑ │ │ |
| 64 | 66 | │ │ │ │ │ |
| ... | ... | @@ -81,8 +83,9 @@ |
| 81 | 83 | │ │ │ ~/.termux/boot/start-hermes.sh │ │ │ |
| 82 | 84 | │ │ └─────────────────────────────────────────────────────────────────┘ │ │ |
| 83 | 85 | │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ |
| 84 | -│ │ │ Frida Server (可选,用于注入 daemon) │ │ │ | |
| 85 | -│ │ │ /data/local/tmp/frida-server │ │ │ | |
| 86 | +│ │ │ HermesWeChatSendMsg APK (含 A11y service) │ │ │ | |
| 87 | +│ │ │ /data/app/com.hermes.wechat.xposed-*/base.apk │ │ │ | |
| 88 | +│ │ │ 必须在 设置 → 辅助功能 中启用 HermesAccessibilityService │ │ │ | |
| 86 | 89 | │ │ └─────────────────────────────────────────────────────────────────┘ │ │ |
| 87 | 90 | │ └───────────────────────────────────────────────────────────────────────┘ │ |
| 88 | 91 | │ ↑ │ │ |
| ... | ... | @@ -134,7 +137,7 @@ |
| 134 | 137 | 8. auto-reply.js 提取 content 作为回复文本 |
| 135 | 138 | │ |
| 136 | 139 | ▼ |
| 137 | -9. 写入 /data/local/tmp/frida_daemon/queue.jsonl | |
| 140 | +9. 写入 /data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl | |
| 138 | 141 | │ |
| 139 | 142 | ▼ |
| 140 | 143 | 10. Frida Daemon(注入微信进程)读取队列,调用微信内部 API 发送消息 |
| ... | ... | @@ -296,7 +299,7 @@ Frida Daemon 注入微信主进程,通过 Java.perform 调用微信内部 API |
| 296 | 299 | ``` |
| 297 | 300 | auto-reply.js |
| 298 | 301 | │ |
| 299 | - ├── 写入队列文件 /data/local/tmp/frida_daemon/queue.jsonl | |
| 302 | + ├── 写入队列文件 /data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl | |
| 300 | 303 | │ {"talker": "filehelper", "content": "回复内容", "id": "msg_xxx"} |
| 301 | 304 | │ |
| 302 | 305 | ▼ |
| ... | ... | @@ -346,8 +349,8 @@ Java.perform → 调用微信内部发送 API |
| 346 | 349 | #!/bin/bash |
| 347 | 350 | |
| 348 | 351 | # 启动 frida-server |
| 349 | -if [ -f "/data/local/tmp/frida-server" ]; then | |
| 350 | - su -c "/data/local/tmp/frida-server &" | |
| 352 | +if [ -f "/data/data/com.termux/files/home/hermes-wechat/bin/frida-server" ]; then | |
| 353 | + su -c "/data/data/com.termux/files/home/hermes-wechat/bin/frida-server &" | |
| 351 | 354 | fi |
| 352 | 355 | |
| 353 | 356 | # 启动微信(如果未运行) |
| ... | ... | @@ -501,7 +504,7 @@ curl http://127.0.0.1:8642/v1/chat/completions \ |
| 501 | 504 | |
| 502 | 505 | ```bash |
| 503 | 506 | # 1. 确保 frida-server 运行 |
| 504 | -su -c "/data/local/tmp/frida-server &" | |
| 507 | +su -c "/data/data/com.termux/files/home/hermes-wechat/bin/frida-server &" | |
| 505 | 508 | |
| 506 | 509 | # 2. 注入 daemon 到微信进程(从 Mac 或手机端) |
| 507 | 510 | frida -U -n com.tencent.mm -l frida_daemon.js |
| ... | ... | @@ -557,7 +560,7 @@ chmod +x ~/.termux/boot/start-hermes.sh |
| 557 | 560 | **A**: |
| 558 | 561 | 1. 确认 frida-server 运行中:`ps aux | grep frida-server` |
| 559 | 562 | 2. 确认 frida_daemon.js 已注入微信进程 |
| 560 | -3. 检查队列文件权限:`ls -la /data/local/tmp/frida_daemon/queue.jsonl` | |
| 563 | +3. 检查队列文件权限:`ls -la /data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl` | |
| 561 | 564 | 4. 备用方案:回退到 `sendTextMessageBroadcast()` |
| 562 | 565 | |
| 563 | 566 | ### Q: 息屏后完全失效 | ... | ... |
wechat_plain/scripts/local-testkit/docs/README.md
| ... | ... | @@ -47,28 +47,41 @@ sqlcipher 直接查询 EnMicroMsg.db |
| 47 | 47 | └─────────────────────────┘ └─────────────────────────┘ |
| 48 | 48 | │ |
| 49 | 49 | ▼ |
| 50 | -┌─────────────────────────┐ ┌─────────────────────────┐ | |
| 51 | -│ 微信在前台? │ No →│ wakeWechat() 点亮+解锁 │ | |
| 52 | -└─────────────────────────┘ │ + am start 启动微信 │ | |
| 53 | - │Yes └─────────────────────────┘ | |
| 54 | - ▼ | |
| 55 | 50 | ┌─────────────────────────┐ |
| 56 | -│ sendTextMessageFrida │────→ 写入 /data/local/tmp/frida_daemon/queue.jsonl | |
| 57 | -│ (android-wechat-sender-frida) │ | |
| 58 | -└─────────────────────────┘ | |
| 51 | +│ 微信在前台? │ No → am start LauncherUI | |
| 52 | +└─────────────────────────┘ + (息屏时) wakeWechat() | |
| 53 | + │Yes | |
| 54 | + ▼ | |
| 55 | +┌──────────────────────────────────────────────────────────┐ | |
| 56 | +│ am broadcast -a com.hermes.wechat.SEND_MSG │ | |
| 57 | +│ --es toUser <wxid> │ | |
| 58 | +│ --es toUserDisplayName <nickname> ← 必传! │ | |
| 59 | +│ --es content <text> │ | |
| 60 | +└──────────────────────────────────────────────────────────┘ | |
| 59 | 61 | │ |
| 60 | 62 | ▼ |
| 61 | -┌─────────────────────────┐ | |
| 62 | -│ Frida Daemon (注入微信) │────→ Java.perform → 调用微信内部发送 API | |
| 63 | -└─────────────────────────┘ | |
| 63 | +┌──────────────────────────────────────────────────────────┐ | |
| 64 | +│ HermesAccessibilityService (lsposed-wechat-sendmsg 模块) │ | |
| 65 | +│ 1. startActivity(LauncherUI, CLEAR_TASK) │ | |
| 66 | +│ 2. 点搜索按钮 (id=jha) │ | |
| 67 | +│ 3. ACTION_SET_TEXT 输入 nickname → 点搜索结果 │ | |
| 68 | +│ 4. 进入聊天后 ACTION_SET_TEXT 写入 content │ | |
| 69 | +│ 5. 点发送按钮 (id=bql) │ | |
| 70 | +└──────────────────────────────────────────────────────────┘ | |
| 71 | + │ | |
| 72 | + ▼ | |
| 73 | + 微信走自己正常的发送流程 → 服务器 → message 表 isSend=1 | |
| 64 | 74 | ``` |
| 65 | 75 | |
| 66 | 76 | **优势**: |
| 67 | -- 不依赖 LSPosed hook(WeChat 8.0.72 上 Xposed hook 不稳定) | |
| 77 | +- 不依赖反射 hook 微信内部 NetSceneSendMsg(在 WeChat 8.0.72 上主线程提交持续 NPE 失败) | |
| 78 | +- 不依赖 Frida 注入(Frida 17 + 微信 8.0.72 注入会触发反调试 / 进程崩溃) | |
| 68 | 79 | - sqlcipher 直接查询,数据完整(含群聊名称、发送者等) |
| 69 | -- Frida 发送比 am broadcast 更可靠(绕过 Android 10+ 后台限制) | |
| 80 | +- 走 UI 是慢一点(~8s/条)但**唯一稳定可用** | |
| 70 | 81 | - 通过 hermes-agent API 享受完整的 Agent 编排、工具调用、记忆能力 |
| 71 | 82 | |
| 83 | +> ⚠️ **历史遗留命名警告**:`android-wechat-sender-frida.js` 中的 `sendTextMessageFrida` 写入 `.frida_queue/queue.jsonl` 的设计**已废弃**。同样地,README 早期版本把 Frida daemon 描述为主路径——那是错的。所有发送最终都走 `am broadcast` → A11y service。 | |
| 84 | + | |
| 72 | 85 | ### 备用流程:Python Daemon |
| 73 | 86 | |
| 74 | 87 | `hermes_wechat_daemon.py` 作为备用通道,当 Node.js plugin 不可用时运行: |
| ... | ... | @@ -174,16 +187,22 @@ curl http://127.0.0.1:8642/v1/chat/completions -X POST \ |
| 174 | 187 | -d '{"model":"hermes-agent","messages":[{"role":"user","content":"hi"}]}' |
| 175 | 188 | ``` |
| 176 | 189 | |
| 177 | -### 4. 启动 Frida Daemon(发送端依赖) | |
| 190 | +### 4. 启用 HermesAccessibilityService(发送端依赖) | |
| 191 | + | |
| 192 | +A11y service 是消息真正能发出去的唯一路径——务必确认启用: | |
| 178 | 193 | |
| 179 | 194 | ```bash |
| 180 | -# 确保 frida-server 在运行 | |
| 181 | -su -c "/data/local/tmp/frida-server &" | |
| 195 | +# 启用 | |
| 196 | +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'" | |
| 182 | 197 | |
| 183 | -# 注入 daemon 到微信进程(从 Mac 端执行) | |
| 184 | -frida -U -n com.tencent.mm -l frida_daemon.js | |
| 198 | +# 验证 | |
| 199 | +adb shell "settings get secure enabled_accessibility_services" | |
| 200 | +adb shell "dumpsys accessibility | grep -A3 Hermes" | |
| 201 | +# 应看到: Bound services / Enabled services 都包含 HermesAccessibilityService | |
| 185 | 202 | ``` |
| 186 | 203 | |
| 204 | +`enabled_accessibility_services` 在系统重启 / 微信或模块更新 / 用户在设置里关闭后会变成 `null`,发送会静默失败——把这个检查写进保活脚本里。 | |
| 205 | + | |
| 187 | 206 | ### 5. 启动 auto-reply plugin(推荐主流程) |
| 188 | 207 | |
| 189 | 208 | ```bash |
| ... | ... | @@ -256,11 +275,13 @@ python3 hermes_wechat_daemon.py --daemon |
| 256 | 275 | |
| 257 | 276 | | 问题 | 排查步骤 | |
| 258 | 277 | |------|---------| |
| 278 | +| 发送接口返回 ok 但 yibooo 没收到 / `message` 表没新行 | **最常见**:A11y service 未启用。`adb shell "settings get secure enabled_accessibility_services"` 若为 `null` 即是。重新启用见上文 | | |
| 279 | +| A11y 日志 `Root package=com.termux` 后退出 | 广播触发时微信不在前台。先 `am start -n com.tencent.mm/.ui.LauncherUI` 再发 broadcast | | |
| 280 | +| A11y 日志 `Search button not found` | 当前页面不是主界面 / nickname 错。检查 `toUserDisplayName` 必须是好友的 WeChat 昵称(如"火星人"),不是 wxid | | |
| 281 | +| `[HermesWX] Send result: true` 但 DB 没新消息 | LSPosed 反射路径的**误报**,是已知 bug。看 `HermesA11y:*` 日志才反映真实状态 | | |
| 259 | 282 | | auto-reply 收不到消息 | 检查 sqlcipher 能否查询:`su -c "sqlcipher /data/.../EnMicroMsg.db 'PRAGMA key=\\\"e03ce81\\\"; SELECT COUNT(*) FROM message;'"` | |
| 260 | 283 | | hermes-agent API 返回空 | 检查 hermes-agent 是否运行:`curl http://127.0.0.1:8642/v1/chat/completions ...` | |
| 261 | 284 | | 息屏后完全失效 | 确认电池优化已关闭;检查 `keepalive.sh` 是否运行;检查通知权限 | |
| 262 | -| 发送失败(Frida) | 确认 frida-server 运行中;确认 frida_daemon.js 已注入微信进程;检查队列文件 `/data/local/tmp/frida_daemon/queue.jsonl` | | |
| 263 | -| 发送失败(broadcast 备用) | 检查微信是否在前台;检查 LSPosed 模块是否启用 | | |
| 264 | 285 | | Python daemon 查询不到新消息 | 检查微信是否在前台;检查 `sqlcipher_key`;确认 WAL 已复制 | |
| 265 | 286 | | Claude CLI 报错 `root/sudo privileges` | hermes 必须以 proot 内 **ubuntu 用户** 运行,不能是 root | |
| 266 | 287 | ... | ... |
wechat_plain/scripts/lsposed-wechat-sendmsg/README.md
| 1 | -# Hermes WeChat SendMsg — LSPosed/Xposed Module | |
| 1 | +# Hermes WeChat SendMsg — LSPosed Module + AccessibilityService | |
| 2 | 2 | |
| 3 | -LSPosed 模块,目标 WeChat 8.0.72,用于绕过 ContentProvider 权限校验并暴露消息发送接口。 | |
| 3 | +针对 WeChat 8.0.72 的 Android 模块,包含两套发送实现: | |
| 4 | + | |
| 5 | +- ✅ **`HermesAccessibilityService`(生产路径)** — 监听广播 `com.hermes.wechat.SEND_MSG`,纯 UI 自动化:搜索联系人 → 输入文本 → 点发送按钮。**当前唯一稳定可用的发送方式**。 | |
| 6 | +- ❌ **`HermesWeChatModule`(已废弃,保留代码作记录)** — LSPosed 入口,hook `ExtContentProviderBase` / `AESUtil` 后反射调用 `u01.r1.a → m1.a()`。在 WeChat 8.0.72 上主线程 NetScene 提交持续 NPE 失败,`Send result: true` 是误报,消息不进 `message` 表。 | |
| 4 | 7 | |
| 5 | 8 | ## 功能 |
| 6 | 9 | |
| 7 | -1. **Auth Bypass** — Hook `ExtContentProviderBase.h()` 和 `b()`,使任意调用者通过权限校验 | |
| 8 | -2. **AESUtil Bypass** — 跳过联系人 ID 的加密/解密,直接使用明文 wxid | |
| 9 | -3. **消息发送** — 外部应用通过 ContentProvider 写入发送请求,模块在 WeChat 主进程内通过广播接收并调用内部 NetSceneSendMsg API 发送 | |
| 10 | -4. **消息监控** — Hook `MsgInfo.setContent()`,将收发消息实时记录到日志 | |
| 10 | +1. **A11y 自动化发送(主路径)** — `HermesAccessibilityService` 模拟用户在微信主界面搜索联系人、点进聊天、写文本、点发送按钮 | |
| 11 | +2. **Auth Bypass / AESUtil / MsgInfo hook(历史遗留)** — `HermesWeChatModule` 内的 LSPosed hook 对生产路径没有影响,可选保留 | |
| 12 | +3. **ContentProvider 暴露** — `HermesWeChatProvider` 把 `content insert` 翻译成广播,主要给非 root 的调用方用 | |
| 11 | 13 | |
| 12 | 14 | ## 项目结构 |
| 13 | 15 | |
| ... | ... | @@ -57,32 +59,57 @@ $ANDROID_HOME/build-tools/34.0.0/apksigner sign --ks hermes-debug.keystore --ks- |
| 57 | 59 | |
| 58 | 60 | ## 安装 |
| 59 | 61 | |
| 60 | -### 前提:LSPosed 框架(Zygisk 版) | |
| 62 | +### A11y 路径(生产,必做) | |
| 63 | + | |
| 64 | +1. 安装本模块 APK:`adb install app/build/outputs/apk/release/app-release.apk` | |
| 65 | +2. **启用 AccessibilityService**(关键,最容易踩的坑): | |
| 66 | + ```bash | |
| 67 | + 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'" | |
| 68 | + ``` | |
| 69 | + 或手动:设置 → 辅助功能 / 无障碍 → Hermes WeChat SendMsg → 启用 | |
| 70 | +3. 验证: | |
| 71 | + ```bash | |
| 72 | + adb shell "dumpsys accessibility | grep -A3 Hermes" | |
| 73 | + # 应看到 Bound services / Enabled services 都包含 HermesAccessibilityService | |
| 74 | + ``` | |
| 75 | + | |
| 76 | +A11y service 会在重启、微信更新、本模块更新、用户在设置里关闭后失效,发送将静默失败——把启用检查纳入保活脚本。 | |
| 77 | + | |
| 78 | +### LSPosed 模块路径(可选,目前无功能贡献) | |
| 61 | 79 | |
| 62 | -1. 确保 Magisk 已安装且 **Zygisk 已启用**(Magisk 设置 → Zygisk → 开) | |
| 63 | -2. 下载 [LSPosed Zygisk 版](https://github.com/JingMatrix/LSPosed/releases)(或 Vector 2.0+) | |
| 64 | -3. 在 Magisk 中安装 LSPosed 模块,重启手机 | |
| 65 | -4. 安装本模块 APK:`adb install app/build/outputs/apk/release/app-release.apk` | |
| 66 | -5. 打开 LSPosed Manager → 模块 → 启用 HermesWeChatSendMsg | |
| 67 | -6. **作用域**中选择 **微信(com.tencent.mm)** | |
| 68 | -7. 强行停止微信,重新打开 | |
| 80 | +如果同时希望 LSPosed hook 生效(虽然反射发送已坏,但 hook 本身对 A11y 没影响): | |
| 81 | + | |
| 82 | +1. 确保 Magisk 已安装且 **Zygisk 已启用** | |
| 83 | +2. 安装 [LSPosed Zygisk 版](https://github.com/JingMatrix/LSPosed/releases)(或 Vector 2.0+) | |
| 84 | +3. LSPosed Manager → 模块 → 启用 HermesWeChatSendMsg → 作用域选择 **微信(com.tencent.mm)** | |
| 85 | +4. 强行停止微信,重新打开 | |
| 69 | 86 | |
| 70 | 87 | ## 使用 |
| 71 | 88 | |
| 72 | -模块安装并启用后,可通过 `adb shell content` 命令与模块交互。 | |
| 89 | +### 发送文本消息(推荐方式:直接广播) | |
| 90 | + | |
| 91 | +```bash | |
| 92 | +adb shell "am broadcast -a com.hermes.wechat.SEND_MSG \ | |
| 93 | + --es toUser yibooo \ | |
| 94 | + --es toUserDisplayName 火星人 \ | |
| 95 | + --es content '你好,这是测试消息'" | |
| 96 | +``` | |
| 97 | + | |
| 98 | +字段说明: | |
| 99 | +- `toUser` — 接收者 wxid(如 `wxid_xxxxxx`、`yibooo`、`filehelper`)。**注意:A11y 流程不直接用这个值发送,但留着用于校验** | |
| 100 | +- `toUserDisplayName` — **必须传**:WeChat 里的 nickname(如 `火星人`、`文件传输助手`)。A11y 用它来在搜索框里输入并定位联系人。如果只传 wxid 通常搜不到 | |
| 101 | +- `content` — 消息内容 | |
| 73 | 102 | |
| 74 | -### 发送文本消息 | |
| 103 | +### 备用:ContentProvider insert | |
| 75 | 104 | |
| 76 | 105 | ```bash |
| 77 | 106 | adb shell content insert \ |
| 78 | 107 | --uri content://com.hermes.wechat.xposed.provider/send \ |
| 79 | - --bind toUser:s:filehelper \ | |
| 80 | - --bind content:s:"你好,这是测试消息" | |
| 108 | + --bind toUser:s:yibooo \ | |
| 109 | + --bind content:s:"你好" | |
| 81 | 110 | ``` |
| 82 | 111 | |
| 83 | -字段说明: | |
| 84 | -- `toUser` — 接收者 wxid(如 `wxid_xxxxxx`、`filehelper`)或群聊 ID | |
| 85 | -- `content` — 消息内容 | |
| 112 | +这条路径会**双广播**(同时触发 LSPosed 反射 + A11y),LSPosed 那条会失败但不影响最终结果;A11y 这条把消息真正发出去。但因为 `content insert` 不传 `toUserDisplayName`,A11y 会用 toUser 当 nickname 去搜,搜不到的情况下发送失败——所以推荐用 `am broadcast` 显式传 displayName。 | |
| 86 | 113 | |
| 87 | 114 | ### 查询发送状态 |
| 88 | 115 | |
| ... | ... | @@ -96,31 +123,63 @@ adb shell content query \ |
| 96 | 123 | ### 查看模块日志 |
| 97 | 124 | |
| 98 | 125 | ```bash |
| 99 | -adb logcat -s HermesWX:D LSPosed-Bridge:D | grep HermesWX | |
| 100 | -``` | |
| 126 | +# A11y 路径(看这个判断实际是否发送) | |
| 127 | +adb logcat -s HermesA11y:* | |
| 101 | 128 | |
| 102 | -或查看 Vector/LSPosed 日志: | |
| 103 | -```bash | |
| 129 | +# LSPosed 路径(已废弃,仅供参考) | |
| 104 | 130 | adb shell su -c "cat /data/adb/lspd/log/verbose_*.log | grep HermesWX | tail -20" |
| 105 | 131 | ``` |
| 106 | 132 | |
| 107 | -## 技术细节 | |
| 133 | +正常 A11y 成功日志: | |
| 134 | +``` | |
| 135 | +HermesA11y: Received send request: to=yibooo searchName=火星人 content=... | |
| 136 | +HermesA11y: Root package=com.tencent.mm | |
| 137 | +HermesA11y: Already in correct chat, typing directly (或 "Not in chat, starting search") | |
| 138 | +HermesA11y: Setting chat text: ... | |
| 139 | +HermesA11y: Clicking send button (by ID) | |
| 140 | +HermesA11y: State reset | |
| 141 | +``` | |
| 108 | 142 | |
| 109 | -### 跨进程通信 | |
| 143 | +## 技术细节 | |
| 110 | 144 | |
| 111 | -模块代码运行在 **WeChat 主进程** 中(通过 LSPosed 注入),ContentProvider 运行在 **模块 APK 自身进程** 中。两者通过 Android 广播通信: | |
| 145 | +### 进程拓扑 | |
| 112 | 146 | |
| 113 | 147 | ``` |
| 114 | -hermes / adb shell content → 模块 Provider 进程 → 发送广播 (com.hermes.wechat.SEND_MSG) | |
| 115 | - ↓ | |
| 116 | - WeChat 主进程(注入的模块) | |
| 117 | - ↓ | |
| 118 | - 广播接收器 → NetSceneSendMsg 内部 API | |
| 119 | - ↓ | |
| 120 | - 微信服务器 | |
| 148 | +外部调用方(adb / hermes / shell) | |
| 149 | + │ | |
| 150 | + │ am broadcast com.hermes.wechat.SEND_MSG | |
| 151 | + ▼ | |
| 152 | +Android system broadcast manager | |
| 153 | + │ | |
| 154 | + ├──────────────────────────┐ | |
| 155 | + │ │ | |
| 156 | + ▼ ▼ | |
| 157 | +HermesAccessibilityService HermesWeChatModule (LSPosed) | |
| 158 | +(模块 APK 进程) (注入在微信主进程) | |
| 159 | + │ │ | |
| 160 | + │ AccessibilityNodeInfo │ 反射调用 u01.r1.a → m1.a() | |
| 161 | + ▼ ▼ | |
| 162 | +微信主进程 UI(前台) 【已坏:主线程 NPE 被 swallow】 | |
| 163 | + │ | |
| 164 | + ▼ | |
| 165 | +微信走自己的发送流程 → 服务器 → message 表 | |
| 121 | 166 | ``` |
| 122 | 167 | |
| 123 | -### Hook 点 | |
| 168 | +两个广播 receiver **并行触发**,A11y 那条成功、LSPosed 那条静默失败。整个生产路径只依赖 A11y。 | |
| 169 | + | |
| 170 | +### A11y 用到的 UI 节点(WeChat 8.0.72) | |
| 171 | + | |
| 172 | +| 用途 | resource-id | | |
| 173 | +|------|-------------| | |
| 174 | +| 主界面搜索按钮 | `com.tencent.mm:id/jha` | | |
| 175 | +| 搜索输入框 | `com.tencent.mm:id/d98` | | |
| 176 | +| 聊天输入框容器(FrameLayout,内含 EditText) | `com.tencent.mm:id/bkk` | | |
| 177 | +| 发送按钮 | `com.tencent.mm:id/bql` | | |
| 178 | +| 聊天页标题(用于校验进入了正确的会话) | `com.tencent.mm:id/obn` | | |
| 179 | + | |
| 180 | +微信版本升级后这些 ID 会变化,需要重新 uiautomator dump 验证。 | |
| 181 | + | |
| 182 | +### LSPosed Hook 点(历史记录,目前无生产作用) | |
| 124 | 183 | |
| 125 | 184 | | 目标类 | 方法 | 作用 | |
| 126 | 185 | |--------|------|------| |
| ... | ... | @@ -130,25 +189,18 @@ hermes / adb shell content → 模块 Provider 进程 → 发送广播 (com. |
| 130 | 189 | | `AESUtil` | `a(String)` → 返回原值 | 跳过解密 | |
| 131 | 190 | | `MsgInfo` (f9) | `c1(String)` | 监控消息内容变更,记录日志 | |
| 132 | 191 | |
| 133 | -### 内部发消息调用链 | |
| 134 | - | |
| 135 | -``` | |
| 136 | -u01.r1.a(String toUser) → Builder | |
| 137 | - Builder.e(String content) → 设置文本内容 | |
| 138 | - Builder.h(int type) → 设置联系人类型 (0=普通) | |
| 139 | - Builder.a() → u01.m1 (Result) | |
| 140 | - m1.a() → 提交网络请求(必须在主线程调用) | |
| 141 | -``` | |
| 142 | - | |
| 143 | -类名 `u01.*` 随 WeChat 版本变化,需通过 smali 分析确认。 | |
| 192 | +LSPosed 反射发送的调用链 `u01.r1.a → builder.e → builder.h(0) → builder.a() → m1.a()` 在 WeChat 8.0.72 上 `m1.a()` 主线程提交触发 NPE 失败。已确认无法修复(怀疑 WeChat 在 NetScene 链路加了反作弊校验)。代码仍保留在 `HermesWeChatModule.java` 中作为反编译记录。 | |
| 144 | 193 | |
| 145 | 194 | ## 故障排查 |
| 146 | 195 | |
| 147 | 196 | | 现象 | 可能原因 | 解决 | |
| 148 | 197 | |------|---------|------| |
| 149 | -| `content insert` 无响应 | 广播未注册(WeChat 未启动或模块未启用) | 检查 logcat 中 `Send broadcast receiver registered` | | |
| 150 | -| `Send result: false` | 内部 API 调用失败(类名不匹配或不在主线程) | 检查日志中的具体错误,确认 WeChat 版本 | | |
| 151 | -| `m1.a() return type` 异常 | WeChat 版本升级后混淆类名变化 | 重新反编译 APK,更新 `CLS_SEND_MSG_FACTORY` 等常量 | | |
| 198 | +| `am broadcast` 返回 0 但 yibooo 没收到 / message 表无新行 | **最常见**:A11y service 未启用 | `adb shell "settings get secure enabled_accessibility_services"`,为 `null` 用上文命令重新启用 | | |
| 199 | +| HermesA11y 日志 `Root package=com.termux` 后退出 | 广播触发时微信不在前台 | 先 `adb shell am start -n com.tencent.mm/.ui.LauncherUI` 再发 broadcast | | |
| 200 | +| `Search button not found` | 当前不是 LauncherUI / nickname 错 | 确认 `toUserDisplayName` 是 WeChat 里实际的昵称 | | |
| 201 | +| `No clickable search result found` | 搜不到该 nickname | 在微信里手动搜一下验证 | | |
| 202 | +| `[HermesWX] Send result: true` 但 message 表没新行 | LSPosed 反射路径的**误报**(已知 bug) | 忽略 LSPosed 日志,看 HermesA11y 日志判断真实状态 | | |
| 203 | +| 微信 UI 节点 ID 找不到(升级后) | WeChat 版本变化导致 resource-id 变 | `adb shell uiautomator dump` 后重新核对 | | |
| 152 | 204 | | 模块未出现在 LSPosed Manager | APK 未正确安装或签名问题 | 重新安装模块 APK | |
| 153 | 205 | |
| 154 | 206 | ## 安全提示 | ... | ... |