请添加图片描述

从API 26开始,HarmonyOS给开发者带来了一个特别香的能力——Skill。简单说,它让你App里的业务功能可以被系统AI智能体直接调度,用户说一句话,AI就能帮你调起对应App的功能,而你几乎不用改原来的代码。

一、Skill是个啥?为啥要搞它?

想象一下这个场景:用户对小艺说"帮我查一下明天北京天气",然后你的天气App就被自动调起来返回了结果——用户甚至不需要手动打开你的App。

这就是Skill干的事。它本质是一个声明式的能力外化机制

  • 你写一份SKILL.md,告诉系统"我这个Skill能干啥、怎么调、返回啥"
  • 你写一个ArkTS入口脚本,当个"薄适配层",把AI传进来的参数转交给App内部已有的业务代码
  • module.json5里注册一下,绑定到某个Ability上

就这样,你的App能力就对外开放了,而且原来业务代码一行都不用改

注意:只支持Stage模型,FA模型用不了。

二、整体架构长啥样?

先看目录结构,以一个"天气查询Skill"为例:

Application/
├── AppScope/
│   ├── app.json5
│   └── resources/
└── entry/
    ├── skills/                          ← 【固定值】Skill根目录
    │   └── weather-query/               ← Skill名,跟SKILL.md的name一致
    │       ├── scripts/                 ← 【固定值】脚本目录
    │       │   └── WeatherSkill.ets     ← 入口脚本
    │       └── SKILL.md                 ← 【固定值】描述文件
    └── src/
        └── main/
            ├── ets/
            │   ├── entryability/
            │   │   └── EntryAbility.ets
            │   └── service/
            │       └── WeatherService.ets  ← App内已有的业务服务
            ├── module.json5
            └── resources/

几个关键点:

  • skills/ 目录名是固定的,必须放模块根目录下
  • scripts/ 也是固定
  • Skill目录名、SKILL.md里的name、module.json5里的name,三者必须完全一致

三、一步步来,手把手搞定

Step 1:配置module.json5

entry/src/main/module.json5module 标签下加上 skillProfiles

{
  "module": {
    // ... 其他配置

    "skillProfiles": [
      {
        "name": "weather-query",           // 跟SKILL.md的name、目录名保持一致
        "abilityName": "EntryAbility",      // 关联的Ability
        "srcEntries": [                     // 脚本路径列表
          "../../skills/weather-query/scripts/WeatherSkill.ets"
        ]
      }
    ],

    "requestPermissions": [                 // Skill需要的权限
      { "name": "ohos.permission.INTERNET" },
      { "name": "ohos.permission.LOCATION" }
    ]
  }
}

这里的 srcEntries 路径是相对于 src/main/ 的,所以要用 ../../ 回到模块根目录再进 skills/

Step 2:实现ArkTS入口脚本

入口脚本就是那个"薄适配层",它只干三件事:接参数 → 调业务 → 报结果

2.1 导入依赖
import { scriptManager } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { WeatherService, WeatherInfo, ForecastResult } from '../../../src/main/ets/service/WeatherService';
2.2 定义入口类

export default 导出一个类,类里每个 public async 方法对应SKILL.md里声明的一项能力:

  • 方法名必须和SKILL.md的 functionName 严格一致
  • 第一个参数类型固定scriptManager.ArkTSScriptInfo
export default class WeatherSkill {
  public async queryWeather(info: scriptManager.ArkTSScriptInfo, ...argv: string[]): Promise<void> {
    // 具体实现见下文
  }

  public async queryForecast(info: scriptManager.ArkTSScriptInfo, ...argv: string[]): Promise<void> {
    // 同理
  }
}
2.3 解析和校验参数

AI智能体传进来的参数都在 argv 里,按位置排列,咱们得自己做校验:

// queryWeather:需要城市名,日期可选
const city: string = argv.length > 0 ? argv[0].trim() : '';
const date: string = argv.length > 1 ? argv[1].trim() : '';
if (city.length === 0) {
  // 城市都没传,直接走错误分支
  const payload: Record<string, Object> = {
    'type': 'result',
    'status': 'failed',
    'errCode': 'ERR_INVALID_PARAMS',
    'errMsg': 'city is required',
    'suggestion': '你想查哪个城市的天气呢?'
  };
  await this.report(info, { code: -1, result: payload });
  return;
}
// queryForecast:城市必传,天数可选(默认7天)
const city: string = argv.length > 0 ? argv[0].trim() : '';
if (city.length === 0) {
  const payload: Record<string, Object> = {
    'type': 'result',
    'status': 'failed',
    'errCode': 'ERR_INVALID_PARAMS',
    'errMsg': 'city is required',
    'suggestion': '请告诉我你想查哪个城市的预报'
  };
  await this.report(info, { code: -1, result: payload });
  return;
}
const days: number = argv.length > 1 ? parseInt(argv[1], 10) : 7;
if (days < 1 || days > 15) {
  const payload: Record<string, Object> = {
    'type': 'result',
    'status': 'failed',
    'errCode': 'ERR_INVALID_PARAMS',
    'errMsg': 'days must be between 1 and 15',
    'suggestion': '目前只支持查询1到15天的预报哦'
  };
  await this.report(info, { code: -1, result: payload });
  return;
}
2.4 调用业务实现 + 构造结果回传

校验通过后,调App内部已有的业务接口,然后把结果按SKILL.md声明的契约封装成 ExecuteResult,通过 completeArkTSScriptInApp 回传:

public async queryWeather(info: scriptManager.ArkTSScriptInfo, ...argv: string[]): Promise<void> {
  const city: string = argv.length > 0 ? argv[0].trim() : '';
  const date: string = argv.length > 1 ? argv[1].trim() : '';

  if (city.length === 0) {
    const payload: Record<string, Object> = {
      'type': 'result',
      'status': 'failed',
      'errCode': 'ERR_INVALID_PARAMS',
      'errMsg': 'city is required',
      'suggestion': '你想查哪个城市的天气呢?'
    };
    await this.report(info, { code: -1, result: payload });
    return;
  }

  try {
    const weather: WeatherInfo = WeatherService.getCurrentWeather(city, date);

    const data: Record<string, Object> = {
      'city': weather.city,
      'temperature': weather.temperature,
      'condition': weather.condition,
      'humidity': weather.humidity,
      'wind': weather.wind
    };
    const payload: Record<string, Object> = {
      'type': 'result',
      'status': 'success',
      'data': data
    };
    await this.report(info, { code: 0, result: payload });
  } catch (e) {
    const err = e as BusinessError;
    if (err.code === 404) {
      const payload: Record<string, Object> = {
        'type': 'result',
        'status': 'failed',
        'errCode': 'ERR_NOT_FOUND',
        'data': { 'searchedCity': city },
        'suggestion': `暂时没有找到${city}的天气数据`
      };
      await this.report(info, { code: -1, result: payload });
    } else {
      const payload: Record<string, Object> = {
        'type': 'result',
        'status': 'failed',
        'errCode': 'ERR_INTERNAL',
        'errMsg': err.message,
        'suggestion': '查询天气出了点问题,稍后再试试吧'
      };
      await this.report(info, { code: -1, result: payload });
    }
  }
}
2.5 report方法——唯一的回包出口

