ask-sdk V2 for node.js 開発メモ

ask-sdk も node.js も分からない状態からスキル開発を行いました。その時のメモです。
誰かの役に立つかもしれないのでまとめ方雑ですが載せます。

node.js

strict mode

コードの先頭に"use strict";と記述があるかもしれません。 これは通常よりも厳しくコードのチェックを行う宣言で、エラーではないけど落とし穴になりそうな内容をエラーとして扱います。

具体的には以下のように扱いが変わります。

  • 従来は受け入れていた一部のミスをエラーへ変換
    • 偶発的にグローバル変数を作成できないようにします
    • 代入文で暗黙的に失敗せずに例外が発生するようにします
    • 削除できないプロパティを削除しようとするとエラーが発生します
  • 変数の使用の単純化
    • with を禁止します
    • eval は新しい変数を周囲のスコープに広めません
    • 単純名の削除を禁止します
  • eval および arguments の単純化
    • eval および arguments という名前に対して言語構文でのバインドや代入を不可にします
    • 内部で作成された arguments オブジェクトのプロパティがエイリアスになりません
    • rguments.callee をサポートしません
  • JavaScript の "セキュア化"
    • this として関数に渡された値をオブジェクトへボクシングしません
    • ECMAScript の一般的な実装である拡張を通して JavaScript のスタックを "渡り歩く" ことができません
    • arguments は対応する関数の呼び出し時の変数にアクセスできません
  • 将来の ECMAScript への準備
    • 識別子 implements、interface、let、package、private、protected、public、static、yield を予約語にします
    • スクリプトのトップレベルまたは関数内にない function 文を禁止します

スキルに対してもこの機能を有効にするかどうかの判断が必要になります。

参考 - https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode - https://www.sejuku.net/blog/58342

var

変数の宣言。関数のスコープで定義する。 最近(ES6)では殆ど使ってないっぽい。 基本は後述するletconstを使う。

let

変数の宣言。ブロックのスコープで定義する。 代入のし直しが可能。 再宣言ができない。エラーとなる。

const

変数の宣言。ブロックのスコープで定義する。 再代入による変更はできず、再宣言もできない。

startsWith

文字列が特定の文字列で始まるかどうかを判断する。 (英文字の)大文字・小文字を区別する。

parseInt

文字列を整数に変換する。

parseInt()関数は第1引数を文字列に変換し、解析したうえで、整数またはNaNを返します。戻り値は、NaNでなければ、第1引数のstringを第2引数radixの基数によって示す10進数の整数です。たとえば、radixが10なら10進数、8は8進数、16であれば16進数による変換を意味します。10以上の基数については、9より大きい数字はアルファベットで示されます。たとえば、16進数(基数16)ではAからFが用いられます。

indexOf

indexOf() メソッドは、呼び出す String オブジェクト中で、 fromIndex から検索を始め、指定された値が最初に現れたインデックスを返します。値が見つからない場合は -1 を返します。

例)

var paragraph = 'The quick brown fox jumps over the lazy dog. If the dog barked, was it really lazy?';

var searchTerm = 'dog';
var indexOfFirst = paragraph.indexOf(searchTerm);

console.log('The index of the first "' + searchTerm + '" from the beginning is ' + indexOfFirst);
// expected output: "The index of the first "dog" from the beginning is 40"

console.log('The index of the 2nd "' + searchTerm + '" is ' + paragraph.indexOf(searchTerm, (indexOfFirst + 1)));
// expected output: "The index of the 2nd "dog" is 52"

IsMatch

値がMapオブジェクトとして分類されているかどうかを確認します。 https://www.npmjs.com/package/lodash.ismatch

Array.prototype.join()

join() メソッドは、配列 (または配列風オブジェクト) の全要素を順に連結した文字列を新たに作成して返します。区切り文字はカンマ、または指定された文字列です。

var elements = ['Fire', 'Wind', 'Rain'];

console.log(elements.join());
// expected output: "Fire,Wind,Rain"

console.log(elements.join(''));
// expected output: "FireWindRain"

console.log(elements.join('-'));
// expected output: "Fire-Wind-Rain"

... (ドット3つ)

ドットが3つ連続しているものをスプレッド構文と呼ぶ。 順番に値が取り出されて、個数分の要素が該当部分に入るような配列を作成できる。

var ary = [0, "A", false];
var str = "あいう";
var connectedAry = [...ary, ...str];
console.log(connectedAry);
/*
  [0, "A", false, "あ", "い", "う"]
*/

サンプル

