HarmonyOS 自动化支持

Midscene 可以驱动 HDC(HarmonyOS Device Connector)工具来实现 HarmonyOS NEXT 设备自动化。

由于适配了视觉模型方案,整个自动化过程可以适配任意的 HarmonyOS App 技术栈,无论是 ArkTS 原生还是其他框架构建的 App 都能使用。开发者只需面向最终效果调试 UI 自动化脚本即可。

HarmonyOS UI 自动化解决方案具备 Midscene 的全部特性:

  • 支持使用 Playground 进行零代码试用。
  • 支持 JavaScript SDK。
  • 支持使用 YAML 格式的自动化脚本和命令行工具。
  • 支持生成 HTML 报告来回放所有操作路径。

案例展示

Prompt :打开设置,滚动查找「关于手机」,查看设备信息。

查看此次任务的完整报告:report.html

查看更多案例:showcases

本指南将带你完成使用 Midscene 自动化 HarmonyOS 设备所需的一切:通过 HDC 连接真机、配置模型 API Key、体验零代码 Playground,并运行你的首个 JavaScript 脚本。

配置 AI 模型服务

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

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

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

准备工作

在编写脚本前,先确认 HDC 能够连接设备且设备信任当前电脑。

安装 HDC

HDC(HarmonyOS Device Connector)是 HarmonyOS 提供的命令行工具,用于与 HarmonyOS 设备通信。安装方式:

验证 HDC 是否安装成功:

hdc version

出现版本号表示安装成功。

配置 HDC 路径

如果 hdc 不在系统 PATH 中,你可以设置 HDC_HOME 环境变量指向 HDC 所在目录:

export HDC_HOME=/path/to/hdc/directory

启用开发者模式并验证设备

在 HarmonyOS 设备的设置中进入 开发者选项,开启 USB 调试,然后用数据线连接设备。

验证连接:

hdc list targets

出现设备 ID 代表连接成功:

0123456789ABCDEF

试用 Playground(零代码)

Playground 是验证连接、观察 AI Agent 的最快方式,无需编写代码。它与 @midscene/harmony 共享相同的代码实现,因此在 Playground 上验证通过的流程,用脚本运行时也完全一致。

  1. 启动 Playground CLI:
npx --yes @midscene/harmony-playground
  1. 点击 Playground 窗口中的齿轮按钮,粘贴你的 API Key 配置。如果还没有 API Key,请回到 模型配置 获取。

开始体验

配置完成后,你可以立即开始体验 Midscene。它提供了多个关键操作 Tab:

  • Act: 与网页进行交互,这就是自动规划(Auto Planning),对应于 aiAct 方法。比如
在搜索框中输入 Midscene,执行搜索,跳转到第一条结果
填写完整的注册表单,注意主要让所有字段通过校验
  • Query: 从界面中提取 JSON 结构的数据,对应于 aiQuery 方法。

类似的方法还有 aiBoolean(), aiNumber(), aiString(),用于直接提取布尔值、数字和字符串。

提取页面中的用户 ID,返回 { id: string } 结构的 JSON 数据
  • Assert: 理解页面,进行断言,如果不满足则抛出错误,对应于 aiAssert 方法。
页面上存在一个登录按钮,它的下方有一个用户协议的链接
  • Tap: 在某个元素上点击,这就是即时操作(Instant Action),对应于 aiTap 方法。
点击登录按钮

关于自动规划(Auto Planning)和即时操作(Instant Action)的区别,请参考 API 文档。

集成 Midscene Agent

当 Playground 运行正常后,就可以切换到可复用的 JavaScript 脚本。

第 1 步:安装依赖

npm
yarn
pnpm
bun
deno
npm install @midscene/harmony dotenv --save-dev

第 2 步:编写脚本

下面的示例会在设备上打开设置应用,并执行滚动操作。

