基于SmartQQ协议的QQ聊天机器人-5

本节主题是项目回顾,从总体上分析QQ机器人的数据流

1. 项目的生命周期:

/**
 * 下面是我的理解和注释:
 * 本模块功能:提供各种qq服务的基础函数库 
 * 项目的运行流程是:
 * 比如,我现在1.0版最主要的一个功能就是:接受群消息,并自动回复
 * 它的流程是:
 * 0. 系统在哪里加载了XiaoVGetUpServlet呢???{因为整个项目只有XiaoVGetUpServlet调用了QQService.java},而QQService必须启动
 * 答案见:XiaoVGetUpServlet.java中我的注释 {其实是一个非常简单的机制,在下面}
 * 
 * 1. 本项目的web.xml中定义了XiaoVGetUpServlet.java的map,系统在latke.properties中定义了项目启动的第一个request,然后导向到DispatcherServlet,依次导向到QRCodeShowServlet,然后是XiaoVGetUpServlet
 * 2. XiaoVGetUpServlet中使用了QQService的initQQClient,initQQClient中定义了用于回复消息的callback{即onGroupMessage};initQQClient又使用了SmartQQClient
 * 3. SmartQQClient会做三件事:
		 * 每次new一个SmartQQClient,就回去执行同名的构造方法SmartQQClient
		 * 1. 登陆
		 * 2. 开启一个守护线程,循环地跑:做一件事:接收消息pollMessage
		 * 3. 本执行callback {即onGroupMessage}
 * 4. onGroupMessage又会去调用onQQGroupMessage,{这两个名字很像,不要看错了,我一开始就看晕了。。。}
 * 5. onQQGroupMessage依次做了三件事:1. 判断提问是否“感兴趣”;2.如果感兴趣就调用answer获得提问的答案;3.并把答案给sendMessageToGroup
 * 6. sendMessageToGroup会调用SmartQQClient类的sendMessageToGroup{好坑,同名的。。。},把答案以response的形式发送到对应群
 * 
 */

2. 对应的核心代码分析:

2.1 QQService.java

... //省略一部分非核心代码,见源码
@Service
public class QQService {
    
    ...// 省略

    /**
     * Initializes QQ client.
     */
    public void initQQClient() {
        LOGGER.info("开始初始化小薇");

        xiaoV = new SmartQQClient(new MessageCallback() {
        	// 有个疑问:这里new了一个SmartQQClient,而SmartQQClient的构造方法里面是有login的,但是事实是我只登陆了一次
            @Override
            public void onMessage(final Message message) {
                new Thread(() -> {
                    try {
                        Thread.sleep(500 + RandomUtils.nextInt(1000));

                        final String content = message.getContent();
                        final String key = XiaoVs.getString("qq.bot.key");
                        if (!StringUtils.startsWith(content, key)) { // 不是管理命令,只是普通的私聊
                            // 让小薇进行自我介绍
                            xiaoV.sendMessageToFriend(message.getUserId(), XIAO_V_INTRO);

                            return;
                        }

                        final String msg = StringUtils.substringAfter(content, key);
                        LOGGER.info("Received admin message: " + msg);
                        sendToPushQQGroups(msg);
                    } catch (final Exception e) {
                        LOGGER.log(Level.ERROR, "XiaoV on group message error", e);
                    }
                }).start();
            }

            @Override
            public void onGroupMessage(final GroupMessage message) {
                new Thread(() -> {
                    try {
                        Thread.sleep(500 + RandomUtils.nextInt(1000));

                        onQQGroupMessage(message);
                    } catch (final Exception e) {
                        LOGGER.log(Level.ERROR, "XiaoV on group message error", e);
                    }
                }).start();
            }

            @Override
            public void onDiscussMessage(final DiscussMessage message) {
                new Thread(() -> {
                    try {
                        Thread.sleep(500 + RandomUtils.nextInt(1000));

                        onQQDiscussMessage(message);
                    } catch (final Exception e) {
                        LOGGER.log(Level.ERROR, "XiaoV on group message error", e);
                    }
                }).start();
            }
        });

        reloadGroups();
        reloadDiscusses();

        LOGGER.info("小薇初始化完毕");
    }

    ...//省略

