My Avatar

Shadow

I love bleak day, like something will happen

Hubot

2016年10月08日 星期六, 发表于 北京

如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)

一个聊天机器人


一. 介绍

  Hubot是Github团队开发的一个聊天机器人,他可以帮助进行很多自动化的工作,从而减少人工的重复的劳动,现在他已经开源了,是用coffeescript写的node.js程序,可以部署到heroku、linux、windows等平台上。

  那么Hubot究竟能干什么,最基本的,它能找图、翻译等,当然它的扩展能力很强,你可以通过script来实现更多的功能。

二. 安装、配置、启动

  前提:

  先去https://nodejs.org/en/download/上下载node.js的安装包,注意下载Linux Binaries,而不是source code,因为source code还需要自己编译,而binaries解压缩就可以了。

  node.js已经自带npm,所以不用再单独安装npm,解压缩后,注意修改~/.bash_profile,将path添加上node/bin目录

  搞定后,输入node -v测试是否安装成功

  接着,输入

1
npm install -g yo generator-hubot

  安装hubot的生成器,然后:

1
2
3
% mkdir myhubot
% cd myhubot
% yo hubot

  生成一个机器人,这里会问你四个问题,机器人的主人名字邮件、机器人的名字、机器人的描述、所采用的adapter是什么(具体adapter干嘛后面介绍),这样之后就在当前目录下生成了一堆文件,如下:

  这样之后,我们在当前目录下输入:(注意不能进到bin目录下,输入./hubot,这样会报错。。)

1
bin/hubot

  这样默认启动了shell adapter,也就是直接通过界面与机器人交互,适合于开发模式。显示如下:

  可以看到报了一个错,因为我们并不是在heroku上部署hubot,所以这个script是不需要的,我们可以将文件external-scripts.json中的hubot-heroku-keepalive去掉,再启动就可以了。另外hubot-redis-brain: Using default redis on localhost:6379显示hubot-redis-brain这个script需要用到redis,默认情况下使用localhost的redis服务,可以通过环境变量改变redis服务地址,具体参考https://github.com/hubot-scripts?page=1里面的hubot-redis-brain的用法。当然这里我们本地就算没有redis,也可以启动hubot,并不会报错。

  启动后,我们输入

1
Dave help

   会展示出目前所有的指令,以及对应的用途,之所以有这些指令,是因为在external-scripts.json中已经预先设置了预装的一些scripts,如果不想要只要去掉就行,另外help这个指令也是一个script实现的,它能将你使用的所有script的command注释中的语句打印出来(这一点还是蛮强大的)。

  譬如,我们输入

1
Dave ship it

  他就会返回一条url,如下:

1
https://dl.dropboxusercontent.com/u/602885/github/squirrelmobster.jpeg

  再输入一次,又返回另一条url:

1
http://1.bp.blogspot.com/_v0neUj-VDa4/TFBEbqFQcII/AAAAAAAAFBU/E8kPNmF1h1E/s640/squirrelbacca-thumb.jpg

  这两个都是松鼠的图片,可以这条指令就是用于随机搜索一张松鼠的图片并返回。

