FinoChat Botkit机器人开发框架
机器人框架主要基于Matrix聊天协议接入FinoChat应用并通过FSM(有限状态机)管理会话状态,以及FinoChat ConvoUI协议扩展实现的聊天机器人。
1. 安装
1.1 npm 安装
npm install finochat-botkit
nodejs版本要求为v9.4.0
2. 原理:
2.1 大致流程
- 基于本框架的应用程序在启动时,就启动matrix聊天协议的客户端,并保持连接,并进行聊天服务器事件的监听
- 用户添加机器人后,机器人受邀自动加入房间,机器人可以发送欢迎消息提示
- 状态机以某个状态进行初始化,机器人接收到用户消息后进行匹配,成功后发给用户发送会话提问消息
- 用户在ConvoUI操作或者文字回复,机器人根据回复消息选择是否推进会话到下一个状态,或者停留在本状态继续会话,甚至结束会话
2.2 ConvoUI协议
- ConvoUI是指聊天对话当中的富文本消息(文字、图片、视频、菜单等等的组合)
- 机器人框架通过封装ConvoUI工厂方法向客户端发送满足协议的扩展消息体,由客户端实现对扩展消息体的具体渲染
3. 使用
3.1 基本用法
3.1.1 配置文件
config.js说明
- homeserver:finoChat聊天服务器接口
- loginUrl:机器人中心登录接口
- fcid:机器人登录ID
- password:机器人登录密码
- ENABLE_MONITOR:是否开启该机器人的监控,默认false
module.exports = {
// 以下配置接口和机器人账号仅供演示测试用
homeserver: process.env.HOMESERVER || "https://api.finolabs.club",
loginUrl: process.env.LOGIN_URL || "https://api.finolabs.club/api/v1/registry/botlogin",
fcid: process.env.FCID || "@test-bot:finolabs.club",
password: process.env.PASSWORD || "123456",
ENABLE_MONITOR: process.env.ENABLE_MONITOR || false
logLevel: 'debug',
timeout: 30000,
routelist: [],
whitelist: [],
blacklist: [],
};
3.1.2 引入依赖
const botkit = require('finochat-botkit');
3.1.3 定义状态
const States = {
INIT: 'INIT',
STEP1: 'STEP1',
STEP2: 'STEP2'
};
3.1.4 机器人开发
继承botkit.Bot类
class demoBot extends botkit.Bot
- 机器人开发者只需继承botkit.Bot类,然后在状态机定义函数 describe()里面进行状态描述和消息匹配,即可集中于书写机器人业务处理逻辑。
- describe(fsm, bot)接收两个参数,fsm状态机实例,bot实例
引入状态机和匹配 API
// 引入状态机API
const { startWith, when, goto, stay, stop } = botkit.DSL(fsm);
// 引入匹配API
const { match, BodyCase, ActionCase, CommandCase, DefaultCase } = botkit.Matcher;
状态机API基本用法
// 初始化状态,接收一个状态参数
startWith(States.INIT);
// 描述某个状态,接收一个状态参数,返回一个asnyc回调函数
// sender发送消息的用户信息,content用户发送的消息体(注意:content.body才是消息体文本内容)
when(States.STEP1)(asnyc (sender, content) => {
// 机器人发送消息接口,第一个参数为房间ID,第二个参数为JSON消息体
bot.sendMessage(sender.roomId, { body: 'hello from bot' });
// 机器人发送文本消息接口,第一个参数为房间ID,第二个参数为具体文本
bot.sendMessage(sender.roomId, 'hello from bot');
// 描述状态转移,全部都可以链式调用withConvoMsg方法给用户附带发送消息,其中goto接收下一个状态作为参数
return stop()/stay()/goto(States.STEP2).withConvoMsg('Hi there!');
});
匹配API基本用法
when(States.STEP1)(asnyc (sender, content) => {
// 匹配函数,接收第一个参数(content消息体)和其他具体匹配方法作为后续参数传入
return match(content,
// 普通聊天文本消息匹配,支持字符串和正则表达式,匹配成功后做对应的逻辑处理
BodyCase('Hey bot!')(() => {
return goto(States.STEP1).withConvoMsg('Hi there!');
})
);
});
3.1.5 机器人运行
new demoBot(require('config')).run();
3.2 代码demo
// 引入机器人框架依赖
const botkit = require('finochat-botkit');
// 定义状态机的各种状态
const States = {
INIT: 'INIT',
STEP1: 'STEP1',
STEP2: 'STEP2'
};
// 创建自己的机器人
class demoBot extends botkit.Bot {
// 机器人被邀请加入房间
onJoinRoom(bot, roomId, userId, displayName) {
return `亲,我是DemoBot【roomId: ${roomId}, userId: ${userId}, displayName: ${displayName}】`;
}
// 用户被邀请加入房间
onUserJoinRoom(bot, roomId, userId, displayName) {
return this.onJoinRoom(bot, roomId, userId, displayName);
}
// 用户当前视图切换入房间
onUserEnterRoom(bot, roomId, userId, displayName) {
return this.onJoinRoom(bot, roomId, userId, displayName);
}
// 会话超时结束
onTimeout(bot, roomId, userId, displayName) {
return `会话结束【roomId: ${roomId}, userId: ${userId}, displayName: ${displayName}】`;
}
// 状态机定义函数, 主要的逻辑写在这里
describe(fsm, bot) {
/**
* 状态机 DSL
* startWith 描述状态机的初始状态和初始 data,只需调用一次
* when 描述某个状态下,发生Event时,Bot业务执行与状态迁移的细节
* goto 用于生成when()函数的返回值,返回 nextState
* stay goto(CurrentState)的另一种形式,停留在本状态
* stop goto(Done)的另一种形式,结束会话
*/
const { startWith, when, goto, stay, stop } = botkit.DSL(fsm);
/**
* matcher API
* BodyCase 匹配普通聊天中的string, 支持变长 pattern(String or RegExp type)
* ActionCase 匹配convoUI消息的action,支持变长 pattern(String or RegExp type)
* CommandCase 匹配convoUI消息的command类型,支持变长pattern(String or RegExp type)
* DefaultCase 模式匹配的Default分支
*/
const { match, BodyCase, ActionCase, CommandCase, DefaultCase } = botkit.Matcher;
// 初始化状态
startWith(States.INIT);
// INIT状态描述
when(States.INIT)(async (sender, content) => {
// 匹配函数,接收第一个参数(content消息体)和其余参数(消息匹配方法)
return match(content,
// 匹配消息成功后回调,返回客户端消息(withConvoMsg方法)并转移到STEP1状态
BodyCase('Hey bot!')(() => {
return goto(States.STEP1).withConvoMsg('Hi there!');
})
);
});
when(States.STEP1)(async (sender, content) => {
return match(content,
ActionCase('action1')(() => {
return goto(States.STEP2).withConvoMsg('U r in Step2 now');
}),
DefaultCase(() => {
// ConvoUI工厂方法创建Assist消息
const ui = botkit.ConvoFactory.ui()
// body文本在ConvoUI无法渲染时显示,类似HTML中img标签的alt提示属性
.setBody('assist demo')
// Layout工厂方法创建带两个按钮的Assist消息
.setPayload(
botkit.LayoutFactory.assist().setTitle('assist').addItems(
botkit.ActionFactory.button('button1', 'action1')
botkit.ActionFactory.button('button2', 'action2')
)
);
return stay().withConvoMsg(ui);
})
);
});
when(States.STEP2)(async (sender, content) => {
return match(content,
BodyCase('apple')(() => {
return stop().withConvoMsg('You can buy an Apple product in https://www.apple.com/');
})
);
});
}
}
// 机器人运行
new demoBot(require('config')).run();
4. 接口
4.1 会话事件
- onJoinRoom(bot, roomId, userId, displayName) :机器人被邀请加入房间
- onUserJoinRoom(bot, roomId, userId, displayName):用户被邀请加入房间
- onUserEnterRoom(bot, roomId, userId, displayName):用户当前视图切换入房间
- onTimeout(bot, roomId, userId, displayName):会话超时结束
4.2 状态机DSL
- startWith:描述状态机的初始状态和初始 data,只需调用一次
- when:描述某个状态下,发生Event时,Bot业务执行与状态迁移的细节
- goto:用于生成when()函数的返回值,返回 nextState
- stay:goto(CurrentState)的另一种形式,停留在本状态
- stop:goto(Done)的另一种形式,结束会话
4.3 消息匹配API
- BodyCase:匹配普通聊天中的string, 支持变长 pattern(String or RegExp type)
- ActionCase:匹配convoUI消息的action,支持变长 pattern(String or RegExp type)
- CommandCase:匹配convoUI消息的command类型,支持变长pattern(String or RegExp type)
- DefaultCase:模式匹配的Default分支
4.4 状态机data的共享
状态机describe()
的方法体内可以使用如下两种方式共享变量(session scope)
- 常规方式是在状态迁移时,将修改后的data对象传递给
using()
. 这里建议通过spread-rest
语法构造 immutable object 对象。后续会方便利用到状态跟踪,重演,TimeTravel Debugging 等很多玩法。 - 还有一种可行的方式是,直接将变量挂在 fsm 上面
4.5 底层API
在回调函数内,可以不借助 matcher API,通过判断 content 或者 data 的具体细节来控制分支走向, 获得最大的灵活度:
when(MyStates.IDLE)(async (sender, content, data) => {
if(content.body === "step1") {
return goto(MyStates.STEP1).withConvoMsg({body: "I goto step1!"});
} else if (content.body === "开始业务2") {
return goto(MyStates.STEP2)
}
return stay().withConvoMsg({body: "Stay!"});
});