植入到公众号的机器人聊天机器人该如何开发?

阅读以下内容大概需要15分钟,以下为文章的目录结构:

  • 极有趣味的聊天机器人
  • 智能聊天机器人功能介绍
  • 智能机器人揭秘
  • 公众号的开发者工具
  • 公众号消息如何转发
  • 实操
    1.填写服务器配置
    2.开发验证代码
    3.验证token并启用
    4.验证消息接收
    5.消息接收与解析
    6.消息的分发处理
    7.响应的协议组装
  • 写在最后

极有趣味的聊天机器人

最近的文章一直是围绕着网站改造展开的文章,与大家分享一下项目实战中的一些好玩的、规划以及架构上的设计,今天给大家带来的话题是:趣味聊天机器人。

中秋节,看到这样一个短视频,通过微信实现的自动回复聊天机器人。

看上去着实挺酷的、十足的趣味性着实也吸引到我了,因此,留言中也少不了有人在问怎么样实现的?技术爱好者也会索要源码。

看了之后,我同样好奇,打开Idea开始探索之路,并且成功了!当然,也很Easy!

智能聊天机器人功能介绍

打开“面试怪圈”公众号, 聊天框输入:来份Java面试题,机器人自动从资源库检索到合适的内容返回给用户,目前看效果还是比较精准。

以下是功能截图:

看上去是不是很智能呢,有没有心动呢?

当然,要做这个功能前,首先系统内部已经具备检索的能力,利用中秋的时间,我已经实现界面化搜索功能,你也可以访问面试怪圈的官网来体验,来快速查找你要的内容:

因此,以上功能的检索接口,可以被智能机器人复用。

智能机器人揭秘

那么今天我就来给大家揭秘一下智能机器人的原理和实操步骤,跟着以下步骤走,你也可以拥有一个属于你私人定制的智能AI机器人。——并且怎么对话,你说了算!

在实现一个功能之前,我喜欢这样去思考问题:整体流程是怎么的呢?需要做哪些准备呢?

以下是我的手绘图:

通过上图可以看出我们需要优先找到以下解题的突破口:

  • 如何将微信的聊天内容转发到我们自己的服务器?
  • 微信的通讯协议是什么样的?

公众号的开发者工具

如果你也开通了公众号,你会发现菜单中的【开发者工具】是帮我们灵活扩展公众号的能力。

开发者可通过该api的扩展,可以打破公众号「根据关键字回复」200个关键字的限制!并且此无上限!是不是很有诱惑力!

因此,一份开发者文档,官方肯定会附带给我们的:

文档中接收普通消息和被动回复用户消息看上去跟我们要找的消息转发协议很像,既然我已经阅读了文档,我就不卖关子了:

这两个协议就是:转发消息的Request和Response,具体应该遵循什么样的协议,文档中已经定义清楚,其中消息的类型包含了文本消息、图片消息、语音消息、图文消息等。

接收的文本消息的协议是用xml来定义的,如下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
</xml>

公众号消息如何转发

既然从文档中找到了消息转发的协议,那文档中肯定也定义消息如何转发给我们自己的服务器,你可以从文档中的「接入指南」中找到答案。

接入微信公众平台开发,开发者需要按照如下步骤完成:

1、填写服务器配置

2、验证服务器地址的有效性

3、依据接口文档实现业务逻辑

实操

以下是具体操作步骤(含代码):

1.填写服务器配置

登录微信公众平台官网后,在公众平台官网的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。如图:

特别注意一点:一旦开启服务器配置,公众号的自动回复及自定义菜单将失效。

加密是非必选,我们其实只需要填写两个基本信息:

  1. 服务器地址
    比如在你的主机开放一个url:
    http://你的域名/wechat/access
  2. Token
    自己定义一个字符串即可

2.开发验证代码

上一步填写的url和token并不能直接编辑验证,而需要配合服务端的验证代码才能够完成,在Springboot中定义了WeChatController,以下是url的验证地址:

@RequestMapping("/access")
 public String access(HttpServletRequest request, HttpServletResponse response) throws Exception {
  boolean isGet = request.getMethod().toLowerCase().equals("get");
if (isGet) {
   // 只有验证token,才是GET请求
   return weChatService.vertifyToken(request);
  } else {
   // 微信服务器POST消息时用的是UTF-8编码,在接收时也要用同样的编码,否则中文会乱码;
   request.setCharacterEncoding("UTF-8");
   // 在响应消息(回复消息给用户)时,也将编码方式设置为UTF-8,原理同上;
   response.setCharacterEncoding("UTF-8");
   String responseMsg = weChatService.receive(request);
   log.info("wechat response:{}", responseMsg);
   return responseMsg;
}
}

