Android 自动化支持

Midscene 可以驱动 adb 工具来实现 Android 自动化。

由于适配了视觉模型方案,整个自动化过程可以适配任意的 App 技术栈,无论是 Native、Flutter 还是 React Native 构建的 App 或小程序都能使用。开发者只需面向最终效果调试 UI 自动化脚本即可。

Android UI 自动化解决方案具备 Midscene 的全部特性:

  • 支持使用 Playground 进行零代码试用。
  • 支持 JavaScript SDK。
  • 支持使用 YAML 格式的自动化脚本和命令行工具。
  • 支持生成 HTML 报告来回放所有操作路径。

案例展示

Prompt : 打开懂车帝,搜索 su7 车型,查看参数配置

查看此次任务的完整报告:report.html

Prompt : Open the Booking App, search for a hotel in Tokyo for four adults on Christmas, with a score of 8 or above.

查看此次任务的完整报告:report.html

查看更多案例:showcases

本指南将带你完成使用 Midscene 自动化 Android 设备所需的一切:通过 adb 连接真机、配置模型 API Key、体验零代码 Playground,并运行你的首个 JavaScript 脚本。

配置 AI 模型服务

将你的模型配置写入环境变量,可参考 模型策略 了解更多细节。

export MIDSCENE_MODEL_BASE_URL="https://替换为你的模型服务地址/v1"
export MIDSCENE_MODEL_API_KEY="替换为你的 API Key"
export MIDSCENE_MODEL_NAME="替换为你的模型名称"
export MIDSCENE_MODEL_FAMILY="替换为你的模型系列"

更多配置信息请参考 模型策略模型配置

准备工作

在编写脚本前,先确认 adb 能够连接设备且设备信任当前电脑。

安装 adb 并设置 ANDROID_HOME

adb --version

出现类似输出表示安装成功:

Android Debug Bridge version 1.0.41
Version 34.0.4-10411341
Installed as /usr/local/bin//adb
Running on Darwin 24.3.0 (arm64)
echo $ANDROID_HOME

有输出即代表配置成功:

/Users/your_username/Library/Android/sdk

启用 USB 调试并验证设备

在系统设置的开发者选项中开启 USB 调试(若有 USB 调试(安全设置) 也请一并开启),然后用数据线连接手机。

android usb debug

验证连接:

adb devices -l

出现类似输出代表连接成功:

List of devices attached
s4ey59	device usb:34603008X product:cezanne model:M2006J device:cezan transport_id:3

试用 Playground(零代码)

Playground 是验证连接、观察 AI Agent 的最快方式,无需编写代码。它与 @midscene/android 共享相同的代码实现,因此在 Playground 上验证通过的流程,用脚本运行时也完全一致。

  1. 启动 Playground CLI:
npx --yes @midscene/android-playground
  1. 点击 Playground 窗口中的齿轮按钮,粘贴你的 API Key 配置。如果还没有 API Key,请回到 模型配置 获取。

开始体验

配置完成后,你可以立即开始体验 Midscene。它提供了多个关键操作 Tab:

  • Act: 与网页进行交互,这就是自动规划(Auto Planning),对应于 aiAct 方法。比如
在搜索框中输入 Midscene,执行搜索,跳转到第一条结果
填写完整的注册表单,注意主要让所有字段通过校验
  • Query: 从界面中提取 JSON 结构的数据,对应于 aiQuery 方法。

类似的方法还有 aiBoolean(), aiNumber(), aiString(),用于直接提取布尔值、数字和字符串。

提取页面中的用户 ID,返回 { id: string } 结构的 JSON 数据
  • Assert: 理解页面,进行断言,如果不满足则抛出错误,对应于 aiAssert 方法。
页面上存在一个登录按钮,它的下方有一个用户协议的链接
  • Tap: 在某个元素上点击,这就是即时操作(Instant Action),对应于 aiTap 方法。
点击登录按钮

关于自动规划(Auto Planning)和即时操作(Instant Action)的区别,请参考 API 文档。

集成 Midscene Agent

当 Playground 运行正常后,就可以切换到可复用的 JavaScript 脚本。

第 1 步:安装依赖

npm
yarn
pnpm
bun
deno
npm install @midscene/android dotenv --save-dev

第 2 步:编写脚本

下面的示例会在设备上打开浏览器、搜索 eBay,并断言结果列表。

