<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Sunshine 的技术手记 · AI × 移动端测试</title>
  <subtitle>记录 AI 与移动端测试的实战探索——可复现的步骤、真实的踩坑、跑通的代码。</subtitle>
  <link href="https://sunshine2176.github.io/feed.xml" rel="self"/>
  <link href="https://sunshine2176.github.io/"/>
  <updated>2026-11-06T10:32:42+08:00</updated>
  <id>https://sunshine2176.github.io/</id>
  <author><name>Sunshine</name></author>
  
  <entry>
    <title>不写 XPath，让 AI 在真机上跑测试：Appium MCP 全流程实战</title>
    <link href="https://sunshine2176.github.io/2026/11/05/appium-mcp-ai-android-real-device/"/>
    <updated>2026-11-05T18:30:00+08:00</updated>
    <id>https://sunshine2176.github.io/2026/11/05/appium-mcp-ai-android-real-device/</id>
    <summary>用一句自然语言驱动 AI 在安卓真机上完成定位、点击与断言，再把操作固化成可回归的 TestNG 代码。零 API key、零视觉模型，附 13 次工具调用链、110 秒实测与四个真实环境坑。</summary>
    <content type="html">&lt;blockquote&gt;
  &lt;p&gt;版本基准：源码核对与实测基于 &lt;strong&gt;appium-mcp v1.99.1&lt;/strong&gt;（2026-11-05）；截至发布，上游最新版为 &lt;strong&gt;v1.99.3&lt;/strong&gt;&lt;br /&gt;
上游热度：GitHub &lt;strong&gt;517 stars / 96 forks&lt;/strong&gt;（2026-11-05 查询）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;结论先行&lt;/strong&gt;：把 Appium 官方的 MCP server（appium-mcp）接进 AI 助手，你可以用一句自然语言——「打开设置，把蓝牙关掉」——驱动 AI 在安卓真机上完成探索、定位、点击、断言的全过程，最后还能让它把操作固化成可回归的测试代码。整个过程零 API key、零视觉模型、零额外服务：一台开了 USB 调试的安卓手机 + 一根数据线就能跟做。&lt;/p&gt;

&lt;p&gt;本文的差异化在两处：一是全流程给实测数据（每步耗时、工具调用链），二是给出「分阶段方法论」——官方 32 个工具直接丢给 AI 之外，还有更好的用法。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;1-为什么是-appium-mcp&quot;&gt;1 为什么是 Appium MCP&lt;/h2&gt;

&lt;p&gt;传统移动端自动化有三高：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;门槛高&lt;/strong&gt;：Java/Python + Appium 全家桶 + WebDriver API，环境配一天起步。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;维护高&lt;/strong&gt;：XPath 一改就碎，UI 一改一排用例红。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;复现难&lt;/strong&gt;：脚本写好了，别人跑不起来，问题出在环境而不是用例。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP（Model Context Protocol）改变的是交互方式：AI 助手直接持有「控制手机」的工具集——截图、找元素、点、滑、输入、读文本——你用自然语言下指令，它看着屏幕层级实时决策。写 XPath 的活儿消失了，因为 AI 自己读 page source。&lt;/p&gt;

&lt;p&gt;appium-mcp 是 Appium 官方团队维护的 MCP server（已进 appium.io 官方生态页），本文全程使用它。本教程覆盖 Android 真机 + 系统设置 App——设置 App 零 APK 分发问题，任何读者拿自己手机就能跟做。iOS 真机涉及 WDA 签名，留到续篇。&lt;/p&gt;

&lt;h2 id=&quot;2-15-分钟环境准备&quot;&gt;2 15 分钟环境准备&lt;/h2&gt;

&lt;p&gt;四样东西：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;依赖&lt;/th&gt;
      &lt;th&gt;要求&lt;/th&gt;
      &lt;th&gt;验证命令&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Node.js&lt;/td&gt;
      &lt;td&gt;≥ v22&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node --version&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;JDK&lt;/td&gt;
      &lt;td&gt;≥ 8&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java -version&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Android SDK&lt;/td&gt;
      &lt;td&gt;adb 可用&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adb devices&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;真机&lt;/td&gt;
      &lt;td&gt;USB 调试已开&lt;/td&gt;
      &lt;td&gt;同上，看到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;device&lt;/code&gt; 状态&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;appium-mcp 有两种驱动模式：&lt;strong&gt;内嵌模式&lt;/strong&gt;（默认，无需安装全局 Appium server，包内自带 uiautomator2 驱动）和 &lt;strong&gt;远程模式&lt;/strong&gt;（配 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remoteServerUrl&lt;/code&gt; 对接已有 Appium 服务）。本文用内嵌模式，读者零额外部署。&lt;/p&gt;