isGet:只有验证token,才是GET请求;Else中的逻辑为接收具体的消息。

vertifyToken就是验证token:

public String vertifyToken(HttpServletRequest request) {
  String msgSignature = request.getParameter("signature");
  String msgTimestamp = request.getParameter("timestamp");
  String msgNonce = request.getParameter("nonce");
  String echostr = request.getParameter("echostr");
  if (wXPublicUtil.verifyUrl(msgSignature, msgTimestamp, msgNonce)) {
   return echostr;
  }
  return StrUtil.EMPTY;
 }

wXPublicUtil中实现了验证token的具体逻辑:

@Value("${wechat.token}")
  private String token;
/**
   * 验证Token
   * 
   * @param msgSignature 签名串,对应URL参数的signature
   * @param timeStamp    时间戳,对应URL参数的timestamp
   * @param nonce        随机串,对应URL参数的nonce
   *
   * @return 是否为安全签名
   * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
   */
  public boolean verifyUrl(String msgSignature, String timeStamp, String nonce) {
   // 这里的 WXPublicConstants.TOKEN 填写你自己设置的Token就可以了
   String signature = SHA1.getSHA1(token, timeStamp, nonce);
   if (!signature.equals(msgSignature)) {
    return false;
   }
   return true;
  }

其中,${wechat.token}就是在服务器配置界面配置的token。你可以将此参数配置到类似application.yml这样的配置文件中去。

3.验证token并启用

代码写好后,将代码发布到你的云服务器上,并启动服务。

并且,在微信公众号后台的服务器配置中,提交url和token,并启用。

如果以上步骤没有问题,当提交时,会提示token验证成功!

这里注意一点,如果你的url填写的http://ip:port,微信只认80端口,你可以在你的nginx中做好端口映射。

4.验证消息接收

以上步骤完成后,已经能够在微信公众号的留言区做验证了,比如你在留言区发送一句消息,WeChatController中的access,就能够收到对应消息协议啦!

5.消息接收与解析

controller接收的消息,我们进行解析处理,可参考如下代码:

@Override
  public String receive(HttpServletRequest request) throws Exception {
   Map<String, Object> map = wXPublicUtil.xmlToMap(request);
   log.info("reveive wechat message:{}", JSONUtil.toJsonStr(map));
boolean hasEvent = map.containsKey("Event");
if (hasEvent) {
    String event = map.get("Event").toString();
    if (WeChatCommand.SUBSCRIBE.equals(event)) {
     // 订阅事件
     return subscribeHandler.handle(map);
    } else if (WeChatCommand.UNSUBSCRIBE.equals(event)) {
     // 取消订阅事件
     return unSubscribeHandler.handle(map);
    }
   } else {
    String msgType = map.get("MsgType").toString();
    log.info("MsgType:{}", msgType);
    if ("text".equals(msgType)) {
     return textMessageHandler.handle(map);
    }
   }
return null;
  }

其中,xmlToMap对xml文件解析成具体的map以便我们更加灵活的处理业务逻辑,这里引用dom4j的xml解析能力,你可以通过pom文件引入对应的依赖:

/**
   * xml转为map
   * 
   * @param request
   * @return
   * @throws DocumentException
   * @throws IOException
   */
  public static Map<String, Object> xmlToMap(HttpServletRequest request) throws DocumentException, IOException {
   Map<String, Object> map = new HashMap<String, Object>();
SAXReader reader = new SAXReader();
   InputStream ins = null;
   try {
    ins = request.getInputStream();
    Document doc = reader.read(ins);
Element root = doc.getRootElement();
    List<Element> list = root.elements();
    for (Element e : list) {
     map.put(e.getName(), e.getText());
    }
   } finally {
    IoUtil.close(ins);
   }
return map;
  }

解析好的协议,根据类型进行转发处理,其中 :

Event为事件,并不是具体的消息,比如关注事件、取关事件,后面可以单独介绍,也很有趣。

其他的为具体的消息,我们可以根据MsgType判断是文本消息、图片消息还是其他类型的。我们的智能机器人只解析了普通的文本类型消息。

6.消息的分发处理

虽然接收了文本消息,不同类型的消息实际上有不同的处理器,这里的代码就可以自由发挥了,以下是我对消息的处理归类:

其中对应的handler分发代码如下:

public String handle(Map<String, Object> map) {
String content = map.get("Content").toString();
if (StrUtil.isBlank(content)) {
   return null;
  }
if (isHello(content)) {
   // hi
   return hiHandler.handle(map);
  } else if (isDownloadCommand(content)) {
   // 要响应下载码
   return downloadHandler.handle(map);
  } else if (isGetResource(content)) {
   // 搜索资料
   return getResourceHandler.handle(map);
  } else if (isGetWebSite(content)) {
   // 获取官网
   return getWebSiteHandler.handle(map);
  } else if (isSendResource(content)) {
   // 送资料
   return sendResourceHandler.handle(map);
  } else if (isJoinFriend(content)) {
   // 加好友
   return joinFriendHandler.handle(map);
  }else if (isJoinQuaner(content)) {
   // 加群
   return joinQuanerHandler.handle(map);
  }
return aiHandler.handle(map);
}

由于以上策略并不能完全覆盖用户问题,因此aiHandler负责处理以上固定策略外的应答,该应答的语料维护在数据库,给机器人赋予一定的温度,比如:

- 你在干嘛
- 刚刚在想你怎么还不来找我聊天,呀,你就来了,我们真是心有灵犀呀!
或者是这样的:
- 来杯奶茶
- 等我~秋天的第一杯一定是我送你的,[亲亲]

其中[亲亲]、等会在聊天中自动被解析为表情。

7.响应的协议组装

在http接口,我们直接组装xml格式的字符串返回,就可以被转发给发送方啦:

  • 这是一个普通的文本消息响应组装的逻辑:
public String textResponse(Map<String, Object> map, String content) {
  HashMap<String, Object> mapXml = new HashMap<>();
  mapXml.put("ToUserName", map.get("FromUserName"));
  mapXml.put("FromUserName", map.get("ToUserName"));
  mapXml.put("CreateTime", Long.parseLong(map.get("CreateTime").toString())+1);
  mapXml.put("MsgType", "text");
  // 根据receive组装回复的内容
  mapXml.put("Content", content);
  return map2Xmlstring(mapXml);
 }
  • 当然也可以自由定义图文消息

以下图文的文章链接可以是外部的资源链接:

public String articleResponse(Map<String, Object> map, String title, String description,String pic, String url) {
  String article = "<item>\n" + 
    "      <Title>%s</Title>\n" + 
    "      <Description>%s</Description>\n" + 
    "      <PicUrl>%s</PicUrl>\n" + 
    "      <Url>%s</Url>\n" + 
    "    </item>";
article = String.format(article, title, description, pic, url);
HashMap<String, Object> mapXml = new HashMap<>();
  mapXml.put("ToUserName", map.get("FromUserName"));
  mapXml.put("FromUserName", map.get("ToUserName"));
  mapXml.put("CreateTime", Long.parseLong(map.get("CreateTime").toString())+1);
  mapXml.put("MsgType", "news");
  mapXml.put("ArticleCount", 1);
  // 根据receive组装回复的内容
  mapXml.put("Articles", article);
  return map2Xmlstring(mapXml);
 }

是不是很简单?是不是很酷!按照以上步骤,你可以打造一个私人定制机器人呢?来试试吧!

关注/取关事件通知

上面聊到定义的access接口可以接收到事件,我们可以将关注/取关作为事件,然后推送给我们的手机客户端。

比如你可以在收到关注事件后,推送一封邮件给你的QQ邮箱。 这样,只要在手机上安装一个QQ邮箱客户端,有人关注你,你的手机就会收到通知,再也不用去公众号的后台去看了,完全以事件为驱动,并保证通知的实时性。

同时,由于微信公众号获取关注者数量的接口有权限限制,你可以将关注数记录在自己的数据库。有了这个数据,可以成为私人监控系统的一个重要监控指标了!

写在最后

开发这个功能,我一共消耗了中秋节的6个小时左右,其实官方的文档不是特别健全,尤其是在描述token验证逻辑以及token验证和消息接收接口上是如何区分的这一块。 我也百度了一些资料,才找到答案。

但不管怎么样,明白了原理,实现就特别简单。如果你感兴趣,你可以根据我的文章更快速的搭建。

我是公众号「面试怪圈」的Yesterday,喜欢我的原创,可以关注我,点个「在看」和「收藏」是对我最大的支持和鼓励!!!

以下是面试怪圈的官网,其中第一份为我面试腾讯、字节、京东、美团等大厂的面试总结,非常珍贵: