New generation init

This commit is contained in:
Richard Chien
2018-06-15 06:58:24 +08:00
parent 6ce021926e
commit a3844eda69
61 changed files with 266 additions and 4063 deletions

View File

@ -1,28 +0,0 @@
# 统一消息上下文
消息上下文是本程序中最重要的概念,也是贯穿整个程序执行流程的一个对象,无论命令、过滤器或是其它插件类型,都需要使用这个对象,在代码中常以 `ctx_msg``ctx` 等形式出现,它是字典类型,其中保存了当前(目前正在处理的这条)消息的上报类型、消息内容、消息类型、发送者、接受者、消息源名称等等重要信息。消息源适配器的主要工作,就是将不同消息源的上报数据统一成下方所定义的消息上下文的格式。
下面定义一个统一化之后的消息上下文对象应当符合的形式:
| 字段名 | 是否必须(是/否/建议) | 数据类型 | 支持的值 | 说明 |
| ---------------------------- | ------------ | ---- | --------------------------- | ---------------------------------------- |
| `raw_ctx` | 是 | dict | - | 消息源上报过来的原始数据 |
| `post_type` | 是 | str | `message` | 上报类型,可以随意设置,但此字段值非 `message` 的消息上下文将会被某一层过滤器过滤掉 |
| `time` | 建议 | int | - | 消息、事件等发生的时间戳 |
| `msg_id` | 建议 | str | - | 消息的唯一 ID |
| `msg_type` | 是 | str | `private``group``discuss` | 消息类型,标识私聊消息或群组消息等 |
| `format` | 是 | str | `text``media` 等 | 可随意填写,但实际上能够直接理解的只有 `text`,对于 `media` 格式,只有语音识别过滤器对 Mojo-Webxin 进行了单独支持 |
| `content` | 是 | str | - | 消息内容文本 |
| `receiver` | 建议 | str | - | 接受者显示名(当有备注名时为备注名,否则为昵称,见表格下方注释 1 |
| `receiver_name` | 否 | str | - | 接受者昵称 |
| `receiver_id``receiver_tid` | 建议 | str | - | 接受者 ID分别是固定 ID 和 临时 ID见注释 2 |
| `sender` | 建议 | str | - | 发送者显示名(当有备注名时为备注名,否则为昵称) |
| `sender_name` | 否 | str | - | 发送者昵称 |
| `sender_id``sender_tid` | 是 | str | - | 发送者 ID |
| `group` | 建议 | str | - | 来源群组显示名 |
| `group_id``group_tid` | 是(当为群组消息时) | str | - | 来源群组 ID |
| `discuss` | 建议 | str | - | 来源讨论组显示名 |
| `discuss_id``discuss_tid` | 是(当为讨论组消息时) | str | - | 来源讨论组 ID |
- 注释 1消息的接受者通常情况下表示当前消息源中登录的账号。
- 注释 2所有 `xxx_id``xxx_tid` 分别表示固定 ID 和临时 ID固定 ID`xxx_id`)表示重新登录不会变的 ID通常即为该消息平台的账号微信 ID、QQ 号);临时 ID`xxx_tid`)表示在消息源的此次登录中不会变的 ID但下次登录可能同一个用户的 ID 和上次不同,对于某些平台(如微信),可能有时完全无法获取到固定 ID此时临时 ID 将成为发送消息时的重要依据。当没有(或无法获取)固定 ID 时,可将固定 ID 置空,如果同时也没有临时 ID将意味着程序可能无法回复消息因为没有任何能够唯一标记消息来源的值当有固定 ID 没有临时 ID 时,应直接将临时 ID 设置为和固定 ID 相同。

View File

