5

Github GitHub - dephrast/botbay: botbay-以生物多样性为命题的私有机器人赋能平台

 3 years ago
source link: https://github.com/dephrast/botbay
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

项目背景-BOTBAY机器人港湾

BotBay致力于创建专属的拟人机器人,我们理想中它可以接入不同的平台【微信、5G】,作为每一人完成日常工作生活专属助理,你可以给它起一个名字,这样就可以伴随终身,我们希望无论你今后的工作生活如何变化,它都可以普适的服务能力,目前版本我们赋能BotBay工作消息整理和待办提醒功能,例如: 1.把机器人拉进群,帮助我记录群里面的文字、图片、文件,并自动将文件存储到云盘,文字经过过滤后形成纪要; 2.在群里面@我或者私聊我,要求查看当日信息“日报”、“纪要”、并支持将“纪要发送邮箱”; 3.模拟一个工作任务,看看机器人如何提醒我的。

B站链接

账号绑定和给机器人起名字

新用户启动chatbot交互时,由于它还不认识你,所以需要向你确认账户【基于本团队之前开发过的一套用户体系】和机器人它自己的姓名

停止与启动机器人应答

由于我们使用的是本人微信号,考虑到不影响日常收发消息,所以实现了开关

自动纪要生成

根据关键词提取算法,判断群聊消息中那些内容更加有可能属于重要信息,支持纪要发送邮箱【模拟会议纪要的过程】

群文件、图片、音频、视频自动归档-移动端

一个工程向的小机制,帮助归档群聊文件,防止文件过期、手机电脑更换等问题

当然这个其实是一个正儿八经的网盘系统【基于本团队之前开发过的一套网盘】

待办提醒与代操作

如果BOTBAY接入了业务办公系统的话,那它就可以采用询问的方式协助你处理待办工作,如下图我们模拟了一个申请单提交审批流程

信息归档日报

根据收集到的Text/Audio/Video/Attachement/Image,以及Room/Contact/mentionList等信息,进行归类、统计、分析

当然也有PC端的展现

本项目采用一入口,一平台,多支撑的模式进行设计与开发,其中:

  • 一入口 - 微信入口,采用chatbot模式实现用户与系统的交互与应答。

  • 一平台 - botPlatform:托管chatbot,启动wechaty实例,接收消息,按状态机模式处理基础消息响应与逻辑分发。

  • 多支撑 - paddleWorkers:使用paddleHub提供的支撑服务,本项目中使用paddle提供的图片OCR解析微信消息中的图片文字,今后可拓展不同的paddle服务,支撑chatbot实现更多功能。

botPlatform-托管chatbot

技术路线为NodeJs+Express+MongoDB,主要关键技术为:状态机、分词与关键词提取 由于整体代码量巨大,因此本次只上传了关键部分代码

序号 模块名称 功能 代码 1 CMX-CoreHandler 实现用户认证、用户管理、角色权限等功能 无 1.1 user.js 用户相关功能 无 1.2 bot.js chatbot相关功能 1.3 application.js 应用相关功能 无 2 CMX-FileHandler 实现文件处理、自动归档,网盘功能 无 3 CMX-ResourceHandler 实现流程处理、表单数据处理功能 无

这里主要是botWechatMap变量在后面的login过程限制了可以扫码的微信用户白名单

var _LANG = 'ch';//默认中文
const BOTCONFIG = {
  autoregistHello: 'hello bot',
  botWechatMap: {
    porbello: 'https://u.wechat.com/MG3oDlaSML_iJ3AN6me3Uv4'//不是随意的微信扫码都有效,这里配置了白名单
  },
  language: {
    ch: {
      hello: '您好,我是您的专属助手',
      //...等等其它语言
    }
  }
};

config/index.js

const config = {
	bot: {
      enable: true,//开启机器人服务
      tokens: ['puppet_padlocal_e55XXXXXXXX55906d2']//如果有多个token,可以启动多个实例
  	},
}

启动wechaty实例

