目录

元服务SKILL开发规范

元服务SKILL是将元服务中的业务功能拆解为标准化的原子接口与原子组件,封装为SKILL供智能体调度。

在元服务项目中ascf/ascf_src目录,采用ASCF框架开发skills分包,目录结构如下:

ascf/ascf_src/skills/
├── flight-booking-skill/ # 示例:预定航班 SKILL
│ ├── SKILL.md # 业务说明文档,行数小于500行(必须)
│ ├── mcp.json # 能力声明:暴露给模型的可调接口清单(必须)
│ ├── index.js # 原子接口实现入口(必须)
│ ├── apis     # 原子接口实现
│ └── components # 原子组件配置(将结构化数据渲染为对话流卡片)
└── drink-skill/ # 示例:饮品点单 SKILL
└── ...

mcp.json描述"单个接口怎么用",SKILL.md描述整个业务怎么跑:业务流程、接口依赖、跨接口约束。

app.json配置SKILLS

ascf/ascf_src/app.json配置skills分包和skills配置。一个元服务可声明多个SKILLs,并且一个独立分包也可以放置多个 SKILLs。

字段 必填 说明
name 是 名称,最大长度64字符。
description 是 SKILL简要说明。
path 是 SKILL目录路径,建议使用项目内相对路径,且与name匹配。

​app.json配置示例:

{
  "subPackages": [
    {
      "root": "skills",
      "pages": []
    }
  ],
  "agent": {
    "skills": [
      {
        "name": "flight-booking-skill",
        "description": "机票预订场景:搜索航班、查看详情、确认订单、支付订单完成预订",
        "path": "skills/flight-booking-skill"
      }
    ]
  }
}

SKILL.md需要带上YAML frontmatter,用于声明name和description:

---
name: flight-booking-skill
description: 机票预订场景:搜索航班、查看详情、确认订单、支付订单完成预订

---

SKILL规范

元服务SKILL需要遵从 HarmonyOS Skill开发规范 。主要有如下差异点:

[h2]SKILL中需对于mcp.json中定义的原子接口做工具调用示例说明

## Function: GetFlightPrice
- **toolName**: GetFlightPrice
- **bundleName**: com.atomicservice.xxx
- **description**: 获取低价航班日历(航班价格趋势卡片)。用户想看某条航线一段时间内每天最低价(如「几天飞最便宜」「未来一个月低价日历」)时调用。仅返回价格日历,不含具体航班明细。
- **参数**: {"type":"object","properties":{"end":{"type":"String","description":"日历结束日期 YYYY-MM-DD,须不早于 start;未传时默认 start 起共7天"},"fromCity":{"type":"String","description":"出发城市名称"},"toCity":{"type":"String","description":"到达城市名称"},"date":{"type":"String","description":"出发日期 YYYY-MM-DD"},"start":{"type":"String","description":"日历开始日期 YYYY-MM-DD;相对时间须先转具体日期;未传时默认明天"}},"required":["fromCity","toCity"]}
- **调用方式**:invoke(functionName:"GetFlightPrice", arguments:{bundleName:"com.atomicservice.xxx", appId:"xxx", skillName:"flight-skill", apiName:"getFlightPrice", renderAtomicCard:true, componentName:"flight-skill_flight-price-card", query:"<用户的语料>", fromCity:"<出发城市>", toCity:"<到达城市>", start:"<YYYY-MM-DD 可选>", end:"<YYYY-MM-DD 可选>"})

[h2]SKILL中支持显示 invoke 调用原子接口

invoke(functionName:"SearchFlights", arguments:{bundleName:"com.atomicservice.xxx", appId:"xxx", skillName:"flight-skill", apiName:"searchFlights", renderAtomicCard:true, componentName:"flight-skill_flight-card", args:{query:"<用户的语料>", departure:"<出发城市,未说明留空>", destination:"<目的城市,无flightNumber时必填>", flightNumber:"<航班号,可选>", departDateTime:"<YYYY-MM-DD>"},query:"<用户的语料>"})

其中renderAtomicCard参数控制是否原子组件,默认为true。

