我的微信'智障助手'的设计思路

前言

每次写前言最费神,就是感兴趣想研究研究,有了一点点成果希望分享交流,如果能帮助别人就很好,如果有人指导一下就更好了。这次是关于'微信机器人'的个人设计。

功能简介

现在的功能比较简陋,仅实现了聊天机器人(基于图灵机器人API) 和 定时提醒功能(可以实现按时点/周期定制)。但是整体构架是设计出来了,进行扩展 应该 还是挺方便的。放几张图作示例。

登陆部署程序的端口,用个人微信扫码登陆(注意:请先确认微信账号是否能正常登陆wx.qq.com) 登陆图片 扫码图片 扫码后,通过另一个账号发送 召唤智障机器人 激活。大致聊天情况如下 示例 示例 示例 因为功能还不稳定,所以就不挂我自己的程序了。如果稳定了再挂

依赖介绍

  • JDK8
  • Springboot2,最近使用Springboot习惯,写起来顺手,同时希望能部署在服务器上长期运行,所以设计成web服务管理。
  • itchat4j 开源的基于微信web协议开发的个人微信号扩展接口的java实现。免除了自己去研究微信web,感谢。为了符合项目需求,做了一点微小的改动,jar包在源码的libs文件夹下
  • mysql,实现一些数据持久化功能。主要原因在于微信web是有登陆时限的,同时升级程序重启时希望能恢复定时任务、保留用户关联信息等。
  • 图灵机器人API 需要自己申请一个apikey。免费版聊天真的很智障( ╯╰ )

整体设计思路

itchat4j 提供了 Wechat 主类作为入口程序,需要注入一个实现IMsgHandlerFace接口的实现类作为接收消息的回调。这方面我自己完成一个CentreMessageHandler,目前只处理text消息。

    @Override
    public String textMsgHandle(BaseMsg baseMsg) {
        // processorManager处理器的管理类
        BaseProcessor filter = processorManager.decision(baseMsg);
        if (filter != null && filter instanceof TextProcessor) {
            TextProcessor processor = (TextProcessor)filter;
            try {
                return processor.answer(baseMsg);
            }catch (AnswerException e) {
                logger.error("An answerException happened", e);
                return  "我的爸爸写了个bug,可能是缺少女朋友导致,要关心一下吗";
            }
        }
        return null;
    }

我希望程序能更智能,针对不同的输入响应不同的内容,而逻辑不可能都写在一个方法里。所以我学习了一下SpringSecury的Filter链设计,设计了一个Processor链。Processor需要实现我定义的一个接口处理程序

/**
 * 处理器接口, 定义decide用于指示处理器是否响应消息
 *
 * 提供默认的process方法, 用于提供在 {@link Decision} 响应PROCESS时处理消息
 * @author [email protected]
 */
public interface BaseProcessor {
    Logger logger = LoggerFactory.getLogger(BaseProcessor.class);

    /**
     * 是否处理消息
     * @param message
     * @return {@link Decision} 按需返回你的决定
     */
    Decision decide(BaseMsg message);


    /**
     * 处理消息,但不会打断消息的传播
     * @param message
     */
    default void process(BaseMsg message) {
        if ( logger.isDebugEnabled() ) {
            logger.debug("你请求处理了一个消息, 但是却没有实现 class = " + this.getClass().getName());
        }
        // ignore
    }


    enum Decision {
        /**
         * 处理并结束流程
         */
        ACCEPT,
        /**
         * 调用process方法处理消息但不结束流程
         */
        PROCESS,
        /**
         * 不处理但不结束流程
         */
        PASS,
        /**
         * 拒绝并结束流程
         */
        DENY;
    }
}

这个接口核心在于decide方法,decide的返回值决定了消息的处理逻辑,详见enum Decision。BaseProcessor 在ProcessorManager中其实就是一段ArrayList,按预定义好的Processor顺序决定哪个Processor可以获得处理权限,manager的实现是这样的

    /**
     * 决定使用哪个处理组件进行处理
     * @param message
     * @return null 代表不响应
     */
    public BaseProcessor decision(BaseMsg message) {
        for (BaseProcessor filter: processors) {
            switch (filter.decide(message)) {
                case ACCEPT:
                    return filter;
                case PROCESS:
                    filter.process(message);
                    continue;
                case DENY:
                    return null;
                default:
                    // pass
            }
        }
        return null;
    }

我一共设计了5个Processor,执行顺序

  • CommandTextProcessor 强指令响应器, 用于响应固定系统基本指令, 如开启机器人, 关闭机器人等
  • UserFilter 服务响应过滤器, 可以用于指定服务于特定用户
  • ContextService 上下文管理响应器,用于响应连续的对话内容
  • ScheduleProcessor 行程提醒处理器 响应定制提醒及提醒取消
  • TuringTextProcessor 图灵机器人响应器, 调用图灵机器人接口与用户聊天响应 manager中开放了管理Processor的接口,所以可以实现自己的处理逻辑,进行扩展。

如果对Processor具体逻辑感兴趣可以到源码中看,这里我主要介绍一下ContextService 上下文响应处理器。因为我希望程序聊天是可以有情景的,机器人可以响应一段连续的对话,所以额外设计了一个SceneContext的概念。ContextService 的decide方法就是检查用户是否有场景值,场景值是由其他处理器创建的(在本例中,都是由ScheduleProcessor创建的)。SceneContextHolder用于储存场景值。

    @Override
    public Decision decide(BaseMsg message) {
        // 用户场景值有消息
        if ( SceneContextHolder.getArgumentsByUserId(message.getFromUserName()) != null) {
            return Decision.ACCEPT;
        }
        return Decision.PASS;
    }

场景是一段预定义的响应,act是执行方法

/**
 * 场景信息接口
 * @author [email protected]
 */
public interface BaseSceneContext {
    // 定义场景Id,保证程序级别的唯一
    String sceneId();

    String act(String userId, BaseMsg message);

    /**
     * 定义是否在响应后自动移除场景值,默认true
     * @return true 自动移除
     */
    default boolean isRemovedAfterResponse() {
        return true;
    }

    /**
     * 定义会话过期时间 毫秒值 未实现
     * @return -1 永不过期
     */
    long express();
}

那么我如何知道用户的场景呢?这是一段约定,在ScheduleProcessor行程提醒处理器中,用户消息达成了它的要求时,它会根据消息往SceneContextHolder中置入场景

BaseSceneContext context = SceneContextHolder.getSceneBySceneId('定义的场景Id');
// Arguments是场景参数,约定args[0] 存储场景
SceneContextHolder.setArgumentsByUserId(message.getFromUserName(), new Object[]{ context });

这样ContextService就可以通过用户Id拿到场景并执行。 在ChatrobotConfig中会扫描bean,将实现BaseSceneContext的实现类都放到SceneContextHolder中,所以在程序中想新增场景就直接实现BaseSceneContext接口就可以了。

嗯,核心想分享的思路就到这里了,其他的具体实现可以看看源码。我觉得我的程序设计比较繁琐,但是保留了比较好的扩展性,因为也是自己第一次做比较完整的程序设计,所以想分享一下。如果由大佬恰巧看到,愿意指导一下也是极好的。

整个程序目前只能算半成品,修修补补的地方很多。我本想做的差不多再分享一下,不过感觉任重道远,所以提前献丑。

源码传送门

chatrobot

git 关联时少同步了application.properties,主要保存了我私有的key,需要补充完整才能启动

spring.datasource.url = 
spring.datasource.username = 
spring.datasource.password = 
        
chatrobot.turing.key = 在图灵API申请的key

存在的已知问题

  • 程序重启时应从数据库中加载任务,暂时没有实现
  • 用户实体关联还没有建立
  • 收到消息时,都会收到一个fromUserId的属性,程序主要靠这个实现指定收发消息。但根据测试,这个id对用户而言并不是固定的!就是每次重新登陆后好友的UserId会变,需要设计一个方法实现唯一用户关联(nickname可能重复)
  • 服务器部署在外网可能会有异常退出。我第一次对外测试的时候部署在aws上,但是半个多小时后莫名挂了。

Futrue

  • 实现定时任务在启动时加载。
  • 将用户也放入Mysql中进行管理。
  • 其他想到什么写什么

希望多多交流

如果有更好的设计思路,希望能分享一下,Thanks!

猜你喜欢

转载自my.oschina.net/u/3491123/blog/1807004