    private void sendMessageToGroup(final Long groupId, final String msg) {
        Group group = QQ_GROUPS.get(groupId);
        if (null == group) {
            reloadGroups();

            group = QQ_GROUPS.get(groupId);
        }

        if (null == group) {
            LOGGER.log(Level.ERROR, "Group list error [groupId=" + groupId + "], 请先参考项目主页 FAQ 解决"
                    + "(https://github.com/b3log/xiaov#报错-group-list-error-groupidxxxx-please-report-this-bug-to-developer-怎么破),"
                    + "如果还有问题,请到论坛讨论帖中进行反馈(https://hacpai.com/article/1467011936362)");

            return;
        }

        LOGGER.info("Pushing [msg=" + msg + "] to QQ qun [" + group.getName() + "]");
        xiaoV.sendMessageToGroup(groupId, msg);
    }

    ..//省略
    
    /*
     * 这是我的注释1.0
     * 这个函数非常重要,我会抽时间把这个函数讲清楚 TODO
     * */

    private void onQQGroupMessage(final GroupMessage message) throws SQLException {
        final long groupId = message.getGroupId();
        final long userId = message.getUserId();// 获取消息的sender的QQ号,与机器人的QQ做比较
        //LOGGER.debug(Long.toString(userId));
        //final long botId = XiaoVs.getInt("qq.bot.id");//从配置文件中读当前机器人的QQ号 {还有点bug,后面再修}
        
        // 为了解决2872995315溢出的问题,只能把userId和机器人ID比较由 Long比较   转化成  字符串比较
        String s_userId = Long.toString(userId);
        //final String s_botId = "2872995315";//暂时写死
        final String s_botId = XiaoVs.getString("qq.bot.id"); //从xiaov.properties配置文件中读

        final String content = message.getContent();
        final String userName = Long.toHexString(message.getUserId());
        // Push to third system
        String qqMsg = content.replaceAll("\["face",[0-9]+\]", "");
        if (StringUtils.isNotBlank(qqMsg)) {
            qqMsg = "<p>" + qqMsg + "</p>";
            sendToThird(qqMsg, userName);
        }

        String msg = "";
        // 下面是对于QQ用户提问的语句进行合法性分析,如果符合规则,那就收集答案,并发送到QQ群 {要避免机器人自问自答的情况发生}
        /*
        if (StringUtils.contains(content, XiaoVs.QQ_BOT_NAME)
                || (StringUtils.length(content) > 6
                && (StringUtils.contains(content, "?") || StringUtils.contains(content, "?") || StringUtils.contains(content, "问")))) {
            msg = answer(content, userName);
        }*/
        if ( StringUtils.contains(content, XiaoVs.QQ_BOT_NAME) // TODO:这里是对提问的基本要求{过滤不合法的提问}
             || (StringUtils.length(content) > 0) && !(s_userId.equals(s_botId))  ) { //彻底解决了机器人自问自答的bug
            msg = answer(content, userName);
        }
        
        if (StringUtils.isBlank(msg)) {
            return;
        }

        if (RandomUtils.nextFloat() >= 0.9) {
            Long latestAdTime = GROUP_AD_TIME.get(groupId);
            if (null == latestAdTime) {
                latestAdTime = 0L;
            }

            final long now = System.currentTimeMillis();

            if (now - latestAdTime > 1000 * 60 * 30) {
                msg = msg + "。
" + ADS.get(RandomUtils.nextInt(ADS.size()));

                GROUP_AD_TIME.put(groupId, now);
            }
        }

        sendMessageToGroup(groupId, msg);
    }

    ...//省略
    
    /*
     * 这是我对xiaov-1.0的注释1.0
     * 这个函数非常重要,定义了提问和回答这两个功能的数据结构及数据来源
     * 我会抽时间把这个函数讲清楚 TODO
     * */

