我之前一直在用 midsence 搞 UI 自动化,全 AI 的定位和代码编写方式还是挺爽的。 现在总结一下 midsence 的空间定位原理。
一句话总结:midsence 基于 playwright。 利用视觉 +DOM 分析精确定位控件位置,使用 CDP 操作浏览器。
你的代码:
await agent.aiTap('"确定"按钮')
↓
Midscene 内部:
page.screenshot()
↓
获取当前视口的 PNG 图像
(全彩、包含实时渲染内容)
关键点:
page.screenshot()(非 CDP)同步获取 DOM 可访问性树(Accessibility Tree)
page.evaluate(() => {
// 获取 DOM 树结构、标签名、属性、文案等
return {
html: document.documentElement.outerHTML, // 完整 DOM
accessibility: getAccessibilityTree(), // 无障碍树
viewport: { width: 1920, height: 1080 },
url: window.location.href
}
})
关键点:
客户端 服务端
┌─────────────────────┐ ┌──────────────────────┐
│ Midscene Client │ │ Qwen-VL-Max 视觉模型 │
│ │ │ (DashScope OpenAI API)
│ 构建请求: │ │ │
│ ┌─────────────────┐ │ │ 接收: │
│ │ 截图(PNG) │ ├───────►│ • 操作指令 │
│ │ DOM树 │ │ HTTP │ • 截图 │
│ │ 可访问性树 │ │ POST │ • DOM树 │
│ │ 操作指令: │ │ │ │
│ │ "点击确定按钮" │ │ │ 返回: │
│ └─────────────────┘ │ │ { │
└─────────────────────┘ │ x: 960, │
│ y: 540, ← 坐标! │
│ label: "确定" │
│ } │
└──────────────────────┘
关键点:
{ x, y } 屏幕坐标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)// 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();
关键点:
┌─────────────────────────────────────────────────────┐
│ 你的测试代码 │
│ 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 操作 │
│ • 事件触发 │
│ • 页面渲染 │
└──────────────────────────────────┘
结论:
// 你需要:
// 1. 打开浏览器开发者工具看 DOM
// 2. 手工分析选择器
// 3. 编写脆弱的 CSS
await page.locator('div.modal button.btn-primary:nth-of-type(2)').click();
// 问题:
// - 选择器易失效(HTML 改动就断裂)
// - 动态/隐藏元素无法处理
// - 不够精确
// 你只需:
// 1. 用自然语言描述
// 2. Midscene 自动定位
await agent.aiTap('弹窗右下角的确定按钮');
// 优点:
// - 对人类友好
// - 模型自动适应 UI 变化
// - 处理动态/隐藏元素
// - 视觉语义清晰
输入:"点击右上角的用户菜单"
↓
模型理解:我需要找一个"用户相关的菜单",位置在"右上角"
看到的:[截图 PNG]
• 看到右上角有个头像
• 旁边有个向下箭头(符号)
• 这看起来是个菜单触发器
同时看到 DOM:
<div class="user-menu">
<img src="avatar.png" />
<span class="dropdown-icon">▼</span>
</div>
模型判断:
• 用户头像在屏幕上的像素坐标 (1850, 30)
• 或者点击旁边的菜单箭头 (1870, 30)
↓
返回给 Midscene:
{
x: 1870,
y: 30,
confidence: 0.95, // 置信度
label: "用户菜单"
}
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();
// 文件: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 消耗 | 说明 |
|---|---|---|
aiTap() |
~300-500 | 截图 + 文本 |
aiInput() |
~300-500 | 截图 + 文本 |
aiAssert() |
~300-500 | 截图 + 推理 |
aiQuery() |
~300-800 | 提取结构化数据 |
aiAction() |
~500-1000 | 多步合并(更省钱) |
// 好:元素有稳定属性
await page.locator('input[name="username"]').fill('test');
// 坏:每次用 AI(浪费 Token)
await agent.aiInput('用户名输入框', { value: 'test' });
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",
然后点击登录按钮
`);
aiAssert 做轮询验证// 稳定:轮询等待元素出现
const deadline = Date.now() + 10000;
while (Date.now() < deadline) {
try {
await agent.aiAssert('页面显示了"操作成功" Toast');
return true; // 成功
} catch {
await this.wait(500); // 继续轮询
}
}
| 维度 | Midscene | Selenium / Cypress |
|---|---|---|
| 定位方式 | 视觉 AI + 自然语言 | CSS/XPath 选择器 |
| 对 UI 变化的抗性 | 高(AI 自适应) | 低(选择器易失效) |
| 动态元素处理 | 优秀(看得见就能点) | 困难(需等待/重试) |
| 学习曲线 | 低(就像跟人说话) | 中等(需学选择器语法) |
| Token 成本 | ~300-1000/次 | 0(完全离线) |
| 速度 | 中等(需网络调用) | 快速(本地执行) |
| 多浏览器支持 | Playwright(都支持) | 都支持 |