建议把 completeArkTSScriptInApp 的调用统一封装到一个私有方法里,别在每个业务分支里重复写:

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}`);
  }
}

这里用到两个关键接口成员:

  • info.context:绑定的Ability上下文,系统传进来的
  • info.requestCode:当前请求的标识码,回包时必须原样传回

Step 3:编写SKILL.md——整个机制的灵魂

SKILL.md是系统智能体做"意图→能力"匹配的唯一依据。写得好不好,直接决定你的Skill会不会被正确触发。

3.1 元数据(YAML Front Matter)
---
name: weather-query
description: 提供城市天气查询与未来天气预报能力,响应"北京天气"、"明天上海热不热"、"未来一周深圳天气预报"等天气类指令
---

name 必须三处一致(目录名、SKILL.md的name、module.json5的name),description 要简洁,这是AI做初次筛选的关键依据。

3.2 触发场景

用自然语言写,帮AI搞清楚"什么时候该调我、什么时候不该调我":

## 触发场景

当用户询问**某个城市的天气或预报**时调用。典型话术:

- "北京今天天气怎么样"
- "明天上海热不热"
- "深圳未来一周天气预报"
- "广州下雨了吗"

不调用的情况:

- 用户说"帮我设个明天7点的闹钟"——这是闹钟功能,跟天气无关
- 用户说"今天适合跑步吗"——这是运动建议,除非明确提到天气查询
- 用户说"这张天空照片真好看"——这是社交评价,不是查天气
- 用户说"帮我关空调"——这是智能家居控制,不是天气查询

划重点:边界说明特别重要!不写清楚的话,AI很容易误触发。比如"今天适合出门吗"这种话,如果你的Skill只查天气不做出行建议,就要明确排除。

3.3 能力1:queryWeather的参数契约
### 场景1:查询天气(queryWeather)

#### 执行参数

exec-cli(command: ohos-arkTSScript --skillName 'weather-query' --scriptPath 'scripts/WeatherSkill.ets' --functionName 'queryWeather' --args '{
    "arg1": "北京",
    "arg2": "明天"
}'
)

参数Schema:

```json
{
  "args": {
    "type": "object",
    "properties": {
      "arg1": {
        "type": "string",
        "description": "城市名,如北京、上海、深圳"
      },
      "arg2": {
        "type": "string",
        "description": "日期,如今天、明天、后天,可选"
      }
    },
    "required": ["arg1"]
  }
}
几个要点:
- `command` 固定写 `ohos-arkTSScript`
- `skillName` 跟SKILL.md的name一致
- `scriptPath` 是相对于Skill目录的脚本路径
- `functionName` 必须跟入口脚本的public方法名**严格对应**
- `args` 的Schema决定了AI能传什么参数进来,`required` 标记必填项

#### 3.4 能力1:queryWeather的返回值契约

先把所有可能的返回结果列出来:

```markdown
#### 执行返回值

结果示例:

// 1. 查询成功
{
    "type": "result",
    "status": "success",
    "data": {
        "city": "北京",
        "temperature": "28℃",
        "condition": "晴",
        "humidity": "35%",
        "wind": "北风3级"
    }
}

// 2. 参数缺失
{
    "type": "result",
    "status": "failed",
    "errCode": "ERR_INVALID_PARAMS",
    "errMsg": "city is required",
    "suggestion": "你想查哪个城市的天气呢?"
}

// 3. 城市未找到
{
    "type": "result",
    "status": "failed",
    "errCode": "ERR_NOT_FOUND",
    "data": { "searchedCity": "阿凡达" },
    "suggestion": "暂时没有找到阿凡达的天气数据"
}

// 4. 内部错误
{
    "type": "result",
    "status": "failed",
    "errCode": "ERR_INTERNAL",
    "errMsg": "network timeout",
    "suggestion": "查询天气出了点问题,稍后再试试吧"
}

然后给出整体的JSON Schema约束:

{
  "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": [
    { "required": ["data"], "properties": { "status": { "const": "success" } } },
    { "required": ["errMsg", "suggestion"], "properties": { "errCode": { "const": "ERR_INVALID_PARAMS" } } },
    { "required": ["data", "suggestion"], "properties": { "errCode": { "const": "ERR_NOT_FOUND" } } },
    { "required": ["errMsg", "suggestion"], "properties": { "errCode": { "const": "ERR_INTERNAL" } } }
  ]
}
3.5 能力2:queryForecast的参数契约
### 场景2:查询天气预报(queryForecast)

#### 执行参数

exec-cli(command: ohos-arkTSScript --skillName 'weather-query' --scriptPath 'scripts/WeatherSkill.ets' --functionName 'queryForecast' --args '{
    "arg1": "深圳",
    "arg2": "7"
}'
)

参数Schema:

```json
{
  "args": {
    "type": "object",
    "properties": {
      "arg1": {
        "type": "string",
        "description": "城市名,如深圳、杭州"
      },
      "arg2": {
        "type": "string",
        "description": "预报天数,1-15,默认7"
      }
    },
    "required": ["arg1"]
  }
}
执行返回值

