与任意界面集成

你可以使用 Midscene 的 Agent 来控制任意界面,比如 IoT 设备、内部应用、车载显示器等,只需要实现一个符合 AbstractInterface 定义的 UI 操作类。

在实现了 UI 操作类之后,你可以获得 Midscene Agent 的全部特性:

  • TypeScript 的 GUI 自动化 Agent SDK,支持与任意界面集成
  • 用于调试的 Playground
  • 通过 yaml 脚本控制界面
  • 暴露 UI 操作的 MCP 服务

演示和社区项目

我们已经为你准备了一个演示项目,帮助你学习如何定义自己的界面类。强烈建议你查看一下。

还有一些使用此功能的社区项目:

  • midscene-ios - 使用 Midscene 驱动 "iPhone 镜像" 应用的项目

配置 AI 模型服务

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

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

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

实现你自己的界面类

关键概念

  • AbstractInterface 类:一个预定义的抽象类,可以连接到 Midscene 智能体
  • 动作空间:描述可以在界面上执行的动作集合。这将影响 AI 模型如何规划和执行动作

步骤 1. 从 demo 项目开始

我们提供了一个演示项目,运行了本文档中的所有功能。这是最快的启动方式。

# 准备项目
git clone https://github.com/web-infra-dev/midscene-example.git
cd midscene-example/custom-interface
npm install
npm run build

# 运行演示
npm run demo

步骤 2. 实现你的界面类

定义一个继承 AbstractInterface 类的类,并实现所需的方法。

你可以从 ./src/sample-device.ts 文件中获取示例实现。让我们快速浏览一下。

import type { DeviceAction, Size } from '@midscene/core';
import { getMidsceneLocationSchema, z } from '@midscene/core';
import {
  type AbstractInterface,
  defineAction,
  defineActionTap,
  defineActionInput,
  // ... 其他动作导入
} from '@midscene/core/device';

export interface SampleDeviceConfig {
  deviceName?: string;
  width?: number;
  height?: number;
  dpr?: number;
}

/**
 * SampleDevice - AbstractInterface 的模板实现
 */
export class SampleDevice implements AbstractInterface {
  interfaceType = 'sample-device';
  private config: Required<SampleDeviceConfig>;

  constructor(config: SampleDeviceConfig = {}) {
    this.config = {
      deviceName: config.deviceName || 'Sample Device',
      width: config.width || 1920,
      height: config.height || 1080,
      dpr: config.dpr || 1,
    };
  }

  /**
   * 必需:截取屏幕截图并返回 base64 字符串
   */
  async screenshotBase64(): Promise<string> {
    // TODO:实现实际的屏幕截图捕获
    console.log('📸 Taking screenshot...');
    return 'data:image/png;base64,...'; // 你的屏幕截图实现
  }

  /**
   * 必需:获取界面尺寸
   */
  async size(): Promise<Size> {
    return {
      width: this.config.width,
      height: this.config.height,
      dpr: this.config.dpr,
    };
  }

  /**
   * 必需:定义 AI 模型的可用动作
   */
  actionSpace(): DeviceAction[] {
    return [
      // 基础点击动作
      defineActionTap(async (param) => {
        // TODO:实现在 param.locate.center 坐标的点击
        await this.performTap(param.locate.center[0], param.locate.center[1]);
      }),

      // 文本输入动作  
      defineActionInput(async (param) => {
        // TODO:实现文本输入
        await this.performInput(param.locate.center[0], param.locate.center[1], param.value);
      }),

      // 自定义动作示例
      defineAction({
        name: 'CustomAction',
        description: '你的自定义设备特定动作',
        paramSchema: z.object({
          locate: getMidsceneLocationSchema(),
          // ... 自定义参数
        }),
        call: async (param) => {
          // TODO:实现自定义动作
        },
      }),
    ];
  }

  async destroy(): Promise<void> {
    // TODO:清理资源
  }

  // 私有实现方法
  private async performTap(x: number, y: number): Promise<void> {
    // TODO:你的实际点击实现
  }

  private async performInput(x: number, y: number, text: string): Promise<void> {
    // TODO:你的实际输入实现  
  }
}

需要实现的关键方法有:

  • screenshotBase64()size():帮助 AI 模型获取界面上下文
  • actionSpace():一个由 DeviceAction 组成的数组,定义了在界面上可以执行的动作。AI 模型将使用这些动作来执行操作。Midscene 已为常见界面与设备提供了预定义动作空间,同时也支持定义任何自定义动作。

