Commit 295637a05aa2c51ef5832e8a78ed96c053d4ad95
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>
Showing
11 changed files
with
60643 additions
and
752 deletions
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 +9,10 @@ | ||
| 9 | 9 | ||
| 10 | export PATH=/data/data/com.termux/files/usr/bin:$PATH | 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 | fi | 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,12 +41,21 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 41 | private static final String ID_CHAT_SEND = "com.tencent.mm:id/bql"; // 发送按钮 (Button, text="发送") | 41 | private static final String ID_CHAT_SEND = "com.tencent.mm:id/bql"; // 发送按钮 (Button, text="发送") |
| 42 | private static final String ID_CHAT_TITLE = "com.tencent.mm:id/obn"; // 聊天界面标题栏 (TextView) | 42 | private static final String ID_CHAT_TITLE = "com.tencent.mm:id/obn"; // 聊天界面标题栏 (TextView) |
| 43 | private static final String ID_BUBBLE_TEXT = "com.tencent.mm:id/bkl"; // 消息气泡 TextView (long-clickable=true) | 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 | // 长按气泡后弹出的菜单项文案(WeChat 8.0.72 简中) | 45 | // 长按气泡后弹出的菜单项文案(WeChat 8.0.72 简中) |
| 48 | private static final String MENU_QUOTE_TEXT = "引用"; | 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 | private final Handler mainHandler = new Handler(Looper.getMainLooper()); | 59 | private final Handler mainHandler = new Handler(Looper.getMainLooper()); |
| 51 | 60 | ||
| 52 | private String pendingToUser = null; // wxid / username (for verification) | 61 | private String pendingToUser = null; // wxid / username (for verification) |
| @@ -54,8 +63,6 @@ public class HermesAccessibilityService extends AccessibilityService { | @@ -54,8 +63,6 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 54 | private String pendingContent = null; | 63 | private String pendingContent = null; |
| 55 | private String pendingQuoteText = null; // 要引用的消息片段(substring 匹配 bkl text),null=不引用 | 64 | private String pendingQuoteText = null; // 要引用的消息片段(substring 匹配 bkl text),null=不引用 |
| 56 | private String pendingAtSender = null; // 要 @ 的群成员显示名,null=不 @ | 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 | @Override | 67 | @Override |
| 61 | protected void onServiceConnected() { | 68 | protected void onServiceConnected() { |
| @@ -110,378 +117,75 @@ public class HermesAccessibilityService extends AccessibilityService { | @@ -110,378 +117,75 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 110 | pendingContent = content; | 117 | pendingContent = content; |
| 111 | pendingQuoteText = (quoteText != null && !quoteText.isEmpty()) ? quoteText : null; | 118 | pendingQuoteText = (quoteText != null && !quoteText.isEmpty()) ? quoteText : null; |
| 112 | pendingAtSender = (atSender != null && !atSender.isEmpty()) ? atSender : null; | 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 | Intent launch = getPackageManager().getLaunchIntentForPackage("com.tencent.mm"); | 122 | Intent launch = getPackageManager().getLaunchIntentForPackage("com.tencent.mm"); |
| 117 | if (launch != null) { | 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 | startActivity(launch); | 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 | root.recycle(); | 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 | private void performSearch(AccessibilityNodeInfo root) { | 148 | private void performSearch(AccessibilityNodeInfo root) { |
| 361 | - // Debug: list all clickable nodes | ||
| 362 | - List<AccessibilityNodeInfo> allClickable = root.findAccessibilityNodeInfosByViewId("*"); | ||
| 363 | Log.d(TAG, " performSearch: root child count=" + root.getChildCount()); | 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 | private void typeIntoSearch(AccessibilityNodeInfo root) { | 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,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 | Path path = new Path(); | 236 | Path path = new Path(); |
| 521 | - // 从屏幕中下部往上滑(露出列表下方的会话);坐标按竖屏估算,避开顶部搜索栏和底部 nav | ||
| 522 | - path.moveTo(540, 1500); | ||
| 523 | - path.lineTo(540, 600); | 237 | + path.moveTo(x, y); |
| 524 | GestureDescription gesture = new GestureDescription.Builder() | 238 | GestureDescription gesture = new GestureDescription.Builder() |
| 525 | - .addStroke(new GestureDescription.StrokeDescription(path, 0, 300)) | 239 | + .addStroke(new GestureDescription.StrokeDescription(path, 0, 60)) |
| 526 | .build(); | 240 | .build(); |
| 527 | boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() { | 241 | boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() { |
| 528 | @Override public void onCompleted(GestureDescription g) { if (onCompleted != null) onCompleted.run(); } | 242 | @Override public void onCompleted(GestureDescription g) { if (onCompleted != null) onCompleted.run(); } |
| @@ -531,395 +245,6 @@ public class HermesAccessibilityService extends AccessibilityService { | @@ -531,395 +245,6 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 531 | if (!dispatched && onCompleted != null) onCompleted.run(); | 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 | private void resetState() { | 248 | private void resetState() { |
| 924 | pendingToUser = null; | 249 | pendingToUser = null; |
| 925 | pendingToUserDisplayName = null; | 250 | pendingToUserDisplayName = null; |