    private String answer(final String content, final String userName) throws SQLException {
        if (keywords.size() == 0) // 加载一次即可
        {
            // 第一次使用,注册下jdbc驱动,通过new一个AnswersFromSQLite对象来激发static代码块
            // AnswersFromSQLite namexxx = new AnswersFromSQLite();
            // 获取keys,只调用一次
            keywords = AnswersFromSQLite.getAllKeys();
        }
        // LOGGER.debug(keywords.get(0));// 测试下content
        
        String keyword = "";
        for (final String kw : keywords) {
            if (StringUtils.containsIgnoreCase(content, kw)) {
                keyword = kw;
                break;
            }
        }
        
       // LOGGER.debug(content);// 测试下content
       // LOGGER.debug(keyword);// 测试下keyword有没有捕捉到

        String ret = "";
        String msg = replaceBotName(content);
        if (StringUtils.isNotBlank(keyword)) {
            try {
                ret = AnswersFromSQLite.getValue(keyword);//我自定义的回复函数
            	ret= URLEncoder.encode(ret, "UTF-8");
            } catch (final UnsupportedEncodingException e) {
                LOGGER.log(Level.ERROR, "Search key encoding failed", e);
            }
        } else if (StringUtils.contains(content, XiaoVs.QQ_BOT_NAME) && StringUtils.isNotBlank(msg)) {
            if (1 == QQ_BOT_TYPE && StringUtils.isNotBlank(userName)) {
              ... //省略
        }
        
        try {
        	ret= URLDecoder.decode(ret, "UTF-8");
        } catch (final UnsupportedEncodingException e) {
            LOGGER.log(Level.ERROR, "ret decoding failed", e);
        }
        return ret;
    }
    
... //省略
   
}

2.2 XiaoVGetUpServlet.java

/**
 * 下面是我的注释:
 * 在web.xml定义了一个Servlet配置项,就是把一个url路由和这个XiaoVGetUpServlet类绑定了,后来只要访问那个url,就跳转到这个class来处理
 * 问题来了,访问那个url被谁读了呢?我需要进一步看源码找答案!!!
 * 上述的疑问其实是对jvm加载web.xml的机制不熟悉导致的:
 * 但是:
 * 不用管,因为本项目的web.xml配置了XiaoVGetUpServlet为<load-on-startup>3</load-on-startup>,就是说优秀级为第3自动加载
 * 一旦项目启动,读取web.xml,然后等待优先级到了3,就自动调用XiaoVGetUpServlet的init(),接下来的流程见QQService.java中我的描述
 */
public class XiaoVGetUpServlet extends HttpServlet {

    /**
     * Serial version UID.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(XiaoVGetUpServlet.class);

    /**
     * Bean manager.
     */
    private LatkeBeanManager beanManager;

    @Override
    public void init() throws ServletException {
        new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (final Exception e) {
                LOGGER.log(Level.ERROR, e.getMessage());
            }

            beanManager = Lifecycle.getBeanManager();

            final QQService qqService = beanManager.getReference(QQService.class);
            qqService.initQQClient();
        }).start();
    }
}

2.3 SmartQQClient.java

/**
 * 下面是我的理解和注释:
 * Api客户端.
 * 每次new一个SmartQQClient,就回去执行同名的构造方法SmartQQClient
 * 1. 登陆
 * 2. 开启一个守护线程,循环地跑:做一件事:接收消息pollMessage
 * 3. 本执行callback
 */
public class SmartQQClient implements Closeable {

    ...//省略
    //线程开关
    private volatile boolean pollStarted;

