From 52b74daa386cd44df2a782d36cce9055a7097f54 Mon Sep 17 00:00:00 2001 From: Richard Chien Date: Sat, 14 Jan 2017 16:14:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3=E3=80=81?= =?UTF-8?q?=E6=B8=85=E7=90=86=E5=9E=83=E5=9C=BE=E3=80=81=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20Travis=20CI=20=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 15 +++++++++++ README.md | 23 +++++++++++++---- docker-compose.yml | 16 ++++++------ docs/Write_Command.md | 2 +- docs/Write_Filter.md | 35 +++++++++++++++----------- docs/Write_NLProcessor.md | 24 ++++++++++++++++++ docs/config.js | 47 +++++++++++++++++++---------------- filters/_frequency_limiter.py | 39 ----------------------------- 8 files changed, 112 insertions(+), 89 deletions(-) create mode 100644 .travis.yml create mode 100644 docs/Write_NLProcessor.md delete mode 100644 filters/_frequency_limiter.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..60ee7bc6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +sudo: required + +language: python +python: + - "3.5" + +services: + - docker + +script: + - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS + - export TAG=`if [[ $TRAVIS_BRANCH =~ ^v[0-9.]+$ ]]; then echo ${TRAVIS_BRANCH#v}; else echo $TRAVIS_BRANCH; fi` + - docker build -f Dockerfile -t $DOCKER_REPO:$TAG . + - if [[ $TRAVIS_BRANCH" =~ "^v[0-9.]+$ ]]; then docker tag $DOCKER_REPO:$TAG $DOCKER_REPO:latest; fi + - docker push $DOCKER_REPO \ No newline at end of file diff --git a/README.md b/README.md index 12534d1b..6d83a8ef 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ #### 使用 Docker -本仓库根目录下的 `docker-compose.yml` 即为 Docker Compose 的配置文件,直接跑就行。如果你想对镜像进行修改,可以自行更改 Dockerfile 来构建或者继承已经构建好的镜像。 +本仓库根目录下的 `docker-compose.yml` 即为 Docker Compose 的配置文件,直接跑就行(某些功能可能需要自行修改一下 `docker-compose.yml` 里的环境变量,例如如果要使用天气功能,需要在里面填上你的和风天气 API KEY)。如果你想对镜像进行修改,可以自行更改 Dockerfile 来构建或者继承已经构建好的镜像。 #### 手动运行 @@ -23,6 +23,8 @@ python app.py 注意要求 Python 3.x。 +你可以通过设置环境变量来控制程序的某些行为,请参考 `docker-compose.yml` 文件中的最后一个容器的环境变量设置。 + ## 使用 ![](https://ww3.sinaimg.cn/large/006tNbRwgw1fb4a75bp2dj30ku1nsaey.jpg) @@ -32,16 +34,27 @@ python app.py 由于 QQ 的限制,现有下列问题: - 可能无法连续在线较长时间,因此需要频繁重启服务(大约一到两天一次) -- 无法处理临时消息和讨论组消息 +- 无法处理临时消息 +- 无法接受图片、语音消息等非文字消息 - 单条消息无法发送很长的内容 - 有时候群消息会被屏蔽,私聊消息则正常 +目前看来微信相比 QQ 要更稳定一些,并且也可以接收图片、语音、视频等,不过有时候需要多次扫码才能登录成功。 + ## 插件 -程序支持两种插件形式,一种是过滤器/Filter,一种是命令/Command。 +程序支持三种插件形式,分别是过滤器/Filter、命令/Command、自然语言处理器/NLProcessor,也即程序的三个处理层次。 + +用户可以自行编写插件来扩展功能,具体请看 [文档](https://cczu-dev.github.io/xiaokai-bot/)。下面简要介绍三层命令的执行流程。 + +### 过滤器 收到消息后,依次运行所有过滤器,即按照优先级从大到小顺序运行 `filters` 目录中的 `.py` 文件中指定的过滤器函数,函数返回非 False 即表示不拦截消息,从而消息继续传给下一个过滤器,如果返回了 False,则消息不再进行后续处理,而直接抛弃。 -命令分发器(`filters/command_dispatcher.py`)是一个预设的优先级为 0 的过滤器,它根据命令的开始标志判断消息中有没有指定命令,如果指定了,则执行指定的命令,如果没指定,则看当前用户有没有开启交互式会话,如果开启了会话,则执行会话指定的命令,否则,使用默认的 fallback 命令。 +### 命令 -用户可以自行编写插件来扩展功能,具体请看 [文档](https://cczu-dev.github.io/xiaokai-bot/)。 +命令分发器(`filters/command_dispatcher.py`)是一个预设的优先级为 0 的过滤器,它根据命令的开始标志判断消息中有没有指定命令,如果指定了,则执行指定的命令,如果没指定,则看当前用户有没有开启交互式会话,如果开启了会话,则执行会话指定的命令,否则,使用默认的 fallback 命令(`config.py` 中 `fallback_command` 指定,默认为 `natural_language.process`)。 + +### 自然语言处理器 + +程序默认的 fallback 命令是 `natural_language.process`,也即自然语言处理命令,这个命令会通过消息的分词结果寻找注册了相应关键词的 NL 处理器并调用它们,得到一个有可能的等价命令列表,然后选择其中置信度最高且超过 60 的命令作为最佳识别结果执行。如果没有超过 60 的命令,则调用另一个 fallback 命令(`config.py` 中 `fallback_command_after_nl_processors` 指定,默认为 `ai.tuling123`)。 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2db45361..73fd3e99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: mojo-webqq-api: - image: daocloud.io/richardchien/mojo-webqq + image: sjdy521/mojo-webqq container_name: mojo-webqq-api networks: - my-net @@ -10,11 +10,11 @@ services: volumes: - /tmp:/tmp environment: - - PORT=5000 - - POST_API=http://xiaokai-bot:8888/qq/ + - MOJO_WEBQQ_PLUGIN_OPENQQ_PORT=5000 + - MOJO_WEBQQ_PLUGIN_OPENQQ_POST_API=http://xiaokai-bot:8888/qq/ restart: always mojo-weixin-api: - image: daocloud.io/richardchien/mojo-weixin + image: sjdy521/mojo-weixin container_name: mojo-weixin-api networks: - my-net @@ -23,18 +23,18 @@ services: volumes: - /tmp:/tmp environment: - - PORT=5001 - - POST_API=http://xiaokai-bot:8888/wx/ + - MOJO_WEIXIN_PLUGIN_OPENQQ_PORT=5001 + - MOJO_WEIXIN_PLUGIN_OPENQQ_POST_API=http://xiaokai-bot:8888/wx/ restart: always xiaokai-bot: - image: daocloud.io/richardchien/xiaokai-bot + image: richardchien/xiaokai-bot container_name: xiaokai-bot networks: - my-net expose: - '8888' volumes: - - ./data/qq-bot:/data + - ./data/xiaokai-bot:/data - /tmp:/tmp environment: - TURING123_API_KEY=YOUR_API_KEY diff --git a/docs/Write_Command.md b/docs/Write_Command.md index 7ee5638d..0060ad93 100644 --- a/docs/Write_Command.md +++ b/docs/Write_Command.md @@ -1,6 +1,6 @@ # 编写命令 -当你需要自己编写命令时,可能需要了解或参考以下内容。 +`commands` 目录中所有不以 `_` 开头的 `.py` 文件会被加载进程序,一般把命令放在这个目录里。对于临时不需要的命令,可以通过在文件名前加 `_` 来屏蔽掉。要自行编写命令,需要涉及到的概念比较多,请参考下面内容,或参考已内置的命令的写法。 ## 命令仓库 Command Registry diff --git a/docs/Write_Filter.md b/docs/Write_Filter.md index c17be954..7785ea51 100644 --- a/docs/Write_Filter.md +++ b/docs/Write_Filter.md @@ -1,13 +1,15 @@ # 编写过滤器 +`filters` 目录中所有不以 `_` 开头的 `.py` 文件会被加载进程序,一般把过滤器放在这个目录里。对于临时不需要的过滤器,可以通过在文件名前加 `_` 来屏蔽掉。 + ## 写法 -编写过滤器比较简单,只需要调用 `filter.py` 中的 `add_filter` 函数,传入过滤器函数和优先级,即可。 +编写过滤器比较简单,只需要调用 `filter.py` 中的 `add_filter` 函数或 `as_filter` 装饰器,传入过滤器函数和优先级,即可。 比如我们需要做一个消息拦截器,当匹配到消息中有不文明词汇,就发送一条警告,并拦截消息不让后续过滤器和命令处理,代码可能如下: ```python -from filter import add_filter +from filter import add_filter, as_filter from commands import core @@ -17,8 +19,16 @@ def _interceptor(ctx_msg): return False return True +add_filter(_interceptor, priority=100) -add_filter(_interceptor, 100) +# 或下面这样 + +@as_filter(priority=100) +def _interceptor(ctx_msg): + if 'xxx' in ctx_msg.get('content', ''): + core.echo('请不要说脏话', ctx_msg) + return False + return True ``` 一般建议优先级设置为 0~100 之间。 @@ -27,14 +37,11 @@ add_filter(_interceptor, 100) ## 现有的几个重要过滤器 -### 消息日志过滤器 - -此过滤器用于把收到的消息打印在标准输出,在 `filters/message_logger_1000.py` 中定义,优先级 1000,一般不建议添加其它优先级比它高的过滤器,以确保日志不受任何干扰。 - -### 分离@开头过滤器 - -用于分离群组和讨论组中消息开头的 `@CCZU 小开`,优先级 50,在 `filters/split_at_xiaokai_50.py` 中定义。通过此过滤器的消息的 `content` 字段会被更新为分离掉开头的剩余部分,也就是说通过此过滤器的消息,就是确定用户的意图就是和这个 bot 说话的消息。 - -### 命令分发过滤器 - -用于处理消息中的命令,优先级 0,在 `filters/command_dispatcher_0.py` 中定义。 +| 文件 | 优先级 | 作用 | 备注 | +| ------------------------------------- | ----- | ---------------------------------------- | -------------------------------------- | +| unitize_context_message_10000.py | 10000 | 对来自不同平台(QQ、微信)的消息上下文进行统一化,以避免耦合 | 不建议添加比它优先级更高的过滤器 | +| message_logger_1000.py | 1000 | 把收到的消息打印在标准输出 | 不建议添加比它优先级更高的过滤器 | +| intercept_some_message_formats_100.py | 100 | 拦截某些不支持的消息类型,对于文本消息,会把 `content` 字段复制到 `text` 字段 | 如果要自己编写插件,这里可以按需修改 | +| speech_recognition_90.py | 90 | 对语音消息进行语音识别(仅私聊消息),并把识别出的文字放到 `text` 字段,并标记 `from_voice` 字段为 True | 如果不需要可以删掉 | +| split_at_xiaokai_50.py | 50 | 分离群组和讨论组中消息开头的 `@CCZU 小开`,并更新 `text` 字段为剩余部分 | 也就是说通过此过滤器的消息,就是确定用户的意图就是和这个 bot 说话的消息 | +| command_dispatcher_0.py | 0 | 识别消息中的命令,并进行相应的调用 | | \ No newline at end of file diff --git a/docs/Write_NLProcessor.md b/docs/Write_NLProcessor.md new file mode 100644 index 00000000..daaf2b0e --- /dev/null +++ b/docs/Write_NLProcessor.md @@ -0,0 +1,24 @@ +# 编写自然语言处理器 + +`nl_processors` 目录中所有不以 `_` 开头的 `.py` 文件会被加载进程序,一般把自然语言处理器(后面称 NL 处理器)放在这个目录里。对于临时不需要的 NL 处理器,可以通过在文件名前加 `_` 来屏蔽掉。 + +## 流程 + +程序执行时 `natural_language.process` 命令会调用 `nl_processor.py` 中的 `parse_potential_commands` 函数来解析可能的等价命令,此函数会对消息文本进行分词,然后进行关键词匹配(关键词在注册 NL 处理器是传入),并调用所有匹配到的 NL 处理器,每个 NL 处理器会返回 None 或一个四元组(从 0 到 3 分别是:置信度(0~100)、命令名、参数、已解析到的数据)。 + +完成后,`parse_potential_commands` 把所有非 None 的结果放在一个 list 返回给 `natural_language.process` 命令,该命令再从中选择置信度最高,且超过 60 的命令执行(在调用之前会把已解析到的数据放在消息上下文的 `parsed_data` 字段)。如果没有置信度超过 60 的命令,则调用 `config.py` 中 `fallback_command_after_nl_processors` 字段指定的命令。 + +## 写法 + +由以上流程可知,在编写 NL 处理器时需要注册关键词,然后返回一个包含可能的等价命令和置信度的四元组。例子如下: + +```python +from nl_processor import as_processor + + +@as_processor(keywords=('翻译(为|成|到)?', '.+(文|语)')) +def _processor(sentence, segmentation): + return 90, 'translate.translate_to', '', None +``` + +注意关键词需要传入一个可迭代对象,每个元素为一个正则表达式字符串;函数接收的参数有且只有两个必填项,第一个为原文本字符串,第二个为使用 jieba 分词之后的分词列表,每个元素都包含 `flag` 和 `word` 两个属性(是对象的属性,不是字典的键),分别是词性标记(jieba 分词的词性标记见 [ICTCLAS 汉语词性标注集](https://gist.github.com/luw2007/6016931#ictclas-汉语词性标注集))和词语的字符串。 \ No newline at end of file diff --git a/docs/config.js b/docs/config.js index eedac48c..258c71be 100644 --- a/docs/config.js +++ b/docs/config.js @@ -1,25 +1,28 @@ self.$config = { - title: 'XiaoKai Bot 文档', - home: 'https://raw.githubusercontent.com/CCZU-DEV/xiaokai-bot/master/README.md', - repo: 'CCZU-DEV/xiaokai-bot', - url: 'https://cczu-dev.github.io/xiaokai-bot', - nav: { - default: [ - { - title: '首页', path: '/' - }, - { - title: '编写插件', type: 'dropdown', - items: [ - { - title: '过滤器', path: '/Write_Filter' - }, - { - title: '命令', path: '/Write_Command' - } + title: 'XiaoKai Bot 文档', + home: 'https://raw.githubusercontent.com/CCZU-DEV/xiaokai-bot/master/README.md', + repo: 'CCZU-DEV/xiaokai-bot', + url: 'https://cczu-dev.github.io/xiaokai-bot', + nav: { + default: [ + { + title: '首页', path: '/' + }, + { + title: '编写插件', type: 'dropdown', + items: [ + { + title: '过滤器', path: '/Write_Filter' + }, + { + title: '命令', path: '/Write_Command' + }, + { + title: '自然语言处理器', path: '/Write_NLProcessor' + } + ] + } ] - } - ] - }, - plugins: [] + }, + plugins: [] }; diff --git a/filters/_frequency_limiter.py b/filters/_frequency_limiter.py deleted file mode 100644 index 7725f080..00000000 --- a/filters/_frequency_limiter.py +++ /dev/null @@ -1,39 +0,0 @@ -from datetime import datetime, timedelta - -from cachetools import TTLCache as TTLDict - -from filter import add_filter -from little_shit import get_target -from commands import core - -_freq_count = TTLDict(maxsize=10000, ttl=2 * 60 * 60) -_max_message_count_per_hour = 120 - - -def _limiter(ctx_msg): - target = get_target(ctx_msg) - if target not in _freq_count: - # First message of this target in 2 hours (_freq_count's ttl) - _freq_count[target] = (0, datetime.now()) - - count, last_check_dt = _freq_count[target] - now_dt = datetime.now() - delta = now_dt - last_check_dt - - if delta >= timedelta(hours=1): - count = 0 - last_check_dt = now_dt - - if count >= _max_message_count_per_hour: - # Too many messages in this hour - core.echo('我们聊天太频繁啦,休息一会儿再聊吧~', ctx_msg) - count = -1 - - if count >= 0: - count += 1 - - _freq_count[target] = (count, last_check_dt) - return count >= 0 - - -add_filter(_limiter, 100)