内容是「7Tan 手机写的微信消息」。
我没碰屏幕。手机搁在桌上,手指离它十公分,屏幕自己亮了,自己打字,自己按了发送。
先把话说前面:没有 root,没有电脑,没有 adb,没刷机,没装第三方自动化工具。就一台用了两年的华为,Android 12。
网上讲「让 AI 操作手机」的文章我看了不少,绝大多数第一段就写着「需要 root」。我这条路上确实没用,代价是折腾一整天,其中一半时间浪费在几个很蠢的地方。
记下来,省得别人再踩。
先试的四条路,全堵死
一开始我没想改 App。
手机上给 AI 的「身体」是 Termux 里的 Python,它要点屏幕,最直接的办法是调系统命令。我试了四条:
- input tap 500 900,报 java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission
- screencap -p 能执行,但写出来的文件 0 字节
- adb connect 127.0.0.1:5555,Connection refused,无线调试没开
- su,No su program found
顺带查了进程身份:u0_a1125,只有 inet 和 everybody 两个组,跑在 untrusted_app_27 沙箱里。
结论很直白:Android 的设计就是让一个 App 没法操作另一个 App。 这不是配置问题,是架构。想绕,只有三条正规路:无障碍服务、无线调试的 ADB、Shizuku。
后两条我排掉了。它们都要电脑或者重启后重新配对,手机一关就废。无障碍服务是唯一一个开一次、以后开机自动恢复的。
无障碍服务:安卓自己留的门
这功能本来是为视障用户做的。屏幕上的字念出来,按钮替人点。系统级接口,不需要 root,也不是破解。
我在自家 App 里加了一个 Service,结构不复杂:
- App 里跑一个 AccessibilityService,能读当前窗口的控件树,也能 dispatchGesture 模拟点击
- 同一个进程里起一个 HTTP 服务,只听 127.0.0.1 的 19200 端口,带 Token 校验
- Termux 那边的 Python 插件,通过这个端口下指令
监听地址选 127.0.0.1 而不是 0.0.0.0,是为了让同一个 WiFi 下的其他设备也打不进来。
权限边界得讲清楚,这是方案最大的代价。无障碍服务能读到屏幕上的所有内容:聊天记录、银行余额、密码框、短信验证码。 它比相机权限大一个数量级。
我做了三条限制:只监听回环地址;每次请求校验 Token;不做常驻监听,只在收到指令时读一次当前屏幕,不后台收集。
三条限制压得住风险,但消不掉。真正的开关在你手上,系统设置里随时能关,关了我就彻底看不见也动不了。

系统设置里「已安装的服务」,第四项就是那天加进去的。列表里另外两个自动点击类工具都是关着的。
手机打 APK,最坑的是签名
代码写完得打包才能装。
这台机器没有 Android Studio,用的是 Termux。之前已经装好 openjdk、aapt2、d8、apksigner,build-tools 也补齐了。有个细节:Google 只发 linux-x86_64 的二进制,aarch64 上跑不了,aapt/aapt2/aidl 换成了 Termux 原生版,zipalign 干脆用 Python 自己写了一个,手工算 local header,写 0xd935 extra field 做 4 字节对齐。
你以为最麻烦的是编译,其实不是。最麻烦的是装不上。
第一次装,系统弹一句:需要更高版本才能安装。
我去查版本号,手机上 13,我打的是 15。15 大于 13,够了。
于是又抬到 16、17,还是那句话。我一度以为是 EMUI 的安装器有毛病。
真相是签名。
手机里那个 1.3.0 是另一台机器上的 debug key 签的,证书指纹 SHA-256 开头是 51bad0e6。我在手机上重新构建,默认用本机 debug.keystore,指纹开头 c08b2a83。
一把钥匙开一个门。Android 不允许用不同密钥覆盖安装同一个包名,实际报的是 INSTALL_FAILED_UPDATE_INCOMPATIBLE。部分 ROM 把这条错误翻译成「需要更高版本」,这个提示会把人往错的方向带很远。
解决办法不难:让构建脚本用手机里那把钥匙。我在 build.py 里加了环境变量 BOOT_DEBUG_KS,指向手机 Download 目录里的 debug.keystore,构建时打印实际用的路径,一眼看得出用的是哪把。
顺手做的第二件事:每次构建前 rm -rf build 全量重编。之前有过 stale 的 .class 混进去,改的代码没生效,白折腾半小时。
那天一共打了六个包,版本号从 13 一路涨到 20。真正必需的只有签名那一改,版本号是顺手往上推的。
写了按钮,界面上却看不见
这个 bug 值得单独说,因为它暴露的是我的检查方式有问题。
我在主界面加了个「⑤ 开启手机操作(无障碍)」按钮,点了跳系统无障碍设置页。代码写完,编译了,装了。
打开 App,没有那个按钮。
翻源码,handsBtn 这个变量在文件里出现三次:声明、refreshHandsBtn() 里改它的文字、openHandsSettings() 里被调用。看着挺完整。
问题是,从来没有一行代码创建它。没有 button() 调用,没有 addView(),没有 setOnClickListener。
所以 handsBtn 永远是 null,refreshHandsBtn() 第一行 if handsBtn == null return 就直接返回了。
我当时怎么检查的?grep 源码里有没有那个中文字符串。有,就认为做完了。
后来学了正确做法:从签好名的 APK 里把 dex 解出来查。但这里还有个坑。
用 strings 查中文字符串,一条都查不出来。因为 strings 默认只输出 ASCII 可打印字符,中文全被过滤。用 grep -c 统计,结果永远是 0。
我以为按钮没进包,又去改代码。假阴性比真错误更浪费时间,因为它让你怀疑一个本来没问题的地方。 最后换成 grep -a -o -E 直接扫二进制,才看见那行中文确实在里面。
微信和 QQ,不是一个难度
这部分是我最想写的。
服务通了以后,先拿 QQ 试。过程是这样:
- 打开 QQ,2.4 秒后读到 111 个控件
- 会话列表、消息预览、未读数、底部四个 tab,全在
- 点进「我的电脑」,用控件动作点击,一次成功
- 聊天页 17 个控件,输入框 id 是 input,标着可输入、有焦点
- 调 ACTION_SET_TEXT 写进 16 个字,界面直接显示出来
- 点 id 为 send_btn 的按钮
- 消息气泡出现在列表里
从打开到发出,一次成功。太顺了,我以为是运气。

QQ 这边。气泡在右侧,时间是下午 3:10,电脑那端同时收到了。
然后换微信。
界面上明明有东西,控件树是空的。
我把桥接服务返回的原始 JSON 打出来看,五个窗口:
窗口0 type=3 layer=4 pkg=android kids=0
窗口1 type=3 layer=3 pkg=android kids=0
窗口2 type=3 layer=2 pkg=android kids=0
窗口3 type=3 layer=1 pkg=com.android.systemui kids=1
窗口4 type=1 layer=0 pkg=com.tencent.mm focused=True active=True cls=空 kids=0
窗口1 type=3 layer=3 pkg=android kids=0
窗口2 type=3 layer=2 pkg=android kids=0
窗口3 type=3 layer=1 pkg=com.android.systemui kids=1
窗口4 type=1 layer=0 pkg=com.tencent.mm focused=True active=True cls=空 kids=0
微信那个窗口,focused 和 active 都是 True,说明确实抓到了前台窗口。但类名是空字符串,子节点是 0。
同一个服务、同一套代码、同一分钟,四组对照:
- QQ 读到 111 个控件
- 华为桌面读到 15 个
- 7Tan 自己读到 12 个
- 微信读到 0 个
这不是我的 bug,是微信主动把内容对无障碍屏蔽了。 它自己的界面照样能截图,就是不给你读控件。
所以微信不能按文字找按钮。没有锚点,只能按坐标点。
视觉模型给的坐标,不能信
那就截图,让视觉模型看,它告诉我在哪。
结果:描述画面很准,给坐标很离谱。
720 像素宽的截图上,它返回过一次 x=980,超出画面一半。1600 高的图上它说输入框在 y=625,实际位置在 y 附近 1005。我把图切小块放大再问,它还是一直偏左。
后来换了个办法,把「找坐标」这件事从模型身上拿走。
先让程序扫像素,找出每一行列表的中心 y,图上画红线,右侧标上 1 到 10 的行号。然后只问模型一句话:第几行是文件传输助手。
它答第 8 行。我按第 8 行的 y 点下去,一次就中。
这办法能自证。之前我按 y=671 点下去,进的是「服务号」;模型读标注图时,指向「服务号」在第 5 行。两次说的是同一件事,才敢信。

这张就是标注图。红线是程序扫像素找出来的行中心,右边数字是我画上去的。模型只需要回答第几行,不用给坐标。
眼睛负责回答「这是什么」,像素负责回答「在哪里」。混在一起用,两个都会错。
白色气泡把我骗了一次
知道要靠像素之后,我又栽了个跟头。
我在截图里扫出一段特别白的区域,在 y 850 到 935 之间,x 160 到 600,认定了那是输入框,按下去长按。
弹出来的菜单是:提醒、连续朗读、打开。
这是消息气泡的菜单,不是输入框的。那段白色是我之前发给自己的文件消息,背景也是纯白,亮度跟输入框几乎一样。
真输入框长按之后弹的是另一套:粘贴、语音输入、全屏输入、搜一搜、边写边译。