使用这些命令运行 Agent:

  • npm run build 重新编译 Agent 代码
  • npm run demo 使用 JavaScript 运行智能体
  • npm run demo:yaml 使用 yaml 脚本运行智能体

步骤 3. 使用 Playground 测试 Agent

为 Agent 附加一个 Playground 服务,即可在浏览器中测试你的 Agent。

import 'dotenv/config'; // 从 .env 文件里读取 Midscene 环境变量
import { playgroundForAgent } from '@midscene/playground';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 实例化 device 和 agent
const device = new SampleDevice();
await device.launch();
const agent = new Agent(device);

// 启动 playground
const server = await playgroundForAgent(agent).launch();

// 关闭 Playground
await sleep(10 * 60 * 1000);
await server.close();
console.log('Playground 已关闭!');

步骤 4. 测试 MCP 服务

(仍在开发中)

步骤 5. 发布 npm 包,让你的用户使用它

./index.ts 文件已经导出了你的 Agent 与界面类。现在可以发布到 npm。

package.json 文件中填写 nameversion,然后运行以下命令:

npm publish

你的 npm 包的典型用法如下:

import 'dotenv/config'; // 从 .env 文件里读取 Midscene 环境变量
import { playgroundForAgent } from '@midscene/playground';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 实例化 device 和 agent
const device = new SampleDevice();
await device.launch();
const agent = new Agent(device);

await agent.aiAct('click the button');

步骤 6. 在 Midscene CLI 和 YAML 脚本中调用你的类

编写一个包含 interface 字段的 yaml 脚本来调用你的类:

interface:
  module: 'my-pkg-name'
  # export: 'MyDeviceClass' # 如果是具名导出,使用该字段

config:
  output: './data.json'

该配置等价于:

import MyDeviceClass from 'my-pkg-name';
const device = new MyDeviceClass();
const agent = new Agent(device, {
  output: './data.json',
});

YAML 的其他字段与自动化脚本文档一致。

API 参考

AbstractInterface

import { AbstractInterface } from '@midscene/core';

AbstractInterface 是智能体控制界面的关键类。

以下是你需要实现的必需方法:

  • interfaceType: string:为界面定义一个名称,这不会提供给 AI 模型
  • screenshotBase64(): Promise<string>:截取界面的屏幕截图并返回带有 'data:image/ 前缀的 base64 字符串
  • size(): Promise<Size>:界面的大小和 dpr,它是一个具有 widthheightdpr 属性的对象
  • actionSpace(): DeviceAction[] | Promise<DeviceAction[]>:界面的动作空间,它是一个 DeviceAction 对象数组。在这里你可以使用预定义动作,或是自定义交互操作。

类型签名:

import type { DeviceAction, Size, UIContext } from '@midscene/core';
import type { ElementNode } from '@midscene/shared/extractor';

abstract class AbstractInterface {
  // 必选
  abstract interfaceType: string;
  abstract screenshotBase64(): Promise<string>;
  abstract size(): Promise<Size>;
  abstract actionSpace(): DeviceAction[] | Promise<DeviceAction[]>;

  // 可选:生命周期/钩子
  abstract destroy?(): Promise<void>;
  abstract describe?(): string;
  abstract beforeInvokeAction?(actionName: string, param: any): Promise<void>;
  abstract afterInvokeAction?(actionName: string, param: any): Promise<void>;
}

以下是你可以实现的可选方法:

  • destroy?(): Promise<void>:销毁
  • describe?(): string:界面描述,这可能会用于报告和 Playground,但不会提供给 AI 模型
  • beforeInvokeAction?(actionName: string, param: any): Promise<void>:在动作空间中调用动作之前的钩子函数
  • afterInvokeAction?(actionName: string, param: any): Promise<void>:在调用动作之后的钩子函数

动作空间(Action Space)

动作空间是界面上可执行动作的集合。AI 模型将使用这些动作来执行操作。所有动作的描述和参数模式都会提供给 AI 模型。

为了帮助你轻松定义动作空间,Midscene 为最常见的界面和设备提供了一组预定义的动作,同时也支持定义任意自定义动作。

以下是如何导入工具来定义动作空间:

import {
	type ActionTapParam,
	defineAction,
	defineActionTap,
} from "@midscene/core/device";

预定义的动作

这些是最常见界面和设备的预定义动作空间。你可以通过实现动作的调用方法将它们暴露给定制化界面。

