精選

園丁的話

歡迎到叡揚第一次嘗試的部落格分享,這也是我們從鼓勵內部分享後邁出的另一大步。

不同於公司公開的官方文件或是網頁資料,這些部落格來自公司現在與過去的員工的個人意見 ── 它們表達我們在工作中智慧上的領悟與挫折,或是情感上的喜樂與哀愁。

繼續閱讀 “園丁的話”

利用 Install-WindowsFeature 安裝角色服務或功能

通常在架設Server時,會透過伺服器管理員來新增角色及功能,這時,就準備要開始經歷眼花撩亂的勾選人生
有的項目還是動態長,所以會在伺服器角色、功能來來回回
若公司開發機勾選一台,客戶正式、測試各一台
這樣就需要痛苦三次,如果某次在勾選時,真的不小心漏勾了一個項目,
搞不好網站還架不起來。
在某次演講,聽到某個部門是利用PowerShell來安裝這些角色及功能,
只需要將勾選起的組態設定匯出,接著,就利用 Install-WindowsFeature指令來完成安裝動作,相當的快速且方便
剛好自己也準備裝機,想說就來試試看這個方法。

繼續閱讀 “利用 Install-WindowsFeature 安裝角色服務或功能”

您的網路銀行應用程式有多安全?

撰文及整理:叡揚資訊資安事業處

數位化是現代銀行不可或缺的趨勢。幾乎每家銀行都提供網站和行動APP,民眾的接受度也越來越高。最近的一項PWC調查(註1)發現,46%的消費者使用網路銀行,這比之前2012年調查的只有27%大幅增加。

對於銀行和其他金融機構而言,也必需因應這個趨勢提供網路銀行或行動銀行APP資訊。用戶渴望隨時隨地享受銀行業務帶來的便利,雖然能夠在個人的行動裝置或電腦上執行銀行服務所帶來的優勢是不可否認的,但另一個問題仍然存在:這些網路銀行APP安全嗎?

2017年底發布的研究可能會回答這個問題。這項研究在伯明罕(Birmingham)大學進行,發現行動銀行APP中的安全問題可能使數百萬用戶受到攻擊。他們發現的主要問題與憑證綁定有關,從The Register(註2)文章可以發現:系統測試未能發現「嚴重的漏洞,可能讓攻擊者控制受害者的網路銀行」。

這樣的安全問題顯示了在開發數位銀行APP時需要考量所有基礎設施。雖然數位銀行可以幫助培養新客源並提供創新的服務,為金融機構帶來更多的收益,同時數位銀行也存在很大的風險。

對於銀行APP開發人員的七個關鍵步驟

當使用者擁有不錯的安全意識,習慣使用安全的wifi去存取網路銀行app,且設定於不使用裝置時鎖定系統、用獨立且唯一的密碼,且不在公用的電腦上登入時;那確保使用者安全的重責大任就落於應用系統開發人員與團隊身上。

軟體開發生命週期(SDLC)對於安全應用程式的持續開發至關重要。SDLC將制定一項戰略,確保產品兼顧安全性與開發效率。以下是SDLC中的七個關鍵步驟,開發人員和安全團隊應共同努力確保發布安全的網路銀行與行動銀行APP。

1.定義安全需求

  第一步是了解銀行應用程式的安全需求。由於銀行APP的敏感性,分配至少一位安全團隊成員與系統建置團隊合作非常重要,這種合作關係需要在實際開發之前開始。
  在SDLC的流程中,開發人員和安全人員應識別軟體中的關鍵安全風險,包括軟體必須遵循的標準(包含組織內和法律/法規)。而開發團隊與安全團對應明確的溝通,確保流程中的任何差異。只有安全需求得到雙方同意,才能開始進行開發。

2.分析攻擊層面(註3)

任何軟體都會具有風險區域,稱為可攻擊面(Attack Surface)。OWASP將此定義為:

  1. 資料/命令存取應用程式的所有路徑的總和
  2. 保護這些路徑的程式碼
  3. 應用程式中使用的資料,包括機密和金鑰,智慧財產權,關鍵商業資料與個資
  4. 保護處理這些資料的程式碼

各種組織都提出了量化可攻擊面的方法:微軟 Michael Howard 設計了相對攻擊層面商數(Relative Attack Surface Quotient);Carnegie Mellon的研究人員創立了攻擊面度量法(Attack Surface Metric)。在設計最適合單位內部的方法時,可參考上述的方法可能會有所幫助。

分析攻擊層面是保護任何應用程式的一個複雜但至關重要的部分,因為它可以確定程式碼中最關鍵和最易受攻擊的區域,以確保它們在開發和測試期間得到適當的保護。

3.實作威脅模型分析(註4)

SSDLC的下一步是執行威脅模型分析。威脅模型分析可幫助開發人員了解應用程式需要哪些安全控制,以進一步確保從一開始就構建安全性。威脅模型分析過程涉及定義需要保護哪些資產和資源、獲取這些資產和資源的入口和接入點,以及對每個被優先排序的威脅制定管控計畫。

威脅模型分析和攻擊層面分析之間的關係非常緊密,這意味著對攻擊層面的任何更改都需要進行威脅模型分析,而威脅模型分析可以幫助利益相關者更容易了解攻擊層面。

4.執行靜態分析安全測試(註5)

最基本的靜態分析安全測試(SAST)測試應用程式的源碼以檢測關鍵漏洞。進階SAST工具以自動化執行,從源頭保護程式碼,並可在開發過程的初始階段實施。SAST測試是SDLC的一個重要部分,因為它可以在應用程式進入正式環境之前檢測到漏洞,這意味著可以最簡單及最便宜的找到並修復錯誤。SAST的工作原理是將開發人員指向程式碼中存在問題的特定區域。

除了檢測OWASP Top 10 中包含的漏洞之外,SAST還檢查程式碼是否存在與法律和監管標準相關的相容性問題,包括許多銀行必須遵守的PCI-DSS。

5.執行互動式應用安全測試

  IAST是SSDLC的下一步,它會測試該軟體的實際版本。從本質上講,SAST是安全開發人員,而IAST是一名駭客,致力於訪問應用程式中應該無法訪問的區域。IAST不需要深入了解應用程式,只關注它是否容易遭受攻擊以及如何利用該漏洞。

混合使用靜態和動態安全測試是確保在程式碼和應用程式上線之前消除漏洞的最佳方法。特別是對於銀行軟體等更敏感的應用程式,這種組合決定性的減少漏洞和盡可能降低攻擊風險。

6.建立Security Gates

SDLC最重要的部分是確保有中高風險漏洞的軟體無法進入正式環境。微軟的SDL流程將Security Gates稱為「bug bars」,但這個想法是一樣的。Security Gates建立最低限度的安全性,並檢查程式碼是否存在高風險漏洞。任何標記為高風險的程式碼都應該發送給開發人員以實現修復。這些檢查最好在開發階段進行靜態分析測試時執行,因為CxSAST可以自動檢測這些漏洞,以協助開發人員在開發階段就能從源頭修復它。

基本上一旦建立了Security Gates,它們就不會被打破,無論應用程式發布的程度如何:永遠不允許中高風險的漏洞進入正式環境,Security Gates有助於確認以及如何防範風險。

7.持續導入安全開發教育(註6)

雖然不屬於SDLC,但開發人員教育是SSDLC的一部分。開發人員培訓在安全開發組織中迅速發展,因為它有效地讓對安全一無所知的開發人員蛻變為安全開發人員。

特別是在像銀行應用這樣的高度敏感的環境中,讓了解安全流程的開發人員可以更快地發布安全軟體。

 

