华为快服务智慧平台根据不同的业务垂类,定义了标准的接口规范,同时提供了一套可以适配不同场景的通用卡片模板,对于仅提供内容或服务的开发者,可以通过华为快服务智慧平台提供的OnlineEdit在线编辑器,将自有的接口或服务适配到华为快服务接口,最终以通用卡片的形式提供快服务,从而达到服务快速接入的目的。

本题目中,我们提供了部分典型通用卡片的结构和样式以及对应的接口规范,选用了合作伙伴健康有益开放的Health AI接口(健康饮食文字查询接口),拟开发一个食物能量查询服务,通过该快服务满足用户查询不同食物能量信息的需求。
该快服务的主要功能如下:
根据用户输入的不同食物关键字,查询并展示匹配到的食物热量信息(demo中取接口返回的"能量(千焦)"字段),本题提供的demo使用了HAG提供的通用卡片中的宫格型卡片。

如输入食物名称关键字"草莓",会搜索匹配到和草莓相关的食物信息列表,卡片默认取前6条食物的热量信息使用宫格型卡片展示效果如下:

开发者可以根据健康饮食文字查询接口返回的信息,结合附录提供的卡片模板丰富和展示更多内容,如食物热量详情、食物基本信息等。

登录华为快服务智慧平台:
https://developer.huawei.com/consumer/cn/console#/hag-widget-list/

如上述地址无法直接跳转,请依次在"管理中心"->"快服务"菜单下,找到华为快服务智慧平台入口卡片:

点击"华为快服务智慧平台"卡片进入系统后,点击 "内容接口类"快服务,根据此题目的需求,服务分类选择"运动健康>健康":

在"配置>交互模型"配置页面,创建用户意图,按此题目要求,意图分类可以选择"卡路里查询",触发方式选择"关键字"触发:

1.选择"快服务Function"履行方式

在"配置>Fulfillment"配置页面,配置服务履行信息,此题目中,配置方式需要选择为"代码编辑":

2.添加依赖

进入"依赖集"页签,双击选中hag-base依赖集,我们已经为您内置了使用Nodejs的常用依赖:

3.配置服务调用依赖的appId和apiKey

demo中提供了供测试Health AI接口用的appId和apiKey,开发者可以自行注册并获取appId和apiKey。

appId:5d3968e82a146866b22e8c4f apiKey:1e895a64fbd04604bfc1ad92f6fe0c82

接口中使用的appId和apiKey可以通过环境变量的方式来配置,快服务平台以加密形式保存,在Function入口函数中,通过context.env.appId形式获取。

打开"环境变量配置"开关,参考上述提供的变量值,依次添加appId和apiKey配置项:

4.配置查询参数/槽位

快服务提供的查询参数/槽位,可以在"参数配置"部分进行配置,在Function入口函数中,通过event.inquire.intent.slots获取。
打开"参数配置"开关,添加本快服务开放的查询参数,如根据食物名进行查询,则添加参数foodName:

5.接口适配开发

回到"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": "血糖生成指数", };

6.保存Function及服务履行配置

Function开发完成后,请依次点击"部署代码"按钮保存Function,点击右上角"保存"按钮保存整体Fulfillment配置。

依次完成快服务其他信息的配置。

注意:为了使用接口测试部分的卡片在线预览能力,快服务需要提供图文能力,因此在"输出能力"配置时,需要选中"服务提供了文字、图片内容":

服务配置完成后,进入"测试>接口测试"页面,点击"测试发布"按钮,发布测试态快服务:

进入"接口测试"页面,选择前面创建的意图,在槽位信息栏输入查询关键字,如"草莓",点击"接口测试"按钮测试接口执行情况:

Function执行成功后,可以看到卡片预览效果:

干得好,您已经成功完成了codelab并学到了:

卡片模板

1.基本型卡片模板:

图文型模板标识: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" } ] } ]

2.复合模板

根据业务需要,可把基本类型通用模板拼合成复合模板,规范允许以下复合类型:
列表型:同类模板上下重复排列;
主副型:两类模板上下排列,上为主模板,下为副模板,副模板可重复排列。

复合型模板标识: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文档>健康饮食文本查询"

已复制代码