@ -1,9 +0,0 @@
# 消息源列表
「消息源」在文档的某些位置可能还称为「消息平台」「聊天平台客户端」「消息源客户端」「机器人前端」等。比如网上很多开源的 SmartQQ 封装或网页微信封装,我们这里就称它们为「消息源」,它们的功能通常是模拟登录账号、维护登录状态、上报接收到的消息、通过某种方式调用接口发送消息等。通过适配器,本程序可以支持多种消息源,下面是目前所支持的消息源列表:
| 消息源名称 | 官网/项目地址 | 配置文件中定义时填写项目 |
| -------------- | ---------------------------------------- | ---------------------------------------- |
| mojo_webqq | https://github.com/sjdy521/Mojo-Webqq | `api_url`API 根地址(必填) |
| mojo_weixin | https://github.com/sjdy521/Mojo-Weixin | `api_url`API 根地址(必填) |
| coolq_http_api | https://github.com/richardchien/coolq-http-api | `api_url`API 根地址(必填);`token`:发送请求时的 token设置了的情况下必填 |

View File

@ -1,152 +0,0 @@
# 编写消息源适配器
消息源适配器是用来在消息源和本程序之间进行数据格式的一类程序,相当于一个驱动程序,通过不同的驱动程序,本程序便可以接入多种聊天平台。后文中简称为「适配器」。
通常情况下一个消息源需要能够支持通过 HTTP 来上报消息和调用操作,才能够便于开发适配器,不过实际上如果有需求,你也可以直接在适配器中对程序的 HTTP 服务端进行请求,例如某些直接以模块形式给出的消息平台客户端,通过回调函数来通知事件,此时你可以在这个事件的回调函数中,手动请求本程序的上报地址并发送相应的数据。但这不在此文的讨论范围之内,这属于另一类适配器,与本程序无直接关联。
我们这里讨论在本程序接收到 HTTP 上报消息之后、及内部逻辑中产生了对适配器的接口调用之后,需要将上报数据转换成本程序能够识别的数据格式,或将本程序中发出的接口调用转换成消息源客户端能够识别的接口调用,例如我们调用 `adapter.send_private_message`,相对应的适配器将会在内部通过 HTTP 请求这个消息源客户端的用来发送私聊消息的接口。
为了形象的理解,你可能需要去参考已有的那些适配器的代码。
## 写法
其实写起来非常简单,就和那些 Web 框架的 Handler 一样,继承一个基类,实现某几个固定的函数接口即可,这里需要继承的是 `msg_src_adapter.py` 中的 `Adapter` 类,此基类中已经实现了一些通用的、或默认的逻辑,对于像 `unitize_context`(上报数据统一化)、`send_private_message`(发送私聊消息)、`get_sender_group_role`(获取发送者在群组中的身份)等等接口,通常需要在子类中进行具体的、差异化的操作。
我们直接以 Mojo-Webqq 的适配器 `msg_src_adapters/mojo_webqq.py` 为例,代码如下(可能不是最新):
```python
import requests
from msg_src_adapter import Adapter, as_adapter, ConfigurationError
@as_adapter(via='mojo_webqq')
class MojoWebqqAdapter(Adapter):
def __init__(self, config: dict):
super().__init__(config)
if not config.get('api_url'):
raise ConfigurationError
self.api_url = config['api_url']
def unitize_context(self, ctx_msg: dict):
new_ctx = {'raw_ctx': ctx_msg, 'post_type': ctx_msg['post_type'], 'via': ctx_msg['via'],
'login_id': ctx_msg['login_id']}
if new_ctx['post_type'] != 'receive_message':
return new_ctx
new_ctx['post_type'] = 'message' # Just handle 'receive_message', and make 'post_type' 'message'
new_ctx['time'] = ctx_msg['time']
new_ctx['msg_id'] = str(ctx_msg['id'])
new_ctx['msg_type'] = ctx_msg['type'].split('_')[0]
new_ctx['msg_type'] = 'private' if new_ctx['msg_type'] == 'friend' else new_ctx['msg_type']
new_ctx['format'] = 'text'
new_ctx['content'] = ctx_msg['content']
new_ctx['receiver'] = ctx_msg.get('receiver', '')
new_ctx['receiver_name'] = (requests.get(self.api_url + '/get_user_info').json() or {}).get('name', '')
new_ctx['receiver_id'] = str(ctx_msg.get('receiver_uid', ''))
new_ctx['receiver_tid'] = str(ctx_msg.get('receiver_id', ''))
new_ctx['sender'] = ctx_msg.get('sender', '')
friend = list(filter(
lambda f: f.get('uid') == ctx_msg['sender_uid'],
requests.get(self.api_url + '/get_friend_info').json() or []
))
new_ctx['sender_name'] = friend[0].get('name', '') if friend else ''
new_ctx['sender_id'] = str(ctx_msg.get('sender_uid', ''))
new_ctx['sender_tid'] = str(ctx_msg.get('sender_id', ''))
if new_ctx['msg_type'] == 'group':
new_ctx['group'] = ctx_msg.get('group', '')
new_ctx['group_id'] = str(ctx_msg.get('group_uid', ''))
new_ctx['group_tid'] = str(ctx_msg.get('group_id', ''))
if new_ctx['msg_type'] == 'discuss':
new_ctx['discuss'] = ctx_msg.get('discuss', '')
new_ctx['discuss_tid'] = str(ctx_msg.get('discuss_id', ''))
return new_ctx
def get_login_info(self, ctx_msg: dict):
json = requests.get(self.api_url + '/get_user_info').json()
if json:
json['user_tid'] = json.get('id')
json['user_id'] = json.get('uid')
json['nickname'] = json.get('name')
return json
def _get_group_info(self):
return requests.get(self.api_url + '/get_group_info').json()
def get_sender_group_role(self, ctx_msg: dict):
groups = list(filter(
lambda g: str(g.get('id')) == ctx_msg['raw_ctx'].get('group_id'),
self._get_group_info() or []
))
if len(groups) <= 0 or 'member' not in groups[0]:
# This is strange, not likely happens
return 'member'
members = list(filter(
lambda m: str(m.get('id')) == ctx_msg['raw_ctx'].get('sender_id'),
groups[0].get('member')
))
if len(members) <= 0:
# This is strange, not likely happens
return 'member'
return members[0].get('role', 'member')
def send_private_message(self, target: dict, content: str):
params = None
if target.get('user_id'):
params = {'uid': target.get('user_id')}
elif target.get('user_tid'):
params = {'id': target.get('user_tid')}
if params:
params['content'] = content
requests.get(self.api_url + '/send_friend_message', params=params)
def send_group_message(self, target: dict, content: str):
params = None
if target.get('group_id'):
params = {'uid': target.get('group_id')}
elif target.get('group_tid'):
params = {'id': target.get('group_tid')}
if params:
params['content'] = content
requests.get(self.api_url + '/send_group_message', params=params)
def send_discuss_message(self, target: dict, content: str):
params = None
if target.get('discuss_tid'):
params = {'id': target.get('discuss_tid')}
if params:
params['content'] = content
requests.get(self.api_url + '/send_discuss_message', params=params)
```
代码逻辑上很简单,首先调用 `@as_adapter(via='mojo_webqq')` 把类注册为适配器,`via` 就是配置文件里定义消息源时候要填的那个 `via`,同时也是上报消息路径里的那个 `via`。初始化函数里面要求配置文件中的消息源定义里必须有 `api_url`
`unitize_context` 函数是用来统一上报消息上下文的这个上下文Context是一个字典类型在整个程序中起核心作用此函数需要将消息源发送来的数据转换成本程序能够理解的格式也就是对字段进行翻译需要翻译成一个统一的格式这个格式见 [统一消息上下文](https://cczu-dev.github.io/xiaokai-bot/#/Context)。
其它的函数就是对调用操作的翻译,例如把 `send_group_message` 的调用翻译成对 `self.api_url + '/send_group_message'` 的 HTTP 请求。
### 消息发送目标的定义
由于发送消息使用一个统一接口,插件中调用时并不知道是哪个适配器接收到调用,所以发送消息的目标同样是需要统一的,也即 `send_message` 函数的 `target` 参数,此参数应当和消息上下文兼容,也就是说,当调用发送消息的接口时,直接把消息上下文传入,就应当能正确发送到此消息上下文所在的语境(比如和某个用户的私聊消息或某个群组中)。
它主要应当接受如下字段:
| 字段名 | 说明 |
| -------------------------- | ---------------------------------------- |
| `user_id``user_tid` | 消息要发送的对象(私聊用户)的 ID |
| `group_id``group_tid` | 要发送的群组 ID |
| `discuss_id``discuss_tid` | 要发送的讨论组 ID |
| `content` | 消息内容,通常是 str 类型目前所有适配器只支持发送文本消息str 类型) |
以上所有 `xxx_id``xxx_tid` 分别表示固定 ID 和临时 ID这和消息上下文中的定义一样固定 ID`xxx_id`)表示重新登录不会变的 ID通常即为该消息平台的账号微信 ID、QQ 号),临时 ID`xxx_tid`)表示在消息源的此次登录中不会变的 ID但下次登录可能同一个用户的 ID 和上次不同,对于某些平台(如微信),可能有时完全无法获取到固定 ID此时临时 ID 将成为发送消息时的重要依据。
### 其它
对于需要对群组中用户身份进行区分的情况,例如某些命令只允许群组管理员运行,要实现 `get_sender_group_role` 函数,此函数返回的成员身份应为 `member``admin``owner` 三者之一。

