【共创稿事节】HarmonyOS 7 应用 Skill 化实战:从“被打开“到“被调用“,把功能递进系统意图分发池
本文基于 HarmonyOS 7(API 26)的《基于ArkTS脚本的应用Skill开发指导》与官方新能力一览整理。文中代码是为说明问题自写的完整示例,不是官方示例的搬运;API 名称、标签与版本号等事实性信息均标注官方出处;涉及真机表现的部分已明确标注,未做任何实测数据编造。

引子:图标是一扇门,但门不会自己开
V哥认识一个做本地餐饮应用的朋友,功能做得很全:扫码点单、排队取餐、会员储值,一个不落。有天V哥问他:"用户找你们’排队取餐’这个功能,要点几步?"他掰指头数:解锁、找图标、进首页、过广告位、点 tabBar、再进二级页——五步。
而用户的表达其实只有四个字:“帮我取餐。”
这事在 HarmonyOS 7(API 26)之前无解,应用只能在图标里等用户来点。7.0 把这个前提改了:应用内业务能力可以以 Skill 的形式开放给系统智能体调用,用户说一句"帮我取餐",系统自己找到你的能力、自己调(官方新能力一览)。V哥的原话概括就是标题那半句——应用从"被打开",变成了"被调用"。
别小看这一个词的差别。被打开,流量入口是图标和应用市场;被调用,流量入口变成了系统级智能分发。这期V哥把 Skill 化的完整路径拆开讲:机制、目录、契约、代码,最后给一份上线前自检清单。
一、先想明白:Skill 到底开放了什么
先看官方的关键表述(基于ArkTS脚本的应用Skill开发指导):
从 API 版本 26.0.0 开始,Ability Kit 支持将应用内业务能力以 Skill 形式开放给系统智能体调用。Skill 提供一种声明式的能力外化机制……运行时,系统智能体依据描述文件完成"意图—能力"的语义匹配,并将结果转化为面向用户的自然语言回复。
V哥从这段话里读出三个重点,比 API 本身重要:
| 重点 | 意思 | 对开发者的直接影响 |
|---|---|---|
| 声明式 | 你不写"怎么被调用",只声明"能做什么、什么时候调、参数什么样" | 主体工作量在写契约,不在写代码 |
| 薄封装 | 不改造既有业务实现,入口脚本只是"参数适配器" | 老业务一行不动,加个壳就能上 |
| 语义匹配 | 系统靠你的描述文件猜用户意图,匹配上了才调你 | 契约写得糙 = 永远匹配不上 |
第三条是很多人会漏的。图标时代,你的应用名称写错顶多搜索排名靠后;Skill 时代,描述文件就是你在意图分发池里的全部简历——系统智能体不进你的代码,只看这份简历决定调不调你。
还有一个硬边界先说在前:这套机制仅支持 Stage 模型,FA 模型不可用(官方原文)。存量 FA 模型工程得先迁移,这不是 Skill 的坑,是前置条件。
二、Skill 的解剖图:一个目录、两份文件、一处注册
官方把一个 Skill 的物理形态规定得很死,V哥画成一张链路图:

落到工程里,就是在模块(entry)下建一个 skills/ 目录,里面每个 Skill 一个文件夹(官方开发指导):
entry/
├── skills/ <- 固定值:本模块所有 Skill 的根目录
│ └── vge-org-milktea-assistant/ <- Skill 名,须与 SKILL.md 的 name 一致
│ ├── scripts/
│ │ └── MilkteaSkill.ets <- 入口脚本(薄适配层)
│ └── SKILL.md <- 描述文件(意图匹配的唯一依据)
└── src/main/
├── ets/service/MilkTeaService.ets <- 应用既有业务(被脚本调用,零改动)
└── module.json5 <- 在这里注册 skillProfiles
三处名字必须完全一致:目录名 = SKILL.md 的 name = module.json5 里 skillProfiles[].name。官方还专门提醒,为防命名冲突,Skill 名推荐用公司或组织名做前缀——V哥示例里的 vge-org- 就是这个用途。
注册写进 module.json5 的 skillProfiles 标签(官方新增标签):
{
"module": {
"skillProfiles": [
{
"name": "vge-org-milktea-assistant", // 与目录名、SKILL.md 的 name 三处一致
"abilityName": "EntryAbility", // Skill 绑定到哪个 Ability 的运行上下文
"srcEntries": [ // 入口脚本路径
"../../skills/vge-org-milktea-assistant/scripts/MilkteaSkill.ets"
],
"version": "1.0.0"
}
],
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" } // Skill 运行需要的权限照常声明
]
}
}
注意 abilityName 这一项:Skill 不是悬浮的,它绑定在指定 Ability 的运行上下文里执行。权限也是老规矩——Skill 要联网就声明联网,该最小化就最小化。
三、动手:把"点奶茶"递给小艺
V哥的示例应用是奶茶点单,既有业务里已经有 MilkTeaService(下单、查单)。现在要开放两个能力给系统智能体:点单(orderMilkTea)和查取餐进度(queryQueue)。
入口脚本:一个只做"翻译"的类
入口脚本以 export default 导出一个类,类里每个 public async 方法对应 SKILL.md 声明的一项能力,方法名必须与契约里的 functionName 严格一致,第一个参数固定是 ArkTSScriptInfo(官方约定)。V哥的写法:
// MilkteaSkill.ets —— 薄适配层:只翻译参数,不装业务
import { scriptManager } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 应用既有业务模块,Skill 化前后零改动
import { MilkTeaService, OrderResult } from '../../../src/main/ets/service/MilkTeaService';
export default class MilkteaSkill {
// 点单:品名必填,糖度、冰量可选(与 SKILL.md 的 args Schema 对应)
public async orderMilkTea(info: scriptManager.ArkTSScriptInfo, ...argv: string[]): Promise<void> {
const drink: string = argv.length > 0 ? argv[0].trim() : '';
const sugar: string = argv.length > 1 ? argv[1].trim() : '五分甜';
const ice: string = argv.length > 2 ? argv[2].trim() : '少冰';
// ① 前置校验:品名为空直接走参数错误分支,不进业务
if (drink.length === 0) {
await this.report(info, {
code: -1,
result: {
type: 'result', status: 'failed',
errCode: 'ERR_INVALID_PARAMS',
errMsg: 'drink name is empty',
suggestion: 'V哥没听清品名,你想喝哪杯?'
}
});
return;
}
// ② 调既有业务:入口脚本不写业务逻辑
try {
const order: OrderResult = MilkTeaService.createOrder(drink, sugar, ice);
// ③ 按契约把业务结果装进回包
await this.report(info, {
code: 0,
result: {
type: 'result', status: 'success',
data: {
orderNo: order.orderNo,
pickupCode: order.pickupCode,
etaMinutes: order.etaMinutes
}
}
});
} catch (e) {
const err = e as BusinessError;
// 业务异常统一映射到内部错误分支
await this.report(info, {
code: -1,
result: {
type: 'result', status: 'failed',
errCode: 'ERR_INTERNAL',
errMsg: err.message,
suggestion: '下单失败了,稍后再试试'
}
});
}
}
// 查询取餐进度(契约结构同理,略)
// ④ 唯一回包出口:只管上报,不参与结果构造
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}`);
}
}
}
四个环节对应官方规定的四步:解析校验入参 → 调既有业务 → 按契约构造 ExecuteResult → 经 completeArkTSScriptInApp 回传。核心接口就三个:ExecuteResult(脚本执行结果)、ArkTSScriptInfo(系统传进来的脚本上下文)、completeArkTSScriptInApp(上报结果),都挂在 @ohos.app.ability.scriptManager 下(接口参考)。
V哥最想强调的是那句注释——唯一的回包出口。官方示例把 completeArkTSScriptInApp 收进一个 report 私有方法,V哥照做了,理由很实际:回包点一散,某个分支忘了报、报错了没人知道,系统智能体只会觉得"这个 Skill 失联了",下次直接不调你。
SKILL.md:系统智能体的"招聘简历"
SKILL.md 分三段:元数据、触发场景、能力契约(官方开发指导 · 第 4 节)。V哥按自己的示例写了一份:
---
name: vge-org-milktea-assistant
description: 提供奶茶点单与取餐进度查询能力,响应"点一杯奶茶"、"帮我点单"、
"还有多久能取餐"等指令
---
## 触发场景
当用户明确表达**点奶茶**或**查询取餐进度**时调用。典型话术:
- "点一杯芝士葡萄,五分甜少冰"
- "帮我点单,要三分甜的珍珠奶茶"
- "我的奶茶做到哪了"、"还要等多久"
不调用的情况:
- 用户说"附近的奶茶店有哪些"——意图是搜索店铺,本 Skill 只管下单和进度。
- 用户说"这杯奶茶多少钱"——意图是查价格,应走商品查询能力。
- 用户说"取消订单"——当前版本不支持,避免误触发。
### 场景1:点单(orderMilkTea)
执行参数:
exec-cli(command: ohos-arkTSScript --skillName 'vge-org-milktea-assistant'
--scriptPath 'scripts/MilkteaSkill.ets' --functionName 'orderMilkTea'
--args '{ "arg1": "芝士葡萄", "arg2": "五分甜", "arg3": "少冰" }')
(JSON Schema:arg1 品名为必填,arg2 糖度、arg3 冰量为可选枚举,此处略)
执行返回值:
(先列成功/参数非法/门店未命中/内部错误四组示例,再用 oneOf 收口,此处略)
三段各有各的命门:
- 元数据:
name三处一致不赘述;description是系统做初次筛选的依据——写"奶茶相关服务"这种糊涂话,筛选这关就过不去。 - 触发场景:官方明确建议除了列典型话术,还要写**“不调用的情况”**来划清能力边界。V哥的理解:不写边界,相邻意图全算你头上,误触发一多,用户和小艺对你的信任一起掉。
- 能力契约:每项能力一个
exec-cli调用示例加 JSON Schema。anyOf(二选一必填)、oneOf(多种回包分支互斥)都是标准 JSON Schema 玩法,跟后端同学对过接口规范的会有熟悉感。
四、契约质量 = 分发质量:V哥的三条判断
写完代码只是及格线,Skill 化真正的功夫在契约上。V哥给三条判断:
① 典型话术要覆盖"同义改写",不是罗列功能。 “点一杯”“来一杯”"帮我带一杯"是三个说法一个意图,全写进典型话术,匹配面才够宽。只写功能名的契约,跟简历只写岗位名不写经历的候选人一样——不是不能干,是没人敢调。
② 边界条款是防误触发的保险丝。 官方建议每条典型话术配"不调用的情况",V哥把它当测试用例来写:把容易混淆的相邻意图一条条列出来、明确踢出去。误触发一次,用户说"这 App 抢答";漏触发一次,用户说"这 App 失聪"——但误触发的伤害更大,因为它透支的是对整个入口的信任。
③ suggestion 字段要写人话。 回包里的 suggestion 是直接给用户看的。V哥见过把内部异常码直接怼进去的写法,用户看到的回复是"ERR_TIMEOUT_504"——这不叫失败提示,这叫劝退。错误分支的 suggestion 按"用户下一步能干什么"来写。
五、上线前自检清单
V哥把整条链路压成一张自检表,六项全勾再提交:
| # | 检查项 | 挂了会怎样 |
|---|---|---|
| 1 | Stage 模型?FA 模型直接不可用,先迁移 | 编译期就过不去 |
| 2 | 目录名 = SKILL.md name = skillProfiles[].name? | 注册失败或匹配不上,无报错 |
| 3 | 方法名与契约 functionName 严格一致?签名首参 ArkTSScriptInfo? | 调用静默失败 |
| 4 | 每个能力方法都有回包?所有分支都走 report 出口? | 系统侧"Skill 失联",被降权 |
| 5 | exec-cli 的 skillName/scriptPath/functionName 与工程一致? | 匹配上了也调不通 |
| 6 | 触发场景含"不调用的情况"?suggestion 是人话? | 误触发 / 回复劝退 |
开发完成后走官方的真机测试流程调试(真机测试),别只跑模拟器。
六、V哥的收尾判断:红利窗口就是现在
回看整套机制,V哥的总结是:Skill 化把"应用分发"的粒度从 App 降到了功能。以前用户装你的应用才能用你的功能;现在你的功能直接进意图分发池,被系统智能体按需调用。入口前置了,露出变短了,长尾功能第一次有机会被"说"出来。
代价是新增了一种工程资产——契约文件。它不是写完就完的:话术要随用户表达演进、边界要随能力扩展修订。V哥把它类比成"给智能体维护的 API 文档",版本化管起来,和代码一个待遇。
7.0 刚发,意图分发池里占位的人还不多。第一期的上架审核那篇V哥写过一句话:合规是门票。这一期补上后半句——Skill 化是新的门票,门开着的窗口不会一直开着。
参考与出处
本文涉及的机制、接口与配置项来自以下官方文档:
- 基于ArkTS脚本的应用Skill开发指导(Ability Kit)
- @ohos.app.ability.scriptManager 接口参考
- Skill 真机测试
- HarmonyOS 新能力一览(7 / API 26)
最后一句:图标时代你的功能在等人点,意图时代你的功能在被点名——把 SKILL.md 当简历写,把薄脚本当翻译写,小艺念到名字的那一刻,就是你长尾功能翻身的那一天。
更多推荐


所有评论(0)