[h2]SKILL中支持renderAtomicCard渲染原子组件

```renderAtomicCard
{"appId": "xxx", "skillName": "flight-skill", "componentName": "flight-skill_flight-card", "abilityDatas": {"structuredContent": <全量原始数据>, "_meta": <插件返回的 _meta 元数据,原样透传>}, "componentPath": "<卡片组件路径>"}
```

为了提升推理性能,需尽量减少structuredContent的内容,可以用关键信息和索引结合。

mcp.json 声明原子接口和关联的原子组件

[h2]原子接口声明

mcp.json 包含所有原子接口的 Schema 声明。

属性 是否必填 说明
name 是 标识符,跟 index.js 中导出的原子接口函数名一致,最大长度64字符。
description 是 原子接口的功能描述。
inputSchema 是 原子接口的入参,需为对象格式。
outputSchema 建议填 原子接口返回的 structuredContent 对应的 schema。
_meta 否 可指定渲染的原子组件,componentPath 相对于 SKILL 目录,如 { "ui": { "componentPath": "components/xx/xx" } }。
{
  "apis": [
    {
      "name": "SearchFlights",
      "description": "搜索航班",
      "inputSchema": {
        "type": "object",
        "properties": {
          "departure": {
            "type": "string",
            "description": "【位置】出发城市或机场"
          },
          "destination": {
            "type": "string",
            "description": "【位置】目的城市或机场"
          },
          "departDateTime": {
            "type": "string",
            "description": "出发日期,格式 YYYY-MM-DD"
          }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "flights": {
            "type": "array",
            "description": "航班列表;可基于已返回结果筛选/排序后更新",
            "items": {
              "type": "object",
              "properties": {
                "flightNo": { "type": "string", "description": "航班号" },
                "price": { "type": "number", "description": "票价" }
              }
            }
          },
          "total": {
            "type": "number",
            "description": "航班总数"
          },
          "hasMore": {
            "type": "boolean",
            "description": "是否显示「查看更多」入口;仅 total>3 时为 true;Agent 回复只说明条数,勿引导用户点击「查看更多」"
          }
        }
      },
      "_meta": {
        "ui": {
          "componentPath": "components/flight-card/index"
        }
      }
    }
  ]
}

[h2]原子组件声明

在mcp.json的components字段下可以声明原子组件的额外配置:

参数 类型 必填 描述
path string 是 组件路径(相对于 SKILL 目录)。
relatedPage string 是 关联的元服务页面路径。
relatedPageLabel string 否 关联的元服务页面按钮文字,不配置或者为空不显示。
expirable boolean 否 该组件渲染的卡片是否可被过期,默认 false。
expiredText string 否 过期后显示的文案,默认"服务已过期"。

[h2]原子接口返回值结构

原子接口的返回值包含以下字段:

参数 类型 必填 描述
isError boolean 是 是否为错误结果。
content ITextContent[] 是 返回给 LLM 的文本内容。
structuredContent Record<string, any> 否 返回给 LLM 的结构化数据,需最小必要原则。
_meta Record<string, any> 否 对 LLM 不可见,可携带元数据。

其中isError、structuredContent、content三个字段的数据会作为 LLM 的上下文供其理解,而 _meta内容对 LLM 不可见,用于携带私有数据传递给原子组件。

content 目前仅支持1个ITextContent:

interface ITextContent {
  type: "text";
  text: string;
}

[h2]原子接口正常与异常流程

当isError: true时,不进行卡片渲染,structuredContent将被忽略,content常用于异常流程的引导。

当isError: false且该原子接口绑定了原子组件时,智能体将倾向于渲染卡片。structuredContent数据会传给原子组件用来渲染(便于模型理解卡片内容),content用于后续流程引导。

实现示例

在原子接口的内部实现中,可以通过has.request从服务器请求数据,也可以调用项目自身封装的服务获取数据。

async function getStores({ location }) {
  // 原子接口内部实现,此处省略
  // has.request({ url: 'https://www.example.com/xx' })

  return {
    isError: false,
    content: [{ type: "text", text: "已为您查询附近门店" }],
    structuredContent: {
      location: "深圳",
      stores: [
        { name: "坂田 1 店", address: "xxx", code: "001" },
        { name: "坂田 2 店", address: "xxx", code: "002" },
      ]
    },
    _meta: {}
  }
}

[h2]注册原子接口

在SKILL的index.js中通过has.modelContext.registerAPI注册原子接口。当智能体发起调用时,会在注册列表中找到对应的函数入口并执行:

const searchFlights = require('./apis/searchFlights.js');

const skill = has.modelContext.createSkill('skills/flight-booking-skill');

skill.registerAPI('SearchFlights', searchFlights);

[h2]skill.use注册中间件

为方便在项目侧统一封装接口注册、登录校验、埋点上报和错误处理逻辑,避免相同逻辑散落在每个原子接口中,我们提供了中间件能力。

// 1. 创建 skill,需与 app.json 中保持一致
const searchFlights = require('./apis/searchFlights.js');

const skill = has.modelContext.createSkill('skills/flight-booking-skill');

// 2. 可用 skill 注册 API,通过这种方式的注册才会走到中间件
skill.registerAPI('SearchFlights', searchFlights);

// 3. 注册中间件
skill.use(async (ctx, next) => {
  // 登录校验
  const token = has.getStorageSync({ key: 'token'});
  if (!token) {
    // 执行登录操作
    // 也可以直接抛出错误,让 Agent 决定其他操作
    // throw new Error('登录过期,接下来自动执行 login 工具')
  }
  await next()  // 执行实际 handler
});
  • 只有通过skill注册API才会走到中间件。
  • 中间件ctx类型:
interface IModelContextMiddlewareContext {
  name: string    // 当前执行的原子接口名
  skillPath: string    // 当前中间件执行时的 skillPath
  arguments: Record<string, any>    // 原子接口的入参
}

原子组件

原子组件用于承接原子接口返回的数据和内容展示。使用与元服务的自定义组件相同编码风格。通过has.modelContext.getContext / getViewContext获取渲染所需的尺寸、数据等信息。原子组件与ASCF框架的组件采用类似范式开发,元服务SKILL模式下运行采用了全新的原生渲染机制,能力与ASCF框架普通模式下有差异,需避免兼容性问题。当前支持的基础组件如下:

组件 支持范围
view 支持。
text 支持,不支持 user-select。
button 不支持 open-type。
image 支持,仅支持网络图片和png、jpg 格式,且只支持缩放模式。
scroll-view 支持横向和纵向滚动,但是建议不要滚动。
canvas 支持。

原子组件的运行环境与原子接口隔离,处于不同的上下文中,可调用的接口能力也有所不同。

功能和约束如下:

原子组件中的 CSS 仅支持少量选择器:

[h2]元服务关联页

原子组件渲染出来的卡片左上方是标题栏,右上方提供了进入元服务关联页的按钮。点击按钮默认进入配置关联页面。在mcp.json中声明关联页面。

{
  "apis": [],
  "components": [
    {
      "path": "components/flight-list-card/index",
      "relatedPage": "/pages/index/index",
      "relatedPageLabel": "打开元服务"
    }
  ]
}

关联页接受query参数:

Page({
  onLoad(query) {
    console.info(query);
  }
})
  • setRelatedPage 支持path字段,动态指定跳转的关联页面。
  • 每次进行setRelatedPage,内部path和query都会进行重置,若不传path,则默认重置为json中配置的关联页。

[h2]适配深色模式

媒体查询仅支持深色模式:

@media (prefers-color-scheme: dark) {
    /* DarkMode 下的样式 */
}

支持onThemeChange 和 offThemeChange监听深色模式切换:

// 注册监听
has.onThemeChange((res) => {
  console.info('主题变化:', res.theme)  // 'light' 或 'dark'
})

// 注销监听
has.offThemeChange()

[h2]适配屏幕宽度

折叠屏场景下,原子组件宽度会动态变化,需监听原子组件宽度变化做适配。尤其是canvas场景必须要适配:

Component({
  pageLifetimes: {
    resize(res) {
      res.size.windowWidth // 新的显示区域宽度
      res.size.windowHeight // 新的显示区域高度
    }
  }
})

[h2]开发原子组件

一般位于skills目录中components下开发组件,监听NotificationType.Result获取数据:

Component({
  lifetimes: {
    created() {
      const { NotificationType } = has.modelContext;
      const ctx = has.modelContext.getContext(this);
      const vCtx = has.modelContext.getViewContext(this);
      ctx.on(NotificationType.Result, (data) => {
        const id = data.result.id;
        // 在原子组件渲染时动态设置 query 参数
        vCtx.setRelatedPage({ query: `id=${id}` });
      });
    }
  },
})

[h2]组件过期

提供两种粒度的过期接口:

方式一:has.modelContext.expireAllCards()

可在原子接口或组件里调用。所有声明了expirable: true的组件将被标记为过期并渲染蒙层。

// 过期所有可过期卡片
has.modelContext.expireAllCards();

// 按 componentPath 过滤
has.modelContext.expireAllCards({
  componentPaths: ['components/flight-list-card/index']
});

// 只过期最近一张匹配的卡片
has.modelContext.expireAllCards({
  componentPaths: ['components/flight-list-card/index'],
  match: 'latest'
});

方式二:vCtx.expirePreviousCards()

只能在原子组件里调用,将当前组件之前渲染过并声明了expirable: true的组件标记为过期。

const viewCtx = has.modelContext.getViewContext(this);

viewCtx.expirePreviousCards()

// 按 componentPath 过滤
viewCtx.expirePreviousCards({
  componentPaths: ['components/flight-list-card/index']
})

// 只过期最近一张匹配的卡片
viewCtx.expirePreviousCards({
  componentPaths: ['components/flight-list-card/index'],
  match: 'latest'
})

[h2]拉起半屏页面

当需要展示更多详细信息或需要用户补充信息时,可通过原子组件上按钮引导用户打开半屏页面完成。半屏页面与元服务环境基本一致,不允许任何跳转行为。当完成指定任务,需要进行下一步操作时,可通过半屏里的按钮触发一段上行文本消息,此时会同步关闭半屏页面回到智能体界面,智能体将继续处理新的用户请求。

在原子组件内点击并打开半屏页面:

<view onTap="handleOpenMore">查看更多</view>
Component({
  methods: {
    handleOpenMore() {
      const vCtx = has.modelContext.getViewContext(this);
      vCtx.openDetailPage({
        url: '/pages/detail/index?id=xxx'
      })
    }
  }
})

[h2]ASCF元服务中发送上行消息和更新卡片

在半屏页面中可通过接口代用户以第一人称视角发送一条上行文本消息,之后会自动关闭半屏页面。

Page({
  onTap() {
    const ctx = has.modelContext.getContext();
    ctx.sendFollowUpMessage({
      content: [
        {
          type: 'text',
          text: '选择商品'
        },
        {
          // 指定下一步应调用哪个原子接口
          type: 'api/call',
          data: {
            name: 'selectGoods',
            arguments: {}
          }
        }
      ]
    });
  }
})

[h2]更新卡片

打开半屏页面操作后,有时需要更新卡片,支持通过重新调用对应的原子接口的方式,来重新渲染原子组件。

const ctx = has.modelContext.getContext();

ctx.reapplyApiCall({
  arguments: {
    city: '深圳'
  }
});

[h2]ASCF元服务中H5中发送上行消息和更新卡片

若半屏页面是个web-view加载的H5页面,也可通过相同的方式发送上行。

// html页面中发送消息:
// post sendFollowUpMessage message
has.ascfweb.postMessage({
  options: { mode: 'realtime' },
  data: {
    type: 'sendFollowUpMessage',
    args: {
      content: [
        { type: 'text', text: '选择智利火山酒庄火魂红葡萄酒' },
        { type: 'api/call', data: { name: 'selectProduct', arguments: '{"drinkId":10774}' } }
      ]
    }
  },
});

