跳到正文

Phone Harness

一套 Python 库加一份 agent 技能,把真机 iPhone 或 Android 交到编码 agent 手里。agent 靠 OCR 或无障碍树读屏,用 HID 事件或 adb 点击、输入,然后回看屏幕确认刚才发生了什么;手机上不装任何东西,也不用越狱——Mac 上手机就是 iPhone Mirroring 窗口,其他平台是 USB 或 Wi-Fi 上的 adb。

Screenshot of Phone Harness
编辑截图, 1 Oct 2026Phone Harness ↗

这是什么

一件把真机放到 agent 工具调用末端的工具。agent 写一小段 Python 脚本——open_app、tap、tap_text、type_text、screenshot、ocr——由它拿真机执行,再把屏幕交回去让 agent 核对刚才发生了什么。两个后端共用同一套 helper。iPhone 那边,Mac 上的 iPhone Mirroring 窗口就是手机:它截取那个窗口,用 Apple 的 Vision 框架读出文字并给出可直接点击的坐标,再以 HID 级别的鼠标与键盘事件完成点击、滑动与输入。Android 那边以 adb 为传输:screencap 出图,手机的无障碍树当文字来源,input 当手,USB 或 Wi-Fi 都行,不需要任何窗口。手机上不装东西,也不用越狱;桌上没有真机时,还有一项托管服务出租能响应同一套 helper 的 Android 手机与 iPhone。

谁做的这个仓库实际上就是一个人:GitHub 账号 ShawnPana 写了 98 次提交里的 97 次,其中 66 次用的是 ucsd.edu 域下的邮箱,recon 报告抓到的贡献者列表上也只有这一个名字。README 说这个项目免费、由作者用自己的时间维护,仓库里有一份资助配置文件,README 也有一节 Sponsor。98 次提交里 52 次带共同作者尾注,其中 48 条写的是某个 Claude 模型——Claude Fable 5、Claude Fable 5.1 或 Claude Opus 5——也就是说大多数提交旁边都挂着一个模型的名字。剩下那一次提交来自未关联账号 ayataro48,GitHub 没有把它对到任何用户上。

它是怎么搭起来的

组成 · 6

它的形状是 agent 与手机之间的一座本地桥,桥墩是手机本身已有的能力。agent 从不直接碰设备:它提交一段 Python 脚本,由 harness 拿一组固定的 helper 去执行,每个 helper 都把「看到了什么」交回来,好让 agent 在走下一步之前先核对上一步。这也是为什么手机上什么都不用装——iPhone Mirroring 本来就是一个接受鼠标与键盘的 Mac 窗口,adb 本来就是一条命令通道——也是为什么仓库里没有配套 App 需要构建、签名、要求人信任。两个平台对不上的地方则刻意分开:一份契约两个后端,iOS 侧用 OCR,因为透过镜像没有树可读,Android 侧用无障碍树;再往上一层是托管,它发出来的手机本来就说 adb,会话里带的是 adb 块,或者一个含 URL 与 token 的 control 链接。

src/phone_harness/
十三个文件、188 KB,整个程序都在这里。最大的是 42 KB 的 android.py,其后是 34 KB 的 cloud.py、29 KB 的 mirror.py、23 KB 的 helpers.py、16 KB 的 background.py、11 KB 的 config.py、8.5 KB 的 ios.py、7.8 KB 的 run.py、7 KB 的 transport.py、5.9 KB 的 admin.py、5 KB 的 telemetry.py、2.1 KB 的 ocr.py,以及一个空的 __init__.py。
helpers.py
提交进来的脚本所用的词汇表,预先导入,所以脚本里只剩下步骤。README、issue 与 pull request 里出现过的名字有 open_app、tap、tap_text、type_text、find_text、screenshot、ocr、screen_info、image_point、tap_image_point、press、home、app_switcher,以及轮询用的 wait_stable,两个后端都得全部应答。
iOS 那条路:mirror.py、background.py、ios.py、ocr.py
截取 iPhone Mirroring 窗口并往里投递事件,外加一个后台模式,目的是让用户在 agent 操作手机时还能继续用 Mac。ocr.py 是架在 Apple Vision 框架上的文字读取器,也是 Linux 或 Windows 上的 Android 主机曾经踩到的那块——它在模块层就导入 Quartz 与 Vision。
外面那一圈命令行
165 字节的启动脚本 phone-harness;run.py 执行从标准输入进来的脚本;config.py 存默认平台与设置;admin.py 是 --doctor 及其权限检查;旁边还有 transport.py 与 telemetry.py。
给 agent 看的文档
17 KB 的 SKILL.md 是日常指南,也是全仓最大的文档,比 README 的四倍还多;5.8 KB 的 install.md 与 4.1 KB 的 onboarding.md 装着安装步骤与那段安装提示词交给 agent 走的流程;agent-workspace/agent_helpers.py 是 773 字节、给 agent 自己工作区用的材料。
tests/ 与 .github/workflows/
三个测试文件、27 KB,其中 18.7 KB 的 test_cloud.py 占大头,另有 6.2 KB 的 test_android.py 和 2.6 KB 的 test_backend_contract.py 把两个后端按同一个接口卡住。1.6 KB 的 ci.yml 与 1.3 KB 的 publish.yml 就是全部自动化,pyproject.toml 是 1.8 KB。

取舍,以及它替代了什么

  • 用 Mac 上的 iPhone Mirroring 窗口来驱动 iPhone 替代 在手机上装一个配套 App,或者要求越狱

    README 把好处和机制写在一起——「No jailbreak, no Xcode, nothing installed on the phone」——并把 Mirroring 描述成把手机渲染成一个转发鼠标与键盘为触摸的 Mac 窗口。代价也写在同一个文档里:解锁手机镜像会暂停,而 harness 只能看见窗口里显示的,这正是 issue 106 讲的事。

  • iPhone 用 OCR 读屏,而不是读视图树 替代 Android 那条走无障碍树的路

    README 把两种来源分开写,而不是声称两边一样:Android 的文字来自手机的无障碍树,iPhone 的文字来自 Apple Vision 框架对截图做识别、返回可点的坐标。价钱也写明了——「OCR sees text, not icons」,没有标签的控件需要截图加一个能看图的模型——而 issue 91 是同一个限制的另一面:识别语言从未设置,中文手机读回来是拉丁字母。

  • 一套 helper 词汇,两个后端 替代 每个平台一套接口

    「Same helpers on both」是 README 的一句话主张,而 config set platform ios|android 选的是默认平台,不是另一套词汇。托管手机以同样条件并入,因为它们是通过 adb 接入的 Android 手机,所以「every helper works on them unchanged」;tests/test_backend_contract.py 的存在就是让这句主张可测,而不是一句愿望。

  • Android 用 adb 当传输,而不是把东西放到设备上 替代 一个跑在手机上的 agent

    README 点出三块:screencap 负责截取、无障碍树负责文字、input 负责手,并说明 USB 或 Wi-Fi 都行、不需要窗口。后果没有藏起来,而是写进 Limits:锁着 PIN 的 Android 需要用户,连接手机永远是用户自己的事。

  • 把托管手机的推荐放在用户自己的手机与 demo 之后 替代 在安装流程里就推荐,排在 Verify 与 Demo 之前

    PR 80 把托管推荐挪到第 6 步,并写下了理由——「a new user was pitched a second phone before seeing their own work」;PR 79 补上这次推荐的条款:一个问题三个答案,登录与是否加入名单都由用户自己在浏览器里完成,除了那一次只读验证之外不花用户的钱。

  • 一次性的事用一次性托管会话 替代 复用已经挂上的那台保存过的手机

    PR 82 把故障和修法写在一起:cloud start --temp 原本会复用已挂上的保存手机,于是「the requested throwaway mode can operate on persistent data」。现在它会另起一个临时会话,让原来那台继续跑并说明这一点,临时创建失败时保留原来的挂载。

