手机有线自动化
签到、领奖、装应用等重复操作
- 当前状态
- 开发中
- 使用技术
- Python · ADB · MCP
- 适用场景
- 签到、领奖、装应用等需要人在旁边的重复操作
01
问题
被看见的需求
手机上有一类劳动,量很小、频率很高、毫无智力含量,但必须由人亲手点:
- 各家 App 的每日签到、领积分、领券;
- 游戏与云游戏的日常:签到、领体力、领邮件、活动奖励;
- 新机或新环境的准备:装某个 App、走一遍授权登录、跑完引导;
- 一句固定的话,发到某个聊天窗口。
单个任务往往只有五到十五次点击,耗时一到两分钟。人工做的成本不在时间,在这件事必须有个人在场。
现成方案为什么不顺手
| 方案 | 问题 |
|---|---|
| 手机端自动化 App | 要在手机上装东西,常要无障碍或 root 权限;脚本录的是固定坐标,界面一改就失效。 |
| Appium / UIAutomator 工程 | 能做,但要写测试代码、维护页面对象。为了「点一下签到」上这套体系太重。 |
| 自研无障碍服务 | 要开发并侧载一个 APK,等于为了点按钮先写一个 App。 |
| 云手机 / 群控平台 | 要额外付费,账号还得登录在第三方环境里,隐私和资产风险不可控。 |
| 让 AI 直接看手机 | 模型看不见屏幕,也点不了屏幕,缺一条感知与执行的通路。 |
真正的难点:手机是不可编程的终端
难点一,感知。原生界面可以用系统工具导出控件树,但游戏、云游戏和部分自绘 App 的画面是视频流或画布,控件树里几乎是空的。只看控件树,一半场景直接失明。
难点二,定位。点击命令只要坐标在屏幕范围内就返回成功,点空了它也不会告诉你。靠看图目测比例估坐标会系统性偏移一百到两百像素:云游戏右上角的关闭按钮视觉上在 x≈2065,真实有效位置在 x≈2210,差 145 像素,连续七次点击全部落空,还误开了别的界面。
难点三,输入。普通的点击命令是极短的下压加抬起,在屏幕顶部会被系统手势识别成「下拉通知栏」,手势判赢、点击事件被丢弃;而文本输入只支持 ASCII,中文没有原生通道。
难点四,验证。点击成功、消息发出、奖励到账是三件事。没有验证闭环的自动化,等同于「点了就算做了」,出错还查不出是哪一步。
由此定下的设计目标
- 零侵入:只走标准 ADB,不 root、不改手机、不在手机上安装任何自动化 App。
- 双通道感知:控件树能抓就抓,抓不到就退回「看图 + 程序化测量坐标」。
- 语义优先、坐标兜底:优先按文字或控件编号定位,抗界面改版;实在没有语义才用坐标。
- 每步可验证:动作之后必须有判据,失败必须留证据。
- 能被 Agent 直接调用:能力以 MCP 工具暴露,模型不需要写脚本就知道怎么用。
- 可复用、可定时:跑通的路径能固化成声明式 playbook,交给计划任务重复执行。
02
方案
整体架构
使用者 / Agent
│ 自然语言:「把云游戏的每日奖励领了」
▼
Skill 文档层 SKILL.md + references/
│ 策略与经验:先看什么、怎么定位、已知的坑、中文怎么打
▼
MCP 工具层 16 个工具,参数裁剪、异常兜底
│ 参数校验、结构化回传,服务绝不退出
▼
引擎层 Phone / UiTree / Runner / Adb,八个模块
│ 截图、定位、点击、编排
▼
设备层 adb.exe ──USB──> 安卓手机
能力边界:screencap / uiautomator / input / am / pm
分层的关键取舍是一句话:怎么做进代码,该怎么做进文档。引擎只提供原子能力——截图、拿控件树、点、滑、输入、等待,不做业务判断;判断交给 Agent 读文档里的策略。界面改版时改的是文档里的经验,不用重新编译引擎。
一次调用的数据流
Agent 发起 tools/call
→ McpServer.handle() 解析 JSON-RPC,方法路由
→ PhoneService.dispatch() 按工具名分发,裁掉 schema 之外的参数
→ Phone.method() 门面带状态:屏幕尺寸缓存、截图目录、稳定等待
→ Adb.run(["shell", ...]) 统一超时与编码处理
→ adb.exe → USB → 手机内核
← stdout(截图走 exec-out 拿原始 PNG 字节,不做换行转换)
← JSON 序列化后回传,isError 标记业务失败
协议实现刻意做得极小:只实现握手、列工具、调工具、探活四个方法,日志一律走标准错误输出。所有标准输出必须是合法 JSON-RPC——这是这类服务最容易踩的坑,一条无关的打印就能让客户端解析失败。
感知:一次快照同时拿到三样东西
快照一次返回原始截图、扁平化的控件树、当前前台的应用组件。截图坐标就是点击坐标,实测不需要做旋转或缩放换算,这一条省掉了后面几乎所有的对齐问题。
截图落盘时用「时间戳 + 画面内容摘要前八位」命名,同一画面重名覆盖,事后翻查不会堆一堆重复文件。
控件树抓取准备了四条命令、三轮回退:带压缩输出、不带压缩、写文件再读、最后再退一级。不同系统版本上导出行为并不一致,只写一条命令的实现在某些机型上会直接失明。
定位:语义优先,几何启发式兜底
这是整个项目最核心的一段逻辑。安卓上(尤其是 MIUI)的真实结构经常是这样:
<LinearLayout clickable="true"> ← 真正响应点击的容器
<TextView text="每日签到"/> ← 文本挂在这里,自己并不可点
</LinearLayout>
按文字找到的那个控件点不动。所以要按四级策略去找「该点的那个东西」:
- 匹配的节点自身可点且可见,就用它;
- 否则向上找几何上包含它、且尺寸合理的最小可点祖先——这是最常见的情况;
- 再不行就找它的可点后代;
- 都没有,才退回匹配节点本身,由调用方按中心坐标点。
第二步有一条必要的排除:占屏超过 95% 的可点容器不计入。那是滚动容器一类的东西,点它的中心等于盲点屏幕正中,会误触到别处。
另外两处去重是被 MIUI 逼出来的:同一个图标名会重复挂在两三层嵌套节点上、边界完全一致,直接把遍历结果丢给模型,元素清单会被重复项淹没。所以对外描述时按「文字 + 编号 + 边界」去重,取文字映射时按文字去重。
找不到元素时不静默失败:点击工具会返回当前界面的可点元素清单(最多四十条),让 Agent 改选,而不是让它以为点成功了。
输入:边缘用长按,中文用拼音
边缘点击。底层换成「同一点按下并保持 220 毫秒」,系统的手势识别会判定为长按而非滑动,不会被下拉通知栏抢走,对绝大多数按钮等价于一次点击。
中文输入。文本输入只吃 ASCII,中文没有原生通道。这里做了一个明确的产品决策:不安装任何第三方输入法,不用剪贴板旁路,只允许「模拟点击手机自带输入法」这一条路。做法是把输入法切到系统自带,输入一小句拼音让它进入组合态,截图读候选,点第一个候选出字。实测八到十五个字母的小句命中率最高,超过约三十个字母必然断错。
这条约束不是为了炫技,是为了干净:手机上不装东西、不留剪贴板痕迹、不需要广播注入。代价是慢,所以它被写进工作纪律——即使候选很难挑,也只能拆更短的小句继续点,不许换通道。
验证:动作之后必须有判据
- 等界面稳定:连续抓屏比对指纹,连续两次相同(间隔 0.4 秒)即认为动画或加载结束。
- 等某段文字出现:轮询控件树,比如等「签到成功」。
- 硬断言:playbook 里可以断言文字或前台组件,不满足即判失败。
- 失败存证:每一步失败都自动截图,文件名带步骤号。这是排查「App 改版导致脚本失效」的唯一线索。
工具面:十六个工具
工具的说明是写给模型看的,所以写得比较具体,比如明确告诉它「边缘点击失效时改用原地长按」。
| 工具 | 作用 |
|---|---|
| phone_doctor | 环境自检:adb、设备连接、授权、截屏、控件树、模拟输入 |
| phone_devices | 列出所有设备及状态(device / unauthorized / offline) |
| phone_info | 型号、品牌、安卓版本、分辨率、系统版本 |
| phone_screenshot | 截图落盘,并返回可点元素清单 |
| phone_ui | 只抓控件树,比截图快;可选返回原始 XML |
| phone_tap | 点击:文字 / 控件编号 / 无障碍描述 / 坐标,四选一 |
| phone_press_at | 原地长按 220 毫秒,专治屏幕边缘点击被吞 |
| phone_swipe | 整屏滚动(方向),或精确拖拽(四坐标) |
| phone_key | 按键:home / back / enter / del 等三十余个名字,或数字键值 |
| phone_input_text | 输入 ASCII;中文走拼音加点候选 |
| phone_launch | 按包名启动 App |
| phone_packages | 列出已安装包名,可按关键字过滤 |
| phone_activity | 当前前台包名与组件名 |
| phone_wait_text | 轮询等待某段文字出现,用来确认结果 |
| phone_wait_stable | 等待界面停止变化(动画或加载结束) |
| phone_unlock | 亮屏并上滑解锁,可带数字密码 |
同一套能力另有一套命令行(python -m phone_agent shot / tap --text "签到"),输出统一为 JSON,方便人手动排查或写进别的脚本。
自动化面:声明式 playbook
跑通的路径不该只留在对话里。执行器把它固化成 YAML——声明找什么、做什么,而不是写死坐标:
name: 巡星之礼每日领取
package: com.miHoYo.cloudgame.starrail
steps:
- action: wait_stable
- action: tap
text: 点击进入
- action: press_at
xy: [2210, 50] # 贴近屏幕顶部,用长按避免被下拉手势吞掉
- action: wait_text
text: 已领取
- action: assert_activity
package: com.miHoYo.cloudgame.starrail
支持十八种动作:
| 类别 | 动作 |
|---|---|
| 应用控制 | 启动 · 停止 · 解锁 · 亮屏 |
| 点击 | 按文字 / 编号 / 描述 / 坐标点击 · 原地长按 |
| 等待与断言 | 等文字 · 等稳定 · 断言文字 · 断言前台 |
| 输入 | 文本 · 按键 · 滑动 |
| 编排 | 等待 · 截图 · 重复 |
三层防呆:执行前静态校验(缺参数、动作名拼错在跑之前就报出来);空跑模式只打印不碰手机;执行时若声明了包名,会先启动目标 App 再跑脚本,避免点到别的界面上去。整轮报告写成 JSON 落到日志目录,供定时任务事后查。
再把它交给 Windows 计划任务,就得到「每天定时签到」。手机没插的时候会记录「没有已授权设备」并跳过,不会挂死。
工程约束与分发
- 零依赖启动:引导脚本五步走——查 Python、查或准备 adb、验引擎、查设备、打印填好本机绝对路径的配置。换电脑只跑这一个脚本。
- 自包含:adb 跟着一起分发,按四处候选路径依次解析,也可以用环境变量覆盖。
- 多设备保护:检测到多台在线且没有指定设备时直接报错,不猜。点错手机比报错危险得多。
- 版本化分发:打个约 4.3 MB 的压缩包,排除截图与日志,对方解压即用。
03
运行过程
从零到可用的启动链路
① 手机侧 开发者选项 → USB 调试 →(小米另需)USB 调试(安全设置)
→ 数据线连接选「传输文件」→ 弹窗点允许并勾选「一律允许」
② 电脑侧 引导脚本:查 Python → 准备 adb → 引擎自检 → 查设备 → 生成配置
(配置会被自动热加载,不必重启)
③ 自检 逐项确认 adb、连接、授权、截屏、控件树
任一项失败先修它,别急着点
④ 任务 自然语言下发,进入「看 → 想 → 点 → 验」
小米机型上「USB 调试(安全设置)」是最具迷惑性的坑:不开它,点击命令会被系统静默丢弃——不报错、退出码是零、屏幕毫无反应。这个现象和坐标算错了几乎一模一样。
核心循环
① 看 截图 + 元素清单 + 前台应用
│
② 想 有控件树 → 按文字或控件编号定位(最抗改版)
└ 没有(游戏 / 云游戏 / 画布)→ 像素测量定坐标
│
③ 点 普通位置 → 点击
└ 屏幕边缘 → 原地长按
│
④ 验 等文字出现 / 查前台组件 / 再截一张图
│
├─ 符合预期 → 汇报结果,固化成 playbook
└─ 不符合 → 回到第 ① 步
这条闭环里没有一步可以省。省掉「看」就会盲点;省掉「验」就会把「点了」当成「成了」。签到和领奖这类任务,必须看到「已签到」「已领取」或者按钮状态发生变化才算完成——这条被写成了纪律,不是建议。
没有控件树时怎么点准
游戏和云游戏只能走坐标,于是坐标本身要先标定:
- 用最新那张截图,不用上一次的。界面变了,旧坐标必然落空。
- 先判断有没有控件树:如果控件树里文字为空、元素只有一两个节点,说明看到的一切都在视频流里,别在控件树上浪费时间。
- 程序化测量:游戏按钮通常是亮色叠在暗色背景上。取目标条带,按亮度阈值二值化,按列聚类、间隔超过十二列视为一个图标结束,再按行求上下界,得出每个图标的中心。实测输出了间距整齐(约 110 像素)的一排中心点——间距规整本身就是测量可信的证据。
- 裁剪放大目视核对,并把换算公式一起打出来,否则多轮估算会把基准搞混。
python scripts\measure_coords.py 截图.png --region 1500 2340 20 150
最后判断目标是否落在边缘区,是则改用原地长按。
案例:云游戏领奖
真实跑通的一次,全程没有控件树,靠的全是上面的测量流程:
启动云游戏客户端
等云实例加载(网络慢,超时给到 20 秒)
看图找到「开始游戏」→ 原地长按(按钮贴近顶部,普通点击会被手势吞)
等进入游戏画面
关掉「回归玩家」弹窗 —— 用面板自带的关闭按钮,
不按系统返回键,那会弹出「是否退出游戏」
打开活动列表 → 找到「巡星之礼」→ 点「领取」
看到「已领取」才汇报
结果:当天奖励到账,星轨专票一张。云游戏按分钟计费,所以纪律里专门有一条——调试要紧凑,别长时间挂着试错。这条成本是真实的钱。
案例:三十题问卷
这是「能力边界」最诚实的一次测量。目标是在手机浏览器里填一份问卷,二十道单选加十道中文多行文本。没有控件树问题、没有坐标问题,瓶颈在输入法词库:
| 环节 | 实测结果 |
|---|---|
| 拼音长度 | 八到十五个字母最稳;超过三十个字母必断错 |
| 生僻字 | 不在候选首页,要展开、再翻页挑 |
| 同音错选 | 需要人工复核 |
| 焦点丢失 | 没提交的拼音会整句消失,实测丢过整句 |
| 浏览器行为 | 聚焦输入框会放大页面并横向裁切;键盘把目标框顶出视口;按返回键会关掉标签页 |
| 成本 | 一道四五十字的多行题约二十到四十次操作;十道长文本硬打不可行 |
结论写进了文档:中文输入的瓶颈是输入法词库,不是点击精度。长文本只写极简答案并避开生僻词;中途靠问卷自带的「继续上次填写」断点续填,救回了已经填好的二十二题。
这一趟还暴露了一个纯工程 bug:命令参数里带撇号会触发 shell 的引号错误,后面的点击和等待被当成文字整条打进了输入框,污染过一次问卷答案。此后「拼接命令时禁止出现撇号」成了硬规则。
04
演示
录屏还没有。这个项目不太好录,它跑起来就是在点屏幕,录下来是一串指尖动作,看不出它在想什么。
目前能提供的是截图。每一步失败都会自动存一张带步骤号的截图,这比一次成功的录屏更能说明它是怎么工作的——成功的那次看不出边界在哪,失败的才看得出。
想做但还没做的两件事:
- 任务回放:把一次成功任务的截图序列导出成时间线,让人自己复核它到底做了什么。
- 可试玩的版本:现在要装 Python、连数据线、跑引导脚本,离「点一下就能用」还很远。
05
开发日志
三个版本不是规划出来的,是三次真实任务失败逼出来的。
| 版本 | 日期 | 触发任务 | 一句话 |
|---|---|---|---|
| 0.1.1 | 10 月 17 日 | 装 App → 走授权登录 → 发一句「你好」 | 先让流程在新系统上跑起来 |
| 0.1.2 | 10 月 18 日 | 三十题问卷(二十道单选 + 十道中文长文本) | 中文输入的成本被彻底摸清 |
| 0.1.3 | 10 月 18 日 | 同一任务的复盘 | 删掉第三方输入法方案 |
0.1.1 先让它跑起来
第一次端到端任务,暴露的全是环境兼容问题。新系统把前台应用组件的字段改了名,原来的匹配写法匹配不到、退出码是一,导致整个截图功能失效;代码里用了一个新版本才有的方法,在 Python 3.8 上直接报错;中文注释没存成带 BOM 的编码,在 Windows PowerShell 5.1 里全成了乱码;引导脚本还把 adb 输出的提示信息误判成致命错误。
业务侧同时补进文档的还有两条:应用商店的搜索结果里混着广告位和同名 App,点偏一行就会装成别的东西;文本匹配「同意」会模糊命中「不同意」。
0.1.2 把中文输入的成本量化
这一版的核心不是加功能,是改认知。上一版以为中文输入可以用第三方输入法绕过去,实测后重写成「拼音 + 点候选」的完整实操手册:对比出系统自带输入法会进入拼音组合态、点候选出字且无残留,另一个输入法不会组合、点完还残留拼音需要清理;记下了候选栏和翻页键的坐标速查表;给出命中率规则和删除手法分层;把「每打完一小句立刻点候选提交」定为规则——因为焦点一丢,整句拼音直接消失。
0.1.3 删掉一个「聪明」的方案
0.1.2 之后做了一个反向决策:把第三方键盘那套方案整个删掉。理由不是它不能用,是它不干净——要往手机上装一个键盘、要切输入法、要用广播注入文本,事后还得恢复原输入法。为了「点按钮」这个需求在别人手机上装第三方键盘,收益比不上引入的风险。
这是整个项目里唯一一次「用能力换约束」的取舍,事后看不亏:少一条维护线,也少一批潜在的支持问题。
五个值得留档的 bug
| # | 现象 | 根因 | 修法 |
|---|---|---|---|
| 1 | 截图在新系统上整体报错 | 前台组件字段改名,匹配不到导致退出码为一 | 三级回退解析,自己解析组件名 |
| 2 | 加了「改代码自动重启」后服务直接不可用 | 在 Windows 上用系统调用重启会杀掉标准输入输出进程 | 移除自重启,改为手动重载 |
| 3 | 云游戏顶部按钮连点七次全落空 | 坐标目测偏 145 像素,且边缘点击被手势吞掉 | 像素聚类测量 + 边缘改长按 |
| 4 | 问卷答案被后面的命令污染 | 参数里的撇号触发引号错误,后续命令被当文本输入 | 拼接命令禁用撇号;发送前先截图确认 |
| 5 | 部分机型控件树永远抓不到 | 各系统版本的导出行为不一致 | 四条命令三轮回退,失败时明确报「可能被无障碍屏蔽」 |
已知边界
写清楚比藏起来好:
- 游戏和云游戏只能看图定位,一改版就要重新测量;
- 银行、支付类 App 主动屏蔽无障碍,抓不到控件树,部分界面点不动,这是系统级限制,无解;
- 息屏时一切失效,任务开头必须先解锁;
- 中文输入慢,长文本场景不适合走这条路;
- 云游戏按分钟计费,调试成本是真实成本。
下一步
- 游戏内的定位从「亮度阈值 + 列聚类」升级为模板或特征匹配,提高改版后的存活率;
- 把常用 App 的日常沉淀成可以分享的 playbook 集合;
- 支持无线 ADB 与多设备并发——现在的多设备是「拒绝执行」,需要改成「指定即执行」;
- 任务回放:把一次成功任务的截图序列导出成时间线;
- 补齐非 Windows 平台的引导脚本。引擎本身已经是跨平台的,缺的是分发和引导。