&lt;p&gt;把 server 写进 WorkBuddy 的 MCP 配置（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.workbuddy/mcp.json&lt;/code&gt;）：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;appium-mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;disabled&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;appium-mcp@latest&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;env&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ANDROID_HOME&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/Users/你的用户名/Library/Android/sdk&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;两个坑提前说：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;ANDROID_HOME 必须显式写&lt;/strong&gt;。就算你终端里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;echo $ANDROID_HOME&lt;/code&gt; 有值，MCP server 是独立进程，不一定继承你的 shell 环境。路径写错的表现是工具调用报错，而不是启动失败——排查时先查这里。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Node 版本卡 v22&lt;/strong&gt;。appium-mcp 的 engines 要求 v22+，v20 会直接装不上或运行时炸。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;配好之后，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adb devices&lt;/code&gt; 应该能看到你的设备在线：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ adb devices
List of devices attached
ABCD1234EFGH	device      # 正常状态是 device；unauthorized 表示手机上还没点「允许 USB 调试」
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后在连接器管理页对新 server 点 &lt;strong&gt;Trust&lt;/strong&gt;——&lt;strong&gt;这一步不能漏，也不会有任何报错&lt;/strong&gt;：不点 Trust，工具列表里就是空的，新手 90% 卡在这里。首次 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx appium-mcp@latest&lt;/code&gt; 会冷启动下载依赖（包本体 1.9 MB / 593 个文件，但依赖不小），实测&lt;strong&gt;约 2 分 20 秒&lt;/strong&gt;；期间没有任何进度提示，别以为卡死了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/appium-mcp/06-trust.png&quot; alt=&quot;连接器管理页：appium 条目已信任&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Trust 之后，工具列表里应该出现 &lt;strong&gt;32 个&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_*&lt;/code&gt; 工具（截图里那行「32/32 个工具已启用」）。源码里定义了 33 个工具，少的那一个是视觉定位的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_ai&lt;/code&gt;——它默认不注册，需要配外部视觉模型 API 才会出现（见 §6）。数量对不上就先查 MCP 进程有没有重启、ANDROID_HOME 有没有写错。&lt;/p&gt;

&lt;h2 id=&quot;3-第一条指令打开设置关掉蓝牙&quot;&gt;3 第一条指令：打开设置，关掉蓝牙&lt;/h2&gt;

&lt;p&gt;指令原文：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;打开设置，找到蓝牙开关，把它关掉，然后确认它已经关了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;为什么选这个场景：蓝牙开关在设置 App 里有稳定的 resource-id（安卓原厂 ROM），AI 需要走完「打开 App → 找到入口 → 定位开关 → 执行 → 断言」完整链路，但每一步都不难——是验证链路的最小完整任务。&lt;/p&gt;

&lt;p&gt;值得知道的是：appium-mcp 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_mobile_device_control&lt;/code&gt; 工具只有 lock / unlock / shake / open_notifications 四个动作，&lt;strong&gt;没有直接开关蓝牙的捷径&lt;/strong&gt;——AI 必须真的在 UI 里走一遍，跟人手操作同一条路。这正是「UI 自动化」的本意：测的就是用户视角的路径。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这次首跑的工具调用链&lt;/strong&gt;（2026-11-05 实测，OPPO Reno7 / ColorOS，共 13 次工具调用）：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;#&lt;/th&gt;
      &lt;th&gt;工具名&lt;/th&gt;
      &lt;th&gt;关键参数&lt;/th&gt;
      &lt;th&gt;目的&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select_device&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;platform=android&lt;/td&gt;
      &lt;td&gt;发现真机（1 台 USB 设备在线）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_session_management&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;action=create&lt;/td&gt;
      &lt;td&gt;建立 UiAutomator2 会话（约 18 秒，含驱动推送）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_screenshot&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;记录初始画面&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_app_lifecycle&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;action=activate, id=com.android.settings&lt;/td&gt;
      &lt;td&gt;打开设置&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_screenshot&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;设置首页（注意：停在页面底部）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_gesture&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;action=scroll_to_element, xpath &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;//*[@text=&quot;蓝牙&quot;]&lt;/code&gt;, direction=up&lt;/td&gt;
      &lt;td&gt;上滑 2 次找到入口&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;7&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_find_element&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;strategy=xpath, selector=&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;//*[@text=&quot;蓝牙&quot;]&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;拿到 elementId&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_gesture&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;action=tap, elementUUID=…3d&lt;/td&gt;
      &lt;td&gt;进入蓝牙页&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_get_page_source&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;pattern=蓝牙, saveToFile=true&lt;/td&gt;
      &lt;td&gt;读层级定位开关，发现 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checked=&quot;true&quot;&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_screenshot&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;关键帧：开关开着&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;11&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_gesture&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;action=tap, x=930, y=442&lt;/td&gt;
      &lt;td&gt;点击开关关闭&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_get_page_source&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;pattern=switch_widget&lt;/td&gt;
      &lt;td&gt;断言 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checked=&quot;false&quot;&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;13&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_screenshot&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;关键帧：开关已关&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;三个真实坑（本文最有价值的部分）&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;开关节点是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clickable=&quot;false&quot;&lt;/code&gt;&lt;/strong&gt;。真机上这个节点的完整属性是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class=&quot;android.widget.Switch&quot; resource-id=&quot;android:id/switch_widget&quot; checkable=&quot;true&quot; checked=&quot;true&quot; clickable=&quot;false&quot;&lt;/code&gt;——父节点 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;android:id/widget_frame&lt;/code&gt; 也是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clickable=&quot;false&quot;&lt;/code&gt;。&lt;strong&gt;「定位到元素」和「元素可点击」是两件事&lt;/strong&gt;，这一点是确定的。