依据README.md(文件树里 3,877 字节,含「How it works」与「Limits」两节)、完整的 29 个文件树及其字节体积、install.md、onboarding.md、SKILL.md,以及编号 79、80、82、102、103、104、105 的 pull request 正文。结构一节按文件树体积写成;decisions 只取文档明确写出选择与理由的地方。仓库里没有任何架构文档——recon 报告找过,没有找到。

制作过程

5 个阶段
  1. 01

    七周、三个 release,和一个没人计划的 Android 后端

    仓库建于 2026-08-07,第一次提交当晚落地,标题是「phone-harness v0: iPhone control via iPhone Mirroring」。报告统计到 98 次提交——八月 82 次、九月 16 次——最新一次是 2026-09-20,而 API 记录的最近一次推送是 2026-09-27。仓库有六个 tag(0.1、0.1.0、0.1.1、0.2.0、0.2.1、0.3.0),其中三个是正式 release,而它们的标题就是三步路线图:2026-08-17 的 0.1.0、次日 0.2.0 — Android、2026-09-20 的 0.3.0 — Cloud (Android)。也就是说,树里最老的 iPhone 那条路,在十一天内多了一个 Android 后端,大约一个月后又多了一个托管后端。旁边是 3,120 个星、321 个 fork、16 个 watcher,一个 445 KB 的 Python 包,MIT 许可,topics 是 agent、ai、automation、developer-tools,以及按 API 口径的 55 个未关闭 issue 与 pull request。

  2. 02

    两种接入手机的方式,一套词汇

    README 给两个平台各一段,而文件体积说明力气花在哪:src/phone_harness/android.py 42 KB,是全仓最大的文件,其后是 34 KB 的 cloud.py 与 29 KB 的 mirror.py,而 ocr.py 只有 2.1 KB。Android 的传输是 adb:screencap 出图,手机的无障碍树是文字来源,input 是手,USB 或 Wi-Fi 都行。iPhone 那边手机是一个 Mac 窗口,于是它截取窗口,用 Apple Vision 框架 OCR 出带坐标的文字,再投递 HID 级别事件完成点击、滑动与输入。脚本从标准输入进来,helper 已经预先导入;README 开篇给的就是它的形状:agent 想打开 Weather,调用 find_text("Weather") 得到 (400, 468),点下去,读屏,报告天气已经出来。两个后端共享的是一份契约而不是巧合——tests/test_backend_contract.py 就是用来盯住它的——而 phone-harness config set platform ios|android 选的是默认平台,不是第二套词汇。安装路径是一段粘给 agent 的提示词:克隆到 ~/.phone-harness,先读 install.md,把 phone-harness 放进 PATH,再以 phone-harness skill 为正文注册成一个技能,然后照着 onboarding.md 走一遍。

  3. 03

    权限挂在启动它的那个 App 名下

    在 macOS 上它要 Accessibility 与 Screen Recording,PR 104 解释了旧提示为什么没用:「When the harness is launched from an agent app (Cursor, VS Code, Grok Bot, …), TCC attributes the grant to that host app, not the terminal」——终端自己已经有这两个权限也不管用,而 Accessibility 尤其不会自己弹窗。那份 PR 里的 doctor 会逐个请求缺失的授权、点出该负责的 App 名字、让用户彻底退出再重开,并且只在「only when stdin is a terminal」时最多等 45 秒;在 agent 里它只发请求、打印该做什么、然后返回。--doctor ios --fix 会直接打开对应的设置面板。Android 侧要的是开发者选项和一次 adb 授权,README 把界线写得很直白:「Connecting the phone is always the user’s job」。Limits 一节又补上:锁着 PIN 的 Android 需要用户,解锁 iPhone 会让镜像暂停。托管侧也把自己的规矩写下来:onboarding 里写着「Never sign them up or click Join for them; never spend their credit beyond the proof」,说明生产环境的新用户赠额是 500 美分、前 100 分钟免费,用 --minutes 把单次会话限制在 30 分钟,而 PR 82 让 cloud start --temp 拒绝复用已保存的手机,因为一次性模式原本可以「operate on persistent data」。内容这条边界还在画:仓库里有 telemetry.py,PR 100 把脚本内容、控制台输出与原始报错从使用事件里删掉,同时说明它「does not implement the suggested first-run notice or content opt-in」。

  4. 04

    一整个月的外部 pull request,几乎都还开着

    报告里有三十个 issue 与 pull request,编号从 78 到 108(缺 90),值得注意的是属于作者本人的很少。DivyamTalwar 交来一长串针对 cloud.py、android.py、helpers.py 的加固改动,每一条都注明是针对某个具体提交 88b3642ac4733c47daf12e0f2837bc789eda63c6 复现的,而且大多清楚说明自己没主张什么:分页修复记住访问过的 cursor 并在出现环时报告,同时「without adding an arbitrary page cap or rejecting advancing empty pages」,并被标注为低频防御性加固,「no live service cursor loop was observed」。KingAmo 提的三个 issue 像第二遍审计:测试套件每次都会让机器在假的 watch 地址上真的打开八次浏览器;--doctor ios 恰好在「上一行检查存在的目的就是报告的那种截屏失败」上崩掉;ocr() 从不设置识别语言,于是中文手机识别出来是拉丁乱码。那一条也是三十个条目里唯一有评论的,而评论是一次更正:报告人先假设带地区的宿主语言标签会被拒绝,测过之后发现 ["zh-Hans-CN"] 能正确识别出 22 个框,于是把错误的提示公开改掉而不是留着。别处也有更锋利的报告——henrikra 说 Mirroring 窗口被挡住之后,后台模式里 agent 看到的画面会冻结;D-E-A-G 说某个 pyobjc 调用让 OCR 在 macOS 27 上失效;另两位是带着代码而不是报告来的:Sam780214 在 Arch Linux 上对着华为 NOH-AN00 验了修复,dishanest 用一个新 PR 取代了自己早先那个,改走 Xcode 的 CoreDevice 键盘服务,在 iPhone 上打字而不再抢走 Mac 的焦点。

  5. 05

    作者自己写下的做不到

    README 结尾是四条限制,写法是限制而不是免责声明:解锁 iPhone 会让镜像暂停,锁着 PIN 的 Android 需要用户;「OCR sees text, not icons」,没有标签的控件要靠截图加一个能看图的模型;不支持多点触控,没有相机与 Face ID 流程,DRM 视频是黑屏;连接手机永远是用户自己的事。同一种直觉也体现在改文档上:SKILL.md 从 311 行重排到 272 行且「nothing dropped」,好让一个测试 agent 不必先读完大约 150 行 iPhone Mirroring 的坑才看到托管那一节,新顺序是先讲用哪台手机,再讲对所有手机通用的工作方法,然后是托管、Android、iPhone。onboarding 在一个周末里被改了两次:托管手机的推荐从 Verify 与 Demo 之前挪到之后的第 6 步,理由是「a new user was pitched a second phone before seeing their own work」;另外三个默认观众还在等名单的字符串,在 Clerk 的注册模式当天翻成 Open 时被一并改写。编译期形状的故障也在历史里:PR 83 是一个提交一个修复的四连发——把脚本当作 CLI 唯一参数传进去时原本只打印用法而不执行;没有 pyobjc 的 Android 主机在 screen_info() 里以 ModuleNotFoundError: No module named "Quartz" 死掉;在 cp936 或 cp949 控制台上打印 emoji 或中日韩文字会抛错;以及托管测试会真的打开浏览器标签页。那个只在 macOS 上能导入的依赖,让 Linux 与 Windows 上的 Android 一直坏着,直到贡献者提交 PR 108——一个仍然开着的改动,用 struct 直接从 PNG 或 JPEG 头里读尺寸。

相关档案

全部档案 →