public enum KMActionType
{
none = 0,
SearchKeyword,
ReplyDoc
}
[JsonConverter(typeof(KMActionConverter))]
public class KMAction
{
public KMActionType Action { get; set; }
}
//查詢 Keyword 的 Action
public class KMSearchAction : KMAction
{
public KMSearchAction()
{
Action = KMActionType.SearchKeyword;
}
public string Keyword { get; set; }
public int PageIndex { get; set; }
}
//回覆 Mention 的 Action
public class KMReplyMentionAction : KMAction
{
public KMReplyMentionAction()
{
Action = KMActionType.ReplyDoc;
}
//這個是讓 User 輸入的內容,會對應到 TextInput 的 Id
public string ReplyContent { get; set; }
//這個是要回覆所需要的資訊
public KMPost PostInfo { get; set; }
}
//回覆所需要的資訊物件
public class KMPost
{
public string ParentId { get; set; } //這裡指的是 Document Id
// .....
}
////https://blog.mbwarez.dk/deserializing-different-types-based-on-properties-with-newtonsoft-json/
public class KMActionConverter : JsonConverter
{
/// <summary>
/// 依 KMActionType 來決定要轉回什麼物件
/// </summary>
/// <param name="reader"></param>
/// <param name="objectType"></param>
/// <param name="existingValue"></param>
/// <param name="serializer"></param>
/// <returns></returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObject = JToken.ReadFrom(reader);
KMActionType type = jObject["Action"].ToObject<KMActionType>();
KMAction result = null;
switch (type)
{
case KMActionType.SearchKeyword:
result = new KMSearchAction();
break;
case KMActionType.ReplyDoc:
result = new KMReplyMentionAction();
break;
default:
throw new ArgumentOutOfRangeException();
}
serializer.Populate(jObject.CreateReader(), result);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
// Not needed, as we register our converter directly on Vehicle
throw new NotImplementedException();
}
//只做Read,不做Write
public override bool CanWrite => false;
}
//會依 KMActionType 來分別轉成對應的物件 (KMSearchAction or KMReplyMentionAction )
messageAction = JsonConvert.DeserializeObject<KMAction>(dataValue);
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
var searchKeyword = message.Text;
KMAction messageAction = null;
var dataValue = message.Value?.ToString();
//如果 value 有值,就是按 action 進來的
if (!string.IsNullOrWhiteSpace(dataValue))
{
//button 進來的
//會透過 KMActionConverter 來自動轉換
messageAction = JsonConvert.DeserializeObject<KMAction>(dataValue);
}
else
{
//從輸入框進來的 Keyword,所以是在第一頁(base 0)
var pageIndex = 0;
messageAction = new KMSearchAction
{
Action = KMActionType.SearchKeyword,
Keyword = searchKeyword,
PageIndex = pageIndex
};
}
var kmUser = GetKMUser(message);
//依不同的 Action 來產生對應的處理 Class
var actionStrategy = ActionStrategyResolver.ResolveActionStrategy(messageAction.Action);
await actionStrategy.DoAction(context, kmUser, messageAction);
context.Done("");
}
/// <summary>
/// 取得使用者的 UserName
/// </summary>
/// <param name="userToBotMessage"></param>
/// <returns></returns>
private static KMUser GetKMUser(IMessageActivity userToBotMessage)
{
var loginId = new KMUser(userToBotMessage.From.Id);
return loginId;
}
}
筆者建立執行 Action 的 IActionStrategy interface,然後將 Keywrod Search 與 回覆 Mention 分別放到不同的類別之中,並實作 IActionStrategy 。
然後再透過 KMActionType 來決定要生成那個類別,最後執行 DoAction 就可以了 。
/// <summary>
/// 定義 Action 共通的 interface
/// </summary>
public interface IActionStrategy
{
Task DoAction(IDialogContext context, KMUser user, KMAction action);
}
public class ActionStrategyResolver
{
/// <summary>
/// 依不同的 Type 決定要用那個 Class
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IActionStrategy ResolveActionStrategy(KMActionType type)
{
IActionStrategy result = null;
switch (type)
{
case KMActionType.SearchKeyword:
result = new SearchKeywordActionStrategy();
break;
case KMActionType.ReplyDoc:
result = new ReplyMentionActionStrategy();
break;
default:
throw new ArgumentOutOfRangeException();
}
return result;
}
}
/// <summary>
/// 專門處理 KM Keyword Search
/// </summary>
public class SearchKeywordActionStrategy : IActionStrategy
{
/// <summary>
/// 處理Search Keyword動作
/// </summary>
/// <param name="context"></param>
/// <param name="userName"></param>
/// <param name="messageAction"></param>
/// <returns></returns>
public async Task DoAction(IDialogContext context, KMUser kmUser, KMAction messageAction)
{
var action = messageAction as KMSearchAction;
//先做 Action 前的驗證
//1.必需要2個字(以上)
const int lowestLength = 2;
if (string.IsNullOrWhiteSpace(action.Keyword) || action.Keyword.Length < lowestLength)
{
throw new BotException($"查詢文字請至少{lowestLength}個字元,謝謝您!");
}
var attachment = await BuildSearchResultCard(kmUser.LoginId, action);
if (attachment == null)
{
//查不到
throw new BotException($"查不到任到資料,請重新查詢...");
}
var replyMessage = context.MakeMessage();
replyMessage.Attachments.Add(attachment);
await context.PostAsync(replyMessage);
}
/// <summary>
/// 依使用者及Action 建立 Search結果的卡片
/// </summary>
/// <param name="userId"></param>
/// <param name="action"></param>
/// <returns></returns>
private static async Task<Attachment> BuildSearchResultCard(string userId, KMSearchAction action)
{
var pageIndex = action.PageIndex;
var searchKeyword = action.Keyword;
//同時取回 文章及總筆數
var searchResult = SearchKM(searchKeyword, pageIndex);
var searchCount = searchResult.Item2;
//沒任何資料
if (searchCount == 0)
return null;
var card = new AdaptiveCard();
card.Body.Add(new AdaptiveTextBlock()
{
Text = $"查詢「{searchKeyword}」共 {searchCount} 筆,",
Weight = AdaptiveTextWeight.Bolder
});
card.Body.Add(new AdaptiveTextBlock()
{
Text = VitalsESPHelper.GetRangeString(pageIndex, searchCount),
Weight = AdaptiveTextWeight.Bolder
});
//內容
foreach (var doc in searchResult.Item1)
{
card.Body.Add(new AdaptiveTextBlock()
{
//markdown link
Text = $"[{doc.Title}]({doc.Url})",
Weight = AdaptiveTextWeight.Bolder
});
card.Body.Add(new AdaptiveTextBlock()
{
Text = $"...子資訊...",
IsSubtle = true
});
}
//產生上、下一頁
var pageSize = 5;
var totalPage = (searchCount + pageSize - 1) / pageSize;
if (pageIndex > 0)
{
//上一頁
var actionPageIndex = pageIndex - 1;
var preAction = new KMSearchAction
{
Action = KMActionType.SearchKeyword,
Keyword = searchKeyword,
PageIndex = actionPageIndex
};
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = "上一頁",
Data = $"",
DataJson = JsonConvert.SerializeObject(preAction)
});
}
if ((pageIndex + 1) < totalPage)
{
//下一頁
var actionPageIndex = pageIndex + 1;
var nextAction = new KMSearchAction
{
Action = KMActionType.SearchKeyword,
Keyword = searchKeyword,
PageIndex = actionPageIndex
};
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = "下一頁",
Data = $"",
DataJson = JsonConvert.SerializeObject(nextAction)
});
}
var attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card
};
return attachment;
}
//依 keyword 去Search,取回查詢結果
public static async Task<Tuple<List<KMDocument>, int>> SearchKM(string keyword, int pageIndex)
{
var searchCount = search 總筆數;
var kmDocs = search 總文件;
return new Tuple<List<KMDocument>, int>(kmDocs, searchCount);
}
}
public static async Task<Attachment> BuildMentionCard()
{
//顯示 mention reply card
var card = new AdaptiveCard();
// Body content
card.Body.Add(new AdaptiveTextBlock()
{
Text = $"誰誰誰在一篇文件(那篇文章)中提到你",
Weight = AdaptiveTextWeight.Bolder,
Wrap = true
});
//mention 內容
card.Body.Add(new AdaptiveTextBlock()
{
Text = $"...文章內容...",
IsSubtle = true,
Wrap = true,
Separator = true
});
//reply
card.Body.Add(new AdaptiveTextInput()
{
//id要跟 KMReplyMentionAction 中的 ReplyContent 屬性一樣
Id = "replyContent",
IsMultiline = true,
Placeholder = $"訊息請回覆於此",
// IsRequired = true (未來才會實作)
});
var post = 從km取得要回覆的相關資訊;
var replyAction = new KMReplyMentionAction
{
Action = KMActionType.ReplyDoc,
PostInfo = post
};
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = "送出",
DataJson = JsonConvert.SerializeObject(replyAction)
});
var attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card
};
return attachment;
}
處理使用者回覆的 ReplyMentionActionStrategy Class ,只要呼叫 KM API 檢查狀態 OK ,就可以了,如下,
public class ReplyMentionActionStrategy : IActionStrategy
{
/// <summary>
/// 處理快速回覆的Action
/// </summary>
/// <param name="context"></param>
/// <param name="kmUser"></param>
/// <param name="messageAction"></param>
/// <returns></returns>
public async Task DoAction(IDialogContext context, KMUser kmUser, KMAction messageAction)
{
var action = messageAction as KMReplyMentionAction;
var replyMessage = context.MakeMessage();
if (!string.IsNullOrWhiteSpace(action.ReplyContent))
{
//將內容reply進去
var postResult = await 呼叫KMAPI;
replyMessage.Text = $"回覆完成!";
}
else
{
throw new BotException($"您回覆的內容為「空白」,無法回覆!");
}
await context.PostAsync(replyMessage);
}
}
而在上面有些檢查筆者是 throw BotException,然後透過 Bot Framework Custom Error Messages and Exception Handling 來將訊息顯示給使用者,如下,
public class BotException : Exception
{
public BotException()
{}
public BotException(string message) : base(message)
{}
public BotException(string message, Exception inner) : base(message, inner)
{}
}
public class PostUnhandledExceptionToUserOverrideTask : IPostToBot
{
private readonly ResourceManager resources;
private readonly IPostToBot inner;
private readonly IBotToUser botToUser;
private readonly TraceListener trace;
public PostUnhandledExceptionToUserOverrideTask(IPostToBot inner, IBotToUser botToUser, ResourceManager resources, TraceListener trace)
{
SetField.NotNull(out this.inner, nameof(inner), inner);
SetField.NotNull(out this.botToUser, nameof(botToUser), botToUser);
SetField.NotNull(out this.resources, nameof(resources), resources);
SetField.NotNull(out this.trace, nameof(trace), trace);
}
public async Task PostAsync(IActivity activity, CancellationToken token)
{
try
{
await inner.PostAsync(activity, token);
}
catch (Exception ex)
{
try
{
//顯示 Message 給使用者看
await botToUser.PostAsync(ex.Message, cancellationToken: token);
}
catch (Exception inner)
{
trace.WriteLine(inner);
}
throw;
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/8.4.2/markdown-it.js" />
//先發送 typing 的 message
var connector = new GssConnectorClient(new Uri(activity.ServiceUrl));
var isTypingReply = activity.CreateReply();
isTypingReply.Type = ActivityTypes.Typing;
await connector.Conversations.ReplyToActivityAsync(isTypingReply);