机器人开发文档


  • administrators

    FinoChat Botkit机器人开发框架

    Build Status


    机器人框架主要基于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!"});
    });