UserState 在 onMembersAdded 函数中显示 null

UserState showing null in onMembersAdded function

我的 onMembersAdded 函数中有逻辑来加载用户状态并查看 userData.accountNumber 属性是否存在。如果没有,则使用 运行 和 auth 对话框获取用户帐号。如果属性确实存在,欢迎消息应该在没有提示的情况下显示。

当我在本地测试时,这工作正常。但是当我在 Azure 上测试时,我总是以 !userData.accountNumber 块结束。通过查看控制台日志,我可以看到在 onMembersAdded 函数中,userData 对象显示 {}。但是在 auth 对话框中,即使我跳过提示(我们允许用户这样做),accountNumber 属性仍然存在于 userState 中(如果之前已输入)。

我唯一能想到的是,以某种方式使用 BlobStorage 作为状态,就像我在 Azure 上所做的那样,以某种方式表现出与我用于本地测试的 MemoryStorage 不同的行为。我认为这可能是一个时间问题,但我正在等待获取用户状态调用,此外,如果我 doauth 对话框中输入帐号,控制台日志立即按照提示显示更新后的帐号,没问题。

编辑: 从下面的评论中可以看出,问题显然出在渠道处理 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 接收对话更新,您有两个选择:

  1. 不要使用密码连接到 Direct Line,而是使用令牌连接(推荐)。请注意,仅当您在 Generate Token 请求的正文中提供 user 属性 时才有效。
  2. 让网络聊天自动向机器人发送首字母 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 运行 整个代码处理了这个问题,它似乎工作正常。