如何在iOS应用中解析Alexa自定义技能返回的响应体为JSON
问题现状
- 已完成Alexa自定义技能搭建,可从iOS应用端调用触发该技能
- 在Alexa控制台模拟器测试时,可直接看到技能返回的完整JSON结构
- iOS端代码运行正常,可正常播放自定义技能返回的语音文本响应,但无法提取目标结构化JSON内容
目标提取的JSON结构示例:
{ "body": { "version": "1.0", "response": { "outputSpeech": { "type": "SSML", "ssml": "<speak>Hello this is a custom skill</speak>" }, "type": "_DEFAULT_RESPONSE" }, "sessionAttributes": {}, "userAgent": "ask-node/2.9.0 Node/v10.23.1 sample/hello-world/v1.2" } }
现有代码实现
func postRecording(authToken:String, jsonData:String, audioData: Data) { var request = URLRequest(url: URL(string: EVENTS_ENDPOINT)!) request.httpShouldHandleCookies = false request.timeoutInterval = 60 request.httpMethod = "POST" request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") let boundry = NSUUID().uuidString let contentType = "multipart/form-data; boundary=\(boundry)" request.setValue(contentType, forHTTPHeaderField: "Content-Type") var bodyData = Data() bodyData.append("--\(boundry)\r\n".data(using: String.Encoding.utf8)!) bodyData.append("Content-Disposition: form-data; name=\"metadata\"\r\n".data(using: String.Encoding.utf8)!) bodyData.append("Content-Type: application/json; charset=UTF-8\r\n\r\n".data(using: String.Encoding.utf8)!) bodyData.append(jsonData.data(using: String.Encoding.utf8)!) bodyData.append("\r\n".data(using: String.Encoding.utf8)!) bodyData.append("--\(boundry)\r\n".data(using: String.Encoding.utf8)!) bodyData.append("Content-Disposition: form-data; name=\"audio\"\r\n".data(using: String.Encoding.utf8)!) bodyData.append("Content-Type: audio/L16; rate=16000; channels=1\r\n\r\n".data(using: String.Encoding.utf8)!) bodyData.append(audioData) bodyData.append("\r\n".data(using: String.Encoding.utf8)!) bodyData.append("--\(boundry)--\r\n".data(using: String.Encoding.utf8)!) session.uploadTask(with: request, from: bodyData, completionHandler: { (data:Data?, response:URLResponse?, error:Error?) -> Void in if (error != nil) { print("Send audio error: \(String(describing: error?.localizedDescription))") } else { let res = response as! HTTPURLResponse if (res.statusCode >= 200 && res.statusCode <= 299) { if let contentTypeHeader = res.allHeaderFields["Content-Type"] { let boundary = self.extractBoundary(contentTypeHeader: contentTypeHeader as! String) let directives = self.extractDirectives(data: data!, boundary: boundary) self.directiveHandler?(directives) } else { print("Content type in response is empty") } } } }).resume() } func extractDirectives(data: Data, boundary: String) -> [DirectiveData] { var directives = [DirectiveData]() let innerBoundry = "--\(boundary)".data(using: String.Encoding.utf8)! let endBoundry = "--\(boundary)--".data(using: String.Encoding.utf8)! let contentTypeApplicationJson = "Content-Type: application/json; charset=UTF-8".data(using: String.Encoding.utf8)! let contentTypeAudio = "Content-Type: application/octet-stream".data(using: String.Encoding.utf8)! let headerEnd = "\r\n\r\n".data(using: String.Encoding.utf8)! var startIndex = 0 while (true) { let firstAppearance = data.range(of: innerBoundry, in: startIndex..<(data.count)) if (firstAppearance == nil) { break } var secondAppearance = data.range(of: innerBoundry, in: (firstAppearance?.upperBound)!..<(data.count)) if (secondAppearance == nil) { secondAppearance = data.range(of: endBoundry, in: (firstAppearance?.upperBound)!..<(data.count)) if (secondAppearance == nil) { break } } else { startIndex = (secondAppearance?.lowerBound)! } let subdata = data.subdata(in: (firstAppearance?.upperBound)!..<(secondAppearance?.lowerBound)!) var contentType = subdata.range(of: contentTypeApplicationJson) if (contentType != nil) { let headerRange = subdata.range(of: headerEnd) var directiveData = String(data: subdata.subdata(in: (headerRange?.upperBound)!..<subdata.count), encoding: String.Encoding.utf8) ?? "Directive data is not String" directiveData = directiveData.replacingOccurrences(of: "\r\n", with: "") directives.append(DirectiveData(contentType: "application/json", data: directiveData.data(using: String.Encoding.utf8)!)) print("Directive: \(directiveData)") } contentType = subdata.range(of: contentTypeAudio) if (contentType != nil) { let headerRange = subdata.range(of: headerEnd) let audioData = subdata.subdata(in: (headerRange?.upperBound)!..<subdata.count) directives.append(DirectiveData(contentType: "application/octet-stream", data: audioData)) print("Audio data") } } return directives }
当前解析的实际返回
现在代码运行后,仅能解析得到SpeechSynthesizer命名空间下的Speak指令,结构如下:
{ "header": { "namespace": "SpeechSynthesizer", "name": "Speak", "messageId": "35601dca-413f-4788-93ed-305e04328d0e", "dialogRequestId": "db933fd9-3766-407d-99f6-2b33d9c814e6", "keys": { "isBlocking": true, "channel": "audio" } }, "payload": { "format": "AUDIO_MPEG", "token": "amzn1.as-ct.v1.ThirdPartySdkSpeechlet#ACRI#ValidatedSpeakDirective_amzn1.ask.skill.5718d3ec-b56a-4a5a-bd61-fdd4ab501fc2_ca3ae40b-52cd-4be9-b831-98e975a57e5a_VoiceInitiated#ACRI#[[ENCRYPTED_WITH_AlexaServiceKeyMasterUtil]]AAAAAAAAAAAHT6zrrzQh3EDQVwUGrjOpUAAAAAAAAACFZJ/VU0i9/OCY89UvtcV9X0291NLF7L7nE4irlDAGaph7NlUpgq0Ps31s8OflwHh/9wr3vp5Py+Gz+2Lgd8N6fPXWxPHp8+E6R8sLiRh9Cg==" } }
可落地的实现方案
核心原因是:当前调用的EVENTS_ENDPOINT是Alexa Voice Service(AVS)的语音事件上报接口,接口返回的是AVS协议层封装后的标准指令集合,不会直接透传Alexa技能平台内部的原始响应JSON。你在控制台看到的技能返回结构,是Alexa技能计算服务流转的内部数据,AVS客户端接口默认不会将这部分原始数据直接下发给移动端。要拿到目标结构化数据,可按以下步骤调整:
- 调整技能端的响应结构:将你需要在客户端获取的全量业务数据,存入技能返回的
sessionAttributes字段中。AVS网关会完整透传sessionAttributes的所有内容,不会像outputSpeech字段一样被转换为可播放的音频指令。示例调整方式:{ "version": "1.0", "response": { "outputSpeech": { "type": "SSML", "ssml": "<speak>Hello this is a custom skill</speak>" }, "type": "_DEFAULT_RESPONSE" }, "sessionAttributes": { "rawSkillResponse": { "version": "1.0", "response": { "outputSpeech": { "type": "SSML", "ssml": "<speak>Hello this is a custom skill</speak>" }, "type": "_DEFAULT_RESPONSE" }, "userAgent": "ask-node/2.9.0 Node/v10.23.1 sample/hello-world/v1.2" } } } - 修复iOS端的多part解析逻辑:当前
extractDirectives方法仅硬编码匹配了两种Content-Type的part,会漏掉AVS在响应流末尾附带的Alexa.Context命名空间的UpdateState指令——这部分指令的payload里,就携带了当前会话全量的sessionAttributes内容。你需要在拿到所有JSON类型的directive后,遍历匹配header中namespace == "Alexa.Context"且name == "UpdateState"的条目,即可提取存在sessionAttributes中的业务数据。 - 优化解析的鲁棒性:现有边界匹配逻辑没有处理part首尾的换行符,容易出现内容截断;同时不要仅靠硬编码匹配Content-Type字符串识别内容类型,要补充解析每个part的
Content-Disposition字段,避免因服务端返回的Header格式微调导致解析失效。
内容的提问来源于stack exchange,提问作者elbert rivas
相关产品推荐
相关产品推荐