但要说清楚：我第一次跑时「对 elementUUID 直接 tap 不生效、只能按 bounds 取中心坐标点」——&lt;strong&gt;第二次复现时同样的 elementUUID tap 直接成功了&lt;/strong&gt;（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;关闭 → 开启&lt;/code&gt;，页面结构随之变化）。所以我不能断言”clickable=false 就点不动”，只能记录这个不一致：&lt;strong&gt;很可能当时是页面还没稳定就点了&lt;/strong&gt;。对你的意义是——遇到点了没反应，先确认页面稳定状态，再怀疑元素本身。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;设置首页不是从顶部开始的&lt;/strong&gt;。这台机器上次停在页面底部，AI 打开设置时看到的是「权限与隐私 / 电池 / 关于本机」——如果直接假设顶部有蓝牙入口就会找不到。所以第 6 步用了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scroll_to_element&lt;/code&gt; 而不是”先截图再看”。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;蓝牙页属于另一个包&lt;/strong&gt;。ColorOS 的蓝牙设置页 package 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.oplus.wirelesssettings&lt;/code&gt;，不是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.android.settings&lt;/code&gt;——跨 ROM 差异的真实体现。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;耗时&lt;/strong&gt;：从下第一条指令到断言完成，&lt;strong&gt;共 110 秒&lt;/strong&gt;，其中建立会话约 18 秒，其余为 UI 操作与 AI 决策。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;关键帧&lt;/strong&gt;：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/appium-mcp/01-initial.png&quot; alt=&quot;初始画面&quot; /&gt;
&lt;img src=&quot;/assets/images/appium-mcp/02-settings-home.png&quot; alt=&quot;设置首页（停在底部）&quot; /&gt;
&lt;img src=&quot;/assets/images/appium-mcp/03-bluetooth-on.png&quot; alt=&quot;蓝牙开关处于开启状态&quot; /&gt;
&lt;img src=&quot;/assets/images/appium-mcp/04-bluetooth-off.png&quot; alt=&quot;关闭后：开关变灰、可用设备列表消失&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;先说结论：这次跑得很顺，而且”顺”本身就是本教程最重要的一次观察。&lt;/strong&gt; 为什么？因为这个任务的&lt;strong&gt;路径是明确的&lt;/strong&gt;——设置里有蓝牙入口，开关有稳定 resource-id，模型全程不需要做决策，照着指令走就是了。110 秒完成，中间没有一次重试和回退。&lt;/p&gt;

&lt;p&gt;顺，说明链路通。但”AI 帮我操作手机”的价值上限，取决于它在&lt;strong&gt;路径不明确&lt;/strong&gt;的任务上表现如何——那是第 5 节的主题，也是我为什么让你先跑这个”太简单”的场景：你得先亲眼见过它顺的样子，才能在下一节识别出它开始乱的样子。&lt;/p&gt;

&lt;h2 id=&quot;4-从会话到资产生成可回归的测试代码&quot;&gt;4 从会话到资产：生成可回归的测试代码&lt;/h2&gt;

&lt;p&gt;上面跑完的只是一次「会话」——关了终端就没了。测试自动化的产出物得是&lt;strong&gt;代码资产&lt;/strong&gt;：可回归、可入 CI、可给团队复用。&lt;/p&gt;

&lt;p&gt;appium-mcp 自带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_generate_tests&lt;/code&gt; 工具。但读它的源码（v1.99.1，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/tools/test-generation/generate-tests.ts&lt;/code&gt;）会发现一个有意思的真相：&lt;strong&gt;它不是代码生成器，是「SOP 注入器」&lt;/strong&gt;。工具本身只返回一段工作流指令文本，大意是：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;没会话先建会话；&lt;/li&gt;
  &lt;li&gt;用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_find_element&lt;/code&gt; 拿目标元素 ID；&lt;/li&gt;
  &lt;li&gt;只有点击的元素才可点，只有可聚焦的元素才能输入；&lt;/li&gt;
  &lt;li&gt;交互失败就换更稳的定位策略重试；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;所有步骤在真会话上执行完之后才准生成代码——”never generate from the scenario alone”&lt;/strong&gt;（绝不允许只看脚本描述就凭空写用例）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;真正写代码的是宿主 AI 模型，工具返回的这段指令就是给它的工作纪律。换句话说：&lt;strong&gt;官方自己就在用 prompt 约束模型分阶段行事&lt;/strong&gt;——这不是社区玄学，是上游设计。&lt;/p&gt;

