
适用范围:HarmonyOS 7、API 26 Developer Beta。根据官方文档,基于 ArkTS 脚本的应用 Skill 从 API 26.0.0 开始提供,仅支持 Stage 模型。Beta 阶段接口、权限与上架要求仍可能调整,请以当前 SDK 和正式版文档为准。
很多人第一次接触 ArkAF Skill,会把它理解成"给 AI 暴露一个接口"。真正做过业务接入后会发现,最难的并不是写一个可调用的方法,而是回答四个问题:什么表达应该触发、什么表达绝不能触发、业务执行到哪一层、失败后如何让调用方安全恢复。
本文不做功能罗列,而是沿用官方 music-assistant 结构,完成一个可以迁移到真实项目的最小闭环:从 SKILL.md、module.json5、ArkTS 入口脚本,到幂等、授权、结果契约和自动化测试。
一、先定义一个能验收的 Skill
假设音乐应用已有"播放指定歌曲"的页面能力。我们不开放"控制整个音乐 App",而只开放一个窄能力:根据歌曲 ID 播放一首可用歌曲。
这个 Skill 的验收标准不是"智能体能回答",而是下面五条都成立:
| 维度 | 必须满足 | 反例 |
|---|---|---|
| 触发 | 用户明确要求播放具体歌曲 | "有哪些热门歌"也误触发播放 |
| 参数 | 使用稳定 songId,名称仅作展示 |
只凭模糊歌名直接执行 |
| 授权 | 只访问当前账户有权播放的内容 | 绕过会员、地区或年龄限制 |
| 副作用 | 重复请求不会叠加多个播放任务 | 超时重试导致连续切歌 |
| 结果 | 返回机器可读状态和恢复建议 | 只返回"失败了" |
能力越窄,触发、权限、测试和审计越容易稳定。Skill 不是页面快捷方式,也不应该直接操作组件树。

