Android ADB 截图与 AI 坐标定位源码分析¶
官方链路分析版本:Midscene 1.10.5,基线提交
2a8a5bb4Custom Action 补充版本:Midscene 1.10.6,项目侧实验实现 分析日期:2026-07-16,补充日期:2026-07-23 范围:@midscene/android的截图和点击实现、@midscene/core从截图到 AI 定位坐标的处理链路,以及项目侧使用 Android CLI Layout Custom Action 替换坐标定位的扩展方案
1. 结论摘要¶
Midscene Android 的核心工作方式是“截图驱动的视觉定位”,不是 Android UIAutomator/XML 驱动:
AndroidDevice.screenshotBase64()从真机取得屏幕位图。- Core 根据截图尺寸、设备逻辑尺寸和
screenshotShrinkFactor建立坐标换算关系。 - 规划模型读取当前截图并决定下一步动作,例如
Tap、Input或Swipe。 - 目标坐标可以在规划阶段直接返回,也可以由独立的定位模型二次看图后返回。
- 模型返回的 bbox/point 先被模型适配器统一成截图像素 bbox,再取 bbox 中心点。
- 中心点从“发送给模型的截图坐标”换算为“设备逻辑坐标”,最后换算成 ADB 使用的物理像素坐标。
- Android 最终通过
adb shell input ...执行点击或滑动。 - Midscene 官方提供
customActions扩展口。Custom Action 被加入与Tap、Input同级的 Action Space,并不是在官方Tap内部替换 locator。 - 项目侧的
AndroidLayoutTap参数 schema 没有 Midscenelocate字段,因此TaskBuilder不会创建Planning/Locate任务,而是直接调用 Custom Action 的call()。 - Custom Action 每次执行时调用
android layout --device <serial> --pretty,使用当前 Layout JSON 的text、content-desc和center计算坐标,再通过 ADB 点击。 - 该方案只替换“动作目标的坐标定位”。AI 规划、任务截图、视觉断言和 Layout 失败后的视觉回退仍然可以使用截图与多模态模型。
默认截图实际上仍然基于 Android 的 screencap:第一优先级的 appium-adb.takeScreenshot() 在当前锁定的 appium-adb@12.12.1 中执行的是 adb exec-out screencap -p。Midscene 另外提供了设备文件加 adb pull 的回退路径,以及可选的 scrcpy H.264 视频流截图路径。
2. 端到端调用链¶
flowchart TD
A[AndroidAgent.aiAct] --> B[TaskExecutor.action]
B --> C[Agent.getUIContext]
C --> D[commonContextParser]
D --> E[AndroidDevice.screenshotBase64]
E --> F{scrcpy 已启用且可用?}
F -->|是| G[H.264 关键帧]
G --> H[ffmpeg 解码为 JPEG]
F -->|否或失败| I[appium-adb.takeScreenshot]
I --> J{截图有效?}
J -->|否| K[adb shell screencap 到设备文件]
K --> M{命令执行失败?}
M -->|否| L[adb pull 到本机]
M -->|是| N[yadb forceScreenshot]
N --> L
J -->|是| O[PNG Buffer]
H --> P[Base64 截图]
L --> P
O --> P
P --> Q[计算截图尺寸、方向和缩放比]
Q --> R[必要时缩图并转 JPEG]
R --> S[规划模型选择动作]
S --> T{规划结果已带坐标?}
T -->|是| U[模型适配器统一为 PixelBbox]
T -->|否| V[AiLocateElement 二次视觉定位]
V --> U
U --> W[PixelBbox 转 Rect 并取中心点]
W --> X[截图坐标除以 shrunkShotToLogicalRatio]
X --> Y[逻辑坐标换算为物理像素]
Y --> Z[adb shell input swipe/tap]
主要入口:
AndroidAgent.aiAct()负责启动规划与执行循环。commonContextParser()获取设备逻辑尺寸和截图,并建立坐标比例。AndroidDevice.screenshotBase64()实现 Android 截图。plan()将截图和任务发给规划模型。AiLocateElement()实现独立视觉定位。TaskBuilder将规划结果转换为定位任务和设备动作。
3. ADB 获取截图¶
3.1 默认路径:adb exec-out screencap -p¶
AndroidDevice.screenshotBase64() 默认先调用 adb.takeScreenshot():
appium-adb 的版本固定在 packages/android/package.json。在 12.12.1 中,takeScreenshot() 使用 exec-out,避免先在设备上写临时文件,返回值直接是 PNG Buffer。
Midscene 收到 Buffer 后调用 validateScreenshotBuffer(),校验内容包括:
- Buffer 不能为空。
- 文件头必须是 PNG 或 JPEG。
- 默认不得小于 1024 字节。
minScreenshotBufferSize可以调整大小阈值,设为0只关闭最小尺寸检查,不关闭空 Buffer 和图片格式检查。
这里没有像素内容检查。因此一张尺寸正常、格式合法但内容全黑的 PNG 仍会被视为有效截图。
3.2 回退路径:设备文件 + adb pull¶
以下情况会进入文件回退路径:
adb.takeScreenshot()抛出异常。- 返回 Buffer 未通过格式或大小校验。
- 配置了
displayId,此时会跳过takeScreenshot(),以便明确控制截图显示屏。 - 主路径连续三次返回“格式或大小无效”的 Buffer,之后会直接跳过主路径。
回退流程位于 device.ts:
需要注意,takeScreenshotFailCount 只在“主路径成功返回 Buffer,但 Buffer 校验失败”时递增。adb.takeScreenshot() 自身抛错不会增加计数,因此网络/ADB 命令异常不会触发三次后永久跳过主路径。
设备临时文件使用独立的 execFile(adb, ...) 异步删除,避免清理命令占用主 ADB 连接并阻塞后续操作。
3.3 yadb 强制截图¶
如果 adb shell screencap 命令本身执行失败,Midscene 调用 forceScreenshot():
它的设计目标是补充普通 screencap 无法工作的场景。不过当前逻辑只有在 screencap 命令抛错时才进入 yadb;如果 screencap 成功生成了一张合法但全黑的图片,不会触发 yadb 回退。
另外,yadb 以预编译二进制形式下载,具体截图实现不在本仓库源码中。不能仅根据调用注释假设它一定能绕过所有厂商限制或 FLAG_SECURE。
3.4 多显示屏¶
Android 设备配置支持:
displayId:为screencap和input指定显示屏。usePhysicalDisplayIdForScreenshot:截图时使用物理显示屏 ID。usePhysicalDisplayIdForDisplayLookup:读取尺寸、方向时使用物理显示屏 ID。
屏幕尺寸优先从 dumpsys display 中读取指定显示屏;无法匹配时回退到 wm size。相关实现见 getScreenSize()。
3.5 可选 scrcpy 路径¶
设置 scrcpyConfig.enabled: true 后,AndroidDevice.connect() 会初始化 scrcpy。初始化失败会把当前适配器标记为不可用,本次设备生命周期内不再重试,然后继续使用 ADB 截图。
scrcpy 路径的关键行为:
- 将 scrcpy server 推送到设备。
- 通过
@yume-chan/adb-scrcpy启动 H.264 视频流。 - 配置
audio: false、control: false、maxFps: 10。 - 缓存 SPS/PPS 和最近的 IDR 关键帧。
- 截图时最多等待 300ms 获取新关键帧;静态页面没有新帧时使用最近缓存帧。
- 将 H.264 关键帧交给 ffmpeg 解码为 JPEG。
- 30 秒无活动后默认断开流。
源码入口:
ScrcpyDeviceAdapterScrcpyScreenshotManager.ensureConnected()ScrcpyScreenshotManager.getScreenshotJpeg()
scrcpy 的优势是连续截图时不需要每次启动 screencap 和传输整张 PNG。代价是依赖 ffmpeg、scrcpy server 和 H.264 解码;每张最终被使用的截图仍需执行一次 ffmpeg 解码。
3.6 UI Observer 的连续帧¶
普通 aiAct() 使用单张代表截图。agent.startObserving() 用于捕获 toast、动画等短暂状态:
- scrcpy 可用时,观察期间只缓存原始 H.264 关键帧引用,结束后再解码真正需要的帧。
- scrcpy 不可用时,按间隔重复调用
screenshotBase64()。 - 帧数达到上限后进行抽稀,但保留内容变化点。
- 最终把时间序列截图一起交给 assert/extract 模型。
实现见 UIObserver 和 openScrcpyFrameSource()。
4. 截图进入 AI 前的处理¶
4.1 逻辑尺寸与截图尺寸¶
Android 同时存在两种尺寸:
- 物理尺寸:例如
1080 x 2400,来自wm size或dumpsys display。 - 逻辑尺寸:物理尺寸除以 density ratio,
density / 160,例如360 x 800。
AndroidDevice.size() 返回逻辑尺寸。Core 再读取实际截图宽高,并计算:
部分设备会报告错误方向。commonContextParser() 会比较逻辑尺寸和截图尺寸的横竖屏关系;不一致时交换逻辑宽高后再计算比例。
4.2 缩图与格式转换¶
screenshotShrinkFactor 控制发送给模型的截图尺寸:
缩图可以降低图像 token、网络传输和报告体积,但会降低小图标和细文字的定位精度。
未缩放时,如果截图是 PNG,Core 会以 90 质量转成 JPEG;已经是 JPEG 的 scrcpy 截图不会重复转换。相关实现见 commonContextParser()。
4.3 模型专用预处理¶
prepareModelImage() 按模型适配器执行额外预处理。目前典型场景是 Qwen 2.5:将图片右侧和底部补白到 28 的倍数。
系统同时记录:
preparedSize:真正发给模型的尺寸,包含 padding。contentSize:原始有效截图区域。
模型坐标先相对 preparedSize 解析,再裁剪到 contentSize,避免点击补白区域。
5. AI 如何得到目标坐标¶
5.1 Android 路径不依赖 UI XML¶
AndroidDevice.getElementsInfo() 返回空数组,getElementsNodeTree() 也返回空树。也就是说,当前 Android 主流程不会读取 UIAutomator XML,也不会用 resource-id/text 节点反查坐标。
模型看到的是截图和自然语言任务,坐标来自视觉模型对位图的理解。ADB 只负责:
- 获取截图。
- 获取屏幕尺寸、方向和 density。
- 执行模型规划后的点击、滑动、按键和输入命令。
5.2 规划阶段直接定位¶
当规划模型和默认定位模型使用同一个 default slot,且未开启 deepThink 时,includeLocateInPlanning=true。规划提示词会要求模型在动作参数中同时返回目标描述和坐标,例如:
规划结果经 normalizePlanningActionLocateFields() 调用模型适配器,把原始 bbox 或 point 转为统一的 locatedPixelBbox。
TaskBuilder 仍会创建一个 Locate 任务,但它优先使用规划阶段的 locatedPixelBbox,不再发起第二次 AI 定位请求。
5.3 独立定位模型二次定位¶
以下场景通常只在规划结果中保留 prompt,然后调用 AiLocateElement() 二次看图:
- 配置了独立 planning slot。
- 开启
deepThink。 - 需要
deepLocate精细定位。
独立定位提示词要求模型返回 JSON:
实际字段、顺序和坐标范围由模型适配器决定。提示词会明确告诉模型返回像素坐标、0-1000 归一化坐标,或者 0-1 归一化坐标。
5.4 模型坐标适配¶
不同视觉模型的原始坐标协议不同,Midscene 使用 LocateResultAdapter 统一处理:
| 模型 family | 原始形状 | 顺序 | 坐标范围 |
|---|---|---|---|
gpt-5 |
bbox | [xmin, ymin, xmax, ymax] |
截图实际像素 |
qwen2.5-vl |
bbox/point | xy | 截图实际像素,图片可能补白到 28 的倍数 |
qwen3-vl / qwen3 / qwen3.5 / qwen3.6 |
bbox | xy | 0-1000 |
doubao-vision / doubao-seed |
bbox/point | xy | 0-1000 |
gemini |
bbox | [ymin, xmin, ymax, xmax] |
0-1000 |
glm-v |
bbox | xy | 0-1000 |
kimi |
point | xy | 小于等于 1 时按 0-1,否则按截图像素 |
vlm-ui-tars* |
bbox/point | xy | 0-1000 |
适配链路位于 model-locate-result:
- 从
bbox、bbox_2d或point字段提取原始值。 - 将字符串、嵌套数组等结果解析为有限数字。
- 根据
xy/yx顺序重排。 - 根据
normalizedBy映射到图片像素。 - point 结果扩成一个小 bbox。
- 验证坐标顺序、有限值和图片边界。
- 将 padding 区域裁剪到真实内容边界。
归一化到像素的核心公式是:
使用 imageSize - 1 是因为内部 PixelBbox 使用包含端点的像素索引。例如一张宽 200 的图片,归一化值 1000 会映射到最右侧像素 199。
5.5 deepLocate¶
deepLocate 会先寻找较大的搜索区域,再裁剪并放大该区域做第二次精确定位:
- 先使用规划阶段粗 bbox、调用 section locator,或执行一次全图
AiLocateElement找到目标区域。 - 扩大搜索区域。
- 裁剪原始截图。
- 将裁剪图放大 2 倍交给定位模型。
- 将局部坐标除以放大倍数,再加裁剪区域 offset,映射回完整截图。
对应实现:
Service.resolveLocateSearchArea()buildSearchAreaConfig()mapSearchAreaPixelBboxToOriginalPixelBbox()
6. 从 AI bbox 到 ADB 点击坐标¶
6.1 bbox 转中心点¶
统一后的 PixelBbox 格式是包含端点的:
它先转换为:
再由 generateElementByRect() 计算中心:
偶数宽高时,中心点取中间四个像素中的左上像素。
6.2 截图坐标转逻辑坐标¶
动作执行前,parseActionParam() 将模型截图坐标除以 shrunkShotToLogicalRatio:
这一步同时消除:
- Android density 带来的物理像素/逻辑像素差异。
screenshotShrinkFactor带来的图片缩放差异。- scrcpy
maxSize带来的视频流缩放差异,因为比例基于实际截图尺寸重新计算。
6.3 逻辑坐标转 ADB 物理像素¶
AndroidDevice.adjustCoordinates() 计算逻辑尺寸与物理尺寸的 X/Y 比例:
X/Y 分开计算,因此也支持非等比的自定义逻辑尺寸。
6.4 完整数值示例¶
假设:
- 物理屏幕和原始截图:
1080 x 2400 - Android 逻辑尺寸:
360 x 800 screenshotShrinkFactor = 2- 发给模型的图片:
540 x 1200 - GPT 返回 bbox:
[90, 300, 210, 420]
计算过程:
最终点击点 (300, 720) 与原始 1080 x 2400 截图上的目标中心一致。
6.5 ADB 点击命令¶
Tap 动作从 locate 结果读取中心点,经 tapPoint() 换算后执行:
这里使用起点和终点相同、持续 150ms 的 swipe 来模拟点击,而不是直接使用 input tap。双击则使用两次 input tap,中间间隔 50ms。
相关源码:
7. Custom Action 如何替换截图坐标定位¶
7.1 它作用在什么位置¶
Custom Action 作用在 Midscene 的 Action Space 与设备动作执行边界。它没有修改截图方法、规划模型或官方 AiLocateElement(),而是注册了一组新的动作类型:
规划模型可以在官方 Tap 和这些 Custom Action 之间选择。两条执行链的分叉发生在模型输出动作类型之后:
flowchart TD
A["aiAct 收到自然语言操作"] --> B["获取当前截图和 UIContext"]
B --> C["规划模型读取截图、任务和 Action Space"]
C --> D{"模型选择哪种动作?"}
D -->|"官方 Tap"| E["TaskBuilder 发现 locate 字段"]
E --> F["创建 Planning / Locate 任务"]
F --> G["规划内 bbox 或 AiLocateElement 看图定位"]
G --> H["截图坐标转逻辑坐标和物理坐标"]
H --> I["AndroidDevice.tapPoint"]
D -->|"AndroidLayoutTap"| J["TaskBuilder 未发现 locate 字段"]
J --> K["直接调用 Custom Action.call"]
K --> L["android layout --pretty"]
L --> M["按 text 和 content-desc 匹配当前节点"]
M --> N["读取节点当前 center"]
N --> O["adb shell input tap"]
因此,“Custom Action 修改截图定位行为”更准确的说法是:
它没有改写 Midscene 原 locator,而是为同一个操作目标提供了另一条不经过 locator 的设备动作路径。
7.2 注册链路¶
项目侧实现首先使用官方 defineAction() 定义动作。每个动作包含:
示例实现入口位于:
官方 AndroidDevice 构造函数保存 options.customActions,然后在 actionSpace() 中合并:
Agent 再读取设备 Action Space,并将其交给 TaskExecutor:
对应源码:
AndroidDevice保存并合并 Custom ActionAgent将完整 Action Space 传给 TaskExecutorAndroidDeviceOpt.customActions
这个扩展点属于 Midscene 官方公开能力,所以业务项目不需要 fork 或修改 @midscene/core 才能加入新的定位方式。
7.3 AI 为什么知道可以调用它¶
Planning Prompt 不是写死一份固定动作列表。systemPromptToTaskPlanning() 会遍历当前设备的 Action Space,把每个动作的名称、描述、参数 schema 和 sample 拼进系统提示词:
因此模型看到的不只是:
还会看到:
项目侧又通过两层提示增加动作选择约束:
AndroidAgent.aiActionContext声明原生 UI 优先使用 Android Layout Custom Action。NativeMidsceneExecutor.operationPrompt()在每次操作中重复声明点击、滚动和输入对应的首选动作。
Prompt 只负责引导,不足以证明模型一定照做。执行完成后,NativeMidsceneExecutor 还会读取报告中的 Action Space tasks:
- 没有调用要求的 Custom Action,测试失败。
- 没有 Layout miss 却直接使用官方视觉动作,测试失败。
- 出现当前测试操作之外的动作,测试失败。
对应实现位于:
所以当前方案是“Prompt 引导 + 执行记录审计”,不是只靠一句提示词。
7.4 跳过截图 Locate 的关键:参数 schema 没有 locate¶
官方 Tap 的参数 schema 是:
locate 是一个带有 Midscene locator 标记的特殊 schema。TaskBuilder.handleActionPlan() 会调用 findAllMidsceneLocatorField() 扫描动作参数:
项目侧的点击动作故意使用普通字符串参数:
这个 schema 中不存在 Midscene locator 字段,因此 locateFields 是空数组,TaskBuilder 不会插入 Locate task。参数经过 Zod 校验后直接执行:
对应源码见 TaskBuilder 调用 Action。这就是 Custom Action 能真正绕开官方截图坐标定位的源码原因。
7.5 Custom Action 内部如何得到坐标¶
AndroidLayoutTap 的执行链是:
当前匹配依据主要是:
| 条件 | 基础分数 |
|---|---|
text 与 target 完全相等 |
120 |
content-desc 与 target 完全相等 |
118 |
content-desc 以 target 开头 |
112 |
text 包含 target |
92 |
content-desc 包含 target |
90 |
| 节点可点击 | 额外 +8 |
navigation 且描述包含 tab |
额外 +12 |
chip 且可 check |
额外 +10 |
只有分数达到 90 才允许点击。每次动作前都会重新执行 android layout,所以 Custom Action 没有缓存旧坐标;控件移动或尺寸变化后,只要语义标签仍可匹配,就会使用新 Layout 中的中心点。
Layout JSON 同时保存到测试产物中,例如:
这让报告中的坐标来源可以追溯,而不是只能看到模型返回了一个 bbox。
7.6 三个动作分别替换了什么¶
| Custom Action | 替换的官方视觉路径 | 当前实现 |
|---|---|---|
AndroidLayoutTap |
Tap.locate -> bbox -> center |
查当前 Layout,匹配具名节点,点击 center |
AndroidLayoutScrollUntil |
AI 反复判断目标是否出现并决定滚动 | 每轮查 Layout,未进入安全区域就执行固定 ADB swipe |
AndroidLayoutReplaceText |
AI 找输入框坐标后执行 Input | 找 field label,再选择其下方暴露的输入节点,点击、清空并输入 |
它们只覆盖当前定义的操作类型。视觉断言仍使用 agent.aiAssert();系统启动、清数据、Back 等操作走确定性的 Android/ADB 系统动作。
7.7 为什么仍然能在报告里看到截图¶
Custom Action 并没有让整个 aiAct() 变成无截图流程:
- Planning task 执行前仍然构建
UIContext,其中包含当前截图。 - 规划模型仍然需要截图理解页面状态,并决定调用哪个动作、使用什么语义 target。
- 每次重新规划都会附带最新截图。
aiAssert()仍然通过截图判断业务结果。- Midscene 报告会保存 planning、action 前后状态和断言证据。
被省掉的是官方动作中独立的“截图找 bbox/point”步骤:
| 阶段 | 纯 Midscene | Android Layout Custom Action |
|---|---|---|
| 规划前截图 | 有 | 有 |
| AI 理解任务和页面 | 有 | 有 |
| AI 生成动作 | Tap |
AndroidLayoutTap |
Planning/Locate task |
有,或使用 planning 内 bbox | 无 |
| 坐标来源 | 多模态模型读取截图 | android layout JSON |
| 坐标换算 | 截图 bbox -> 逻辑坐标 -> 物理坐标 | 直接使用 Layout 当前 center |
| 结果断言截图 | 有 | 有 |
因此它降低的是定位 token 和视觉误差,不会消除规划和断言的图像 token。
7.8 Layout miss 后如何回退视觉定位¶
以下场景 Android Layout 可能找不到目标:
- Canvas、自绘控件或游戏画面。
- WebView 没有把内部元素暴露到 Android 可访问节点树。
- 图标按钮没有稳定的
text/content-desc。 - 元素被遮挡、未进入可见区域或标签已经改名。
controller 在找不到高置信度目标时抛出 AndroidLayoutTargetError。外层 runExpected() 将其转换为:
同时通过 task.planningFeedback 告诉下一轮规划:只允许为当前失败操作使用一次 Midscene 内置视觉动作。Core 的 TaskExecutor 会收集 action task 的 planningFeedback,与最新截图一起送入下一轮 Planning。
这条回退链是:
flowchart LR
A["AndroidLayoutTap"] --> B["查询 Layout"]
B --> C{"高置信度目标存在?"}
C -->|"是"| D["ADB 点击"]
C -->|"否"| E["返回 recoverable layout miss"]
E --> F["planningFeedback"]
F --> G["下一轮 Planning + 最新截图"]
G --> H["仅本操作允许内置视觉 Tap"]
H --> I["执行记录审计"]
需要注意:success: false 是 Custom Action 的业务返回值,不等同于 JavaScript 抛错。是否继续规划由 Midscene 的 planning loop、shouldContinuePlanning 和反馈共同决定;外层审计负责防止模型静默绕过 Custom Action。
7.9 页面结构变化时的行为¶
Custom Action 对“坐标变化”和“交互流程变化”的适应能力不同:
| 页面变化 | 行为 |
|---|---|
| 按钮移动或尺寸变化 | 每次重新读取 Layout 和 center,通常不受影响 |
层级改变但 text/content-desc 不变 |
通常仍能匹配 |
| 按钮改名 | 旧 target 匹配失败,进入视觉回退或重新规划 |
| 按钮变成折叠菜单中的菜单项 | 旧的一步点击计划可能先失败;AI 需要重新规划为“打开菜单,再点菜单项” |
| 出现多个同名节点 | 当前取最高分节点,仍有误点风险 |
| 目标变成 WebView/Canvas 自绘 | Layout 可能完全不可见,需要视觉回退 |
Plan Cache 的命中键不包含截图或 Layout。即使页面结构变化,只要任务 prompt 和 context 没变,也可能先复用旧计划。Custom Action 会使用最新坐标,但不会自动把“一步按钮点击”改成“两步菜单操作”。结构或流程变化时应重建 Plan Cache,并用结果断言防止假通过。缓存细节见 Midscene AI 任务缓存源码分析。
7.10 如何证明实际使用了 Android Layout,而不是截图定位¶
不能只看报告里的截图,因为 Custom Action 模式本来仍会为 Planning 和 Assert 截图。应同时检查以下证据:
- 报告中的 Action task
subType是AndroidLayoutTap、AndroidLayoutScrollUntil或AndroidLayoutReplaceText。 - 对应操作之前没有
Planning / Locatetask。 - 产物目录存在该操作生成的
*-before.jsonLayout 文件。 - Custom Action 返回结果包含
matched、center、score和layout文件名。 - 汇总指标满足
layoutQueryCount >= androidLayoutCustomActionCalls。 - 如果出现官方
Tap,必须同时存在对应的 recoverablelayoutMiss,否则策略审计应让测试失败。
tests/midscene/src/midscene-action-audit.mjs 已按 Action task 类型分类并统计:
这组证据比“模型说自己使用了 Android CLI”可靠,因为它来自实际执行任务和命令产物。
7.11 设计边界¶
这个方案的本质是把职责拆成三层:
优点:
- 使用 Midscene 官方扩展口,不需要修改 Core。
- 坐标来自当前 Layout,不依赖 AI 估算 bbox。
- 页面只移动位置时不需要重建坐标缓存。
- Layout JSON、匹配分数和 ADB 坐标都能留作审计证据。
- Layout 不可用时仍能保留 Midscene 视觉回退。
限制:
- AI 仍通过截图规划,规划 token 没有被完全消除。
- 匹配依赖可访问语义,
content-desc质量会直接影响稳定性。 kind当前主要用于加分,不是严格的控件类型校验。- 同名元素、动态文案、折叠菜单和跨页面流程仍需要断言与重规划。
- 当前是项目测试层的实验实现,不是 Midscene 内置的通用 Android locator adapter。
8. 失败保护和已知边界¶
8.1 已有保护¶
- 截图校验空 Buffer、图片文件头和最小体积。
- 无效主截图连续出现三次后跳过主路径。
- scrcpy 初始化或截图失败后自动回退 ADB。
- 模型坐标必须是有限数字,且不得超出对应坐标系统范围。
- bbox 必须满足
right >= left、bottom >= top。 - 模型 padding 区域会裁剪到真实图片内容范围。
- 设备方向与截图横竖屏不一致时,会交换逻辑宽高后计算比例。
- 每次动作前把截图坐标统一转换为逻辑坐标,避免缩图后直接误点。
8.2 仍需关注的边界¶
-
合法黑屏不会触发回退 当前校验不做亮度、熵或像素方差检测。受
FLAG_SECURE、锁屏、视频 DRM 或厂商策略影响的合法黑图会直接发给模型。 -
yadb 只在命令异常时触发
screencap命令成功但图片内容错误时,不会自动重试 yadb。 -
失败时本机临时文件可能残留 回退截图只有成功走到方法末尾才异步删除本机临时文件;读取或校验中途抛错时可能残留文件。
-
屏幕信息默认缓存
cachedScreenSize、cachedOrientation和cachedAdjustScale默认缓存到destroy()。测试过程中旋转屏幕、切换外接显示屏或动态修改分辨率时,建议设置alwaysRefreshScreenInfo: true。 -
视觉定位不保证控件语义 Android 路径没有 resource-id、accessibility node 或 XML 的确定性辅助。相同文字、很小的图标、遮挡、动画和低清缩图都会降低定位稳定性。
-
bbox 中心不一定是最佳可点击区域 系统固定点击 bbox 中心。模型如果返回整个列表项、文字和按钮的组合区域,中心点可能落到不可点击位置。定位提示词因此强调返回紧凑的目标区域。
-
远程 ADB 的 scrcpy 连接范围
ScrcpyDeviceAdapter当前连接127.0.0.1:5037。远程设备需要先出现在本机 ADB server 中;它不会直接复用remoteAdbHost/remoteAdbPort创建 scrcpy transport。
9. 调试建议¶
9.1 判断使用了哪条截图路径¶
启用 Midscene debug 日志后关注:
典型日志:
Attempting scrcpy screenshot...:正在使用 scrcpy。Scrcpy screenshot failed, falling back...:scrcpy 失败,切换 ADB。Taking screenshot via adb.takeScreenshot:主 ADB 路径。Fallback: taking screenshot via shell screencap:设备文件回退路径。screencap failed, using forceScreenshot:进入 yadb。calculated dpr、shrunkShotToLogicalRatio:坐标比例。planResult:规划模型原始动作。resRect:模型坐标统一后的截图 Rect。executing action:动作执行前的最终参数。
9.2 坐标偏移排查顺序¶
- 保存并检查原始截图是否黑屏、旋转或被裁剪。
- 对比
shotSize与实际发送给模型的图片尺寸。 - 检查模型 family 是否配置正确,避免用错
xy/yx或归一化规则。 - 检查模型原始 bbox 是否紧贴目标元素。
- 检查
shrunkShotToLogicalRatio是否符合截图宽度 / 逻辑宽度。 - 检查
size()、物理尺寸和adjustCoordinates()的 X/Y scale。 - 多显示屏设备检查截图和 input 是否使用同一个
displayId。 - 旋转或切屏场景临时启用
alwaysRefreshScreenInfo验证是否为缓存问题。
10. 相关测试¶
仓库已经覆盖这条链路的关键行为:
packages/android/tests/unit-test/page.test.ts:逻辑坐标到物理坐标。packages/android/tests/unit-test/page.test.ts:主截图、回退、空 Buffer 和最小体积。packages/android/tests/unit-test/scrcpy-adapter.test.ts:scrcpy Base64 截图。packages/core/tests/unit-test/common-context-parser-orientation.test.ts:方向不一致和比例计算。packages/core/tests/unit-test/common-context-parser-shrink-factor.test.ts:缩图比例。packages/core/tests/unit-test/locate-result-adapter.test.ts:不同坐标协议、归一化、边界和异常值。packages/core/tests/unit-test/action-param-validation.test.ts:截图坐标到逻辑坐标。
11. 关键源码索引¶
| 模块 | 文件 | 职责 |
|---|---|---|
| Android Agent | packages/android/src/agent.ts |
Android Agent 封装与设备连接 |
| Android Device | packages/android/src/device.ts |
ADB、截图、尺寸、坐标换算和输入动作 |
| scrcpy Adapter | packages/android/src/scrcpy-device-adapter.ts |
scrcpy 生命周期和 ADB 回退边界 |
| scrcpy Manager | packages/android/src/scrcpy-manager.ts |
H.264 流、关键帧缓存和 ffmpeg 解码 |
| UI Context | packages/core/src/agent/utils.ts |
截图尺寸、方向、缩图和坐标比例 |
| AI Planning | packages/core/src/ai-model/llm-planning.ts |
任务规划和截图输入 |
| AI Locate | packages/core/src/ai-model/inspect.ts |
独立视觉定位和 deepLocate |
| Locate Adapter | packages/core/src/ai-model/shared/model-locate-result |
模型坐标协议统一 |
| Task Builder | packages/core/src/agent/task-builder.ts |
定位任务、坐标转换和动作执行 |
| Device Actions | packages/core/src/device/index.ts |
Tap/Input/Swipe 等标准动作 |
| Action Space Prompt | packages/core/src/ai-model/prompt/llm-planning.ts |
把动作描述、参数 schema 和 sample 提供给规划模型 |
| Custom Action 注册 | tests/midscene/src/android-layout-actions.mjs |
定义 Android Layout 动作、匹配节点并执行 ADB 命令 |
| Custom Action 装配 | tests/midscene/src/native-suite.mjs |
将 Custom Action 注入 AndroidDevice,并配置 Agent 约束 |
| Custom Action 审计 | tests/midscene/src/native-executor.mjs、tests/midscene/src/midscene-action-audit.mjs |
校验模型是否真实使用 Custom Action,并限制视觉回退 |