三. Scripts

  接下来,我们重点介绍hubot之所以可扩展性很强大的原因,即各种各样的脚本。脚本本质上就是一个.coffee或.js文件,放在scripts目录下即可,那么脚本里面究竟都可以干些什么呢?

  监听和响应,发送与回复

  作为一个聊天机器人,最基本,它需要与用户发送的信息进行交互,hubot能监听聊天室内的聊天内容,在监听到特定的消息后发送相应的消息,同时也能对直接询问他的消息进行回复。代码如下:

  robot就是我们的机器人的实例,hear方法代表监听,respond方法代表响应,如果聊天信息中出现了badger,那么就会执行相应的代码,假如你的机器人叫Dave,那么如果你输入了Dave open the pod bay doors,那么就会执行相应的代码。

  那么机器人在监听或者收到相应的请求后,可以发送消息或者回复请求,看如下所示代码:

  上面的res参数就是回复的实例(很多以前的scripts中用msg来表示也是可以的),通过res,调用send方法表示发送一条消息回聊天室,调用reply方法表示给向机器人发消息的用户回复一条消息,emote则不太了解,大概是回一个表情吧,需要相应的adapter支持。

  当然,上面都是对一些静态消息的回复和相应,hubot也支持对消息进行正则匹配,来捕获需要的数据

  上述代码表示,res.match[0]代表”open the pod bay doors”, 而res.match[1]则是”pod bay”

  发起Http请求

  Hubot能够发起http请求,以便调用第三方api,最基本的get请求如下:

  最基本的post请求如下:

  其中err为过程中出错后的错误信息,res为nodejs中的http.ServerResponse,而body则是我们应该关心的返回的数据,是一个字符串。如果返回的body是一个json,我们可以通过如下方式来解析:

  随机选择

  如果我们需要从一个数组中随机选择一条回复,Hubot自己封装了一个简单的用法,如下:

  主题

  Hubot能对聊天室的主题变化产生相应的反应。当然这需要adapter的支持。

  进入和离开

  如果adapter支持的话,hubot能感知到用户进入和离开聊天室,从而做出相应的反应:

  自定义监听器

  前面那些功能基本涵盖了用户基本的需求,但是如果你需要更灵活的监听器,可以通过如下方式实现:

  上面的代码通过listen方法,自定义了一个匹配方法,如果消息发送用户的姓名为Steve,而且随机数大于0.8时,就会给用户回复一个消息,需要注意的是response.match这里返回的是上面匹配方法中的布尔判断的实例,也就是Math.random()

  环境变量

  Hubot能访问环境变量,但是开发者必须知道如果环境变量未定义的情况下,应该怎么处理,是退出还是给出提示。如下所示就是在环境变量未定义的时候退出程序:

  依赖

  Hubot使用npm来管理它的依赖,如果需要添加额外的依赖,在package.json中的dependencies下添加,例如,如果需要添加lolimadeupthispackage 1.2.3,那么package.json中的dependencies如下:

  如果你需要使用Hubot-Scripts,即第三方脚本,它们有可能需要依赖其他的包是你之前没有依赖的,那么你可以从相应的script的dependencies documentation中找到相应的依赖,添加到package.json中即可。

  Timeout 和 Intervals

  这个就跟vertx里的setTimer和setPeriod一样,可以设置延时多少后开始执行或者每隔一段时间开始执行一次,具体用法参考https://hubot.github.com/docs/scripting/#timeouts-and-intervals

  Http监听器

  Hubot除了能发起http调用外,也能作为http server来监听http请求。这种方式一般用于监听web服务的webhook,并将消息通知到相应的聊天室或用户。      默认监听端口为8080,可以通过设置环境变量改变

  上述代码,在接到post请求后,会向聊天室发送一条消息。

  事件

  Hubot可以通过事件在不同的脚本间传送数据。通过robot.emit和robot.on来实现

  上述代码表示:如果收到github的post-commit事件,则将其传输给另一个脚本,该脚本收到该事件后会给相应用户发送一条消息。

  错误处理

  之前的Hubot如果发生了uncaught exceptions,就会使得Hubot实例崩掉,但是后来Hubot提供了一个 uncaughtException handler来处理这些exception

  你可以在里面做任何你想做的事,虽然他可以处理为未捕获的异常,但是这样会导致你的程序处在一个未知的状态里,所以你应该在你自己的脚本里注意捕获异常,然后通过发事件的方式来将错误发送到这个handler中处理,如下:

  中间件

  何谓中间件呢,有点类似拦截器的概念,在hubot接收到信息后,会根据脚本中的程序,去匹配指令,从而执行代码,这其实是一条链,所有匹配起来的任务会依次执行,而这些任务其实都是一个listener,而中间件就是在执行这些任务之前或之后去做一些额外的处理

  目前有三种中间件:

  具体的MiddleWare用法参考文档:https://hubot.github.com/docs/scripting/#listener-metadata

  脚本文档

  Hubot脚本最开头应该是一些注释,这些注释展示了该脚本的一些信息,如下:

  Description:脚本功能的描述

  Dependencies: 依赖的模块

  Configuration: 需要设置的环境变量

  Commands: 支持的指令

  Notes: 备注

  Author:作者

  Persistence

  Hubot有一个内存的key value存储,即robot.brain,可以用来存储和获取数据

  而且,brain还能获得用户数据,根据id、姓名、模糊姓名查询等,userForName, userForId, userForFuzzyName, and usersForFuzzyName

  脚本加载

  现在有两种方式加载脚本,一种是你自己写的,要么是coffee文件,要么是js文件,放在scripts目录下即可,另一种是使用的第三方脚本,你需要通过npm进行安装,然后在 external-scripts.json中加上相应的脚本名即可。

  如果你想找第三方的hubot-scripts,你可以按照如下步骤进行:

  首先,输入:npm search hubot-scripts ,这样会列出所有跟query相关的脚本,接着输入 npm install --save ,将你想要的脚本安装,然后在external-scripts.json中添加相应的包,如果你想看这个脚本如何使用,可以输入npm home 来打开浏览器进入官方页查看使用说明。或者直接去github上搜索包名也能搜到。

  脚本分享

  如果你做了脚本需要分享给别人,你可以按照步骤进行操作:https://hubot.github.com/docs/scripting/#sharing-scripts

  脚本实践

  下面我就实践实现一个自己脚本,使得机器人能通过指令发送微信企业号消息给指定用户。脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
