智能小程序OPENCARD
接入指南

实现卡片接口

基本概念

下面这张图描述了开发者接入卡片后的搜索请求数据流:

数据流图

在开发者作为一个资源方接入一个卡片后,当用户搜索卡片相关的搜索词,开放平台会根据 intent 配置对该搜索词进行语义分析。分析得到的 intent 会发送到资源方的 Webhook URL,开发者根据自己数据和业务逻辑生成符合开放平台要求的资源数据返回给开放平台。开放平台将会用该资源数据结合卡片的配置渲染生成搜索结果中的卡片,并插入到搜索结果页中的适当位置。

出于对搜索速度,结果稳定性和减轻开发者服务器压力考虑,开放平台将会采取一定的缓存策略。也就是说,在一部分情况下用户的搜索请求是通过缓存来满足的。

Webhook API

开放平台发送给 Webhook URL 的请求 JSON object,遵照以下形式。其中的 intent 字段对于每个卡片来说,内容是不同的,即 srcid 决定了 intent 的内部数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "sp_ala", // 智能小程序阿拉丁请求,以便与其它请求区分
"srcid": "123", // 卡片的 ID,每个卡片不同
"surface": "mobile", // 卡片展示在哪个产品上, mobile: 支持小程序的移动搜索,web_h5: 支持 H5 的移动搜索
"intent": { // 卡片的 intent,每个卡片不同
...
},
"location": { // 定位信息,非 LBS 卡片不提供
"province": "浙江", // 省级行政单位短名(不含行政区划单位,例如"市、省")
"city": "杭州" // 城市名(不含行政区划单位,例如"市")
}
}

Webhook 返回的响应 JSON object,遵照以下形式。其中的 data 字段对于每个卡片来说,内容是不同的,即请求的 srcid 决定了响应消息中 data 的内部数据结构。

1
2
3
4
5
6
7
8
{
"status": 0, // 结果状态码,0 代表正确,1 代表无结果,2 代表请求参数错误,3 代表内部服务错误
"msg": "", // 出错消息
"data": { // 资源的结果内容,每个资源分类不同
...
},
"lifetime": 1559705004 // 可选字段,秒级 Unix 时间戳,指示该数据的有效期
}

以上内容主要是卡片 API 接口的公共字段。至于每个卡片的 srcid编号,以及 intentdata字段的数据格式和含义,请参考”开放类目卡片列表 “下对应卡片的接口说明。

Webhook 加密协议

为保护开放平台和开发者双方的数据安全,以上请求和响应消息都不能通过明文发送,需要对消息进行加密。加密算法的具体实现细节请参考”API 加密协议“一节中的说明。

示例

下面我们以景点门票为例说明各阶段数据的处理过程。

开放平台生成请求 JSON 明文

1
{"intent":{"scenic_spot":"\u6545\u5bab"},"srcid":"123","surface":"mobile","type":"sp_ala"}

开放平台根据开发者配置的 PSK 对请求进行加密,生成 JWE 密文

加密参数

  • PSK: "0123456789abcdef"
  • base64url(PSK): "MDEyMzQ1Njc4OWFiY2RlZg"
  • kid: "0"

会话参数

  • rid: "1559123682789-315431431"
1
eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoiMCIsInJpZCI6IjE1NTkxMjM2ODI3ODktMzE1NDMxNDMxIn0.1vDnKkf50N3piN9sLgr87h2maEm61IdJIC39WEhHB5N99P1JjNUMhQ.fnj_JIk0aYbkGKkGaT1QsA.HpabSZGitPw3CTmIuwpDS-XCN1Yxf2N9CLKBtJprC2q5qSMEHicubEV4jjcIYgctp8F1jYFSu3yhvWuxGtA7h4p_Ek07jmQRjZRE7GB9GhX_uE3IdoJavWm0cYEgJ7gF.B7iwwd5Eh4KaLdNID2f4UQ

开放平台将密文 HTTP POST 到 webhook

我们以 webhook http://localhost:8000 为例,用 curl 模拟发送 POST 请求:

1
curl -X POST "http://localhost:8000" -H "Content-Type:application/jwt" -d'eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoiMCIsInJpZCI6IjE1NTkxMjM2ODI3ODktMzE1NDMxNDMxIn0.1vDnKkf50N3piN9sLgr87h2maEm61IdJIC39WEhHB5N99P1JjNUMhQ.fnj_JIk0aYbkGKkGaT1QsA.HpabSZGitPw3CTmIuwpDS-XCN1Yxf2N9CLKBtJprC2q5qSMEHicubEV4jjcIYgctp8F1jYFSu3yhvWuxGtA7h4p_Ek07jmQRjZRE7GB9GhX_uE3IdoJavWm0cYEgJ7gF.B7iwwd5Eh4KaLdNID2f4UQ'

