前言
微信公众号的开发,园子里有很多资料,这里简述。
虽说是智能,现在是仿佛智障,很多是hard code逻辑,日后将逐步加入LUIS,现在一些常用的打招呼(你好,您好,hi,hey,hello,how are you等识别比较好)。
业务性的处理,更多是逻辑上,比如常用的回复1,2,3,4然后返回对应的消息。这里涉及多轮会话,要求对上文的记忆,不然容易回答串题了。
还有就是一些分词技术,这个目前对空格敏感,还有就是南京市长江大桥,是南京市长,还是长江大桥?这个是日后话题了。
.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") };
- }
验证成功之后,所有消息,微信会转到这个方法:
- 用户发送消息后,微信平台自动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);
效果图
就是打招呼的时候,会回复一个简单得产品介绍,暂时没有多轮会话,因为上下文理解出现一些问题,就不献丑了(滑稽)。
后记
日后还有个切换到人工客服,当输入人工客服时,调用微信公众号的客服消息接口,每两小时(过期时间7200s)获取一次token,
然后根据用户的OPENID,将微信支持的消息格式,发送到粉丝端,获取token的IP必须添加到白名单中。
这个要求公众号是认证的,个人的没有授权。