View File

@ -1,120 +0,0 @@
# 编写命令
`commands` 目录中所有不以 `_` 开头的 `.py` 文件会被加载进程序,一般把命令放在这个目录里。对于临时不需要的命令,可以通过在文件名前加 `_` 来屏蔽掉。要自行编写命令,需要涉及到的概念比较多,请参考下面内容,或参考已内置的命令的写法。
## 命令仓库 Command Registry
每个 `.py` 文件,就是一个命令仓库,里面可以注册多个命令,每个命令也可以注册多个命令名。
程序启动时,会自动加载 `commands` 目录下的所有 `.py` 文件(模块)中的 `__registry__` 对象,这是一个 `CommandRegistry` 类型的对象,在创建这个对象的时候,可以指定一个 `init_func` 参数作为初始化函数,将会在命令仓库被加载时调用。
使用 `__registry__` 对象的 `register` 装饰器可用来将一个函数注册为一个命令,装饰器的一个必填参数为命令名,可选参数 `hidden` 表示是否将命令名暴露为可直接调用(即形如 `/command_name` 这样调用),如果此项设为 True 则只能在命令名前加仓库名调用。
同一个函数可以注册多次不同命令名(相当于别名),以适应不同的语境。
`CommandRegistry` 类的 `restrict` 装饰器用于限制命令的调用权限,这个装饰器必须在注册命令到仓库之前调用,表现在代码上就是 `restrict` 装饰器必须在所有 `register` 装饰器下方。关于此装饰器的参数基本上从参数名就能看出含义,具体见 `command.py` 中的代码。
有一点需要注意的是,`full_command_only` 参数和 `register` 装饰器的 `hidden` 参数表现出的特性相同(都是防止直接访问),但不同之处在于它对整个命令有效,无论命令被以不同命令名注册过多少次。这导致 `restrict``full_command_only` 参数和 `register``hidden` 参数的建议使用场景有所不同,比如,当需要添加一个可以确定不希望普通用户意外调用到的命令时,可使用如下方式注册:
```python
@__registry__.register('pro_command')
@__registry__.restrict(full_command_only=True, allow_group=False)
def pro_command(args_text, ctx_msg):
pass
```
而如果需要添加一个希望普通用户使用、同时也让高级用户使用起来更舒适的命令,可能用如下方式注册:
```python
@__registry__.register('list_all', hidden=True)
@__registry__.register('列出所有笔记')
@__registry__.restrict(group_admin_only=True)
def list_all(args_text, ctx_msg):
pass
```
这样可以在保持高级用户可以通过简洁的方式调用命令的同时,避免不同命令仓库下同名命令都被调用的问题(因为在默认情况下命令中心在调用命令时,不同仓库中的同名命令都会被依次调用)。
16.12.29 注:由于 Mojo-Weixin 无法获取到群组中成员的身份(普通成员还是管理员或群主),因此这里的对群组的限制在微信上不起效果,超级用户限制在能够获取到发送者微信 ID 的情况下有效。
17.2.15 注:如果使用 Mojo-Webqq 作为消息源,现在也无法获取群组中的成员身份了,因此造成对群成员身份有要求的命令在此种情况下也无法使用。
## 命令中心 Command Hub
程序启动时加载的命令仓库全部被集中在了命令中心,以 `.py` 文件名(除去后缀)(也即模块名)为仓库名。命令中心实际上应作为单例使用(插件编写者不应当自己创建实例),即 `command.py` 中的 `hub` 对象,类型是 CommandHub调用它的 `call` 方法将会执行相应的命令,如果调用失败(如命令不存在、没有权限等)会抛出相应的异常。
## 命令之间内部调用
调用命令中心的 `call` 方法是一种可行的命令内部调用方法,不过由于 `call` 方法内会进行很多额外操作(例如命令名匹配、权限检查等),所以并不建议使用这个方法来进行内部调用。
一般而言,当编写命令时发现需要调用另一个已有的命令,可以直接导入相应的模块,然后调用那个命令函数,这样避免了冗杂的命令名匹配过程,例如:
```python
from commands import core
@__registry__.register('cmd_need_call_another')
def cmd_need_call_another(args_text, ctx_msg):
core.echo(args_text, ctx_msg)
```
这里直接调用了 `core.echo`
## 数据持久化
可以使用数据库或文件来对数据进行持久化,理论上只要自行实现数据库和文件的操作即可,这里为了方便起见,在 `little_shit.py` 提供了获取默认数据库路径、默认临时文件路径等若干函数。
使用默认的路径,可以保持文件结构相对比较清晰。
## Source 和 Target
对于用户发送的消息,我们需要用某种标志来区分来源,对于用户保存的数据,也要区分这份数据属于谁。在私聊消息的情况下,这个很容易理解,不同的 QQ 号就是不同的来源,然而在群消息的情况下,会产生一点区别,因此这里引入 Source 和 Target 两个概念。
Source 表示命令的来源由谁发出Target 表示命令将对谁产生效果。
在私聊消息中,这两者没有区别。在群聊中,每个用户(如果限制了权限,则可能只有管理员或群主,但这不影响理解)都可以给 bot 发送命令,但命令在后台保存的数据,应当是属于整个群组的,而不是发送命令的这个用户。与此同时,不同的用户在群聊中发送命令时,命令应当能区分他们,并在需要交互时正确地区分不同用户的会话(关于会话的概念,在下一个标题下)。
`commands/note.py` 中的命令为例,多个管理员可以同时开启会话来添加笔记,最终,这些笔记都会存入群组的数据中,因此这些命令通过 Source 来区分会话,用 Target 来在数据库中区分数据的归属。这也是建议的用法。
至于如何获取 Source 和 Target可用 `little_shit.py` 中的 `get_source``get_target` 函数,当然,如果你对默认的行为感到不满意,也可以自己去实现不一样的区分方法。
16.12.29 注:在支持了 Mojo-Weixin 消息源之后,此处有所变化,由于一些限制,有时候无法获得发送者的微信 ID而群组甚至没有一个固定 ID因此这里对 Source 和 Target 做一个精确定义Source 是一个用来表示当前消息的发送者的唯一值但重新登录后可能变化并且每次获取一定可以获取到Target 是一个用来表示当前消息所产生的效果需要作用的对象,这个值是永久(或至少长期)不变的,如果当前的消息语境下不存在这样的值,则为 None。
17.2.15 注:在使用了适配器模式后,`get_source``get_target` 函数的实现实际上已经移到了 `msg_src_adapter.py` 中的 `Adapter` 基类。
## 交互式命令 Interactive Command
通常我们会希望一条命令直接在一条消息中发出然后直接执行就好,但这在某些情况下对普通用户并不友好,因此这里支持了交互式命令,也就是在一条命令调用之后,进行多次后续互动,来引导用户完成数据的输入。
为了实现交互式命令,引入了「会话 Session」的概念。
我们以 `commands/note.py` 里的 `note.take` 命令为例,如果发送命令 `/记笔记`(此为一个别名,和 `/note.take` 等价),且不加参数,那么 `note.take` 命令就认为需要开启交互式会话来引导用户输入需要记录的内容,调用 `interactive.py` 中的 `get_session` 函数,由于原先不存在该 Source 的会话,且传入了 `cmd` 参数,就会新创建一个会话,并注册在 `interactive.py` 中的 `_sessions` 字典(这是一个 TTL 字典,目前写死了有效时间 5 分钟,如果需要实现其它有效时间的会话,请先自行实现)。在这个获取的会话对象中,可以保存当前会话的状态和数据。
注意这里的会话对象,是每个 Source 对应一个。一旦创建了一个会话对象,该 Source 原来可能对应的会话就会被关闭。
另外,主程序 `app.py` 中处理接收到的消息时,面临两种消息,一种是命令,一种是不带命令的普通消息,对这两种消息,分别作如下处理:
- 如果是命令,那么不管前面是不是在会话中,都会清除原来的会话,然后启动新的命令(至于新的命令会不会开启新的会话,并没有影响);
- 如果是普通消息,那么如果当前 Source 在某个会话中,就会将消息内容作为参数,调用该会话的命令,如果没有在会话中,则调用 fallback 命令(一般让图灵机器人去处理)。
除了获取会话对象,还需要在命令中自己实现一个状态机,根据会话对象中保存的状态来判断当前这个 Source 处在交互式命令的哪一个阶段。
总体来说交互式命令相比普通命令写起来更复杂一点,具体写法可以参考 `commands/note.py`
## 计划任务型命令
高级用户可以使用 `commands/scheduler.py` 里的命令来添加计划任务以定期执行某一个或一连串命令,但对普通用户来说可能较难使用,因此对于可能有需要定期执行的命令,可以编写相应的订阅命令来方便用户使用。
命令编写者只需要在后台帮用户把要执行的任务翻译成 `commands/scheduler.py` 能够处理的形式,并直接调用其中的函数即可,该文件中的命令一般接受一个 `internal` 参数来表示是否是命令间的内部调用,在调用时,指定该参数为 True 将不会对用户发送消息,并且在执行结束后会返回相应的返回值以便调用者知道命令执行是否成功等,具体可参见 `commands/scheduler.py` 的代码。
## 命令参数
命令的函数的第一个参数为命令参数,默认情况下,是一个字符串,即用户发送的消息中命令后面的内容,可以自行切割、分析。如果需要使用默认的命令参数分隔符,可以使用 `command.py` 中的 `split_arguments` 装饰器,使用之后,命令的函数将接受到一个名为 `argv` 的参数,为分割后的参数列表,而原先第一个参数还保留为原字符串。例如:
```python
@__registry__.register('test')
@__registry__.restrict(group_admin_only=True)
@split_arguments()
def test(args_text, ctx_msg, argv=None):
if argv:
print(args[0])
```

View File

@ -1,46 +0,0 @@
# 编写过滤器
`filters` 目录中所有不以 `_` 开头的 `.py` 文件会被加载进程序,一般把过滤器放在这个目录里。对于临时不需要的过滤器,可以通过在文件名前加 `_` 来屏蔽掉。
## 写法
编写过滤器比较简单,只需要调用 `filter.py` 中的 `add_filter` 函数或 `as_filter` 装饰器,传入过滤器函数和优先级,即可。
比如我们需要做一个消息拦截器,当匹配到消息中有不文明词汇,就发送一条警告,并拦截消息不让后续过滤器和命令处理,代码可能如下:
```python
from filter import add_filter, as_filter
from commands import core
def _interceptor(ctx_msg):
if 'xxx' in ctx_msg.get('content', ''):
core.echo('请不要说脏话', ctx_msg)
return False
return True
add_filter(_interceptor, priority=100)
# 或下面这样
@as_filter(priority=100)
def _interceptor(ctx_msg):
if 'xxx' in ctx_msg.get('content', ''):
core.echo('请不要说脏话', ctx_msg)
return False
return True
```
一般建议优先级设置为 0100 之间。
过滤器函数返回 True 表示让消息继续传递,返回 False 表示拦截消息。由于很多情况下可能不需要拦截,因此为了方便起见,将不返回值的情况(返回 None作为不拦截处理因此只要返回结果 is not False 就表示不拦截。
## 现有的几个重要过滤器
| 文件 | 优先级 | 作用 | 备注 |
| ------------------------------------- | ---- | ---------------------------------------- | -------------------------------------- |
| message_logger_1000.py | 1000 | 把收到的消息打印在标准输出 | 不建议添加比它优先级更高的过滤器 |
| intercept_some_message_formats_100.py | 100 | 拦截某些不支持的消息类型,对于文本消息,会把 `content` 字段复制到 `text` 字段 | 如果要自己编写插件,这里可以按需修改 |
| speech_recognition_90.py | 90 | 对语音消息进行语音识别(仅私聊消息),并把识别出的文字放到 `text` 字段,并标记 `from_voice` 字段为 True | 此过滤器只对 Mojo-Weixin 消息源生效,如果不需要可以删掉 |
| split_at_xiaokai_50.py | 50 | 分离群组和讨论组中消息开头的 `@CCZU 小开`,并更新 `text` 字段为剩余部分 | 也就是说通过此过滤器的消息,就是确定用户的意图就是和这个 bot 说话的消息 |
| command_dispatcher_0.py | 0 | 识别消息中的命令,并进行相应的调用 | |

View File

@ -1,24 +0,0 @@
# 编写自然语言处理器
`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-汉语词性标注集))和词语的字符串。

View File

@ -1,36 +0,0 @@
docute.init({
title: 'XiaoKai Bot 文档',
home: 'https://raw.githubusercontent.com/CCZU-DEV/xiaokai-bot/master/README.md',
repo: 'CCZU-DEV/xiaokai-bot',
nav: {
default: [
{
title: '首页', path: '/'
},
{
title: '消息源列表', path: '/Message_Sources'
},
{
title: '开发', type: 'dropdown',
items: [
{
title: '统一消息上下文', path: '/Context'
},
{
title: '编写消息源适配器', path: '/Write_Adapter'
},
{
title: '编写过滤器', path: '/Write_Filter'
},
{
title: '编写命令', path: '/Write_Command'
},
{
title: '编写自然语言处理器', path: '/Write_NLProcessor'
}
]
}
]
},
plugins: []
});

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="cleartype" content="on">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<title>XiaoKai Bot 文档</title>
<link rel="stylesheet" href="https://unpkg.com/docute@latest/dist/docute.css">
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/docute@latest/dist/docute.js"></script>
<script src="./config.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-python.min.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-bash.min.js"></script>
</body>
</html>