Commit ceb78d7fdf8ecee0aa19dfc2111da248401f4f88

Authored by yibo
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>
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 ## 安全提示
... ...