    /*
     * 每次new 一个SmartQQClient就自动执行构造函数
     * */
    public SmartQQClient(final MessageCallback callback) {
        this.client = Client.pooled().maxPerRoute(5).maxTotal(10).build();
        this.session = client.session();
        login();
        if (callback != null) {
            this.pollStarted = true;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {        	//这就是循环接受群消息的核心所在;这个线程永远在循环地 接收群消息{即循环地监听}
                        if (!pollStarted) {
                            return;
                        }
                        try {
                            pollMessage(callback); //仅仅是接受消息,callback中会定义如何解析消息和回复消息
                        } catch (RequestException e) {
                            //忽略SocketTimeoutException
                            if (!(e.getCause() instanceof SocketTimeoutException)) {
                                LOGGER.error(e.getMessage());
                            }
                        } catch (Exception e) {
                            LOGGER.error(e.getMessage());
                        }
                    }
                }
            }).start();
        }
    }

    /**
     * 登录
     */
    private void login() {
        getQRCode();
        String url = verifyQRCode();
        getPtwebqq(url);
        getVfwebqq();
        getUinAndPsessionid();
        getFriendStatus(); //修复Api返回码[103]的问题
        //登录成功欢迎语
        UserInfo userInfo = getAccountInfo();
        LOGGER.info(userInfo.getNick() + ",欢迎!");
    }

    //登录流程1:获取二维码
    private void getQRCode() {
        LOGGER.debug("开始获取二维码");

        //本地存储二维码图片
        String filePath;
        try {
            filePath = new File("qrcode.png").getCanonicalPath();
        } catch (IOException e) {
            throw new IllegalStateException("二维码保存失败");
        }
        Response response = session.get(ApiURL.GET_QR_CODE.getUrl())
                .addHeader("User-Agent", ApiURL.USER_AGENT)
                .file(filePath);
        for (Cookie cookie : response.getCookies()) {
            if (Objects.equals(cookie.getName(), "qrsig")) {
                qrsig = cookie.getValue();
                break;
            }
        }
        LOGGER.info("二维码已保存在 " + filePath + " 文件中,请打开手机QQ并扫描二维码");
    }

    //用于生成ptqrtoken的哈希函数
    private static int hash33(String s) {
        int e = 0, n = s.length();
        for (int i = 0; n > i; ++i)
            e += (e << 5) + s.charAt(i);
        return 2147483647 & e;
    }

    //登录流程2:校验二维码
    private String verifyQRCode() {
        LOGGER.debug("等待扫描二维码");

        //阻塞直到确认二维码认证成功
        while (true) {
            sleep(1);
            Response<String> response = get(ApiURL.VERIFY_QR_CODE, hash33(qrsig));
            String result = response.getBody();
            if (result.contains("成功")) {
                for (String content : result.split("','")) {
                    if (content.startsWith("http")) {
                        LOGGER.info("正在登录,请稍后");

                        return content;
                    }
                }
            } else if (result.contains("已失效")) {
                LOGGER.info("二维码已失效,尝试重新获取二维码");
                getQRCode();
            }
        }

    }

    //登录流程3:获取ptwebqq
    private void getPtwebqq(String url) {
        LOGGER.debug("开始获取ptwebqq");

        Response<String> response = get(ApiURL.GET_PTWEBQQ, url);
        this.ptwebqq = response.getCookies().get("ptwebqq").iterator().next().getValue();
    }

    //登录流程4:获取vfwebqq
    private void getVfwebqq() {
        LOGGER.debug("开始获取vfwebqq");

        Response<String> response = get(ApiURL.GET_VFWEBQQ, ptwebqq);
        int retryTimes4Vfwebqq = retryTimesOnFailed;
        while (response.getStatusCode() == 404 && retryTimes4Vfwebqq > 0) {
            response = get(ApiURL.GET_VFWEBQQ, ptwebqq);
            retryTimes4Vfwebqq--; 
        }
        this.vfwebqq = getJsonObjectResult(response).getString("vfwebqq");
    }

    //登录流程5:获取uin和psessionid
    private void getUinAndPsessionid() {
        LOGGER.debug("开始获取uin和psessionid");

        JSONObject r = new JSONObject();
        r.put("ptwebqq", ptwebqq);
        r.put("clientid", Client_ID);
        r.put("psessionid", "");
        r.put("status", "online");

        Response<String> response = post(ApiURL.GET_UIN_AND_PSESSIONID, r);
        JSONObject result = getJsonObjectResult(response);
        this.psessionid = result.getString("psessionid");
        this.uin = result.getLongValue("uin");
    }

    /**
     * 获取群列表
     *
     * @return
     */
    public List<Group> getGroupList() {
        LOGGER.debug("开始获取群列表");

        JSONObject r = new JSONObject();
        r.put("vfwebqq", vfwebqq);
        r.put("hash", hash());

        Response<String> response = post(ApiURL.GET_GROUP_LIST, r);
        int retryTimes4getGroupList = retryTimesOnFailed;
        while (response.getStatusCode() == 404 && retryTimes4getGroupList > 0) {
            response = post(ApiURL.GET_GROUP_LIST, r);
            retryTimes4getGroupList--;
        }
        JSONObject result = getJsonObjectResult(response);
        return JSON.parseArray(result.getJSONArray("gnamelist").toJSONString(), Group.class);
    }

    /**
     * 拉取消息
     *
     * @param callback 获取消息后的回调
     * 
     * 下面是我的注释:
     * 这个函数非常重要,有时间需要把它讲清楚 TODO
     */
    private void pollMessage(MessageCallback callback) {
        LOGGER.debug("开始接收消息");

        JSONObject r = new JSONObject();
        r.put("ptwebqq", ptwebqq);
        r.put("clientid", Client_ID);
        r.put("psessionid", psessionid);
        r.put("key", "");
        
     // 先用post(){本质是post方式}把r发给ApiURL.POLL_MESSAGE,得到response
        Response<String> response = post(ApiURL.POLL_MESSAGE, r); 
        JSONArray array = getJsonArrayResult(response);// 改造成JsonArray格式
        for (int i = 0; array != null && i < array.size(); i++) {
            JSONObject message = array.getJSONObject(i);
            String type = message.getString("poll_type");
            if ("message".equals(type)) { // 确认是message是qq私聊的消息类型
                callback.onMessage(new Message(message.getJSONObject("value")));
            } else if ("group_message".equals(type)) { //qq群消息 {这也是目前我使用和研究的模块:QQ群}
                callback.onGroupMessage(new GroupMessage(message.getJSONObject("value")));
            } else if ("discu_message".equals(type)) { //qq讨论组消息
                callback.onDiscussMessage(new DiscussMessage(message.getJSONObject("value")));
            }
        }
    }

    /**
     * 发送群消息
     *
     * @param groupId 群id
     * @param msg     消息内容
     * 
     * 
     * 下面是我的注释:
     * 这个函数也很重要,抽时间讲清楚 TODO
     */
    public void sendMessageToGroup(long groupId, String msg) {
        LOGGER.debug("开始发送群消息");
        JSONObject r = new JSONObject();
        r.put("group_uin", groupId);
        r.put("content", JSON.toJSONString(Arrays.asList(msg, Arrays.asList("font", Font.DEFAULT_FONT))));  //注意这里虽然格式是Json,但是实际是String
        LOGGER.debug(r.get("content"));
        r.put("face", 573);
        r.put("clientid", Client_ID);
        r.put("msg_id", MESSAGE_ID++);
        r.put("psessionid", psessionid);

        Response<String> response = postWithRetry(ApiURL.SEND_MESSAGE_TO_GROUP, r);
        checkSendMsgResult(response);
    }
}

