> For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt.

# 与任意界面集成

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

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

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

## 演示和社区项目

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

- [演示项目](https://github.com/web-infra-dev/midscene-example/tree/main/custom-interface) - 一个简单的演示项目，展示如何定义自己的界面类

- [Android (adb) Agent](https://github.com/web-infra-dev/midscene/blob/main/packages/android/src/device.ts) - 这是 Midscene Android (adb) Agent，同样依赖此特性实现

- [iOS (WebDriverAgent) Agent](https://github.com/web-infra-dev/midscene/blob/main/packages/ios/src/device.ts) - 这是 Midscene iOS (WebDriverAgent) Agent，同样依赖此特性实现

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

- [midscene-ios](https://github.com/lhuanyu/midscene-ios) - 使用 Midscene 驱动 "iPhone 镜像" 应用的项目

## 配置 AI 模型服务

下面以豆包 Seed 2.1 Turbo 为例：

```bash
export MIDSCENE_MODEL_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
export MIDSCENE_MODEL_API_KEY="your-api-key"
export MIDSCENE_MODEL_NAME="doubao-seed-2-1-turbo-260628"
export MIDSCENE_MODEL_FAMILY="doubao-seed"
```

将 `your-api-key` 替换为你的 API Key。

> 如需使用千问（Qwen）、GLM、Gemini 或 GPT-5 等其他模型，请参考[使用模型 API Key](/zh/model-api-setup.md)。

全部配置项请参考[模型配置](/zh/model-config.md)。

## 实现你自己的界面类

### 关键概念

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

### 步骤 1. 从 demo 项目开始

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

```bash
# 准备项目
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`](https://github.com/web-infra-dev/midscene-example/blob/main/custom-interface/src/sample-device.ts) 文件中获取示例实现。让我们快速浏览一下。

```typescript
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。

```ts
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](https://github.com/anthropics/skills) 让 AI 编程助手（如 Claude Code、Cline 等）可以通过 CLI 命令驱动你的自定义界面。了解更多请参考 [Skills 文档](/zh/skills.md)。

在你的 npm 包中添加一个 CLI 入口文件（如 `./src/cli.ts`）：

```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` 字段：

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

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

关于 Skill 的编写规范和更多示例，请参考 [midscene-skills](https://github.com/web-infra-dev/midscene-skills) 仓库。

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

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

在 `package.json` 文件中填写 `name` 和 `version`，然后运行以下命令：

```bash
npm publish
```

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

```typescript
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 脚本来调用你的类：

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

config:
  output: './data.json'
```

该配置等价于：

```typescript
import MyDeviceClass from 'my-pkg-name';
const device = new MyDeviceClass();
const agent = new Agent(device, {
  output: './data.json',
  customActions: [], // optional additional custom DeviceAction list
});
```

YAML 的其他字段与[自动化脚本](/zh/automate-with-scripts-in-yaml.md)文档一致。

## API 参考

### `AbstractInterface` 类

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

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

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

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

类型签名：

```ts
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 为最常见的界面和设备提供了一组预定义的动作，同时也支持定义任意自定义动作。

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

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

#### 预定义的动作

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

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

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

#### 定义一个自定义动作

你可以使用 `defineAction()` 函数定义自己的动作。你也可以使用这种方式为 [PuppeteerAgent](/zh/integrate-with-puppeteer.md)、[AgentOverChromeBridge](/zh/sdk/chrome-bridge.md#constructor) 和 [AndroidAgent](/zh/sdk/android.md) 定义更多动作。

API 签名：

```typescript
import type { ExecutorContext } from "@midscene/core";
import { defineAction } from "@midscene/core/device";

defineAction(
  {
    name: string,
    description: string,
    paramSchema: z.ZodType<T>;
    planningFeedbackMaxLength?: number | 'unlimited';
    call: (
      param: z.infer<z.ZodType<T>>,
      context?: ExecutorContext,
    ) => Promise<void>;
  }
)
```

- `name`：动作的名称，AI 模型将使用此名称调用动作
- `description`：动作的描述，AI 模型将使用此描述来理解动作的作用。对于复杂动作，你可以在这里给出更详细的示例说明
- `paramSchema`：动作参数的 [Zod](https://www.npmjs.com/package/zod) 模式，AI 模型将根据此模式帮助填充参数
- `planningFeedbackMaxLength`：可选的 `context.task.planningFeedback` 字符上限。不填时使用 core 默认值 500；填数字时使用该 action 的上限；设为 `'unlimited'` 时，该 action 的反馈不做截断。
- `call`：调用动作的函数，你可以从符合 `paramSchema` 的 `param` 参数中获取参数
- `context`：可选的执行上下文。动作的返回值会写入 `task.output`（供你的 API 调用方和报告使用），**不会**发给规划器。若想给下一轮规划传递反馈，请设置 `context.task.planningFeedback`；是否截断及字符上限由 `planningFeedbackMaxLength` 控制。

示例：

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

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

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

```typescript
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` 函数

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

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

#### 函数签名

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

#### 参数

- `agent: Agent`：要为其启动 Playground 的 Agent 实例

#### 返回值

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

#### `launch` 方法选项

```typescript
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` 方法返回值

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

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

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

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

#### 使用示例

```typescript
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](https://github.com/web-infra-dev/midscene/issues)。
