# 自定义拦截器

iFLYOS为用户提供了大量的常用技能,比如音乐播放、天气查询、股票查询等等。但如果你不想使用其中的某些技能或者希望iFLYOS的技能无法处理某些用户请求时,对用户的语料进行自己的特殊处理,那么拦截器将会是你最好的选择。关于拦截器的配置,请前往设备接入控制台 (opens new window)进行配置:设备接入控制台 > 设备 > 云端语义

# 拦截器类型

# 前拦截器

前拦截器可以在用户请求进入iFLYOS技能之前,对用户的请求进行拦截。当你配置了前拦截器,我们会生成一个PreInterceptorRequest,发送到拦截器的地址,根据拦截器的响应做进一步的端指令处理。拦截器响应请查看:拦截器响应说明

# 后拦截器

iFLYOS技能无法处理用户请求(即用户语料未能命中任意技能)时,如果你配置了后拦截器,那么我们会生成一个PostInterceptorRequest发送到你的拦截器,再根据拦截器的响应做进一步的端指令处理。拦截器响应请查看:拦截器响应说明

拦截器要求

  • 发布后的产品的拦截器链接必须使用https协议。在测试调试阶段我们支持http的链接,为了保证产品上线后拦截器可用,请在提交审核前把链接改为https,并保证拦截器服务可用。
  • 拦截器需要对请求进行安全校验,以确认是来自于iFLYOS的请求,请参考:校验说明
  • 拦截器需要在800ms内返回,否则iFLYOS将会当做无响应,继续下一步的处理

# 回调地址

根据拦截器语义处理类型不同,回调地址的含义也不相同。

使用私有技能 for iFLYOS 作为拦截器,回调地址用于接收设备上报的自定义事件。如果你的设备未定义自定义事件,回调地址可不填。

使用 AIUI webAPI 应用作为拦截器,此处填写你在 AIUI 应用配置中的后处理地址,iFLYOS 服务将把 AIUI webAPI 应用的语义理解结果 (opens new window)发送至你配置的回调地址。

使用自定义语义理解服务作为拦截器,此处填写你的自定义语义理解服务地址,用于接收用户请求。

# 拦截器请求校验

回调地址服务需要校验请求是否来源于 iFLYOS,所有发送给技能的请求Header都包含Signature字段

# 校验步骤

校验该签名的步骤:

  1. 从企业平台获取校验签名的公钥public_key
  2. Signature的值进行Base64-decode,得到decoded_signature
  3. 使用SHA-1摘要算法(十六进制编码)对请求Body生成hash
  4. 使用RSA算法, 使用公钥public_keydecoded_signature,hash进行校验,摘要类型为sha256

假设用户从企业平台拿到的密钥为

-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlN9BU3eBo9YbR/KaH42W\nmgkE3j/Sm+WkXHDOeP5IDmehq0yTlWQtfUpoAj6T0/KIQgnhQm6MULXlRtvYIam4\nW5I4gRSx1Yk4dpBTpJ8z6/QJG6DqywjuATfZgyEiEr9Nc6sjW2bXILHOLlCvMT+5\n8aX9+QNB+WRqMSNkHN06Fa9aIfE7fbrjASlfZB4oYlr+ldTM1Q6pUOhLDJtZw906\nVNqfgdZUPOBU7D9bYonBZrMCZN//YMr7jxSo9p6H4a0v9HNAvKPWFgPs7SmM/mC2\ndWsF+A2TaA+znshWbmYPzNMphrBul+oDbYtOi6zP7Co00Xgg+ivNf3PdEhMuiJ6E\nbQIDAQAB\n-----END PUBLIC KEY-----\n

IVS的请求body为

"{\"message\":\"ok\"}"

Signature为

LG9565Z7KF92BKXWUdihbJ10oSelQg0YeR6QGYF4n4dd1QtP+2Gig8nWFkQaev06fJ2t30+Jh7ZmEdlZaoKJFEXxjXaG00mcVlc2VI0C7HJ/XXahBRcGt9guVrkDAfS0BEihN2hnsPev4QZ2WHVX/RLG+JnkA2j+eUKJnnMNIEjkgWJ8U17yWd9Etdn2Zj/8l/4TMqhvtG/5qB8ILkB7633agOj7z1ShD6eb9+blMYwx209pXPZomQ6E8QA0vYw8AcK7BFbcw7ikU1Ii2LLKxDg6aYRg82nFGfNQZftNmb4AR60g55ZsFPo9aSfWYADDR1YczUI5hPeLZkDoL9BaLw==

