目录

基于ArkTS脚本的应用Skill开发指导

概述

从API版本26.0.0开始,Ability Kit支持将应用内业务能力以Skill形式开放给系统智能体调用。Skill提供一种声明式的能力外化机制:开发者将应用内可被外部调用的业务能力组织为若干能力单元,每个单元由一份描述文件(声明其触发场景、入参约束与返回值契约)与一份ArkTS入口脚本(将外部调用桥接到应用内既有业务实现)共同构成,并通过模块配置绑定到指定Ability的运行上下文。运行时,系统智能体依据描述文件完成“意图—能力”的语义匹配,并将结果转化为面向用户的自然语言回复。

通过Skill,开发者得以在不改造既有业务实现的前提下,以薄封装将应用能力开放给系统智能体;系统智能体无需理解各应用的内部实现,仅依赖统一的声明契约即可完成调度。

仅支持Stage模型,FA模型不可用。

接口说明

以下是基于ArkTS脚本开发应用Skill使用的主要接口,更多接口及使用方式请见@ohos.app.ability.scriptManager。

接口名 描述
ExecuteResult ArkTS脚本执行结果。
ArkTSScriptInfo 应用的ArkTS脚本入口函数的第一个参数,用于接收系统传递的脚本上下文信息。
completeArkTSScriptInApp(context: Context, requestCode: string, result: ExecuteResult): Promise<void> 完成应用的ArkTS脚本执行,上报执行结果。使用Promise异步回调。

开发步骤

