微信公衆號 智能客服

前言

微信公衆號的開發,園子裏有不少資料,這裏簡述。
雖然說是智能,如今是彷彿智障,不少是hard code邏輯,往後將逐步加入LUIS,如今一些經常使用的打招呼(你好,您好,hi,hey,hello,how are you等識別比較好)。json

業務性的處理,更可能是邏輯上,好比經常使用的回覆1,2,3,4而後返回對應的消息。這裏涉及多輪會話,要求對上文的記憶,否則容易回答串題了。api

還有就是一些分詞技術,這個目前對空格敏感,還有就是南京市長江大橋,是南京市長,仍是長江大橋?這個是往後話題了。服務器

.NET Core APIi的方式,建立一個WeChat API Controller

驗證微信服務器地址的方法:微信

[HttpGet]
    [Route("api/wechat")]
    public IActionResult Get(string signature, string timestamp, string nonce, string echostr)
    {
        var token = ConfigReader.TokenStr;//微信公衆平臺後臺設置的Token
        if (string.IsNullOrEmpty(token))
        {
            return NotFound("請先設置Token!");
            //return new HttpResponseMessage() { Content = new StringContent("請先設置Token!", Encoding.GetEncoding("UTF-8"), "application/x-www-form-urlencoded") };
        }
        var ent = "";
        if (!BasicAPI.CheckSignature(signature, timestamp, nonce, token, out ent))
        {
            return NotFound("驗證微信簽名失敗!");
            //return new HttpResponseMessage() { Content = new StringContent("參數錯誤!", Encoding.GetEncoding("UTF-8"), "application/x-www-form-urlencoded") };
        }
        //返回隨機字符串則表示驗證經過
        return Ok(echostr);
        //return new HttpResponseMessage() { Content = new StringContent(echostr, Encoding.GetEncoding("UTF-8"), "application/x-www-form-urlencoded") };
    }

驗證成功以後,全部消息,微信會轉到這個方法:markdown

