☰
iPhone-use 工具回退指南:在 MCP 绑定失效时通过 phone.py 与 Runtime 继续操作真实 iPhone
2026/11/2 1:27:35 网站建设 项目流程

【免费下载链接】iPhone-use

让 Codex 通过 USB 操作真实 iPhone:引导安装、App 自动化、实时屏幕与截图回退。

项目地址:https://gitcode.com/gh_mirrors/ip/iPhone-use
点击查看免费下载

本篇聚焦 iPhone-use 这个 Codex 插件在“MCP 工具绑定缺失、宿主转发错误或封装层已确认异常”时的工程化回退路径。读完后你将能够:准确区分“工具名找不到”“参数写错”“手机通道故障”这三类问题;用scripts/phone.py或直接导入server/iphone_use.py的Runtime继续同一已授权动作;并保证回退入口复用相同的配置、会话、操作锁与异常语义,而不是另起一套并行控制。

本篇内容以仓库内的 工具故障与代码调用 为主线,并结合 server/iphone_use.py、scripts/phone.py、server/wda_client.py、server/wda_controller.py 与 tests/test_fallback.py 的实际实现进行印证。

回退的第一原则:绑定缺失不等于 PUA 不可用

参考文档给出的总原则非常克制:优先调用封装好的工具,默认乐观执行常规动作,下一步需要信息时顺带判断上一动作。工具绑定缺失、宿主转发错误或已确认的封装问题,都不等于底层 PUA(Phone Use Agent)不可用;此时可对同一已授权动作使用插件代码入口。

关键约束有两条:

  1. 代码回退保留相同的配置、会话、操作锁和明确异常处理,不新增每步验收;
  2. 回退不是“换一套更宽松的通道”,而是换一个入口,走同一份共享实现。

第二条在源码里可以直接看到:server/iphone_use.py 中的Runtime._dispatch对绝大多数工具都会先打开state_dir/operation.lock,以fcntl.LOCK_EX | fcntl.LOCK_NB抢占独占锁,抢不到就抛出device_busy。pua_screen、pua_screen_frame、pua_screen_action等预览相关路径才走旁路。也就是说,无论请求来自 MCP stdio 进程还是phone.py子进程,同一台手机的控制权都被这把文件锁串行化。

先看本回合实际可调用的工具绑定

参考文档明确要求:普通版提供18 个模型工具;要判断“工具是否可用”,应当查看本回合实际可调用的工具绑定(宿主支持时用ALL_TOOLS或工具搜索),不能只从tools/list、文档或先前聊天推断已经绑定。

源码层面也能解释这个要求:server/iphone_use.py 基于SCHEMAS生成TOOLS列表,其中screen_frame与screen_action是APP_TOOLS,通过_meta.ui.visibility=["app"]只暴露给预览 App,模型不可见;因此SCHEMAS的键数不等于模型工具数。

此外还有两个容易误判的细节:

  • 0.1.6 的install.sh同时注册同名标准 MCP,避免 Codex 插件共享说明预算隐藏工具,并保留完整 batch schema。见 scripts/register_mcp.py:它以安装结果中的installedPath定位server/iphone_use.py,执行codex mcp add iphone_use -- python3 <server>,并只退役自己此前注册过的同名旧条目。
  • 新版安装后需要重新连接聊天;实际仍缺少所需工具时,才使用代码入口继续,不要默认把已封装操作都改成 CLI。

CHANGELOG 的 0.1.6 条目同样记录了这次改造的三件事:同名标准 MCP 注册、batch 发布 schema 精简(selector/observe提取为$defs,保持低于宿主 5KB 有损压缩门槛)、以及运行时闭集参数校验保持完整。对应实现在 published_schema,其 docstring 明确说明:Codex 会压缩超过 5KB 的 schema,可能丢参数分支,因此发布版复用$ref瘦身,而运行时校验仍使用完整的SCHEMAS,不做$ref解析。

先区分错误:八类典型场景的处置口径

参考文档的核心价值在于把“看起来都像失败”的结果拆成可执行的分支。下表在其基础上补充了源码中可直接核对的错误码与字段。

错误 / 信号含义正确处置
schema / 参数错误(invalid_argument)参数与当前闭集 schema 不符;action_executed=false表示动作未发出按argument_path、unknown_fields、allowed_fields修正一次调用;MCP 绑定仍不可用时可用代码入口;不要自动忽略未知参数
device_busy另一操作持有共享锁等待它结束;不另建 Runtime 配置或 state directory 绕过锁
stale_observation提供的 ID 不属于当前 Runtime,或 App / 视口上下文已改变用下一步所需的新信息定位;不为整图哈希、文本数字刷新或固定 30 秒期限增加读图;ID 是可选上下文,不是每个坐标动作的必填许可
界面异常(selector / 焦点失败、滚动后找不到可点击目标、无进展、区域被挡、上下文改变、输入不符、预期页面未出现)语义定位已不可靠,需要视觉判断统一先看截图:先用结果里已附的截图;缺图才补一次screenshot;再选择可见目标、处理浮层或决定是否继续滚动;不先重读树、不换标签写法、不原样重试
input_continuation_expired长文本续传已失效,本次没有输入任何内容读取字段实际文字,用replace=false只补缺少的部分
occluded_target/offscreen_target目标点击没有执行,不能推断“未执行的点击打开了浮层”occluded_target按附带截图判断:目标可见就按tap_point用坐标点击,确有遮挡先处理遮挡;offscreen_target先向目标滑动。自绘半屏面板可能没有原生 Alert / Sheet 节点,按真实截图判断
显式验收的postcondition_failed动作已接收但验收条件未满足查看实际状态再决定剩余步骤;不因验收失败自动重放动作
action_executed=true, action_complete=false、action_uncertain、uncertain=true或动作后 timeout至少部分动作可能已生效先用action_uncertain.transport_error区分传输类型,按 recovery 等待或检查连接,再观察真实字段 / 记录;不能切换代码入口后盲目再做输入、发送、提交、删除或整个批次
pua_timeoutsocket 等待或调用预算耗尽,不证明断连按retry_after_seconds在宿主侧等待(当前为 2 秒),再取一次pua_observe(mode="screenshot");不立即重试整树或动作,也不用pua_wait(它会新增 AX 查询)。截图仍超时最多再延迟尝试一次,之后用pua_setup(action="status")诊断;重复超时本身不授权重启。若原调用是 READY,用相同参数延迟重试 READY
pua_unreachable连接拒绝、连接重置、管道关闭或 EOF 等 HTTP 通道连接失败调用pua_setup(action="status"),复用当前 start / recover 工作;确认服务未运行、已配置、没有活动工作且用户允许启动时,才 start 一次;连接失败本身不证明物理 USB 已拔出
pua_transport_error其他传输或协议异常按返回细节检查 setup status / 日志;不当作超时或确定断连;不自动重启或重放动作
local.pid.0、pua_foreground_unavailable或 XCTestCode 41通道故障调用 READY 恢复;不要通过更改 selector 或重复手机按钮处理;锁屏、签名与信任问题按 setup skill;App 密码 / Face ID 按认证接管

这张表与源码的分类函数一一对应:

  • WDAClient.transport_error 把传输异常拆成pua_timeout、pua_unreachable、pua_transport_error三类;只读请求返回pua_timeout等原始码并补一句“本次读没有执行手机动作”,写请求统一包装为action_uncertain且uncertain=True,并附带transport_error供上层区分。
  • Runtime.channel_fault 识别 XCTest 授权失败(not authorized for performing ui testing actions、XCTDAEMONERRORDOMAIN+code=41)与前台不可用(pua_foreground_unavailable、local.pid.搭配stale element reference),据此决定进入恢复流程还是直接返回原始错误。
  • validate 在对象出现未知字段时抛出invalid_argument,并在details中带上action_executed: false、argument_path、unknown_fields、allowed_fields与recovery.next_step——这正是“按字段修正一次”而非“猜着重试”的依据。

还有一条容易被忽略的口径:查询失败不证明手机动作已执行,POST 查询接口不应按 mutation 处理;查询超时也不自动立即重读,遵循上述有界延迟恢复。

截图的转发:不要把图片变成 base64 文本

参考文档专门用一段说明通过functions.exec调工具时的转发方式:逐个内容块转发,文字用text(block.text),图片用image(block);不要text(result)把截图变成 base64 文本。图片无法转发时用view_image打开image.path/error.observation.image.path,坐标按image.pixel_to_point换算。

这不是风格问题,而是正确性问题。见 result_content:返回值是“一个紧凑 JSON 文本块 + 命中时追加一个 image 块”,并且刻意不发送structuredContent,因为宿主一旦只看 structured 对象,就可能丢弃包括图片在内的所有 content 块。同理,pua_screen_frame走structuredContent且不产生模型文本(预览帧不应变成上下文负担)。

image.pixel_to_point的生成位置在 PhoneController.capture:用真实视口尺寸除以图像像素尺寸并保留 4 位小数,因此“图像像素 × pixel_to_point = iPhone 点”是由实现保证的换算关系,而不是经验值。

READY 的有界恢复:先重试,再重启

恢复章节是整份文档中最需要“按字面执行”的部分,因为它直接决定会不会误杀一个其实健康的服务。

常规路径

正常任务用pua_ready(recover=true)或省略recover;false仅用于用户明确禁止重启或只读诊断,并应保留该限制。暂时失效的前台只清理旧观察 / 会话并重读一次;持续故障才重启已核验归属的本插件服务,并复用有效构建。整个过程不会重放导航、输入或提交。

源码中的两段式处理正是如此:Runtime.ready 在首次失败后,若判定为foreground_unavailable,会关闭 client、清空session_id、调用phone.reset(),再执行一次ready_once,并在结果里标注recovery={"state":"read_recovered","session_recreated":True,"replayed_action":False}。这个replayed_action: false是给上层模型看的强约束信号。

READY 的pua_timeout不进入重启流程

按retry_after_seconds在宿主侧等待,再用相同参数重试,最多两次延迟尝试;仍超时只查询 setup status 与日志。不得用pua_wait代替宿主等待,不因累计超时自动 start / recover。

源码给出的 guidance 字段写得很具体:retry_after_seconds=2、max_wait_retries=2、next_tool="pua_ready"、replay_action=False,并要求“最多两次延迟尝试后仅做诊断性 setup(status)”(iphone_use.py)。

pua_unreachable才进入连接 / 启动检查

此时原始错误会被补上initialization_required=True与recovery.next_tool="pua_setup"、recovery.next_arguments={"action":"status"},并明确写着“连接失败或not_ready不证明服务已停止”(iphone_use.py)。

恢复中的状态查询

ready=false, state="recovering"携带recovery.job_id和查询参数。status 返回jobs数组,需要找到id与job_id相等的工作;检查recovery_phase,从stopping/starting到serving后重验 READY。Runner 可以长期 running,不等 succeeded、不重复 start。按阶段、日志和retry_after_seconds查询,不写固定 15 秒 × 20 次的等待循环,也不能读取不存在的单个 job 并吞掉解析异常。

当ready=false, state="recovery_required", reason="recovery_disabled"时,只有用户指令允许才按next_tool/next_arguments调用recover=true;明确禁止则说明阻塞。注意这两种状态没有error、MCPisError=false,但仍不能继续手机动作——这一点在 tests/test_fallback.py 里被显式断言:结果里assertNotIn("error", result),同时 CLI 退出码为 1。

归属与冷却

恢复要求配置、endpoint、worker 和监听端口归属匹配;归属不明、外部 PUA、端口冲突或缺少构建时按返回步骤处理,不按端口杀进程。120 秒冷却按retry_after_seconds诊断,不连续重启。信任、解锁与 UI 自动化确认由用户在手机完成。冷却常量RECOVERY_COOLDOWN_SECONDS = 120在 server/wda_setup.py 中定义。

README 与 skill 也反复强调一个边界:recover=true是运行时恢复,不是冷启动;pua_timeout不等同于断连;action_uncertain必须保留transport_error以区分超时与连接失败,永不重放不确定的动作。

