HarmonyOS 7 新特性(二)|ArkAF:把 App 业务能力安全开放为 Skill

适用范围:HarmonyOS 7、API 26 Developer Beta。根据官方文档,基于 ArkTS 脚本的应用 Skill 从 API 26.0.0 开始提供,仅支持 Stage 模型。Beta 阶段接口、权限与上架要求仍可能调整,请以当前 SDK 和正式版文档为准。

很多人第一次接触 ArkAF Skill,会把它理解成"给 AI 暴露一个接口"。真正做过业务接入后会发现,最难的并不是写一个可调用的方法,而是回答四个问题:什么表达应该触发、什么表达绝不能触发、业务执行到哪一层、失败后如何让调用方安全恢复。

本文不做功能罗列,而是沿用官方 music-assistant 结构,完成一个可以迁移到真实项目的最小闭环:从 SKILL.mdmodule.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。

官方参考

相关推荐
OH_TPC27 分钟前
async 和 await
华为·harmonyos·鸿蒙
贾伟康42 分钟前
【天体运行模拟|03】HarmonyOS ArkTS 场景选择实战:组织行星、卫星与天文现象入口
list·harmonyos·arkts·arkui·页面路由
加农炮手Jinx10 小时前
Flutter for OpenHarmony 实战:flutter_animate 声明式动画让 UI 灵动如原生
flutter·ui·华为·harmonyos·鸿蒙
里欧跑得慢10 小时前
Flutter 三方库 function_tree — 鸿蒙应用开发中的动态数学公式解析与计算神器,实现鸿蒙深度适配下的复杂函数逻辑运行时求值实战(适配鸿蒙 HarmonyOS Next ohos)
android·前端·安全·flutter·华为·harmonyos
2501_9197490313 小时前
华为鸿蒙免费制定规则软件—小羊规则
华为·harmonyos·鸿蒙
lilian23314 小时前
HarmonyOS 7 新特性(四)|沉浸光感:空间材质、性能分级与降级策略
前端·pytorch·华为·harmonyos·材质
大雷神14 小时前
HarmonyOS AR Engine 物体形状实战:把实体书识别为 RECTANGLE,再放进地面 AR 盒子
ar·restful·harmonyos
AI备忘录14 小时前
(十六)GRE/IPSec 隧道配置命令五厂商对照:华为 华三 锐捷 迈普 思科
运维·服务器·网络·网络协议·网络安全·华为
BlueAsia_Lab16 小时前
华为 SuperCharge 认证,覆盖哪些产品?全品类梳理
运维·服务器·华为