&lt;p&gt;用起来很简单：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;把刚才关蓝牙的操作生成一份 TestNG 测试代码。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI 会按上述工作流先复述操作序列，再产出 Java/TestNG 代码（含 Page Object 结构）。&lt;/p&gt;

&lt;h3 id=&quot;生成的代码&quot;&gt;生成的代码&lt;/h3&gt;

&lt;p&gt;下面这份是模型按上面那段工作流、基于真机上真实观察到的定位信息写出来的（&lt;strong&gt;不是凭空编的&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;android:id/switch_widget&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;//*[@text=&apos;蓝牙&apos;]&lt;/code&gt; 都是在会话里实际找到过的）：&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;tests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;io.appium.java_client.android.AndroidDriver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;io.appium.java_client.android.options.UiAutomator2Options&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.openqa.selenium.By&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.openqa.selenium.WebElement&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.openqa.selenium.support.ui.ExpectedConditions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.openqa.selenium.support.ui.WebDriverWait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.testng.Assert&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.testng.annotations.AfterClass&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.testng.annotations.BeforeClass&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;org.testng.annotations.Test&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;java.net.URL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;java.time.Duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BluetoothToggleTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AndroidDriver&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebDriverWait&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@BeforeClass&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;UiAutomator2Options&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UiAutomator2Options&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setUdid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ABCD1234EFGH&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// 换成 `adb devices` 里你的设备序列号&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setAppPackage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.android.settings&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setAppActivity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;.Settings&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setNoReset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AndroidDriver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://127.0.0.1:4723&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebDriverWait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ofSeconds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;turnOffBluetooth&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 1. 进蓝牙设置页&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;WebElement&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bluetoothEntry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;until&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;ExpectedConditions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;presenceOfElementLocated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;By&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;xpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;//*[@text=&apos;蓝牙&apos;]&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;bluetoothEntry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 2. 关掉主开关&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;WebElement&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;switchWidget&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;until&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;ExpectedConditions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;presenceOfElementLocated&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;By&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;android:id/switch_widget&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;switchWidget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 3. 断言已关闭&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;until&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;关闭&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findElement&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;By&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;android:id/switch_widget&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getAttribute&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)));&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Assert&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findElement&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;By&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;android:id/switch_widget&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getAttribute&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;关闭&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;蓝牙主开关应处于关闭状态&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@AfterClass&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tearDown&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;实测结果&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mvn test&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tests run: 1, Failures: 0&lt;/code&gt;，8.5 秒跑完；系统层复查 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dumpsys bluetooth_manager&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enabled: false, state: OFF&lt;/code&gt;（跑之前是开着的），即它真的把蓝牙关了，不是自我安慰的断言。&lt;/p&gt;

&lt;h3 id=&quot;那要手改吗代码零改动改的全是环境&quot;&gt;那要手改吗？——代码零改动，改的全是环境&lt;/h3&gt;

&lt;p&gt;这是我这次最意外的发现：&lt;strong&gt;生成的 Java 代码一行都没改就跑通了&lt;/strong&gt;。所有坑都在环境侧，而且每一个都不是「AI 写得不对」：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;手改 1（必须）：pom 里锁死 Selenium 版本&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;第一次 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mvn test&lt;/code&gt; 连编译都过不去：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[ERROR] 无法访问org.openqa.selenium.ContextAware —— 找不到类文件
[ERROR] 无法访问org.openqa.selenium.html5.LocationContext —— 找不到类文件
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;查下去是版本解析的锅：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java-client 9.4.0&lt;/code&gt; 对 Selenium 声明的是&lt;strong&gt;开区间&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[4.26.0, 5.0)&lt;/code&gt;，Maven 老老实实解析到当时最新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4.52.0&lt;/code&gt;；而 Selenium 在 &lt;strong&gt;4.34.0&lt;/strong&gt; 里删掉了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ContextAware&lt;/code&gt; 接口，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AndroidDriver&lt;/code&gt; 仍在实现它 → 编译期找不到类。&lt;/p&gt;

&lt;p&gt;实测边界（逐个版本拆 jar 验的）：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4.33.0&lt;/code&gt; 有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ContextAware&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4.34.0&lt;/code&gt; 没有。所以：&lt;/p&gt;