直接执行提供的脚本:scripts/phone.py

当宿主无法走 MCP 工具时,最轻的回退是直接运行脚本。参考文档给出的常规 Home 示例如下:

python3 <PLUGIN_ROOT>/scripts/phone.py pua_press_button '{"name":"home","observe":"none","verify":false}'

其中<PLUGIN_ROOT>要从当前项目或已安装插件确定,不要照抄某人的版本 cache 路径。仓库内该脚本位于 scripts/phone.py。

参数与行为

  • 第一个位置参数是工具名,取值范围由SCHEMAS派生:argparse的choices=["pua_" + name for name in SCHEMAS],因此不会出现脚本支持但运行时未知的工具名;
  • 第二个位置参数是与 MCP 工具 schema 一致的 JSON 对象,默认{};
  • --state-dir、--url只在原通道确实使用非默认目录 / URL 时才传;不得为避开 busy 换目录或并行控制同一手机。

默认状态目录的解析见 state_directory:优先--state-dir,其次环境变量IPHONE_USE_STATE_DIR或旧名WDA_STATE_DIR,否则~/.local/share/iphone-use;若新目录不存在而旧目录~/.local/share/iphone-use-wda/config.json存在,则复用旧目录以保留既有安装状态。

为什么它“看起来像 CLI,行为却像工具”

脚本本体只有 36 行,但每一步都对齐 MCP 路径:

  1. 先把server目录插入sys.path,再from iphone_use import PUAError, Runtime, SCHEMAS;
  2. JSON 解析使用parse_constant拒绝NaN/Infinity,避免非有限数字进入数值校验;
  3. 构造Runtime(args.state_dir, args.url)后调用runtime.call(tool, arguments)——与 MCP 调用同一个Runtime.call,因此共享参数校验、operation.lock、session.json会话恢复与异常语义;
  4. PUAError转成{"error": exc.as_dict()},ValueError/TypeError转成invalid_argument;
  5. finally中调用runtime.close(),关闭预览与 client、刷新分析缓冲。

输出与退出码

命令输出只有 JSON:截图位于image.path(已缩放的 JPEG,同名.png是原始截图),需要查看时用可用的图片工具打开,像素乘以image.pixel_to_point得到 iPhone 点。

退出码语义在 phone.py 一行内:

return 1 if "error" in result or (args.tool == "pua_ready" and result.get("ready") is not True) else 0

即:有error字段,或pua_ready的ready不是严格的True,都返回 1。这是“诊断状态也是非零”的刻意设计,ready=1或ready="true"都不算就绪,tests/test_fallback.py的test_fallback_ready_requires_explicit_true对这三种取值逐一断言。

被测试钉住的行为