var bots = [];
if (config.bot && config.bot.enable) {
  for (let i = 0; i < config.bot.tokens.length; i++) {//如果有多个token,则循环运行实例
    const bot = new Wechaty({
      puppet: new PuppetPadlocal({
        token: config.bot.tokens[i]
      }),
      name: 'BotBay'
    });
    bot.cmx = bot.cmx || {};
    bot.cmx.use = false;
    bots.push(bot);
    bot
      .on('scan', (qrcode, status) => {
        bot.cmx.qrcode = qrcode;//有二维码时赋值
      })
      .on('login', async (user) => {
        console.log(`User ${user} logged in`);
        const contact = bot.userSelf();
        if (BOTCONFIG.botWechatMap[contact.id]) {
          console.log(`check pass`);
          bot.cmx.use = true;//本bot状态是否为待机
          bot.cmx.qrcode = '';//把二维码输出到前端管理页面用
          bot.cmx.wechatqr = BOTCONFIG.botWechatMap[contact.id];//给前端管理页面显示本bot对应的微信二维码
        } else {
          console.log(`check fail`);//如果不是白名单微信扫码,则强制登出,这里复现貌似登出后不能立刻回调到scan事件
          await bot.logout();
          //TOOD 不知道是否需要主动重启wechaty
        }

      })
      .on('logout', user => {
        console.log(`User ${user} log out`);
        bot.cmx.use = false;//bot状态置为待机
      })
      .on('error', e => console.info('Bot', 'error: %s', e))
      .on('message', message => onMessage(message, bot))
      .on('friendship', friendship => onFriendship(friendship, bot))
      .on('room-join', (room, inviteeList, inviter) => onRoomJoin(room, inviteeList, inviter, bot))
      .start();
  }
}

获取我自己的bot

async function getMyBot(wechatid) {
  return new Promise((resolve, reject) => {
    Models.Botlists.findOne({//里面存储了每个人bot的信息
      wechatid: wechatid
    }).lean().exec((err, data) => {
      if (err || !data) resolve(false);
      else {
        if (data.owner) {
          Models.Userworkspacelinks.findOne({
            user: data.owner
          }).lean().exec((dmErr, dmData) => {
            if (dmErr || !dmData) resolve(false);
            else resolve(Object.assign(data, {
              workspace: dmData.workspace
            }));
          });
        } else {
          resolve(false);
        }
      }
    });
  });
}

其中bot的字段大致如下

const botlistsScheMa = new Schema({
    nickname: String,//昵称
    owner: String,//所属人
    expires: { type: Date },//过期时间
    state: { type: Number, default: 1 },//状态
    birthday: { type: Date, default: Date.now },//生日
    desc: String,//个人简介
    worldranking: Number,//排名
    level: Number,//等级
    wechatid: String,//关联微信号
    hello: String//自定义触发语
});

状态机状态枚举

  1. HELLO - 初始化状态,新添加机器人为好友或使用“变身机器人”触发
  2. WAITUSERNAME - 检查发现不明确用户账户,等待账户信息
  3. WAITNICKNAME - 检查用户尚未给本机器人起名,等待昵称信息
  4. FREE - 目前基础信息完整,响应交互

在下文中状态机在不同时机发生状态变化

async function onFriendship(friendship, bot) {
  const contact = friendship.contact();
  if (friendship.type() === bot.Friendship.Type.Receive) { // 1. receive new friendship request from new contact
    let hasbotinfo = await ((key, wechatid) => {
      return new Promise((resolve, reject) => {
        Models.Botlists.findOne({
          $or: [{ hello: key }, { wechatid: wechatid }]//根据触发语或微信号检查是否已有机器人
        }).lean().exec((err, data) => {
          if (err) resolve(false);
          else resolve(data || false);
        });
      });
    })(friendship.hello(), contact.id);
    if (hasbotinfo === false && friendship.hello() == BOTCONFIG.autoregistHello) {//autoregistHello是默认通用的触发语
      hasbotinfo = 'new wechat user';
    }
    if (hasbotinfo !== false) {
      await friendship.accept();//接收好友申请
      console.log(`Request from ${contact.name()} is accept succesfully!`);
      if (hasbotinfo == 'new wechat user') {
        RedisClient.set('BOT-' + contact.id, 'WAITUSERNAME');//状态机置为等待账户名
        await fsmJob(bot, contact);
      } else {
        if (!isEmpty(hasbotinfo.nickname)) {
          RedisClient.set('BOT-' + contact.id, 'HELLO');//状态机置为打招呼
          await fsmJob(bot, contact, hasbotinfo.nickname);
        } else {
          RedisClient.set('BOT-' + contact.id, 'WAITNICKNAME');//状态机置为等待昵称
          await fsmJob(bot, contact);
        }
        await ((_query, _updatedata) => {
          return new Promise((resolve, reject) => {
            Models.Botlists.updateOne(_query, _updatedata, (err, data) => {
              if (err) {
                console.error(err);
                resolve(false);
              } else resolve(data);
            });
          });
        })({
          _id: hasbotinfo._id
        }, {
          wechatid: contact.id//更新一下微信号
        });
      }
    } else {
      RedisClient.del('BOT-' + contact.id);
      console.log(`no exist botinfo from ${friendship.hello()}`);
    }
  } else if (friendship.type() === bot.Friendship.Type.Confirm) { // 2. confirm friendship
    console.log(`New friendship confirmed with ${contact.name()}`);
  }
}
async function onMessage(msg, bot) {
  const contact = msg.talker();
  if (contact.id == 'wexin' || msg.self()) {
    return;
  }
  await fsmJob(bot, contact, msg, true);//直接调用状态机动作
}

