手机有线自动化

签到、领奖、装应用等重复操作

当前状态
开发中
使用技术
Python · ADB · MCP
适用场景
签到、领奖、装应用等需要人在旁边的重复操作

01

问题

被看见的需求

手机上有一类劳动,量很小、频率很高、毫无智力含量,但必须由人亲手点:

  • 各家 App 的每日签到、领积分、领券;
  • 游戏与云游戏的日常:签到、领体力、领邮件、活动奖励;
  • 新机或新环境的准备:装某个 App、走一遍授权登录、跑完引导;
  • 一句固定的话,发到某个聊天窗口。

单个任务往往只有五到十五次点击,耗时一到两分钟。人工做的成本不在时间,在这件事必须有个人在场。

现成方案为什么不顺手

方案 问题
手机端自动化 App 要在手机上装东西,常要无障碍或 root 权限;脚本录的是固定坐标,界面一改就失效。
Appium / UIAutomator 工程 能做,但要写测试代码、维护页面对象。为了「点一下签到」上这套体系太重。
自研无障碍服务 要开发并侧载一个 APK,等于为了点按钮先写一个 App。
云手机 / 群控平台 要额外付费,账号还得登录在第三方环境里,隐私和资产风险不可控。
让 AI 直接看手机 模型看不见屏幕,也点不了屏幕,缺一条感知与执行的通路。

真正的难点:手机是不可编程的终端

难点一,感知。原生界面可以用系统工具导出控件树,但游戏、云游戏和部分自绘 App 的画面是视频流或画布,控件树里几乎是空的。只看控件树,一半场景直接失明。

难点二,定位。点击命令只要坐标在屏幕范围内就返回成功,点空了它也不会告诉你。靠看图目测比例估坐标会系统性偏移一百到两百像素:云游戏右上角的关闭按钮视觉上在 x≈2065,真实有效位置在 x≈2210,差 145 像素,连续七次点击全部落空,还误开了别的界面。

难点三,输入。普通的点击命令是极短的下压加抬起,在屏幕顶部会被系统手势识别成「下拉通知栏」,手势判赢、点击事件被丢弃;而文本输入只支持 ASCII,中文没有原生通道。

难点四,验证。点击成功、消息发出、奖励到账是三件事。没有验证闭环的自动化,等同于「点了就算做了」,出错还查不出是哪一步。

由此定下的设计目标

  1. 零侵入:只走标准 ADB,不 root、不改手机、不在手机上安装任何自动化 App。
  2. 双通道感知:控件树能抓就抓,抓不到就退回「看图 + 程序化测量坐标」。
  3. 语义优先、坐标兜底:优先按文字或控件编号定位,抗界面改版;实在没有语义才用坐标。
  4. 每步可验证:动作之后必须有判据,失败必须留证据。
  5. 能被 Agent 直接调用:能力以 MCP 工具暴露,模型不需要写脚本就知道怎么用。
  6. 可复用、可定时:跑通的路径能固化成声明式 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>

按文字找到的那个控件点不动。所以要按四级策略去找「该点的那个东西」:

  1. 匹配的节点自身可点且可见,就用它;
  2. 否则向上找几何上包含它、且尺寸合理的最小可点祖先——这是最常见的情况;
  3. 再不行就找它的可点后代;
  4. 都没有,才退回匹配节点本身,由调用方按中心坐标点。

第二步有一条必要的排除:占屏超过 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
          └─ 不符合   → 回到第 ① 步

这条闭环里没有一步可以省。省掉「看」就会盲点;省掉「验」就会把「点了」当成「成了」。签到和领奖这类任务,必须看到「已签到」「已领取」或者按钮状态发生变化才算完成——这条被写成了纪律,不是建议。

没有控件树时怎么点准

游戏和云游戏只能走坐标,于是坐标本身要先标定:

  1. 用最新那张截图,不用上一次的。界面变了,旧坐标必然落空。
  2. 先判断有没有控件树:如果控件树里文字为空、元素只有一两个节点,说明看到的一切都在视频流里,别在控件树上浪费时间。
  3. 程序化测量:游戏按钮通常是亮色叠在暗色背景上。取目标条带,按亮度阈值二值化,按列聚类、间隔超过十二列视为一个图标结束,再按行求上下界,得出每个图标的中心。实测输出了间距整齐(约 110 像素)的一排中心点——间距规整本身就是测量可信的证据。
  4. 裁剪放大目视核对,并把换算公式一起打出来,否则多轮估算会把基准搞混。
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 主动屏蔽无障碍,抓不到控件树,部分界面点不动,这是系统级限制,无解;
  • 息屏时一切失效,任务开头必须先解锁;
  • 中文输入慢,长文本场景不适合走这条路;
  • 云游戏按分钟计费,调试成本是真实成本。

下一步

  1. 游戏内的定位从「亮度阈值 + 列聚类」升级为模板或特征匹配,提高改版后的存活率;
  2. 把常用 App 的日常沉淀成可以分享的 playbook 集合;
  3. 支持无线 ADB 与多设备并发——现在的多设备是「拒绝执行」,需要改成「指定即执行」;
  4. 任务回放:把一次成功任务的截图序列导出成时间线;
  5. 补齐非 Windows 平台的引导脚本。引擎本身已经是跨平台的,缺的是分发和引导。