commit v1.0 sync aistudio
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实现更多功能。
技术路线为NodeJs+Express+MongoDB,主要关键技术为:状态机、分词与关键词提取 由于整体代码量巨大,因此本次只上传了关键部分代码
这里主要是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,可以启动多个实例 }, }
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(); } }
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//自定义触发语 });
在下文中状态机在不同时机发生状态变化
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提供底层分词算法实现
本次只使用了图片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)
虽然写前端页面的工作相比于高大上的机器学习、深度学习、人工智能、自然语言处理这些门类,显得不上档次,但是有一个”友好一点点”的界面总还算是件好事。
基本上就是这个样子按组件化编写的页面
<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/icon@2x.png" alt />群聊文件 </p> <duplicate-file v-if="fileBot.length" :data="fileBot"></duplicate-file> <a-empty v-else :image="require('~/static/images/error/no-data@2x.png')" :image-style="{ height: '95px', }" > <span slot="description">暂无数据~</span> </a-empty> </div> </a-col>
开发语言为VUE,使用Echarts的图表,这部分就不赘述如何开发的了,按设计稿实现就好了。
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802032778号
项目背景-BOTBAY机器人港湾
BotBay致力于创建专属的拟人机器人,我们理想中它可以接入不同的平台【微信、5G】,作为每一人完成日常工作生活专属助理,你可以给它起一个名字,这样就可以伴随终身,我们希望无论你今后的工作生活如何变化,它都可以普适的服务能力,目前版本我们赋能BotBay工作消息整理和待办提醒功能,例如: 1.把机器人拉进群,帮助我记录群里面的文字、图片、文件,并自动将文件存储到云盘,文字经过过滤后形成纪要; 2.在群里面@我或者私聊我,要求查看当日信息“日报”、“纪要”、并支持将“纪要发送邮箱”; 3.模拟一个工作任务,看看机器人如何提醒我的。
作品演示
视频
B站链接
部分截图
账号绑定和给机器人起名字
停止与启动机器人应答
自动纪要生成
群文件、图片、音频、视频自动归档-移动端
待办提醒与代操作
信息归档日报
平台架构
本项目采用一入口,一平台,多支撑的模式进行设计与开发,其中:
一入口 - 微信入口,采用chatbot模式实现用户与系统的交互与应答。
一平台 - botPlatform:托管chatbot,启动wechaty实例,接收消息,按状态机模式处理基础消息响应与逻辑分发。
多支撑 - paddleWorkers:使用paddleHub提供的支撑服务,本项目中使用paddle提供的图片OCR解析微信消息中的图片文字,今后可拓展不同的paddle服务,支撑chatbot实现更多功能。
核心逻辑
botPlatform-托管chatbot
配置信息
config/index.js
启动wechaty实例
获取我自己的bot
其中bot的字段大致如下
状态机状态枚举
在下文中状态机在不同时机发生状态变化
添加好友
接收消息
状态机动作
主要逻辑在botDoProcess变量中实现,
分词与关键词提取
使用CppJieba提供底层分词算法实现
paddleWorkers-提供chatbot支撑服务
webpages-实现前端页面
基本上就是这个样子按组件化编写的页面
开发语言为VUE,使用Echarts的图表,这部分就不赘述如何开发的了,按设计稿实现就好了。
尚未解决问题