Commit 295637a05aa2c51ef5832e8a78ed96c053d4ad95

Authored by yibo
1 parent 12ab73b9

chore(wechat): A11y 坐标化发送重写 + 联系人导出 + frida 路径/调试脚本

- HermesAccessibilityService: 折叠态封面屏(1080×2364)下 A11y 节点 bounds 全 0、
  ACTION_CLICK/SET_TEXT 失效,改用固定坐标 dispatchGesture 全流程(搜索图标 →
  设剪贴板长按粘贴 → 首条结果 → IME 剪贴板候选插入 → 绿色发送按钮),
  删 ~750 行旧反射/节点路径(-749/+74)。
- setup_termux_boot.sh: frida-server 路径挪到 Termux 家目录(~/hermes-wechat/bin),
  弃用 /data/local/tmp(Termux 路径约定)。
- server/services/wechat/wechat-contacts.{json,md}: 从 EnMicroMsg.db rcontact 导出
  5423 联系人(发送时确定真实 wxid 的权威来源)。
- wechat_plain/scripts/frida-daemon/: Frida 调试脚本(已废弃发送路径,留作参考)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
server/services/wechat/wechat-contacts.json 0 → 100644
No preview for this file type
server/services/wechat/wechat-contacts.md 0 → 100644
  1 +# WeChat 联系人参考表
  2 +
  3 +> 从 Pixel `EnMicroMsg.db`.`rcontact` 导出。**这是发送消息时确定真实 wxid 的唯一权威来源**。完整原始 JSON 见 `wechat-contacts.json`。
  4 +
  5 +- 联系人总数:5423(已过滤 deleteFlag=1)
  6 +- 群聊:20
  7 +- 公众号 / 服务号:117
  8 +- 系统账号:8
  9 +- 普通联系人(非陌生):4849
  10 +- 陌生人(encryptUsername 含 @stranger):429
  11 +
  12 +## 关键发现:发送是否成功取决于联系人状态
  13 +
  14 +`u01.r1.a(wxid)` 内部会查 `rcontact` 表来构造 builder。
  15 +
  16 +| 联系人类型 | wxid 形式 | 通过 `am broadcast` 能否发? |
  17 +|---|---|---|
  18 +| 系统账号(如 filehelper) | 固定字符串 | ✅ 已验证 |
  19 +| 已加好友(type=1/3/4,verifyFlag=0) | `wxid_xxx` 或自定义号 | ✅ 应该可以 |
  20 +| 群聊 | `xxxxxxxxx@chatroom` | ✅ 应该可以 |
  21 +| 公众号 | `gh_xxxxx` | ⚠️ 待验证 |
  22 +| **陌生人(encryptUsername=…@stranger)** | wxid | ❌ **会被丢弃**(火星人 yibooo 即此) |
  23 +
  24 +**结论**:发消息前,从这个 MD(或 JSON)查到的联系人,如果是陌生人,必须先用别的途径加为好友。
  25 +
  26 +## 系统账号(固定 wxid)
  27 +
  28 +| wxid | 显示名 |
  29 +|---|---|
  30 +| `facebookapp` | Facebook辅助功能 |
  31 +| `feedsapp` | 朋友圈 |
  32 +| `filehelper` | filehelper |
  33 +| `floatbottle` | 漂流瓶 |
  34 +| `fmessage` | 朋友推荐消息 |
  35 +| `medianote` | 语音记事本 |
  36 +| `newsapp` | 腾讯新闻 |
  37 +| `weixin` | 微信团队 |
  38 +
  39 +## 群聊(20 个)
  40 +
  41 +| wxid (chatroom id) | 群名 | 备注 |
  42 +|---|---|---|
  43 +| `57100316212@chatroom` | (无群名) | |
  44 +| `34778561085@chatroom` | 1-TGO和朋友们的AGI | |
  45 +| `34416781008@chatroom` | 2-TGO和朋友们的AGI | |
  46 +| `38848750609@chatroom` | 3-TGO和朋友们的AGI | |
  47 +| `34479597226@chatroom` | 4-TGO和朋友们的AGI | |
  48 +| `44136003054@chatroom` | 5-TGO和朋友们的AGI | |
  49 +| `39078484390@chatroom` | 6-TGO和朋友们的AGI | |
  50 +| `58180005816@chatroom` | 7-TGO和朋友们的AGI | |
  51 +| `54655425669@chatroom` | 8-TGO和朋友们的AGI | |
  52 +| `44997537649@chatroom` | BigQuant量化交流群21 | |
  53 +| `3164323182@chatroom` | FYI QQ群 | |
  54 +| `44183433488@chatroom` | LemonAI交流群13 | |
  55 +| `11522253153@chatroom` | Model3•卡斯基 售后服务车友3群 | |
  56 +| `49820485020@chatroom` | 【八爪鱼】官方交流群69 | |
  57 +| `34576206251@chatroom` | 宜创AI全员群 | |
  58 +| `5027730487@chatroom` | 宜创财务出纳行政群 | |
  59 +| `20151222593@chatroom` | 景福苑社区禧瑞春秋居民3群 | |
  60 +| `51929848445@chatroom` | 智干CEO群 | |
  61 +| `23910365268@chatroom` | 算法妈妈客户群 | |
  62 +| `5272481601@chatroom` | 颐堤港 特斯拉 车主之家 | |
  63 +
  64 +## 公众号 / 服务号(117 个)
  65 +
  66 +<details><summary>展开</summary>
  67 +
  68 +| wxid | 名称 |
  69 +|---|---|
  70 +| `gh_240fbf8b33e4` | 36氪 |
  71 +| `gh_3e5d3b151ac4` | 42章经 |
  72 +| `gh_a8c2db39e962` | AGENT橘 |
  73 +| `gh_92a47dd34b3c` | AIGC开放社区 |
  74 +| `gh_9fb3dcdc4f72` | AI产品分析 |
  75 +| `gh_a98e02bf062e@app` | AI智面 |
  76 +| `gh_2165be4aabe9` | AI范儿 |
  77 +| `gh_df9ba0e70ea4@app` | AI面试通 |
  78 +| `gh_dfb13315d6ae` | ASRer |
  79 +| `gh_a327cb04644a@app` | BOSS直聘丨求职招聘找工作 |
  80 +| `gh_bfb2685d4cd6@app` | BOSS直聘小助手 |
  81 +| `gh_bd2952682e36` | IDG资本 |
  82 +| `gh_594f61bbb1f2@app` | Kimi 智能助手 |
  83 +| `gh_761c35ed1ede` | MasterGo 莫高设计 |
  84 +| `gh_5c53f4ddc2e3` | OneFlow |
  85 +| `gh_c6597e696013@app` | Uber 优步丨全球打车出行 |
  86 +| `gh_565272eda49a` | ZWO振旺 |
  87 +| `gh_1f470e46e48e` | vika维格云 |
  88 +| `gh_55d3428e6139@app` | 三国:冰河时代 |
  89 +| `gh_1747fc7e6d5d` | 中国人保财险 |
  90 +| `gh_a5cc7bf61915@app` | 中国人保财险丨车险意外险健康险 |
  91 +| `gh_ebc9637ebb5e` | 中国人寿保险 |
  92 +| `gh_5fb54244cdbb@app` | 中油好客e站 |
  93 +| `gh_3c32b864a8f0` | 云葫芦 |
  94 +| `gh_5c3b824b59ae@app` | 亚朵ATOUR |
  95 +| `gh_e840a10b23f2` | 亿欧网 |
  96 +| `gh_9bdd29b8c131@app` | 住范儿装修助手 |
  97 +| `gh_af3dae7e67ba@app` | 住范儿超级家居MALL |
  98 +| `gh_5f4b9d5eb97c` | 傅盛 |
  99 +| `gh_5d931da39f99` | 光速软件 CodeFun |
  100 +| `gh_38f4f838d747@app` | 公众号 |
  101 +| `gh_4cb202fe890f` | 创业邦 |
  102 +| `gh_98bb47766c7a` | 创米数联 |
  103 +| `gh_07dede3720da` | 北京万科设计VA |
  104 +| `gh_df997a3a899f` | 北京人保财险 |
  105 +| `gh_335977ce636c` | 北京大学哲学系宗教学系 |
  106 +| `gh_51d217a531b6` | 北京环球度假区 |
  107 +| `gh_292ab08c0733` | 北京联通 |
  108 +| `gh_0e333bc80b47` | 北大哲学系学生会 |
  109 +| `gh_a20db0b73762@app` | 北森AI面试 |
  110 +| `gh_37ecccd576f0@app` | 华住会酒店预订汉庭全季桔子 |
  111 +| `gh_4accc57a1b9e` | 即时设计JsDesign |
  112 +| `gh_53c3f9e838a4` | 可能的同事 |
  113 +| `gh_ba353193523e` | 可能的同事 |
  114 +| `gh_66b3144de690@app` | 名片全能王+ |
  115 +| `gh_e083a8815e86` | 启迪之星创投 TusStarVC |
  116 +| `gh_b71f17419711` | 和小马哥一起学习 |
  117 +| `gh_12d8f401095e` | 嗨动Plus |
  118 +| `gh_06b1bae0faa6@app` | 国窖荟 |
  119 +| `gh_a2c1fa416ac7` | 基本操作Playground |
  120 +| `gh_660f116ad712` | 塔罗烩 |
  121 +| `gh_275e05e9ba82@app` | 多点 |
  122 +| `gh_b09b492d0fa3` | 太平洋产险北京分公司 |
  123 +| `gh_394a4f49824d` | 奇游电竞加速器 |
  124 +| `gh_556366d929b8` | 宅总有理 |
  125 +| `gh_01e439516021@app` | 宗门模拟器 |
  126 +| `gh_dc03ea524954` | 宜创(北京)科技有限公司消息 |
  127 +| `gh_1870dd2e5f4d` | 宜创AI |
  128 +| `gh_ce2b4a1265cc` | 宜创人工智能 |
  129 +| `gh_8225916f8124` | 宜创科技 |
  130 +| `gh_33b014481665` | 宜创科技消息 |
  131 +| `gh_105f69c6d394` | 宜创(北京)科技有限公司 |
  132 +| `gh_80019632e2eb` | 宜创(北京)科技有限公司消息 |
  133 +| `gh_5649b8abe532@app` | 小极快投 |
  134 +| `gh_7aac992b0363` | 微信卡包 |
  135 +| `gh_3610de7f9728@app` | 微信发票助手 |
  136 +| `gh_3dfda90e39d6` | 微信支付 |
  137 +| `gh_25d9ac85a4bc` | 微信游戏 |
  138 +| `gh_4a4307103487@app` | 招行企业视频连线 |
  139 +| `gh_3a849b303c14@app` | 探探 |
  140 +| `gh_108f2a2a27f4` | 新智元 |
  141 +| `gh_f8a7c37a022d@app` | 无尽冬日 |
  142 +| `gh_906c819c6695@app` | 易事达广场易会员 |
  143 +| `gh_0ab42211a4e8` | 易观分析服务号 |
  144 +| `gh_4cf4a631a0c9@app` | 星巴克 |
  145 +| `gh_44e9b2141610` | 晚点LatePost |
  146 +| `gh_ee100dd42cd1` | 智能涌现 |
  147 +| `gh_176c57f06d21` | 曾府传家湘菜 |
  148 +| `gh_13df3458e28e` | 曾鸣书院 |
  149 +| `gh_aabd1acbf942` | 未命名企业 |
  150 +| `gh_dbc0a5474692` | 机器之心 |
  151 +| `gh_afb1d49930b8` | 标智客Logo设计 |
  152 +| `gh_61ad2449e380` | 江苏广播 |
  153 +| `gh_d00398dac3f0@app` | 洽洽会员俱乐部 |
  154 +| `gh_7b6b03a3f821@app` | 洽洽食品优选商城 |
  155 +| `gh_59e8fba3a52c` | 海底捞火锅 |
  156 +| `gh_7a5c4141778f@app` | 滴滴出行丨打车骑行公交租车代驾 |
  157 +| `gh_7ee6b4cdb5ff@app` | 爱回收 |
  158 +| `gh_6d7fd5e70002` | 特斯拉 |
  159 +| `gh_d7fb90ded6b5@app` | 特斯拉Tesla |
  160 +| `gh_68ca562891a1` | 特斯拉Tesla |
  161 +| `gh_b0d52e1a8460` | 王国广场 |
  162 +| `gh_1da8da8216de` | 甲子光年 |
  163 +| `gh_271633b4f577` | 电手 |
  164 +| `gh_a4794c6bf930` | 白鲸技术栈 |
  165 +| `gh_7d91e9294b51` | 砰砰法师 |
  166 +| `gh_047a89d1d6db` | 硅兔赛跑 |
  167 +| `gh_002366d6ee1e` | 稀牛学苑 |
  168 +| `gh_b82c60fc26b1` | 网易游戏玩家 |
  169 +| `gh_0d44c4ecb81c` | 腾源会 |
  170 +| `gh_19a5d2409766` | 腾讯游戏新游社 |
  171 +| `gh_7b4a83abfa7d@app` | 蔚来 |
  172 +| `gh_4303895ee2c3` | 该账号已注销 |
  173 +| `gh_2c767d902345@app` | 车讯达+ |
  174 +| `gh_9d1ff972a4d1@app` | 逸扉UrCove |
  175 +| `gh_114e76fd6e5d` | 量子位 |
  176 +| `gh_6877f44ad66f` | 阿拉丁数据助手 |
  177 +| `gh_ce26d62c0347` | 阿里云万网 |
  178 +| `gh_55e2acc382c6` | 集简云平台 |
  179 +| `gh_bb6b4cfdef72` | 雷军 |
  180 +| `gh_7208505ac475@app` | 面试AI |
  181 +| `gh_3ce2a12522fd` | 飞书多维表格 |
  182 +| `gh_594955ae4aaa` | 餐十 |
  183 +| `gh_f05e41738ac2` | 饭统戴老板 |
  184 +| `gh_15190b990b4e` | 首创北京创享家 |
  185 +| `gh_bc1b298ce89b@app` | 魔方租房 l 公寓长短租房源 |
  186 +| `gh_ab6c7fcdd329` | 黑马天启科技 |
  187 +
  188 +</details>
  189 +
  190 +## 个人联系人(4849 个非陌生人)
  191 +
  192 +> 太多了,下面只列**有备注 `conRemark`** 的;其余请查 `wechat-contacts.json`。
  193 +
  194 +### 有备注的(0)
  195 +
  196 +| wxid | 备注 | 昵称 | 微信号 |
  197 +|---|---|---|---|
  198 +
  199 +## 陌生人(429 个,发送会失败)
  200 +
  201 +<details><summary>展开(按 nickname 排序)</summary>
  202 +
  203 +| wxid | 昵称 | 微信号 |
  204 +|---|---|---|
  205 +| `wxid_d4ec7sz8rrs422` | . | |
  206 +| `wxid_g9c4to0cg58b22` | 1. | Wangjie-0219 |
  207 +| `wxid_a2l0n6k4o09k22` | A Manage:汤 | |
  208 +| `adadiaodeaitan` | A-陶13651264968 | |
  209 +| `wxid_3ypl1z37uk0d22` | A01博洛尼王尧满 | bln2023 |
  210 +| `wxid_5enau7wmhzv522` | A02-政采标书预算01-18630997325 | A18610164372 |
  211 +| `wxid_1ahgrwhops6622` | AA可研报告&商计19339959758 | |
  212 +| `wxid_kiuc5a2rsvdo22` | AA空调安装维修销售13933666384 | HL13933666384 |
  213 +| `wxid_vcmx2yh91imi22` | AB3-政采标书预算02-18610160031 | sj_1853539 |
  214 +| `wxid_siogrw83sbzg22` | AF1-11电子卖场&招投标18510081304 | dx17310383561 |
  215 +| `wxid_ipsxtux2bay022` | AI获客📱雅欣 | WYX961130010 |
  216 +| `wxid_exyf2l5m3kwb22` | AR 瑞士表行 | |
  217 +| `suny616158030` | Aaron孙无极 | sunedwin |
  218 +| `aifen86` | Aifen | |
  219 +| `wxid_t6to61qnmawy22` | Ai获客 😎 高雅 | Y8881781 |
  220 +| `wxid_0uqtcbcb8jg212` | Ai获客🍎玥晗 | ZXJ14789632588 |
  221 +| `wxid_dn27mbeisgxr12` | Ai获客🚀晓玥 | DQdeyixin2 |
  222 +| `wxid_z7a79c2yt5cc12` | Awen 雯 | Awen-Awen2020 |
  223 +| `wxid_lvp8ggxsmh6m22` | A事务所~邢明广15632261771 | x17762006445 |
  224 +| `wxid_0k0lf0j1hjea22` | A企业修复-文经理 | klsxyxf017 |
  225 +| `wxid_9xtiy8xf89cz22` | A华夏良子望京店邵丽15210017295 | sl15210017295 |
  226 +| `wxid_zwknqd6m400k22` | A天硕硒鼓厂 小梁15309249464 | |
  227 +| `wxid_gwlfzxunt4an22` | A审计 评估·小侯🐒🐒 | |
  228 +| `wxid_5cq26tlu9q5n12` | A月姐13611116622 | |
  229 +| `wxid_5fmbns8hjsx122` | A趙迎希 | z2042707151 |
  230 +| `wxid_nh0gjxkd277p22` | A郭宇乾 | |
  231 +| `wxid_1cku0s3uki4h22` | A(企业信贷)王凌云 | |
  232 +| `wxid_qdm72ocut4bg22` | BOSS直聘 谢芳 | xf05281108 |
  233 +| `wxid_2965579656012` | Basketball(孙冶平) | Basketball-ko |
  234 +| `cache_go` | Che | |
  235 +| `wxid_4473324734314` | Chris 廖 | wx13901946293 |
  236 +| `wxid_5z6eo5e5170n41` | DDD_loan_金宇 | |
  237 +| `wxid_mnbdwb4ly2zn21` | DX | DXDHLXL |
  238 +| `wxid_mm70e5ssspnr12` | DanielQ | DanielQ75 |
  239 +| `wxid_9hi58rfmth6z12` | Dora董小满儿 | dongxiaohou521 |
  240 +| `aze701432` | Emilio | |
  241 +| `MaplestoryFeng` | Feng | |
  242 +| `wxid_cc5t8qm2d9aq22` | G. | G0987213 |
  243 +| `wxid_bkqi4xxmmjag22` | Grace | Gracecheng988 |
  244 +| `wxid_gcn9sb54yezg22` | Gz高 | |
  245 +| `wxid_6764947649312` | Home | wangtao-629 |
  246 +| `wxid_4n6wio1j2vms22` | ISO体系 检测 -方经理 | Fang19264400201 |
  247 +| `wxid_1o89xrtlz53y22` | Iris CJ | IrisCJ923 |
  248 +| `wxid_ljqbfk0rv7ka22` | Jerry | |
  249 +| `jwc2005` | Jiang姜伟超 | |
  250 +| `wxid_wwtmiqoyeqn421` | John Liu | JohnZCLiu |
  251 +| `wxid_zdiyjeudgs2212` | Joyce | _Joyce_Liu_ |
  252 +| `wxid_w9is3ro7vpo311` | Kevin胡 | KevinHu1117 |
  253 +| `wxid_6p8ptdlmieju21` | Laven骆 | vala1213 |
  254 +| `love135579` | Lilian💫 | Aliliannnnn |
  255 +| `wxid_26u8pg8gwfcc21` | Lilyyu | |
  256 +| `qlz99183398` | Lim | qlz937742955 |
  257 +| `ss_lorelei` | Lori | |
  258 +| `wxid_cp6h9u6rycib22` | L🍻 | |
  259 +| `wxid_2640046402512` | Mr.190 | xm664842967 |
  260 +| `wxid_r2ico7rhd4or12` | Ms Ding | dys2248666318 |
  261 +| `wxid_n9r4vszgiq2i22` | Ms.何 | |
  262 +| `wxid_0vo4474qxnqp22` | N | |
  263 +| `wxid_7b9lop16iuwb21` | Nancy-Z | Nancy100707 |
  264 +| `wphappy8` | Pp | ds2dy-pp |
  265 +| `wxid_1ge0ku4s2lry22` | Sina | Sina-0707 |
  266 +| `wxid_mhtnun2dnc7v12` | Starry🐬小蒋 | yc_8063 |
  267 +| `wxid_gln6ceohpbu922` | Stella梓薇 | stella0007777 |
  268 +| `v979900185` | Summer | pengxiaofeng7218 |
  269 +| `wxid_wpbizv2k18ou12` | Tesla特斯拉 | teslabj00073 |
  270 +| `wxid_c3lyoo3tjsa622` | Treacy | Q19537613546 |
  271 +| `wxid_ch80lvlu9btx21` | UV.Z | zy-wider |
  272 +| `wxid_pgjn602hgfg922` | Ubiquity_ | miss_sun_624 |
  273 +| `wxid_q6kk53si63an11` | Wangwang | Eva_106 |
  274 +| `Feelwe` | X | |
  275 +| `wxid_grlt3eyyq47522` | Z. | |
  276 +| `wxid_p7mka8kennci22` | a泽创企服集团🌸 陈玉 | |
  277 +| `wxid_8579195791512` | elva | elva-specopp |
  278 +| `F992170706` | fwj | |
  279 +| `jimmy1673` | jy | |
  280 +| `wxid_0zqrwfuo49a021` | karat | karat-k |
  281 +| `wxid_ejmqw229nywv22` | key | Anznl33 |
  282 +| `wxid_n512npxecnw821` | lelang🦄 | lelelanglang2025 |
  283 +| `wxid_oauy1ywaeoad12` | nemo | lnn756614060 |
  284 +| `wxid_u90yo746k3hp22` | shalom | ShalomZeng |
  285 +| `wxid_izx0cvr6m9lj22` | slofer | |
  286 +| `afdjkshd1241321` | spring | spring102625 |
  287 +| `wxid_eb44miwtfvyt22` | w4n63r | w4n63r |
  288 +| `wxid_sn1roglz7w1p22` | | qer135000 |
  289 +| `wxid_5ly37pxti8xb12` | xυáɴ | LX_A-X |
  290 +| `wxid_96a8llinxbqc52` | ø | Yyyyyy-_y |
  291 +| `wxid_6643626437212` | ʕ•̀ω•́ʔ✧王洛阳 | foxlu_520 |
  292 +| `wxid_h4itw7u86y7822` | 【汾酒集团宝泉涌】青花酒厂畅利刚 | fenjiu000001 |
  293 +| `wxid_i81zori19e8b22` | 【築地水产】日本料理 | j13718018657 |
  294 +| `wxid_k5kk5bdds6oh22` | ㅤ | zly_20000310 |
  295 +| `wxid_zbrx6dqg448y22` | 一壶莲花醉 | zhen001000 |
  296 +| `wxid_2lgc83hxd17f22` | 一生宠爱᭄ꦿฅ | qq1617271929 |
  297 +| `wxid_ws6k41duerzq22` | 一萌 | sly103980521 |
  298 +| `wxid_myfq1ee9wn0q12` | 丁加春 | djc13693557788 |
  299 +| `wxid_gkqkjy54q98812` | 丁子虔-18515911326 | w18515911326 |
  300 +| `wxid_cbs66zkc8w1f22` | 七叶莲 | wx1-6-8-9 |
  301 +| `wxid_6025960260312` | 万事胜意🇨🇳 | cy_20100310 |
  302 +| `wxid_bcgm3w7sf13822` | 万事顺遂🍀 | jiali36919 |
  303 +| `wxid_fn3zqyaiuuqz22` | 三月飞鸟 | |
  304 +| `wxid_d58jwlux4tnu22` | 不睡觉假扮王祖贤 | qqttqq0808 |
  305 +| `wxid_w9fkzfbq8tdo22` | 专利 商标 高新项目申报👈 | |
  306 +| `wxid_g4sb8r5qwv9a22` | 业之峰牟红 | yzf_mh |
  307 +| `wxid_dthcm2zyry2112` | 东晓 | xiaodongchai |
  308 +| `sange77521` | 两块三刀 | sange1993615 |
  309 +| `wxid_q4ofrxtoekso22` | 中国人保 | |
  310 +| `wxid_dk0pdabsxjjx22` | 中国传感网大学书法美术学院李金柱 | L13152140521 |
  311 +| `yunfan85` | 云帆 | yunfan0301 |
  312 +| `wxid_2vwe96xlphqw22` | 云服务商刘文娟服务器邮箱AI大模型 | -juan200320 |
  313 +| `wxid_4414274143825` | 云杜安 | |
  314 +| `wxid_wrugs8dkvwjx21` | 亚昕 | BYX850 |
  315 +| `wxid_vuqr6jcsjw522` | 人生百态 | p659417369 |
  316 +| `wxid_gdrcdp1nmkgj22` | 仲亿 | zhongyiya666666 |
  317 +| `wxid_wyhs8ilh56q222` | 任໊ོ宇⁶⁶⁶⁹⁹⁹࿐ | |
  318 +| `wxid_1toohf9hgwh122` | 任ོ宇⁶⁶⁶⁹⁹⁹࿐ | |
  319 +| `wxid_aklmm2owildo22` | 任ⁱ宇⁶⁶⁶⁹⁹⁹࿐ | |
  320 +| `wxid_qzeiw4cqf78i22` | 企业咨询@文靖 | cczxywj |
  321 +| `wxid_2dvp3ab3mjde21` | 企业礼品定制印刷品小田 | TXY19520226330 |
  322 +| `wxid_j78deaj6s6g622` | 企业融资渠道.易成 | yirr520025 |
  323 +| `wxid_h2fcttw6z12222` | 伊擎_qing华为 | myq0118yyds |
  324 +| `wxid_m4benxch6ex421` | 何 | hyw19871220 |
  325 +| `wxid_lzmimdvnjoz322` | 何彷 | zax6325139 |
  326 +| `wxid_nx5tqz365eg622` | 佼不佼 | |
  327 +| `wxid_tulunulz5hxu21` | 信念 | |
  328 +| `wxid_nf5wkiy8w4sy12` | 元气梦 | meng58009 |
  329 +| `a7a5ahehe` | 光 | NotFound2291 |
  330 +| `wxid_xb52e2xaj8bt22` | 光大银行企业信贷渠道部 | |
  331 +| `lanlan1286075146` | 兰艳楠 | |
  332 +| `wxid_g94ucqlu28fg22` | 冀鲁.劳务 | lu674737408 |
  333 +| `wxid_6n4e5behyfq622` | 写意空间北居店-Sunny王明爽 | PR-LR190710-05 |
  334 +| `wxid_ayl0fe41opiu51` | 冯海英 | |
  335 +| `icequeenjingjing` | 冰王s | |
  336 +| `wxid_zap5tcnl116j22` | 刘 骏 | liu13811051447 |
  337 +| `wxid_qos8ygqo5tmb21` | 刘 剑 | |
  338 +| `wxid_bb9tngrjt7qz12` | 刘 欣 | |
  339 +| `wxid_jw6ot95cm4y722` | 刘乐-NJ13814082251 | |
  340 +| `wxid_gdzbkqlp6pne22` | 刘仲谋 Francis | FrancisSNLiu |
  341 +| `liuxiangnan7889` | 刘向南 | |
  342 +| `wxid_0pkd8icpk07s22` | 刘国杰18210750400 | |
  343 +| `liudi0199` | 刘狄 | |
  344 +| `wxid_38bafv02q5g312` | 刘白Daniel | LiubaiDLB |
  345 +| `liuqiuchen002` | 刘秋辰 | CCCHENCHEN1123 |
  346 +| `wxid_sh5myy3vdcm222` | 别墅全案设计装修刘广(炳尘) | |
  347 +| `wxid_toiabxqa7qsk22` | 北京中知信赵茹 软著专利高新 | |
  348 +| `wxid_yp6g6tbzuht022` | 北京印刷厂-王昊 | |
  349 +| `wxid_lidxaasfaj6z12` | 北京印刷厂~Lu | |
  350 +| `wxid_998pn5ncpdan22` | 北京萌货机器人 | MengZongRobot1 |
  351 +| `wxid_bkjcr0jnvpfq12` | 北京设计师马儿 | szm756614 |
  352 +| `wxid_d791ox7kon0622` | 北京银行~尚总监 | |
  353 +| `wxid_uhc2ngm8pkhb22` | 十里长歌 | |
  354 +| `wxid_i644tr6jwk2s22` | 南京律师事务所|夏13260722623 | gaozhanls669 |
  355 +| `wxid_bkdvllbot0zd22` | 南京标识厂家| 张18120173676 | J18120173676 |
  356 +| `wxid_g7sduux8rfhf12` | 博洛尼总部刘艳新. | liuzeyu2023 |
  357 +| `wxid_yjbqjetq3s5622` | 博洛尼钛马赫整装刘建刚 | |
  358 +| `wxid_xklt0f7fpjpw12` | 卡斯基此号已满请加18888800122 | K18888800122 |
  359 +| `wxid_i92r427zoz0t12` | 卢锦伟 | Lujinwei78 |
  360 +| `wxid_s2tbw9vatymy22` | 叙永冠珠陶瓷熊强 | Look365up |
  361 +| `wxid_2f4it0v69lm522` | 口味堂-市井长沙菜(望京) | |
  362 +| `wxid_q1pny0h0305d22` | 可乐(补卡·组队·售道具) | |
  363 +| `sunyan19831209` | 吐沫的小螃蟹ღ | xiaopangxie1209- |
  364 +| `wxid_m2o0k7yr7lsb22` | 向晚💞 | jisu98888 |
  365 +| `qq532570294` | 吴杉 | v532570294 |
  366 +| `wxid_fm3co5fko6e122` | 吴贤忠 | WXZ1514623 |
  367 +| `qq249687611` | 呀娜索 | yanasuo |
  368 +| `wxid_vcayjsnpord121` | 周周周 | zzzc5566 |
  369 +| `wxid_o9i336hfms8h21` | 周复兴 | |
  370 +| `wxid_evsh7usx1a7522` | 周露 | |
  371 +| `wxid_k5sbh5dlz5h522` | 品匯和牛(国宾店) | PHHN111 |
  372 +| `t2247948390` | 唐吉林 | |
  373 +| `wxid_xv8es4e4mohl21` | 嗓子哑了 | Luhui667700 |
  374 +| `wxid_egoklwoay4gt22` | 大友子~ | goudannb110 |
  375 +| `wxid_8uaofduldblp22` | 大川 | xf168861999 |
  376 +| `wxid_4n52ylpmyvj012` | 天空 | |
  377 +| `wxid_29j4ji7azmx722` | 天道酬勤 | hdzyh8888 |
  378 +| `wxid_pt29afg3mglm22` | 奈斯小婧 | Ace100331 |
  379 +| `wxid_pzuow7xciohb12` | 妙笔生花 | fatt3399 |
  380 +| `wxid_5i63bjyevk5n22` | 姚 | |
  381 +| `wxid_mauov065xzjq22` | 姜棱梨 | jiang_lengliii |
  382 +| `laixiaona7980` | 娜么快乐 | lhn0524 |
  383 +| `wxid_3628326266914` | 娟 | lxj881121 |
  384 +| `wxid_foqmzjeji62222` | 婷婷 | wjt11219423 |
  385 +| `wxid_a60kba930w2s22` | 嬌~Dr. | |
  386 +| `zhaogengzi` | 子庚 | |
  387 +| `wxid_uc6ohkh069ud22` | 孙宇晴 | tranquillement610 |
  388 +| `hyacinthblue3` | 孙尧 | sunyao617 |
  389 +| `wxid_kz0mc3kbk6ci12` | 孙林泉 | cfpl0611 |
  390 +| `wxid_5s0gj88nigwl12` | 孙盈 | |
  391 +| `wxid_j5naqu3uda0311` | 孙腾飞 | Stf10-11 |
  392 +| `wxid_vrgpld7sczsk12` | 孙齐 | |
  393 +| `wxid_6yxcypkhoery22` | 孟瑶 | |
  394 +| `wxid_brrpc521juq229` | 学习教练·瀚海老师 | LDCKER |
  395 +| `wxid_2mxdl8dzc4vm22` | 宁波如宜工作服~周周 | |
  396 +| `wxid_ntmdd0klw6rl22` | 宁秋 | happybear1886 |
  397 +| `wxid_mtnz43yw1jb612` | 守得云开 | zxf133928 |
  398 +| `wxid_zrua3a6nglio41` | 安静-专利(预审优审) | |
  399 +| `wxid_aee6rwrm1p8b41` | 宋文东 | |
  400 +| `wxid_ujobsa2lv10012` | 宋艺龙 | Xiaosong20190804 |
  401 +| `wxid_yqlwy1cpux6922` | 宏瑶🌴 | A18513935689 |
  402 +| `wxid_h3z586ln3tqc12` | 宜创AI | llmfarmyunying |
  403 +| `wxid_a91jewm2zypg22` | 宜创科技 | |
  404 +| `wxid_bzloi5nyiehi22` | 宜建华 | |
  405 +| … 还有 229 个,请查 JSON | | |
  406 +
  407 +</details>
  408 +
  409 +## type / verifyFlag 字段语义
  410 +
  411 +rcontact 表里观察到的 type 分布:
  412 +
  413 +| type | 数量 | 推测含义 |
  414 +|---|---|---|
  415 +| 4 | 4804 | 通讯录好友(最常见) |
  416 +| 3 | 188 | 已删除联系人但保留聊天 |
  417 +| 10 | 185 | 公众号 |
  418 +| 1 | 95 | 好友(type & 1 = 1) |
  419 +| 0 | 41 | 占位 / 未分类 |
  420 +| 2 | 28 | 黑名单 / 仅公众号 |
  421 +| 33 | 24 | 公众号变体 |
  422 +| 8 | 10 | 默认对话 / 系统 |
  423 +| 513 | 10 | 群聊变体 |
  424 +| 2050 | 7 | 陌生人变体 |
  425 +| 32771 | 7 | ? |
  426 +| 515 | 6 | 群聊变体 |
  427 +| 2051 | 6 | 陌生人(encryptUsername=...@stranger) |
  428 +| 11 | 4 | 公众号+好友 |
  429 +| 8388609 | 4 | ? |
  430 +| 9 | 2 | ? |
  431 +| 65539 | 1 | ? |
  432 +| 8388616 | 1 | ? |
  433 +
  434 +## 代码使用方式
  435 +
  436 +```js
  437 +// Node.js
  438 +const contacts = require('./wechat-contacts.json');
  439 +const c = contacts.find(c => c.conRemark === '火星人' || c.nickname === '火星人');
  440 +if (c.isStranger) throw new Error('陌生人不能直接发消息');
  441 +// 用 c.username 作为 am broadcast --es toUser 的值
  442 +```
  443 +
  444 +```bash
  445 +# 查 wxid
  446 +jq -r '.[] | select(.nickname=="火星人") | .username' wechat-contacts.json
  447 +```
  448 +
  449 +---
  450 +
  451 +生成方式:见仓库 `wechat_plain/scripts/local-testkit/` 下的 sqlcipher 导出流程。