2.4 web.xml

    <listener>
        <listener-class>org.b3log.xiaov.XiaoVServletListener</listener-class>
    </listener>
    
    <filter>
        <filter-name>EncodingFilter</filter-name>
        <filter-class>org.b3log.latke.servlet.filter.EncodingFilter</filter-class>
        <init-param>
            <param-name>requestEncoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>responseEncoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>EncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <session-config>
        <session-timeout>
            60
        </session-timeout>
    </session-config>
    
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name> <!-- 项目启动后第1个就加载这个调度器,执行它的init -->
        <servlet-class>org.b3log.latke.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    
    <servlet>
        <servlet-name>XiaoVGetUpServlet</servlet-name> <!-- 项目启动后第3个就加载这个小薇机器人的唤醒 -->
        <servlet-class>org.b3log.xiaov.processor.XiaoVGetUpServlet</servlet-class>
        <load-on-startup>3</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>XiaoVGetUpServlet</servlet-name>
        <url-pattern>/getup</url-pattern>
    </servlet-mapping>
    
    <servlet>
        <servlet-name>QRCodeShowServlet</servlet-name> <!-- 项目启动后第2个就加载这个二维码处理器 -->
        <servlet-class>org.b3log.xiaov.processor.QRCodeShowServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>QRCodeShowServlet</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>
</web-app>

2.5 我的回复函数AnswersFromSQLite.java

...//省略

public class AnswersFromSQLite {

    public static String getValue(String key) throws SQLException {
        // 测试查询某条记录
        Dao<t_answers, Integer> dao = getDao();
        List<t_answers> ans = queryByOPtions(dao, key);
        // logger.info(ans.get(0).getValue());
        if (ans != null) {
            return ans.get(0).getValue(); // 仅返回第一条记录的value字段
        }
        return null;
    }


    public static List<String> getAllKeys() throws SQLException {
        
        Dao<t_answers, Integer> dao = getDao();
        List<t_answers> t_answers = queryByOPtions(dao);
        List<String> keys = new ArrayList<String>();
        for (t_answers t : t_answers) {
            keys.add(t.getKey());
            // logger.info(t.getKey());
        }
        return keys;
    }