robot.respond /send (.*) to (.*) using wechat/i, (res) ->
    content = res.match[1]
    touser = res.match[2]
    corpid = "******"
    secret = "******"
    token = "*******"

    data = JSON.stringify({
      touser: "#{touser}",
      agentid: '1',
      content: "#{content}"
    })

    robot.http("http://xx.xx.xx.xx:8082/qywechatapi/sendtext?corpId="+ corpid + "&secret=" + secret + "&token=" + token)
      .header('Content-Type', 'application/json').header('Accept', 'application/json').post(data) (err,response,body) ->
        if err
          res.send "Error occured while sending wechat msg: #{err}"
          return

        result = JSON.parse body
        if "#{result.status}" is "0"
          res.send "send wechat msg failed cause: #{result.errmsg}"
        else
          res.send "send wechat msg completed !"

  在上面的代码中,我们让机器人能对指令: send xxx to xxx using wechat 进行响应,从指令中获取发送的消息内容和消息接收人,然后调用http请求,来发送微信企业号消息。

  需要注意的是,coffee script跟python一样用缩进来处理语句块,因此所有同一层的缩进必须用相同的缩进方式,要么都是空格,要么都是tab键,不然很容易编译不过。

四. Adapters

  adapter就是调用hubot的终端,目前官方支持的是shell(用于开发)和Campfire,shell就不说了,campfire是一个web端的IM系统,下面就介绍下如何将hubot聊天机器人添加到你的campfire聊天室中去。

  首先,进到campfire官网:https://www.campfirenow.com/,然后点击上方的Plans & Pricing,这里面就是告诉你用多少钱可以买到什么样的服务,当然,我们是想免费用用的,所以看下图,最下面有一行小字,很鸡贼地告诉你可以使用免费的服务,只是限制了只能4个人交流并且只能使用10MB的存储空间,当然我们不care

  进去后,创建账户,然后就进入了web IM 聊天界面如下:

  上图只是大厅,右边默认创建了一个聊天室room1,你也可以点击右上角的create a new room来创建新的房间。我们点击Room1,进入聊天室:

  这样,我们就进入到聊天室了,最下方有输入框可以打字,右边可以看到谁在,那么我们怎么让hubot加入到这个聊天室呢?

  首先,我们要为hubot也创建一个campfire的账户,创建完成后,进入hubot的campfire大厅,然后点击右上角的My info按钮,获取hubot账户的api token

  然后回到我们自己的账号,拿两个信息,一个是你的campfire账号的account,也就是看你的url,例如我的url长这样:https://c63c9adc.campfirenow.com,那么account就是c63c9adc,第二个拿你想要hubot加入的聊天室的id,你进入到聊天室的界面,再看url:https://c63c9adc.campfirenow.com/room/624778,那么这个聊天室的id就是624778

  好的,这样我们就有了需要的三个信息:token、account、roomid,进入到部署hubot的系统,这里我们是在linux上部署,那么设置三个环境变量:

1
2
3
4
5
export HUBOT_CAMPFIRE_TOKEN="..."

export HUBOT_CAMPFIRE_ROOMS="123,321"

export HUBOT_CAMPFIRE_ACCOUNT="..."

  注意聊天室是支持多个聊天室同时添加hubot的,用逗号分隔开房间号id即可

  接着我们使用如下指令启动hubot:

1
bin/hubot -a campfire

  神坑!

  为什么说是神坑呢!!!我被这玩意儿坑了一上午才调通,在campfire聊天室内加上hubot。不得不吐槽下campfire。。

  首先,我们需要两个campfire账号,一个是自己的,一个是为hubot准备的,也就是说,要想聊天室里部署hubot,首先hubot得有自己的账号。然后按照官方文档,只要获取hubot账号里的api token和自己账号下的account以及roomid就可以了,但是我按照上述方式启动后老是报:invalid access token的错误,也就是说hubot的api token无效。。。我检查了一万遍,试了各种方式,都没法调通,后来发现,得先用我的账号邀请hubot来房间,让hubot获取进入房间的权限后,会发一封邮件给hubot,hubot通过邮件链接进入到房间界面,在这里再点击My info,获取到的api token才是正确的api token。。。这样就调通了。

  这里提供一个启动hubot的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash 
## 
## Wrapper for Hubot startup 
## 