... ...
wechat_plain/scripts/frida-daemon/debug_builder_class.js 0 → 100644
  1 +/**
  2 + * Debug script: verify builder class name when calling u01.r1.a("yibooo")
  3 + *
  4 + * Run on Mac:
  5 + * frida -U -n com.tencent.mm -l debug_builder_class.js
  6 + *
  7 + * This script will:
  8 + * 1. Call u01.r1.a("yibooo") and print builder.getClass().getName()
  9 + * 2. Print builder class hierarchy
  10 + * 3. Test method calls via both builder.getClass() and Java.use("u01.q1")
  11 + * 4. Check if setIntField("f", 0) / ("i", 4) causes any issue
  12 + */
  13 +
  14 +function log(msg) {
  15 + console.log("[DebugBuilder] " + msg);
  16 +}
  17 +
  18 +Java.perform(function () {
  19 + var toUser = "yibooo";
  20 + var content = "test msg from frida debug";
  21 +
  22 + try {
  23 + var r1 = Java.use("u01.r1");
  24 + var builder = r1.a(toUser);
  25 +
  26 + var builderClass = builder.getClass();
  27 + var className = builderClass.getName();
  28 + log("builder.getClass().getName() = " + className);
  29 +
  30 + // Print hierarchy
  31 + var superClass = builderClass.getSuperclass();
  32 + while (superClass) {
  33 + log(" extends: " + superClass.getName());
  34 + superClass = superClass.getSuperclass();
  35 + }
  36 +
  37 + // Check if it's exactly u01.q1 or a subclass
  38 + var isExactQ1 = (className === "u01.q1");
  39 + log("Is exactly u01.q1? " + isExactQ1);
  40 +
  41 + // List all declared methods of the actual class
  42 + var methods = builderClass.getDeclaredMethods();
  43 + log("Declared methods on " + className + ":");
  44 + for (var i = 0; i < methods.length; i++) {
  45 + var m = methods[i];
  46 + if (m.getName() === "e" || m.getName() === "h" || m.getName() === "a") {
  47 + log(" " + m.getName() + "(" + m.getParameterTypes().map(function(t){return t.getName();}).join(", ") + ") -> " + m.getReturnType().getName());
  48 + }
  49 + }
  50 +
  51 + // Test 1: Call via builder.getClass().getMethod() (same as LSPosed)
  52 + log("\n--- Test 1: builder.getClass().getMethod() ---");
  53 + try {
  54 + var m_e = builderClass.getMethod("e", java.lang.String.class);
  55 + var m_h = builderClass.getMethod("h", java.lang.Integer.TYPE);
  56 + var m_a = builderClass.getMethod("a");
  57 +
  58 + m_e.invoke(builder, content);
  59 + log(" e() OK");
  60 +
  61 + m_h.invoke(builder, 0);
  62 + log(" h(0) OK");
  63 +
  64 + // Try setIntField equivalent via reflection
  65 + try {
  66 + var f_field = builderClass.getDeclaredField("f");
  67 + f_field.setAccessible(true);
  68 + f_field.setInt(builder, 0);
  69 + log(" set f=0 OK");
  70 + } catch (f_err) {
  71 + log(" set f=0 FAILED: " + f_err);
  72 + }
  73 +
  74 + try {
  75 + var i_field = builderClass.getDeclaredField("i");
  76 + i_field.setAccessible(true);
  77 + i_field.setInt(builder, 4);
  78 + log(" set i=4 OK");
  79 + } catch (i_err) {
  80 + log(" set i=4 FAILED: " + i_err);
  81 + }
  82 +
  83 + var result1 = m_a.invoke(builder);
  84 + log(" a() result class: " + result1.getClass().getName());
  85 +
  86 + var submit1 = result1.getClass().getMethod("a");
  87 + var ret1 = submit1.invoke(result1);
  88 + log(" submit result: " + ret1);
  89 +
  90 + } catch (e) {
  91 + log("Test 1 FAILED: " + e);
  92 + if (e.getCause) log(" cause: " + e.getCause());
  93 + }
  94 +
  95 + // Test 2: Call via Java.use("u01.q1") (same as working frida code)
  96 + log("\n--- Test 2: Java.use(\"u01.q1\") ---");
  97 + try {
  98 + var builder2 = r1.a(toUser);
  99 + var q1 = Java.use("u01.q1");
  100 + q1.e.call(builder2, content);
  101 + log(" q1.e() OK");
  102 + q1.h.call(builder2, 0);
  103 + log(" q1.h(0) OK");
  104 + var result2 = q1.a.call(builder2);
  105 + log(" q1.a() result class: " + result2.getClass().getName());
  106 + var m1cls = Java.use("u01.m1");
  107 + var ret2 = m1cls.a.call(result2);
  108 + log(" submit result: " + ret2);
  109 + } catch (e) {
  110 + log("Test 2 FAILED: " + e);
  111 + }
  112 +
  113 + log("\n--- Done ---");
  114 +
  115 + } catch (e) {
  116 + log("Overall FAILED: " + e);
  117 + }
  118 +});
... ...
wechat_plain/scripts/frida-daemon/frida_daemon.js 0 → 100644
  1 +/**
  2 + * Frida Daemon — 注入微信进程,读取队列文件并发送消息
  3 + *
  4 + * 运行方式(Mac 端):
  5 + * frida -U -p $(adb shell "su -c 'pgrep -x com.tencent.mm'") -l frida_daemon.js
  6 + *
  7 + * 队列文件:/data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl
  8 + * 格式:每行一个 JSON {"talker":"wxid","content":"msg","id":"msg_xxx"}
  9 + */
  10 +
  11 +// 统一放在 Termux 可访问目录,避免 /data/local/tmp 权限问题
  12 +var QUEUE_FILE = "/data/data/com.termux/files/home/hermes-wechat/.frida_queue/queue.jsonl";
  13 +var LOG_TAG = "[HermesDaemon]";
  14 +
  15 +function log(msg) {
  16 + console.log(LOG_TAG + " " + msg);
  17 +}
  18 +
  19 +// ── Send message via WeChat internal API ──
  20 +function sendMessage(talker, content) {
  21 + log("Sending to " + talker + ": " + content.substring(0, 40));
  22 +
  23 + Java.perform(function () {
  24 + Java.scheduleOnMainThread(function () {
  25 + try {
  26 + var r1 = Java.use("u01.r1");
  27 + var builder = r1.a(talker);
  28 + log("Builder class: " + builder.getClass().getName());
  29 +
  30 + var q1 = Java.use("u01.q1");
  31 + q1.e.call(builder, content);
  32 + q1.h.call(builder, 0);
  33 + log("Content set, type=0");
  34 +
  35 + var m1 = q1.a.call(builder);
  36 + log("Built m1 class: " + m1.getClass().getName());
  37 +
  38 + var m1cls = Java.use("u01.m1");
  39 + var ret = m1cls.a.call(m1);
  40 + log("Submit result: " + ret);
  41 +
  42 + } catch (e) {
  43 + log("Send FAILED: " + e);
  44 + }
  45 + });
  46 + });
  47 +}
  48 +
  49 +// ── Read and process queue file ──
  50 +function processQueue() {
  51 + try {
  52 + var File = Java.use("java.io.File");
  53 + var FileInputStream = Java.use("java.io.FileInputStream");
  54 + var BufferedReader = Java.use("java.io.BufferedReader");
  55 + var InputStreamReader = Java.use("java.io.InputStreamReader");
  56 + var FileWriter = Java.use("java.io.FileWriter");
  57 +
  58 + var file = File.$new(QUEUE_FILE);
  59 + if (!file.exists()) {
  60 + return;
  61 + }
  62 +
  63 + var fis = FileInputStream.$new(file);
  64 + var reader = BufferedReader.$new(InputStreamReader.$new(fis));
  65 +
  66 + var lines = [];
  67 + var line;
  68 + while ((line = reader.readLine()) !== null) {
  69 + lines.push(line);
  70 + }
  71 + reader.close();
  72 + fis.close();
  73 +
  74 + if (lines.length === 0) {
  75 + return;
  76 + }
  77 +
  78 + log("Processing " + lines.length + " queued messages");
  79 +
  80 + var successCount = 0;
  81 + for (var i = 0; i < lines.length; i++) {
  82 + try {
  83 + var msg = JSON.parse(lines[i]);
  84 + if (msg.talker && msg.content) {
  85 + sendMessage(msg.talker, msg.content);
  86 + successCount++;
  87 + }
  88 + } catch (e) {
  89 + log("Parse error: " + e);
  90 + }
  91 + }
  92 +
  93 + // Clear queue after processing
  94 + var fw = FileWriter.$new(file, false);
  95 + fw.write("");
  96 + fw.close();
  97 +
  98 + log("Processed " + successCount + "/" + lines.length + " messages, queue cleared");
  99 +
  100 + } catch (e) {
  101 + log("Queue processing error: " + e);
  102 + }
  103 +}
  104 +
  105 +// ── Main loop ──
  106 +Java.perform(function () {
  107 + log("Daemon injected into WeChat process");
  108 + log("Watching queue: " + QUEUE_FILE);
  109 + log("Send API: u01.r1.a(talker) -> q1.e(content) -> q1.h(0) -> q1.a() -> m1.a()");
  110 +
  111 + // Process queue every 500ms
  112 + setInterval(processQueue, 500);
  113 +
  114 + // Also expose manual trigger via rpc
  115 + rpc.exports = {
  116 + send: function(talker, content) {
  117 + sendMessage(talker, content);
  118 + return "queued";
  119 + },
  120 + process: function() {
  121 + processQueue();
  122 + return "done";
  123 + }
  124 + };
  125 +
  126 + log("Daemon ready. RPC exports: send(talker, content), process()");
  127 +});
... ...
wechat_plain/scripts/frida-daemon/run_debug.py 0 → 100644
  1 +import frida
  2 +import sys
  3 +import time
  4 +
  5 +# Connect to USB device
  6 +device = frida.get_usb_device(timeout=10)
  7 +print(f"Connected to: {device.name}")
  8 +
  9 +# Attach to running WeChat
  10 +session = device.attach("com.tencent.mm")
  11 +print(f"Attached to com.tencent.mm")
  12 +
  13 +# Read JS script
  14 +with open("/Users/yibo/Documents/aipcrm/wechat_plain/scripts/frida-daemon/debug_builder_class.js", "r") as f:
  15 + script_source = f.read()
  16 +
  17 +# Load script
  18 +script = session.create_script(script_source)
  19 +
  20 +def on_message(message, data):
  21 + if message["type"] == "send":
  22 + print(f"[Script] {message['payload']}")
  23 + elif message["type"] == "error":
  24 + print(f"[Error] {message['stack']}")
  25 +
  26 +script.on("message", on_message)
  27 +script.load()
  28 +print("Script loaded, waiting for output...")
  29 +
  30 +# Wait for script to complete
  31 +time.sleep(8)
  32 +
  33 +# Cleanup
  34 +session.detach()
  35 +print("Done.")
... ...
wechat_plain/scripts/frida-daemon/run_debug2.py 0 → 100644
  1 +import frida
  2 +import sys
  3 +
  4 +# Try USB device first, fallback to TCP
  5 +try:
  6 + device = frida.get_usb_device(timeout=5)
  7 + print(f"USB device: {device.name}")
  8 +except Exception as e:
  9 + print(f"USB failed: {e}")
  10 + device = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
  11 + print(f"TCP device: {device.name}")
  12 +
  13 +# Try attach by PID directly
  14 +pid = 15059 # current WeChat PID
  15 +print(f"Attaching to PID {pid}...")
  16 +session = device.attach(pid)
  17 +print(f"Attached!")
  18 +
  19 +with open("/Users/yibo/Documents/aipcrm/wechat_plain/scripts/frida-daemon/debug_builder_class.js", "r") as f:
  20 + script_source = f.read()
  21 +
  22 +script = session.create_script(script_source)
  23 +
  24 +def on_message(message, data):
  25 + if message["type"] == "send":
  26 + print(f"[Script] {message['payload']}")
  27 + elif message["type"] == "error":
  28 + print(f"[Error] {message}")
  29 +
  30 +script.on("message", on_message)
  31 +script.load()
  32 +print("Script loaded. Waiting...")
  33 +
  34 +import time
  35 +time.sleep(10)
  36 +print("Done.")
... ...
wechat_plain/scripts/frida-daemon/run_spawn_test.py 0 → 100644
  1 +import frida
  2 +import time
  3 +
  4 +device = frida.get_usb_device(timeout=5)
  5 +print(f"Device: {device.name}")
  6 +
  7 +# Spawn WeChat
  8 +print("Spawning com.tencent.mm...")
  9 +pid = device.spawn(["com.tencent.mm"])
  10 +print(f"Spawned PID: {pid}")
  11 +
  12 +session = device.attach(pid)
  13 +print("Attached to spawned process")
  14 +
  15 +script = session.create_script("""
  16 +console.log("Process spawned, Java.available = " + (typeof Java !== 'undefined' ? Java.available : 'undefined'));
  17 +if (typeof Java !== 'undefined' && Java.available) {
  18 + Java.perform(function() {
  19 + console.log("Java.perform OK");
  20 + try {
  21 + var r1 = Java.use("u01.r1");
  22 + console.log("u01.r1 loaded");
  23 + var builder = r1.a("yibooo");
  24 + var clsName = builder.getClass().getName();
  25 + console.log("BUILDER_CLASS:" + clsName);
  26 + var superCls = builder.getClass().getSuperclass();
  27 + if (superCls) {
  28 + console.log("SUPER_CLASS:" + superCls.getName());
  29 + }
  30 + } catch(e) {
  31 + console.log("Error: " + e);
  32 + }
  33 + });
  34 +} else {
  35 + console.log("Java not available");
  36 +}
  37 +""")
  38 +
  39 +def on_message(message, data):
  40 + print(f"[MSG] {message}")
  41 +
  42 +script.on("message", on_message)
  43 +script.load()
  44 +print("Script loaded, resuming...")
  45 +
  46 +device.resume(pid)
  47 +
  48 +time.sleep(8)
  49 +print("Done")
... ...
wechat_plain/scripts/frida-daemon/run_tcp_test.py 0 → 100644
  1 +import frida
  2 +import time
  3 +
  4 +# Connect via TCP through adb forward
  5 +device = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
  6 +print(f"Connected via TCP: {device.name}")
  7 +
  8 +# Get PID by enumerating processes (or hardcode)
  9 +try:
  10 + processes = device.enumerate_processes()
  11 + wechat_pid = None
  12 + for p in processes:
  13 + if p.name == "com.tencent.mm":
  14 + wechat_pid = p.pid
  15 + print(f"Found WeChat PID: {wechat_pid}")
  16 + break
  17 + if not wechat_pid:
  18 + print("WeChat not found, using hardcoded PID 15059")
  19 + wechat_pid = 27337
  20 +except Exception as e:
  21 + print(f"enumerate_processes failed: {e}")
  22 + wechat_pid = 27337
  23 +
  24 +print(f"Attaching to PID {wechat_pid}...")
  25 +session = device.attach(wechat_pid)
  26 +print("Attached!")
  27 +
  28 +script = session.create_script("""
  29 +console.log("Java.available = " + (typeof Java !== 'undefined' ? Java.available : 'undefined'));
  30 +if (typeof Java !== 'undefined' && Java.available) {
  31 + Java.perform(function() {
  32 + console.log("Java.perform OK");
  33 + try {
  34 + var r1 = Java.use("u01.r1");
  35 + console.log("u01.r1 loaded");
  36 + var builder = r1.a("yibooo");
  37 + var clsName = builder.getClass().getName();
  38 + console.log("BUILDER_CLASS:" + clsName);
  39 + } catch(e) {
  40 + console.log("Error: " + e);
  41 + }
  42 + });
  43 +} else {
  44 + console.log("Java not available");
  45 +}
  46 +""")
  47 +
  48 +def on_message(message, data):
  49 + print(f"[MSG] {message}")
  50 +
  51 +script.on("message", on_message)
  52 +script.load()
  53 +print("Script loaded")
  54 +
  55 +time.sleep(5)
  56 +print("Done")
... ...
wechat_plain/scripts/frida-daemon/run_test_java.py 0 → 100644
  1 +import frida
  2 +import time
  3 +
  4 +device = frida.get_usb_device(timeout=5)
  5 +print(f"Device: {device.name}")
  6 +
  7 +pid = 15059
  8 +session = device.attach(pid)
  9 +print(f"Attached to {pid}")
  10 +
  11 +script = session.create_script("""
  12 +console.log("Java.available = " + (typeof Java !== 'undefined' ? Java.available : 'undefined'));
  13 +if (typeof Java !== 'undefined' && Java.available) {
  14 + Java.perform(function() {
  15 + console.log("Java.perform OK");
  16 + try {
  17 + var r1 = Java.use("u01.r1");
  18 + console.log("u01.r1 loaded");
  19 + var builder = r1.a("yibooo");
  20 + console.log("builder class: " + builder.getClass().getName());
  21 + } catch(e) {
  22 + console.log("Error: " + e);
  23 + }
  24 + });
  25 +} else {
  26 + console.log("Java not available in this process");
  27 +}
  28 +""")
  29 +
  30 +def on_message(message, data):
  31 + print(f"[MSG] {message}")
  32 +
  33 +script.on("message", on_message)
  34 +script.load()
  35 +print("Script loaded")
  36 +
  37 +time.sleep(5)
  38 +print("Done")
... ...
wechat_plain/scripts/local-testkit/setup_termux_boot.sh
... ... @@ -9,9 +9,10 @@
9 9  
10 10 export PATH=/data/data/com.termux/files/usr/bin:$PATH
11 11  
12   -# 启动 frida-server (如果已安装)
13   -if [ -f "/data/local/tmp/frida-server" ]; then
14   - su -c "/data/local/tmp/frida-server &"
  12 +# 启动 frida-server (统一放在 Termux 可访问目录)
  13 +FRIDA_SERVER="/data/data/com.termux/files/home/hermes-wechat/bin/frida-server"
  14 +if [ -f "$FRIDA_SERVER" ]; then
  15 + su -c "$FRIDA_SERVER &"
