夜雨聆风学习资料网

ARTICLE · 1064937

吴小见-安卓端的聊天Jev意图分析助手开发实录

吴小见-安卓端的聊天Jev意图分析助手开发实录

0x00 起点:iOS 版的遗产,Android 的第一堵墙

iOS 版做完,遗产清单很清楚:Jev 类型化判断的接入方式、上下文组装的 token 预算、白名单与节流去重,还有那条宪法——永不自动发送。这些都能平移。

平移不了的,是根基。iOS 靠注入进微信进程,站在消息的必经之路上;Android 上对应的是 Xposed hook——但那条路的账很不好看:要 root 或者刷框架、要碰微信本体、封号风险实实在在,而且微信每次升级,hook 点都得重新考古。

于是拍板换范式:一个普通 App + 系统无障碍服务,"旁挂"在微信旁边。不进你的进程、不碰你的包、不读你的数据库,只借系统给无障碍服务的读屏能力。好处也实打实:微信崩溃面为零(我崩也是崩我自己的 App),封号面最小化(没改你任何东西)。

代价当场标好:iOS 版能把卡片种进消息流,是因为插件就在微信屋里;Android 版进不了屋,产品形态从"消息流内嵌卡"退成"旁挂悬浮面板"。客人变成了窗外举牌的人——先接受这个,再谈别的。

然后,第一堵墙当场就到。

0x01 微信认身份,不认能力

无障碍服务,Android 给辅助功能留的正规通道,读屏是天职。按理说注册一个服务,微信聊天页的每个气泡都该看得见。

实际上,一把 uiautomator dump 梭下去:空树。根节点干干净净,一个子节点都不给。

微信 8.0.52 起对"非白名单"的无障碍服务混淆/隐藏节点。系统自带的 TalkBack 什么都看得见,我的服务什么都看不见——服务开着、权限全给、通道正规,就是瞎。这感觉每个做过安卓逆向的都懂:不是能力问题,是点名问题。

这里面藏着一个非常有意思的判定逻辑:微信认的不是你能不能读,而是你是谁。系统内置辅助服务在豁免名单上,第三方名字不在。

那,能不能顶一个豁免名单上的名字?

把服务的类名注册成系统内置的 com.google.android.accessibility.selecttospeak.SelectToSpeakService("选择即朗读"那个系统组件),微信那边按类名放行,节点树照常给。而 Android 判断组件冲突用的是完整标识 = 本 App 包名 + 类全名——我包名下的同名类,和系统 TalkBack 包下的同名类,各是各的,井水不犯河水。伪装就这么成立:名字是系统的,身体是自己的。

这个思路不是我原创,是社区项目 Jev聊天助手 验证过的公开做法(也是本项目采集与 OCR 实现的参考对象,MIT 开源,在此致谢)。站在前人肩膀上,但老规矩得守两条:

一,别人踩过的坑别再踩一遍。 早期社区有人伪装 TalkBackService 类名——小米上直接触发系统朗读服务误判,屏幕常驻朗读,关都关不掉。要伪装就伪装 SelectToSpeak,别的不碰。

二,别人的结论只能当线索。 "伪装有效"是在别人的设备、别人的微信版本上成立的。落地的唯一方式是自己上手打一遍——这就有了 0x02。

0x02 先验货:别拿别人的结论当天花板

逆向的老规矩:结论不值钱,复现值钱。伪装是整条路线的地基,地基必须自己验。

做法很糙但很硬:写俩服务,代码一模一样,唯一区别是类名——一个顶着系统 SelectToSpeakService 的名号,一个老老实实用自家名字。同一个微信聊天页,广播触发,各 dump 一把,logcat 见真章。

结果一步到位:自家名字的,树空得干干净净;顶系统名号的,气泡 id、文本、左右边距全须全尾——连"哪边是对方"都靠气泡位置分得明明白白。顺手再在系统设置页拍一遍,两组都读得到——排除法收尾:服务本身没瞎,是微信在按名字拦人。

混淆是真的,伪装是真的,且只打"名字不对"的。

验完货,对照组当场裁撤——探针的使命是给路线背书,不是陪产品变老,仓库里现在只剩正式的伪装服务,清清爽爽(这套验证的完整证据链在开源仓库里,验收清单可查)。

风险也如实记账:伪装吃的是"按身份放行"这个判定,哪天微信把校验做细,它就安静失效——读不到而已,不崩、不吞消息。这笔账写在开发计划书里,不藏。

0x03 旁挂的三件套:读得到、想得清、递得回

窗外举牌的人,要有三件套。

读得到——微信适配器。 伪装只是把门敲开,门里的东西还得一行行捡:微信气泡带稳定 id(id/bkl),按气泡的水平位置分"对方/我",标题单独取;白名单过滤、去重、防抖三道闸门伺候着,连发消息不重复分析,同一条消息只进一次上下文。

想得清——Jev 双路。 判断路走类型化 API,7 道判断题一次出齐:危险等级、对方真实意图加置信度、对方需要什么、建议动作、可不可以给实质——模型没有出口胡说的余地,不存在"好的,以下是您要的结果"。回复路由任意 OpenAI 兼容端点起草 3 条有策略差异的候选,再由判断路排序。两路的地址、密钥、模型在设置页各配各的,换供应商下一次调用即生效——换脑子,不换脸,iOS 版的老规矩原样继承。

递得回——一键回填。 候选回复经 ACTION_SET_TEXT 写进微信输入框,失败退剪贴板加粘贴指令。然后停手。发送键永远由人按。

微信聊天页上的悬浮面板:危险等级、真实意图(置信度)、判断标签、候选回复与排序,点「填入」落进输入框。

工程上还有几处小仗:悬浮窗首选 TYPE_ACCESSIBILITY_OVERLAY(免悬浮窗权限),个别 ROM 不认再退 SYSTEM_ALERT_WINDOW;MIUI/HyperOS 杀后台凶,前台保活把进程抬到前台重要级,仍被冻结就切回 App 一次自愈——不过度对抗,如实告知用户;界面这次用 Compose + miuix 做成 HyperOS 原生风格,为此 AI 把整条工具链梭哈到最新(Gradle 9 / AGP 9 / Kotlin 2.4),踩了一晚上坑,此处按下不表。

控制台:服务状态、密钥、目标 App 安装情况一眼可见;权限向导三条——无障碍、悬浮窗、自启动加省电。

记录详情:对话原文、判断结构、候选与排序,每次分析存一条,只存本机。

0x04 红线:一克都没让

iOS 版立的宪法,Android 版原文照抄,还加了一条新的:

  • 永不自动发送:唯一写动作是用户点「填入」的回填;绝不点击发送按钮
  • 不 hook、不注入、不改包、不读数据库:只用系统无障碍与截屏,进程外旁挂
  • 不碰钱:转账、红包、收款码界面元素一律不交互
  • 自动分析默认关(新增):默认手动点悬浮球才分析;想自动的自己去开,且有每日上限——自动化必须是用户知情的选择,不是默认
  • 隐私有闸门:聊天内容只在分析那一刻发给模型接口,不落盘、不进日志;密钥只存本机 App 私有目录,不进日志不进 git

0x05 复盘:范式交换的账本

两个版本放一起,其实就是一道选择题的两个答案:

iOS 版(注入)
Android 版(旁挂)
站位
进程内,消息必经之路
进程外,读屏旁挂
展示
消息流内嵌卡,1:1 贴脸
悬浮面板,隔窗举牌
前置
TrollStore(设备受限)
无(任意机型 Android 11+)
崩溃面
在微信进程里,崩就是微信崩
崩了自己的 App
维护面
微信升级,hook 点重新考古
微信升级,适配器节点重对(成本低一个量级)

没有全胜的选项,只有清楚代价的选项。工程里大多数"路线之争",争的其实不是谁更强,是谁的代价你付得起。

协作方式也和 iOS 版一脉相承:我出眼睛和手,AI 出脑子和手速。这轮里最值钱的反馈依然是那几种——真机上"读到空树"的截图、两组服务对比的 logcat 原文、"面板挡住聊天内容了"这样的现象清单。AI 负责把伪装、适配器一样样立起来,我负责在真机上如实回话:成了就是成了,空树就是空树。一张截图开始的故事,这回用一个"假名字"续上了。

项目已开源:github.com/eatmans/wxjev(含开发计划书与真机验收清单),Release 页可下载 APK(Android 11+)。

附:交流与后续

仅限本人设备、本人有权查看会话的个人研究用途,不商用;伪装是兼容手段,微信更新可能失效,已在文档中如实计入风险。

关注公众号,发送「吴小见」,欢迎和我交流无障碍旁挂、人机协作与两个平台的范式取舍。

相关学习资料