audioData = [
  {
    title: 'Episode 139',
    url: 'test1.url',
  },
  {
    title: 'Episode 140',
    url: 'test2.url',
  },
];

const ary1 = audioData;
console.log(ary1);
// output
// [ { title: 'Episode 139', url: 'test1.url' },
//   { title: 'Episode 140', url: 'test2.url' } ]

const ary2 = [...audioData];
console.log(ary2);
// output
// [ { title: 'Episode 139', url: 'test1.url' },
//   { title: 'Episode 140', url: 'test2.url' } ]

console.log(audioData.keys());
// output
// Object [Array Iterator] {}

const ary3 = [...audioData.keys()];
console.log(ary3);
// output
// [ 0, 1 ]

Array.from()

配列型 (array-like) や反復型 (iterable) オブジェクトから、新しい Array インスタンスを生成します。

const str = "あいう";
const ary = Array.from(str);
console.log(ary);
// output
// [ 'あ', 'い', 'う' ]

サンプル

const aryfrom1 = [...Array("A","B","C")];
console.log(aryfrom1);
// output
// [ 'A', 'B', 'C' ]

const aryfrom2 = [...Array("A","B","C")].length;
console.log(aryfrom2);
// output
// 3

const aryfrom3 = [...Array(3)]
console.log(aryfrom3);
// output
// [ null, null, null ]

const aryfrom4 = [...Array("A","B","C")].length;
const aryfrom5 = [...Array(aryfrom4).keys()];
console.log(aryfrom5);
// output
// [ 0, 1, 2 ]

async

Node.jsのasyncは非同期処理を可読性の高いコードで実装できます。

非同期処理とはページが更新された際などに、更新前と更新後を比較して足りない部分だけをデータ通信する処理のことです。 この非同期処理の事をフロントエンド側の処理ではAjaxと呼ぶ事もあります。

スキルの場合はasyncを handler に設定して、awaitを handler 内の DynamoDB 等と通信する部分に設定すると非同期処理ができる。

== (等価演算子)

== は、文字列と数値の比較の場合、文字列を数値に変換してくれる。

数値と文字列を比較するとき、文字列は数値に変換されます。JavaScript は文字列の数値リテラルを Number 型の数値に変換しようと試みます。最初に、その文字列の数値リテラルから数学的な値を引き出します。次に、最も近い Number 型の値にこの値を丸めます。

=== (厳密等価演算子)

オペランド同士が、型を変換することなく厳密に等しいならば真を返します。

Object.prototype.hasOwnProperty()

オブジェクトが指定されたプロパティを持っているかどうかを示す真偽値を返します。

下記の例では、propという名前のプロパティをオブジェクトが含むか否かを確認しています。

    o = new Object();
    o.prop = 'exists';
    function changeO() {
      o.newprop = o.prop;
      delete o.prop;
    }
    o.hasOwnProperty('prop');   // returns true
    changeO();
    o.hasOwnProperty('prop');   // returns false

Object.getOwnPropertyNames()

与えられたオブジェクトで発見されたすべてのプロパティ (列挙可能・不可能を問わず) の配列を返します。

const object1 = {
  a: 1,
  b: 2,
  c: 3
};

console.log(Object.getOwnPropertyNames(object1));
// expected output: Array ["a", "b", "c"]

Array.length

length プロパティは配列の要素数を取得します。これは符号なし32bitの整数で、常に配列内インデックスの最大値よりも大きな数値になっています。

サンプル

var clothing = ['shoes', 'shirts', 'socks', 'sweaters'];

console.log(clothing.length);
// expected output: 4
var clothing = [{PrefectureName: "北海道", Romanization: "hokkaido", PrefecturalOfficeLocation: "札幌", PrefectureFlower: "ハマナス", PrefectureOrder: 1 },
                {PrefectureName: "鹿児島県", Romanization: "kagoshima", PrefecturalOfficeLocation: "鹿児島", PrefectureFlower: "ミヤマキリシマ", PrefectureOrder: 46 },
                {PrefectureName: "沖縄県", Romanization: "okinawa", PrefecturalOfficeLocation: "那覇", PrefectureFlower: "デイゴ", PrefectureOrder: 47 }
                ];

console.log(clothing.length);
// expected output: 3

Array.keys()

keys() メソッドは、配列の各インデックスのキーを含む、新しい Array Iterator オブジェクトを返します。

var array1 = ['a', 'b', 'c'];
var iterator = array1.keys();

for (let key of iterator) {
  console.log(key);
}

// output
// > 0
// > 1
// > 2

Object.keys()

Object.keys() メソッドは、指定されたオブジェクトが持つ names プロパティの配列を、通常のループで取得するのと同じ順序で返します。

const object1 = {
  a: 'somestring',
  b: 42,
  c: false
};

console.log(Object.keys(object1));
// expected output: Array ["a", "b", "c"]

for...of

オブジェクトの一覧をループで出力。

for...of 文は、iterableオブジェクトに対して反復的な処理をするループを作成します

let iterable = [10, 20, 30];

for (let value of iterable) {
  value += 1;
  console.log(value);
}
// output
// 11
// 21
// 31


let hoge = [{foo:10}, {bar:20}, {baz:30}];

for (let value of hoge) {
  console.log(value);
}
// output
// [object Object]
// [object Object]
// [object Object]

for (let value of hoge.keys()) {
  console.log(value);
}
// output
// 0
// 1
// 2

オブジェクトとプロパティ

以下はmyCarというオブジェクトの生成です。

let myCar = new Object();

以下のようにしてプロパティを定義することが可能です。

myCar.make = 'Ford';
myCar.model = 'Mustang';
myCar.year = 1969;

console.log(myCar)
// output
// {
//   make: "Ford" ,
//   model: "Mustang" ,
//   year: 1969
// }

未定義のプロパティはundefinedになります。

console.log(myCar.color)
// output
// undefined

オブジェクトのプロパティの名前には、あらゆる有効な JavaScript 文字列(空文字列を含む)か、文字列に変換できるものが使用できます。
しかしながら、JavaScript 識別子として有効ではないプロパティ名(例えば空白やダッシュを含んでいたり、数字で始まったりするプロパティ名)には、ブラケット(角括弧)表記法でのみアクセスできます。
この表記法はプロパティ名を動的に決める場合(プロパティ名が実行時に決まる場合)に便利です。

モジュール

moment

日時を取得するために使用。 https://momentjs.com/

ask-sdk V2

canHandle

Alexa SDK V2の、リクエストハンドラーとエラーハンドラーのインターフェースのメソッド。
SDKによって呼び出され、指定されたハンドラーが受け取ったリクエストやエラーを処理できるかどうかを判断します。

ex) HelloWorldIntentHandlerの処理を、HelloWorldIntentを受け取った時に呼び出す記述。

const HelloWorldIntentHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'IntentRequest' &&
           request.intent.name === 'HelloWorldIntent';
  },
  handle(handlerInput) {
    const speechText = 'こんにちは';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard('Hello World', speechText)
      .getResponse();
  }
};

Playbackディレクティブ

PlaybackStopped

「アレクサ」と声をかけ、アレクサが待ち受け状態になったときに送信される。

PlaybackController

PlaybackControllerリクエストに応答する場合、AudioPlayerディレクティブでしか応答できません。
応答には、outputSpeech、card、repromptなどの標準のプロパティはいずれも含めることができません。
サポートされていないプロパティを含む応答を送信すると、エラーが発生します。
(次へ前へのボタンを押した動作には発話させられずシンプルカードの表示もできない)

LaunchIntent

スキルの呼び出し時にパラメーターも付与していたときは、LaunchIntentの処理をスキップして後続のパラメーターを処理するHandlerを呼び出す。
LaunchIntentに必要な処理を定義していると、スキップ時は意図しない状態でHandlerを呼び出すこともあるので注意。

Launch時にshouldEndSessionフラグをtrueにするとError retrieving device rendering.が返される?
https://github.com/alexa/skill-sample-nodejs-audio-player/issues/144

カスタムインテント

標準ビルトインインテントと同じフレーズを登録可能。
標準ビルトインインテントと競合するフレーズを登録していた場合、優先して処理される。
標準ビルトインインテントの方が幅広い範囲を網羅できるため、競合させない方が良い。
AudioPlayerを実行した後は受付できない。

標準ビルトインインテント

カスタムインテントより処理の優先度が低い。 インテントによってフレーズの拡張ができるもの、できないものがある。

AudioPlayerインタフェースを有効にすると、ビルトインインテントインテントに登録していなくても 受け付ける。

インテントのサンプル発話に登録できない言葉

仕様で登録できない言葉がある。

  • 終了
  • その他終了に近しい表現

Dialogモデル

slot値を満たせなかったときの発話をskill側で定義して対応可能。 MP3は流せない。

addRequestInterceptors

前処理(RequestInterceptor)を追加します。
LaunchRequestHandlerのcanHandleメソッド ⇒ 前処理 ⇒ LaunchRequestHandlerのhandleメソッド の順で処理。
インテント処理も呼び出すたびに実行される。

addResponseInterceptors