./demo.ts
import 'dotenv/config'; // 通过 dotenv/config 自动加载 .env 文件中的环境变量
import {
  AndroidAgent,
  AndroidDevice,
  getConnectedDevices,
} from '@midscene/android';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
Promise.resolve(
  (async () => {
    const devices = await getConnectedDevices();
    const device = new AndroidDevice(devices[0].udid);

    const agent = new AndroidAgent(device, {
      aiActionContext:
        'If any location, permission, user agreement, etc. popup, click agree. If login page pops up, close it.',
    });
    await device.connect();

    await agent.aiAct('open browser and navigate to ebay.com');
    await sleep(5000);
    await agent.aiAct('type "Headphones" in search box, hit Enter');
    await agent.aiWaitFor('There is at least one headphone product');

    const items = await agent.aiQuery(
      '{itemTitle: string, price: Number}[], find item in list and corresponding price',
    );
    console.log('headphones in stock', items);

    await agent.aiAssert('There is a category filter on the left');
  })(),
);

第 3 步:运行示例

npx tsx demo.ts

第 4 步:查看报告

脚本成功后会输出 Midscene - report file updated: /path/to/report/some_id.html。在浏览器中打开该 HTML 文件即可回放每一步交互、查询与断言。

进阶

当你需要自定义设备行为、把 Midscene 接入独立框架,或排查 adb 问题时,可参考本节内容。更多构造函数参数可前往 API 参考(Android)

扩展 Android 上的 Midscene

使用 defineAction() 定义自定义手势,并通过 customActions 传入。Midscene 会把自定义动作追加到规划器中,让 AI 可以调用你领域特定的动作名。

import { getMidsceneLocationSchema, z } from '@midscene/core';
import { defineAction } from '@midscene/core/device';
import { AndroidAgent, AndroidDevice, getConnectedDevices } from '@midscene/android';

const ContinuousClick = defineAction({
  name: 'continuousClick',
  description: 'Click the same target repeatedly',
  paramSchema: z.object({
    locate: getMidsceneLocationSchema(),
    count: z.number().int().positive().describe('How many times to click'),
  }),
  async call(param) {
    const { locate, count } = param;
    console.log('click target center', locate.center);
    console.log('click count', count);
  },
});

const devices = await getConnectedDevices();
const device = new AndroidDevice(devices[0].udid);
await device.connect();

const agent = new AndroidAgent(device, {
  customActions: [ContinuousClick],
});

await agent.aiAct('click the red button five times');

关于自定义动作和动作 Schema 的更多解释,请参阅 与任意界面集成

常见问题

为什么连接了设备,但仍然无法控制?

一个典型的错误信息是:

Error:
Exception occurred while executing 'tap':
java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.

请在系统设置的开发者选项中确认以下选项已开启:

  1. USB 调试
  2. USB 调试(安全设置)(如果存在)

android usb debug

输入文本后,输入框内容被清空或消失

Midscene 在输入文本后会自动隐藏键盘,默认行为是发送 ESC 按键事件。然而,部分输入框(尤其是 WebView 中的输入框)会监听 ESC 按键事件,导致以下副作用:

  • 清空刚输入的文本
  • 关闭包含输入框的弹窗或模态框
  • 导航离开当前页面

你可以按以下优先级逐步尝试解决:

方案一:改用 BACK 键(Android 返回键)隐藏键盘

keyboardDismissStrategy 设为 'back-first',用 Android BACK 键替代 ESC 键来隐藏键盘:

const device = new AndroidDevice('device-id', {
  keyboardDismissStrategy: 'back-first',
});

方案二:关闭自动隐藏键盘

如果你的输入框同时监听了 BACK 键,可以彻底关闭自动隐藏键盘,由 AI Agent 或后续操作自行管理键盘状态:

const device = new AndroidDevice('device-id', {
  autoDismissKeyboard: false,
});

关闭后键盘不会自动隐藏,可能导致键盘覆盖大量屏幕区域。你可以通过以下方式应对:

  • 使用 aiAct 指令手动隐藏键盘,例如 await agent.aiAct('点击键盘上的收起按钮')
  • 安装并切换到 ADBKeyBoard——这是一款极小面积的虚拟键盘,即使不隐藏也几乎不影响屏幕操作

英文文本被 Android 输入法改写

如果报告中显示的输入参数是正确的,但 App 实际收到的是不同文本、缺字,或被改成中文/拼音候选词,通常是当前 Android 输入法改写了输入内容。纯 ASCII 文本如果走原生 adb input text 路径,在中文输入法或带自动纠错的输入法下就可能出现这个问题。

使用已有的 imeStrategy 选项,将所有文本输入强制改为 yadb:

const device = new AndroidDevice('device-id', {
  imeStrategy: 'always-yadb',
});

YAML 脚本中可以这样配置:

android:
  imeStrategy: always-yadb

也可以通过环境变量设置:

export MIDSCENE_ANDROID_IME_STRATEGY=always-yadb

这和“输入后被清空”不是同一个问题。如果文本先正确输入、随后消失,请优先检查 keyboardDismissStrategyautoDismissKeyboard

如何使用自定义的 adb 路径或远程 adb 服务器?

通过环境变量设置:

export MIDSCENE_ADB_PATH=/path/to/adb
export MIDSCENE_ADB_REMOTE_HOST=192.168.1.100
export MIDSCENE_ADB_REMOTE_PORT=5037

也可以通过 AndroidDevice 构造函数传入:

const device = new AndroidDevice('s4ey59', {
  androidAdbPath: '/path/to/adb',
  remoteAdbHost: '192.168.1.100',
  remoteAdbPort: 5037,
});

更多

API 参考

当你需要自定义设备行为、把 Midscene 接入框架,或排查 adb 问题时,请查阅本节。关于通用构造函数(报告、Hook、缓存等)的参数说明,请参考平台无关的 API 参考

Action Space(动作空间)

AndroidDevice 使用以下动作空间,Midscene Agent 在规划任务时可以使用这些操作:

  • Tap —— 点击元素。
  • DoubleClick —— 双击元素。
  • Input —— 输入文本,支持 replace/typeOnly/clear 模式(appendtypeOnly 的已废弃别名)。支持可选参数 autoDismissKeyboard
  • Scroll —— 以元素为起点或从屏幕中央向上/下/左/右滚动,支持滚动到顶/底/左/右。
  • DragAndDrop —— 从一个元素拖拽到另一个元素。
  • KeyboardPress —— 按下指定键位。
  • LongPress —— 长按目标元素,可选自定义时长。
  • PullGesture —— 上拉或下拉(如下拉刷新),可选距离与持续时间。
  • Pinch —— 双指缩放手势。scale > 1 放大,scale < 1 缩小。
  • ClearInput —— 清空输入框内容。
  • Launch —— 打开网页或 package/.Activity
  • Terminate —— 按包名强制停止应用。
  • RunAdbShell —— 执行原始 adb shell 命令。
  • AndroidBackButton —— 触发系统返回。
  • AndroidHomeButton —— 回到桌面。
  • AndroidRecentAppsButton —— 打开多任务/最近应用。

AndroidDevice

创建一个可供 AndroidAgent 驱动的 adb 设备实例。

导入

import { AndroidDevice, getConnectedDevices } from '@midscene/android';

构造函数

const device = new AndroidDevice(deviceId, {
  // 设备参数...
});

设备选项

  • deviceId: string —— 来自 adb devicesgetConnectedDevices() 的值。
  • autoDismissKeyboard?: boolean —— 输入完成后自动隐藏键盘,默认 true
  • keyboardDismissStrategy?: 'esc-first' | 'back-first' —— 关闭键盘的顺序,默认 'esc-first'
  • androidAdbPath?: string —— adb 可执行文件的自定义路径。
  • remoteAdbHost?: string / remoteAdbPort?: number —— 指向远程 adb server。
  • imeStrategy?: 'always-yadb' | 'yadb-for-non-ascii' —— 控制何时调用 yadb 进行文本输入,默认 'yadb-for-non-ascii'
    • 'yadb-for-non-ascii'(默认)—— 对 Unicode 字符(包括 Latin Unicode 如 ö、é、ñ)、中文、日文以及格式化符号(如 %s、%d)使用 yadb。纯 ASCII 文本使用更快的原生 adb input text
    • 'always-yadb' —— 对所有文本输入始终使用 yadb,提供最大兼容性,但对纯 ASCII 文本稍慢。
  • displayId?: number —— 在设备镜像多个屏幕时,选择特定虚拟屏幕。
  • screenshotResizeScale?: number —— 已废弃。 此选项已移除,不再生效。如需控制发送给 AI 模型的截图尺寸,请使用 AgentOpt 中的 screenshotShrinkFactor
  • minScreenshotBufferSize?: number —— 截图 buffer 大小校验阈值,单位为字节;低于该值的 buffer 会被视为截图采集失败或已损坏。默认 1024(1KB)。设置为 0 仅跳过此大小校验;Midscene 仍会拒绝空 buffer 和无效图片格式。
  • alwaysRefreshScreenInfo?: boolean —— 每一步都重新查询旋转角度与屏幕尺寸,默认 false
  • scrcpyConfig?: object —— Scrcpy 高性能截图配置,默认关闭。详见下方 Scrcpy 截图模式

Scrcpy 截图模式

默认情况下,Midscene 通过 adb shell screencap 截图,每次耗时约 500–2000ms。开启 Scrcpy 模式后,通过 H.264 视频流实时获取画面,每次截图耗时约 100–200ms

开启方式:

const device = new AndroidDevice(deviceId, {
  scrcpyConfig: {
    enabled: true,
  },
});

可选参数:

参数类型默认值说明
enabledbooleanfalse是否启用 Scrcpy 截图
maxSizenumber0视频流最大分辨率(宽或高),0 表示不缩放
videoBitRatenumber2000000H.264 编码码率(bps)
idleTimeoutMsnumber30000空闲超时后自动断开连接(ms),设为 0 禁用
Tip

Scrcpy 模式在连接失败时会自动降级到 ADB 截图,无需额外处理。

使用说明

  • 可以使用 getConnectedDevices() 发现设备,udidadb devices 输出一致。
  • 可以使用 remoteAdbHost/remoteAdbPort 连接远程 adb;如果 adb 不在 PATH 中,可设置 androidAdbPath

示例

快速开始
import { AndroidAgent, AndroidDevice, getConnectedDevices } from '@midscene/android';

const [first] = await getConnectedDevices();
const device = new AndroidDevice(first.udid);
await device.connect();

const agent = new AndroidAgent(device, {
  aiActionContext: 'If a permissions dialog appears, accept it.',
});

await agent.launch('https://www.ebay.com');
await agent.aiAct('search "Headphones" and wait for results');
const items = await agent.aiQuery(
  '{itemTitle: string, price: number}[], find item in list and corresponding price',
);
console.log(items);
启动原生 App
await agent.launch('com.android.settings/.Settings');
await agent.back();
await agent.home();

AndroidAgent

将 Midscene 的 AI 规划能力绑定到 AndroidDevice,实现 UI 自动化。

导入

import { AndroidAgent } from '@midscene/android';

构造函数

const agent = new AndroidAgent(device, {
  // 通用 Agent 参数...
});

Android 特有选项

  • customActions?: DeviceAction[] —— 通过 defineAction 扩展规划器的可用动作。
  • appNameMapping?: Record<string, string> —— 将友好的应用名称映射到包名。当你在 launch(target) 里传入应用名称时,Agent 会在此映射中查找对应的包名;若未找到映射,则按原样尝试启动 target
  • 其余字段与 API constructors 一致:generateReportreportFileNameaiActionContextmodelConfigcacheIdcreateOpenAIClientonTaskStartTip 等。

使用说明

Info

Android 特有方法

agent.launch()

启动网页或原生 Android activity/package。

function launch(target: string): Promise<void>;
  • target: string —— 可以是网页 URL,也可以是 package/.Activity 形式的字符串,例如 com.android.settings/.Settings,也可以是应用包名、URL 或应用名称。若传入应用名称且在 appNameMapping 中存在映射,将自动解析为对应包名;若未找到映射,则直接按 target 启动。
agent.runAdbShell()

通过连接的设备运行原始的 adb shell 命令。传入的内容只需要包含 shell 命令本身,不要包含 adb shell 前缀。

function runAdbShell(command: string, opt?: { timeout?: number }): Promise<string>;
  • command: string —— 原样传递给 adb shell 的命令。例如要传 input tap 100 200,不要传 adb shell input tap 100 200
  • opt.timeout?: number —— 可选的命令执行超时时间,单位为毫秒。
const result = await agent.runAdbShell('dumpsys battery', { timeout: 60 * 1000 });
console.log(result);

await agent.runAdbShell('input tap 100 200');
agent.terminate()

终止(强制停止)正在运行的 Android 应用。

function terminate(uri: string): Promise<void>;
  • uri: string —— 应用包名、appNameMapping 中的应用名称,或 package/.Activity(仅使用包名部分)。
await agent.terminate('com.android.settings');
导航辅助
  • agent.back(): Promise<void> —— 触发 Android 系统的返回操作。
  • agent.home(): Promise<void> —— 返回桌面。
  • agent.recentApps(): Promise<void> —— 打开多任务/最近应用界面。

辅助工具

agentFromAdbDevice()

从任意已连接的 adb 设备创建 AndroidAgent

function agentFromAdbDevice(
  deviceId?: string,
  opts?: PageAgentOpt & AndroidDeviceOpt,
): Promise<AndroidAgent>;
  • deviceId?: string —— 连接特定设备;留空表示使用“第一个可用设备”。
  • opts?: PageAgentOpt & AndroidDeviceOpt —— 在一个对象中合并 Agent 选项与 AndroidDevice 的设置。
getConnectedDevices()

列举 Midscene 可驱动的 adb 设备。

function getConnectedDevices(): Promise<Array<{
  udid: string;
  state: string;
  port?: number;
}>>;