// post reapplyApiCall message
has.ascfweb.postMessage({
  options: { mode: 'realtime' },
  data: {
    type: 'reapplyApiCall',
    args: {
      arguments: {
        name: 'searchDrinks',
        arguments: '{"keyword": "葡萄酒"}'
      }
    }
  },
});


// 监听h5中的事件
// <web-view src="resource://rawfile/html/index.html" bindmessage="handleMsg"></web-view>
Page({
  handleMsg(e) {
    const ctx = has.modelContext?.getContext?.();
    const data = e.detail?.data?.[0];
    if (!ctx || !data) {
      return;
    }
    // 发送上行消息
    if (data.type === 'sendFollowUpMessage' && data.args) {
      ctx.sendFollowUpMessage(data.args);
    } else if (data.type === 'reapplyApiCall' && data.args) {
      ctx.reapplyApiCall(data.args);
    }
  },
});

[h2]ArkUI元服务以及H5中发送上行消息和更新卡片

对于ArkUI元服务,需要依赖ASCF框架提供的接口能力。

// entry\src\main\ets\entryability\EntryAbility.ets
import { AtomicServiceAI } from '@atomicservice/ascfapi';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AtomicServiceAI.getInstance().ensureInit(this.context, want);
  }

  onDestroy(): void {
    AtomicServiceAI.getInstance().cleanup();
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AtomicServiceAI.getInstance().ensureInit(this.context, want);
  }
}

// 使用说明
import { AtomicServiceAI } from '@atomicservice/ascfapi';

AtomicServiceAI.getInstance().sendFollowUpMessage({
  content: [
    { type: 'text', text: '选择智利火山酒庄火魂红葡萄酒' },
    { type: 'api/call', data: { name: 'selectProduct', arguments: '{"drinkId":10774}' } }
  ]
});

对于ArkUI元服务中的H5页面,通过AtomicServiceEnhancedWeb双向通信机制,可以实现半屏通信功能。详细示例如下:

html页面中发送消息:

// post sendFollowUpMessage message
has.asWeb.postMessage({
  options: { mode: 'realtime' },
  data: {
    type: 'sendFollowUpMessage',
    args: {
      content: [
        { type: 'text', text: '选择智利火山酒庄火魂红葡萄酒' },
        { type: 'api/call', data: { name: 'selectProduct', arguments: '{"drinkId":10774}' } }
      ]
    }
  },
});

// post reapplyApiCall message
has.asWeb.postMessage({
  options: { mode: 'realtime' },
  data: {
    type: 'reapplyApiCall',
    args: {
      arguments: {
        name: 'searchDrinks',
        arguments: '{"keyword": "葡萄酒"}'
      }
    }
  },
});

webview中监听消息并且处理:

AtomicServiceEnhancedWeb({
  src: $rawfile("html/index.html"),
  controller: this.controller,
  onMessage: (event: OnMessageEvent) => {
    const payload = event.data?.[0] as Record<string, object>; // 注意转换类型
    if (payload?.type === 'sendFollowUpMessage') {
      AtomicServiceAI.getInstance().sendFollowUpMessage(payload.args);
    } else if (payload?.type === 'reapplyApiCall') {
      AtomicServiceAI.getInstance().reapplyApiCall(payload.args);
    }
  }
})

原子接口和原子组件 API 支持列表

在元服务SKILL下,原子接口、原子组件、半屏详情页都有一定的限制。

[h2]原子接口新增 API

[h2]原子组件新增 API

Context

ViewContext

[h2]原有 API 支持情况

原有元服务 API 在元服务SKILL下并不是全部可用,不同运行环境支持的接口有所不同,支持情况如下表。