二、按官方约定组织目录
官方开发指导给出的应用 Skill 结构包含脚本、描述文件和应用内领域实现。以 music-assistant 为例,可以整理为:
text
entry/
├── skills/
│ └── music-assistant/
│ ├── scripts/
│ │ └── MusicSkill.ets
│ └── SKILL.md
└── src/main/ets/
└── service/
└── MusicPlayer.ets
这里最重要的边界是:MusicSkill.ets 只做协议适配,MusicPlayer.ets 才承载业务。以后系统入口、App 页面和 A2A Agent 都可以复用同一个领域服务,不需要复制三份播放逻辑。
三、用 module.json5 注册,而不是靠扫描猜测
Skill 需要在模块配置中注册。下面只保留关键字段,具体结构以 API 26 当前模板为准:
json5
{
"module": {
"name": "entry",
"type": "entry",
"skillProfiles": [
{
"name": "music-assistant",
"abilityName": "EntryAbility",
"srcEntries": ["./skills/music-assistant/SKILL.md"],
"version": "1.0.0"
}
]
}
}
建议把 name 当成不可随意改动的协议标识,把 version 纳入变更评审。若删除参数、收紧枚举或改变结果含义,应创建新版本,而不是让旧调用方在运行时才发现不兼容。
四、SKILL.md 既写"何时调用",也写"何时不调用"
高质量描述不是一句"可以播放音乐"。它应给模型足够的边界信息,同时让工程师可以据此写测试。
markdown
---
name: music-assistant
description: 当用户明确要求播放一首已知歌曲时使用。
---
# 使用边界
- 适用:播放用户明确指定、且账户有权访问的歌曲。
- 不适用:推荐歌单、识别哼唱、购买会员、修改账户资料。
- 缺少 songId 时:先查询候选并让用户选择,不得猜测。
# 执行
使用 ohos-arkTSScript:
- skillName: music-assistant
- scriptPath: scripts/MusicSkill.ets
- functionName: playSong
官方文档要求 skillName 与 Skill 名称一致,scriptPath 使用相对路径,functionName 与脚本公开方法一致。三个名字只要有一个漂移,就可能出现"能发现能力,但执行失败"的灰色故障。
五、先冻结输入输出契约
不要让入口脚本接收一个任意对象。为真实项目定义可校验 DTO,并对每个字段说明来源。
ts
export interface PlaySongInput {
requestId: string
songId: string
source: 'voice' | 'system-agent' | 'app'
expectedTitle?: string
}
export type PlaySongCode =
| 'OK'
| 'INVALID_ARGUMENT'
| 'LOGIN_REQUIRED'
| 'NO_PERMISSION'
| 'NOT_FOUND'
| 'TEMPORARILY_UNAVAILABLE'
export interface PlaySongOutput {
code: PlaySongCode
songId?: string
title?: string
retryable: boolean
userAction?: 'login' | 'choose-another' | 'retry-later'
}
expectedTitle 只用于二次校验和展示,不能代替稳定 ID;retryable 明确告诉调用方是否允许重试;userAction 则把恢复路径变成结构化信息,而不是一段无法解析的自然语言。
六、入口脚本保持"薄"
官方接口中,公开方法的第一个参数固定为 scriptManager.ArkTSScriptInfo。入口收到参数后只做校验、调用领域服务、映射结果:
ts
import { scriptManager } from '@kit.AbilityKit'
import { MusicPlayer } from '../../src/main/ets/service/MusicPlayer'
export default class MusicSkill {
public async playSong(
scriptInfo: scriptManager.ArkTSScriptInfo,
input: PlaySongInput
): Promise<PlaySongOutput> {
const validated = validatePlaySongInput(input)
if (!validated.ok) {
return {
code: 'INVALID_ARGUMENT',
retryable: false,
userAction: 'choose-another'
}
}
return MusicPlayer.getInstance().play(validated.value, scriptInfo)
}
}
上面是工程化示例,领域方法签名需要按项目实际实现。不要在这个类里读取页面组件、切换路由或拼装弹窗;否则应用不在前台、页面未创建或系统恢复进程时,Skill 会变得不可预测。
七、把幂等放进领域层
智能调用天然会遇到超时与重试。一次播放请求可能已经成功,但结果回传前链路中断;如果服务端把第二次请求当成新任务,就会重复切歌。
ts
class MusicPlayer {
private results = new Map<string, PlaySongOutput>()
async play(command: PlaySongInput): Promise<PlaySongOutput> {
const cached = this.results.get(command.requestId)
if (cached) return cached
const song = await this.catalog.findById(command.songId)
if (!song) return this.remember(command.requestId, {
code: 'NOT_FOUND', retryable: false, userAction: 'choose-another'
})
const access = await this.policy.canPlay(song)
if (!access.allowed) return this.mapAccessDenied(access)
await this.player.replaceCurrent(song)
return this.remember(command.requestId, {
code: 'OK', songId: song.id, title: song.title, retryable: false
})
}
}
幂等键的生命周期必须覆盖调用方可能重试的窗口。不能只用内存 Map 作为生产实现;跨进程恢复时应保存必要结果,并设置合理过期时间。
八、三类动作采用三种确认策略
| 风险等级 | 示例 | 是否需要执行前确认 | 日志要求 |
|---|---|---|---|
| 只读 | 查询播放状态、搜索歌曲 | 通常不需要 | 记录来源与结果码 |
| 可逆写入 | 播放、暂停、收藏 | 依据上下文,可省略或轻确认 | 记录请求 ID 和撤销结果 |
| 高风险/不可逆 | 购买、外发、删除账户数据 | 必须明确确认 | 完整审计,敏感字段脱敏 |
"意图命中"不等于"用户已授权执行"。特别是付款、外发消息、共享隐私数据等操作,确认必须发生在真正提交副作用之前,不能藏在一段长条款里。
九、统一错误,但不要抹平原因
入口层可以把内部异常收敛为稳定错误码,但监控必须保留可定位信息。建议至少区分:参数无效、未登录、权限不足、资源不存在、业务冲突、临时不可用、超时和内部错误。
ts
function toSkillResult(error: DomainError): PlaySongOutput {
switch (error.kind) {
case 'SessionExpired':
return { code: 'LOGIN_REQUIRED', retryable: false, userAction: 'login' }
case 'NetworkTimeout':
return { code: 'TEMPORARILY_UNAVAILABLE', retryable: true, userAction: 'retry-later' }
case 'RegionRestricted':
return { code: 'NO_PERMISSION', retryable: false, userAction: 'choose-another' }
default:
return { code: 'TEMPORARILY_UNAVAILABLE', retryable: false }
}
}
对用户可以说"当前地区暂不可播放",对日志则记录内部错误族、请求 ID、Skill 版本和耗时。歌曲名、账户令牌等敏感信息不要进入普通日志。
十、完成回调只能由一个出口负责
当业务需要返回应用内完成结果时,官方提供 completeArkTSScriptInApp(context, requestCode, result)。项目中应封装单一完成器,保证成功、失败、取消只完成一次。
ts
class SkillCompletion {
private completed = false
complete(context: UIAbilityContext, requestCode: number,
result: scriptManager.ExecuteResult): void {
if (this.completed) return
this.completed = true
scriptManager.completeArkTSScriptInApp(context, requestCode, result)
}
}
这里使用了 API 26 文档中的接口名称;ExecuteResult 的字段以当前 SDK 声明为准。重复完成、页面销毁后再回调、异常分支忘记回调,都是需要专项测试的问题。
十一、用测试证明边界,而不是只演示成功
建议把 SKILL.md 触发语句也纳入用例库:正样本验证应该调用,负样本验证绝不能调用。领域层则至少覆盖以下矩阵。
ts
describe('playSong skill', () => {
it('returns same result for duplicated requestId', async () => {
const first = await service.play(fixtureRequest)
const second = await service.play(fixtureRequest)
expect(second).toEqual(first)
expect(fakePlayer.replaceCount).toBe(1)
})
it('does not play when account has no permission', async () => {
policy.deny('RegionRestricted')
const result = await service.play(fixtureRequest)
expect(result.code).toBe('NO_PERMISSION')
expect(fakePlayer.replaceCount).toBe(0)
})
})
还要真机验证应用未启动、后台、前台、锁屏、登录过期、弱网、进程被回收和 API 不支持等场景。Beta SDK 上编译通过,只能证明语法与当前工具链匹配,不能替代端到端验证。
十二、上线前检查清单
- Skill 名称、模块注册名和
SKILL.md执行配置完全一致; - 描述中同时写清触发条件与禁止触发条件;
- 输入使用稳定 ID,所有字段都有校验和默认策略;
- 入口脚本不依赖页面生命周期;
- 写操作具备幂等键,高风险动作执行前再次确认;
- 错误码可穷尽,调用方知道能否重试和如何恢复;
- 日志能关联意图、请求、领域执行与最终结果,但不泄露敏感数据;
- 不支持 API 26 的环境可以回退到 App 内常规路径。

结语
ArkAF Skill 的核心不是"让 AI 能调用一个方法",而是把 App 中一项真实业务能力整理成可发现、可校验、可授权、可恢复的机器契约。最稳妥的接入顺序是:先选一个窄能力,冻结输入输出,再实现薄适配层、幂等领域服务和失败测试。这个闭环稳定后,才适合扩展更多 Skill。
官方参考
- 基于 ArkTS 脚本的应用 Skill 开发指导:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-skill-development-guide
- HarmonyOS 7 新能力一览:https://developer.huawei.com/consumer/cn/features/