用戶發送消息後,微信平臺自動Post一個請求到這裏,並等待響應XML。
    [HttpPost]
    [Route("api/wechat")]
    public async Task<IActionResult> Post()
    {
        WeChatMessage message = null;
        string signature = HttpContext.Request.Query["signature"].ToString();
        string timestamp = HttpContext.Request.Query["timestamp"].ToString();
        string nonce = HttpContext.Request.Query["nonce"].ToString();
        string echostr = HttpContext.Request.Query["echostr"].ToString();

        var safeMode = HttpContext.Request.Query["encrypt_type"].ToString() == "aes";
        using (var streamReader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
        {
            var decryptMsg = string.Empty;
            var msg = streamReader.ReadToEnd();

            #region 解密
            if (safeMode)
            {
                var msg_signature = HttpContext.Request.Query["msg_signature"];
                var wechatBizMsgCrypt = new WeChatMsgCrypt(ConfigReader.TokenUrl, ConfigReader.EncodingAESKey, ConfigReader.AppId);
                var ret = wechatBizMsgCrypt.DecryptMsg(msg_signature, timestamp, nonce, msg, ref decryptMsg);
                if (ret != 0)//解密失敗
                {
                    //TODO:失敗的業務處理邏輯
                }
            }
            else
            {
                decryptMsg = msg;
            }
            #endregion

            message = ParseMessageType.Parse(decryptMsg);
        }
        var response = await new WeChatExecutor(repo).Execute(message);
        var encryptMsg = string.Empty;

        #region 加密
        if (safeMode)
        {
            var msg_signature = HttpContext.Request.Query["msg_signature"];
            var wxBizMsgCrypt = new WeChatMsgCrypt(ConfigReader.TokenUrl, ConfigReader.EncodingAESKey, ConfigReader.AppId);
            var ret = wxBizMsgCrypt.EncryptMsg(response, timestamp, nonce, ref encryptMsg);
            if (ret != 0)//加密失敗
            {
                //TODO:開發者加密失敗的業務處理邏輯
            }
        }
        else
        {
            encryptMsg = response;
        }
        #endregion

        return Ok(encryptMsg);
        //return new HttpResponseMessage()
        //{
        //    Content = new StringContent(encryptMsg, Encoding.GetEncoding("UTF-8"), "application/x-www-form-urlencoded")
        //    //ContentType = "text/xml",              
        //};
    }

將接收到的消息,轉發到Bot(Microsoft BotFramework)服務,將Bot服務的SecretKey配置上:

public async static Task<string> PostMessage(string message, string openId)
    {
        HttpClient client;
        HttpResponseMessage response;

        bool IsReplyReceived = false;

        string ReceivedString = null;

        client = new HttpClient();
        client.BaseAddress = new Uri("https://directline.botframework.com/api/conversations/");
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        //bot-int WeChat channel
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("BotConnector", ConfigReader.BotSecretKey);

        response = await client.GetAsync("/api/tokens/");
        if (response.IsSuccessStatusCode)
        {
            var conversation = new Conversation();
            response = await client.PostAsJsonAsync("/api/conversations/", conversation);
            if (response.IsSuccessStatusCode)
            {
                Conversation ConversationInfo = response.Content.ReadAsAsync(typeof(Conversation)).Result as Conversation;

                var contentString = response.Content.ReadAsStringAsync();
                string conversationUrl = ConversationInfo.conversationId + "/messages/";
                Message msg = new Message() { text = message, from = openId, channelData = "WeChat" };
                response = await client.PostAsJsonAsync(conversationUrl, msg);
                if (response.IsSuccessStatusCode)
                {
                    response = await client.GetAsync(conversationUrl);
                    if (response.IsSuccessStatusCode)
                    {
                        MessageSet BotMessage = response.Content.ReadAsAsync(typeof(MessageSet)).Result as MessageSet;
                        ReceivedString = BotMessage.messages[1].text;
                        IsReplyReceived = true;
                    }
                }
            }
        }
        return ReceivedString;
    }

這裏要定義幾個model,用於序列化Bot返回的數據類型:

public class Conversation
{
    public string conversationId { get; set; }
    public string token { get; set; }
    public int expires_in { get; set; }
}

public class MessageSet
{
    public Message[] messages { get; set; }
    public string watermark { get; set; }
    public string eTag { get; set; }
}

public class Message
{
    public string id { get; set; }
    public string conversationId { get; set; }
    public DateTime created { get; set; }
    public string from { get; set; }
    public string text { get; set; }
    public string channelData { get; set; }
    public string[] images { get; set; }
    public Attachment[] attachments { get; set; }
    public string eTag { get; set; }
}

public class Attachment
{
    public string url { get; set; }
    public string contentType { get; set; }
}

Bot server中的消息路由處理方法

public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
    {
        if (activity != null && activity.GetActivityType() == ActivityTypes.Message)
        {
            logger.Information(new ConversationEvent
            {
                EventType = LogEventType.ChatBotActivity,
                Message = $"ChatBot message controller receives a message, content: {activity.Text}",
                Activity = activity
            });
            var isAgent = await ServiceManager.AgentManager.IsAgent(activity.From.Id);
            var context = await ServiceManager.ContextManager.GetUserContext(activity, isAgent);

            var activityLogger = new Logger.CosmosDbActivityLogger();
            if (await ServiceManager.CommandMessageHandler.HandleCommandAsync(activity, isAgent) == false)
            {
                if (!string.IsNullOrEmpty(activity.Text)
                    && activity.Text.ToLower().Contains(CommandRequestConnection))
                {
                    var contextManager = ServiceManager.ContextManager;
                    var messageRouterResultHandler = ServiceManager.MessageRouterResultHandler;
                    var result = await contextManager.RequestConnection(context, activity);
                    await activityLogger.LogAsync(activity);
                    // Handle the result, if required
                    await messageRouterResultHandler.HandleResultAsync(result);
                }
                else
                {
                    if (isAgent)
                    {
                        IAgentActor agentActor = SFHelper.GetActor<IAgentActor>(activity);
                        await activityLogger.LogAsync(activity);
                        await agentActor.MessagePost(context, new ActivityDataContract(activity));
                    }
                    else
                    {
                        ICustomerActor customerActor = SFHelper.GetActor<ICustomerActor>(activity);
                        await customerActor.MessagePost(context, new ActivityDataContract(activity));
                        if (activity.Text.Equals("沒有解決") || activity.Text.Equals("解決了"))
                        {
                            await ServiceManager.InquiryManager.CloseInquiryAsync(context);
                        }
                    }
                }
            }
        }
        else
        {
            HandleSystemMessage(activity);
        }
        return new HttpResponseMessage(HttpStatusCode.Accepted);
    }

定義回覆消息內容,markdown格式,微信支持不友好。

public static Activity BuildProactiveGreeting(Activity context)
    {
        var responseMessage = context.CreateReply();

        // TODO: Move to MenuManager via DataReposotry to persist to CosmosDB
        var topLevelMenuItems = new List<CardActionItem>
        {
            new CardActionItem() { Title = "技術支持", ActionType = ActionTypes.ImBack },
            new CardActionItem() { Title = "帳單/訂閱/配額支持", ActionType = ActionTypes.ImBack },
            new CardActionItem() { Title = "產品諮詢", ActionType = ActionTypes.ImBack },
            new CardActionItem() { Title = "註冊問題", ActionType = ActionTypes.ImBack }
        };

        // TODO: make sure we get correct name from wechat channel
        string cardTitleText = "Hi,我是您的智能客服,等您好久啦~猜您可能對如下內容感興趣:";
        var heroCard = BuildHeroCardWithActionButtons(cardTitleText, topLevelMenuItems);

        responseMessage.AttachmentLayout = AttachmentLayoutTypes.List;
        responseMessage.Attachments = new List<Attachment>() { heroCard.ToAttachment() };
        return responseMessage;
    }

Bot回覆消息:

var client = new ConnectorClient(new Uri(message.ServiceUrl), new MicrosoftAppCredentials());
var reply = GreetingHelper.BuildProactiveGreeting(message);
client.Conversations.ReplyToActivityAsync(reply);

效果圖

就是打招呼的時候,會回覆一個簡單得產品介紹,暫時沒有多輪會話,由於上下文理解出現一些問題,就不獻醜了(滑稽)。app

後記

往後還有個切換到人工客服,當輸入人工客服時,調用微信公衆號的客服消息接口,每兩小時(過時時間7200s)獲取一次token,
而後根據用戶的OPENID,將微信支持的消息格式,發送到粉絲端,獲取token的IP必須添加到白名單中。
這個要求公衆號是認證的,我的的沒有受權。微信公衆平臺

相關文章
相關標籤/搜索