校验示例(Ruby)

public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlN9BU3eBo9YbR/KaH42W\nmgkE3j/Sm+WkXHDOeP5IDmehq0yTlWQtfUpoAj6T0/KIQgnhQm6MULXlRtvYIam4\nW5I4gRSx1Yk4dpBTpJ8z6/QJG6DqywjuATfZgyEiEr9Nc6sjW2bXILHOLlCvMT+5\n8aX9+QNB+WRqMSNkHN06Fa9aIfE7fbrjASlfZB4oYlr+ldTM1Q6pUOhLDJtZw906\nVNqfgdZUPOBU7D9bYonBZrMCZN//YMr7jxSo9p6H4a0v9HNAvKPWFgPs7SmM/mC2\ndWsF+A2TaA+znshWbmYPzNMphrBul+oDbYtOi6zP7Co00Xgg+ivNf3PdEhMuiJ6E\nbQIDAQAB\n-----END PUBLIC KEY-----\n"

body = "{\"message\":\"ok\"}"
hash = OpenSSL::Digest.hexdigest("SHA1", body)

signature = "LG9565Z7KF92BKXWUdihbJ10oSelQg0YeR6QGYF4n4dd1QtP+2Gig8nWFkQaev06fJ2t30+Jh7ZmEdlZaoKJFEXxjXaG00mcVlc2VI0C7HJ/XXahBRcGt9guVrkDAfS0BEihN2hnsPev4QZ2WHVX/RLG+JnkA2j+eUKJnnMNIEjkgWJ8U17yWd9Etdn2Zj/8l/4TMqhvtG/5qB8ILkB7633agOj7z1ShD6eb9+blMYwx209pXPZomQ6E8QA0vYw8AcK7BFbcw7ikU1Ii2LLKxDg6aYRg82nFGfNQZftNmb4AR60g55ZsFPo9aSfWYADDR1YczUI5hPeLZkDoL9BaLw=="
decoded_signature = Base64.decode64(signature)

pub_key = OpenSSL::PKey::RSA.new(public_key)
pub_key.verify("SHA256", decoded_signature, hash) # => true

校验示例(Java)

public static void main(String[] args){
	String publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlN9BU3eBo9YbR/KaH42W\nmgkE3j/Sm+WkXHDOeP5IDmehq0yTlWQtfUpoAj6T0/KIQgnhQm6MULXlRtvYIam4\nW5I4gRSx1Yk4dpBTpJ8z6/QJG6DqywjuATfZgyEiEr9Nc6sjW2bXILHOLlCvMT+5\n8aX9+QNB+WRqMSNkHN06Fa9aIfE7fbrjASlfZB4oYlr+ldTM1Q6pUOhLDJtZw906\nVNqfgdZUPOBU7D9bYonBZrMCZN//YMr7jxSo9p6H4a0v9HNAvKPWFgPs7SmM/mC2\ndWsF+A2TaA+znshWbmYPzNMphrBul+oDbYtOi6zP7Co00Xgg+ivNf3PdEhMuiJ6E\nbQIDAQAB\n-----END PUBLIC KEY-----\n";
	publicKey = publicKey.replace("-----BEGIN PUBLIC KEY-----", "").replace("\n", "").replace("-----END PUBLIC KEY-----", "");
	String sign = "LG9565Z7KF92BKXWUdihbJ10oSelQg0YeR6QGYF4n4dd1QtP+2Gig8nWFkQaev06fJ2t30+Jh7ZmEdlZaoKJFEXxjXaG00mcVlc2VI0C7HJ/XXahBRcGt9guVrkDAfS0BEihN2hnsPev4QZ2WHVX/RLG+JnkA2j+eUKJnnMNIEjkgWJ8U17yWd9Etdn2Zj/8l/4TMqhvtG/5qB8ILkB7633agOj7z1ShD6eb9+blMYwx209pXPZomQ6E8QA0vYw8AcK7BFbcw7ikU1Ii2LLKxDg6aYRg82nFGfNQZftNmb4AR60g55ZsFPo9aSfWYADDR1YczUI5hPeLZkDoL9BaLw==";
	String body = "{\"message\":\"ok\"}";
	System.out.println("verify: " + verify(publicKey, body, sign));
}