後処理(ResponseInterceptor)を追加します。 handleメソッド ⇒ 後処理 ⇒ 返事。 エラーインテントに振り分けられた場合は実行されていない様子。 正常時はインテント処理を呼び出すたびに実行される。

SSML audioタグ

  • 5つ以上のaudioタグを埋め込んだlambda関数を実行するとスキルの起動に失敗する。
  • 音声再生が240秒を超える音声を再生するとスキルが正しく応答しないというエラーを報告してスキルが終了する。(オーディオタグの再生時に音声の長さを把握しているように見受けられる)
  • 音声再生中でもリプロンプトの応対可能。

SSML breakタグ

連続して複数使用できる?

スキルの待ち受け時間

  • アレクサはユーザからの返答(リプロンプト)を7~8秒待つ
  • その後、リプロンプト処理を行い再度7~8秒待つ
  • 応答がなければスキルを終了する
  • リプロンプト回数は変更できない

アレクサ(スキル) からユーザーへの返答時間

8秒。

プログレッシブ応答により、応答に使える総時間が変わるわけではありません。ユーザーがスキルを呼び出すと、スキルは約8秒以内に完全な応答を返す必要があります。スキルは完全な応答だけでなく、プログレッシブ応答の処理についてもこの時間内に終了する必要があります。 プログレッシブ応答の送信手順

https://developer.amazon.com/ja/docs/custom-skills/send-the-user-a-progressive-response.html

attributes の管理

async/await を使ったattributesの操作を行うときは処理の順序を意識しないと変数が空のままなので注意。

永続アトリビュートのキー

DynamoDB側ではAlexaデバイスにログインしているuserIdをプライマリパーティションキーとする。

ダイアログモード

Dialogインターフェースは、スキルとユーザーとの間のマルチターンの会話を管理するためのディレクティブを提供します。ユーザーのリクエストに応えるために必要な情報をユーザーにたずねるときに使用できます

AudioPlayerインタフェース

metadata の設定

  • addAudioPlayerPlayDirective は6つ目の引数に metadata パラメータを付与することで、画面付き端末にタイトル情報や画像を表示できる。
  • SDKのバージョンが古いと metadata を取得しない。(Lambda のfactテンプレートで設定するSDKは古いバージョンのため注意すること。自分はこれでハマった。)
  • 画像の表示はキャッシュを利用して行う。キャッシュはtokenの値に対応しており、同じtokenを使いまわすと意図しない画像を表示する可能性がある。

特定のインテントに該当しないインテントの処理

ErrorHandlerで処理する。

リクエストタイプ

PlaybackController.PlayCommandIssued

ユーザーが再生を開始また再開するためにインテントで「再生」または「再開」ボタンを使用した場合に送信されます。

System.ExceptionEncountered

PlaybackControllerリクエストに対する応答が原因でエラーが発生した場合、System.ExceptionEncounteredリクエストがスキルに送信されます。応答に含まれるディレクティブはすべて無視されます。

発話が一致しない場合にフォールバックを提供する

ユーザーの音声入力がスキルの他のインテントとまったく一致しない場合、AMAZON.FallbackIntent(英語を使用するロケールとドイツ語で利用可能)がトリガーされます。

https://developer.amazon.com/ja/docs/custom-skills/standard-built-in-intents.html#fallback

現状は日本では使用できないため、登録したサンプルと似ない発話もカスタムインテントで処理されることは仕様。

エラー

Unsupported Directive

AudioPlayer is currently an unsupported namespace. Check the device log for more information.

AlexaシミュレータでAudioPlayerによる音声再生をしようとすると発生。 AlexaシミュレータがAudioPlayerインタフェースの再生に対応していないため実機のechoを使う必要がある。

RequestHandlerChain not found!

条件に合致するHandlerが無いと発生する。 スキルのI/O入力とLambdaで定義しているHandlerの条件を確認し、意図しないパラメーターを保持していないか確認する。

Cannot read property 'trim' of undefined

returnで返す値に未定義のものがあると発生する。 repromptの定義忘れとかありませんか?

Task timed out after 3.00 seconds

Lambdaがタイムアウトしている。 Lambdaのタイムアウト時間を調整してあげよう。

Unable to find a suitable request handler.

不明。

DynamoDB

DynamoDB.DocumentClient

SDK

DynamoDBはテーブル作成時にプライマリキーとして以下の2つのキーを登録できる。

DynamoDBからGetを行うときはプライマリキーを指定する。 パーティションキーのみを定義した場合はパーティションキーのみの指定で良いが、 ソートキーも定義した場合は、パーティションキーおよびソートキーの指定が必要になる。