Update docs

This commit is contained in:
Richard Chien 2019-01-26 22:21:51 +08:00
parent 00ff96aed0
commit e22e4a019f
53 changed files with 387 additions and 199 deletions

View File

@ -18,7 +18,7 @@ NoneBot 在其底层与酷 Q 交互的部分使用 [python-aiocqhttp](https://gi
得益于 Python 的 [asyncio](https://docs.python.org/3/library/asyncio.html) 机制NoneBot 处理消息的吞吐量有了很大的保障,再配合 CoolQ HTTP API 插件可选的 WebSocket 通信方式也是最建议的通信方式NoneBot 的性能可以达到 HTTP 通信方式的两倍以上,相较于传统同步 I/O 的 HTTP 通信,更是有质的飞跃。
需要注意的是NoneBot 仅支持 Python 3.6.1+ 及 CoolQ HTTP API 插件 v4.2+。
需要注意的是NoneBot 仅支持 Python 3.6.1+ 及 CoolQ HTTP API 插件 v4.7+。
## 示意图

View File

@ -59,14 +59,16 @@ module.exports = {
collapsable: false,
children: [
'',
'session',
'command-session',
'command-argument',
'command-group',
'message',
'permission',
'decorator',
'database',
'scheduler',
'logging',
'configuration',
'argparse',
'larger-application',
'deployment',
]

View File

@ -10,5 +10,5 @@ features:
details: 精心设计的消息处理流程及强大的 API 使得你可以很方便地将最简单的原型变为具有大量实用功能的完整聊天机器人,并持续保证扩展性。
- title: 高性能
details: 基于时下流行的 asyncio 模块,利用 WebSocket 进行通信,以获得极高的性能;同时,支持使用多个机器人账号来负载均衡用户消息,减少业务宕机的可能。
footer: MIT Licensed | Copyright © 2018 Richard Chien
footer: MIT Licensed | Copyright © 2019 Richard Chien
---

View File

@ -1,4 +1,8 @@
# 类 Shell 的参数解析
# 命令参数
## `session.get()` 和参数解析器
## 类 Shell 参数解析
`nonebot.argparse` 模块主要继承自 Python 内置的同名模块(`argparse`),用于解析命令的参数。在需要编写类 shell 语法的命令的时候,使用此模块可以大大提高开发效率。
@ -97,7 +101,7 @@ COMMAND
""".strip()
```
上面的例子出自 [cczu-osa/amadeus](https://github.com/cczu-osa/amadeus) 项目的计划任务插件,这里我们只关注前 15 行。
上面的例子出自 [cczu-osa/aki](https://github.com/cczu-osa/aki) 项目的计划任务插件,这里我们只关注前 15 行。
`on_command``shell_like=True` 参数告诉 NoneBot 这个命令需要使用类 shell 语法NoneBot 会自动添加命令参数解析器来使用 Python 内置的 `shlex` 包分割参数。分割后的参数被放在 `session.args['argv']`,可通过 `session.argv` 属性来快速获得。

View File

@ -0,0 +1 @@
# 命令组

View File

@ -0,0 +1,9 @@
# 命令会话
## 生命周期
## 状态数据
## 暂停、终止
## 切换上下文

View File

@ -0,0 +1 @@
# 数据库

View File

@ -1,5 +1,7 @@
# 部署
## 基本部署
NoneBot 所基于的 python-aiocqhttp 库使用的 web 框架是 Quart因此 NoneBot 的部署方法和 Quart 一致([Deploying Quart](https://pgjones.gitlab.io/quart/deployment.html))。
Quart 官方建议使用 Hypercorn 来部署,这需要一个 ASGI app 对象,在 NoneBot 中,可使用 `nonebot.get_bot().asgi` 获得 ASGI app 对象。
@ -29,3 +31,5 @@ hypercorn run:app
```
另外NoneBot 配置文件的 `DEBUG` 项默认为 `True`,在生产环境部署时请注意修改为 `False` 以提高性能。
## 使用 Docker Compose 与 酷Q 同时部署

View File

@ -1 +1,7 @@
# 大型应用的最佳实践
## 使用独立 Logger
## 项目结构
## 根据运行环境加载不同的配置

View File

@ -1 +1,5 @@
# 消息处理
## CQ 码和消息段
## Expression

View File

@ -1 +0,0 @@
# 会话

View File

@ -4,6 +4,10 @@ sidebar: auto
# 更新日志
## v1.2.1
- 修复 `nonebot.helpers.context_id``group` 模式无法正确产生私聊用户 ID 的 bug
## v1.2.0
#### 新增

View File

@ -16,11 +16,11 @@ NoneBot 在其底层与 酷Q 交互的部分使用 [python-aiocqhttp](https://gi
得益于 Python 的 [asyncio](https://docs.python.org/3/library/asyncio.html) 机制NoneBot 处理消息的吞吐量有了很大的保障,再配合 CoolQ HTTP API 插件可选的 WebSocket 通信方式也是最建议的通信方式NoneBot 的性能可以达到 HTTP 通信方式的两倍以上,相较于传统同步 I/O 的 HTTP 通信,更是有质的飞跃。
需要注意的是NoneBot 仅支持 Python 3.6.1+ 及 CoolQ HTTP API 插件 v4.2+。
需要注意的是NoneBot 仅支持 Python 3.6.1+ 及 CoolQ HTTP API 插件 v4.7+。
## 它如何工作?
NoneBot 的运行离不开 酷Q 和 CoolQ HTTP API 插件。酷Q 扮演着「无头 QQ 客户端」的角色,它进行实际的消息、通知、请求的接收和发送,当 酷Q 收到消息时,它将这个消息包装为一个事件(通知和请求同理),并通过它自己的插件机制将事件传送给 CoolQ HTTP API 插件,后者再根据其配置中的 `post_url``ws_reverse_event_url` 等项来将事件发送至 NoneBot。
NoneBot 的运行离不开 酷Q 和 CoolQ HTTP API 插件。酷Q 扮演着「无头 QQ 客户端」的角色,它进行实际的消息、通知、请求的接收和发送,当 酷Q 收到消息时,它将这个消息包装为一个事件(通知和请求同理),并通过它自己的插件机制将事件传送给 CoolQ HTTP API 插件,后者再根据其配置中的 `post_url``ws_reverse_url` 等项来将事件发送至 NoneBot。
在 NoneBot 收到事件前,它底层的 aiocqhttp 实际已经先看到了事件aiocqhttp 根据事件的类型信息,通知到 NoneBot 的相应函数。特别地,对于消息类型的事件,还将消息内容转换成了 `aiocqhttp.message.Message` 类型,以便处理。

View File

@ -25,7 +25,7 @@ awesome-bot
上一章中我们知道 NoneBot 内置了 `echo``say` 命令,我们已经测试了 `echo` 命令,并且正确地收到了机器人的回复,现在来尝试向它发送一个 `say` 命令:
```
/say [CQ:music,type=163,id=478490650]
/say [CQ:music,type=qq,id=209249583]
```
可以预料,命令不会起任何效果,因为我们提到过,`say` 命令只有超级用户可以调用,而现在我们还没有将自己的 QQ 号配置为超级用户。
@ -38,7 +38,7 @@ from nonebot.default_config import *
SUPERUSERS = {12345678}
```
这里的第 1 行是从 NoneBot 的默认配置中导入所有项,通常这是必须的,除非你知道自己在做什么,否则始终应该在配置文件的开头写上这一行。
**这里的第 1 行是从 NoneBot 的默认配置中导入所有项,通常这是必须的,除非你知道自己在做什么,否则始终应该在配置文件的开头写上这一行。**
之后就是配置 `SUPERUSERS` 了,这个配置项的要求是值为 `int` 类型的**容器**,也就是说,可以是 `set`、`list`、`tuple` 等类型,元素类型为 `int``12345678` 是你想设置为超级用户的 QQ。
@ -60,7 +60,7 @@ if __name__ == '__main__':
重启 NoneBot 后再次尝试发送:
```
/say [CQ:music,type=163,id=478490650]
/say [CQ:music,type=qq,id=209249583]
```
可以看到这次机器人成功地给你回复了一个音乐分享消息。
@ -73,7 +73,7 @@ if __name__ == '__main__':
from nonebot.default_config import *
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}
```
首先需要知道NoneBot 默认的 `COMMAND_START` 是一个 `set` 对象,如下:
@ -82,9 +82,9 @@ COMMAND_START.add('')
COMMAND_START = {'/', '!', '', ''}
```
因此调用 `COMMAND_START.add('')` 将会向 `set` 中添加一个空字符串,也就告诉了 NoneBot我们希望不需要任何起始字符也能调用命令。
这表示会尝试把 `/`、`!`、``、`` 开头的消息理解成命令。而我们上面修改了的 `COMMAND_START` 加入了空字符串 `''`,也就告诉了 NoneBot我们希望不需要任何起始字符也能调用命令。
当然,你可以直接覆盖整个 `COMMAND_START`,其值和 `SUPERUSERS` 一样,可以是 `list`、`tuple`、`set` 等任意容器类型,元素类型可以是 `str` 或正则表达式,例如:
`COMMAND_START`值和 `SUPERUSERS` 一样,可以是 `list`、`tuple`、`set` 等任意容器类型,元素类型可以是 `str` 或正则表达式,例如:
```python
import re

View File

@ -2,10 +2,19 @@
到目前为止,我们都在调用 `CommandSession` 类的 `send()` 方法,而这个方法只能回复给消息的发送方,不能手动指定发送者,因此当我们需要实现将收到的消息经过处理后转发给另一个接收方这样的功能时,这个方法就用不了了。
幸运的是,`NoneBot` 类是继承自 [python-aiocqhttp] 的 `CQHttp` 类的,而这个类实现了一个 `__getattr__()` 方法,由此提供了直接通过 bot 对象调用 CQHTTP 的 API 的能力,如下:
幸运的是,`NoneBot` 类是继承自 [python-aiocqhttp] 的 `CQHttp` 类的,而这个类实现了一个 `__getattr__()` 方法,由此提供了直接通过 bot 对象调用 CQHTTP 的 API 的能力
[python-aiocqhttp]: https://github.com/richardchien/python-aiocqhttp
要获取 bot 对象,可以通过如下两种方式:
```python
bot = session.bot
bot = nonebot.get_bot()
```
Bot 对象的使用方式如下:
```python
await bot.send_private_msg(user_id=12345678, message='你好~')
```
@ -14,10 +23,19 @@ await bot.send_private_msg(user_id=12345678, message='你好~')
通过这种方式调用 API 时,需要注意下面几点:
1. **所有参数必须为命名参数keyword argument**,否则无法正确调用
2. 这种调用**全都是异步调用**,因此需要适当 `await`
3. **调用失败时(没有权限、对方不是好友、无 API 连接等)可能抛出 `nonebot.CQHttpError` 异常**,注意捕获
4. **当多个机器人使用同一个 NoneBot 后端时**,可能需要加上参数 `self_id=<机器人QQ号>`,例如 `await bot.get_group_list(self_id=session.ctx['self_id'])`
- **所有参数必须为命名参数keyword argument**,否则无法正确调用
- 这种调用**全都是异步调用**,因此需要适当 `await`
- **调用失败时(没有权限、对方不是好友、无 API 连接等)可能抛出 `nonebot.CQHttpError` 异常**,注意捕获,例如:
```python
try:
info = await bot.get_group_list()
except CQHttpError:
pass
```
- **当多个机器人使用同一个 NoneBot 后端时**,可能需要加上参数 `self_id=<机器人QQ号>`,例如:
```python
info = await bot.get_group_list(self_id=ctx['self_id'])
```
另外,在需要动态性的场合,除了使用 `getattr()` 方法外,还可以直接调用 `bot.call_action()` 方法,传入 `action``params` 即可,例如上例中,`action` 为 `'send_private_msg'``params` 为 `{'user_id': 12345678, 'message': '你好~'}`

View File

@ -4,4 +4,4 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}

View File

@ -1 +1 @@
nonebot>=1.0.0
nonebot>=1.1.0

View File

@ -5,7 +5,7 @@ from nonebot import on_command, CommandSession
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
# 从 Session 对象中获取城市名称city如果当前不存在则询问用户
# 从会话状态session.state中获取城市名称city如果当前不存在则询问用户
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
# 获取城市的天气预报
weather_report = await get_weather_of_city(city)
@ -19,13 +19,22 @@ async def weather(session: CommandSession):
async def _(session: CommandSession):
# 去掉消息首尾的空白符
stripped_arg = session.current_arg_text.strip()
if session.current_key:
# 如果当前正在向用户询问更多信息(本例中只有可能是要查询的城市),则直接赋值
session.args[session.current_key] = stripped_arg
elif stripped_arg:
# 如果当前没有在询问,但用户已经发送了内容,则理解为要查询的城市
# 这种情况通常是用户直接将城市名跟在命令名后面,作为参数传入
session.args['city'] = stripped_arg
if session.is_first_run:
# 该命令第一次运行(第一次进入命令会话)
if stripped_arg:
# 第一次运行参数不为空,意味着用户直接将城市名跟在命令名后面,作为参数传入
# 例如用户可能发送了:天气 南京
session.state['city'] = stripped_arg
return
if not stripped_arg:
# 用户没有发送有效的城市名称(而是发送了空白字符),则提示重新输入
# 这里 session.pause() 将会发送消息并暂停当前会话(该行后面的代码不会被运行)
session.pause('要查询的城市名称不能为空呢,请重新输入')
# 如果当前正在向用户询问更多信息(例如本例中的要查询的城市),且用户输入有效,则放入会话状态
session.state[session.current_key] = stripped_arg
async def get_weather_of_city(city: str) -> str:

View File

@ -6,6 +6,8 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -4,4 +4,4 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}

View File

@ -1 +1 @@
nonebot>=1.0.0
nonebot>=1.1.0

View File

@ -1,5 +1,5 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@ -15,21 +15,27 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg_text = session.msg_text.strip()
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg_text)
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
@ -39,5 +45,5 @@ async def _(session: NLPSession):
# ns 词性表示地名
city = word.word
# 返回处理结果,三个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {'city': city})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -6,6 +6,8 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -4,5 +4,5 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}

View File

@ -1,2 +1,2 @@
nonebot>=1.0.0
nonebot>=1.1.0
jieba

View File

@ -4,7 +4,7 @@ from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
@ -20,7 +20,7 @@ EXPR_DONT_UNDERSTAND = (
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.get_optional('message')
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
@ -38,7 +38,7 @@ async def tuling(session: CommandSession):
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:

View File

@ -1,5 +1,5 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@ -15,21 +15,27 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg_text = session.msg_text.strip()
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg_text)
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
@ -39,5 +45,5 @@ async def _(session: NLPSession):
# ns 词性表示地名
city = word.word
# 返回处理结果,三个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {'city': city})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -6,6 +6,8 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -4,7 +4,7 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

@ -1,3 +1,3 @@
nonebot>=1.0.0
nonebot>=1.1.0
jieba
aiohttp

View File

@ -4,7 +4,7 @@ from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
@ -20,7 +20,7 @@ EXPR_DONT_UNDERSTAND = (
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.get_optional('message')
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
@ -38,7 +38,7 @@ async def tuling(session: CommandSession):
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:

View File

@ -1,5 +1,5 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@ -15,21 +15,27 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg_text = session.msg_text.strip()
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg_text)
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
@ -39,5 +45,5 @@ async def _(session: NLPSession):
# ns 词性表示地名
city = word.word
# 返回处理结果,三个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {'city': city})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -6,6 +6,8 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -4,7 +4,7 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

@ -1,3 +1,3 @@
nonebot>=1.0.0
nonebot>=1.1.0
jieba
aiohttp

View File

@ -4,7 +4,7 @@ from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
@ -20,7 +20,7 @@ EXPR_DONT_UNDERSTAND = (
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.get_optional('message')
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
@ -38,7 +38,7 @@ async def tuling(session: CommandSession):
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:

View File

@ -1,5 +1,5 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@ -15,21 +15,27 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg_text = session.msg_text.strip()
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg_text)
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
@ -39,5 +45,5 @@ async def _(session: NLPSession):
# ns 词性表示地名
city = word.word
# 返回处理结果,三个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {'city': city})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -6,6 +6,8 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -4,7 +4,7 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

@ -1,4 +1,4 @@
nonebot>=1.0.0
nonebot>=1.1.0
jieba
aiohttp
pytz

View File

@ -4,7 +4,7 @@ from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
__plugin_name__ = '智能聊天'
@ -27,7 +27,7 @@ EXPR_DONT_UNDERSTAND = (
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.get_optional('message')
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
@ -45,7 +45,7 @@ async def tuling(session: CommandSession):
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:

View File

@ -1,5 +1,5 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@ -22,21 +22,27 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg_text = session.msg_text.strip()
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg_text)
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
@ -46,5 +52,5 @@ async def _(session: NLPSession):
# ns 词性表示地名
city = word.word
# 返回处理结果,三个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {'city': city})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -6,6 +6,8 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -4,7 +4,7 @@ HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START.add('')
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

@ -1,4 +1,4 @@
nonebot>=1.0.0
nonebot>=1.1.0
jieba
aiohttp
pytz

View File

@ -33,7 +33,7 @@ awesome-bot
现在我们的插件目录已经有了一个空的 `weather.py`,实际上它已经可以被称为一个插件了,尽管它还什么都没做。下面我们来让 NoneBot 加载这个插件,修改 `bot.py` 如下:
```python {1,9-10}
```python {1,9-12}
from os import path
import nonebot
@ -42,8 +42,10 @@ import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins')
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()
```
@ -80,7 +82,7 @@ from nonebot import on_command, CommandSession
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
# 从 Session 对象中获取城市名称city如果当前不存在则询问用户
# 从会话状态session.state中获取城市名称city如果当前不存在则询问用户
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
# 获取城市的天气预报
weather_report = await get_weather_of_city(city)
@ -94,13 +96,22 @@ async def weather(session: CommandSession):
async def _(session: CommandSession):
# 去掉消息首尾的空白符
stripped_arg = session.current_arg_text.strip()
if session.current_key:
# 如果当前正在向用户询问更多信息(本例中只有可能是要查询的城市),则直接赋值
session.args[session.current_key] = stripped_arg
elif stripped_arg:
# 如果当前没有在询问,但用户已经发送了内容,则理解为要查询的城市
# 这种情况通常是用户直接将城市名跟在命令名后面,作为参数传入
session.args['city'] = stripped_arg
if session.is_first_run:
# 该命令第一次运行(第一次进入命令会话)
if stripped_arg:
# 第一次运行参数不为空,意味着用户直接将城市名跟在命令名后面,作为参数传入
# 例如用户可能发送了:天气 南京
session.state['city'] = stripped_arg
return
if not stripped_arg:
# 用户没有发送有效的城市名称(而是发送了空白字符),则提示重新输入
# 这里 session.pause() 将会发送消息并暂停当前会话(该行后面的代码不会被运行)
session.pause('要查询的城市名称不能为空呢,请重新输入')
# 如果当前正在向用户询问更多信息(例如本例中的要查询的城市),且用户输入有效,则放入会话状态
session.state[session.current_key] = stripped_arg
async def get_weather_of_city(city: str) -> str:
@ -124,7 +135,7 @@ async def get_weather_of_city(city: str) -> str:
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
# 从 Session 对象中获取城市名称city如果当前不存在则询问用户
# 从会话状态session.state中获取城市名称city如果当前不存在则询问用户
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
# 获取城市的天气预报
weather_report = await get_weather_of_city(city)
@ -132,11 +143,17 @@ async def weather(session: CommandSession):
await session.send(weather_report)
```
首先,`session.get()` 函数调用尝试从当前 Session 对象中获取 `city` 这个参数,所有的参数都被存储在 `session.args` 变量(一个 `dict`)中,如果发现存在,则直接返回,并赋值给 `city` 变量,而如果 `city` 参数不存在,`session.get()` 会**中断**这次命令处理的流程,并保存当前会话Session,然后向用户发送 `prompt` 参数的内容。这里的「中断」,意味着如果当前不存在 `city` 参数,`session.get()` 之后的代码将不会被执行,这是通过抛出异常做到的。
首先,`session.get()` 函数调用尝试从当前会话Session的状态中获取 `city` 这个参数,**所有的参数和会话中需要暂存的临时数据都被存储在 `session.state` 变量(一个 `dict`)中**,如果发现存在,则直接返回,并赋值给 `city` 变量,而如果 `city` 参数不存在,`session.get()` 会**中断**这次命令处理的流程,并保存当前会话,然后向用户发送 `prompt` 参数的内容。**这里的「中断」,意味着如果当前不存在 `city` 参数,`session.get()` 之后的代码将不会被执行,这是通过抛出异常做到的。**
向用户发送 `prompt` 中的提示之后,会话会进入等待状态,此时我们称之为「当前用户正在 weather 命令的会话中」当用户再次发送消息时NoneBot 会唤起这个等待中的会话,并重新执行命令,也就是**从头开始**重新执行上面的这个函数,如果用户在一定时间内(默认 5 分钟,可通过 `SESSION_EXPIRE_TIMEOUT` 配置项来更改)都没有再次跟机器人发消息,则会话因超时被关闭。
你可能想问了,既然是重新执行,那执行到 `session.get()` 的时候不还是会中断吗?这时候就需要参数解析器出场了,也就是下面这个函数:
你可能想问了,既然是重新执行,那执行到 `session.get()` 的时候不还是会中断吗实际上NoneBot 在 1.0.0 及更早版本中确实是这样的,必须手动编写下面要说的参数解析器,才能够让 `session.get()` 正确返回;而从 1.1.0 版本开始NoneBot 会默认地把用户的完整输入作为当前询问内容的回答放进会话状态。
::: tip 提示
删掉下面这段参数解析器,天气命令也可以正常使用,可以尝试不同的输入,看看行为上有什么不同。
:::
但这里我们还是手动编写参数解析器,以应对更复杂的情况,也就是下面这个函数:
```python
# weather.args_parser 装饰器将函数声明为 weather 命令的参数解析器
@ -145,27 +162,42 @@ async def weather(session: CommandSession):
async def _(session: CommandSession):
# 去掉消息首尾的空白符
stripped_arg = session.current_arg_text.strip()
if session.current_key:
# 如果当前正在向用户询问更多信息(本例中只有可能是要查询的城市),则直接赋值
session.args[session.current_key] = stripped_arg
elif stripped_arg:
# 如果当前没有在询问,但用户已经发送了内容,则理解为要查询的城市
# 这种情况通常是用户直接将城市名跟在命令名后面,作为参数传入
session.args['city'] = stripped_arg
if session.is_first_run:
# 该命令第一次运行(第一次进入命令会话)
if stripped_arg:
# 第一次运行参数不为空,意味着用户直接将城市名跟在命令名后面,作为参数传入
# 例如用户可能发送了:天气 南京
session.state['city'] = stripped_arg
return
if not stripped_arg:
# 用户没有发送有效的城市名称(而是发送了空白字符),则提示重新输入
# 这里 session.pause() 将会发送消息并暂停当前会话(该行后面的代码不会被运行)
session.pause('要查询的城市名称不能为空呢,请重新输入')
# 如果当前正在向用户询问更多信息(例如本例中的要查询的城市),且用户输入有效,则放入会话状态
session.state[session.current_key] = stripped_arg
```
参数解析器的参数和命令处理函数一样,都是当前命令的 Session 对象,并且,它们会在命令处理函数之前执行,以确保正确解析参数以供后者使用。
参数解析器的 `session` 参数和命令处理函数一样,都是当前命令的会话对象。并且,参数解析器会在命令处理函数之前执行,以确保正确解析参数以供后者使用。
上面的例子中,参数解析器会判断 `session.current_key` 是否为空,如果不为空,说明此前已经经历过「`session.get()` 发现所需参数不存在,然后提示用户输入」这个过程了,因为在这个过程中,`session.current_key` 会被赋值为所需的参数名字,在本例中是 `city`,于是,参数解析器将此刻的用户输入(`session.current_arg_text`Session 的这个属性保存用户发送的消息中纯文本的部分)当做当前所需参数的值,赋值给 `session.args[session.current_key]`之前提到Session 中,参数保存在 `args` 属性);相反,如果 `session.current_key` 为空,则表示目前还没有调用过 `session.get()`,而这个时候如果用户发送的消息中,存在命令参数,解析器会把它理解为要查询的城市名,赋值给 `session.args['city']`,此时用户发送的完整消息可能是这样的:
上面的例子中,参数解析器会判断当前是否是该会话第一次运行(用户刚发送 `/天气`,触发了天气命令)。如果是,则检查用户触发天气命令时有没有附带参数(即 `stripped_arg` 是否有内容),如果带了参数(例如用户发送了 `/天气 南京`),则把附带的参数当做要查询的城市放进会话状态 `session.state`,以 `city` 作为状态的 key——也就是说如果用户触发命令时就给出了城市则命令处理函数中的 `session.get('city')` 第一次执行时就能返回结果。
如果不是第一次运行,那就说明命令处理函数中向用户询问了更多信息,导致会话被中断,并等待用户回复(也就是 `session.get()` 的效果)。这时候需要判断用户输入是不是有效,因为我们已经明确地询问了,如果用户此时发送了空白字符,显然这是没有意义的内容,需要提示用户重新发送。相反,如果有效的话,则直接以 `session.current_key` 作为 key也就是 `session.get()` 的第一个参数,上例中只有可能是 `city`),将输入内容存入会话状态。
::: tip 提示
上面用了 `session.current_arg_text` 来获取用户当前输入的参数,这表示从用户输入中提取纯文本部分,也就是说不包含图片、表情、语音、卡片分享等。
如果需要用户输入的原始内容,请使用 `session.current_arg`,里面可能包含 CQ 码。除此之外,还可以通过 `session.current_arg_images` 获取消息中的图片 URL 列表。
:::
现在我们已经理解完了天气命令的代码,是时候运行一下看看实际效果了,启动 NoneBot 后尝试向它分别发送下面的两个带参数和不带参数的消息:
```
/查天气 南京
```
现在我们已经理解完了天气命令的代码,是时候运行一下看看实际效果了,启动 NoneBot 后尝试向它分别发送上面这个带参数的消息和下面这个不带参数的消息:
```
/查天气
/天气 南京
/天气
```
观察看看有什么不同,以及它的回复是否符合我们对代码的理解。如果成功的话,此时你已经完成了一个**可交互的**天气查询命令的雏形,只需要再接入天气 API 就可以真正投入使用了!

View File

@ -31,17 +31,25 @@ if __name__ == '__main__':
python bot.py
```
运行后会产生如下日志:
```
[2019-01-26 14:24:15,984 nonebot] INFO: Succeeded to import "nonebot.plugins.base"
[2019-01-26 14:24:15,987 nonebot] INFO: Running on 127.0.0.1:8080
Running on https://127.0.0.1:8080 (CTRL + C to quit)
```
除此之外可能有一些红色的警告信息和 `ASGI Framework Lifespan error` 等,可以忽略。
## 配置 CoolQ HTTP API 插件
单纯运行 NoneBot 实例并不会产生任何效果,因为此刻 酷Q 这边还不知道 NoneBot 的存在,也就无法把消息发送给它,因此现在需要对 CoolQ HTTP API 插件做一个简单的配置来让它把消息等事件上报给 NoneBot。
如果你在之前已经按照 [安装](/guide/installation.md) 的建议使用默认配置运行了一次 CoolQ HTTP API 插件,此时 酷Q 的 `data\app\io.github.richardchien.coolqhttpapi\config\` 目录中应该已经有了一个名为 `<user-id>.json` 的文件(`<user-id>` 为你登录的 QQ 账号)。修改这个文件,**添加**如下配置项:
如果你在之前已经按照 [安装](/guide/installation.md) 的建议使用默认配置运行了一次 CoolQ HTTP API 插件,此时 酷Q 的 `data/app/io.github.richardchien.coolqhttpapi/config/` 目录中应该已经有了一个名为 `<user-id>.json` 的文件(`<user-id>` 为你登录的 QQ 账号)。修改这个文件,**修改如下配置项(如果不存在相应字段则添加)**
```json
{
"ws_reverse_api_url": "ws://127.0.0.1:8080/ws/api/",
"ws_reverse_event_url": "ws://127.0.0.1:8080/ws/event/",
"ws_reverse_reconnect_on_code_1000": true,
"ws_reverse_url": "ws://127.0.0.1:8080/ws/",
"use_ws_reverse": true
}
```
@ -50,21 +58,25 @@ python bot.py
这里的 `127.0.0.1:8080` 对应 `nonebot.run()` 中传入的 `host``port`,如果在 `nonebot.run()` 中传入的 `host``0.0.0.0`,则插件的配置中需使用任意一个能够访问到 NoneBot 所在环境的 IP。特别地如果你的 酷Q 运行在 Docker 容器中NoneBot 运行在宿主机中,则默认情况下这里需使用 `172.17.0.1`(不同机器有可能不同,需使用 `docker inspect bridge` 查看,具体见 Docker 文档的 [Configure networking](https://docs.docker.com/network/))。
:::
::: warning 注意
如果使用 CoolQ HTTP API 插件官方 Docker 镜像运行 酷Q则配置文件所在目录可能是 `app/io.github.richardchien.coolqhttpapi/config/`
:::
修改之后,在 酷Q 的应用菜单中重启 CoolQ HTTP API 插件,或直接重启 酷Q以使新的配置文件生效。
## 历史性的第一次对话
一旦新的配置文件正确生效之后NoneBot 所在的控制台(如果正在运行的话)应该会输出类似下面的内容:
一旦新的配置文件正确生效之后NoneBot 所在的控制台(如果正在运行的话)应该会输出类似下面的内容(两条路径为 `/ws/` 的访问日志)
```
[2018-08-14 23:35:35,532] 127.0.0.1:8080 GET /ws/api/ ws 101 - 2736
[2018-08-14 23:35:35,534] 127.0.0.1:8080 GET /ws/event/ ws 101 - 4682
[2019-01-26 16:23:17,159] 172.29.84.18:50639 GET /ws/ 1.1 101 - 986
[2019-01-26 16:23:17,201] 172.29.84.18:53839 GET /ws/ 1.1 101 - 551
```
这表示 CoolQ HTTP API 插件已经成功地连接上了 NoneBot与此同时插件的日志文件中也会输出反向 WebSocket 连接成功的日志。
::: warning 注意
如果到这一步你没有看到上面这样的日志,而是出现了 `ASGI Framework Lifespan error, continuing without Lifespan support`请查看插件的日志文件中是否在不断尝试重连(可通过将插件的 `show_log_console` 配置项设置为 `true` 来显示日志控制台,方便调试),如果没有在不断重连,也说明连接成功.
如果到这一步你没有看到上面这样的日志,请查看插件的日志文件中是否在不断尝试重连(可通过将插件的 `show_log_console` 配置项设置为 `true` 来显示日志控制台,方便调试),如果没有在不断重连,也说明连接成功.
除此之外,也可以直接向机器人随便发送一些消息,观察 NoneBot 运行日志中是否有输出,如果有,说明连接成功。

View File

@ -26,7 +26,7 @@ python setup.py install
前往 酷Q 官方论坛的 [版本发布](https://cqp.cc/b/news) 页面根据需要下载最新版本的 酷Q Air 或 Pro解压后启动 `CQA.exe``CQP.exe` 并登录 QQ 机器人账号。
如果你的操作系统是 Linux 或 macOS可以使用版本发布页中 酷Q 官方提供的 Docker 镜像,也可以直接跳至下一,使用 CoolQ HTTP API 插件官方提供的 Docker 镜像。
如果你的操作系统是 Linux 或 macOS可以使用版本发布页中 酷Q 官方提供的 Docker 镜像,也可以直接跳至下一个标题,使用 CoolQ HTTP API 插件官方提供的 Docker 镜像。
::: tip 提示
如果这是你第一次使用 酷Q建议完成它自带的新手教程从而对 酷Q 的运行机制有所了解。
@ -34,8 +34,8 @@ python setup.py install
## CoolQ HTTP API 插件
前往 [CoolQ HTTP API 插件官方文档](https://cqhttp.cc/docs/),按照其教程安装插件。安装后,请先使用默认配置运行,并查看 酷Q 日志窗口的输出,以确定插件的加载、配置的生成和读取、插件版本等符合预期。
前往 [CoolQ HTTP API 插件官方文档](https://cqhttp.cc/docs/),按照其教程的「使用方法」安装插件。安装后,请先使用默认配置运行,并查看 酷Q 日志窗口的输出,以确定插件的加载、配置的生成和读取、插件版本等符合预期。
::: warning 注意
请确保你安装的插件版本 >= 4.2,通常建议插件在大版本内尽量及时升级至最新版本。
请确保你安装的插件版本 >= 4.7,通常建议插件在大版本内尽量及时升级至最新版本。
:::

View File

@ -53,10 +53,16 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
```
`weather/data_source.py` 内容如下:
@ -70,9 +76,9 @@ async def get_weather_of_city(city: str) -> str:
`weather/__init__.py` 文件添加内容如下:
```python {2,23-29}
```python {2,29-35}
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from .data_source import get_weather_of_city
@ -87,22 +93,28 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 返回处理结果3 个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather')
```
代码中的注释已经进行了大部分解释,这里再详细介绍一下 `NLPResult` 这个类。
代码中的注释已经进行了大部分解释,这里再详细介绍一下 `IntentCommand` 这个类。
在 NoneBot 中,自然语言处理器的工作方式就是将用户的自然语言消息解析成一个命令和命令所需的参数,由于自然语言消息的模糊性,在解析时不可能完全确定用户的意图,因此还需要返回一个置信度作为这个命令的确定程度。
@ -110,7 +122,9 @@ async def _(session: NLPSession):
置信度的计算需要自然语言处理器的编写者进行恰当的设计,以确保各插件之间的功能不会互相冲突。
:::
在实际项目中,很多插件都会注册有自然语言处理器,其中每个都按照它的解析情况返回 `NLPResult` 对象NoneBot 会将所有自然语言处理器返回的 `NLPResult` 对象按置信度排序,取置信度最高且大于等于 60.0 的结果中的命令来执行,`NLPResult` 的第三个参数(上面代码中的 `{}`)会被赋值给命令的 `session.args`(还记得上一章中用到这个属性吗)。
在实际项目中,很多插件都会注册有自然语言处理器,其中每个都按照它的解析情况返回 `IntentCommand` 对象也可能不返回NoneBot 会将所有自然语言处理器返回的 `IntentCommand` 对象按置信度排序,**取置信度最高且大于等于 60.0 的意图命令来执行**。
<!-- 除了上面雏形中填的两个必要参数(置信度和命令名),`IntentCommand` 还接受 `args`(类型 `dict`)和 `current_arg`(类型 `str`参数也就是命令所需的参数。当一个意图命令被选中置信度最高NoneBot 会根据这个意图命令给出的命令名、命令参数来创建命令会话(`CommandSession`),其中 `args` 参数的内容会被全部放入 `CommandSession``state` 属性中,也就是前一章中用到的的 `session.state`,而 `current_arg` 将可以通过 `session.current_arg` 访问。后面的代码中将会用到 `current_arg` 参数。 -->
目前的代码中,直接根据关键词 `天气` 做出响应,无论消息其它部分是什么,只要包含关键词 `天气`,就会理解为 `weather` 命令。
@ -142,9 +156,9 @@ pip install jieba
有了结巴分词之后,扩充 `weather/__init__.py` 如下:
```python {3,29-43}
```python {3,35-49}
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@ -160,21 +174,27 @@ async def weather(session: CommandSession):
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
if session.current_key:
session.args[session.current_key] = stripped_arg
elif stripped_arg:
session.args['city'] = stripped_arg
if session.is_first_run:
if stripped_arg:
session.state['city'] = stripped_arg
return
if not stripped_arg:
session.pause('要查询的城市名称不能为空呢,请重新输入')
session.state[session.current_key] = stripped_arg
# on_natural_language 装饰器将函数声明为一个自然语言处理器
# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str
# 如果不传入 keywords则响应所有没有被当作命令处理的消息
@on_natural_language(keywords=('天气',))
@on_natural_language(keywords={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg_text = session.msg_text.strip()
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg_text)
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
@ -184,11 +204,17 @@ async def _(session: NLPSession):
# ns 词性表示地名
city = word.word
# 返回处理结果3 个参数分别为置信度、命令名、命令会话的参数
return NLPResult(90.0, 'weather', {'city': city})
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')
```
我们使用结巴分词的 posseg 模块进行词性标注,然后找出第一个标记为 `ns`(表示地名,其它词性见 [ICTCLAS 汉语词性标注集](https://gist.github.com/luw2007/6016931#ictclas-%E6%B1%89%E8%AF%AD%E8%AF%8D%E6%80%A7%E6%A0%87%E6%B3%A8%E9%9B%86))的词,赋值给 `city`,进而作为 `weather` 命令的参数传入 `NLPResult`
这里我们首先使用结巴分词的 posseg 模块进行词性标注,然后找出第一个标记为 `ns`(表示地名,其它词性见 [ICTCLAS 汉语词性标注集](https://gist.github.com/luw2007/6016931#ictclas-%E6%B1%89%E8%AF%AD%E8%AF%8D%E6%80%A7%E6%A0%87%E6%B3%A8%E9%9B%86))的词,赋值给 `city`,进而作为 `weather` 命令的参数传入 `IntentCommand`(如果 `city` 为空,则给 `current_arg` 传入空字符串)。
::: tip 提示
这里使用了 `current_arg`,因为之前编写的天气命令能够处理第一次运行时就附带了参数(城市名)的情况。
你也可以在你自己的功能中使用 `args` 传入更复杂的初始参数。
:::
现在运行 NoneBot尝试向机器人分别发送下面两句话
@ -199,9 +225,24 @@ async def _(session: NLPSession):
如果一切顺利,第一句它会问你要查询哪个城市,第二句会直接识别到城市。
## 理清自然语言处理器的逻辑
为了更好地理解自然语言处理器,这里再来尝试理清楚它的逻辑。
**自然语言处理器的核心功能,是从用户的任意消息中识别意图,并产生一个包含有初始参数的意图命令。**
比如上面例子中的自然语言处理器所做的事情,就是进行如下所示的意图识别:
```
今天天气怎么样? => /天气
今天南京天气怎么样? => /天气 南京
```
箭头左边是用户发送的**没有明确格式的任意消息**,右边是自然语言处理器从中识别出的**真正意图所对应的命令**。
## 优化群聊中的使用体验
到目前为止我们都只关注了私聊的情况,实际上我们的天气插件在群聊中也可以正常工作,但是有一个问题,我们必须@机器人,它才会回复。一种解决办法是,给 `on_natural_language` 装饰器添加参数 `only_to_me=False`,这样的话,机器人将会响应所有群聊中含有 `天气` 关键词的消息,这对于某些功能的插件来说可能比较适用。另一种办法是通过配置项 `NICKNAME` 设置机器人的昵称,例如:
到目前为止我们都只关注了私聊的情况,实际上我们的天气插件在群聊中也可以正常工作,但是有一个问题,我们必须 @ 机器人,它才会回复。一种解决办法是,给 `on_natural_language` 装饰器添加参数 `only_to_me=False`,这样的话,机器人将会响应所有群聊中含有 `天气` 关键词的消息,这对于某些功能的插件来说可能比较适用。另一种办法是通过配置项 `NICKNAME` 设置机器人的昵称,例如:
```python
NICKNAME = {'小明', '明明'}
@ -213,7 +254,7 @@ NICKNAME = {'小明', '明明'}
小明,今天天气怎么样?
```
此处 `小明` 和@的效果相同。
此处 `小明` @ 的效果相同。
## 更精确的自然语言理解

View File

@ -27,7 +27,7 @@ from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, NLPResult
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
@ -43,7 +43,7 @@ EXPR_DONT_UNDERSTAND = (
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.get_optional('message')
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
@ -61,7 +61,7 @@ async def tuling(session: CommandSession):
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
@ -126,7 +126,7 @@ TULING_API_KEY = ''
```python {3}
@on_natural_language
async def _(session: NLPSession):
return NLPResult(60.0, 'tuling', {'message': session.msg_text})
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
```
根据我们前面一章中已经知道的用法,这里就是直接返回置信度为 60.0 的 `tuling` 命令。之所以返回置信度 60.0,是因为自然语言处理器所返回的结果最终会按置信度排序,取置信度最高且大于等于 60.0 的结果来执行。把置信度设为 60.0 可以保证一条消息无法被其它自然语言处理器理解的时候 fallback 到 `tuling` 命令。
@ -217,7 +217,7 @@ EXPR_DONT_UNDERSTAND = (
@on_command('tuling')
async def tuling(session: CommandSession):
message = session.get_optional('message')
message = session.state.get('message')
reply = await call_tuling_api(session, message)
if reply:
await session.send(escape(reply))
@ -227,7 +227,7 @@ async def tuling(session: CommandSession):
### 可选参数
首先看第 13 行,`session.get_optional()` 可用于获取命令的可选参数,也就是说,从 `session.args` 中尝试获取一个参数,如果没有,返回 `None`,但并不会中断命令的执行,比较类似于 `dict.get()` 方法。
首先看第 13 行,`session.state.get()` 可用于获取命令的可选参数,也就是说,从 `session.state` 中尝试获取一个参数(还记得 `IntentCommand``args` 参数内容会全部进入 `CommandSession``state` 吗),如果没有,返回 `None`,但并不会中断命令的执行。其实这就是 `dict.get()` 方法。
### 消息转义

View File

@ -18,10 +18,10 @@
CoolQ HTTP API 插件收到消息后会将其包装为一个统一的事件格式并对消息内容进行一个初步的处理例如编码转换、数组化、CQ 码增强等,这里的细节目前为止不需要完全明白,在需要的时候,可以去参考 CoolQ HTTP API 插件的 [文档](https://cqhttp.cc/docs/)。
接着,插件把包装好的事件转换成 JSON 格式,并通过反向 WebSocket 客户端的 Event 客户端发送出去。这里的 Event 客户端,连接的就是我们在它的配置中指定的 `ws_reverse_event_url`,即 NoneBot 监听的 WebSocket 入口之一(另一个是 API
接着,插件把包装好的事件转换成 JSON 格式,并通过「反向 WebSocket」发送给 NoneBot。这里的「反向 WebSocket」连接的就是我们在 CoolQ HTTP API 插件的配置中指定的 `ws_reverse_url`,即 NoneBot 监听的 WebSocket 入口。
::: tip 提示
反向 WebSocket 的 Event 和 API 客户端都是在插件启动时建立连接的,在恰当配置了 `ws_reverse_reconnect_interval``ws_reverse_reconnect_on_code_1000` 之后,会在断线时自动尝试重连
「反向 WebSocket」是 CoolQ HTTP API 插件的一种通信方式,表示插件作为客户端,主动去连接配置文件中指定的 `ws_reverse_url`。除此之外还有 HTTP、正向WebSocket 等方式
:::
## NoneBot 出场
@ -42,7 +42,7 @@ CoolQ HTTP API 插件通过反向 WebSocket 将消息事件发送到 NoneBot 后
到这里,我们先暂停一下对消息事件的行踪的描述,回头来说一下最小实例的代码:
```python
```python {4-6}
import nonebot
if __name__ == '__main__':
@ -61,7 +61,7 @@ NoneBot 的内置插件只包含了两个命令,`echo` 和 `say`,两者的
<img alt="Echo and Say" src="./assets/echo_and_say.png" />
</p>
最后,`nonebot.run(host='127.0.0.1', port=8080)` 让 NoneBot 跑在了地址 `127.0.0.1:8080` 上,向 CoolQ HTTP API 插件提供 `/`、`/ws/event/`、`/ws/api/` 三个入口,在我们的反向 WebSocket 配置中,插件利用了后两个入口。
最后,`nonebot.run(host='127.0.0.1', port=8080)` 让 NoneBot 跑在了地址 `127.0.0.1:8080` 上,向 CoolQ HTTP API 插件提供 `/`、`/ws/`、`/ws/event/`、`/ws/api/` 四个入口,在我们的反向 WebSocket 配置中,插件利用了第二个入口。
### 命令处理器
@ -72,7 +72,7 @@ NoneBot 的内置插件只包含了两个命令,`echo` 和 `say`,两者的
```python
@on_command('echo')
async def echo(session: CommandSession):
await session.send(session.get_optional('message') or session.current_arg)
await session.send(session.state.get('message') or session.current_arg)
```
你现在不用关心它是如何从 Session 中拿到参数的,只需看到,命令处理器中实际内容只有一行 `session.send()` 函数调用,这个调用会直接把参数中的消息内容原样发送。

View File

@ -1,6 +1,6 @@
{
"scripts": {
"docs:dev": "vuepress dev -h 127.0.0.1 -p 9090 --debug docs",
"docs:dev": "vuepress dev -h 0.0.0.0 -p 9090 --debug docs",
"docs:build": "vuepress build docs"
},
"devDependencies": {

View File

@ -8,7 +8,7 @@ stub_files = list(filter(lambda x: x.endswith('.pyi'), findall('nonebot')))
setup(
name='nonebot',
version='1.2.0',
version='1.2.1',
url='https://github.com/richardchien/nonebot',
license='MIT License',
author='Richard Chien',