Midscene.js 原理拆解:它不是“自然语言点按钮”,而是一套会看屏幕的 UI 自动化运行时
摘要 Midscene.js 是一套基于视觉识别的UI自动化运行时系统,其核心创新点在于将传统UI自动化流程重构为四个层次:视觉理解、设备执行、结果校验和报告记录。与传统的基于元素定位的自动化工具不同,Midscene采用"先识别屏幕内容再操作"的模式,通过截图让AI模型判断目标位置,再转换为设备坐标执行操作。该系统特别适用于selector不稳定、自绘界面复杂、弹窗变化频繁以
Midscene.js 原理拆解:它不是“自然语言点按钮”,而是一套会看屏幕的 UI 自动化运行时
摘要(先看结论)
- Midscene.js 最值得理解的,不是它能不能帮你点按钮,而是它把 UI 自动化拆成了四层:
看图理解、设备执行、结果校验、报告留痕。 - 如果只看一句
await agent.aiTap('搜索按钮'),你很容易误以为它只是把click()换成了自然语言。真正的情况是:它会先截图、再让模型判断目标在哪、再换算成设备坐标、最后才执行点击。 - 它最适合的场景,不是所有页面都替换成 AI 自动化,而是
selector 不稳、自绘界面多、弹窗变化快、跨端心智不统一的那部分问题。 - 它也不是银弹。它更慢、更贵、判断有时会摇摆,但换来的是一套更贴近“用户眼里页面长什么样”的自动化能力。
为什么 Midscene 值得单独讲
传统 UI 自动化的主流写法,大致都长这样:
先知道元素在哪
-> 再去 click / input / assert
问题在于,“先知道元素在哪” 这件事,在很多真实页面里并不稳定。
常见痛点有四类:
- Web 里 selector 经常随着组件重构、文案调整、层级变化而失效。
- Android 和 iOS 的控件树、自动化工具链、调试方式都不一样,团队很难形成统一心智。
- Canvas、自绘组件、图片按钮、复杂弹窗,本来就不适合强依赖结构树来描述。
- 对业务同学来说,“点第一个搜索入口”和“点
//*[@id='xxx']/div[3]/button”根本不是同一种语言。
Midscene 做的事,可以粗暴概括成一句话:
把“怎么找元素”这件事,
从手写 selector,
前移成“先看懂当前屏幕”。
这听起来像一句大话,所以更需要拆开讲。
先看一个最小示意:一次 Android 搜索链路到底怎么写
下面这段代码不是某个私有仓库的原样拷贝,而是把一个 Android 冒烟场景提炼成了“读者单独看也能懂”的最小示意。
目标很简单:
- 连接一台 Android 真机
- 启动目标 App
- 找到搜索入口
- 输入关键词
- 点击搜索
- 判断结果页是否真的出现
如果你读到这里,已经想自己跑一个 demo,准备工作其实也不复杂,主要分成四块。
跑 demo 前要准备什么
1. 本地环境
最小要求通常是:
- 安装 Node.js 18 或更高版本
- 安装 npm
- 安装 Android Platform Tools,并确保终端里能执行
adb - 准备一台开启 USB 调试的 Android 真机
最基本的自检命令通常是:
node -v
npm -v
adb version
adb devices
如果 adb devices 里看不到状态为 device 的真机,先别急着怪 Midscene,先解决数据线、USB 调试授权、厂商驱动这些基础问题。
2. 新建一个最小工程并安装依赖
如果只是想跑通一个最小 demo,通常一个普通的 Node 工程就够了:
mkdir midscene-android-demo
cd midscene-android-demo
npm init -y
npm install @midscene/android dotenv
npm install -D typescript tsx @types/node
如果你懒得配 TypeScript,也可以直接用 JavaScript。但从可读性和提示信息来看,TypeScript 更适合做这种示意工程。
3. 准备模型参数
Midscene 不是本地规则引擎,它需要一个能看图的模型。
最少你要准备这几个参数:
MIDSCENE_MODEL_NAMEMIDSCENE_BASE_URLMIDSCENE_API_KEYMIDSCENE_MODEL_FAMILY
可以把它们理解成:
MODEL_NAME:这次到底调用哪个模型或接入点BASE_URL:请求发到哪家兼容 OpenAI 协议的网关API_KEY:鉴权凭证MODEL_FAMILY:告诉 Midscene 这是哪一类模型,以及它能不能看图
例如你走火山方舟上的 Doubao Seed 接入点,.env 可以长这样:
ANDROID_SERIAL=你的设备序列号
APP_PACKAGE_NAME=com.example.app
SEARCH_KEYWORD=新能源
MIDSCENE_MODEL_NAME=ep-xxxxxxxxxxxxxxxx
MIDSCENE_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
MIDSCENE_API_KEY=你的 API Key
MIDSCENE_MODEL_FAMILY=doubao-seed
如果你走别的兼容 OpenAI 协议的平台,核心思路也一样:
MIDSCENE_MODEL_NAME填具体模型 ID 或接入点 IDMIDSCENE_BASE_URL填平台网关地址MIDSCENE_API_KEY填平台密钥MIDSCENE_MODEL_FAMILY填 Midscene 能识别的模型族
这里最关键的一点不是“它能不能聊天”,而是“它能不能看图”。因为 Midscene 做的是视觉驱动自动化,不是纯文本问答。
4. 先跑前置检查,再跑主链路
真正写脚本前,建议先做一轮最小检查:
adb devices
adb shell pm list packages | grep 你的包名关键词
确认设备在线、目标 App 已安装之后,再运行你自己的脚本。
更稳的做法是单独写一个 preflight:
- 检查
adb是否可用 - 检查真机是否在线
- 检查目标 App 是否安装
- 检查模型参数是否填全
这样后面一旦失败,你至少知道问题是出在设备、App、还是模型配置。
import 'dotenv/config';
import {
AndroidAgent,
AndroidDevice,
getConnectedDevices,
} from '@midscene/android';
type AppConfig = {
androidSerial?: string;
packageName: string;
searchKeyword: string;
modelName: string;
baseUrl: string;
apiKey: string;
modelFamily: string;
};
function readConfig(env: NodeJS.ProcessEnv): AppConfig {
return {
androidSerial: env.ANDROID_SERIAL,
packageName: env.APP_PACKAGE_NAME || 'com.example.app',
searchKeyword: env.SEARCH_KEYWORD || '新能源',
modelName: env.MIDSCENE_MODEL_NAME || '',
baseUrl: env.MIDSCENE_BASE_URL || '',
apiKey: env.MIDSCENE_API_KEY || '',
modelFamily: env.MIDSCENE_MODEL_FAMILY || '',
};
}
async function main() {
const config = readConfig(process.env);
// 1. 找到当前通过 ADB 连接的 Android 设备
const devices = await getConnectedDevices();
const target = devices.find((item) =>
config.androidSerial ? item.udid === config.androidSerial : true,
);
if (!target) {
throw new Error('No Android device found');
}
// 2. 把这台真机包装成一个可执行动作的设备对象
const device = new AndroidDevice(target.udid, {
keyboardDismissStrategy: 'back-first',
});
await device.connect();
// 3. 基于设备对象创建 AndroidAgent
// 它会把“看图理解 + 设备执行 + 报告记录”绑在一起
const agent = new AndroidAgent(device, {
generateReport: true,
modelConfig: {
MIDSCENE_MODEL_NAME: config.modelName,
MIDSCENE_MODEL_BASE_URL: config.baseUrl,
MIDSCENE_MODEL_API_KEY: config.apiKey,
MIDSCENE_MODEL_FAMILY: config.modelFamily,
},
aiActionContext:
'If permission dialogs, user agreements, or login popups appear, close or accept them so the search flow can continue.',
});
// 4. 拉起目标 App
await agent.launch(config.packageName);
// 5. 先确认首页或搜索入口已经出现,再进入后续动作
await agent.aiWaitFor('首页或搜索入口已经可见');
// 6. 让模型在当前屏幕里找“搜索入口”,然后点击
await agent.aiTap('页面上的搜索入口');
// 7. 找到搜索输入框并输入关键词
await agent.aiInput('搜索输入框', {
value: config.searchKeyword,
autoDismissKeyboard: true,
});
// 8. 找到搜索按钮并点击
await agent.aiTap('搜索按钮');
// 9. 等待结果页核心内容出现
await agent.aiWaitFor('页面出现搜索结果列表或结果页核心内容');
// 10. 最后再问一次模型:现在最能证明已进入结果页的内容是什么
const summary = await agent.aiQuery<string>(
'用一句话描述当前页面最能证明已经进入搜索结果页的文字或内容',
);
console.log(summary);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
这段代码最值得注意的点,不是写法有多复杂,而是它非常短。越短,越说明真正的复杂度被压进了 SDK。
先建立一个总心智模型
如果把这段代码背后的运行过程画出来,大致是这样:
.env / 终端参数
|
v
读取配置
|
v
发现设备 -> 连接设备
|
v
创建 AndroidAgent
|
v
截图 -> 模型理解 -> 执行动作 -> 再截图
|
v
结果校验 + HTML report
再换一种说法,Midscene 在 Android 上真正做的不是:
把自然语言直接翻译成 adb 命令
而是:
先把真机抽象成统一屏幕,
再让模型围绕截图做判断和定位,
最后把动作稳定落到设备执行层。
这就是为什么它更像“运行时”,而不是“自然语言 click 封装”。
先把最基础的问题讲透
为什么很多 Midscene Android 项目都会有一个 preflight
因为这类工程最怕一开始就把所有问题混在一起。
preflight 的价值不是“跑个前置脚本显得专业”,而是先把下面这些问题分离掉:
adb有没有安装- 真机有没有连上
- USB 调试有没有授权
- 目标 App 包名对不对
常见的第一步就是:
adb version
adb devices
adb shell pm list packages
这三条命令背后回答的是三个完全不同的问题:
adb version:系统能不能找到adbadb devices:手机有没有在线pm list packages:目标 App 在不在这台设备上
如果这三步没过,就不要急着怪 Midscene。
逐段看这段代码到底在做什么
下面开始顺着刚才的最小示意,一段一段拆。
第一段:发现设备
const devices = await getConnectedDevices();
const target = devices.find((item) =>
config.androidSerial ? item.udid === config.androidSerial : true,
);
这里最容易冒出来的问题是:getConnectedDevices() 到底是什么?
你可以把它理解成:
去问当前 ADB server:
现在有哪些 Android 设备在线?
它本质上是设备发现入口。
这一步不是 AI,也不是视觉理解,而是纯设备层能力。
第二段:包装成 AndroidDevice
const device = new AndroidDevice(target.udid, {
keyboardDismissStrategy: 'back-first',
});
await device.connect();
AndroidDevice 更像“手脚”。
它负责的不是判断页面里有什么,而是把一台真机变成一个“可以被执行动作的对象”。
它通常会处理这些事情:
- 连接 ADB
- 截图
- 点击
- 输入
- 滑动
- 收起键盘
- 启动 App
对应的 ADB 命令速查
- 连接 ADB:
adb devices,例如adb devices - 截图:
adb exec-out screencap -p > home.png - 点击:
adb shell input tap 980 180 - 输入:
adb shell input text "tesla",其中tesla是具体输入内容 - 滑动:
adb shell input swipe 500 1600 500 500 300 - 收起键盘:
adb shell input keyevent 4 - 启动 App:
adb shell am start -n com.example.demo/.MainActivity
如果没有这层,模型就算知道“搜索按钮大概在右上角”,也没有人真正去点。
第三段:创建 AndroidAgent
const agent = new AndroidAgent(device, {
generateReport: true,
modelConfig: {
MIDSCENE_MODEL_NAME: config.modelName,
MIDSCENE_MODEL_BASE_URL: config.baseUrl,
MIDSCENE_MODEL_API_KEY: config.apiKey,
MIDSCENE_MODEL_FAMILY: config.modelFamily,
},
aiActionContext:
'If permission dialogs, user agreements, or login popups appear, close or accept them so the search flow can continue.',
});
这里是全文的关键点。
AndroidAgent 不等于“更高级的设备对象”,它更像一个把三件事捆起来的指挥官:
- 看图理解
- 调设备执行
- 写 report 留痕
可以记成:
AndroidDevice = 手脚
AndroidAgent = 大脑 + 指挥官
其中 modelConfig 决定它到底找哪个模型来做视觉理解。
MIDSCENE_MODEL_FAMILY 这种字段看起来有点抽象,但它的真实作用很朴素:
告诉 Midscene:
你现在接的是哪一类模型,
它能不能看图,
应该按什么策略去调用。
第四段:真正的动作链路
await agent.launch(config.packageName);
await agent.aiWaitFor('首页或搜索入口已经可见');
await agent.aiTap('页面上的搜索入口');
await agent.aiInput('搜索输入框', {
value: config.searchKeyword,
autoDismissKeyboard: true,
});
await agent.aiTap('搜索按钮');
await agent.aiWaitFor('页面出现搜索结果列表或结果页核心内容');
这一段最容易让人误解成:
就是几条“智能 click”
其实不是。
它真正更像一串 感知 -> 决策 -> 执行 的循环。
为什么 aiTap、aiInput、aiWaitFor 看起来像在和模型多轮对话
因为本质上就是。
以这句为例:
await agent.aiTap('页面上的搜索入口');
它背后大致做的是:
1. 先截图
2. 把截图和“页面上的搜索入口”一起交给模型
3. 模型返回一个大致区域
4. 系统把区域换算成设备坐标
5. 再调用设备层去点击
6. 把这一整步写进 report
同样地,其他几个 API 也能这样理解。
aiWaitFor
作用不是动作,而是状态判断。
await agent.aiWaitFor('首页或搜索入口已经可见');
等价于:
请你看这张截图,
判断这个条件是否已经成立。
它的价值是:先确认页面状态到位,再决定要不要继续。
aiTap
作用不是固定坐标点击,而是:
先定位,再点击
所以它更接近“点屏幕上那个搜索入口”,而不是“点 x=500, y=300”。
aiInput
作用不是把文本硬塞给系统,而是:
先找到输入框
-> 聚焦
-> 清空旧内容
-> 输入文字
-> 必要时收起键盘
在 Android 上这一步尤其麻烦,因为中文输入、键盘弹起、焦点变化都可能出问题。
aiQuery
作用不是执行动作,而是把当前页面翻译成人能读懂的一句话或结构化结果。
例如:
const summary = await agent.aiQuery<string>(
'用一句话描述当前页面最能证明已经进入搜索结果页的文字或内容',
);
它本质上是在问:
你现在看到的页面,
最能证明业务目标已经成立的信号是什么?
这类问题特别适合做业务验收。
这不是“纯 AI”,而是“AI + 设备控制 + 业务规则”
如果只看 aiTap() 这类 API,很容易误以为 Midscene 是“全靠模型自动化”。
更准确的结构应该是:
AI 负责理解
设备层负责执行
业务代码负责兜底判断
拆开看:
AI- 判断首页是否可见
- 找搜索入口、输入框、搜索按钮
- 总结页面内容
AndroidDevice / ADB- 启动 App
- 点击
- 输入
- 截图
业务代码- 决定流程顺序
- 决定什么才算“搜索成功”
这也是为什么一份靠谱的 Midscene 脚本,最后一般不会停在“没报错就算成功”。
为什么最后还要做结果校验
很多人第一次写这种脚本,会默认:
已经点了搜索按钮
=
已经进入搜索结果页
这是错的。
真实世界里可能发生很多事:
- 搜索按钮点到了,但页面卡住了
- 页面跳了,但结果区还是空白
- 结果页出来了,但其实是异常兜底页
- 键盘没收起,真正的搜索动作并没有触发
所以更稳的做法是:
const summary = await agent.aiQuery<string>(
'用一句话描述当前页面最能证明已经进入搜索结果页的文字或内容',
);
const hints = ['搜索', '结果', '车型', '商品', '列表'];
const matched = hints.some((item) => summary.includes(item));
if (!matched) {
throw new Error(`Unexpected result page: ${summary}`);
}
这段代码的意义不是“字符串匹配很高级”,而是把“视觉理解”再收敛成业务能接受的通过条件。
Report 为什么不是附件,而是这类工程的核心设施
Midscene 这类工程,如果没有 report,调试成本会非常高。
因为传统自动化的失败通常很直白:
- selector 没找到
- 元素不可点击
- 超时
而视觉自动化的失败会更模糊:
- 模型把图标认错了
- 页面刚加载出来时被误判为空白
- 搜索入口和标题长得太像
- 键盘挡住了真正的按钮
这时 report 的价值就出来了。它会记录:
- 每一步动作
- 每一步截图
- 模型的判断
- 定位结果
- 耗时
- token 消耗
- 哪一步失败了
它的意义不是“生成一个好看的 HTML”,而是:
没有 report,
你几乎没法稳定解释
“为什么这次成功” 和 “为什么这次失败”。
Midscene 的一个真实边界:同一页面,模型可能前后判断不一致
这类方案最需要读者有心理预期的一点是:
它会摇摆。
一个非常典型的场景是:
- 某次
aiWaitFor('页面出现搜索结果列表或结果页核心内容')返回了false - 过了一步,
aiQuery()又总结说结果页已经出现 - 最终脚本仍然通过
这听起来像自相矛盾,但它在视觉自动化里并不罕见。
原因通常有三类:
- 页面正处在骨架屏、白屏、渐进加载的过渡态
- 两次截图的时机不同
- 提示词本身过于抽象,比如“核心内容”这类词没有明确视觉锚点
这正好说明 Midscene 的真实定位:
它的优势
- 不强依赖 selector
- 更适合跨端和弱结构页面
- 更贴近业务语言
- 失败后更容易回看执行过程
它的代价
- 更慢
- 更贵
- 更依赖模型质量
- 同一页面在不同时间点可能给出不同判断
所以它不是“更稳定的传统自动化替代品”,而是:
一种更灵活、但更依赖感知质量的 UI 自动化范式。
如果页面上没有“搜索按钮”,只有一个放大镜图标,它能识别吗
这个问题很适合拿来理解 Midscene 的边界。
如果你写:
await agent.aiTap('搜索按钮');
而页面上只有一个放大镜图标,没有文字按钮,模型有机会识别,但不能保证每次都稳。
原因是视觉模型会结合:
- 图标本身的语义
- 它在页面上的位置
- 周围是不是搜索框、标题栏、工具栏
所以它不是只看字面词,而是在做语义匹配。
但更稳的写法,通常是把提示词描述得更具体:
页面右上角的搜索按钮或放大镜图标
也就是说,Midscene 不是不要提示词,而是更吃提示词质量。
为什么 Android 是最能看出 Midscene 价值的地方
如果只做 Web,很多团队已经有成熟 selector 体系了,Midscene 的优势未必会立刻显现。
但到了 Android,上述问题会马上变得现实:
- 自绘控件多
- 图片按钮多
- 不同机型分辨率和密度不一致
- 中文输入、键盘、权限弹窗都会影响执行
- 控件树不一定像 Web DOM 那样稳定
这时你会发现,Midscene 真正解决的不是“如何更优雅地写点击”,而是:
如何让自动化脚本
先看懂眼前屏幕,
再决定怎么操作。
这就是它和传统自动化思路最根本的差别。
Midscene 适合什么,不适合什么
更适合它的场景
- 你要同时覆盖 Web、Android、iOS 或桌面
- 页面结构不稳,但视觉语义稳定
- 界面里有自绘组件、图片按钮、复杂弹窗
- 你更关心“用户看到的页面是否符合预期”
- 你想给更高层 Agent 暴露 UI 能力
暂时不必优先用它的场景
- 页面 selector 已经非常稳定
- 你追求极高确定性和极低成本
- 已有成熟 Appium / Playwright / 原生控件树体系
- 团队还没有准备好设备权限、模型配置、失败回放这些基础设施
最后给工程师的判断标准
如果你的问题是:
我想把一套确定性脚本写到极致稳定
那传统自动化依然非常重要。
但如果你的问题是:
我希望脚本先看懂当前屏幕,再决定怎么操作
我希望这套心智模型能跨 Web / Android / iOS
我希望上层 Agent 真正拥有 UI 理解和执行能力
那 Midscene 值得认真看。
它不是替代一切 UI 自动化,而是把一件以前经常停留在 demo 层的事,真正拉进了工程体系:
让“视觉理解”成为 UI 自动化的一等能力。
更多推荐



所有评论(0)