[译] 基于 Node.js 的 Alexa Skills Kit 发布了!

9,815 阅读10分钟

我们今天很高兴地宣布,一个新的基于 Node.js,旨在帮助开发者们更加简单和快捷地开发 Alexa skill 的 alexa-sdk 发布了。通过 Alexa Skills KitNode.js,和 AWS Lambda 开发 Alexa skill 如今已成为最受欢迎的 skill 开发方式。Node.js 事件驱动,非阻塞的特性,使它非常适合开发 Alexa skill,并且 Node.js 也是世界上最大开源系统之一。除此之外,为每个月前一百万个网络请求提供免费服务的亚马逊网络服务系统(AWS)Lambda,能够支持大部分开发者从事 skill 的开发。在使用 AWS Lambda 的同时,你不需要担心管理任何 SSL 证书的问题(因为 Alexa Skills Kit 是被 AWS 信任的触发器)。

在使用 AWS Lambda 创建 Alexa skill 的时候,加入 Node.js 和 Alexa Skills Kit 只是一个简单的流程,但你实际上所需要写的代码要比这复杂得多。我们已经意识到大部分开发 skill 的时间都花费在处理会话(session)的属性、skill 的状态持久化,创建回复以及行为模式上面。因此,Alexa 团队着手于开发一个基于 Node.js 的 Alexa Skills Kit SDK 来帮助你避免这些常见的烦恼,从而专注于你的 skill 自身的逻辑开发而不是样板化编码。

使用基于 Node.js 的 Alexa Skills Kit(alexa-sdk)加速 Alexa Skill 的开发

有了 alexa-sdk,我们的目标是帮助你在能够避免不必要的复杂度的情况下,更快捷地开发 skills。今天我们要发布的这个最新版本的 SDK 具备以下几个特点:

  • 新版SDK是可托管的 NPM 安装包,简化了在任何 Node.js 环境下的开发
  • 可以通过内置事件创建 Alexa 的回复
  • 为新的 session 内置帮助事件(Helper events),并且添加了未处理事件(unhandled events)来捕捉所有异常
  • 提供了能构建基于状态机的用户意图(intent)处理的帮助函数(Helper function)
  • 这让根据当前 skill 的状态定义不同的事件管理器成为现实
  • 属性持久化的配置在 Amazon DynamoDB 的帮助下变得更加简单
  • 所有输出的语音将自动封装在 SSML 下
  • Lambda 事件和和上下文对象 (context objects) 将通过 this.event 读取,并且可以通过 this.contextAbility 重写内置函数,从而让你的状态管理和回复创建更加灵活。例如,将状态属性储存到 AWS S3 上。

安装和调试基于 Node.js 的 Alexa Skills Kit (alexa-sdk)

alexa-sdk 已经上传到了 github,并且可以以 node 包的形式通过下面的指令在你的 Node.js 环境下安装:

npm install --save alexa-sdk

为了开始使用 alexa-sdk,你需要先导入它的库。你只需要在你的项目里简单地创建一个名为 index.js 的文件然后加入以下代码:

var Alexa = require('alexa-sdk');

exports.handler = function(event, context, callback){

    var alexa = Alexa.handler(event, context);

};

这几行代码将会导入 alexa sdk 并且为我们创建一个 alexa 对象以便之后使用。接着,我们需要处理与 skill 交互的️ intent。幸运的是,alexa-sdk 使得在我们想要的意图(Intent)上激活一个函数变得简单。例如,创建一个为 ‘HelloWorldIntent’ 服务的事件管理器,我们只需要简单地用以下代码实现:

var handlers = {

    'HelloWorldIntent': function () {

        this.emit(':tell', 'Hello World!');

                  }

};