    private static List<t_answers> queryByOPtions(Dao<t_answers, Integer> dao) throws SQLException {
        
        //按条件查询多条记录并分页并倒序 这里用到QueryBuilder
        QueryBuilder<t_answers, Integer> queryBuilder = dao.queryBuilder();

        //queryBuilder.where().eq("is_delete", 0).and().eq("status", 0);
        //queryBuilder.limit((long) 10);
        queryBuilder.where().eq("is_delete", 0); // 执行逻辑上的“有效”查询,以后删除也是“逻辑删除” {除非不得已,不要物理删除}
        queryBuilder.orderBy("id", false);
       
        List<t_answers> ts = dao.query(queryBuilder.prepare());
        return ts;
    }
    
    private static List<t_answers> queryByOPtions(Dao<t_answers, Integer> dao, String key) throws SQLException {
        //按条件查询多条记录并分页并倒序 这里用到QueryBuilder
        QueryBuilder<t_answers, Integer> queryBuilder = dao.queryBuilder();

        //queryBuilder.where().eq("is_delete", 0).and().eq("status", 0);
        //queryBuilder.limit((long) 10);
        queryBuilder.where().eq("is_delete", 0).and().eq("key", key);
        queryBuilder.orderBy("id", false);
       
        List<t_answers> t_answerss = dao.query(queryBuilder.prepare());
        return t_answerss;
    }

    private static Dao<t_answers, Integer> getDao() throws SQLException {
        //     //E:/Software_install/SQLite/Repo/d_xiaov.db
        String databaseUrl = "jdbc:sqlite:D:/Myeclipse15/Workspace/db/d_xiaov.db";//d_xiaov.db放在当前项目根目录下 //【只能写绝对路径,否则SQLite报错】

        //创建一个JDBC连接
        ConnectionSource connectionSource = new JdbcConnectionSource(databaseUrl);
        // logger.info(connectionSource.toString());

        //删除表同时忽略错误
        //TableUtils.dropTable(connectionSource, t_answers.class, true);
        //创建Table
        //TableUtils.createTable(connectionSource, t_answers.class);

        //实例化一个DAO,对表进行数据操作
        Dao<t_answers, Integer> dao = DaoManager.createDao(connectionSource, t_answers.class);
        return dao;
    }
}

2.6 Some Tricks:

1.注意小坑:本项目中Application,java只是SmartQQ协议给的测试demo,与本项目无关,不要被这个名字和它的main方法迷惑(它其实和入口文件甚至是本项目没有任何关系,删掉都可以的)
2.注意一个【大坑】:SQLite数据库在win10下不能使用相对路径创建DB_PATH,【必须写绝对路径】,否则会报错:“找不到表”(其实实际表和数据库及代码都很正常)
3.更多细节:其他函数均属于非核心部分(对实现【群消息自动回复】这个需求而言),我做了部分注释,直接写在每个文件中,有兴趣可以翻看。

3. web.xml的配置与java web 项目启动:

1.java项目的启动入口
2.severlet
3.listenr
4.filter

参考:
https://blog.csdn.net/fjtnylk/article/details/50717753
https://blog.csdn.net/reggergdsg/article/details/52698022
https://blog.csdn.net/guihaijinfen/article/details/8363839
http://www.blogjava.net/xzclog/archive/2011/09/29/359789.html
https://www.cnblogs.com/whgk/p/6399262.html
https://blog.csdn.net/xuke6677/article/details/44752207
https://www.cnblogs.com/ygj0930/p/6374384.html
https://blog.csdn.net/reggergdsg/article/details/52891311
https://blog.csdn.net/reggergdsg/article/details/52821502
https://blog.csdn.net/reggergdsg/article/details/52962774
https://blog.csdn.net/reggergdsg/article/details/53024827

4. 总结:

  1. 目前已实现【关键字,提问和答案】从sqlite读入
  2. TODO: 如某同事0所言,理想的交互方式如下:用户提一个问题,机器人分析用户的提问中的关键字集合,然后返回几个完整的提问句子及其对应答案;返回用户若干个【提示性的提问句子集合】并且每个提问后面附带【答案的编号】,然后用户二次回复【答案编号】,机器人再次回复一个完整的答案句子。
  3. TODO:项目部署到阿里云【先用滴滴云练手】
  4. TODO:接入百度NLP,实现分词和智能API回复