&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- 手改 1：锁 Selenium 4.33.0，绕开 4.34.0 移除 ContextAware 导致的编译失败 --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.seleniumhq.selenium&lt;span class=&quot;nt&quot;&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;selenium-bom&lt;span class=&quot;nt&quot;&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.33.0&lt;span class=&quot;nt&quot;&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class=&quot;nt&quot;&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class=&quot;nt&quot;&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;手改 2（必须）：独立 Appium server 要能看见 ANDROID_HOME&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Message: Neither ANDROID_HOME nor ANDROID_SDK_ROOT environment variable was exported.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;注意这个坑的隐蔽之处：我在配 MCP 时已经把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANDROID_HOME&lt;/code&gt; 写进 MCP 的 env 里了，MCP 用得好好的；但&lt;strong&gt;跑生成的代码需要另起一个独立 Appium server&lt;/strong&gt;，那是另一个进程，环境变量得单独给：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;ANDROID_HOME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/path/to/your/android-sdk &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;ANDROID_SDK_ROOT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/path/to/your/android-sdk &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
appium &lt;span class=&quot;nt&quot;&gt;--port&lt;/span&gt; 4723 &lt;span class=&quot;nt&quot;&gt;--address&lt;/span&gt; 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;手改 3（必须，且最坑）：两套 driver 抢着装同一个 APK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这一条我单独放在 §4.1 说，因为它会让你怀疑人生。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;可选改进（不是必须）&lt;/strong&gt;：断言用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text == &quot;关闭&quot;&lt;/code&gt; 是中文硬编码，换英文系统就挂。更稳的是断言 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getAttribute(&quot;checked&quot;).equals(&quot;false&quot;)&lt;/code&gt;——开关节点本来就有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkable=&quot;true&quot; / checked=&quot;true|false&quot;&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&quot;41-为什么你的代码跑不起来而别人的教程里很顺两套驱动在抢装同一个-apk&quot;&gt;4.1 为什么你的代码跑不起来，而别人的教程里很顺：两套驱动在抢装同一个 APK&lt;/h3&gt;

&lt;p&gt;Appium 的 UiAutomator2 驱动在建会话时要往手机装两个辅助 APK：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;io.appium.settings&lt;/code&gt;（管 toast/权限/剪贴板等）和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium-uiautomator2-server&lt;/code&gt;（执行引擎）。问题出在&lt;strong&gt;这两份 APK 是跟着 driver 版本走的&lt;/strong&gt;：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;全局 Appium server&lt;/th&gt;
      &lt;th&gt;appium-mcp（npx 缓存内）&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;driver 版本&lt;/td&gt;
      &lt;td&gt;appium-uiautomator2-driver &lt;strong&gt;4.0.1&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;内嵌 &lt;strong&gt;8.7.2&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;自带 settings APK&lt;/td&gt;
      &lt;td&gt;io.appium.settings &lt;strong&gt;5.12.22&lt;/strong&gt;（2.83 MB）&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;8.4.3&lt;/strong&gt;（3.12 MB）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;于是：你跟着 §2 用 MCP 跑通了（8.4.3 装上了），到了 §4 起独立 server 跑代码——它发现设备上的版本和自己要的不一致，&lt;strong&gt;先卸载、再装自己那份&lt;/strong&gt;；装完你再回去用 MCP，MCP 又发现不一致，&lt;strong&gt;再卸再装&lt;/strong&gt;。两个 driver 来回覆盖，每次覆盖都要过一遍系统安装确认。&lt;/p&gt;

&lt;p&gt;更糟的是：在国产 ROM 上，这次「重装」大概率&lt;strong&gt;装不上&lt;/strong&gt;（见下），失败时它已经把旧版本卸了——我实测到这里时设备上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;io.appium.settings&lt;/code&gt; 直接变成 0 条，两边都用不了了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解法：把两边版本对齐。&lt;/strong&gt; 让全局 server 也用 8.7.2：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;appium driver update uiautomator2     &lt;span class=&quot;c&quot;&gt;# 把 4.0.1 升到与 MCP 内嵌一致的 8.7.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;对齐之后，两边自带同一份 APK，谁也不会再触发重装——这是我这套环境里唯一稳定的组合。&lt;/p&gt;