下文以“音乐助手Skill(example-org-music-assistant)”为示例,演示如何在自有应用中,通过代码开发和封装,实现按名称播放音乐(playMusicByName)与播放控制(controlPlayback)的能力。

  1. 创建文件和目录。

    在模块(entry)下按以下结构创建文件和目录:

    Application/
    ├── AppScope/
    │   ├── app.json5
    │   └── resources/
    └── entry/
        ├── skills/                            <- 【固定值】当前模块所有Skill的根目录
        │   └── example-org-music-assistant/   <- Skill名,需与SKILL.md的name一致,为防止命名冲突,推荐使用公司或组织名作为前缀
        │       ├── scripts/                   <- 【固定值】ETS脚本目录
        │       │   └── MusicSkill.ets         <- Skill入口脚本
        │       └── SKILL.md                   <- 【固定值】Skill描述文件
        └── src/
            └── main/
                ├── ets/
                │   ├── entryability/
                │   │   └── EntryAbility.ets
                │   └── service/
                │       └── MusicPlayer.ets    <- 应用内业务服务(被Skill入口脚本调用)
                ├── module.json5
                └── resources/
  2. 配置module.json5文件。

    在entry/src/main/module.json5的module标签下新增skillProfiles标签,将Skill注册到模块。

    示例Skill运行时需要添加网络权限访问云端音乐列表,在module标签下的requestPermissions标签配置。

    {
      "module": {
        // ...
        "skillProfiles": [
          {
            "name": "example-org-music-assistant",  // Skill名,需与SKILL.md的name一致
            "abilityName": "EntryAbility",          // 与该Skill关联的组件名称
            "srcEntries": [                         // 实现Skill的代码文件路径列表
              "../../skills/example-org-music-assistant/scripts/MusicSkill.ets"
            ],
            "version": "1.0.0"
          }
        ],
    
        "requestPermissions": [  // Skill运行需要的权限列表
          { "name": "ohos.permission.INTERNET" }
        ],
        // ...
      }
    }
  3. 实现ArkTS脚本。

    ArkTS入口脚本(MusicSkill.ets)是Skill调用链路上的“薄适配层”,负责把系统智能体传入的字符串参数转交给应用内已有业务实现,并把业务执行结果按SKILL.md声明的契约回传。其调用的MusicPlayer属于应用既有业务实现,与Skill机制本身无耦合,本节不再展开其内部逻辑。

    3.1 导入Skill相关接口。

    入口脚本需要从@kit.AbilityKit引入scriptManager,同时引入待桥接的应用内业务模块。

    import { scriptManager } from '@kit.AbilityKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    // 应用既有业务模块
    import { MusicPlayer, Track, PlayResult } from '../../../src/main/ets/service/MusicPlayer';

    3.2 定义入口类骨架。

    入口脚本以export default方式导出一个类,类内每个public async方法对应SKILL.md声明的一项能力,须满足以下约定:

    • 方法名约定:必须与SKILL.md中的functionName严格一致(本例为playMusicByName、controlPlayback)。
    • 方法签名约定:第一个参数类型固定为ArkTSScriptInfo。
    export default class MusicSkill {
      // ...
      public async playMusicByName(info: scriptManager.ArkTSScriptInfo, ...argv: string[]): Promise<void> {
        /* 见 3.3 ~ 3.5 */
        // ...
      }
    
      // ...
      public async controlPlayback(info: scriptManager.ArkTSScriptInfo, ...argv: string[]): Promise<void> {
        /* 同上模式 */
        // ...
      }
    
      // ...
    }

    3.3 解析并校验入参。

    每个能力方法的第一项任务是从argv中按位置获取参数,对照SKILL.md的args Schema完成前置校验。

    // 例1:playMusicByName 的两个可选参数,至少一个非空
    const songName: string = argv.length > 0 ? argv[0].trim() : '';
    const singer: string = argv.length > 1 ? argv[1].trim() : '';
    if (songName.length === 0 && singer.length === 0) {
      // 走 ERR_INVALID_PARAMS 分支回包(见 3.5),不再进入业务
          // ...
      return;
    }
        // ...
    
    // 例2:controlPlayback 的枚举值参数,需用白名单二次校验
    const action: string = argv.length > 0 ? argv[0].trim() : '';
    const validActions: string[] = ['pause', 'resume', 'next', 'previous'];
    if (!validActions.includes(action)) {
      // 走 ERR_INVALID_PARAMS 分支回包
          // ...
      return;
    }

    3.4 调用应用内业务实现。

    校验通过后,调用既有业务接口完成实际任务。入口脚本不承载业务逻辑,仅充当“参数适配器”,读取业务返回值与运行时异常,分别映射到SKILL.md声明的不同结果分支。

    try {
      // 直接调用应用内已有业务API
      const playResult: PlayResult | null = MusicPlayer.searchAndPlay(songName, singer);
      // 业务返回值 → 映射到"成功"或"未命中"分支(见 3.5)
          // ...
    } catch (e) {
      // 业务异常 → 统一映射到 ERR_INTERNAL 分支(见 3.5)
      const err = e as BusinessError;
    // ...
    }

    3.5 按契约构造ExecuteResult并回传。

    业务执行完成后,需将结果封装为ExecuteResult,并通过调用completeArkTSScriptInApp回传给系统智能体,回包内容应与SKILL.md中“执行返回值”声明的分支保持一致。

    // 成功分支示例
    const first: Track = playResult.tracks[0];
    const playingTrack: Record<string, Object> = {
      'name': first.name,
      'singer': first.singer,
      'duration': first.duration
    };
    const data: Record<string, Object> = {
      'playingTrack': playingTrack,
      'matchedCount': playResult.tracks.length
    };
    const payload: Record<string, Object> = {
      'type': 'result',
      'status': 'success',
      'data': data
    };
    await this.report(info, { code: 0, result: payload });
          // ...
    
    // 失败分支示例(以 ERR_NOT_FOUND 为例)
    const payloadData: Record<string, Object> = {
      'searchedKeywords': []
    };
    const payload: Record<string, Object> = {
      'type': 'result',
      'status': 'failed',
      'errCode': 'ERR_NOT_FOUND',
      'data': payloadData,
      'suggestion': `没有找到${singer}${songName.length > 0 ? `的《${songName}》` : '相关歌曲'}`
    };
    await this.report(info, { code: -1, result: payload });
    // ...
    
    // 唯一的回包出口:只封装API调用与异常打印,不参与结果构造
    private async report(info: scriptManager.ArkTSScriptInfo, result: scriptManager.ExecuteResult): Promise<void> {
      try {
        await scriptManager.completeArkTSScriptInApp(info.context, info.requestCode, result);
      } catch (e) {
        const err = e as BusinessError;
        console.error(`completeArkTSScriptInApp failed, code: ${err.code}, message: ${err.message}`);
      }
    }
  4. 编写SKILL.md。

    SKILL.md是Skill的声明契约文件,是系统智能体进行“意图—能力”匹配的唯一依据。主要由“元数据->触发场景->能力契约”三段构成。

    4.1 撰写元数据(YAML Front Matter)。

    在文件头部使用YAML Front Matter声明 name 与 description。其中,name 必须与 Skill 目录名以及 module.json5 中 skillProfiles[].name 完全保持一致;description 应简洁地描述能力范围,作为系统智能体进行 Skill 初次筛选的关键依据。

    ---
    name: example-org-music-assistant
    description: 提供音乐搜索播放与播控能力,响应“放首歌”、“切歌”、“暂停”等播放控制类指令
    ---

    4.2 撰写触发场景。

    用自然语言列出典型话术,并补充不要调用的情况以划清能力边界,降低误触发。典型话术应覆盖能力对应的多种说法,边界说明应覆盖容易混淆的相邻意图。

    ## 触发场景
    
    当用户明确表达**播放音乐**或**控制当前播放**时调用。典型话术:
    
    - “放一首SingerA的《SongA》”
    - “来首《SongA》”
    - “暂停”、“继续播放”
    - “切歌”、“下一首”、“上一曲”
    
    不调用的情况:
    
    - 用户说“把这首歌加入收藏”——意图是修改歌单,本Skill仅支持播放与播控。
    - 用户说“今天有什么演唱会”——意图是查询资讯,非播放。
    - 用户没有明确指向播放(如“这首歌真好听”)——情绪表达,无需调用。
    - 用户说“调小音量”——意图是系统音量控制,应走系统能力。

    4.3 为每项能力撰写“执行参数”契约。

    每项能力对应一个### 场景N:能力名(functionName)子小节,“执行参数”部分需包含exec-cli形式的调用示例和一份JSON Schema约束。

    调用示例包含四个核心字段:command固定为ohos-arkTSScript;skillName需与SKILL.md中的name保持一致;scriptPath为相对于Skill目录的入口脚本路径;functionName必须与MusicSkill.ets中public方法名严格对应。

    exec-cli(command: ohos-arkTSScript --skillName 'example-org-music-assistant' --scriptPath 'scripts/MusicSkill.ets' --functionName 'playMusicByName' --args '{
        "arg1": "SongA",
        "arg2": "SingerA"
    }'
    )

    Schema的核心在于args子对象,它定义了系统智能体可填写的入参结构。playMusicByName支持“歌名”和“歌手”二选一填写,因此使用anyOf约束以确保至少包含其中之一:

    "args": {
      "type": "object",
      "properties": {
        "arg1": {
          "type": "string",
          "description": "歌曲名,如《SongA》"
        },
        "arg2": {
          "type": "string",
          "description": "歌手名,如SingerA"
        }
      },
      "anyOf": [
        { "required": ["arg1"] },
        { "required": ["arg2"] }
      ]
    }

    4.4 为每项能力撰写“执行返回值”契约。

    “执行返回值”部分需先列出所有可能的结果示例(成功 + 各类失败),再给出整体的JSON Schema约束。

    以playMusicByName为例,需逐一列出四组示例:

    // 1. 成功播放
    {
        "type": "result",
        "status": "success",
        "data": {
            "playingTrack": {
                "name": "SongA",
                "singer": "SingerA",
                "duration": 269
            },
            "matchedCount": 1
        }
    }
    // 2. 入参非法
    {
        "type": "result",
        "status": "failed",
        "errCode": "ERR_INVALID_PARAMS",
        "errMsg": "songName and singer are both empty",
        "suggestion": "我没听清,你想听哪首歌?"
    }
    // 3. 未命中
    {
        "type": "result",
        "status": "failed",
        "errCode": "ERR_NOT_FOUND",
        "data": {
            "searchedKeywords": ["SongA", "SingerA"]
        },
        "suggestion": "没有找到SingerA的《SongA》"
    }
    // 4. 内部错误
    {
        "type": "result",
        "status": "failed",
        "errCode": "ERR_INTERNAL",
        "errMsg": "network timeout",
        "suggestion": "播放失败了,稍后再试试"
    }

    对应的Schema顶层定义公共字段,并通过oneOf枚举上述四种分支形态:

    {
      "type": "object",
      "required": ["type", "status"],
      "properties": {
        "type":   { "type": "string", "const": "result" },
        "status": { "type": "string", "enum": ["success", "failed"] },
        "data":   { "type": "object" },
        "errCode": {
          "type": "string",
          "enum": ["ERR_INVALID_PARAMS", "ERR_NOT_FOUND", "ERR_INTERNAL"]
        },
        "errMsg":     { "type": "string", "minLength": 1 },
        "suggestion": { "type": "string", "minLength": 1 }
      },
      "oneOf": [
        /* 成功:               要求 data.playingTrack 与 data.matchedCount */
        /* ERR_INVALID_PARAMS:  要求 errMsg 与 suggestion */
        /* ERR_NOT_FOUND:       要求 data.searchedKeywords 与 suggestion */
        /* ERR_INTERNAL:        要求 errMsg 与 suggestion */
      ]
    }
  5. 在完成Skill开发后,请先参考导入Skill将HAP上传,再参考真机测试进行调试。