注意上面出现的一个新语法规则 “:tell”? alexa-sdk 遵循 tell/ask 的响应方式来生成你的[语音输出回复对象](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference#Response Format)。如果我们想要问用户问题的话,我们需要把以上代码改成:

this.emit(‘:ask’, ’What would you like to do?’, ’Please say that again?’);

事实上,你的 skill 生成的许多回复都遵循一样的语法规则。下面是一些常见的 skill 回复生成的例子:

var speechOutput = 'Hello world!';

var repromptSpeech = 'Hello again!';

this.emit(':tell', speechOutput);

this.emit(':ask', speechOutput, repromptSpeech);

var cardTitle = 'Hello World Card';

var cardContent = 'This text will be displayed in the companion app card.';

var imageObj = {

    smallImageUrl: 'https://imgs.xkcd.com/comics/standards.png',

    largeImageUrl: 'https://imgs.xkcd.com/comics/standards.png'

};

this.emit(':askWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, imageObj);

this.emit(':tellWithCard', speechOutput, cardTitle, cardContent, imageObj);

this.emit(':tellWithLinkAccountCard', speechOutput);

this.emit(':askWithLinkAccountCard', speechOutput);

this.emit(':responseReady'); // 在回复创建之后,返回 Alexa 服务之前被调用。Calls :saveState。

this.emit(':saveState', false); // 事件管理器将 this.attributes 的内容和当前管理器的状态存储到 DynamoDB,然后将之前内置的回复发送到 Alexa 服务。如果你想用别的方式处理持久化状态,可以重写它。其中的第二个属性是可选的并且可以通过将它设置为 ‘true’ 以强制储存。

this.emit(':saveStateError'); // 在存储状态的过程出错时被调用。如果你想自己处理异常的话,可以重写它。

一旦我们创建好事件管理器,在新的 session(NewSession)场景下,我们需要用之前创建的 alexa 对象中的 registerHandlers 函数去注册这些管理器。

exports.handler = function(event, context, callback){

    var alexa = Alexa.handler(event, context);

    alexa.registerHandlers(handlers);

};

你也可以同时注册多个事件管理器。与其创建单个管理器对象,我们创建了一个新的 session,其中有许多处理不同事件的不同管理器,并且我们可以通下面的代码同时注册它们:

    alexa.registerHandlers(handlers, handlers2, handlers3, ...);

你所定义的事件管理器可以相互调用,从而保证你的 skill 的回复是统一的。下面是 LaunchRequest 和 IntentRequest(在 HelloWorldIntent 中)都返回 “Hello World” 消息的一个例子。

var handlers = {

    'LaunchRequest': function () {

        this.emit('HelloWorldIntent');

    },

    'HelloWorldIntent': function () {

        this.emit(':tell', 'Hello World!');

};

一旦你注册了所有的意图管理器函数,你只需要简单地用 alexa 对象里的执行函数去运行 skill 的逻辑就可以了。最后一行代码是这样的:

exports.handler = function(event, context, callback){

    var alexa = Alexa.handler(event, context);

    alexa.registerHandlers(handlers);

    alexa.execute();

};

你可以从 github 上下载完整的示例。我们还提供了最新的基于 Node.js 和 alexa-sdk 开发的 skill 示例:FactHelloWorldHighLowHowToTrivia

让 Skill 的状态管理更简单

alexa-sdk 会根据当前状态把即将接受的 intent 传送给正确的管理器函数。它其实只是 session 属性中一个简单的字符串,用来表示 skill 的状态。在定义 intent 管理器的时候,你也可以通过将表示状态的字符串添加到 intent 的名称后面来模仿这个内置传送的过程,但事实上 alexa-sdk 已经帮你做到了。

比如说,让我们根据上一个管理新的 session 事件的例子,创建一个简单的有“开始”和“猜数”两个状态的猜数字游戏。

var states = {
    GUESSMODE: '_GUESSMODE', // User is trying to guess the number.
    STARTMODE: '_STARTMODE'  // Prompt the user to start or restart the game.
};

var newSessionHandlers = {

 // 以下代码将会切断任何即将输入的 intent 或者启动请求,并且把它们都传送给这个管理器。

  'NewSession': function() {

    this.handler.state = states.STARTMODE;

    this.emit(':ask', 'Welcome to The Number Game. Would you like to play?.');

   }

 };

注意当一个新的 session 被创建时,我们简单地通过 this.handler.state 把 skill 的状态设置为 STARTMODE。此时 skill 的状态将会自动被持久化在 session 的属性中,如果你在 DynamoDB 里设置了表格的话,你可以选择将它持久化于各个 session 当中。

值得注意的是,NewSession 是一个很棒的捕捉各种行为的管理器,同时也是一个很好的 skill 入口,但它不是必需的。NewSession 只会在一个以它命名的函数中被唤醒。你所定义的每一个状态都可以有它们自己的 NewSession 管理器,在你使用内置留存时被唤醒。在上面的例子中,我们可以更加灵活地为 states.STARTMODE 和 states.GUESSMODE 定义不同的 NewSession 行为。

为了定义回复 skill 在不同状态下的 intents,我们需要使用 Alexa.CreateStateHandler 函数。任何在这里定义的 intent 管理器将只会在特定状态下工作,这让我们的开发操作更加灵活!

例如,如果我们在上面定义的 GUESSMODE 状态下,我们想要处理用户对一个问题的回复。这可以通过 StateHandlers 实现,就像这样:

var guessModeHandlers = Alexa.CreateStateHandler(states.GUESSMODE, {

    'NewSession': function () {

        this.handler.state = '';

        this.emitWithState('NewSession'); // 等同于 Start Mode 下的 NewSession handler

    },

    'NumberGuessIntent': function() {

        var guessNum = parseInt(this.event.request.intent.slots.number.value);

        var targetNum = this.attributes["guessNumber"];

        console.log('user guessed: ' + guessNum);



        if(guessNum > targetNum){

            this.emit('TooHigh', guessNum);

        } else if( guessNum < targetNum){

            this.emit('TooLow', guessNum);

        } else if (guessNum === targetNum){

            // 通过一个 callback 函数,用 arrow 函数储存正确的 ‘this’ context

            this.emit('JustRight', () => {

                this.emit(':ask', guessNum.toString() + 'is correct! Would you like to play a new game?',

                'Say yes to start a new game, or no to end the game.');

        })

        } else {

            this.emit('NotANum');

        }

    },

    'AMAZON.HelpIntent': function() {

        this.emit(':ask', 'I am thinking of a number between zero and one hundred, try to guess and I will tell you' +

            ' if it is higher or lower.', 'Try saying a number.');

    },

    'SessionEndedRequest': function () {

        console.log('session ended!');

        this.attributes['endedSessionCount'] += 1;

        this.emit(':saveState', true);

    },

    'Unhandled': function() {

        this.emit(':ask', 'Sorry, I didn\'t get that. Try saying a number.', 'Try saying a number.');

    }

});

另一方面,如果我们在 STARTMODE 状态下,我可以用以下方式定义 StateHandlers:

var startGameHandlers = Alexa.CreateStateHandler(states.STARTMODE, {

    'NewSession': function () {

        this.emit('NewSession'); // 在 newSessionHandlers 使用管理器

    },

    'AMAZON.HelpIntent': function() {

        var message = 'I will think of a number between zero and one hundred, try to guess and I will tell you if it' +

            ' is higher or lower. Do you want to start the game?';

        this.emit(':ask', message, message);

    },

    'AMAZON.YesIntent': function() {

        this.attributes["guessNumber"] = Math.floor(Math.random() * 100);

        this.handler.state = states.GUESSMODE;

        this.emit(':ask', 'Great! ' + 'Try saying a number to start the game.', 'Try saying a number.');

    },

    'AMAZON.NoIntent': function() {

        this.emit(':tell', 'Ok, see you next time!');

    },

    'SessionEndedRequest': function () {

        console.log('session ended!');

        this.attributes['endedSessionCount'] += 1;

        this.emit(':saveState', true);

    },

    'Unhandled': function() {

        var message = 'Say yes to continue, or no to end the game.';

        this.emit(':ask', message, message);

    }

我们可以看到 AMAZON.YesIntent 和 AMAZON.NoIntent 在 guessModeHandlers 对象中是没有被定义的,因为对于该状态来说,“是”或者“不是”的回复是没有意义的。这样的回复将会被 ‘Unhandled’ 管理器捕捉到。

还有就是,注意在 NewSession 和 Unhandled 这两个状态中的不同行为。在这个游戏中,我们通过调用 newSessionHandlers 对象中的 NewSession 管理器“重置” skill 的状态。你也可以跳过这一步,然后 alexa-sdk 将会为当前状态调用 intent 管理器。你只需要记住在调用 alexa.execute() 之前去注册你的状态管理器,否则它们将不会被找到。

所有属性将会在你的 skill 结束 session 时自动保存,但是如果用户自己结束了当前的 session,你需要 emit ‘:saveState’ 事件(this.emit(‘:saveState’, true)来强制保存这些属性。你应该在 SessionEndedRequest 管理器中做这件事,因为 SessionEndedRequest 管理器将会在用户通过“退出”或回复超时结束当前 session 的时候被调用。你可以看看以上的代码示例。

我们将上面的例子写在了一个高/低猜数字游戏中,你可以点击这里下载

通过 Amazon DynamoDB 持久化 Skill 属性

很多人喜欢将 session 属性值储存到数据库中以便日后使用。alexa-sdk 直接结合了 Amazon DynamoDB(一个 NoSQL 的数据库服务)让你只需要几行代码就可以实现属性存储。

简单地在你调用 alexa.execute 之前为 alexa 对象中的 DynamoDB 的表格设置一个名字。

exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.appId = appId;
    alexa.dynamoDBTableName = ’YourTableName'; // That’s it!
    alexa.registerHandlers(State1Handlers, State2Handlers);
    alexa.execute();
};

之后,你只需要调用 alexa 对象的 attributes 为你的属性设置一个值。不再需要其他输入而得到单独的函数!

this.attributes[”yourAttribute"] = ’value’;

你可以提前手动创建一个表格或者为你的 Lambda 函数的 DynamoDB 提供创建表格权限然后一切都会自动生成。不过你要知道,在第一次唤醒 skill 的时候,创建表格可能会花费几分钟的时间。

尝试扩展猜数字游戏:

  • 让它能够储存你每次游戏中所猜的平均数
  • 加入声音效果
  • 给玩家有限的猜数字时间

想要获取更多关于学习使用 Alexa Skills Kit 开发的信息,可以看看下面的链接:

基于 Node.js 的 Alexa Skills Kit
Alexa 开发者播客
Alexa 开发培训
关于 Alexa Skills 的介绍
101 条语音交互设计指南
Alexa Skills Kit (ASK)
Alexa 开发者论坛

-Dave (@TheDaveDev)

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