./demo.ts
import 'dotenv/config'; // 通过 dotenv/config 自动加载 .env 文件中的环境变量
import {
  HarmonyAgent,
  HarmonyDevice,
  getConnectedDevices,
} from '@midscene/harmony';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
Promise.resolve(
  (async () => {
    const devices = await getConnectedDevices();
    const device = new HarmonyDevice(devices[0].deviceId, {});

    const agent = new HarmonyAgent(device, {
      aiActionContext:
        '这是一台鸿蒙设备,系统语言为中文。如果出现弹窗,点击同意或关闭。',
    });
    await device.connect();

    // 打开设置应用
    await agent.launch('com.huawei.hmos.settings');
    await sleep(2000);

    // 向下滚动列表
    await agent.aiAct('scroll down one screen');

    // 查询页面内容
    const items = await agent.aiQuery(
      'string[], 列表中可见的所有设置项名称',
    );
    console.log('设置项列表', items);

    // 断言
    await agent.aiAssert('页面中有设置项列表');
  })(),
);

第 3 步:运行示例

npx tsx demo.ts

第 4 步:查看报告

脚本成功后会输出 Midscene - report file updated: /path/to/report/some_id.html。在浏览器中打开该 HTML 文件即可回放每一步交互、查询与断言。

进阶

当你需要自定义设备行为、把 Midscene 接入独立框架,或排查 HDC 问题时,可参考本节内容。更多构造函数参数可前往 API 参考(HarmonyOS)

扩展 HarmonyOS 上的 Midscene

使用 defineAction() 定义自定义手势,并通过 customActions 传入。Midscene 会把自定义动作追加到规划器中,让 AI 可以调用你领域特定的动作名。

import { getMidsceneLocationSchema, z } from '@midscene/core';
import { defineAction } from '@midscene/core/device';
import { HarmonyAgent, HarmonyDevice, getConnectedDevices } from '@midscene/harmony';

const ContinuousClick = defineAction({
  name: 'continuousClick',
  description: 'Click the same target repeatedly',
  paramSchema: z.object({
    locate: getMidsceneLocationSchema(),
    count: z.number().int().positive().describe('How many times to click'),
  }),
  async call(param) {
    const { locate, count } = param;
    console.log('click target center', locate.center);
    console.log('click count', count);
  },
});

const devices = await getConnectedDevices();
const device = new HarmonyDevice(devices[0].deviceId, {});
await device.connect();

const agent = new HarmonyAgent(device, {
  customActions: [ContinuousClick],
});

await agent.aiAct('click the red button five times');

关于自定义动作和动作 Schema 的更多解释,请参阅 与任意界面集成

更多

完整示例(Vitest + HarmonyAgent)

import type { TestStatus } from '@midscene/core';
import { ReportMergingTool } from '@midscene/core/report';
import { sleep } from '@midscene/core/utils';
import {
  HarmonyAgent,
  HarmonyDevice,
  getConnectedDevices,
} from '@midscene/harmony';
import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  it,
} from 'vitest';

describe('HarmonyOS Settings Test', () => {
  let device: HarmonyDevice;
  let agent: HarmonyAgent;
  let itTestStatus: TestStatus = 'passed';
  const reportMergingTool = new ReportMergingTool();

  beforeAll(async () => {
    const devices = await getConnectedDevices();
    device = new HarmonyDevice(devices[0].deviceId);
    await device.connect();
  });

  beforeEach((ctx) => {
    agent = new HarmonyAgent(device, {
      groupName: ctx.task.name,
    });
  });

  afterEach((ctx) => {
    if (ctx.task.result?.state === 'pass') {
      itTestStatus = 'passed';
    } else if (ctx.task.result?.state === 'skip') {
      itTestStatus = 'skipped';
    } else if (ctx.task.result?.errors?.[0].message.includes('timed out')) {
      itTestStatus = 'timedOut';
    } else {
      itTestStatus = 'failed';
    }
    reportMergingTool.append({
      reportFilePath: agent.reportFile as string,
      reportAttributes: {
        testId: `${ctx.task.name}`,
        testTitle: `${ctx.task.name}`,
        testDescription: 'description',
        testDuration: (Date.now() - ctx.task.result?.startTime!) | 0,
        testStatus: itTestStatus,
      },
    });
  });

  afterAll(() => {
    reportMergingTool.mergeReports('my-harmony-setting-test-report');
  });

  it('toggle WLAN', async () => {
    await device.home();
    await sleep(1000);
    await device.launch('com.huawei.hmos.settings');
    await sleep(1000);
    await agent.aiAct('找到并进入 WLAN 设置');
    await agent.aiAct(
      '切换 WLAN 状态一次,如果 WLAN 关闭则打开,否则关闭。',
    );
  });

  it('toggle Bluetooth', async () => {
    await device.home();
    await sleep(1000);
    await device.launch('com.huawei.hmos.settings');
    await sleep(1000);
    await agent.aiAct('找到并进入蓝牙设置');
    await agent.aiAct(
      '切换蓝牙状态一次,如果蓝牙关闭则打开,否则关闭。',
    );
  });
});
Tip

合并报告默认存放在 midscene_run/report 目录。在 CI 中运行时可通过 MIDSCENE_RUN_DIR 覆盖。

常见问题

输入后键盘没有隐藏,或页面发生返回

Midscene 在输入文本后会自动隐藏键盘。HarmonyOS 默认发送 ESC,以减少触发页面返回的概率。如果你的应用里 ESC 无法关闭键盘,可以切换为优先使用 Back:

const device = new HarmonyDevice('device-id', {
  keyboardDismissStrategy: 'back-first',
});

如果你的输入框监听了 Back 并执行清空或关闭操作,可以关闭自动隐藏键盘:

const device = new HarmonyDevice('device-id', {
  autoDismissKeyboard: false,
});

关闭后键盘不会自动隐藏,你可以使用 aiAct 指令手动隐藏键盘,例如 await agent.aiAct('隐藏键盘')

如何使用自定义的 HDC 路径?

通过 HDC_HOME 环境变量指定 HDC 所在目录:

export HDC_HOME=/path/to/hdc/directory

也可以通过构造函数传入:

const device = new HarmonyDevice('0123456789ABCDEF', {
  hdcPath: '/path/to/hdc',
});

API 参考

当你需要自定义设备行为、把 Midscene 接入框架,或排查 HDC 问题时,请查阅本节。关于通用构造函数(报告、Hook、缓存等)的参数说明,请参考平台无关的 API 参考

Action Space(动作空间)

HarmonyDevice 使用以下动作空间,Midscene Agent 在规划任务时可以使用这些操作:

  • Tap —— 点击元素。
  • DoubleClick —— 双击元素。
  • Input —— 输入文本,支持 replace/typeOnly/clear 模式。
  • Scroll —— 以元素为起点或从屏幕中央向上/下/左/右滚动,支持滚动到顶/底/左/右。
  • DragAndDrop —— 从一个元素拖拽到另一个元素。
  • KeyboardPress —— 按下指定键位。
  • LongPress —— 长按目标元素,可选自定义时长。
  • ClearInput —— 清空输入框内容。
  • Pinch —— 不支持。HarmonyOS uitest 框架未提供多触点输入 API。
  • Launch —— 打开 HarmonyOS 应用(bundle name)。
  • Terminate —— 按 bundle name 强制停止应用。
  • RunHdcShell —— 执行原始 hdc shell 命令。
  • HarmonyBackButton —— 触发系统返回。
  • HarmonyHomeButton —— 回到桌面。
  • HarmonyRecentAppsButton —— 打开多任务/最近应用。

HarmonyDevice

创建一个可供 HarmonyAgent 驱动的 HDC 设备实例。

导入

import { HarmonyDevice, getConnectedDevices } from '@midscene/harmony';

构造函数

const device = new HarmonyDevice(deviceId, {
  // 设备参数...
});

设备选项

  • deviceId: string —— 来自 hdc list targetsgetConnectedDevices() 的值。
  • hdcPath?: string —— HDC 可执行文件的自定义路径。若未设置,将依次从 HDC_HOME 环境变量和常见安装路径中查找。
  • autoDismissKeyboard?: boolean —— 输入完成后自动隐藏键盘,默认 true
  • keyboardDismissStrategy?: 'esc-first' | 'back-first' —— 自动隐藏键盘时优先使用的按键,默认 'esc-first'。HarmonyOS 只发送该策略的首选按键:'esc-first' 发送 ESC,'back-first' 发送 Back。
  • screenshotResizeScale?: number —— 已废弃。 此选项已移除,不再生效。如需控制发送给 AI 模型的截图尺寸,请使用 AgentOpt 中的 screenshotShrinkFactor
  • customActions?: DeviceAction[] —— 通过 defineAction 扩展规划器的可用动作。

