与任意界面集成(预览特性)
从 Midscene v0.28.0 开始,我们推出了与任意界面集成的功能。定义符合 AbstractInterface 定义的界面控制器类,即可获得一个功能齐全的 Midscene Agent。
该功能的典型用途是构建一个针对你自己界面的 GUI 自动化 Agent,比如 IoT 设备、内部应用、车载显示器等。
在实现了 UI 操作的类之后,你可以获得以下特性:
- TypeScript 的 GUI 自动化 Agent SDK
- 用于调试的 Playground
- 通过 yaml 脚本控制界面
- Midscene Agent 的全部特性
- MCP 服务器 (仍在开发中...)
请注意:只有具备视觉定位(visual grounding)能力的模型才能用于操作 UI 界面。请阅读文档以选择合适的模型。
预览功能说明
此功能仍在预览阶段,欢迎你在 GitHub 上给我们提建议。
演示和社区项目
我们已经为你准备了一个演示项目,帮助你学习如何定义自己的界面类。强烈建议你查看一下。
还有一些使用此功能的社区项目:
配置 AI 模型服务
将你的模型配置写入环境变量。更多信息请查看 选择 AI 模型。
# 替换为你的 API Key
export OPENAI_API_KEY="sk-abcdefghijklmnopqrstuvwxyz"
# 可能需要更多配置,如模型名称、接入点等,请参考 《选择 AI 模型》文档
export OPENAI_BASE_URL="..."
实现你自己的界面类
关键概念
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 文件中填写 name 和 version,然后运行以下命令:
你的 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.aiAction('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,它是一个具有 width、height 和 dpr 属性的对象
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() 函数定义自己的动作。你也可以使用这种方式为 PuppeteerAgent、AgentOverChromeBridge 和 AndroidAgent 定义更多动作。
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:调用动作的函数,你可以从符合 paramSchema 的 param 参数中获取参数
示例:
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)
我可以使用普通的 LLM 模型(如 GPT-4o)来控制界面吗?
不可以,你不能使用普通的 LLM 模型(如 GPT-4o)来控制界面。你必须使用具备视觉定位能力的模型。具备视觉定位能力的模型可以在页面上定位目标元素并返回元素的坐标,这能显著提升自动化的稳定性。
请阅读文档以选择合适的模型。
我的 interface-controller 可以在本文档中被推荐吗?
可以,我们很乐意收集有创意的项目并将它们列在本文档中。
当项目准备好后,给我们提一个 issue。