15 16 fi
16 17  
17 18 # 启动微信 (如果未运行)
... ...
wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java
... ... @@ -41,12 +41,21 @@ public class HermesAccessibilityService extends AccessibilityService {
41 41 private static final String ID_CHAT_SEND = "com.tencent.mm:id/bql"; // 发送按钮 (Button, text="发送")
42 42 private static final String ID_CHAT_TITLE = "com.tencent.mm:id/obn"; // 聊天界面标题栏 (TextView)
43 43 private static final String ID_BUBBLE_TEXT = "com.tencent.mm:id/bkl"; // 消息气泡 TextView (long-clickable=true)
44   - private static final String ID_CHATLIST_ROW = "com.tencent.mm:id/cj1"; // 主界面 chat list 每行的 clickable 容器 (LinearLayout)
45   - private static final String ID_CHATLIST_NAME = "com.tencent.mm:id/kbq"; // chat list 行里显示会话名 / 群名 的 TextView
46 44  
47 45 // 长按气泡后弹出的菜单项文案(WeChat 8.0.72 简中)
48 46 private static final String MENU_QUOTE_TEXT = "引用";
49 47  
  48 + // 封面屏/外屏 (1080×2364) 固定 UI 坐标。
  49 + // 折叠态封面屏下 A11y 节点 boundsInScreen 全为 0,ACTION_CLICK 空操作,
  50 + // 只能用真实屏幕坐标 dispatchGesture 点击。以下坐标按 1080×2364 校准。
  51 + private static final int SEARCH_ICON_X = 900, SEARCH_ICON_Y = 220; // 主界面右上搜索放大镜
  52 + private static final int SEARCH_BOX_X = 560, SEARCH_BOX_Y = 250; // 搜索页输入框(聚焦后长按粘贴)
  53 + private static final int PASTE_BUBBLE_X = 150, PASTE_BUBBLE_Y = 405; // 长按弹出的「粘贴」气泡
  54 + private static final int RESULT_FIRST_X = 300, RESULT_FIRST_Y = 550; // 首条搜索结果(最常使用/联系人)
  55 + private static final int CHAT_INPUT_X = 400, CHAT_INPUT_Y = 2250; // 聊天页底部输入框(键盘放下时长按)
  56 + private static final int CLIP_CHIP_X = 250, CLIP_CHIP_Y = 1555; // 聚焦后 IME 剪贴板候选(点它插入文本)
  57 + private static final int SEND_BTN_X = 1000, SEND_BTN_Y = 1410; // 键盘弹起时输入条右侧绿色「发送」按钮
  58 +
50 59 private final Handler mainHandler = new Handler(Looper.getMainLooper());
51 60  
52 61 private String pendingToUser = null; // wxid / username (for verification)
... ... @@ -54,8 +63,6 @@ public class HermesAccessibilityService extends AccessibilityService {
54 63 private String pendingContent = null;
55 64 private String pendingQuoteText = null; // 要引用的消息片段(substring 匹配 bkl text),null=不引用
56 65 private String pendingAtSender = null; // 要 @ 的群成员显示名,null=不 @
57   - private int chatListScrolls = 0; // chat list 滚动查找次数(每条消息重置)
58   - private static final int MAX_CHATLIST_SCROLLS = 6; // 滚动上限,超了才退搜索
59 66  
60 67 @Override
61 68 protected void onServiceConnected() {
... ... @@ -110,378 +117,75 @@ public class HermesAccessibilityService extends AccessibilityService {
110 117 pendingContent = content;
111 118 pendingQuoteText = (quoteText != null && !quoteText.isEmpty()) ? quoteText : null;
112 119 pendingAtSender = (atSender != null && !atSender.isEmpty()) ? atSender : null;
113   - chatListScrolls = 0;
114 120  
115   - // 拉起微信主界面(如果不在则会启动;如果在则保留当前 fragment 状态)
  121 + // Bring WeChat to foreground (clear task to ensure main screen)
116 122 Intent launch = getPackageManager().getLaunchIntentForPackage("com.tencent.mm");
117 123 if (launch != null) {
118   - launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
  124 + launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
119 125 startActivity(launch);
120 126 }
121 127  
122   - // 等微信前台后启动状态机
123   - mainHandler.postDelayed(() -> driveStateMachine(0), 3500);
124   - }
125   - };
126   -
127   - // =====================================================================
128   - // WeChat UI state machine
129   - // =====================================================================
130   - //
131   - // 微信能停留的页面种类多(主 tab × 4、聊天页正/错、各类子页、不在微信),
132   - // 旧代码用嵌套 if + relaunch 拼凑容易死角。状态机思路:
133   - //
134   - // 1. classifyState(root) 把当前 root 归到 6 个枚举之一
135   - // 2. switch dispatch 到对应 handler:执行一次动作(点击 / BACK / 启动)
136   - // 3. 动作后 postDelayed 一段时间,递归 driveStateMachine 再 classify
137   - // 4. 总跳转次数 cap 防止死循环(异常状态走 resetState 兜底)
138   - //
139   - // 状态划分:
140   - // TARGET_CHAT bkk + obn.text == displayName → 直接 type/send
141   - // WRONG_CHAT bkk + obn.text != displayName → BACK 一次
142   - // CHAT_LIST 无 bkk,kbq 多个 → 点 cj1 row
143   - // NO_CHAT_VIEW 无 bkk 无 kbq,仍在 com.tencent.mm → 点底部"微信"tab,
144   - // 否则 BACK
145   - // OUTSIDE_WECHAT root.pkg != com.tencent.mm → startActivity
146   - // =====================================================================
147   -
148   - private static final int MAX_STATE_TRANSITIONS = 6;
149   -
150   - private enum WeChatState {
151   - TARGET_CHAT, WRONG_CHAT, CHAT_LIST, NO_CHAT_VIEW, OUTSIDE_WECHAT
152   - }
153   -
154   - private WeChatState classifyState(AccessibilityNodeInfo root) {
155   - if (root == null) return WeChatState.OUTSIDE_WECHAT;
156   - CharSequence pkg = root.getPackageName();
157   - if (pkg == null || !"com.tencent.mm".contentEquals(pkg)) {
158   - return WeChatState.OUTSIDE_WECHAT;
159   - }
160   -
161   - AccessibilityNodeInfo bkk = findNodeById(root, ID_CHAT_EDIT);
162   - if (bkk != null) {
163   - AccessibilityNodeInfo title = findNodeById(root, ID_CHAT_TITLE);
164   - boolean isTarget = false;
165   - if (title != null) {
166   - CharSequence raw = title.getText();
167   - String titleText = (raw != null) ? raw.toString() : "";
168   - // 群聊标题带成员数后缀(如 "宜创AI全员群(60)"),剥掉 "(数字)" 再比对
169   - String normalized = titleText.replaceAll("\\(\\d+\\)$", "").trim();
170   - Log.d(TAG, "classifyState: obn.text=" + titleText + " normalized=" + normalized
171   - + " target=" + pendingToUserDisplayName);
172   - isTarget = pendingToUserDisplayName != null
173   - && (pendingToUserDisplayName.equals(titleText)
174   - || pendingToUserDisplayName.equals(normalized));
175   - title.recycle();
176   - }
177   - bkk.recycle();
178   - return isTarget ? WeChatState.TARGET_CHAT : WeChatState.WRONG_CHAT;
179   - }
180   -
181   - List<AccessibilityNodeInfo> names = root.findAccessibilityNodeInfosByViewId(ID_CHATLIST_NAME);
182   - int kbqCount = (names == null ? 0 : names.size());
183   - if (names != null) for (AccessibilityNodeInfo n : names) n.recycle();
184   - if (kbqCount > 0) return WeChatState.CHAT_LIST;
185   -
186   - return WeChatState.NO_CHAT_VIEW;
187   - }
188   -
189   - private void driveStateMachine(int transitionCount) {
190   - if (transitionCount >= MAX_STATE_TRANSITIONS) {
191   - Log.w(TAG, "State machine cap reached (" + MAX_STATE_TRANSITIONS + "), giving up");
192   - resetState();
193   - return;
194   - }
195   -
196   - AccessibilityNodeInfo root = getRootInActiveWindow();
197   - WeChatState st = classifyState(root);
198   - Log.i(TAG, "FSM[" + transitionCount + "] state=" + st
199   - + " target=" + pendingToUserDisplayName);
200   -
201   - switch (st) {
202   - case TARGET_CHAT: {
203   - Log.i(TAG, "FSM: in target chat, performTypeAndSend");
204   - performTypeAndSend(root);
205   - return;
206   - }
207   -
208   - case CHAT_LIST: {
209   - AccessibilityNodeInfo row = findChatListRow(root, pendingToUserDisplayName);
210   - if (row != null) {
211   - Log.i(TAG, "FSM: clicking chat list row for " + pendingToUserDisplayName);
212   - row.performAction(AccessibilityNodeInfo.ACTION_CLICK);
213   - row.recycle();
214   - root.recycle();
215   - mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 2200);
216   - } else if (chatListScrolls < MAX_CHATLIST_SCROLLS) {
217   - // A 方案:目标不在可见列表 → 滚动列表逐屏找(会话一定在列表里,只是被挤下去)
218   - chatListScrolls++;
219   - Log.i(TAG, "FSM: " + pendingToUserDisplayName + " not visible, scrolling chat list ("
220   - + chatListScrolls + "/" + MAX_CHATLIST_SCROLLS + ")");
221   - root.recycle();
222   - // 滚动不计入 transitionCount(用 chatListScrolls 自己的上限防死循环)
223   - scrollChatListDown(() -> mainHandler.postDelayed(
224   - () -> driveStateMachine(transitionCount), 700));
225   - } else {
226   - // C 方案:滚到底还找不到 → 退回搜索
227   - Log.w(TAG, "FSM: " + pendingToUserDisplayName
228   - + " not found after " + chatListScrolls + " scrolls, falling back to search");
229   - performSearch(root);
230   - // performSearch owns root from here
  128 + // 封面屏上 A11y 标题/节点动作都失效,无法可靠判断"是否已在目标聊天",
  129 + // 故一律:CLEAR_TASK 拉到主界面 → 坐标搜索流(performSearch)。
  130 + mainHandler.postDelayed(() -> {
  131 + AccessibilityNodeInfo root = getRootInActiveWindow();
  132 + if (root == null) {
  133 + Log.w(TAG, "Root is null after launch");
  134 + resetState();
  135 + return;
231 136 }
232   - return;
233   - }
234   -
235   - case WRONG_CHAT: {
236   - Log.i(TAG, "FSM: in wrong chat, GLOBAL_ACTION_BACK");
  137 + Log.i(TAG, "Root package=" + root.getPackageName() + ", starting search");
  138 + performSearch(root);
237 139 root.recycle();
238   - performGlobalAction(GLOBAL_ACTION_BACK);
239   - mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 800);
240   - return;
241   - }
242   -
243   - case NO_CHAT_VIEW: {
244   - // 优先点底部"微信"tab;点不了就 BACK
245   - AccessibilityNodeInfo weixinTab = findBottomNavTab(root, "微信");
246   - root.recycle();
247   - if (weixinTab != null) {
248   - Log.i(TAG, "FSM: NO_CHAT_VIEW, clicking bottom '微信' tab");
249   - weixinTab.performAction(AccessibilityNodeInfo.ACTION_CLICK);
250   - weixinTab.recycle();
251   - mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 800);
252   - } else {
253   - Log.i(TAG, "FSM: NO_CHAT_VIEW, no '微信' tab, GLOBAL_ACTION_BACK");
254   - performGlobalAction(GLOBAL_ACTION_BACK);
255   - mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 800);
256   - }
257   - return;
258   - }
259   -
260   - case OUTSIDE_WECHAT: {
261   - Log.i(TAG, "FSM: outside WeChat, launching LauncherUI");
262   - if (root != null) root.recycle();
263   - Intent re = getPackageManager().getLaunchIntentForPackage("com.tencent.mm");
264   - if (re != null) {
265   - re.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
266   - startActivity(re);
267   - }
268   - mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 3500);
269   - return;
270   - }
271   - }
272   - }
273   -
274   - /** 在 root.findAccessibilityNodeInfosByViewId(ID_CHATLIST_NAME) 里找 displayName 严格匹配,
275   - * 向上回溯到 cj1 row 容器作为可点击目标。 */
276   - private AccessibilityNodeInfo findChatListRow(AccessibilityNodeInfo root, String displayName) {
277   - if (root == null || displayName == null) return null;
278   - List<AccessibilityNodeInfo> names = root.findAccessibilityNodeInfosByViewId(ID_CHATLIST_NAME);
279   - if (names == null) return null;
280   - AccessibilityNodeInfo matched = null;
281   - for (AccessibilityNodeInfo n : names) {
282   - CharSequence t = n.getText();
283   - if (matched == null && t != null && displayName.equals(t.toString())) {
284   - // 向上找 cj1
285   - AccessibilityNodeInfo cur = n;
286   - while (cur != null) {
287   - CharSequence id = cur.getViewIdResourceName();
288   - if (id != null && ID_CHATLIST_ROW.equals(id.toString())) {
289   - matched = cur;
290   - break;
291   - }
292   - AccessibilityNodeInfo parent = cur.getParent();
293   - if (cur != n) cur.recycle();
294   - cur = parent;
295   - }
296   - // fallback: 任意 clickable 祖先
297   - if (matched == null) {
298   - AccessibilityNodeInfo c = n;
299   - while (c != null) {
300   - if (c.isClickable()) { matched = c; break; }
301   - AccessibilityNodeInfo p = c.getParent();
302   - if (c != n) c.recycle();
303   - c = p;
304   - }
305   - }
306   - continue;
307   - }
308   - n.recycle();
  140 + }, 3500);
309 141 }
310   - return matched;
311   - }
312   -
313   - /** 找底部 nav 的指定文本 tab("微信" / "通讯录" / "发现" / "我")的可点击祖先。
314   - * 这些 tab 可能 text 同名节点不止一个(如标题栏也叫"微信"),优先选屏幕**最下方**的。 */
315   - private AccessibilityNodeInfo findBottomNavTab(AccessibilityNodeInfo root, String tabText) {
316   - if (root == null || tabText == null) return null;
317   - List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(tabText);
318   - if (nodes == null || nodes.isEmpty()) return null;
319   -
320   - AccessibilityNodeInfo bestClickable = null;
321   - int bestY = -1;
322   - for (AccessibilityNodeInfo n : nodes) {
323   - CharSequence t = n.getText();
324   - if (t == null || !tabText.equals(t.toString())) { n.recycle(); continue; }
325   -
326   - Rect b = new Rect();
327   - n.getBoundsInScreen(b);
328   -
329   - // 向上找 clickable 祖先
330   - AccessibilityNodeInfo clickable = null;
331   - AccessibilityNodeInfo cur = n;
332   - while (cur != null) {
333   - if (cur.isClickable()) { clickable = cur; break; }
334   - AccessibilityNodeInfo p = cur.getParent();
335   - if (cur != n) cur.recycle();
336   - cur = p;
337   - }
338   - if (clickable == null) {
339   - n.recycle();
340   - continue;
341   - }
342   -
343   - // 选 Y 坐标最大的(即屏幕最下方的那个 = 底部 nav)
344   - if (b.top > bestY) {
345   - if (bestClickable != null) bestClickable.recycle();
346   - bestClickable = clickable;
347   - bestY = b.top;
348   - } else {
349   - if (clickable != n) clickable.recycle();
350   - }
351   - // n 已被 clickable 链接管理或在 if 上面 recycle 过
352   - }
353   - return bestClickable;
354   - }
  142 + };
355 143  
356 144 // =====================================================================
357   - // Search for contact (fallback when chat list doesn't contain target)
  145 + // Search for contact
358 146 // =====================================================================
359 147  
360 148 private void performSearch(AccessibilityNodeInfo root) {
361   - // Debug: list all clickable nodes
362   - List<AccessibilityNodeInfo> allClickable = root.findAccessibilityNodeInfosByViewId("*");
363 149 Log.d(TAG, " performSearch: root child count=" + root.getChildCount());
364   -
365   - // Click search button on main UI
366   - AccessibilityNodeInfo searchBtn = findNodeById(root, ID_SEARCH_BTN);
367   - if (searchBtn != null) {
368   - Log.i(TAG, "Clicking search button");
369   - searchBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK);
370   - searchBtn.recycle();
371   -
372   - // Wait for search UI, then type contact name
373   - mainHandler.postDelayed(() -> {
374   - AccessibilityNodeInfo searchRoot = getRootInActiveWindow();
375   - if (searchRoot != null) {
376   - typeIntoSearch(searchRoot);
377   - searchRoot.recycle();
378   - }
379   - }, 800);
380   - } else {
381   - Log.w(TAG, "Search button not found");
382   - resetState();
383   - }
  150 + // 坐标点搜索图标:ACTION_CLICK 在封面屏零坐标节点上空操作,必须用真实坐标
  151 + Log.i(TAG, "Tapping search icon by coord (" + SEARCH_ICON_X + "," + SEARCH_ICON_Y + ")");
  152 + tapXY(SEARCH_ICON_X, SEARCH_ICON_Y, () -> mainHandler.postDelayed(() -> {
  153 + AccessibilityNodeInfo searchRoot = getRootInActiveWindow();
  154 + if (searchRoot != null) {
  155 + typeIntoSearch(searchRoot);
  156 + searchRoot.recycle();
  157 + }
  158 + }, 1200));
384 159 }
385 160  
386 161 private void typeIntoSearch(AccessibilityNodeInfo root) {
387   - AccessibilityNodeInfo searchInput = findNodeById(root, ID_SEARCH_INPUT);
388   - if (searchInput != null) {
389   - Log.i(TAG, "Typing contact name: " + pendingToUserDisplayName);
390   - Bundle args = new Bundle();
391   - args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, pendingToUserDisplayName);
392   - searchInput.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
393   - searchInput.recycle();
394   -
395   - // Wait for results, then click first match
396   - mainHandler.postDelayed(() -> {
397   - AccessibilityNodeInfo resultRoot = getRootInActiveWindow();
398   - if (resultRoot != null) {
399   - clickFirstSearchResult(resultRoot);
400   - resultRoot.recycle();
401   - }
402   - }, 800);
403   - } else {
404   - Log.w(TAG, "Search input not found");
405   - resetState();
406   - }
  162 + // 封面屏 SET_TEXT 失效,改:设剪贴板 → 点搜索框聚焦 → 长按弹「粘贴」→ 点「粘贴」气泡
  163 + setClipboard(pendingToUserDisplayName);
  164 + tapXY(SEARCH_BOX_X, SEARCH_BOX_Y, () -> mainHandler.postDelayed(() ->
  165 + dispatchLongPress(SEARCH_BOX_X, SEARCH_BOX_Y, 700, () -> mainHandler.postDelayed(() ->
  166 + tapXY(PASTE_BUBBLE_X, PASTE_BUBBLE_Y, () -> mainHandler.postDelayed(
  167 + this::clickFirstSearchResult, 1500)),
  168 + 500)),
  169 + 500));
407 170 }
408 171  
409   - private void clickFirstSearchResult(AccessibilityNodeInfo root) {
410   - List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(pendingToUserDisplayName);
411   - for (AccessibilityNodeInfo node : nodes) {
412   - // Skip the search input box itself
413   - CharSequence nodeId = node.getViewIdResourceName();
414   - if (nodeId != null && nodeId.toString().equals(ID_SEARCH_INPUT)) {
415   - node.recycle();
416   - continue;
417   - }
418   -
419   - // Find the clickable ancestor (the actual list item)
420   - AccessibilityNodeInfo target = node;
421   - while (target != null && !target.isClickable()) {
422   - AccessibilityNodeInfo parent = target.getParent();
423   - if (target != node) target.recycle();
424   - target = parent;
425   - }
426   -
427   - if (target != null && target.isClickable()) {
428   - Log.i(TAG, "Clicking search result (ancestor): " + pendingToUserDisplayName);
429   - target.performAction(AccessibilityNodeInfo.ACTION_CLICK);
430   - target.recycle();
431   - node.recycle();
432   -
433   - mainHandler.postDelayed(() -> {
434   - AccessibilityNodeInfo chatRoot = getRootInActiveWindow();
435   - if (chatRoot != null) {
436   - if (findNodeById(chatRoot, ID_CHAT_EDIT) != null) {
437   - Log.i(TAG, "Chat UI loaded, typing message");
438   - performTypeAndSend(chatRoot);
439   - } else {
440   - Log.w(TAG, "Chat UI not loaded after search click");
441   - resetState();
442   - }
443   - chatRoot.recycle();
444   - }
445   - }, 2500);
446   - return;
447   - }
448   -
449   - if (target != null) target.recycle();
450   - node.recycle();
451   - }
452   -
453   - Log.w(TAG, "No clickable search result found for " + pendingToUserDisplayName);
454   - resetState();
  172 + /** 坐标点首条搜索结果(最常使用/联系人),进聊天后写入文本并发送。 */
  173 + private void clickFirstSearchResult() {
  174 + Log.i(TAG, "Tapping first search result by coord (" + RESULT_FIRST_X + "," + RESULT_FIRST_Y + ")");
  175 + tapXY(RESULT_FIRST_X, RESULT_FIRST_Y, () -> mainHandler.postDelayed(this::coordSendInChat, 2200));
455 176 }
456 177  
457   - // =====================================================================
458   - // Step 3+4: (optional quote) → (optional @ mention) → type message → click send
459   - // =====================================================================
460   -
461   - private void performTypeAndSend(AccessibilityNodeInfo root) {
462   - // 如果需要引用某条消息,先长按它弹菜单
463   - if (pendingQuoteText != null) {
464   - Log.i(TAG, "Quote mode: looking for bubble containing \"" +
465   - pendingQuoteText.substring(0, Math.min(30, pendingQuoteText.length())) + "\"");
466   - AccessibilityNodeInfo bubble = findBubbleByPartialText(root, pendingQuoteText);
467   - if (bubble != null) {
468   - Rect bounds = new Rect();
469   - bubble.getBoundsInScreen(bounds);
470   - bubble.recycle();
471   - Log.i(TAG, "Found bubble at " + bounds + ", dispatching long-press gesture");
472   - // 用 dispatchGesture 派发真实的 down+hold+up,因为 WeChat 用 GestureDetector
473   - // 实现长按,AccessibilityNodeInfo.ACTION_LONG_CLICK 触发不了它
474   - dispatchLongPress(bounds.centerX(), bounds.centerY(), 800,
475   - () -> mainHandler.postDelayed(this::clickQuoteMenuItem, 400));
476   - return;
477   - } else {
478   - Log.w(TAG, "Quote target bubble not found, falling back to plain send");
479   - // 找不到引用目标就降级为普通文本发送
480   - pendingQuoteText = null;
481   - }
482   - }
483   -
484   - proceedToInputAndSend(root);
  178 + /** 聊天页内:设剪贴板(内容) → 点输入框聚焦 → 长按弹粘贴 → 点粘贴 → 点发送(全坐标/合成)。 */
  179 + private void coordSendInChat() {
  180 + setClipboard(pendingContent);
  181 + // 点输入框聚焦 → 键盘弹起 + IME 剪贴板候选出现 → 点候选插入文本 → 点绿色「发送」
  182 + tapXY(CHAT_INPUT_X, CHAT_INPUT_Y, () -> mainHandler.postDelayed(() ->
  183 + tapXY(CLIP_CHIP_X, CLIP_CHIP_Y, () -> mainHandler.postDelayed(() ->
  184 + tapXY(SEND_BTN_X, SEND_BTN_Y, () -> {
  185 + Log.i(TAG, "Send button tapped — done");
  186 + resetState();
  187 + }), 1000)),
  188 + 1300));
485 189 }
486 190  
487 191 /**
... ... @@ -515,14 +219,24 @@ public class HermesAccessibilityService extends AccessibilityService {
515 219 }
516 220 }
517 221  
518   - /** 上滑会话列表(露出下面更多会话),用合成手势。完成后回调。 */
519   - private void scrollChatListDown(Runnable onCompleted) {
  222 + /** 设系统剪贴板 — 封面屏上 SET_TEXT 失效,改用 剪贴板+长按粘贴 合成输入。 */
  223 + private void setClipboard(String text) {
  224 + try {
  225 + android.content.ClipboardManager cm =
  226 + (android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
  227 + cm.setPrimaryClip(android.content.ClipData.newPlainText("hermes", text));
  228 + Log.i(TAG, "Clipboard set len=" + (text != null ? text.length() : 0));
  229 + } catch (Exception e) {
  230 + Log.w(TAG, "setClipboard failed: " + e);
  231 + }
  232 + }
  233 +
  234 + /** 坐标短按(点击)— 合成手势,绕开 ACTION_CLICK 在零坐标节点上的空操作。 */
  235 + private void tapXY(int x, int y, Runnable onCompleted) {
520 236 Path path = new Path();
521   - // 从屏幕中下部往上滑(露出列表下方的会话);坐标按竖屏估算,避开顶部搜索栏和底部 nav
522   - path.moveTo(540, 1500);
523   - path.lineTo(540, 600);
  237 + path.moveTo(x, y);
524 238 GestureDescription gesture = new GestureDescription.Builder()
525   - .addStroke(new GestureDescription.StrokeDescription(path, 0, 300))
  239 + .addStroke(new GestureDescription.StrokeDescription(path, 0, 60))
526 240 .build();
527 241 boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() {
528 242 @Override public void onCompleted(GestureDescription g) { if (onCompleted != null) onCompleted.run(); }
... ... @@ -531,395 +245,6 @@ public class HermesAccessibilityService extends AccessibilityService {
531 245 if (!dispatched && onCompleted != null) onCompleted.run();
532 246 }
533 247  
534   - /** 在长按菜单中点击「引用」 — 搜索所有可见窗口(菜单可能在 popup window 里) */
535   - private void clickQuoteMenuItem() {
536   - AccessibilityNodeInfo menuItem = findClickableByTextAllWindows(MENU_QUOTE_TEXT);
537   - if (menuItem != null) {
538   - Log.i(TAG, "Clicking menu item: " + MENU_QUOTE_TEXT);
539   - menuItem.performAction(AccessibilityNodeInfo.ACTION_CLICK);
540   - menuItem.recycle();
541   -
542   - // 引用 chip 注入到输入框,等界面就绪后继续 @ + 文本
543   - mainHandler.postDelayed(() -> {
544   - AccessibilityNodeInfo r = getRootInActiveWindow();
545   - if (r != null) {
546   - proceedToInputAndSend(r);
547   - r.recycle();
548   - }
549   - }, 600);
550   - } else {
551   - Log.w(TAG, "「" + MENU_QUOTE_TEXT + "」 menu item not found in any window, dumping all visible texts");
552   - dumpAllVisibleTexts();
553   - pendingQuoteText = null;
554   - recoverWithPlainSend();
555   - }
556   - }
557   -
558   - /** 遍历所有可见窗口,找 clickable + text=label 的节点 */
559   - private AccessibilityNodeInfo findClickableByTextAllWindows(String label) {
560   - if (label == null) return null;
561   - List<android.view.accessibility.AccessibilityWindowInfo> windows = getWindows();
562   - if (windows == null || windows.isEmpty()) {
563   - AccessibilityNodeInfo r = getRootInActiveWindow();
564   - if (r == null) return null;
565   - AccessibilityNodeInfo result = findClickableByText(r, label);
566   - if (result != r) r.recycle();
567   - return result;
568   - }
569   - for (android.view.accessibility.AccessibilityWindowInfo w : windows) {
570   - AccessibilityNodeInfo r = w.getRoot();
571   - if (r == null) continue;
572   - AccessibilityNodeInfo result = findClickableByText(r, label);
573   - if (result != null) {
574   - if (result != r) r.recycle();
575   - return result;
576   - }
577   - r.recycle();
578   - }
579   - return null;
580   - }
581   -
582   - /** debug:把所有可见窗口里出现的 text 打到 logcat 方便定位真实的菜单文本 */
583   - private void dumpAllVisibleTexts() {
584   - List<android.view.accessibility.AccessibilityWindowInfo> windows = getWindows();
585   - if (windows == null) return;
586   - for (android.view.accessibility.AccessibilityWindowInfo w : windows) {
587   - AccessibilityNodeInfo r = w.getRoot();
588   - if (r == null) continue;
589   - Log.d(TAG, " Window: type=" + w.getType() + " title=" + w.getTitle()
590   - + " root_class=" + r.getClassName() + " childCount=" + r.getChildCount());
591   - collectTexts(r, 0);
592   - r.recycle();
593   - }
594   - }
595   -
596   - private void collectTexts(AccessibilityNodeInfo n, int depth) {
597   - if (n == null || depth > 8) return;
598   - CharSequence t = n.getText();
599   - if (t != null && t.length() > 0 && t.length() < 30) {
600   - Log.d(TAG, " [d=" + depth + " clickable=" + n.isClickable() + "] " + t);
601   - }
602   - for (int i = 0; i < n.getChildCount(); i++) {
603   - AccessibilityNodeInfo c = n.getChild(i);
604   - if (c != null) {
605   - collectTexts(c, depth + 1);
606   - c.recycle();
607   - }
608   - }
609   - }
610   -
611   - /** 引用步骤失败时的回退:重新拿 root,按普通文本发送 */
612   - private void recoverWithPlainSend() {
613   - AccessibilityNodeInfo r = getRootInActiveWindow();
614   - if (r != null) {
615   - proceedToInputAndSend(r);
616   - r.recycle();
617   - } else {
618   - resetState();
619   - }
620   - }
621   -
622   - /** Step 3+4 真正写入文本 + 点发送(必要时先 @ 群成员) */
623   - private void proceedToInputAndSend(AccessibilityNodeInfo root) {
624   - AccessibilityNodeInfo edit = findNodeById(root, ID_CHAT_EDIT);
625   - if (edit == null) {
626   - Log.w(TAG, "Chat input not found");
627   - resetState();
628   - return;
629   - }
630   -
631   - // If bkk is a container (FrameLayout), find the inner EditText
632   - if (!"android.widget.EditText".equals(edit.getClassName())) {
633   - Log.d(TAG, "bkk is " + edit.getClassName() + ", looking for inner EditText");
634   - AccessibilityNodeInfo inner = null;
635   - for (int i = 0; i < edit.getChildCount(); i++) {
636   - AccessibilityNodeInfo child = edit.getChild(i);
637   - if (child != null && "android.widget.EditText".equals(child.getClassName())) {
638   - inner = child;
639   - break;
640   - }
641   - if (child != null) child.recycle();
642   - }
643   - edit.recycle();
644   - if (inner == null) {
645   - Log.w(TAG, "No EditText child found inside bkk");
646   - resetState();
647   - return;
648   - }
649   - edit = inner;
650   - }
651   -
652   - // Ensure focus before typing
653   - if (!edit.isFocused()) {
654   - edit.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
655   - }
656   -
657   - // 群里 @ 某成员:输入 "@" 触发选人面板,找名字点击
658   - if (pendingAtSender != null) {
659   - Log.i(TAG, "@ mention: typing @ to trigger picker for " + pendingAtSender);
660   - Bundle atArgs = new Bundle();
661   - atArgs.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "@");
662   - edit.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, atArgs);
663   - // 把 edit 暂存供 typeFinalContent 用
664   - final AccessibilityNodeInfo editRef = edit;
665   - mainHandler.postDelayed(() -> pickMentionMemberAndContinue(editRef), 600);
666   - return;
667   - }
668   -
669   - typeFinalContent(edit);
670   - }
671   -
672   - /** 在 @ 选人面板中找到对应成员并点击;失败则降级(不 @ 只发文本) */
673   - private void pickMentionMemberAndContinue(AccessibilityNodeInfo edit) {
674   - AccessibilityNodeInfo root = getRootInActiveWindow();
675   - if (root == null) {
676   - Log.w(TAG, "@ picker: root null, fallback to plain content");
677   - pendingAtSender = null;
678   - typeFinalContent(edit);
679   - return;
680   - }
681   -
682   - // 选人面板里成员节点文本可能是 "昵称: ZZ" / "ZZ" / 带其它前缀,
683   - // 严格 equals 匹配不上,改用"包含"模糊匹配
684   - AccessibilityNodeInfo memberNode = findMentionMember(root, pendingAtSender);
685   - if (memberNode != null) {
686   - Log.i(TAG, "Clicking @ member: " + pendingAtSender);
687   - memberNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
688   - memberNode.recycle();
689   - root.recycle();
690   -
691   - // 点击后 WeChat 会把 @xxx 文本插入到 EditText 末尾,等一下再继续
692   - mainHandler.postDelayed(() -> {
693   - AccessibilityNodeInfo r = getRootInActiveWindow();
694   - AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null;
695   - if (freshEdit != null) {
696   - appendToEditText(freshEdit, " " + pendingContent);
697   - freshEdit.recycle();
698   - } else if (edit != null) {
699   - appendToEditText(edit, " " + pendingContent);
700   - }
701   - if (r != null) r.recycle();
702   - if (edit != null) edit.recycle();
703   - mainHandler.postDelayed(() -> {
704   - AccessibilityNodeInfo sendRoot = getRootInActiveWindow();
705   - if (sendRoot != null) {
706   - clickSendButton(sendRoot);
707   - sendRoot.recycle();
708   - }
709   - }, 400);
710   - }, 500);
711   - } else {
712   - // 关键:没找到成员时,当前停在"选择提醒的人"整页 / 半屏页上,
713   - // 直接 typeFinalContent 会因为找不到 bkk 卡死在选人页。
714   - // 必须先 GLOBAL_ACTION_BACK 退出选人页回到聊天页,再写文本。
715   - Log.w(TAG, "@ member not found in picker: " + pendingAtSender
716   - + " — pressing BACK to exit picker, then plain content");
717   - root.recycle();
718   - pendingAtSender = null;
719   - if (edit != null) edit.recycle();
720   - performGlobalAction(GLOBAL_ACTION_BACK);
721   - mainHandler.postDelayed(() -> {
722   - AccessibilityNodeInfo r = getRootInActiveWindow();
723   - AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null;
724   - if (r != null) r.recycle();
725   - if (freshEdit != null) {
726   - // @ 已经把 "@" 字符留在输入框,typeFinalContent 用 SET_TEXT 会整体覆盖
727   - typeFinalContent(freshEdit);
728   - } else {
729   - Log.w(TAG, "Fresh edit not found after BACK from picker, abort");
730   - resetState();
731   - }
732   - }, 800);
733   - }
734   - }
735   -
736   - /**
737   - * 在 @ 选人面板里找匹配 name 的可点击成员。
738   - * 面板里成员名节点文本形如 "昵称: ZZ" / "ZZ" / "火星人",可能带前缀,
739   - * 用包含匹配 + 向上找 clickable 祖先(list item)。
740   - */
741   - private AccessibilityNodeInfo findMentionMember(AccessibilityNodeInfo root, String name) {
742   - if (root == null || name == null || name.isEmpty()) return null;
743   - // 先试严格的 findClickableByText(最精确)
744   - AccessibilityNodeInfo exact = findClickableByText(root, name);
745   - if (exact != null) return exact;
746   -
747   - // 再试包含匹配:遍历 findByText(name) 命中的节点,向上找 clickable
748   - List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(name);
749   - if (nodes == null) return null;
750   - AccessibilityNodeInfo result = null;
751   - for (AccessibilityNodeInfo n : nodes) {
752   - if (result == null) {
753   - CharSequence t = n.getText();
754   - if (t != null && t.toString().contains(name)) {
755   - AccessibilityNodeInfo cur = n;
756   - while (cur != null) {
757   - if (cur.isClickable()) { result = cur; break; }
758   - AccessibilityNodeInfo p = cur.getParent();
759   - if (cur != n) cur.recycle();
760   - cur = p;
761   - }
762   - if (result != null) continue; // 别 recycle n(在 result 链上)
763   - }
764   - }
765   - n.recycle();
766   - }
767   - return result;
768   - }
769   -
770   - private void typeFinalContent(AccessibilityNodeInfo edit) {
771   - Log.i(TAG, "Setting chat text: " + pendingContent.substring(0, Math.min(30, pendingContent.length())));
772   - Bundle args = new Bundle();
773   - args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, pendingContent);
774   - boolean typed = edit.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
775   - edit.recycle();
776   -
777   - if (!typed) {
778   - Log.w(TAG, "Failed to set text");
779   - resetState();
780   - return;
781   - }
782   -
783   - // Wait for send button to appear (WeChat shows it when text is non-empty)
784   - mainHandler.postDelayed(() -> {
785   - AccessibilityNodeInfo sendRoot = getRootInActiveWindow();
786   - if (sendRoot != null) {
787   - clickSendButton(sendRoot);
788   - sendRoot.recycle();
789   - }
790   - }, 400);
791   - }
792   -
793   - /** 在已有 @chip 的 EditText 末尾追加内容(保持 chip 不被覆盖) */
794   - private void appendToEditText(AccessibilityNodeInfo edit, String append) {
795   - CharSequence cur = edit.getText();
796   - String combined = (cur != null ? cur.toString() : "") + append;
797   - Log.i(TAG, "Append to EditText: " + append);
798   - Bundle args = new Bundle();
799   - args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, combined);
800   - edit.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
801   - }
802   -
803   - /** 在 bkk 容器里找内部 EditText(同样的解包逻辑封装一份方便复用) */
804   - private AccessibilityNodeInfo findInnerEditText(AccessibilityNodeInfo root) {
805   - AccessibilityNodeInfo edit = findNodeById(root, ID_CHAT_EDIT);
806   - if (edit == null) return null;
807   - if ("android.widget.EditText".equals(edit.getClassName())) return edit;
808   - for (int i = 0; i < edit.getChildCount(); i++) {
809   - AccessibilityNodeInfo child = edit.getChild(i);
810   - if (child != null && "android.widget.EditText".equals(child.getClassName())) {
811   - edit.recycle();
812   - return child;
813   - }
814   - if (child != null) child.recycle();
815   - }
816   - edit.recycle();
817   - return null;
818   - }
819   -
820   - private void clickSendButton(AccessibilityNodeInfo root) {
821   - // Primary: find by exact ID
822   - AccessibilityNodeInfo sendBtn = findNodeById(root, ID_CHAT_SEND);
823   - if (sendBtn != null) {
824   - Log.i(TAG, "Clicking send button (by ID)");
825   - sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK);
826   - sendBtn.recycle();
827   - resetState();
828   - return;
829   - }
830   -
831   - // Fallback: find by text "发送"
832   - List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText("发送");
833   - for (AccessibilityNodeInfo node : nodes) {
834   - if (node.isClickable()) {
835   - Log.i(TAG, "Clicking send button (by text)");
836   - node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
837   - node.recycle();
838   - resetState();
839   - return;
840   - }
841   - node.recycle();
842   - }
843   -
844   - Log.w(TAG, "Send button not found");
845   - resetState();
846   - }
847   -
848   - // =====================================================================
849   - // Helpers
850   - // =====================================================================
851   -
852   - private AccessibilityNodeInfo findNodeById(AccessibilityNodeInfo root, String viewId) {
853   - List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByViewId(viewId);
854   - if (nodes != null && !nodes.isEmpty()) {
855   - AccessibilityNodeInfo result = nodes.get(0);
856   - for (int i = 1; i < nodes.size(); i++) {
857   - nodes.get(i).recycle();
858   - }
859   - return result;
860   - }
861   - return null;
862   - }
863   -
864   - /**
865   - * 在所有 bkl 消息气泡里找文本包含 partial(子串)的最后一条(最近收到的那条优先)。
866   - */
867   - private AccessibilityNodeInfo findBubbleByPartialText(AccessibilityNodeInfo root, String partial) {
868   - if (root == null || partial == null || partial.isEmpty()) return null;
869   - List<AccessibilityNodeInfo> bubbles = root.findAccessibilityNodeInfosByViewId(ID_BUBBLE_TEXT);
870   - Log.d(TAG, "findBubbleByPartialText: found " + (bubbles == null ? 0 : bubbles.size()) + " bkl bubbles, looking for: " + partial);
871   - if (bubbles == null || bubbles.isEmpty()) return null;
872   - AccessibilityNodeInfo match = null;
873   - // 后面的 bubble 在列表里通常更靠后(也更新),倒序优先
874   - for (int i = bubbles.size() - 1; i >= 0; i--) {
875   - AccessibilityNodeInfo b = bubbles.get(i);
876   - CharSequence t = b.getText();
877   - String tStr = (t != null) ? t.toString() : "(null)";
878   - String preview = tStr.length() > 40 ? tStr.substring(0, 40) + "…" : tStr;
879   - Log.d(TAG, " bubble[" + i + "] text=" + preview);
880   - if (match == null && t != null && t.toString().contains(partial)) {
881   - Log.i(TAG, " matched at index " + i);
882   - match = b;
883   - continue;
884   - }
885   - b.recycle();
886   - }
887   - return match;
888   - }
889   -
890   - /**
891   - * 在 root 子树里找一个 text 等于 label 且 clickable(自身或最近祖先)的节点。
892   - * 用于点击弹出菜单 / 选人面板里的项。
893   - */
894   - private AccessibilityNodeInfo findClickableByText(AccessibilityNodeInfo root, String label) {
895   - if (root == null || label == null) return null;
896   - List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(label);
897   - if (nodes == null || nodes.isEmpty()) return null;
898   - for (AccessibilityNodeInfo n : nodes) {
899   - CharSequence t = n.getText();
900   - if (t == null) { n.recycle(); continue; }
901   - // 严格匹配避免误命中("引用复制删除…" 这类长 text)
902   - if (!label.equals(t.toString())) { n.recycle(); continue; }
903   - // 自身可点就用自身
904   - if (n.isClickable()) return n;
905   - // 否则向上找 clickable 祖先
906   - AccessibilityNodeInfo cur = n;
907   - AccessibilityNodeInfo result = null;
908   - while (cur != null) {
909   - if (cur.isClickable()) { result = cur; break; }
910   - AccessibilityNodeInfo parent = cur.getParent();
911   - if (cur != n) cur.recycle();
912   - cur = parent;
913   - }
914   - if (result != null) {
915   - if (result != n) n.recycle();
916   - return result;
917   - }
918   - n.recycle();
919   - }
920   - return null;
921   - }
922   -
923 248 private void resetState() {
924 249 pendingToUser = null;
925 250 pendingToUserDisplayName = null;
... ...