使用说明

  • 可以使用 getConnectedDevices() 发现设备,返回的 deviceIdhdc list targets 输出一致。
  • 如果 HDC 不在系统 PATH 中,可通过 HDC_HOME 环境变量或 hdcPath 选项指定路径。

示例

快速开始
import { HarmonyAgent, HarmonyDevice, getConnectedDevices } from '@midscene/harmony';

const [first] = await getConnectedDevices();
const device = new HarmonyDevice(first.deviceId, {});
await device.connect();

const agent = new HarmonyAgent(device, {
  aiActionContext: '这是一台鸿蒙设备,如果出现弹窗,点击同意。',
});

await agent.launch('com.huawei.hmos.settings');
await agent.aiAct('scroll down one screen');
const items = await agent.aiQuery(
  'string[], list all visible setting item names',
);
console.log(items);
启动应用
await agent.launch('com.huawei.hmos.settings'); // 打开系统设置
await agent.launch('com.huawei.hmos.camera');    // 打开相机
await agent.back();
await agent.home();

HarmonyAgent

将 Midscene 的 AI 规划能力绑定到 HarmonyDevice,实现 UI 自动化。

导入

import { HarmonyAgent } from '@midscene/harmony';

构造函数

const agent = new HarmonyAgent(device, {
  // 通用 Agent 参数...
});

HarmonyOS 特有选项

  • customActions?: DeviceAction[] —— 通过 defineAction 扩展规划器的可用动作。
  • appNameMapping?: Record<string, string> —— 将友好的应用名称映射到 bundle name。当你在 launch(target) 里传入应用名称时,Agent 会在此映射中查找对应的 bundle name;若未找到映射,则按原样尝试启动 target
  • 其余字段与 API constructors 一致:generateReportreportFileNameaiActionContextmodelConfigcacheIdcreateOpenAIClientonTaskStartTip 等。

使用说明

Info

HarmonyOS 特有方法

agent.launch()

启动 HarmonyOS 应用。

function launch(uri: string): Promise<void>;
  • uri: string —— 可以是应用 bundle name(如 com.huawei.hmos.settings),也可以是在 appNameMapping 中注册的应用名称。如果传入 http://https:// 开头的 URL,将通过浏览器打开。
await agent.launch('com.huawei.hmos.settings'); // 打开系统设置
await agent.launch('com.huawei.hmos.camera');    // 打开相机
agent.runHdcShell()

通过连接的设备运行原始的 hdc shell 命令。

function runHdcShell(command: string): Promise<string>;
  • command: string —— 原样传递给 hdc shell 的命令。
const result = await agent.runHdcShell('hidumper -s RenderService -a screen');
console.log(result);
agent.terminate()

终止(强制停止)正在运行的 HarmonyOS 应用。

function terminate(uri: string): Promise<void>;
  • uri: string —— 应用 bundle name、appNameMapping 中的应用名称,或 bundle/Ability(仅使用 bundle name 部分)。
await agent.terminate('com.huawei.hmos.settings');
导航辅助
  • agent.back(): Promise<void> —— 触发 HarmonyOS 系统的返回操作。
  • agent.home(): Promise<void> —— 返回桌面。
  • agent.recentApps(): Promise<void> —— 打开多任务/最近应用界面。

辅助工具

agentFromHdcDevice()

从任意已连接的 HDC 设备创建 HarmonyAgent

function agentFromHdcDevice(
  deviceId?: string,
  opts?: HarmonyAgentOpt & HarmonyDeviceOpt,
): Promise<HarmonyAgent>;
  • deviceId?: string —— 连接特定设备;留空表示使用"第一个可用设备"。
  • opts?: HarmonyAgentOpt & HarmonyDeviceOpt —— 在一个对象中合并 Agent 选项与 HarmonyDevice 的设置。
import { agentFromHdcDevice } from '@midscene/harmony';

const agent = await agentFromHdcDevice('0123456789ABCDEF'); // 传入 deviceId
const agent = await agentFromHdcDevice(); // 使用第一个连接的设备
getConnectedDevices()

列举 Midscene 可驱动的 HDC 设备。

function getConnectedDevices(
  hdcPath?: string,
): Promise<Array<{ deviceId: string }>>;
import { getConnectedDevices } from '@midscene/harmony';

const devices = await getConnectedDevices();
console.log(devices); // [{ deviceId: '0123456789ABCDEF' }]