你可以在这些函数的类型定义中找到动作的参数。

  • defineActionTap():定义点击动作。这也是 aiTap 方法的调用函数。
  • defineActionDoubleClick():定义双击动作
  • defineActionInput():定义输入动作。这也是 aiInput 方法的调用函数。这也是 aiInput 方法的调用函数。
  • defineActionKeyboardPress():定义键盘按下动作。这也是 aiKeyboardPress 方法的调用函数。
  • defineActionScroll():定义滚动动作。这也是 aiScroll 方法的调用函数。
  • defineActionDragAndDrop():定义拖放动作
  • defineActionLongPress():定义长按动作
  • defineActionSwipe():定义滑动动作

定义一个自定义动作

你可以使用 defineAction() 函数定义自己的动作。你也可以使用这种方式为 PuppeteerAgentAgentOverChromeBridgeAndroidAgent 定义更多动作。

API 签名:

import { defineAction } from "@midscene/core/device";

defineAction(
  {
    name: string,
    description: string,
    paramSchema: z.ZodType<T>;
    call: (param: z.infer<z.ZodType<T>>) => Promise<void>;
  }
)
  • name:动作的名称,AI 模型将使用此名称调用动作
  • description:动作的描述,AI 模型将使用此描述来理解动作的作用。对于复杂动作,你可以在这里给出更详细的示例说明
  • paramSchema:动作参数的 Zod 模式,AI 模型将根据此模式帮助填充参数
  • call:调用动作的函数,你可以从符合 paramSchemaparam 参数中获取参数

示例:

defineAction({
  name: 'MyAction',
  description: 'My action',
  paramSchema: z.object({
    name: z.string(),
  }),
  call: async (param) => {
    console.log(param.name);
  },
});

如果你想要获取某个元素位置相关的参数,可以使用 getMidsceneLocationSchema() 函数获取特定的 zod 模式。

一个更复杂的示例,关于如何定义自定义动作:

import { getMidsceneLocationSchema } from "@midscene/core/device";

defineAction({
  name: 'LaunchApp',
  description: '启动屏幕上的应用',
  paramSchema: z.object({
    name: z.string().describe('要启动的应用名称'),
    locate: getMidsceneLocationSchema().describe('要启动的应用图标'),
  }),
  call: async (param) => {
    console.log(`launching app: ${param.name}, ui located at: ${JSON.stringify(param.locate.center)}`);
  },
});

playgroundForAgent 函数

import { playgroundForAgent } from '@midscene/playground';

playgroundForAgent 函数用于为特定的 Agent 创建一个 Playground 启动器,让你可以在浏览器中测试和调试你的自定义界面 Agent。

函数签名

function playgroundForAgent(agent: Agent): {
  launch(options?: LaunchPlaygroundOptions): Promise<LaunchPlaygroundResult>
}

参数

  • agent: Agent:要为其启动 Playground 的 Agent 实例

返回值

返回一个包含 launch 方法的对象。

launch 方法选项

interface LaunchPlaygroundOptions {
  /**
   * Playground 服务器端口
   * @default 5800
   */
  port?: number;

  /**
   * 是否自动在浏览器中打开 Playground
   * @default true
   */
  openBrowser?: boolean;

  /**
   * 自定义浏览器打开命令
   * @default macOS 使用 'open',Windows 使用 'start',Linux 使用 'xdg-open'
   */
  browserCommand?: string;

  /**
   * 是否显示服务器日志
   * @default true
   */
  verbose?: boolean;

  /**
   * Playground 服务器实例的唯一标识 ID
   * 同一个 ID 共用 Playground 对话历史
   * @default undefined(生成随机 UUID)
   */
  id?: string;
}

launch 方法返回值

interface LaunchPlaygroundResult {
  /**
   * Playground 服务器实例
   */
  server: PlaygroundServer;

  /**
   * 服务器端口
   */
  port: number;

  /**
   * 服务器主机地址
   */
  host: string;

  /**
   * 关闭 Playground 的函数
   */
  close: () => Promise<void>;
}

使用示例

import 'dotenv/config';
import { playgroundForAgent } from '@midscene/playground';
import { SampleDevice } from './sample-device';
import { Agent } from '@midscene/core/agent';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 创建设备和 Agent 实例
const device = new SampleDevice();
const agent = new Agent(device);

// 启动 Playground
const result = await playgroundForAgent(agent).launch({
  port: 5800,
  openBrowser: true,
  verbose: true
});

console.log(`Playground 已启动:http://${result.host}:${result.port}`);

// 在需要时关闭 Playground
await sleep(10 * 60 * 1000); // 等待 10 分钟
await result.close();
console.log('Playground 已关闭!');

常见问题(FAQ)

我的 interface-controller 是通用的,可以收录到本文档中吗?

可以,我们很乐意收集有创意的项目并将它们列在本文档中。

当项目准备好后,给我们提一个 issue