&lt;h3 id=&quot;42-国产-rom-的四道门槛最后一道才是病根&quot;&gt;4.2 国产 ROM 的四道门槛（最后一道才是病根）&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;① 安装被安全中心拦下&lt;/strong&gt;：ColorOS 会在 adb 安装时弹 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.oplus.appdetail/.guide.InstallGuideActivity&lt;/code&gt;——「检测结果：涉及敏感权限 → 继续安装」。自动化卡在这里，60 秒后 adb 超时，表现为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;install ... timed out after 60000ms&lt;/code&gt;。logcat 里还有一句吓人的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PackageInstaller: PC install attack detected!&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OPLUS_ADB_INSTALL_CANCEL&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/appium-mcp/05-install-blocked.png&quot; alt=&quot;ColorOS 的安装拦截弹窗&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 点了确认还是失败&lt;/strong&gt;：如果你点「继续安装」时，发起安装的 adb 进程已经被杀掉（比如命令超时被回收），那个弹窗就是&lt;strong&gt;僵死的&lt;/strong&gt;——点了只会走到「安装失败」页。必须让 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adb install&lt;/code&gt; 保持存活（放受管后台任务里跑）的同时点确认，才装得上。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ 装完之后还有一屏广告&lt;/strong&gt;：安装成功后 ColorOS 停在「安装成功 + 推荐应用」全屏页。这一页会把后面所有 UI 操作挡住——症状是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;//*[@text=&apos;蓝牙&apos;]&lt;/code&gt; 找不到，看着像定位失败，实际是屏幕上盖着一个安装器页面。我在这里白跑了两轮测试。&lt;strong&gt;装完记得按返回键把它关掉。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;④ 真正的元凶：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-g&lt;/code&gt; 参数（这条最反直觉）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;上面三条都是症状，翻 logcat 才挖到病根：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;appdetail_install_task: installApp io.appium.settings error
You need the android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission
to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adb install -g&lt;/code&gt; 的意思是「装完顺手把所有运行时权限都授予」。ColorOS 的安装器（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.oplus.appdetail&lt;/code&gt;）&lt;strong&gt;没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSTALL_GRANT_RUNTIME_PERMISSIONS&lt;/code&gt; 这个权限&lt;/strong&gt;，于是带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-g&lt;/code&gt; 的安装直接被拒；而 &lt;strong&gt;Appium 内部装辅助 APK 时正是带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-g&lt;/code&gt; 的&lt;/strong&gt;——所以它在国产 ROM 上必然失败，表现就是卡满 60 秒超时。&lt;/p&gt;

&lt;p&gt;去掉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-g&lt;/code&gt; 一次就过：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;APK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~/.npm/_npx/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;/node_modules/appium-mcp/node_modules/appium-uiautomator2-driver/node_modules/io.appium.settings/apks/settings_apk-debug.apk
adb &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &amp;lt;你的设备&amp;gt; &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$APK&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;          &lt;span class=&quot;c&quot;&gt;# 注意：不要加 -g&lt;/span&gt;
adb &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &amp;lt;你的设备&amp;gt; shell dumpsys package io.appium.settings | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;versionName   &lt;span class=&quot;c&quot;&gt;# 确认 8.4.3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;预装好之后，MCP 建会话时检测到版本一致，直接跳过安装环节——这才是「会话秒建」的原因。&lt;/p&gt;

&lt;h2 id=&quot;5-分阶段方法论不裸调工具&quot;&gt;5 分阶段方法论：不裸调工具&lt;/h2&gt;

&lt;h3 id=&quot;51-先接住上一节那个为什么这么顺&quot;&gt;5.1 先接住上一节那个「为什么这么顺」&lt;/h3&gt;

&lt;p&gt;第 3 节关蓝牙一气呵成，不是因为 AI 特别聪明，是因为那个任务的&lt;strong&gt;路径是明确的&lt;/strong&gt;：设置里有「蓝牙」入口，开关有稳定 resource-id，模型全程不需要做决策——它只需要执行。指令里每一个名词，在界面上都唯一对应到一样东西。&lt;/p&gt;

&lt;p&gt;真正的问题是：&lt;strong&gt;当指令里的名词在界面上不对应任何唯一答案时，它会怎么办？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;于是我换了个任务，同一个模型、同一台机器、同一个 MCP，跑两遍。&lt;/p&gt;

&lt;h3 id=&quot;52-实验设计&quot;&gt;5.2 实验设计&lt;/h3&gt;

&lt;p&gt;任务：&lt;strong&gt;「找出设置里所有与位置信息相关的开关，记录每个当前开/关状态」&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;选它的理由是它有三重负担：一是&lt;strong&gt;入口分散&lt;/strong&gt;（系统级开关在「位置信息」页，应用级权限在「权限管理」里，两处不相邻）；二是&lt;strong&gt;要遍历&lt;/strong&gt;（应用级涉及 129 个 App）；三是&lt;strong&gt;要聚合&lt;/strong&gt;（得记住看过什么、漏了什么）。而且成功与否可以客观核对——这正是企业里会付钱的那类任务（合规审计）。&lt;/p&gt;