HUBOT="bin/hubot"
NAME="Dave"
ADAPTER="campfire"
HUBOT_CAMPFIRE_TOKEN="efd5a7303aec31f14678206378987b2dadf63c03"
HUBOT_CAMPFIRE_ACCOUNT="c63c9adc"
HUBOT_CAMPFIRE_ROOMS="624778,624788"

OPTS="--name ${NAME} --adapter ${ADAPTER}"
export HUBOT_CAMPFIRE_TOKEN 
export HUBOT_CAMPFIRE_ACCOUNT 
export HUBOT_CAMPFIRE_ROOMS 

until ${HUBOT} ${OPTS}; 
do echo "Hubot crashed with exit code $?. Restarting." >&2
sleep 5
done

  效果图如下:

  

  开发adapter

  我们当然希望能够在除了campfire的其他IM系统上也添加hubot,所以hubot提供了对adapter的扩展能力,我们只需要继承Adapter,写一个自己的adapter类即可,下面附上我写的在我们公司的IM系统上添加hubot的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
try
  {Robot,Adapter,TextMessage,User} = require 'hubot'
catch
  prequire = require('parent-require')
  {Robot,Adapter,TextMessage,User} = prequire 'hubot'

class Sample extends Adapter

constructor: ->
  super
  @robot.logger.info "Constructor"

send: (envelope, strings...) ->
  @robot.logger.info strings
  data =
    to:
      compid: "1"
      touser: ["liangjfc"]
    type: "notify"
    content: "#{strings[0]}"
  datastr = JSON.stringify(data)
  @robot.logger.info datastr
  @robot.http("http://xx.xx.xx.xx:xxxx/messagecenter/sysmsg")
    .header('Content-Type', 'application/json').header('Accept', 'application/json').post(datastr) (err, res, body) =>
      if err
        @robot.logger.info "#{err}"

reply: (envelope, strings...) ->
  @send envelope, strings

run: ->
  @robot.logger.info "Run"
  @emit "connected"
  user = new User 1001, name: 'Jingfan'
  message = new TextMessage user, 'good morning', 'MSG-001'
  @receive message

  @robot.router.post '/hubot/getmsg', (req, res) =>
    data   = if req.body.payload? then JSON.parse req.body.payload else req.body
    msg = data.msg
    user = new User 1001, name: 'Jingfan'
    message = new TextMessage user, msg, 'MSG-001'

    @receive message
    res.send 'ok'


exports.use = (robot) ->
  new Sample robot

  这是一个及其简陋的adapter,但能达到我的目的

  其实adapter要做的事很简单,就是接消息,和发消息,接到消息,传送给hubot,hubot去匹配各项指令,将返回结果再传回聊天界面。

  要开发一个adapter,应该按照如下步骤:

  创建一个adapter的目录:mkdir sample

  进入sample目录,创建src目录,在src目录下创建自己的adapter文件,例如sample.coffee

  回到sample目录下,执行npm init,初始化项目,会提示你输入一些信息,需要注意的是name需要输入为hubot-开头,例如hubot-sample,还有entry point要写为src/sample.coffee

  编写自己的adapter,记得开头加上

1
2
3
4
5
try
  {Robot,Adapter,TextMessage,User} = require 'hubot'
catch
  prequire = require('parent-require')
  {Robot,Adapter,TextMessage,User} = prequire 'hubot' 

  然后修改package.json文件,加上如下依赖:

1
2
3
4
5
6
7
8
9
 "dependencies": {
   "parent-require": "^1.0.0"
 },
 "peerDependencies": {
   "hubot": ">=2.0"
 },
 "devDependencies": {
   "coffee-script": ">=1.2.0"
 }

  然后在你的hubot目录下,执行:

1
npm link ../sample

  最后,启动你的hubot,指定adapter为sample即可:

1
bin/hubot -a sample

  效果展示

四. 思考

     hubot是一个机器人,但是它本身是没有什么能力的,需要由开发者提供脚本来扩展其能力,所以其实hubot最重要的贡献在于开发者不用关系指令的接收和响应结果发送的逻辑(adapter已有的情况下),而只用专心于开发自己需要提供的服务的脚本即可,当然如果adapter不存在的情况下,你还是需要开发一个自己的adapter。

  所以对于要在自己的IM系统上添加hubot,感觉并没有省什么事,因为你还是要编写接收发送消息的逻辑,而且我们需要hubot支持的功能应该也不是社团里提供的那些脚本能实现的,而是业务相关的,所以脚本我们也得自己实现,这样看下来,跟我们自己在IM系统里实现这样一套聊天机器人其实没有多少区别