UserState 在 onMembersAdded 函数中显示 null
UserState showing null in onMembersAdded function
我的 onMembersAdded
函数中有逻辑来加载用户状态并查看 userData.accountNumber
属性是否存在。如果没有,则使用 运行 和 auth
对话框获取用户帐号。如果属性确实存在,欢迎消息应该在没有提示的情况下显示。
当我在本地测试时,这工作正常。但是当我在 Azure 上测试时,我总是以 !userData.accountNumber
块结束。通过查看控制台日志,我可以看到在 onMembersAdded
函数中,userData
对象显示 {}
。但是在 auth
对话框中,即使我跳过提示(我们允许用户这样做),accountNumber
属性仍然存在于 userState
中(如果之前已输入)。
我唯一能想到的是,以某种方式使用 BlobStorage
作为状态,就像我在 Azure 上所做的那样,以某种方式表现出与我用于本地测试的 MemoryStorage
不同的行为。我认为这可能是一个时间问题,但我正在等待获取用户状态调用,此外,如果我 do 在 auth
对话框中输入帐号,控制台日志立即按照提示显示更新后的帐号,没问题。
编辑: 从下面的评论中可以看出,问题显然出在渠道处理 onMembersAdded
的不同方式上。似乎在模拟器中同时添加了机器人和用户,但在 webchat/directline 上,直到发送第一条消息后才添加用户。这就是我需要解决的问题。
这是构造函数中定义状态变量和 onMembersAdded
函数的代码:
// Snippet from the constructor. UserState is passed in from index.js
// Create the property accessors
this.userDialogStateAccessor = userState.createProperty(USER_DIALOG_STATE_PROPERTY);
this.dialogState = conversationState.createProperty(DIALOG_STATE_PROPERTY);
// Create local objects
this.conversationState = conversationState;
this.userState = userState;
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let member of membersAdded) {
if (member.id === context.activity.recipient.id) {
this.appInsightsClient.trackEvent({name:'userAdded'});
// Get user state. If we don't have the account number, run an authentication dialog
// For initial release this is a simple prompt
const userData = await this.userDialogStateAccessor.get(context, {});
console.log('Members added flow');
console.log(userData);
if (!userData.accountNumber) {
console.log('In !userData.accountNumber block');
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
console.log('In userData.accountNumber block');
var welcomeCard = CardHelper.GetHeroCard('',welcomeMessage,menuOptions);
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
}
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
如果您希望您的 bot 在用户手动发送消息之前使用正确的用户 ID 从 Web Chat 接收对话更新,您有两个选择:
- 不要使用密码连接到 Direct Line,而是使用令牌连接(推荐)。请注意,仅当您在 Generate Token 请求的正文中提供
user
属性 时才有效。
- 让网络聊天自动向机器人发送首字母 activity,这样用户就不必这样做了。这将是对
DIRECT_LINE/CONNECT_FULFILLED
的响应,它可能是一个不可见的事件 activity,因此对于用户来说,它仍然看起来像是对话中的第一个 activity 来自机器人。
如果您选择选项 1,您的机器人将收到一个与机器人和 membersAdded
中的用户的对话更新,activity 的发件人 ID 将是用户 ID。这是理想的,因为这意味着您将能够访问用户状态。
如果您选择选项 2,您的机器人将收到两个对话更新活动。第一个是您现在收到的那个,第二个是带有您需要的用户 ID 的那个。第一次对话更新的有趣之处在于,发件人 ID 是对话 ID 而不是机器人 ID。我认为这是 Web Chat 试图让机器人将其误认为是正在添加的用户,因为 Bot Framework 机器人通常通过检查发件人 ID 是否与正在添加的成员不同来识别对话更新。不幸的是,这可能会导致发送两条欢迎消息,因为很难判断要响应哪个对话更新。
Web Chat 中的对话更新历来不可靠,a series of GitHub issues. Since you may end up having to write channel-aware bot code anyway, you might consider having the bot respond to a backchannel event 在检测到频道是 Web Chat 时而不是对话更新就证明了这一点。这与选项 2 类似,但您会让您的机器人实际响应该事件,而不是因为该事件而发送的对话更新。
根据 Kyle 的回答,我能够解决问题。但是,关于通过令牌启动聊天会话的文档并不完全清楚,所以我想为其他试图解决同样问题的人提供一些指导。
首先,您需要在机器人中创建端点以生成令牌。我最初从 SECRET 启动会话的原因是,当 SECRET 被暴露以生成它时,我没有看到创建令牌的意义。文档中没有明确说明的是,您应该创建一个单独的端点,以便 SECRET 不在浏览器代码中。您 can/should 使用环境变量或密钥库进一步混淆了 SECRET。这是我设置的端点的代码(我从浏览器传入 userId,您稍后会看到)。
server.post('/directline/token', async (req, res) => {
try {
var body = {User:{Id:req.body.userId}};
const response = await request({
url: 'https://directline.botframework.com/v3/directline/tokens/generate',
method: 'POST',
headers: { Authorization: `Bearer ${process.env.DIRECTLINE_SECRET}`},
json: body,
rejectUnauthorized: false
});
const token = response.token;
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
res.write(token);
res.end();
} catch(err) {
console.log(err);
res.setHeader('Content-Type', 'text/plain');
res.writeHead(500);
res.write('Call to retrieve token from Direct Line failed');
res.end();
}
})
您可以在此处 return JSON,但我选择 return 仅将标记作为文本。现在要调用该函数,您需要在部署机器人的任何地方从脚本中访问此端点(假设您使用的是 botframework-webchat CDN)。这是我为此使用的代码。
const response = await fetch('https://YOURAPPSERVICE.azurewebsites.net/directline/token', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({userId:userID})
});
const token = await response.text();
请求正文必须字符串化 JSON。 Fetch
return 将响应作为流,因此您需要使用 .text()
或 .json()
将其转换,具体取决于您从机器人端点发送响应的方式(我使用.text()
)。您需要同时等待 fetch
和 response.text()
。我部署网络聊天的整个脚本都在异步函数中。请注意,如果您像我一样需要它在 IE11 中工作,async/await 将不起作用。完成后,我通过 Babel 运行 整个代码处理了这个问题,它似乎工作正常。
我的 onMembersAdded
函数中有逻辑来加载用户状态并查看 userData.accountNumber
属性是否存在。如果没有,则使用 运行 和 auth
对话框获取用户帐号。如果属性确实存在,欢迎消息应该在没有提示的情况下显示。
当我在本地测试时,这工作正常。但是当我在 Azure 上测试时,我总是以 !userData.accountNumber
块结束。通过查看控制台日志,我可以看到在 onMembersAdded
函数中,userData
对象显示 {}
。但是在 auth
对话框中,即使我跳过提示(我们允许用户这样做),accountNumber
属性仍然存在于 userState
中(如果之前已输入)。
我唯一能想到的是,以某种方式使用 BlobStorage
作为状态,就像我在 Azure 上所做的那样,以某种方式表现出与我用于本地测试的 MemoryStorage
不同的行为。我认为这可能是一个时间问题,但我正在等待获取用户状态调用,此外,如果我 do 在 auth
对话框中输入帐号,控制台日志立即按照提示显示更新后的帐号,没问题。
编辑: 从下面的评论中可以看出,问题显然出在渠道处理 onMembersAdded
的不同方式上。似乎在模拟器中同时添加了机器人和用户,但在 webchat/directline 上,直到发送第一条消息后才添加用户。这就是我需要解决的问题。
这是构造函数中定义状态变量和 onMembersAdded
函数的代码:
// Snippet from the constructor. UserState is passed in from index.js
// Create the property accessors
this.userDialogStateAccessor = userState.createProperty(USER_DIALOG_STATE_PROPERTY);
this.dialogState = conversationState.createProperty(DIALOG_STATE_PROPERTY);
// Create local objects
this.conversationState = conversationState;
this.userState = userState;
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let member of membersAdded) {
if (member.id === context.activity.recipient.id) {
this.appInsightsClient.trackEvent({name:'userAdded'});
// Get user state. If we don't have the account number, run an authentication dialog
// For initial release this is a simple prompt
const userData = await this.userDialogStateAccessor.get(context, {});
console.log('Members added flow');
console.log(userData);
if (!userData.accountNumber) {
console.log('In !userData.accountNumber block');
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
console.log('In userData.accountNumber block');
var welcomeCard = CardHelper.GetHeroCard('',welcomeMessage,menuOptions);
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
}
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
如果您希望您的 bot 在用户手动发送消息之前使用正确的用户 ID 从 Web Chat 接收对话更新,您有两个选择:
- 不要使用密码连接到 Direct Line,而是使用令牌连接(推荐)。请注意,仅当您在 Generate Token 请求的正文中提供
user
属性 时才有效。 - 让网络聊天自动向机器人发送首字母 activity,这样用户就不必这样做了。这将是对
DIRECT_LINE/CONNECT_FULFILLED
的响应,它可能是一个不可见的事件 activity,因此对于用户来说,它仍然看起来像是对话中的第一个 activity 来自机器人。
如果您选择选项 1,您的机器人将收到一个与机器人和 membersAdded
中的用户的对话更新,activity 的发件人 ID 将是用户 ID。这是理想的,因为这意味着您将能够访问用户状态。
如果您选择选项 2,您的机器人将收到两个对话更新活动。第一个是您现在收到的那个,第二个是带有您需要的用户 ID 的那个。第一次对话更新的有趣之处在于,发件人 ID 是对话 ID 而不是机器人 ID。我认为这是 Web Chat 试图让机器人将其误认为是正在添加的用户,因为 Bot Framework 机器人通常通过检查发件人 ID 是否与正在添加的成员不同来识别对话更新。不幸的是,这可能会导致发送两条欢迎消息,因为很难判断要响应哪个对话更新。
Web Chat 中的对话更新历来不可靠,a series of GitHub issues. Since you may end up having to write channel-aware bot code anyway, you might consider having the bot respond to a backchannel event 在检测到频道是 Web Chat 时而不是对话更新就证明了这一点。这与选项 2 类似,但您会让您的机器人实际响应该事件,而不是因为该事件而发送的对话更新。
根据 Kyle 的回答,我能够解决问题。但是,关于通过令牌启动聊天会话的文档并不完全清楚,所以我想为其他试图解决同样问题的人提供一些指导。
首先,您需要在机器人中创建端点以生成令牌。我最初从 SECRET 启动会话的原因是,当 SECRET 被暴露以生成它时,我没有看到创建令牌的意义。文档中没有明确说明的是,您应该创建一个单独的端点,以便 SECRET 不在浏览器代码中。您 can/should 使用环境变量或密钥库进一步混淆了 SECRET。这是我设置的端点的代码(我从浏览器传入 userId,您稍后会看到)。
server.post('/directline/token', async (req, res) => {
try {
var body = {User:{Id:req.body.userId}};
const response = await request({
url: 'https://directline.botframework.com/v3/directline/tokens/generate',
method: 'POST',
headers: { Authorization: `Bearer ${process.env.DIRECTLINE_SECRET}`},
json: body,
rejectUnauthorized: false
});
const token = response.token;
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
res.write(token);
res.end();
} catch(err) {
console.log(err);
res.setHeader('Content-Type', 'text/plain');
res.writeHead(500);
res.write('Call to retrieve token from Direct Line failed');
res.end();
}
})
您可以在此处 return JSON,但我选择 return 仅将标记作为文本。现在要调用该函数,您需要在部署机器人的任何地方从脚本中访问此端点(假设您使用的是 botframework-webchat CDN)。这是我为此使用的代码。
const response = await fetch('https://YOURAPPSERVICE.azurewebsites.net/directline/token', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({userId:userID})
});
const token = await response.text();
请求正文必须字符串化 JSON。 Fetch
return 将响应作为流,因此您需要使用 .text()
或 .json()
将其转换,具体取决于您从机器人端点发送响应的方式(我使用.text()
)。您需要同时等待 fetch
和 response.text()
。我部署网络聊天的整个脚本都在异步函数中。请注意,如果您像我一样需要它在 IE11 中工作,async/await 将不起作用。完成后,我通过 Babel 运行 整个代码处理了这个问题,它似乎工作正常。