使用 Actions on Google Node.js 客户端库构建 fulfillment (Dialogflow)

如果您要在 JavaScript 中创建执行方式 webhook,建议您使用 Actions on Google Node.js 客户端库访问 Actions on Google 平台并与之互动。

简介

Node.js 客户端库是 Actions on Google 的执行方式库,提供以下功能:

  • 支持 Actions on Google 的所有功能,包括文本和富媒体响应、帐号登录、数据存储、交易等。
  • 在 JavaScript 中提供一个惯用的抽象层,用于封装对话 HTTP/JSON webhook API
  • 处理执行方式与 Actions on Google 平台之间的低层通信细节。
  • 可以使用熟悉的软件包管理工具(例如 npmyarn)进行安装。
  • 可让您在 Cloud Functions for FirebaseAWS Lambda 等无服务器计算平台上轻松部署 fulfillment 网络钩子。您还可以在云服务提供商或自行管理的环境中托管执行方式 webhook。
  • Node.js v6.0.0 及更高版本兼容。

您可以将该客户端库与适用于 Actions on Google 的 Dialogflow 集成Actions SDK 结合使用。

如需查看使用客户端库的完整代码示例,您可以访问示例页面

查看 API 参考文档

API 参考文档托管在 Actions on Google Node.js 客户端库的 GitHub 页面上。

您还可以从下载客户端库代码的目录运行以下命令,生成引用的本地副本:

yarn docs

生成的文档位于您下载客户端库代码的目录的 docs 文件夹中。

了解工作原理

在使用客户端库之前,最好先了解一下执行方式 webhook 如何使用客户端库来处理 Actions on Google 发送到您的执行方式的用户请求。

使用 JavaScript 创建执行方式网络钩子时,您可以在无服务器计算环境(例如 Google 的 Cloud Functions for Firebase 或 AWS Lambda)上部署和托管代码。您也可以使用 Express Web 框架自行托管代码,而无需执行额外的工作。

在运行时环境中,fulfillment webhook 可以调用客户端库中的函数来处理用户请求,并将响应发送回 Actions on Google 以呈现到用户输出中。

下面简要总结了 fulfillment 网络钩子借助客户端库处理的关键任务:

图 1. Node.js 客户端库的概要架构
  1. 接收用户请求:当用户向 Google 助理发出查询时,Actions on Google 平台会向您的执行方式 webhook 发送 HTTP 请求;该请求包含 JSON 载荷,其中包含 intent 和其他数据(例如用户输入的原始文本以及用户设备的 Surface 功能)。如需查看 JSON 载荷内容的更多示例,请参阅 Dialogflow 网络钩子格式对话网络钩子格式指南。
  2. 框架调用格式检测:对于受支持的框架,客户端库会自动检测框架的调用格式(例如,如果请求来自 Express Web 框架或来自 AWS Lambda),并了解如何无缝处理与 Actions on Google 平台的通信。
  3. 服务处理程序处理:该客户端库代表用于 Dialogflow 的对话 HTTP/JSON webhook API 以及 Actions SDK 作为服务函数。您的 fulfillment webhook 使用适当的服务来创建全局 app 实例。app 实例充当 HTTP 请求的处理程序,并了解服务的特定协议。
  4. 对话处理:客户端库将每次对话的信息表示为一个附加到 app 实例的 Conversation 对象。您的执行方式 webhook 可以使用 Conversation 对象检索跨对话存储的数据或状态信息、向用户发送响应或关闭麦克风。
  5. 中间件处理:借助客户端库,您可以创建自己的对话服务中间件,其中包含一个或多个函数,这些函数由您定义客户端库在调用 intent 处理程序之前自动运行。fulfillment 网络钩子可以使用中间件将属性或辅助类添加到 Conversation 对象中。
  6. intent 处理程序处理:借助客户端库,您可以定义执行方式 webhook 可以理解的 intent 的处理程序。对于 Dialogflow,客户端库通过映射到 Dialogflow 控制台中定义的 intent 名称的确切字符串,将请求路由到正确的 intent 处理程序。对于 Actions SDK,该操作根据从 Actions on Google 发送的 intent 属性进行路由。
  7. 向用户发送响应:为了构建响应,执行方式 webhook 会调用 Conversation#ask() 函数。可以多次调用 ask() 函数以逐步构建响应。客户端库将响应序列化为带有 JSON 载荷的 HTTP 请求,并将其发送到 Actions on Google。close() 函数的行为与 ask() 类似,但会关闭对话。

