• 简体中文
  • 与任意界面集成

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

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

    • TypeScript 的 GUI 自动化 Agent SDK,支持与任意界面集成
    • 用于调试的 Playground
    • 通过 yaml 脚本控制界面
    • 通过 CLI 命令接入 Skills

    演示和社区项目

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

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

    • 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;
    }
    
    /**
     * 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,
        };
      }
    
      /**
       * 必需:截取屏幕截图并返回 base64 字符串
       */
      async screenshotBase64(): Promise<string> {
        // TODO:实现实际的屏幕截图捕获
        console.log('📸 Taking screenshot...');
        return 'data:image/png;base64,...'; // 你的屏幕截图实现
      }
    
      /**
       * 必需:获取界面尺寸
       * 这里的宽高是指界面的逻辑尺寸(logical size),不需要考虑设备像素比(dpr)。defineActionTap 等动作得到的坐标也是基于这个逻辑尺寸的坐标系。你可以在动作实现中根据需要将逻辑坐标转换为物理坐标。
       */
      async size(): Promise<Size> {
        return {
          width: this.config.width,
          height: this.config.height,
        };
      }
    
      /**
       * 必需:定义 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. 添加 Skill 支持(可选)

    Agent Skills 让 AI 编程助手(如 Claude Code、Cline 等)可以通过 CLI 命令驱动你的自定义界面。了解更多请参考 Skills 文档

    在你的 npm 包中添加一个 CLI 入口文件(如 ./src/cli.ts):

    #!/usr/bin/env node
    import { runSkillCLI } from '@midscene/core/skill';
    import { SampleDevice } from './sample-device';
    
    runSkillCLI({
      DeviceClass: SampleDevice,
      scriptName: 'my-device',
    });

    然后在 package.json 中配置 bin 字段:

    {
      "bin": {
        "my-device": "./dist/cli.js"
      }
    }

    发布后,AI 编程助手即可通过 npx my-device 命令来控制你的自定义界面。

    关于 Skill 的编写规范和更多示例,请参考 midscene-skills 仓库。

    步骤 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>:界面的大小,它是一个具有 widthheight 属性的对象
    • 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 type { ExecutorContext } from "@midscene/core";
    import { defineAction } from "@midscene/core/device";
    
    defineAction(
      {
        name: string,
        description: string,
        paramSchema: z.ZodType<T>;
        call: (
          param: z.infer<z.ZodType<T>>,
          context?: ExecutorContext,
        ) => Promise<void>;
      }
    )
    • name:动作的名称,AI 模型将使用此名称调用动作
    • description:动作的描述,AI 模型将使用此描述来理解动作的作用。对于复杂动作,你可以在这里给出更详细的示例说明
    • paramSchema:动作参数的 Zod 模式,AI 模型将根据此模式帮助填充参数
    • call:调用动作的函数,你可以从符合 paramSchemaparam 参数中获取参数
    • context:可选的执行上下文。动作的返回值会写入 task.output(供你的 API 调用方和报告使用),不会发给规划器。若想给下一轮规划传一段简短的、面向规划器的反馈,请设置 context.task.planningFeedback;core 的规划层会对其做截断,以免撑爆模型上下文。

    示例:

    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