状态机动作

async function fsmJob(bot, contact, msg, reply) {
  let FSM = await (() => {//查询当前用户状态
    return new Promise((resolve, reject) => {
      RedisClient.get('BOT-' + contact.id, function (err, result) {
        if (err) {
          resolve(false);
        } else {
          resolve(result || '');
        }
      });
    });
  })();
  if (FSM)
    await botDoProcess[FSM](bot, contact, msg, reply);//直接执行对应动作
  else {//说明是新用户
    if (reply && msg) {//说明用户主动发消息给bot
      //...有若干代码,主要思想就是根据用户发的消息,进行相应处理
    }
  }
}

主要逻辑在botDoProcess变量中实现,

const botDoProcess = {
   WAITUSERNAME: async (bot, contact, msg, reply)=>{//接收到的是用户账户名,检查数据库是否存在,存在则与bot绑定},
   WAITNICKNAME: async (bot, contact, msg, reply)=>{//接收到的是bot昵称,更新数据库},
   FREE: async (bot, contact, msg, reply)=>{//处理指令【纪要、纪要发送邮箱、日报、帮助等】,对文本、音频、视频、附件、图片进行处理、归档、统计、分析},
   HELLO: async (bot, contact, nickname)=>{
     await contact.say(BOTCONFIG.language[_LANG].hello + nickname);
     RedisClient.set('BOT-' + contact.id, 'FREE');//空闲
     await fsmJob(bot, contact);
   },
}

分词与关键词提取

使用CppJieba提供底层分词算法实现

paddleWorkers-提供chatbot支撑服务

本次只使用了图片OCR这一个功能,并且封装为http接口【因为pyton实现的paddleWorker,nodejs实现的botPlatform】,暴露给botPlatform使用,得力于paddlehub的组件成熟度,所以代码量很少,这里给paddlehub点个赞

from flask import request, Flask
import json
import paddlehub as hub
import cv2
import requests
import os


app = Flask(__name__)
ocr = None


@app.route('/imageOcr', methods=['GET'])
def image_ocr():
    path = request.args.get('imagePath')
    print(path)
    file_name = os.path.basename(path)
    file_down = requests.get(path)
    with open('/mnt/'+file_name,'wb') as f:
        f.write(file_down.content)
    ocr_res = ocr.recognize_text(images=[cv2.imread('/mnt/'+file_name)])
    data = ocr_res[0]['data']
    res_data = {}
    text = []
    for item in data:
        text.append(item['text'])
    res_data['msg'] = '请求成功'
    res_data['code'] = 200
    res_data['data'] = text
    os.remove('/mnt/'+file_name)
    return json.dumps(res_data,ensure_ascii=False)



def load_model():
    global ocr
    ocr = hub.Module(name="chinese_ocr_db_crnn_server")


if __name__ == "__main__":
    load_model()
    app.run(host="0.0.0.0", port=9000)

webpages-实现前端页面

虽然写前端页面的工作相比于高大上的机器学习、深度学习、人工智能、自然语言处理这些门类,显得不上档次,但是有一个"友好一点点"的界面总还算是件好事。

基本上就是这个样子按组件化编写的页面

<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
    <div class="duplicate-file-item border-size p-size dark-bg9">
    <p class="item-title">
        <img src="~assets/img/information-archiving/[email protected]" alt />群聊文件
    </p>
    <duplicate-file v-if="fileBot.length" :data="fileBot"></duplicate-file>
    <a-empty
        v-else
        :image="require('~/static/images/error/[email protected]')"
        :image-style="{
        height: '95px',
        }"
    >
        <span slot="description">暂无数据~</span>
    </a-empty>
    </div>
</a-col>

开发语言为VUE,使用Echarts的图表,这部分就不赘述如何开发的了,按设计稿实现就好了。

尚未解决问题

  1. 目前版本基于状态机的消息处理逻辑是不能应答非标准化的指令的,可以通过引入自然语言处理和多轮对话技术辅助触发状态变化;
  2. 由于wechaty原理基于微信号的消息收发,所以存在添加好友人数上线,目前本方案可支持不添加好友情况下,在微信群中@机器人的方式进行交互,但复杂场景下还是需要添加好友的。考虑到这点,botPlatform在最开始的配置信息中,预留了多个wechaty实例使用的token数组,并且通过循环创建的方式,可以在服务器端启动多个wechaty实例待机,并且根据策略派发实例响应用户交互(ps.不成熟);
  3. 基于wechaty作为消息收发中枢的模式无法满足生产环境下高可用的要求,如果一个实例宕机,其它实例目前没有平滑无缝接管服务的方式,所以只能多拜拜大神了。

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK