C#版Bot Framework v4如何结合对话框与其他命令
问题根因
核心问题是消息路由逻辑顺序错误:
- 所有用户消息进入后,未优先判断是否存在正在执行的多轮对话框,直接将所有输入作为顶层命令做switch匹配
- 用户发送
details启动瀑布对话框后,后续输入的订单号是数字字符串,匹配不到help/orders/details三个预设命令,直接命中default分支,消息根本没有传递给已启动的对话框实例,导致对话流中断 - 另外
OrderDetailDialog中存在一处类型转换bug,即使消息路由修复,执行到第二步也会抛出运行时异常
修复方案
1. 调整TroyBot的消息路由优先级
首先将DialogState存取器提取为类级字段在构造函数中初始化,收到消息后优先判断是否存在活跃对话框,存在则直接将消息交付对话框处理,无活跃对话框时再走顶层命令匹配逻辑。
修正后的TroyBot核心代码:
public class TroyBot<T> : ActivityHandler where T : Dialog { private readonly ConversationState _conversationState; private readonly UserState _userState; private readonly T _dialog; // 提前声明对话框状态存取属性 private readonly IStatePropertyAccessor<DialogState> _dialogStateAccessor; public TroyBot(ConversationState conversationState, UserState userState, T dialog) { _conversationState = conversationState; _userState = userState; _dialog = dialog; // 初始化对话框状态存取器 _dialogStateAccessor = _conversationState.CreateProperty<DialogState>(nameof(DialogState)); } protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken) { foreach (var member in membersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { await turnContext.SendActivityAsync(MessageFactory.Text( "Hi! I'm Troy. I will help you track and manage orders." + "\n\n" + "Say **`help`** to see what I can do." ), cancellationToken); } } } protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken) { // 优先判断是否有正在运行的对话框 var dialogState = await _dialogStateAccessor.GetAsync(turnContext, () => new DialogState(), cancellationToken); // 对话框栈不为空时,直接将当前轮次消息交付给对话框承接流程 if (dialogState.DialogStack.Any()) { await _dialog.RunAsync(turnContext, _dialogStateAccessor, cancellationToken); return; } // 无活跃对话框时才匹配顶层命令 var command = turnContext.Activity.Text.Trim().ToLower(); switch (command) { case "orders": var orders = await GetOrders(); Attachment ordersAttachment = GetOrdersAttachment(orders); await turnContext.SendActivityAsync(MessageFactory.Attachment(ordersAttachment), cancellationToken); break; case "help": var helpText = GetHelpText(); await turnContext.SendActivityAsync(MessageFactory.Text(helpText), cancellationToken); break; case "details": var detailOrders = await GetOrders(); Attachment detailOrdersAttachment = GetOrdersAttachment(detailOrders); await turnContext.SendActivityAsync(MessageFactory.Attachment(detailOrdersAttachment), cancellationToken); await _dialog.RunAsync(turnContext, _dialogStateAccessor, cancellationToken); break; default: await turnContext.SendActivityAsync(MessageFactory.Text( "Hmmm... I don't know how to help you with that.\n\n" + "Try saying `help` to see what I can do." ), cancellationToken); break; } } public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await base.OnTurnAsync(turnContext, cancellationToken); // 保持原有状态保存逻辑即可,不要在此处额外调用dialog.RunAsync,避免重复执行 await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); await _userState.SaveChangesAsync(turnContext, false, cancellationToken); } // 其余自定义工具方法(GetOrders、GetOrdersAttachment、GetHelpText等)保持不变 }
2. 修复OrderDetailDialog的类型转换bug
你使用的是NumberPrompt<int>,其返回结果是int类型值,FoundChoice是ChoicePrompt专用的返回类型,直接强转会抛出类型不匹配异常,修改对应瀑布步代码:
private static async Task<DialogTurnResult> ViewOrderDetailsStep(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // 直接取int类型的订单号 var orderNumber = (int)stepContext.Result; stepContext.Values["orderNumber"] = orderNumber; // 可在此处加入根据orderNumber查询对应订单的逻辑,再生成匹配的详情卡片 return await stepContext.EndDialogAsync(GetOrderDetailsCard(), cancellationToken); }
如果后续需要根据传入的订单号动态生成详情卡片,记得去掉GetOrderDetailsCard()方法的static修饰,接入订单仓储查询逻辑即可。
执行流说明
修复后的运行逻辑:
- 用户发送
details时,对话框栈为空,走对应命令分支,发送订单列表后启动对话框,对话框栈压入对应实例,等待用户输入 - 用户发送订单号时,检测到对话框栈非空,直接将消息交付给对话框,NumberPrompt完成输入校验后进入下一个瀑布步,返回订单详情后清空对话框栈
- 对话框结束后栈为空,后续用户输入会重新进入顶层命令匹配逻辑
内容的提问来源于stack exchange,提问作者Lukas
相关产品推荐
相关产品推荐