开发者根据 PSK 对请求进行解密,获得 JSON 明文

如果解密失败,开发者需返回 400 HTTP 状态码,并在 HTTP body 中注明解密出错原因。

对 JWE 解密时,除获得解密后的 JSON 明文之外,还可以从 protected header 中获得其它加密参数和会话参数。protected header 将会作为参数用于构造返回消息的 JWE 对象,其中的 rid可以用来作为会话的唯一标识打印日志,以及在追查问题时提供给开放平台作为参考。

开发者根据 intent 生成返回结果 JSON 明文

1
{"data":{"item_list":[{"title":"\u6545\u5bab\u535a\u7269\u9662"}],"jump_url":"/path/to/page3"},"msg":"","status":0}

开发者使用同样的参数对结果进行加密,返回 JWE 密文

可以注意到,JWE 的第一段 protected header 与请求完全一致,这代表结果的加密参数(除 PSK 以外)与请求保持了一致。

1
eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoiMCIsInJpZCI6IjE1NTkxMjM2ODI3ODktMzE1NDMxNDMxIn0.a4JDrnuVbcn7C00siEKwODneowk5hwpDmqZtzKycHcW2z-NimkzAJQ.0vEJlCWY7pUG400R9-KNCg.S0eNjmJUl9wUbJg_AB-sZw9LOQov27pa7HuoIR7_pdRUkxYZefzDbUzVpgelXtgAKpXSaZdYxwY_RSxCWbfUtfp1gzW_2pqnYhSOLNs8wBV3tReNdlxmjZocz_Tm_ePG2RNv3yo5H6IzJLEuEvBBp5xqtF-kGvsVF-kBLteHqDQ.-6EJe2xKhrlTB60K3m86Qg

Webhook demo (python)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/env python
# encoding: utf-8

import BaseHTTPServer
import time

# jwcrypto 不是标准库,需额外安装: pip install jwcrypto
from jwcrypto import jwk, jwe
from jwcrypto.common import json_encode, json_decode

# 配置项,测试时请修改以下配置
HOST_NAME = 'localhost'
PORT_NUMBER = 8000
PSK_TABLE = {'0':jwk.JWK.from_json('{"kty":"oct","k":"MDEyMzQ1Njc4OWFiY2RlZg"}')}

# Webhook 基本协议实现
#
# 以下代码为示例,未妥善处理业务字段或异常等因素
class Webhook(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(404)

def do_POST(self):
content_length = int(self.headers['Content-Length'])
req_body = self.rfile.read(content_length)
jwetoken = jwe.JWE()
# 从 protected 字段中解出来 kid
jwetoken.deserialize(req_body)
kid = jwetoken.jose_header["kid"]
# 通过 kid 确定要用哪个解密 key
key = PSK_TABLE[kid]
# 可以将 rid 打印到日志中,以便问题追查时 join 日志
print time.asctime(), "Req head %s" % json_encode(jwetoken.jose_header)
# 用 key 解密取得解密后的内容
jwetoken.decrypt(key)
print time.asctime(), "Req json %s" % jwetoken.payload.decode("unicode_escape")
req = json_decode(jwetoken.payload)
# 如果资源方有多张卡片,从 req 中解析出来 srcid,处理相应的业务逻辑
if req["srcid"] == "123":
# do something
res = {"status":0,"msg":""}
res["data"] = {
"item_list":[{"title":"故宫博物院"}],
"jump_url":"/path/to/page3"
}
else:
res = {"status":2,"msg":"Invalid srcid"}
# 处理业务响应结果
payload = json_encode(res)
print time.asctime(), "Res json %s" % payload.decode("unicode_escape")
# 用请求的 header 和 key 生成加密结果,保持加密算法和头内容(kid/rid)与请求一致
jwetoken = jwe.JWE(plaintext=payload,
protected=json_encode(jwetoken.jose_header),
recipient=key)
res_body = jwetoken.serialize(compact=True)
self.send_response(200)
self.end_headers()
self.wfile.write(res_body)

if __name__ == '__main__':
httpd = BaseHTTPServer.HTTPServer((HOST_NAME, PORT_NUMBER), Webhook)
print time.asctime(), "Webhook Starts - http://%s:%s" % (HOST_NAME, PORT_NUMBER)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print time.asctime(), "Webhook Stops - http://%s:%s" % (HOST_NAME, PORT_NUMBER)
反 馈帮 助 回 到顶 部