現在,任何現代APP都需要比以前更高的安全意識。客戶希望能夠在他們的手機和電腦上做所有事情,這需要考慮更多如何保護我們的企業和客戶。在數位銀行業務方面,這些考量因素甚至更大,需要強而有力的規劃和執行才能保持高水平的安全性。通過SSDLC引導,銀行APP可以安全發布,讓客戶對您的企業和軟體充滿信心。

 

使用SqlBulkCopy做資料轉檔

bcp( sqlbulkcopy ) 原本是命令列上的工具,用來做大量複製資料的工作。
在.NET裡面也可以調用他的功能。

SqlBulkCopy 類別可用於僅將資料寫入 SQL Server 資料表。 但是資料來源不僅限於 SQL Server;可使用任何資料來源,只要該資料可載入 DataTable 執行個體,或可使用 IDataReader 執行個體進行讀取。 M$ docs 

這次我們使用他來做資料表的轉檔,將會有準備資料與執行兩部分。

繼續閱讀 “使用SqlBulkCopy做資料轉檔”

WINDOWS UPDATE 導致Excel匯入功能異常

 

相關文章:https://blog.gss.com.tw/index.php/2017/11/11/oledbconnection_open/

Microsoft Botframework + Adaptive Cards 快速打造 Chatbot

前言

今年的 Chatbot 很火紅,不知大家都用什麼來開發 Chatbot 呢?
筆者使用的是 Microsoft Botframework 來開發,它提供了很多語言的 SDK,讓我們可以快速的開發出 Chatbot。
最近開發 Vitals ESP (KM) Chatbot,一開始規劃好畫面及流程後,很快就開發完成了。
接下來就跟大家分享開發的過程 🙂

需求

Vitals ESP 是 KM 系統,希望 KM Chatbot 可以方便讓人查詢,在手機上畫面不大,所以需要分頁。如果有人 Mention 到你的話,也可以發通知到 Chatbot 上,讓你可以快速地回覆。
所以需求主要有 2 個,

1.使用者在輸入框輸入文字,立即依關鍵字查詢,並顯示出查詢結果(分頁),使用者可以按上一頁、下一頁去瀏覽,並可按文章串到系統去。

下圖為使用者在輸入框輸入「關鍵字」去查詢,並顯示出查詢結果
[輸入關鍵字查詢]

下圖為使用者按「下一頁」,系統切到第 2 頁
[顯示第2頁]

2.當其他人在文章中有 MENTION 到使用者時,使用者可以立馬收到別人在 CUE 你的內容。

下圖為當 KM 系統收到有人在 Cue 使用者時,除了 Mail 通知外,現在會再通知 IM ,讓使用者可以立馬可以知道
[及時通知]

下圖為使用者收到被 Cue 的內容後,可以針對該內容進行回覆
[立即回覆文章]

實作

1.定義 ACTIONS

從需求來看,可以將目前行為規劃為 2 個 Action ,一個是 Keyword Search ,另一個是 Mention 回覆。
當使用者從輸入框輸入文字的查詢,它的查詢頁為第 1 頁,卡片中的上、下頁,則依 Acton 中的頁碼來決定。
Mention 回覆則需要記錄要要回覆的文章相關資訊及回覆的內容。
所以以下就建立這 2 個 Action 的類別,如下,

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;
}

從上面可以發現我們定義了 2 個 Action ,分別為 KMSearchAction 及 KMReplyMentionAction ,它們都繼承自 KMAction 。
而設定 KMActionConverter 可以讓我們依 KMActionType 來分別 Deserialize 到對應的物件 ,如下的程式,

//會依 KMActionType 來分別轉成對應的物件 (KMSearchAction or KMReplyMentionAction )
messageAction = JsonConvert.DeserializeObject<KMAction>(dataValue);

當使用者按 上、下頁時,透過 JsonConvert.DeserializeObject 時,實際上會轉換成 KMSearchAction 類別,
[查詢的 KMSearchAction]

