测试开发之路 Midscene 视觉定位与交互机制深度解析

孙高飞 · 2026年06月29日 · 48 次阅读

前言

我之前一直在用 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(都支持) 都支持

参考资源

再宣传下星球,后续教程都在星球发布

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册