设置本地开发环境

在实现 fulfillment webhook 之前,请确保先安装客户端库。

安装客户端库

如需将客户端库安装到本地开发环境,最简单的方法是使用软件包管理器,例如 npmyarn

如需安装,请在终端运行以下命令之一:

  • 如果使用 npmnpm install actions-on-google
  • 如果使用 yarnyarn add actions-on-google

设置项目文件夹

根据您计划部署 fulfillment webhook 的位置(Google 的 Cloud Functions for Firebase、AWS Lambda 或自托管的 Express),您可能需要创建特定的项目文件夹结构来保存文件。

例如,如果您使用的是 Cloud Functions for Firebase,则可以执行设置 Node.js 和 Firebase CLI 以及初始化 Firebase for Cloud Functions 中所述的步骤来设置所需的项目文件夹。对于 Cloud Functions for Firebase,您通常需要在 /functions/index.js 文件中编写执行方式 webhook。

构建应用实例

Actions on Google 使用特定的消息传递格式与您的 fulfillment webhook 交换请求和响应,具体取决于您是使用 DialogflowActions SDK 构建对话型 Action 还是构建智能家居 Action

为了表示这些不同的请求和响应协议,客户端库提供了三种服务功能:

这两种对话服务(Dialogflow 和 Actions SDK)都使用对话网络钩子协议,但每种服务以不同方式封装消息。

您可以使用服务来创建 app 实例。app 实例封装了网络钩子的全局状态和执行方式逻辑,并使用服务专用协议处理 Actions on Google 与执行方式之间的通信。

您可以配置 app 实例的属性,并调用其方法来指示 fulfillment webhook 的行为。您还可以轻松将 app 实例插入到无服务器计算环境(例如 Cloud Functions for Firebase)中,此类环境接受 JavaScript 函数作为 HTTP 请求的处理程序。

如需在 fulfillment webhook 中构建 app 实例,请按以下步骤操作:

  1. 调用 require() 函数以导入“actions-on-google”模块并加载所需的服务。例如,以下代码段展示了如何加载 dialogflow 服务和一些用于构建响应的元素,并将其赋值给名为 dialogflow 的常量:

    // Import the service function and various response classes
    const {
      dialogflow,
      actionssdk,
      Image,
      Table,
      Carousel,
    } = require('actions-on-google');

    此处,actions-on-google 是指在项目文件夹的 package.json 文件中指定的依赖项(如需查看示例,请参阅package.json 文件示例)。

    获取 app 实例时,您可以视需要指定表示富响应、辅助 intent 以及您要使用的其他 Actions on Google 功能的类。如需查看可加载的有效类的完整列表,请参阅对话响应帮助程序 intent 模块的参考文档。

  2. 通过调用您加载的服务创建一个 app 实例。例如

    const app = dialogflow();

  3. 如需在初始化时配置 app 实例,您可以在调用服务时提供 options 对象作为第一个参数。(如需了解详情,请参阅 DialogflowOptions。) 例如,以下代码段展示了如何通过设置 { debug: true } 标志来记录来自用户请求或响应的原始 JSON 载荷:

const app = dialogflow({
  debug: true
});

为事件设置处理程序

如需在用户与 Action 互动的生命周期内处理客户端库创建的与 Actions on Google 相关的事件,您需要使用客户端库构建处理程序,以处理用户请求并发回响应。

您可以创建函数来充当客户端库可识别的以下主要事件类型的处理程序:

  • intent 事件:intent 是唯一标识符,每当用户请求某些特定功能时,Actions on Google 都会将其发送到您的执行方式。如果您使用的是 Dialogflow,则此操作对应于 Dialogflow 会将用户查询与 Dialogflow 代理中的意图相匹配。
  • 错误事件:当发生 JavaScript 或客户端库错误时,您可以使用 app 实例的 catch 函数来适当地处理错误异常。您应该实现一个 catch 函数来处理您的执行方式关注的所有错误。
  • 后备事件:如果用户发送了 Actions on Google 无法识别的查询,则会发生回退事件。您可以使用 app 实例的 fallback 函数注册通用回退处理程序,如果没有与传入的执行请求匹配的 intent 处理程序,则将触发该处理程序。您应该实现一个 fallback 函数来处理所有回退事件。如果您使用的是 Dialogflow,那么 Dialogflow 可以在没有匹配其他意图时触发特定的后备意图。您应该为该回退 intent 创建相应的 intent 处理程序。

每当用户向您的 Action 发送请求时,app 实例都会创建一个代表该对话会话的 Conversation 对象。您可以通过在 intent 处理程序函数中作为第一个函数参数传递的 conv 变量名称访问此对象。通常,您可以在处理程序中使用 conv 对象向用户发送响应。

用户查询还可以包含您的 Action 可提取并用于优化响应的参数。

  • 如果您使用的是 Actions SDK,则需要在 Action 软件包中定义参数。如需查看如何从 intent 中提取参数的示例,请参阅 Eliza 代码示例
  • 如果您使用的是 Dialogflow,则可以通过 params 变量访问参数值。如需查看在 Dialogflow 中处理含有参数的意图的示例,请参阅访问参数和上下文

为 intent 设置处理程序

如需为 intent 设置处理程序,请调用 app 实例的 intent() 函数。例如,如果您使用的是 Dialogflow,则此参数为 DialogflowApp#intent() 函数。在参数中,指定 intent 名称并提供处理程序函数。

如果您使用的是 Dialogflow,则无需在代理中为每个意图设置处理程序。相反,您可以利用 Dialogflow 的内置响应处理程序来自动处理 intent,而无需实现您自己的处理程序函数。例如,默认欢迎 intent 可以通过这种方式委托给 Dialogflow。

以下示例展示了针对“greeting”和“bye”intent 的 intent 处理程序。其匿名处理程序函数接受 conv 参数,并通过 conv.ask() 函数向用户发回简单的字符串响应:

app.intent('Default Welcome Intent', (conv) => {
  conv.ask('How are you?');
});

app.intent('bye', (conv) => {
  conv.close('See you later!');
});

请注意,close() 函数与 ask() 类似,只不过它会关闭麦克风并且对话结束。

如需详细了解如何为 intent 构建处理程序,请参阅构建 intent 处理程序

为错误事件设置处理程序

如需设置错误处理程序,请调用 app 实例的 catch() 函数。(例如,如果您使用的是 Dialogflow,则此参数为 DialogflowApp#catch() 函数。)

以下示例展示了一个简单的 catch 错误处理程序,该处理程序将错误发送到控制台输出并发回简单的字符串响应,以通过 conv.ask() 函数提示用户:

app.catch((conv, error) => {
  console.error(error);
  conv.ask('I encountered a glitch. Can you say that again?');
});

为回退事件设置处理程序

如需在传入的执行请求没有匹配的 intent 时设置通用回退处理程序,请调用 app 实例的 fallback() 函数。(例如,如果您使用的是 Dialogflow,则此参数为 DialogflowApp#fallback() 函数。)

以下示例展示了一个简单的回退处理程序,该处理程序会发回简单的字符串响应,以通过 conv.ask() 函数提示用户:

app.fallback((conv) => {
  conv.ask(`I couldn't understand. Can you say that again?`);
});

构建 intent 处理程序

本部分介绍了使用客户端库实现 intent 处理程序的一些常见用例。如需查看客户端库如何与 intent 匹配,请参阅了解 intent 处理方式中的“intent 处理程序处理”部分。

访问参数和上下文

如果您使用的是 Dialogflow,则可以在 Dialogflow 代理中定义参数上下文,以维护状态信息和控制对话流。

参数有助于捕获用户查询中的重要字词、短语或值。Dialogflow 在运行时从用户查询中提取相应的参数,您可以在 fulfillment 网络钩子中处理这些参数值,以确定如何响应用户。

每当用户向您的 Action 发送请求时,DialogflowApp 实例都会创建一个 parameters 对象,该对象表示 Dialogflow 从该请求中提取的参数值。此对象可通过 params 变量名称进行访问。

以下代码段展示了如何在用户发送请求时从 params 对象访问 name 属性:

app.intent('Default Welcome Intent', (conv, params) => {
  conv.ask(`How are you, ${params.name}?`);
});

下面是执行相同操作的替代代码段。大括号 ({}) 执行 JavaScript 解构,以便从 parameters 对象获取 name 属性并将其用作局部变量:

app.intent('Default Welcome Intent', (conv, {name}) => {
  conv.ask(`How are you, ${name}?`);
});

在以下代码段中,参数名称为 full-name,但它已进行解构,并被赋予了一个名为 name 的局部变量:

app.intent('Default Welcome Intent', (conv, {'full-name': name}) => {
  conv.ask(`How are you, ${name}?`);
});

上下文是 Dialogflow 的高级功能。您可以使用上下文来管理对话状态、流程和分支。客户端库通过 DialogflowConversation#contexts 对象提供对上下文的访问权限。以下代码段展示了如何在 fulfillment webhook 中以编程方式设置上下文,以及如何检索上下文对象:

app.intent('intent1', (conv) => {
  const lifespan = 5;
  const contextParameters = {
    color: 'red',
  };
  conv.contexts.set('context1', lifespan, contextParameters);
  // ...
  conv.ask('...');
});

app.intent('intent2', (conv) => {
  const context1 = conv.contexts.get('context1');
  const contextParameters = context1.parameters;
  // ...
  conv.ask('...');
});

app.intent('intent3', (conv) => {
  conv.contexts.delete('context1');
  // ...
  conv.ask('...');
});

访问帮助程序 intent 结果

为方便起见,该客户端库提供了辅助 intent 类,用于封装 Action 经常请求的常见用户数据类型。其中包括表示各种 Actions on Google 帮助程序 intent 结果的类。如果您希望 Google 助理处理对话中用户必须提供输入才能继续对话的部分,则可以使用辅助 intent。

示例:确认帮助程序结果

通过确认帮助程序 intent,您可以要求用户确认/否,并获取结果。以下代码段展示了您的网络钩子如何根据确认帮助程序 intent 返回的结果自定义其响应。如需查看更完整的示例,请参阅 Confirmation 类参考文档。

// Create Dialogflow intent with `actions_intent_CONFIRMATION` event
app.intent('get_confirmation', (conv, input, confirmation) => {
  if (confirmation) {
    conv.close(`Great! I'm glad you want to do it!`);
  } else {
    conv.close(`That's okay. Let's not do it now.`);
  }
});

以下代码段展示了 fulfillment webhook 如何根据用户输入的轮播界面内容自定义其响应。借助轮播组件,您的 Action 可以呈现一系列可供用户选择的选项。如需查看更完整的示例,请参阅 Carousel 类参考文档。

app.intent('carousel', (conv) => {
  conv.ask('Which of these looks good?');
  conv.ask(new Carousel({
    items: {
      car: {
        title: 'Car',
        description: 'A four wheel vehicle',
        synonyms: ['automobile', 'vehicle'],
      },
      plane: {
        title: 'Plane',
        description: 'A flying machine',
        synonyms: ['aeroplane', 'jet'],
      }
    }
  }));
});

// Create Dialogflow intent with `actions_intent_OPTION` event
app.intent('get_carousel_option', (conv, input, option) => {
  if (option === 'one') {
    conv.close(`Number one is a great choice!`);
  } else {
    conv.close(`Number ${option} is a great choice!`);
  }
});

配置对话响应对象

客户端库提供对话响应类,用于表示您的 Action 可以发送的丰富响应或多媒体元素。在用户不需要提供任何输入继续对话时,您通常可以发送这些响应或元素。

示例:图片

以下代码段展示了您的执行方式网络钩子如何在响应中发送 Image,该响应将由库自动附加到 BasicCard 响应:

app.intent('Default Welcome Intent', (conv) => {
  conv.ask('Hi, how is it going?');
  conv.ask(`Here's a picture of a cat`);
  conv.ask(new Image({
    url: '/web/fundamentals/accessibility/semantics-builtin/imgs/160204193356-01-cat-500.jpg',
    alt: 'A cat',
  }));
});

进行异步函数调用

Actions on Google Node.js 客户端库专为异步编程而设计。intent 处理程序可以返回一个 promise,在 fulfillment webhook 完成响应生成后进行解析。

以下代码段展示了如何进行异步函数调用以返回 promise 对象,然后在您的执行方式 webhook 收到“greeting” intent 时以消息进行响应。在此代码段中,promise 可确保只有在针对外部 API 调用的 promise 解析后,执行方式 webhook 才会返回对话响应。

在此示例中,我们将使用虚构 API 获取天气数据。

/**
 * Make an external API call to get weather data.
 * @return {Promise<string>}
 */
const forecast = () => {
  // ...
};

app.intent('Default Welcome Intent', (conv) => {
  return forecast().then((weather) => {
    conv.ask('How are you?');
    conv.ask(`Today's weather is ${weather}.`);
  });
});

以下简化的代码段具有相同的效果,但使用了 ECMA 2017 (Node.js 8) 中引入的 async await 功能。要将此代码与 Cloud Functions for Firebase 搭配使用,请确保您使用的是正确版本的 firebase-tools,并进行了正确的配置。

app.intent('Default Welcome Intent', async (conv) => {
  const weather = await forecast();
  conv.ask('How are you?');
  conv.ask(`Today's weather is ${weather}.`);
});

存储对话数据

借助该客户端库,您的 fulfillment webhook 可以将数据保存在对话中以供将来使用。可用于数据存储的关键对象包括:

以下代码段展示了您的执行方式 webhook 如何将数据存储在您定义的任意属性 (someProperty) 中,并将其附加到 Conversation#user.storage 对象。如需查看更完整的示例,请参阅 Conversation#user.storage 类参考文档。

app.intent('Default Welcome Intent', (conv) => {
  conv.user.storage.someProperty = 'someValue';
  conv.ask('...');
});

您可以使用 Conversation#user 对象获取有关用户的信息,包括字符串标识符和个人信息。某些字段(如 conv.user.name.displayconv.user.email)分别要求为 NAME 和 conv.ask(new SignIn) 请求 conv.ask(new Permission) 以访问 Google 登录。

const {Permission} = require('actions-on-google');
app.intent('Default Welcome Intent', (conv) => {
  if (conv.user.last.seen) {
    conv.ask('Welcome back! How are you?');
  } else {
    conv.ask('Nice to meet you! How are you doing?');
  }
});

app.intent('permission', (conv) => {
  conv.ask(new Permission({
    context: 'To greet you personally',
    permissions: 'NAME',
  }));
});

// Create Dialogflow intent with `actions_intent_PERMISSION` event
app.intent('get_permission', (conv, input, granted) => {
  if (granted) {
    conv.close(`Hi ${conv.user.name.display}!`);
  } else {
    // User did not grant permission
    conv.close(`Hello!`);
  }
});

使用中间件进行扩缩

您可以通过中间件扩展客户端库。

中间件层由您定义的一个或多个函数组成,客户端库在调用 intent 处理程序之前自动运行这些函数。使用中间件层,您可以修改 Conversation 实例并添加其他功能。

Dialogflow 和 Actions SDK 服务公开 app.middleware() 函数,您可以通过该函数向 Conversation 实例添加属性或辅助类。

以下代码段举例说明了如何使用中间件:

class Helper {
  constructor(conv) {
    this.conv = conv;
  }

  func1() {
    this.conv.ask(`What's up?`);
  }
}

app.middleware((conv) => {
  conv.helper = new Helper(conv);
});

app.intent('Default Welcome Intent', (conv) => {
  conv.helper.func1();
});

导出应用

如需为 Web 框架或无服务器计算平台公开 fulfillment webhook,您必须将 app 对象导出为可公开访问的 webhook。客户端库开箱即可支持部署到多种环境。

以下代码段展示了如何在不同运行时中导出 app

示例:Cloud Functions for Firebase

const functions = require('firebase-functions');
// ... app code here
exports.fulfillment = functions.https.onRequest(app);

示例:Dialogflow 内嵌编辑器

const functions = require('firebase-functions');

// ... app code here

// Exported function name must be 'dialogflowFirebaseFulfillment'
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

示例:自主托管的 Express 服务器(简单)

const express = require('express');
const bodyParser = require('body-parser');  

// ... app code here

express().use(bodyParser.json(), app).listen(3000);

示例:自托管的 Express 服务器(多个路由)

const express = require('express');
const bodyParser = require('body-parser');

// ... app code here

const expressApp = express().use(bodyParser.json());

expressApp.post('/fulfillment', app);

expressApp.listen(3000);

示例:AWS Lambda API 网关

// ... app code here

exports.fulfillment = app;