前言
我之前一直在用 midsence 搞 UI 自动化,全 AI 的定位和代码编写方式还是挺爽的。 现在总结一下 midsence 的空间定位原理。
Midscene 视觉定位与交互机制深度解析
一句话总结:midsence 基于 playwright。 利用视觉 +DOM 分析精确定位控件位置,使用 CDP 操作浏览器。
一、完整工作流程
步骤 1:截图捕获 → 视觉上下文
你的代码:
await agent.aiTap('"确定"按钮')
↓
Midscene 内部:
page.screenshot()
↓
获取当前视口的 PNG 图像
(全彩、包含实时渲染内容)
关键点:
- 用的是 Playwright 的
page.screenshot()(非 CDP) - 捕获的是真实渲染后的 UI 画面,包括动画、样式、隐藏元素等
- 输出是标准 PNG 图像
步骤 2:DOM 提取 → 语义上下文
同步获取 DOM 可访问性树(Accessibility Tree)
page.evaluate(() => {
// 获取 DOM 树结构、标签名、属性、文案等
return {
html: document.documentElement.outerHTML, // 完整 DOM
accessibility: getAccessibilityTree(), // 无障碍树
viewport: { width: 1920, height: 1080 },
url: window.location.href
}
})
关键点:
- 提供"文字描述"给模型(哪些元素叫什么名字、在哪里)
- 结合截图,模型同时有"看"和"读"的双重上下文
步骤 3:模型推理 → 坐标预测
客户端 服务端
┌─────────────────────┐ ┌──────────────────────┐
│ Midscene Client │ │ Qwen-VL-Max 视觉模型 │
│ │ │ (DashScope OpenAI API)
│ 构建请求: │ │ │
│ ┌─────────────────┐ │ │ 接收: │
│ │ 截图(PNG) │ ├───────►│ • 操作指令 │
│ │ DOM树 │ │ HTTP │ • 截图 │
│ │ 可访问性树 │ │ POST │ • DOM树 │
│ │ 操作指令: │ │ │ │
│ │ "点击确定按钮" │ │ │ 返回: │
│ └─────────────────┘ │ │ { │
└─────────────────────┘ │ x: 960, │
│ y: 540, ← 坐标! │
│ label: "确定" │
│ } │
└──────────────────────┘
关键点:
- 完全基于 HTTP 调用(OpenAI 兼容接口)
- 模型返回的是
{ x, y }屏幕坐标 - 不涉及 CDP(CDP 是低级浏览器通信协议,这里完全用的是 HTTP REST API)
步骤 4:坐标反推 → Locator 生成
Midscene 内部把坐标转换为 Playwright Locator
// 模型返回坐标 (960, 540)
const { x, y } = { x: 960, y: 540 };
// Midscene 内部反推:
// 1. 找到该坐标对应的 DOM 元素
const element = document.elementFromPoint(x, y);
// 2. 生成稳定的 CSS Selector 或 XPath
// 如果有 id/data-testid:
// → "button#confirm-btn"
// 否则根据结构生成:
// → "div.dialog button.primary"
// 3. 返回可执行的 Playwright Locator
return page.locator('button.primary:has-text("确定")');
关键点:
- 通过
document.elementFromPoint()定位元素(JavaScript DOM API) - 再生成稳定的 CSS/XPath selector
- 最终转换为 Playwright Locator
步骤 5:执行操作 → 真实交互
// Midscene 拿到 Locator 后,用 Playwright 执行真实操作
const locator = page.locator('button.primary:has-text("确定")');
await locator.click(); // 真实浏览器点击事件
// 触发:mousedown → mouseup → click
// 也会触发所有 JS 事件监听器
// 或输入
await locator.fill('input value'); // 真实键盘输入
// 或滚动
await locator.scrollIntoViewIfNeeded();
// 或断言
const isVisible = await locator.isVisible();
关键点:
- 都是 Playwright 原生 API 执行
- 都是真实浏览器事件(不是模拟)
- 底层 Playwright 才会用到 Chrome DevTools Protocol(CDP)
- 但这是 Playwright 的实现细节,你的代码无需关心
二、CDP 在整个流程中的位置
┌─────────────────────────────────────────────────────┐
│ 你的测试代码 │
│ await agent.aiTap('"确定"按钮') │
└────────────────────────┬────────────────────────────┘
│
┌────────────────┴────────────────┐
│ │
┌───▼──────┐ ┌──────▼──────┐
│Midscene │ │HTTP REST API│
│1. 截图 │ │(OpenAI格式) │
│2. 提取DOM│◄─────────────────┤Qwen-VL-Max │
│3. 发HTTP │ │模型推理 │
│ │─────────────────►│返回坐标 │
└──────────┘ └─────────────┘
│
│ 4. 坐标→Locator
│ 5. Playwright.click()
│
┌───▼──────────────────────────────┐
│ Playwright (JavaScript API) │
│ • page.locator() │
│ • locator.click() │
│ • page.evaluate() │
└────────────┬─────────────────────┘
│ ⚠️ 这里才用到 CDP
│
┌────────────▼─────────────────────┐
│ Chrome DevTools Protocol (CDP) │
│ • sendMessage() │
│ • Runtime.evaluate │
│ • Input.dispatchMouseEvent │
│ • 低级浏览器通信 WebSocket │
└────────────┬─────────────────────┘
│
┌────────────▼─────────────────────┐
│ Chromium 浏览器内核 │
│ • DOM 操作 │
│ • 事件触发 │
│ • 页面渲染 │
└──────────────────────────────────┘
结论:
- Midscene 的视觉定位不用 CDP(用 HTTP + Playwright DOM API)
- Playwright 的底层实现用到 CDP(但透明、无需关心)
- 你的代码层面完全不需要关心 CDP
三、关键代码对比
传统方式(手写 CSS)
// 你需要:
// 1. 打开浏览器开发者工具看 DOM
// 2. 手工分析选择器
// 3. 编写脆弱的 CSS
await page.locator('div.modal button.btn-primary:nth-of-type(2)').click();
// 问题:
// - 选择器易失效(HTML 改动就断裂)
// - 动态/隐藏元素无法处理
// - 不够精确
Midscene 方式(自然语言)
// 你只需:
// 1. 用自然语言描述
// 2. Midscene 自动定位
await agent.aiTap('弹窗右下角的确定按钮');
// 优点:
// - 对人类友好
// - 模型自动适应 UI 变化
// - 处理动态/隐藏元素
// - 视觉语义清晰
四、视觉模型工作的 4 个阶段
阶段 1:理解操作意图
输入:"点击右上角的用户菜单"
↓
模型理解:我需要找一个"用户相关的菜单",位置在"右上角"
阶段 2:分析视觉信息
看到的:[截图 PNG]
• 看到右上角有个头像
• 旁边有个向下箭头(符号)
• 这看起来是个菜单触发器
同时看到 DOM:
<div class="user-menu">
<img src="avatar.png" />
<span class="dropdown-icon">▼</span>
</div>
阶段 3:坐标预测
模型判断:
• 用户头像在屏幕上的像素坐标 (1850, 30)
• 或者点击旁边的菜单箭头 (1870, 30)
↓
返回给 Midscene:
{
x: 1870,
y: 30,
confidence: 0.95, // 置信度
label: "用户菜单"
}
阶段 4:执行定位
Midscene 接收坐标后:
// 1. document.elementFromPoint(1870, 30)
// → 找到 <span class="dropdown-icon">
// 2. 从该元素反推父容器和选择器
// → button.user-menu-trigger
// 3. 用 Playwright 执行
await page.locator('button.user-menu-trigger').click();
五、你的项目中的实际使用
BasePage 中的 fallback 模式
// 文件:src/pages/BasePage.ts
protected async withFallback(options: {
label: string;
cssAction: () => Promise<boolean>; // 第一选择:CSS 定位
aiFallback: () => Promise<void>; // 兜底:AI 视觉
aiOnly?: boolean;
}): Promise<void> {
if (options.aiOnly) {
// 直接用 AI(UI 太复杂,CSS 无法可靠定位)
await options.aiFallback();
return;
}
try {
// 优先尝试 CSS 定位(快速、零成本)
const success = await options.cssAction();
if (success) return;
} catch (e) {
console.warn(`CSS 定位失败: ${e.message}`);
}
// CSS 失败,降级到 AI
await options.aiFallback();
}
实际使用示例
// 文件:src/pages/AppConfigPage.ts
async clickConfirmButton() {
await this.withFallback({
label: 'clickConfirmButton()',
// 步骤 1:尝试 CSS 定位
cssAction: async () => {
const btn = document.querySelector('button.confirm') as HTMLButtonElement;
if (btn && btn.offsetParent !== null) {
btn.click();
return true;
}
return false;
},
// 步骤 2:AI 视觉兜底(调用 Midscene)
aiFallback: async () => {
// ← 这里触发上面 5 步工作流程!
await this.getAgent().aiTap('弹窗底部的「确定」按钮');
}
});
}
六、Token 消耗与优化
每次 AI 调用的成本
| 操作 | Token 消耗 | 说明 |
|---|---|---|
aiTap() |
~300-500 | 截图 + 文本 |
aiInput() |
~300-500 | 截图 + 文本 |
aiAssert() |
~300-500 | 截图 + 推理 |
aiQuery() |
~300-800 | 提取结构化数据 |
aiAction() |
~500-1000 | 多步合并(更省钱) |
优化策略
策略 1:优先用 CSS(零成本)
// 好:元素有稳定属性
await page.locator('input[name="username"]').fill('test');
// 坏:每次用 AI(浪费 Token)
await agent.aiInput('用户名输入框', { value: 'test' });
策略 2:用 aiAction 合并多步(省 50% Token)
// 低效:3 次截图 + 3 次 AI 调用 = ~1500 Token
await agent.aiInput('用户名', { value: 'alice' });
await agent.aiInput('密码', { value: 'pass123' });
await agent.aiTap('登录按钮');
// 高效:1 次截图 + 1 次 AI 调用 = ~800 Token
await agent.aiAction(`
在用户名输入框填入"alice",
在密码输入框填入"pass123",
然后点击登录按钮
`);
策略 3:用 aiAssert 做轮询验证
// 稳定:轮询等待元素出现
const deadline = Date.now() + 10000;
while (Date.now() < deadline) {
try {
await agent.aiAssert('页面显示了"操作成功" Toast');
return true; // 成功
} catch {
await this.wait(500); // 继续轮询
}
}
七、Midscene vs 传统自动化测试
| 维度 | Midscene | Selenium / Cypress |
|---|---|---|
| 定位方式 | 视觉 AI + 自然语言 | CSS/XPath 选择器 |
| 对 UI 变化的抗性 | 高(AI 自适应) | 低(选择器易失效) |
| 动态元素处理 | 优秀(看得见就能点) | 困难(需等待/重试) |
| 学习曲线 | 低(就像跟人说话) | 中等(需学选择器语法) |
| Token 成本 | ~300-1000/次 | 0(完全离线) |
| 速度 | 中等(需网络调用) | 快速(本地执行) |
| 多浏览器支持 | Playwright(都支持) | 都支持 |
参考资源
- Midscene 官网:https://midscenejs.com
再宣传下星球,后续教程都在星球发布

转载文章时务必注明原作者及原始链接,并注明「发表于 TesterHome 」,并不得对作品进行修改。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暫無回覆。