當使用者在回覆卡片上按下送出時,透過 JsonConvert.DeserializeObject 時,實際上會轉換成 KMReplyMentionAction 類別,
[回覆文章的 KMReplyMentionAction]

2.建立查詢結果及 MENTION 回覆的 ADAPTIVE CARDS

使用 Adaptive Cards 時,需要從 Nuget 中安裝 AdaptiveCards 套件,如下,
[AdaptiveCards 套件]

AdaptiveSubmitAction 物件有一個 DataJson 的屬性,是可以讓我們放入物件的 JSON 字串 。
所以在建立畫面這些 Button 時,就可以建立 Action 物件後,將它們的 JSON 放到 DataJson 屬性中 。
當使用者按下 Button 時,它的值就會在 MessageActivity 的 Value 屬性之中 。
所以在 RootDialog 中,我們就可以用這個屬性值來區分是按下 Button 進來的,還是使用者從輸入框輸入字串進來的,如下,

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;
	}
}

在 SearchKeywordActionStrategy Class 中,依 KMSearchAction 的內容,來建立查詢結果的 Adaptive Card

/// <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);
	}
}

上面建立 Adaptive Cards ,我是透過 Card Elements 一個一個來加入 。
您也可以到 Adaptive Cards Designer 設計好之後,將 json 存檔後,透過 AdaptiveCard.FromJson將它們匯進來哦!

建立 Mention 回覆的 Adaptive Cards ,是在另一個 Controller 在收到通知後,就建立它。
主要部份是建立 KMReplyMentionAction Class 一樣給 AdaptiveSubmitAction 的 DataJson 屬性,而 AdaptiveTextInput 的 Id 值要跟 KMReplyMentionAction Class 中的屬性值相同,只是 KMReplyMentionAction 是 PascalCase ,AdaptiveTextInput 的 Id 值是 CamelCase ,如下,

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;
		}
	}
}

下圖是系統收到 Exception 後,透過客製的錯誤處理將訊息傳送給使用者,
[BotException]

  • 註:在 Application_Start 那記得要跟 Autofac 註冊 PostUnhandledExceptionToUserOverrideTask 哦!

結論

從上面的分享中,可以透過自定的 JsonConverter (KMActionConverter),在 JsonConvert.DeserializeObject 時,取回正確的物件,再交給對應的 Strategy 類別來處理。
所以只要規劃好流程腳本,再透過 Microsoft Bot Framework,就可以讓我們快速開發出 Chatbot, 而 Adaptive Cards 則讓我們可以在 Chatbot 中建構出完整的 UI ,透過 Action.Submit 將 UI 轉化成需要的 Model 大大簡化開發的複雜度。
未來如果再加一個 Action 的話,只要擴充 KMActionType 及對應的 Action 及 ActionStrategy 就可以了哦。

  • 註 1: 目前 Adaptive Cards 的 Action 只能放在最下面,未來版本應該可以放在 Card 的中間,可以到 Adaptive Cards Designer 設計看看哦~
  • 註 2: 上述範例中,因為有使用 Makedown 的 link ,所以如果用 webchat 測試的話,請加入以下的 script 哦!
	
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/8.4.2/markdown-it.js" />

  • 註 3:有時想要讓使用者知道 Chatbot 有收到它的輸入,可以在 MessagesController 的 Post Method 中一收到訊息時,就先回個 Typing 的訊息給使用者,如下,
//先發送 typing 的 message
var connector = new GssConnectorClient(new Uri(activity.ServiceUrl));
var isTypingReply = activity.CreateReply();
isTypingReply.Type = ActivityTypes.Typing;
await connector.Conversations.ReplyToActivityAsync(isTypingReply);	

參考資料

Schema Explorer
Adaptive Cards Designer
Deserializing different types based on properties, with Newtonsoft.Json
Bot Framework Custom Error Messages and Exception Handling
BotFramework-WebChat Customization
Bot Framework Typing Activity – Let users know your bot is responding (and know when they are too)
Customize Web Chat for your websites – 亂馬客