「粘贴」在第一个位置。这条菜单是放大了四倍才读清楚的。
还有个更隐蔽的点。微信的输入框要两步才出键盘:
- 默认是「按住 说话」的语音模式,这时根本没有输入框,只有一个语音按钮
- 先点左下角那个图标切到文字模式
- 再点一下输入框,键盘才弹出来
我一开始是反着做的,直接找输入框点,当然找不到。一直在找那个一次点击就能打字的入口,其实它要两下。
问题出在我拿它跟 QQ 比。QQ 点一下输入框就能打字,我以为所有 App 都一样。微信不是。
剪贴板,只有 App 能写
输入框找到了,怎么把字放进去?
微信控件树是空的,ACTION_SET_TEXT 无从下手。想到剪贴板:先把文字复制进去,长按输入框,点粘贴。
先在 Termux 里试写剪贴板,三条路全堵:
- termux-clipboard-set,直接挂死,超时退出码 124。它是 Termux:API 的命令,本机只装了 com.termux,没有 com.termux.api
- cmd clipboard,命令不存在,无输出
- am broadcast -a clipper.set,广播发出去了,没有任何接收者
根因是 Android 的规则:剪贴板只有应用进程能写。Termux 是一个 App,7Tan 是另一个 App,两个 uid,互不相通。
那就让 App 自己写。我在桥接服务里加了两个路由,十二行代码:
- POST /hands/clip,把文字写进系统剪贴板
- GET /hands/clipget,读回来
放在无障碍检查之前,因为写剪贴板不需要无障碍权限,服务没开也能用。
跑通
到这一步才凑齐。完整链路是这样:
- 写剪贴板:文字是「7Tan 手机写的微信消息」
- 打开微信,进「文件传输助手」
- 点左下角切到文字模式,再点输入框,键盘弹出
- 长按输入框,菜单出来,点「粘贴」
- 文字进了输入框,右边出现「发送」
- 点发送
- 截图

粘贴生效的那一刻。字进了输入框,右侧的「+」自动变成「发送」——这是微信自己判断输入框非空才换的按钮。
最后那张截图里,绿色气泡在右半屏。
为什么强调右半屏?因为微信里自己发的消息靠右,收到的靠左。我用像素量了气泡的边界框:x 从 239 到 614,中心 426,屏幕宽 720,中点是 360。
426 大于 360,是发出去的,不是回显。

微信这边。上一轮还在同一个对话框里折腾了半个小时,就为了等到这个绿气泡。
验收不能看接口返回。接口说成功,可能只是请求送到了。得看屏幕上到底画了什么。
QQ 那边更直接,我在 PC 上收到了消息,内容是「7Tan AI 手机的第一次发言」。那一瞬间比看任何日志都实在。
现在能干到哪一步
说清边界,比说能做多少有用。
能做的:读任何 App 的界面(微信除外)、按文字找控件、按 id 点击、按坐标点击、中文输入、滑动、截图、启动 App。
速度不一样。QQ 从打开到发出 2.4 秒。微信要 20 秒左右,因为每一步都得截图、推理、再点,一步错就重来。
不能做的,我也不打算做:支付、红包、转账、验证码。这类操作点错一次不是重来一次的事。
关于风控,一句话讲完。微信对自动化操作敏感,我做了随机延迟,1.5 到 3 秒不等,不是固定间隔;单次任务最多发一条;不做群发。但账号风险最终是你的,我只能把动作做得像人,不能保证它一定不认得。
还有个现实:微信这条路永远不如 QQ 稳。 它的控件树是空的,我全靠像素和截图在摸,界面一改版就得重新算坐标。QQ 是官方把该给的都给了,按 id 点就行。
如果要给谁发消息,能用 QQ 就用 QQ。
最后
三个晚上,一台没 root 的安卓机,AI 现在能替我干几件小事。
回到开头那个问题:AI 现在缺什么。我觉得不是脑子。它能写、能算、能聊,写出来的东西比我强。它缺的是手,是那个能替你按下按钮的东西。
它什么都会说,就是不会替你按那一下。
这一路真正难的也不是技术。教程里那句「必须 root」,劝退一批人;签名报错那口锅甩给版本号,又劝退一批。拦住人的从来不是机器。
装没装成是小事。知道它能做到哪一步,做不到哪一步,才是这一天换来的东西。 我现在的判断是:日常重复的点击可以交出去,涉及钱和承诺的事,还得自己来。
你手机上最想让 AI 替你点的是哪个按钮?评论区说一个,我挑个靠谱的试试。成了回来汇报,翻车也回来汇报。