&lt;p&gt;两次运行唯一的不同：跑之前给不给流程约束。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;A 组（裸调）&lt;/strong&gt;：只给上面那句话，32 个工具随便用（截图里的「32/32 已启用」），跑完就算。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;B 组（SOP）&lt;/strong&gt;：按「探索 → 设计 → 执行 → 固化」四阶段走，每阶段有明确的进出条件。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;53-数据&quot;&gt;5.3 数据&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;指标&lt;/th&gt;
      &lt;th&gt;A 组 裸调&lt;/th&gt;
      &lt;th&gt;B 组 SOP&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;工具调用次数&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;37&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;32&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;墙钟耗时&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;3 分 52 秒&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;3 分 04 秒&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;找到的开关&lt;/td&gt;
      &lt;td&gt;3 个（定位服务 开 / WLAN 扫描 开 / 蓝牙扫描 关）&lt;/td&gt;
      &lt;td&gt;3 个（同上，逐项核对一致）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;应用级权限（129 个 App）&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;未覆盖，且报告里没有任何提示&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;未覆盖，但明确写出原因与前置条件&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;走错的岔路&lt;/td&gt;
      &lt;td&gt;2 处（「定位控制」子页无开关、白跑；「应用管理」129 个 App 逐个进不现实）&lt;/td&gt;
      &lt;td&gt;2 次无效点击（元素在屏外 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y=2375&lt;/code&gt;、点了没反应），随即修正&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;结果可复现&lt;/td&gt;
      &lt;td&gt;不能（没说怎么找到的，换台机器无法照做）&lt;/td&gt;
      &lt;td&gt;能（入口路径 + 边界条件全部写明）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;54-数据说明了什么以及没说明什么&quot;&gt;5.4 数据说明了什么——以及没说明什么&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;先说没说明的：我原以为「裸调会显著烧轮次」，实测不支持。&lt;/strong&gt; 37 次对 32 次，只差 14%；耗时差 21%。这点差距不足以支撑「裸调很烂」的结论。上一版大纲里我写的「AI 会乱点瞎猜」——&lt;strong&gt;在你用强模型的前提下，是错的&lt;/strong&gt;，这里如实更正。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;真正的差异在最后一行：它不会告诉你它漏了什么。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 组的输出是一张干净的表格——三个开关，状态齐全，看起来完整。但它漏掉了 129 个 App 的位置权限，而且&lt;strong&gt;报告里一个字都没提&lt;/strong&gt;。你把它交上去，没人看得出来这是个残次品。&lt;/p&gt;

&lt;p&gt;B 组走到「权限管理」时撞上一个弹窗：「开启权限监控，可以保护手机的隐私和数据」——这是&lt;strong&gt;设备状态的变更&lt;/strong&gt;，不该由 AI 自作主张。SOP 在这里停下，把这一项写进「未覆盖清单」并注明前置条件；裸调则是绕过去、继续走，最后当它不存在。&lt;/p&gt;

&lt;p&gt;这就是我为什么把主立论换成下面这条。&lt;/p&gt;

&lt;h3 id=&quot;55-三条立论&quot;&gt;5.5 三条立论&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;① 主立论：SOP 是模型参差的对冲（C 位）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我这次实验用的是强模型，它裸调也能跑出及格结果。但你的读者不会都用同一个模型——同一段提示词，换个中等模型，37 次调用可能变成 60 次，也可能在第 12 步就忘了自己要找什么。&lt;/p&gt;

&lt;p&gt;SOP 的作用是把&lt;strong&gt;流程纪律从模型能力里解耦出来&lt;/strong&gt;：不管模型强弱，它都必须先探索、再设计、后执行、最后声明覆盖。强模型因此更稳，弱模型因此不至于崩。这才是方法论真正的价值，而且它不会被我这次「裸调也不差」的实测推翻——反而被它解释得更清楚。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 次立论：路径不明确的任务，无约束试错是真实成本&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 组两处岔路（定位控制死胡同、129 个 App 的不可枚举）不是”乱点”，是&lt;strong&gt;没人事先告诉你哪条路是死的&lt;/strong&gt;。SOP 的「探索阶段」做的就是先画地图再走路——B 组少走的那 5 次调用，全在这两处。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ 第三条：生成代码之前必须验证真实页面&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这条不是我发明的，是 appium-mcp 自己在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_generate_tests&lt;/code&gt; 里写死的纪律：&lt;strong&gt;“Execute all steps in order before emitting an Appium test… never generate from the scenario alone”&lt;/strong&gt;。第 4 节的代码之所以一行没改就能跑，正是因为它基于真机上实际找到的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;android:id/switch_widget&lt;/code&gt;，而不是模型「记得」安卓设置页长什么样。&lt;/p&gt;

&lt;h3 id=&quot;56-sop-四阶段可直接抄给-ai&quot;&gt;5.6 SOP 四阶段（可直接抄给 AI）&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;阶段 1 · 探索（只读，不点任何会改变状态的东西）
  - 逐层 dump 页面结构，建立入口地图：哪个页面有哪几个可点条目
  - 输出：一张「入口 → 页面 → 元素」的清单
  - 进不去的地方记下来，不要硬试

