华为快服务智慧平台根据不同的业务垂类,定义了标准的接口规范,同时提供了一套可以适配不同场景的通用卡片模板,对于仅提供内容或服务的开发者,可以通过华为快服务智慧平台提供的OnlineEdit在线编辑器,将自有的接口或服务适配到华为快服务接口,最终以通用卡片的形式提供快服务,从而达到服务快速接入的目的。
本题目中,我们提供了部分典型通用卡片的结构和样式以及对应的接口规范,选用了合作伙伴健康有益开放的Health AI接口(健康饮食文字查询接口),拟开发一个食物能量查询服务,通过该快服务满足用户查询不同食物能量信息的需求。
该快服务的主要功能如下:
根据用户输入的不同食物关键字,查询并展示匹配到的食物热量信息(demo中取接口返回的"能量(千焦)"字段),本题提供的demo使用了HAG提供的通用卡片中的宫格型卡片。
如输入食物名称关键字"草莓",会搜索匹配到和草莓相关的食物信息列表,卡片默认取前6条食物的热量信息使用宫格型卡片展示效果如下:
开发者可以根据健康饮食文字查询接口返回的信息,结合附录提供的卡片模板丰富和展示更多内容,如食物热量详情、食物基本信息等。
登录华为快服务智慧平台:
https://developer.huawei.com/consumer/cn/console#/hag-widget-list/
如上述地址无法直接跳转,请依次在"管理中心"->"快服务"菜单下,找到华为快服务智慧平台入口卡片:
点击"华为快服务智慧平台"卡片进入系统后,点击 "内容接口类"快服务,根据此题目的需求,服务分类选择"运动健康>健康":
在"配置>交互模型"配置页面,创建用户意图,按此题目要求,意图分类可以选择"卡路里查询",触发方式选择"关键字"触发:
在"配置>Fulfillment"配置页面,配置服务履行信息,此题目中,配置方式需要选择为"代码编辑":
进入"依赖集"页签,双击选中hag-base依赖集,我们已经为您内置了使用Nodejs的常用依赖:
demo中提供了供测试Health AI接口用的appId和apiKey,开发者可以自行注册并获取appId和apiKey。
appId:5d3968e82a146866b22e8c4f
apiKey:1e895a64fbd04604bfc1ad92f6fe0c82
接口中使用的appId和apiKey可以通过环境变量的方式来配置,快服务平台以加密形式保存,在Function入口函数中,通过context.env.appId
形式获取。
打开"环境变量配置"开关,参考上述提供的变量值,依次添加appId和apiKey配置项:
快服务提供的查询参数/槽位,可以在"参数配置"部分进行配置,在Function入口函数中,通过event.inquire.intent.slots
获取。
打开"参数配置"开关,添加本快服务开放的查询参数,如根据食物名进行查询,则添加参数foodName:
回到"index.js"页签,接下来你需要使用华为快服务智慧平台提供的在线编辑器,通过Node.js语言完成Health AI接口向华为快服务接口的适配开发,具体接口规范及卡片模板请参考附录。
我们已经根据需求提供了基本的数据查询框架和转换函数,开发者需要按照自己选择的卡片模板,完成接口的适配和卡片的展示:
/**
* 入口函数
* @param event 内置变量,传入查询请求体FulfillmentReqBody
* @param context 内置变量,提供日志打印、环境变量读取等内置函数
*/
myHandler = function (event, context) {
var https = require('https');
var nodeRequest = require('request');
var appId = context.env.appId;
var apiKey = context.env.apiKey;
var tokenUrl = '/auth/token' + '?appId=' + appId + '&apiKey=' + apiKey;
var tokenOptions = {
hostname: 'api.jiankangyouyi.com',
path: encodeURI(tokenUrl),
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
};
var req = https.request(tokenOptions, function (res) {
res.setEncoding('UTF-8');
var respString = '';
res.on('data', function (data) {
respString += data;
});
res.on('end', function () {
var tokenResp = JSON.parse(respString);
var token = tokenResp.data.token;
context.logger.info("token: " + token);
// 获取Token后,查询食物详细信息
queryFoodInfo(event, context, nodeRequest, token, appId);
});
});
req.on('error', (err) => {
context.logger.info(JSON.stringify(err));
})
req.end();
}
module.exports.myHandler = myHandler;
/**
* 调用CP接口查询卡路里详细信息
* @param event 内置变量,传入查询请求体FulfillmentReqBody
* @param context 内置变量,提供日志打印、环境变量读取等内置函数
* @param nodeRequest 依赖的node request
* @param token 调用CP接口依赖的令牌
* @param appId 调用CP接口依赖的appId
*/
function queryFoodInfo(event, context, nodeRequest, token, appId) {
let { foodName } = event.inquire.intent.slots;
var foodNameValue = getSlotReal(foodName);
var now = new Date();
// 按照CP接口要求,构造HTTP请求的body
var body = {
appId: appId,
version: '2.0',
timestamp: new Date(now.getTime() - (-480) * 60000).toISOString().replace('T', ' ').replace(/\..+$/, ''),
// 查询参数,详细见接口文档;本demo中忽略gender和age的影响,查询count默认为100g/ml时的食物能量,默认取10条
reqData: {
count: 100,
gender: '1',
age: 20,
pageInfo: {
pageNum: 1,
pageSize: 10
},
text: foodNameValue
}
};
// 调用CP接口的完整请求
var foodOptions = {
uri: "https://api.jiankangyouyi.com/ego-gw/v2/query/healthy/text/food/list.do",
method: 'POST',
json: true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'token': token
},
body: body
};
// 调用CP接口后执行的回调函数
function foodReqCallBack(error, response, data) {
if (error) {
context.logger.info(JSON.stringify(error));
var errorinfo = {
"reply": {
"errorCode": "500",
"errorMessage": JSON.stringify(error)
}
}
context.callback(errorinfo);
} else {
context.logger.info("data: " + JSON.stringify(data));
// 在回调中将CP接口返回的数据模型映射到HAG标准模型
var convertResponse = buildFoodResponse(context, data.resData);
// 通过context.callBack将映射后的模型返回给卡片
context.callback(convertResponse);
}
}
nodeRequest(foodOptions, foodReqCallBack);
}
/**
* 将CP返回的数据结构映射到HAG标准模型
* @param context
* @param resData
*/
function buildFoodResponse(context, resData) {
context.logger.info("buildFoodResponse inner: " + resData);
var foodResponse = JSON.parse(resData);
if (!foodResponse.foodList || foodResponse.foodList.length < 1) {
var errorinfo = {
"reply": {
"errorCode": "500",
"errorMessage": "no data"
}
}
return errorinfo;
}
// 构造宫格型卡片数据列表,此处为top6的食物列表
var foodList = new Array();
var foodListLength = Math.min(foodResponse.foodList.length, 6);
for (var i = 0; i < foodListLength; i++) {
foodList.push({
"id": i,
"title": foodResponse.foodList[i].basicInfo.foodName,
"subtitle": "热量约" + foodResponse.foodList[i].compositionInfo[1].value + foodResponse.foodList[i].compositionInfo[1].unit,
"image": [{
"url": foodResponse.foodList[i].basicInfo.imageUrl,
"imageMode": "SQUARE"
}]
});
}
context.logger.info("foodList: " + JSON.stringify(foodList));
// 构造图文型卡片子标题列表,此处为top1食物的详细能量列表
var compositionList = new Array();
var compositionSourceList = foodResponse.foodList[0].compositionInfo;
for (var i = 0; i < compositionSourceList.length; i++) {
compositionList.push({
"subtitle": getCompositionName(compositionSourceList[i].name),
"combBody": compositionSourceList[i].value + compositionSourceList[i].unit
});
}
context.logger.info("compositionList: " + JSON.stringify(compositionList));
var result = {
reply: {
isEndSession: true,
speech: {
type: 'TEXT',
text: '测试一下',
ssml: '测试一下'
},
commands: [{
head: {
namespace: 'Render',
name: 'DisplayTemplate'
},
body: {
templateType: 'compound-template',
templateContent: {
items: [
{
"type": "basic-template",
"contentItems": [
{
"id": "100",
"title": "食物热量列表"
}
]
},
{
"type": "grid-template",
"contentItems": foodList
},
{
"type": "basic-template",
"contentItems": [
{
"id": "200",
"title": "开发者可以自行发挥扩展更多展示信息"
}
]
}
]
}
}
}
]
},
version: '1',
errorCode: '0',
errorMessage: 'ok'
};
context.logger.info("result: " + JSON.stringify(result));
return result;
}
/**
* 根据传入的槽位标识获取槽位值
* @param aSlot 槽位
*/
function getSlotReal(aSlot) {
if (aSlot == undefined) {
return "";
}
let { values } = aSlot;
if (values == undefined || (!Array.isArray(values)) || values.length == 0) {
return "";
}
if (typeof (values[0].real) != "string") {
return "";
}
return values[0].real;
}
/**
* 根据食物成分的标识,获取其中文名称,如根据标识"protein"获取名称"蛋白质"
* @param label 食物成分的标识,如"protein"
*/
function getCompositionName(label) {
return compositionMap[label];
}
/**
* 食物成分标识和中文名称映射表
*/
const compositionMap = {
"kilocalorie": "能量(大卡)",
"kilojoule": "能量(千焦)",
"protein": "蛋白质",
"fat": "脂肪",
"carbohydrate": "碳水化合物",
"dietaryFiber": "膳食纤维",
"cholesterol": "胆固醇",
"calcium": "钙",
"iron": "铁",
"potassium": "钾",
"magnesium": "镁",
"zinc": "锌",
"selenium": "硒",
"sodium": "钠",
"chromium": "铬",
"iodine": "脂肪",
"vitaminA": "维生素A",
"vitaminB1": "维生素B1(硫胺素)",
"vitaminB2": "维生素B2(核黄素)",
"vitaminB3": "维生素B3(烟酸)",
"vitaminB6": "维生素B6",
"vitaminB9": "维生素B9(叶酸)",
"vitaminB12": "维生素B12",
"vitaminC": "维生素C",
"vitaminD": "维生素D",
"vitaminE": "维生素E",
"gi": "血糖生成指数",
};
Function开发完成后,请依次点击"部署代码"按钮保存Function,点击右上角"保存"按钮保存整体Fulfillment配置。
依次完成快服务其他信息的配置。
注意:为了使用接口测试部分的卡片在线预览能力,快服务需要提供图文能力,因此在"输出能力"配置时,需要选中"服务提供了文字、图片内容":
服务配置完成后,进入"测试>接口测试"页面,点击"测试发布"按钮,发布测试态快服务:
进入"接口测试"页面,选择前面创建的意图,在槽位信息栏输入查询关键字,如"草莓",点击"接口测试"按钮测试接口执行情况:
Function执行成功后,可以看到卡片预览效果:
干得好,您已经成功完成了codelab并学到了:
图文型模板标识:basic-template
数据结构示例:
"items": [
{
"type": "basic-template",
"contentItems": [
{
"id": "1",
"title": "复仇者联盟",
"score": "3.5",
"content": [
{
"subtitle": "上映时间",
"combBody": "2018年5月11日"
},
{
"subtitle": "电影介绍",
"combBody": ""
}
],
"tag": [
{
"tagMode": "NORMAL",
"tagContent": "string",
"color": "green"
},
{
"tagMode": "emphasize",
"tagContent": "string",
"color": "green"
},
{
"tagMode": "stamp",
"tagContent": "string",
"color": "green"
}
],
"image": [
{
"url": "https://hag-ability-test.obs.myhwclouds.com/ability/afd36ef0665f46e9bc015cac7446f815/ic_express.png",
"imageMode": "SQUARE"
},
{
"url": "https://hag-ability-test.obs.myhwclouds.com/ability/afd36ef0665f46e9bc015cac7446f815/ic_express.png",
"imageMode": "PORTRAIT"
},
{
"url": "https://hag-ability-test.obs.myhwclouds.com/ability/afd36ef0665f46e9bc015cac7446f815/ic_express.png",
"imageMode": "LANDSCAPE"
}
],
"link": {
"webURL": "https://weburl",
"deepLink": {
"url": "https://deeplinkurl",
"appName": "appname",
"appPackage": "com.app.packagename",
"minVersion": 0,
"minAndroidAPILevel": 0
},
"quickApp": {
"url": "https://quickappurl",
"minPlatformVersion": 0,
"minVersion": 0
}
},
"similarity": 0
}
]
}
]
宫格型模板标识:grid-template数据结构示例:
"items": [
{
"type": "grid-template",
"indexFlag": "true",
"contentItems": [
{
"id": "string",
"title": "string",
"coverTitle": "string",
"subTitle": "string",
"cornerTag": {
"tagMode": "string",
"tagContent": "string",
"color": "green"
},
"bottomTag": {
"tagMode": "string",
"tagContent": "string",
"color": "green"
},
"image": [
{
"url": "https://hag-ability-test.obs.myhwclouds.com/ability/0e7cabd73bd744a6aecaa9283dea9247/ic_sport.png",
"imageMode": "SQUARE"
},
{
"url": "https://hag-ability-test.obs.myhwclouds.com/ability/0e7cabd73bd744a6aecaa9283dea9247/ic_sport.png",
"imageMode": "PORTRAIT"
},
{
"url": "https://hag-ability-test.obs.myhwclouds.com/ability/0e7cabd73bd744a6aecaa9283dea9247/ic_sport.png",
"imageMode": "LANDSCAPE"
}
],
"link": {
"webURL": "https://weburl",
"deepLink": {
"url": "https://deeplinkurl",
"appName": "appname",
"appPackage": "com.app.packagename",
"minVersion": 0,
"minAndroidAPILevel": 0
},
"quickApp": {
"url": "https://quickappurl",
"minPlatformVersion": 0,
"minVersion": 0
}
},
"button": {
"webURL": "https://weburl",
"deepLink": {
"url": "https://deeplinkurl",
"appName": "appname",
"appPackage": "com.app.packagename",
"minVersion": 0,
"minAndroidAPILevel": 0
},
"quickApp": {
"url": "https://quickappurl",
"minPlatformVersion": 0,
"minVersion": 0
},
"buttonImageURL": "string",
"buttonText": "string"
},
"similarity": "string"
}
]
}
]
根据业务需要,可把基本类型通用模板拼合成复合模板,规范允许以下复合类型:
列表型:同类模板上下重复排列;
主副型:两类模板上下排列,上为主模板,下为副模板,副模板可重复排列。
复合型模板标识:compound-template
https://developer.huawei.com/consumer/cn/devservice/doc/5060301
进入:
"6.3 对象类型结构>6.3.19 FulfillmentRspBody"
其中:FulfillmentRspBody.reply.commands.body.templateType参考上述卡片模板ID定义,如使用复合模板,则为compound-template;FulfillmentRspBody.reply.commands.body.templateContent的结构定义参考如下接口数据示例部分,如使用复合模板,则templateContent.items为子模板列表,items[i].type则为子模板类型,如子模板类型为宫格型grid-template。
var data = {
"reply": {
"isEndSession": "true",
"speech": {
"type": "TEXT",
"text": "测试一下",
"ssml": "测试一下"
},
"commands": [
{
"head": {
"namespace": "Render",
"name": "DisplayTemplate"
},
"body": {
"templateType": "compound-template",
"templateContent": {
"items": [
{
"type": "basic-template",
"contentItems": [
{
"id": "1",
"title": "苹果",
"content": [
{
"combBody": "苹果,果实圆形,味甜或略酸,是常见水果,具有丰富营养成分,有食疗、辅助治疗功能。苹果原产于欧洲、中亚、西亚和土耳其一带,于十九世纪传入中国。"
}
],
"image": [{
"url": "https://files.public.jianzhishidai.cn/images/2/70a571e2abf84247994cdc349ac9d917.jpg?hash=Fk5QIPqaRpR0otguFzh7m0jXdKoY&width=400&height=400&fsize=71898&scope=1",
"imageMode": "SQUARE"
}
],
}
]
},
{
"type": "basic-template",
"contentItems": [
{
"id": "2",
"title": "约等于如下食物的热量"
}
]
},
{
"type": "grid-template",
"contentItems": [{
"id": "11",
"title": "1个馒头≈120克",
"image": [{
"url": "https://files.public.jianzhishidai.cn/images/2/a4dbef32888d485fbbcdd50a120cb5d8.jpg?hash=FuJzF7bHgdjx3stHFmCF7zsgdzna&width=20716&height=1&fsize=0&scope=1",
"imageMode": "SQUARE"
}]
},
{
"id": "12",
"title": "1碗米饭≈200克",
"image": [{
"url": "https://files.public.jianzhishidai.cn/images/2/95f0d27377614e21b73d688e3c1d8e64.jpg?hash=Flbqv4C_tvY8qcg8dhQbkyjd70wn&width=26900&height=1&fsize=0&scope=1",
"imageMode": "SQUARE"
}]
},
{
"id": "11",
"title": "1块牛排≈200克",
"image": [{
"url": "https://files.public.jianzhishidai.cn/images/2/c2650e45e5fe4aa596fc2b78d3f5c358.jpg?hash=FkUhykx_k5bIZ62pXM_RIJCwjExX&width=24270&height=1&fsize=0&scope=1",
"imageMode": "SQUARE"
}]
},
]
}
]
}
}
}
]
},
"version": "1",
"errorCode": "0",
"errorMessage": "ok"
};
在线接口文档:
https://healthai.jiankangyouyi.com/views/doc_detail.html?path=1125#MetrologyInfo
进入:
"查询类>健康人群饮食查询>API文档>健康饮食文本查询"