tests/test_fallback.py 用假 client 覆盖了回退入口最容易出错的几处:

  • 默认一次动作、无前台回读:{"name":"home"}返回action_executed=true、foreground_verified=false、verification_deferred=true,且请求序列中没有/wda/activeAppInfo,动作序列只有/wda/homescreen;
  • 显式验收复用同一控制器:{"name":"home","verify":true}才出现foreground_verified=true;
  • 不能绕过他人的操作锁:外部先持有operation.lock时,回退返回device_busy且client.calls == [];
  • 不能绕过锁屏:locked=true时返回phone_locked且没有任何动作;
  • 不重放不确定的 mutation:home_error=WDAError("action_uncertain", ...)时退出码 1、error.uncertain=True,Home 只发出一次;
  • schema 先于手机访问:[]与{"name":"home","extra":true}都在invalid_argument处终止,client.calls == [];
  • 畸形 / 非有限 JSON 在 Runtime 之前被拒:{broken、{"x":NaN}、{"x":Infinity}的Runtime构造次数为 0;
  • 锁文件权限为0600:verify=true用例顺带断言了operation.lock的 mode。

写最小调用代码:直接导入 Runtime

宿主无法调用脚本时,可以导入同一 Runtime。参考文档给出的最小示例如下(保持默认配置和结构化错误):

import json import sys from pathlib import Path root = Path(sys.argv[1]).expanduser().resolve() sys.path.insert(0, str(root / "server")) from iphone_use import PUAError, Runtime runtime = Runtime() try: result = runtime.call("pua_press_button", {"name": "home", "observe": "none", "verify": False}) print(json.dumps(result, ensure_ascii=False)) except PUAError as error: print(json.dumps({"error": error.as_dict()}, ensure_ascii=False)) raise SystemExit(1) finally: runtime.close()

围绕这段代码,有几个必须理解的事实:

  1. 两个入口绕过的是 MCP 绑定,不是共享实现。Runtime.call会做 schema 与语义双重校验(validate+validate_semantics),再进入_dispatch的加锁段。
  2. observation ID 不跨进程共享。若选择使用 ID,必须在同一 Runtime 内取得观察并传给动作;否则会因为不在当前 Runtime 已知集合而得到stale_observation。
  3. 已知坐标或 region 可不提供 ID,不为获得许可额外读图或树。这直接呼应文档中“ID 是可选上下文”的表述。
  4. 使用 iPhone 点坐标,不使用 Mac 屏幕坐标或截图像素。
  5. 共享实现自身故障不会因换入口而修复:遇到异常应按实际错误调整;不要另建外部 PUA session,也不要盲目重复 mutation。
  6. Runtime.close()必须执行。它关闭预览 hub 与 HTTP client,并关闭 analytics;否则连接与后台线程可能悬挂。

关于默认配置的来源,Runtime.__init__的取值顺序是:构造参数base_url→ 环境变量WDA_URL→state_dir/config.json中的local_port(拼成http://127.0.0.1:<port>)→http://127.0.0.1:18100(iphone_use.py)。这也是为什么回退时不应该随手改--url:它与原通道不一致时,操作锁虽在同一state_dir,却可能指向不同服务。

此外,WDAClient 对 URL 本身有硬约束:必须是http且 host 属于127.0.0.1/localhost/::1,路径为空或/,且不允许用户名、query、fragment——错误信息就是“PUA URL must be a local HTTP endpoint; forward USB to loopback first.”。这意味着任何“绕过”的想法在构造阶段就会失败,而不是变成一次隐蔽的远端调用。

会话与原生设置也会被回退入口继承

_dispatch在拿到锁之后,会尝试从state_dir/session.json读回session_id,并校验其中的url与当前base_url一致、ID 形如[A-Za-z0-9-]{1,128};若存在settings_restore(例如snapshotMaxDepth/snapshotMaxChildren),会调用reapply_settings在下次请求前重放(iphone_use.py)。save_session使用临时文件 +os.replace原子写入,权限0600,并在首次写入原生限制前先记账——这解释了为什么“回退后还应该保持原来的树深度设置”,而不是回到库默认值。

滚动与批次:默认乐观,但有明确停止点

回退入口既然是同一实现,滚动与批次的语义也完全一致。

swipe 的默认值

默认verify=false, observe="none":执行一手势,不读 XML 验进展,也不自动 fallback。已知region可直接使用,不强制树、锚点或 ID。下一步本就需要读取列表时,在该次动作直接返回所需观察并顺带判断是否移动;默认未验证返回不等于没有移动。

源码 PhoneController.swipe 与此一致:显式verify=true才做一次几何检查;无进展时附带截图并返回no_scroll_progress;上下文变化则返回scroll_context_changed;recovery.same_gesture_retry=False、replay_action=False明确禁止自动换手势重试。

verify=true 的边界

显式verify=true才检查滚动进展、原生浮层和变化上下文;需要调试时在该次调用选tree/both,复用返回观察,不重复另读。进展以列表几何变化判断,数字或轮播文本刷新不应单独作为滚动成功 / 拒绝动作的依据。原生浮层的验证保护不能识别所有自绘面板,明确遮挡后按真实状态选区域或关闭面板。

verify=true发现第一次手势无进展就附截图停止,不自动换手势;旧参数max_attempts=2仍可传入但不导致自动重试。scroll_find每次最多滑一次,仍找不到可点击目标就先返回截图;遮挡和歧义在滑动前即停止。应先实际查看图片再判断继续、区域、方向和目标,不直接增加max_swipes重跑。

三个状态码的准确含义

  • no_scroll_progress不证明空列表或到底;异常结果即使请求none/tree也附截图,应检查入口、边界、浮层及自绘内容。
  • scroll_context_changed表示手势后上下文变化,先看截图再处理;已执行部分不能当作未执行而重放。
  • 截图读取失败时保留原错误与执行证据,并提示单独取图,不自行继续动作。

batch 的继续规则

batch 可连续执行普通 unverified 导航和输入;明确 error、uncertain 或未验收的 submit 才停止,按completed_steps/stopped_at继续剩余步骤。launch 默认只激活一次;显式verify=true/expect才有界等前台 / 页面。关键验收失败先看实际状态,不自动再次 activate。

PhoneController.batch 把停止条件写死为几种显式情况:input_complete is False→stop_reason="input_continues";submitted但submission_verified为假 →"submission_requires_verification";WDAError→stop_reason=exc.code;超时 →"time_budget"。返回体固定携带completed_steps与非complete的complete:false,并在next_step里写明“从stopped_at继续,已完成步骤不得重复”。

Home 的专用端点

Home 使用手机端专用主屏幕端点。默认不单独等待 SpringBoard;准备下一步时的观察会显示实际状态。需要确认主屏时显式verify=true,它有界核对前台;通用按键接口返回 HTTP 200 而界面未变的问题,不能据此推定 iOS / XCTest 的内部原因。

对应实现 press_button:name != "home"走/wda/pressButton;Home 走/wda/homescreen(session=False,不需要会话),且注释直接写明“XCTest pressButton 可能确认 Home 但前台未变,因此使用专用端点”。这与回退脚本的测试用例互相印证。

阶段结果不等于任务完成

参考文档结尾强调:代码调用成功、Home 完成、某 App 读完或一个 batch 完成,都只是阶段结果;应继续剩余工作,最终关键结果与交付完成后才结束整项任务。

同时,普通verified=false不是错误,不要求 observe 或停止 batch;HTTP accepted 只描述请求边界;常规路径继续,最终关键状态显式验收。pua_metrics提供 HTTP 与工具耗时、每个工具返回的字节数以及两次工具调用之间的等待(宿主、模型与用户时间之和),但评估要以实际任务耗时和交付完整度衡量,不能把“省略每步验收”说成“业务已被证明”。

回退决策清单

在真正落到代码入口之前,按顺序自检:

  1. 是否只看过tools/list或旧聊天?先确认本回合的实际工具绑定;新版安装后是否已重连聊天。
  2. 是参数问题还是通道问题?有action_executed=false+argument_path/unknown_fields就修正一次参数;有device_busy就等待,不要换目录。
  3. 是否 READY 未达ready=true?recovering查同一job_id;recovery_required看用户是否允许重启;pua_unreachable先 setup status;pua_timeout先宿主等待再重试。
  4. 是否有不确定的动作?action_uncertain、action_executed=true, action_complete=false一律先读真实状态,绝不因为“换个入口”就重放。
  5. 界面异常是否先看了截图?缺图补一次screenshot,坐标点用x/y,需要换算时乘image.pixel_to_point。
  6. 是否只用了同一份配置?默认~/.local/share/iphone-use(或旧目录复用);--state-dir/--url只在与原通道一致时使用。
  7. 结束后是否runtime.close()?脚本会自动执行;自写代码需要finally。

只要这七步都通过,代码回退就与 MCP 调用等价:同样的锁、同样的会话、同样的错误码与同样克制的验收标准——这正是 工具故障与代码调用 想要传达的工程边界。

【免费下载链接】iPhone-use

让 Codex 通过 USB 操作真实 iPhone:引导安装、App 自动化、实时屏幕与截图回退。

项目地址:https://gitcode.com/gh_mirrors/ip/iPhone-use
点击查看免费下载
上一篇:10倍效率提升!PowerToys图片尺寸批量处理:从新手到高手的智能优化指南
下一篇:如何用giotto-tda检测引力波:拓扑数据分析在天体物理中的革命性突破 🚀

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询