阶段 2 · 设计（写清单，先不执行）
  - 把任务拆成「系统级」和「应用级」两类，各自列出待查项
  - 每项写清：怎么找到它、取什么属性判断状态
  - 明确标注：哪些项需要改变设备设置才能查（这类要先问人）
  - 输出：一张待执行清单，含预期调用次数

阶段 3 · 执行（严格按清单，一次一项）
  - 点击前确认元素在可视区内（bounds 落在屏幕里），不在就先滚
  - 元素 ID 导航后可能失效，换页必须重新 find
  - 每次操作后 dump 一次状态，不要凭「应该点了」下结论

阶段 4 · 固化（产出必须带覆盖声明）
  - 输出表格：项名 / 所在路径 / 当前状态
  - 强制写「未覆盖清单 + 原因」，没有就写&quot;无&quot;
  - 附上入口路径，让别人能照着重跑一遍
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A 组和 B 组的真正差距，全在阶段 4 那一句「强制写未覆盖清单」上。&lt;/strong&gt; 前面三个阶段，强模型裸调也能糊过去；只有这一条，它不会主动做。&lt;/p&gt;

&lt;h2 id=&quot;6-进阶一瞥&quot;&gt;6 进阶一瞥&lt;/h2&gt;

&lt;p&gt;三样东西本文刻意不碰，但你应该知道它们存在：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;视觉定位&lt;/strong&gt;：appium-mcp 有个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_ai&lt;/code&gt; 工具，能用自然语言描述找元素（「底部那个黄色按钮」）。但它默认&lt;strong&gt;不注册&lt;/strong&gt;——需要配 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AI_VISION_ENABLED=true&lt;/code&gt; 加外部视觉模型 API（OpenAI 兼容接口，官方示例是阿里云 DashScope 的 Qwen3-VL-235B）。也就是说它是服务端自费调外部 API，不是靠宿主模型的多模态，不配 key 就没有这个工具。适合元素没有稳定 id、必须靠视觉特征定位的场景。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OCR / YOLO 自建定位服务&lt;/strong&gt;：生产级方案，门槛在模型服务部署，本文不展开。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;iOS 真机&lt;/strong&gt;：需要 Xcode 签 WebDriverAgent，appium-mcp 提供了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium_prepare_ios_real_device&lt;/code&gt; 一步步引导，续篇见。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;7-这不只是玩具&quot;&gt;7 这不只是玩具&lt;/h2&gt;

&lt;p&gt;回看完整链路：&lt;strong&gt;一句自然语言 → AI 在真机上探索定位执行断言 → 固化为可回归的测试代码&lt;/strong&gt;。中间没有任何「写脚本」环节。&lt;/p&gt;

&lt;p&gt;对个人：手机上任何重复操作（设置审计、App 数据清理、回归验证）都可以一句话委托。对团队：这就是「业务人员描述意图、AI 产出工程资产」的最小闭环——测试用例的生产线从「会写代码的 QA」变成「会描述意图的任何人 + 一个靠谱的执行端」。&lt;/p&gt;

&lt;p&gt;留个互动：你最想让 AI 替你在手机上干什么重复操作？评论区或 issue 告诉我，下一篇拿呼声最高的场景做实战。&lt;/p&gt;

&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: 需要 API key 吗？&lt;/strong&gt;&lt;br /&gt;
A: 不需要。本文全流程零 API key。唯一的付费可选项是 §6 的视觉定位（外部视觉模型 API）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: 模拟器行不行？&lt;/strong&gt;&lt;br /&gt;
A: 行，把 capabilities 里的 udid 指向模拟器即可。但本文教程用真机写，因为「真机」才是测试的信任基础。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: AI 找不到元素会乱点吗？&lt;/strong&gt;&lt;br /&gt;
A: 路径明确的任务（如本文 §3）不会，实测很稳。路径不明确的任务它也不会”乱点”，而是无约束试错——换定位策略反复重试，烧的是轮次和时间，这是 §5 方法论解决的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: 生成的代码质量如何？&lt;/strong&gt;
A: 比我预期好——&lt;strong&gt;一行没改就跑通了&lt;/strong&gt;（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mvn test&lt;/code&gt; 8.5 秒通过，系统层复查蓝牙确实关了）。缺点有两个：断言用的是中文文案 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text == &quot;关闭&quot;&lt;/code&gt;，换英文系统就挂，应改成断言 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checked&lt;/code&gt; 属性；定位用的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;//*[@text=&apos;蓝牙&apos;]&lt;/code&gt;，属于文本定位，主题/语言一变就失效。真正会让你卡住的不是代码，是 §4.1 那两套 driver 和 §4.2 的国产 ROM 安装闸门。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;本文是我的「AI × 移动端测试」系列第一篇。全流程实测于 2026-10-27 至 2026-11-05，工具版本与源码行为以文中标注为准。&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  
</feed>