// 1. 查询成功
{
“type”: “result”,
“status”: “success”,
“data”: {
“city”: “深圳”,
“forecastDays”: 7,
“forecast”: [
{ “date”: “6月18日”, “high”: “33℃”, “low”: “26℃”, “condition”: “多云” },
{ “date”: “6月19日”, “high”: “32℃”, “low”: “25℃”, “condition”: “阵雨” }
]
}
}

// 2. 参数非法(天数超范围)
{
“type”: “result”,
“status”: “failed”,
“errCode”: “ERR_INVALID_PARAMS”,
“errMsg”: “days must be between 1 and 15”,
“suggestion”: “目前只支持查询1到15天的预报哦”
}

## 四、核心接口速查

整个Skill机制涉及的核心接口其实很少,就仨:

| 接口 | 说明 |
|------|------|
| `ExecuteResult` | 脚本执行结果,包含 `code`(结果码)、`result`(结果内容)、`uris`(授权URI列表)、`flags`(URI读写权限) |
| `ArkTSScriptInfo` | 入口函数的首参,包含 `requestCode`(请求标识)和 `context`(Ability上下文) |
| `completeArkTSScriptInApp(context, requestCode, result)` | 上报执行结果,Promise异步回调 |

`ExecuteResult` 的完整结构:

```typescript
interface ExecuteResult {
  code: number;                      // 结果码,0为成功
  result?: Record<string, Object>;   // 脚本执行结果
  uris?: Array<string>;              // 需授权给调用方的URI列表
  flags?: number;                    // URI读写权限
}

五、开发避坑指南

总结几个容易踩的坑:

  1. 名字一致性:Skill目录名、SKILL.md的name字段、module.json5的skillProfiles里的name,三个地方必须完全一样,少一个下划线都不行,否则注册不上。

  2. 方法名严格匹配:入口脚本里的public方法名必须跟SKILL.md里的functionName一模一样,大小写都别搞错。

  3. 第一个参数类型固定:入口方法的第一个参数必须是 scriptManager.ArkTSScriptInfo,这是系统注入的上下文,不能换、不能省。

  4. argv是string数组:AI传进来的参数全是string,需要自己做类型转换(比如 parseInt),同时做好校验和容错。

  5. 必须调用completeArkTSScriptInApp:不管成功还是失败,都必须调用这个接口回传结果,否则系统侧会一直等着,超时后认为执行失败。

  6. suggestion字段很重要:失败时一定要填 suggestion,这是AI转述给用户的提示语,写得好用户体验就好,写得烂用户就一脸懵。

  7. 触发场景要写清楚边界:SKILL.md里"不调用的情况"一定要认真写,否则你的Skill会被AI在各种奇怪的场景下误触发。

  8. srcEntries路径:是相对于 src/main/ 的相对路径,所以要从 ../../skills/ 开始写,别直接写 skills/

六、整体调用流程

用一张流程图串一下整个调用链路:

用户语音/文字输入
       ↓
  系统智能体解析意图
       ↓
  匹配SKILL.md的触发场景
       ↓
  按exec-cli构造调用参数(遵循args Schema)
       ↓
  调用入口脚本对应方法(argv传入)
       ↓
  入口脚本解析参数 → 校验 → 调用App业务接口
       ↓
  按返回值契约构造ExecuteResult
       ↓
  调用completeArkTSScriptInApp回传结果
       ↓
  系统智能体按result内容生成自然语言回复用户

请添加图片描述

七、写在最后

Skill这个机制的设计思路其实挺优雅的——声明式契约 + 薄适配层,把"能力描述"和"能力实现"彻底解耦。对AI来说,它只需要读懂SKILL.md就能调度你的能力;对你来说,只需要写个入口脚本做参数转换,原有业务代码完全不用动。

HarmonyOS 7.0这波是在认真做AI生态基础设施,Skill本质上就是App和AI之间的"USB接口"——标准化、即插即用。如果你的App有对外暴露能力的诉求(而且谁没有呢?),建议尽早熟悉这套机制,先人一步把Skill接入做好,等AI生态起来的时候你就是最早吃到红利的那批。


本文基于HarmonyOS API 26(7.0)编写,Skill相关接口起始版本为26.0.0,仅支持Stage模型。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