分类 API 说明 原子接口 原子组件
基础 has.env 获取客户端环境变量。 支持 支持
静默登录 has.login 华为账号登录。 支持 不支持
获取手机号 has.getPhoneNumber 获取授权的手机号。 支持 不支持
发起请求 has.request 发起 HTTPS 网络请求。 支持 不支持
websocket 全部网络 API WebSocket 网络请求。 支持 不支持
上传 has.uploadFile 上传文件。 支持 不支持
下载 has.downloadFile 下载文件。 支持 不支持
系统信息 has.getDeviceInfo 获取设备基础信息。 支持 支持
系统信息 has.getAppBaseInfo 获取宿主基础信息。 支持 支持
系统信息 has.getWindowInfo 获取窗口信息。 支持 支持
系统信息 has.getSystemInfo 获取系统信息。 支持 支持
数据缓存 has.setStorage 异步存储数据。 支持 支持
数据缓存 has.setStorageSync 同步存储数据。 支持 支持
数据缓存 has.getStorage 异步读取数据。 支持 支持
数据缓存 has.getStorageSync 同步读取数据。 支持 支持
数据缓存 has.getStorageInfo 异步获取缓存相关信息。 支持 支持
数据缓存 has.getStorageInfoSync 同步获取缓存相关信息。 支持 支持
数据缓存 has.removeStorage 异步删除缓存数据。 支持 支持
数据缓存 has.removeStorageSync 同步删除缓存数据。 支持 支持
数据缓存 has.clearStorage 异步清除本地缓存。 支持 支持
数据缓存 has.clearStorageSync 同步清除本地缓存。 支持 支持
位置 has.openLocation 使用内置地图打开位置。 不支持 支持
位置 has.chooseLocation 打开地图选择位置。 不支持 支持
交互反馈 has.showToast 显示消息提示框。 不支持 支持
交互反馈 has.hideToast 隐藏消息提示框。 不支持 支持
拨打电话 has.makePhoneCall 跳转拨打电话。 不支持 支持
元服务信息 has.getServiceInfoSync 获取元服务信息。 支持 支持
图片视频 has.getImageInfo 获取图片信息。 支持 不支持
图片视频 has.chooseImage 选择图片。 不支持 支持
图片视频 has.previewImage 预览图片。 不支持 支持
图片视频 has.saveImageToPhotosAlbum 保存图片到相册。 不支持 支持
文件 has.openDocument 打开文件。 不支持 不支持
扫码 has.scanCode 扫码 不支持。 支持
振动 全部振动 API vibrate 等 不支持。 支持
收货地址 has.chooseAddress 选择收货地址。 不支持 支持
隐私授权 has.getPrivacySetting 获取隐私授权设置。 支持 支持
支付 has.requestPayment 鸿蒙收银台华为支付。 支持 支持
支付 has.cashierPicker 拉起通用收银台。 支持 支持

若某个分类下的API都支持,细分API则不会全部列出,请跳转到接口文档查看。

[h2]半屏页面能力支持列表

选择联系人或者收货地址等功能,可以直接基于元服务的页面通过半屏嵌入运行,实现功能闭环。

半屏页面运行环境基本等价于元服务环境,但是也有一些区别,半屏页面不建议路由跳出,建议直接完成任务后关闭返回。半屏可以通过has.modelContext与对话流直接交互,例如获取上下文:has.modelContext.getContext(),上行消息 ctx.sendFollowUpMessage({ content }) 、更新卡片ctx.reapplyApiCall({ arguments })。

当前半屏页面页面暂时没有做接口能力约束,后续可能会变更。

[h2]实现账号登录与隐私授权

元服务SKILL中账号体系与元服务是互通,支持使用has.login和has.getPhoneNumber实现 静默登录功能。暂不支持使用storage互通数据。

has.login和 has.getPhoneNumber等接口会自动触发隐私弹框授权,当用户授权通过后才会继续执行,否则会接口调用异常,走异常处理流程。为了实现更好的交互体验,建议非必要情况下提供游客模式接口,避免登录后触发权限授权弹框。可以参考如下示例处理:

// 可以根据是否需要隐私授权状态判断,是否已经授权过,当前调用 has.login 才会触发授权
if (has.getPrivacySetting().needAuthorization) {
  // 调用游客模式接口,避免触发隐私弹框授权
} else {
  // 已授权后 has.login 静默登录
  // 非必要情况下,不要额外获取手机号,让半屏进入到元服务后处理
}