public static boolean verify(String publicKey, String body, String sign){
    String hash = SHA1(body);
    try {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        byte[] encodedKey = Base64.getDecoder().decode(publicKey);
        PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
        java.security.Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(pubKey);
        signature.update(hash.getBytes("utf-8"));
        return signature.verify(Base64.getDecoder().decode(sign));
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

public static String SHA1(String decript) {
    try {
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        digest.update(decript.getBytes());
        byte messageDigest[] = digest.digest();
        StringBuffer hexString = new StringBuffer();
        for (int i = 0; i < messageDigest.length; i++) {
            String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
            if (shaHex.length() < 2) {
                hexString.append(0);
            }
            hexString.append(shaHex);
        }
        return hexString.toString();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return "";
}

# 拦截器语义处理类型

# 使用私有技能 for iFLYOS 作为拦截器

你需要:

  1. 技能工作室 (opens new window)创建【私有技能 for iFLYOS】。
  2. 在技能控制台配置你的技能交互模型,参考文档 (opens new window)
  3. 编写你的技能后处理 (opens new window),用于处理技能收到的通用技能请求 (opens new window)
  4. 在设备端实现自定义技能,参考文档 (opens new window)

使用此方式时,服务回调地址将能够接收设备上报的自定义事件。如果你的设备未定义自定义事件,回调地址可不填。

提示

  • 若你的拦截器选择的语义处理服务为【AIUI WebAPI应用】,拦截器应用处理请求的语义,应用后处理会收到 处理结果协议 (opens new window)
  • 若你的拦截器选择的语义处理服务为【其他语义服务】,iFLYOS 会把用户请求直接转发至你的回调地址。

# 使用AIUI webAPI 应用作为拦截器

建议

如果你之前已经通过AIUI应用平台 (opens new window)开发过应用,我们建议你选择这种方式来实现快速开发。

iFLYOS 和 AIUI webAPI 应用实现了打通,你只需要在拦截器配置处填写APPIDAPIKey,用户的语音请求即可发送至你的AIUI webAPI 应用进行语义理解。APPIDAPIKeyAIUI应用控制台 (opens new window)中获取。

iFLYOS 支持对AIUI webAPI 应用的返回结果进行云端处理或设备处理。

# 云端处理

# 设备处理

使用设备处理的方式,iFLYOS 服务将把 AIUI webAPI 应用的语义理解结果按以下格式下发送至设备:

# EVS transfer_semantic 指令
{
  "iflyos_responses": [
    ...,
    {
      "header": {
        "name": "interceptor.custom"
      },
      "payload": {//AIUI 应用返回内容透传
        ... 
      }
    }
  ]
}
# IVS TransferSemantic 指令

消息格式

{
	"directive": {
    "header": {
       "namespace": "Custom",
       "name": "TransferSemantic",
       "dialogRequestId": "...",
       "messageId": "...",
    },
    "payload": {//AIUI 应用返回内容透传
    }
	}
}

# 使用自定义语义理解服务作为拦截器

如果你已经自己定义了语义理解服务,或使用第三方语义理解服务,你可以选择自定义语义理解服务。

此时回调地址必填,用户请求时,你的回调地址会收到以下请求,你需要在处理后返回以下回复。

提示

如果自定义语义理解服务选择不处理该语料,只需要返回http.statusCode为204,http.body为空即可。

# 请求

# HTTP请求头
POST / HTTP/1.1
Content-Type : application/json;charset=UTF-8
Host : your.application.endpoint
Content-Length :
Accept : application/json
Accept-Charset : utf-8
Signature: xxxxxxxxxxxxxxx
# Body格式
{
  "version": "1.0",
  "session": {
    "new": true,
    "sessionId": "f78b7d68...",
    "attributes": {
      "key": "value"
    }
  },
  "context": {
    "System": {
      "device": {
        "deviceId": "f78b7d68...",
        "supportedInterfaces": {
          "AudioPlayer": {},
          "Display":{}
        }
      },
      "application": {
        "applicationId": "f78b7d68..."
      },
      "user": {
        "userId": "f78b7d68...",
        "accessToken": "23bf653f..."
      }
    },
    "AudioPlayer": {
      "playerActivity": "PLAYING",
      "token": "audioplayer-token",
      "offsetInMilliseconds": 0
    },
    "Custom": {
      "key": "value" //自定义数据,参考自定义上下文
    }
  },
  "request": {}
}

关于参数的具体说明请参考:技能请求Body说明 (opens new window)

拦截器文本请求

消息示例

{
  "type": "PreInterceptorRequest", // 后拦截器为:PostInterceptorRequest
  "requestId": "f78b7d68...",
  "timestamp": "2018-08-06T16:13Z ",
  "query": {
    "type": "TEXT",
    "original":"今天天气怎么样?"
  }
}

参数说明

参数 描述 类型 必须出现
type 请求类型,这里取值 PreInterceptorRequest/PostInterceptorRequest String
requestId 代表请求的唯一标识符。 String
timestamp 请求时间戳,以ISO 8601格式发送 String
query 请求信息。
- type:请求类型,取值TEXT
- original:用户语音经过IVS理解后生成的文本
String

拦截器事件请求

{
  "type": "PreInterceptorEventRequest",
  "requestId": "f78b7d68...",
  "timestamp": "2018-08-06T16:13Z ",
  "payload": {
    "key": "value" // 自定义数据
  }
}

参数说明

参数 描述 类型 必须出现
type 请求类型,这里取值 PreInterceptorEventRequest String
requestId 代表请求的唯一标识符。 String
timestamp 请求时间戳,以ISO 8601格式发送 String
payload 请求数据,与设备端上报事件的payload相同,详情请查看自定义设备能力 String

# 响应

标准响应 响应请参考:技能响应说明 (opens new window)

自定义指令响应

厂商除了可以返回标准响应 (opens new window),还可以返回以下自定义指令,IVS在收到自定义指令响应后,我们将把payload中的数据直接放入Custom指令透传至设备。

自定义指令中可以包含一个自定义的json数据,比如:

{
  "version": "1.0",
  "sessionAttributes": {
    "key": "value"
  },
  "response": {
    "directives": [
      {
        "type": "Custom",
        "payload": {
          "key": "value" // 自定义数据
        }
      }
    ],
    "expectSpeech": true,
    "shouldEndSession": true
  }
}

# 自定义设备能力

若你拦截器中选择的语义理解服务为私有技能for iFLYOS 或 其他语义服务,拦截器返回至 IVS 的数据会经过处理后,下发对应指令至设备。如果你的设备使用拦截器时有定义自定义指令,则你的设备需要实现对自定义指令的执行逻辑。

# EVS自定义设备能力

EVS是以response的形式与客户端进行交互的。EVS 设备要实现的自定义指令可参考自定义拦截器

# IVS自定义设备能力

IVS是以指令的形式与客户端进行交互的,当你的设备希望通过一些自定义的指令对设备进行控制,那么,我们为拦截器提供了这样的能力。技能可以在技能返回中将指令发送给IVS,IVS将把数据传送给设备。

IVS协议请参考:设备与IVS的交互约定

# Custom Context

如果你有需要从设备端获取特殊的设备状态时,你应该在发起语音请求时,在请求的上下文中,具体格式请参考:设备状态 Context

{
  "header": {
    "namespace": "Custom",
    "name": "Custom"
  },
  "payload": {
    // 自定义状态
  }
}

# Custom 指令

消息格式

{
  "directive": {
    "header": {
      "namespace": "Custom",
      "name": "Custom",
      "dialogRequestId": "...",
      "messageId": "...",
    },
    "payload": {
      "key": "value" // 自定义数据
    }
  }
}

# Custom 事件

客户端通过上报事件与iFLYOS进行交互,当你的设备希望主动上报信息到iFLYOS,我们会将事件转发到对应的拦截器技能。IVS协议请参考:设备与IVS的交互约定

消息格式

{
  "context": [// 	参考设备上下文
	],
  "event": {
	  "header": {
	    "namespace": "Custom",
	    "name": "Custom",
	    "messageId": "...",
	  },
	  "payload": {
	    "key": "value" // 自定义数据
	  }
	}
}