clean up files

This commit is contained in:
yanyongyu
2020-06-24 21:56:49 +08:00
parent ed404901ca
commit 16099b2c35
99 changed files with 229 additions and 14193 deletions

View File

@ -1,42 +0,0 @@
# 概览
:::tip 提示
如果在阅读本文档时遇到难以理解的词汇,请随时查阅 [术语表](../glossary.md) 或使用 [Google 搜索](https://www.google.com/ncr)。
:::
:::tip 提示
初次使用时可能会觉得这里的概览过于枯燥,可以先简单略读之后直接前往 [安装](./installation.md) 查看安装方法,并进行后续的基础使用教程。
:::
NoneBot 是一个基于 [酷Q](https://cqp.cc/) 的 Python 异步 QQ 机器人框架,它会对 QQ 机器人收到的消息进行解析和处理,并以插件化的形式,分发给消息所对应的命令处理器和自然语言处理器,来完成具体的功能。
除了起到解析消息的作用NoneBot 还为插件提供了大量实用的预设操作和权限控制机制,尤其对于命令处理器,它更是提供了完善且易用的会话机制和内部调用机制,以分别适应命令的连续交互和插件内部功能复用等需求。
NoneBot 在其底层与 酷Q 交互的部分使用 [python-aiocqhttp](https://github.com/richardchien/python-aiocqhttp) 库,后者是 [CQHTTP 插件](https://cqhttp.cc/) 的一个 Python 异步 SDK在 [Quart](https://pgjones.gitlab.io/quart/) 的基础上封装了与 CQHTTP 插件的网络交互。
得益于 Python 的 [asyncio](https://docs.python.org/3/library/asyncio.html) 机制NoneBot 处理消息的吞吐量有了很大的保障,再配合 CQHTTP 插件可选的 WebSocket 通信方式也是最建议的通信方式NoneBot 的性能可以达到 HTTP 通信方式的两倍以上,相较于传统同步 I/O 的 HTTP 通信,更是有质的飞跃。
需要注意的是NoneBot 仅支持 Python 3.7+ 及 CQHTTP 插件 v4.8+。
## 它如何工作?
NoneBot 的运行离不开 酷Q 和 CQHTTP 插件。酷Q 扮演着「无头 QQ 客户端」的角色,它进行实际的消息、通知、请求的接收和发送,当 酷Q 收到消息时,它将这个消息包装为一个事件(通知和请求同理),并通过它自己的插件机制将事件传送给 CQHTTP 插件,后者再根据其配置中的 `post_url``ws_reverse_url` 等项来将事件发送至 NoneBot。
在 NoneBot 收到事件前,它底层的 aiocqhttp 实际已经先看到了事件aiocqhttp 根据事件的类型信息,通知到 NoneBot 的相应函数。特别地,对于消息类型的事件,还将消息内容转换成了 `aiocqhttp.message.Message` 类型,以便处理。
NoneBot 的事件处理函数收到通知后对于不同类型的事件再做相应的预处理和解析然后调用对应的插件并向其提供适合此类事件的会话Session对象。NoneBot 插件的编写者要做的,就是利用 Session 对象中提供的数据,在插件的处理函数中实现所需的功能。
## 示意图
![NoneBot 工作原理](../assets/diagram.png)
## 特色
- 基于异步 I/O
- 同时支持 HTTP 和反向 WebSocket 通信方式
- 支持命令、自然语言处理器等多种插件形式
- 支持多个机器人账号负载均衡
- 提供直观的交互式会话接口
- 命令和自然语言处理器提供权限控制机制
- 支持在命令会话运行过程中切换到其它命令或自然语言处理器
- 多种方式渲染要发送的消息内容,使对话足够自然

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,133 +0,0 @@
# 基本配置
到目前为止我们还在使用 NoneBot 的默认行为,在开始编写自己的插件之前,我们先尝试在配置文件上动动手脚,让 NoneBot 表现出不同的行为。
:::tip 提示
本章的完整代码可以在 [awesome-bot-1](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-1) 查看。
:::
## 项目结构
要使用自定义配置的话,我们的机器人代码将不再只有一个文件(`bot.py`),这时候良好的项目结构开始变得重要了。
在这里,我们创建一个名为 `awesome-bot` 的目录作为我们的项目主目录,你也可以使用其它你想要的名字。然后把之前的 `bot.py` 移动到 `awesome-bot` 中,再新建一个名为 `config.py` 的空文件。此时项目结构如下:
```
awesome-bot
├── bot.py
└── config.py
```
在后面几章中,我们将在此结构上进行改进和扩展。
## 配置超级用户
上一章中我们知道 NoneBot 内置了 `echo``say` 命令,我们已经测试了 `echo` 命令,并且正确地收到了机器人的回复,现在来尝试向它发送一个 `say` 命令:
```
/say [CQ:music,type=qq,id=209249583]
```
可以预料,命令不会起任何效果,因为我们提到过,`say` 命令只有超级用户可以调用,而现在我们还没有将自己的 QQ 号配置为超级用户。
因此下面我们往 `config.py` 中填充如下内容:
```python
from nonebot.default_config import *
SUPERUSERS = {12345678}
```
**这里的第 1 行是从 NoneBot 的默认配置中导入所有项,通常这是必须的,除非你知道自己在做什么,否则始终应该在配置文件的开头写上这一行。**
之后就是配置 `SUPERUSERS` 了,这个配置项的要求是值为 `int` 类型的**容器**,也就是说,可以是 `set``list``tuple` 等类型,元素类型为 `int``12345678` 是你想设置为超级用户的 QQ。
`config.py` 写好之后,修改 `bot.py` 如下:
```python {3,6}
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_builtin_plugins()
nonebot.run(host='127.0.0.1', port=8080)
```
第 3 行导入 `config.py` 模块,第 6 行将 `config.py` 作为配置对象传给 `nonebot.init()` 函数,这样 NoneBot 就知道了超级用户有哪些。
重启 NoneBot 后再次尝试发送:
```
/say [CQ:music,type=qq,id=209249583]
```
可以看到这次机器人成功地给你回复了一个音乐分享消息。
## 配置命令的起始字符
目前我们发送的命令都必须以一个特殊符号 `/` 开头实际上NoneBot 默认支持以 `/`、``、`!`、`` 其中之一作为开头,现在我们希望能够不需要特殊符号开头就可以调用命令,要做到这一点非常简单,在 `config.py` 添加一行即可:
```python {4}
from nonebot.default_config import *
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}
```
首先需要知道NoneBot 默认的 `COMMAND_START` 是一个 `set` 对象,如下:
```python
COMMAND_START = {'/', '!', '', ''}
```
这表示会尝试把 `/`、`!`、``、`` 开头的消息理解成命令。而我们上面修改了的 `COMMAND_START` 加入了空字符串 `''`,也就告诉了 NoneBot我们希望不需要任何起始字符也能调用命令。
`COMMAND_START` 的值和 `SUPERUSERS` 一样,可以是 `list`、`tuple`、`set` 等任意容器类型,元素类型可以是 `str` 或正则表达式,例如:
```python
import re
from nonebot.default_config import *
COMMAND_START = ['', re.compile(r'[/!]+')]
```
现在重启 NoneBot你就可以使用形如 `echo 你好,世界` 的消息来调用 `echo` 命令了,这么做的好处在 `echo` 命令中可能体现不出来,但对于其它实用型命令,可能会让使用更方便一些,比如天气查询命令:
```
天气 南京
```
这里命令名是 `天气`,参数是 `南京`,从肉眼上看起来非常直观,相比 `/天气 南京` 使用起来也更加舒适。
## 配置监听的 IP 和端口
当有了配置文件之后,我们可能会希望将 `nonebot.run()` 参数中的 `host` 和 `port` 移动到配置文件中,毕竟这两项是有可能随着运行场景的变化而有不同的需求的,把它们放到配置文件中有利于配置和代码的解耦。这同样很容易做到,只需进行如下配置:
```python {3-4}
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
```
然后在 `bot.py` 中就不再需要传入 `host` 和 `port`,如下:
```python {8}
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_builtin_plugins()
nonebot.run()
```
实际上,不需要配置这两项也可以直接使用 `nonebot.run()`NoneBot 会使用如下默认配置:
```python
HOST = '127.0.0.1'
PORT = 8080
```

View File

@ -1,8 +0,0 @@
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_builtin_plugins()
nonebot.run()

View File

@ -1,7 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}

View File

@ -1 +0,0 @@
nonebot>=1.1.0

View File

@ -1,43 +0,0 @@
from nonebot import on_command, CommandSession
# on_command 装饰器将函数声明为一个命令处理器
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
# 从会话状态session.state中获取城市名称city如果当前不存在则询问用户
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
# 获取城市的天气预报
weather_report = await get_weather_of_city(city)
# 向用户发送天气预报
await session.send(weather_report)
# weather.args_parser 装饰器将函数声明为 weather 命令的参数解析器
# 命令解析器用于将用户输入的参数解析成命令真正需要的数据
@weather.args_parser
async def _(session: CommandSession):
# 去掉消息首尾的空白符
stripped_arg = session.current_arg_text.strip()
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:
# 这里简单返回一个字符串
# 实际应用中,这里应该调用返回真实数据的天气 API并拼接成天气预报内容
return f'{city}的天气是……'

View File

@ -1,13 +0,0 @@
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -1,7 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}

View File

@ -1 +0,0 @@
nonebot>=1.1.0

View File

@ -1,50 +0,0 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
for word in words:
# 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性
if word.flag == 'ns':
# ns 词性表示地名
city = word.word
break
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -1,2 +0,0 @@
async def get_weather_of_city(city: str) -> str:
return f'{city}的天气是……'

View File

@ -1,13 +0,0 @@
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -1,8 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}

View File

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

View File

@ -1,86 +0,0 @@
import json
from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
# 注册一个仅内部使用的命令,不需要 aliases
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
if reply:
# 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户
# 转义会把消息中的某些特殊字符做转换,以避免 酷Q 将它们理解为 CQ 码
await session.send(escape(reply))
else:
# 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
# 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
@on_natural_language
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
# 调用图灵机器人的 API 获取回复
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None

View File

@ -1,50 +0,0 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
for word in words:
# 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性
if word.flag == 'ns':
# ns 词性表示地名
city = word.word
break
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -1,2 +0,0 @@
async def get_weather_of_city(city: str) -> str:
return f'{city}的天气是……'

View File

@ -1,13 +0,0 @@
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -1,10 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

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

View File

@ -1,21 +0,0 @@
from nonebot import on_request, RequestSession
from nonebot import on_notice, NoticeSession
# 将函数注册为群请求处理器
@on_request('group')
async def _(session: RequestSession):
# 判断验证信息是否符合要求
if session.event.comment == '暗号':
# 验证信息正确,同意入群
await session.approve()
return
# 验证信息错误,拒绝入群
await session.reject('请说暗号')
# 将函数注册为群成员增加通知处理器
@on_notice('group_increase')
async def _(session: NoticeSession):
# 发送欢迎消息
await session.send('欢迎新朋友~')

View File

@ -1,86 +0,0 @@
import json
from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
# 注册一个仅内部使用的命令,不需要 aliases
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
if reply:
# 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户
# 转义会把消息中的某些特殊字符做转换,以避免 酷Q 将它们理解为 CQ 码
await session.send(escape(reply))
else:
# 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
# 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
@on_natural_language
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
# 调用图灵机器人的 API 获取回复
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None

View File

@ -1,50 +0,0 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
for word in words:
# 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性
if word.flag == 'ns':
# ns 词性表示地名
city = word.word
break
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -1,2 +0,0 @@
async def get_weather_of_city(city: str) -> str:
return f'{city}的天气是……'

View File

@ -1,13 +0,0 @@
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -1,10 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

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

View File

@ -1,21 +0,0 @@
from nonebot import on_request, RequestSession
from nonebot import on_notice, NoticeSession
# 将函数注册为群请求处理器
@on_request('group')
async def _(session: RequestSession):
# 判断验证信息是否符合要求
if session.event.comment == '暗号':
# 验证信息正确,同意入群
await session.approve()
return
# 验证信息错误,拒绝入群
await session.reject('请说暗号')
# 将函数注册为群成员增加通知处理器
@on_notice('group_increase')
async def _(session: NoticeSession):
# 发送欢迎消息
await session.send('欢迎新朋友~')

View File

@ -1,16 +0,0 @@
from datetime import datetime
import nonebot
import pytz
from aiocqhttp.exceptions import Error as CQHttpError
@nonebot.scheduler.scheduled_job('cron', hour='*')
async def _():
bot = nonebot.get_bot()
now = datetime.now(pytz.timezone('Asia/Shanghai'))
try:
await bot.send_group_msg(group_id=672076603,
message=f'现在{now.hour}点整啦!')
except CQHttpError:
pass

View File

@ -1,86 +0,0 @@
import json
from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
# 注册一个仅内部使用的命令,不需要 aliases
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
if reply:
# 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户
# 转义会把消息中的某些特殊字符做转换,以避免 酷Q 将它们理解为 CQ 码
await session.send(escape(reply))
else:
# 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
# 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
@on_natural_language
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
# 调用图灵机器人的 API 获取回复
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None

View File

@ -1,50 +0,0 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
for word in words:
# 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性
if word.flag == 'ns':
# ns 词性表示地名
city = word.word
break
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -1,2 +0,0 @@
async def get_weather_of_city(city: str) -> str:
return f'{city}的天气是……'

View File

@ -1,13 +0,0 @@
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -1,10 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

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

View File

@ -1,21 +0,0 @@
from nonebot import on_request, RequestSession
from nonebot import on_notice, NoticeSession
# 将函数注册为群请求处理器
@on_request('group')
async def _(session: RequestSession):
# 判断验证信息是否符合要求
if session.event.comment == '暗号':
# 验证信息正确,同意入群
await session.approve()
return
# 验证信息错误,拒绝入群
await session.reject('请说暗号')
# 将函数注册为群成员增加通知处理器
@on_notice('group_increase')
async def _(session: NoticeSession):
# 发送欢迎消息
await session.send('欢迎新朋友~')

View File

@ -1,16 +0,0 @@
from datetime import datetime
import nonebot
import pytz
from aiocqhttp.exceptions import Error as CQHttpError
@nonebot.scheduler.scheduled_job('cron', hour='*')
async def _():
bot = nonebot.get_bot()
now = datetime.now(pytz.timezone('Asia/Shanghai'))
try:
await bot.send_group_msg(group_id=672076603,
message=f'现在{now.hour}点整啦!')
except CQHttpError:
pass

View File

@ -1,93 +0,0 @@
import json
from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
__plugin_name__ = '智能聊天'
__plugin_usage__ = r"""
智能聊天
直接跟我聊天即可~
""".strip()
# 定义无法获取图灵回复时的「表达Expression
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
# 注册一个仅内部使用的命令,不需要 aliases
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
if reply:
# 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户
# 转义会把消息中的某些特殊字符做转换,以避免 酷Q 将它们理解为 CQ 码
await session.send(escape(reply))
else:
# 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
# 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
@on_natural_language
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
# 调用图灵机器人的 API 获取回复
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None

View File

@ -1,20 +0,0 @@
import nonebot
from nonebot import on_command, CommandSession
@on_command('usage', aliases=['使用帮助', '帮助', '使用方法'])
async def _(session: CommandSession):
# 获取设置了名称的插件列表
plugins = list(filter(lambda p: p.name, nonebot.get_loaded_plugins()))
arg = session.current_arg_text.strip().lower()
if not arg:
# 如果用户没有发送参数,则发送功能列表
await session.send(
'我现在支持的功能有:\n\n' + '\n'.join(p.name for p in plugins))
return
# 如果发了参数则发送相应命令的使用帮助
for p in plugins:
if p.name.lower() == arg:
await session.send(p.usage)

View File

@ -1,57 +0,0 @@
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
__plugin_name__ = '天气'
__plugin_usage__ = r"""
天气查询
天气 [城市名称]
"""
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
for word in words:
# 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性
if word.flag == 'ns':
# ns 词性表示地名
city = word.word
break
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather', current_arg=city or '')

View File

@ -1,2 +0,0 @@
async def get_weather_of_city(city: str) -> str:
return f'{city}的天气是……'

View File

@ -1,13 +0,0 @@
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()

View File

@ -1,10 +0,0 @@
from nonebot.default_config import *
HOST = '0.0.0.0'
PORT = 8080
SUPERUSERS = {12345678}
COMMAND_START = {'', '/', '!', '', ''}
NICKNAME = {'小明', '明明'}
TULING_API_KEY = ''

View File

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

View File

@ -1,203 +0,0 @@
# 编写命令
本章将以一个天气查询插件为例,教你如何编写自己的命令。
:::tip 提示
本章的完整代码可以在 [awesome-bot-2](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-2) 查看。
:::
## 创建插件目录
首先我们需要创建一个目录来存放插件,这个目录需要满足一些条件才能作为插件目录,首先,我们的代码能够比较容易访问到它,其次,它必须是一个能够以 Python 模块形式导入的路径(后面解释为什么),一个比较好的位置是项目目录中的 `awesome/plugins/`,创建好之后,我们的 `awesome-bot` 项目的目录结构如下:
```
awesome-bot
├── awesome
│ └── plugins
├── bot.py
└── config.py
```
接着在 `plugins` 目录中新建一个名为 `weather.py` 的 Python 文件,暂时留空,此时目录结构如下:
```
awesome-bot
├── awesome
│ └── plugins
│ └── weather.py
├── bot.py
└── config.py
```
## 加载插件
现在我们的插件目录已经有了一个空的 `weather.py`,实际上它已经可以被称为一个插件了,尽管它还什么都没做。下面我们来让 NoneBot 加载这个插件,修改 `bot.py` 如下:
```python {1,9-12}
from os import path
import nonebot
import config
if __name__ == '__main__':
nonebot.init(config)
nonebot.load_plugins(
path.join(path.dirname(__file__), 'awesome', 'plugins'),
'awesome.plugins'
)
nonebot.run()
```
这里的重点在于 `nonebot.load_plugins()` 函数的两个参数。第一个参数是插件目录的路径,这里根据 `bot.py` 的所在路径和相对路径拼接得到;第二个参数是导入插件模块时使用的模块名前缀,这个前缀要求必须是一个当前 Python 解释器可以导入的模块前缀NoneBot 会在它后面加上插件的模块名共同组成完整的模块名来让解释器导入,因此这里我们传入 `awesome.plugins`,当运行 `bot.py` 的时候Python 解释器就能够正确导入 `awesome.plugins.weather` 这个插件模块了。
尝试运行 `python bot.py`,可以看到日志输出了类似如下内容:
```
[2018-08-18 21:46:55,425 nonebot] INFO: Succeeded to import "awesome.plugins.weather"
```
这表示 NoneBot 已经成功加载到了 `weather` 插件。
:::warning 注意
如果你运行时没有输出成功导入插件的日志,请确保你的当前工作目录是在 `awesome-bot` 项目的主目录中。
如果仍然不行,尝试先在 `awesome-bot` 主目录中执行下面的命令:
```bash
export PYTHONPATH=. # Linux / macOS
set PYTHONPATH=. # Windows
```
:::
## 编写真正的内容
好了,现在已经确保插件可以正确加载,我们可以开始编写命令的实际代码了。在 `weather.py` 中添加如下代码:
```python
from nonebot import on_command, CommandSession
# on_command 装饰器将函数声明为一个命令处理器
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
# 从会话状态session.state中获取城市名称city如果当前不存在则询问用户
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
# 获取城市的天气预报
weather_report = await get_weather_of_city(city)
# 向用户发送天气预报
await session.send(weather_report)
# weather.args_parser 装饰器将函数声明为 weather 命令的参数解析器
# 命令解析器用于将用户输入的参数解析成命令真正需要的数据
@weather.args_parser
async def _(session: CommandSession):
# 去掉消息首尾的空白符
stripped_arg = session.current_arg_text.strip()
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:
# 这里简单返回一个字符串
# 实际应用中,这里应该调用返回真实数据的天气 API并拼接成天气预报内容
return f'{city}的天气是……'
```
:::tip 提示
从这里开始,你需要对 Python 的 asyncio 编程有所了解,因为 NoneBot 是完全基于 asyncio 的,具体可以参考 [廖雪峰的 Python 教程](https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143208573480558080fa77514407cb23834c78c6c7309000)。
:::
为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API。
上面的代码中基本上每一行做了什么都在注释里写了,下面详细解释几个重要的地方。
要理解这段代码,我们要先单独看这个函数:
```python
# on_command 装饰器将函数声明为一个命令处理器
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
# 从会话状态session.state中获取城市名称city如果当前不存在则询问用户
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
# 获取城市的天气预报
weather_report = await get_weather_of_city(city)
# 向用户发送天气预报
await session.send(weather_report)
```
首先,`session.get()` 函数调用尝试从当前会话Session的状态中获取 `city` 这个参数,**所有的参数和会话中需要暂存的临时数据都被存储在 `session.state` 变量(一个 `dict`)中**,如果发现存在,则直接返回,并赋值给 `city` 变量,而如果 `city` 参数不存在,`session.get()` 会**中断**这次命令处理的流程,并保存当前会话,然后向用户发送 `prompt` 参数的内容。**这里的「中断」,意味着如果当前不存在 `city` 参数,`session.get()` 之后的代码将不会被执行,这是通过抛出异常做到的。**
向用户发送 `prompt` 中的提示之后,会话会进入等待状态,此时我们称之为「当前用户正在 weather 命令的会话中」当用户再次发送消息时NoneBot 会唤起这个等待中的会话,并重新执行命令,也就是**从头开始**重新执行上面的这个函数,如果用户在一定时间内(默认 5 分钟,可通过 `SESSION_EXPIRE_TIMEOUT` 配置项来更改)都没有再次跟机器人发消息,则会话因超时被关闭。
你可能想问了,既然是重新执行,那执行到 `session.get()` 的时候不还是会中断吗实际上NoneBot 在 1.0.0 及更早版本中确实是这样的,必须手动编写下面要说的参数解析器,才能够让 `session.get()` 正确返回;而从 1.1.0 版本开始NoneBot 会默认地把用户的完整输入作为当前询问内容的回答放进会话状态。
:::tip 提示
删掉下面这段参数解析器,天气命令也可以正常使用,可以尝试不同的输入,看看行为上有什么不同。
:::
但这里我们还是手动编写参数解析器,以应对更复杂的情况,也就是下面这个函数:
```python
# weather.args_parser 装饰器将函数声明为 weather 命令的参数解析器
# 命令解析器用于将用户输入的参数解析成命令真正需要的数据
@weather.args_parser
async def _(session: CommandSession):
# 去掉消息首尾的空白符
stripped_arg = session.current_arg_text.strip()
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` 参数和命令处理函数一样,都是当前命令的会话对象。并且,参数解析器会在命令处理函数之前执行,以确保正确解析参数以供后者使用。
上面的例子中,参数解析器会判断当前是否是该会话第一次运行(用户刚发送 `/天气`,触发了天气命令)。如果是,则检查用户触发天气命令时有没有附带参数(即 `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 后尝试向它分别发送下面的两个带参数和不带参数的消息:
```
/天气 南京
/天气
```
观察看看有什么不同,以及它的回复是否符合我们对代码的理解。如果成功的话,此时你已经完成了一个**可交互的**天气查询命令的雏形,只需要再接入天气 API 就可以真正投入使用了!

View File

@ -1,72 +0,0 @@
# CQHTTP 事件和 API
到目前为止,我们都在使用 NoneBot 显式提供的接口,但实际上 CQHTTP 插件还提供了更多的事件数据和 API可能利用这些它们实现更加自由的逻辑。
## 事件数据
在 [发生了什么?](./whats-happened.md) 中我们提到,收到 酷Q 事件后CQHTTP 通过反向 WebSocket 给 NoneBot 发送事件数据。这些数据被 aiocqhttp 包装为 [`aiocqhttp.Event`](https://python-aiocqhttp.cqp.moe/module/aiocqhttp/#aiocqhttp.Event) 对象,随后被 NoneBot 放在了 `session.event` 属性。该对象本质上是一个字典(但也提供了属性来获取其中的字段),你可以通过断点调试或打印等方式查看它的内容,其中的字段名和含义见 CQHTTP 的 [事件列表](https://cqhttp.cc/docs/#/Post?id=事件列表) 中的「上报数据」。
## API 调用
前面我们已经多次调用 `CommandSession` 类的 `send()` 方法,而这个方法只能回复给消息的发送方,不能手动指定发送者,因此当我们需要实现将收到的消息经过处理后转发给另一个接收方这样的功能时,这个方法就用不了了。
幸运的是,`NoneBot` 类是继承自 aiocqhttp 的 [`CQHttp` 类](https://python-aiocqhttp.cqp.moe/module/aiocqhttp/#aiocqhttp.CQHttp) 的,而这个类实现了 `__getattr__()` 魔术方法,由此提供了直接通过 bot 对象调用 CQHTTP 的 API 的能力。
:::tip 提示
如果你在使用 HTTP 通信,要调用 CQHTTP API 要在 `config.py` 中添加:
```python
API_ROOT = 'http://127.0.0.1:5700' # 这里 IP 和端口应与 CQHTTP 配置中的 `host` 和 `port` 对应
```
:::
要获取 bot 对象,可以通过如下两种方式:
```python
bot = session.bot
bot = nonebot.get_bot()
```
Bot 对象的使用方式如下:
```python
await bot.send_private_msg(user_id=12345678, message='你好~')
```
这里,`send_private_msg` 实际上对应 CQHTTP 的 [`/send_private_msg` 接口](https://cqhttp.cc/docs/#/API?id=send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF),其它接口同理。
通过这种方式调用 API 时,需要注意下面几点:
- **所有参数必须为命名参数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=event.self_id)
```
另外,在需要动态性的场合,除了使用 `getattr()` 方法外,还可以直接调用 `bot.call_action()` 方法,传入 `action` 和 `params` 即可,例如上例中,`action` 为 `'send_private_msg'``params` 为 `{'user_id': 12345678, 'message': '你好~'}`。
下面举出一些主动发送消息和调用 API 的例子:
```python
await bot.send_private_msg(user_id=12345678, message='你好~')
await bot.send_group_msg(group_id=123456, message='大家好~')
params = session.event.copy()
del params['message']
await bot.send_msg(**params, message='喵~')
await bot.delete_msg(**session.event)
await bot.set_group_card(**session.event, card='新人请改群名片')
self_info = await bot.get_login_info()
group_member_info = await bot.get_group_member_info(group_id=123456, user_id=12345678, no_cache=True)
```
其它更多接口请自行参考 CQHTTP 的 [API 列表](https://cqhttp.cc/docs/#/API?id=api-列表)。

View File

@ -1,104 +0,0 @@
# 开始使用
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 NoneBot 实例的准备。
## 最小实例
使用你最熟悉的编辑器或 IDE创建一个名为 `bot.py` 的文件,内容如下:
```python
import nonebot
if __name__ == '__main__':
nonebot.init()
nonebot.load_builtin_plugins()
nonebot.run(host='127.0.0.1', port=8080)
```
`if __name__ == '__main__'` 语句块的这几行代码将依次:
1. 使用默认配置初始化 NoneBot 包
2. 加载 NoneBot 内置的插件
3. 在地址 `127.0.0.1:8080` 运行 NoneBot
:::tip 提示
这里 `nonebot.run()` 的参数 `host='127.0.0.1'` 表示让 NoneBot 监听本地环回地址,如果你的 酷Q 运行在非本机的其它位置,例如 Docker 容器内、局域网内的另一台机器上等,则这里需要修改 `host` 参数为希望让 CQHTTP 插件访问的 IP。如果不清楚该使用哪个 IP或者希望本机的所有 IP 都被监听,可以使用 `0.0.0.0`
:::
在命令行使用如下命令即可运行这个 NoneBot 实例:
```bash
python bot.py
```
运行后会产生如下日志:
```
[2020-03-16 15:50:26,166 nonebot] INFO: Succeeded to import "nonebot.plugins.base"
[2020-03-16 15:50:26,166 nonebot] INFO: Running on 127.0.0.1:8080
Running on http://127.0.0.1:8080 (CTRL + C to quit)
[2020-03-16 15:50:26,177] Running on 127.0.0.1:8080 over http (CTRL + C to quit)
```
除此之外可能有一些红色的提示信息如 `ujson module not found, using json` 等,可以忽略。
## 配置 CQHTTP 插件
单纯运行 NoneBot 实例并不会产生任何效果,因为此刻 酷Q 这边还不知道 NoneBot 的存在,也就无法把消息发送给它,因此现在需要对 CQHTTP 插件做一个简单的配置来让它把消息等事件上报给 NoneBot。
如果你在之前已经按照 [安装](/guide/installation.md) 的建议使用默认配置运行了一次 CQHTTP 插件,此时 酷Q 的 `data/app/io.github.richardchien.coolqhttpapi/config/` 目录中应该已经有了一个名为 `<user-id>.json` 的文件(`<user-id>` 为你登录的 QQ 账号)。修改这个文件,**修改如下配置项(如果不存在相应字段则添加)**
:::warning 注意
如果使用 CQHTTP 插件官方 Docker 镜像运行 酷Q则配置文件所在目录可能是 `app/io.github.richardchien.coolqhttpapi/config/`
:::
```json
{
"ws_reverse_url": "ws://127.0.0.1:8080/ws/",
"use_ws_reverse": true,
"enable_heartbeat": true
}
```
:::tip 提示
**这里的 `127.0.0.1:8080` 对应 `nonebot.run()` 中传入的 `host` 和 `port`**,如果在 `nonebot.run()` 中传入的 `host``0.0.0.0`,则插件的配置中需使用任意一个能够访问到 NoneBot 所在环境的 IP**不要直接填 `0.0.0.0`**。特别地,如果你的 酷Q 运行在 Docker 容器中NoneBot 运行在宿主机中,则默认情况下这里需使用 `172.17.0.1`(即宿主机在 Docker 默认网桥上的 IP不同机器有可能不同如果是 Linux 系统,可以使用命令 `ip addr show docker0 | grep -Po 'inet \K[\d.]+'`来获取需要填入的ip如果是 macOS 系统或者 Windows 系统,可以考虑使用 `host.docker.internal`,具体解释详见 Docker 文档的 [Use cases and workarounds](https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds) 的「I WANT TO CONNECT FROM A CONTAINER TO A SERVICE ON THE HOST」小标题
:::
如果你的 CQHTTP 插件版本低于 v4.14.0,还需要删除配置文件中已有的 `ws_reverse_api_url``ws_reverse_event_url` 两项。
修改之后,在 酷Q 的应用菜单中重启 CQHTTP 插件,或直接重载应用,以使新的配置文件生效。
## 历史性的第一次对话
一旦新的配置文件正确生效之后NoneBot 所在的控制台(如果正在运行的话)应该会输出类似下面的内容(两条访问日志):
```
[2020-03-16 15:50:26,435] 127.0.0.1:56363 GET /ws/ 1.1 101 - 7982
[2020-03-16 15:50:26,438] 127.0.0.1:56364 GET /ws/ 1.1 101 - 8977
```
这表示 CQHTTP 插件已经成功地连接上了 NoneBot与此同时CQHTTP 的日志控制台(和日志文件)中也会输出反向 WebSocket 连接成功的日志。
:::warning 注意
如果到这一步你没有看到上面这样的成功日志CQHTTP 的日志中在不断地重连或无反应,请注意检查配置中的 IP 和端口是否确实可以访问。比较常见的出错点包括:
- NoneBot 监听 `0.0.0.0`,然后在 CQHTTP 配置中填了 `ws://0.0.0.0:8080/ws/`
- 在 Docker 容器内运行 酷Q 和 CQHTTP并通过 `127.0.0.1` 访问宿主机上的 NoneBot
- 想从公网访问,但没有修改云服务商的安全组策略或系统防火墙
- NoneBot 所监听的端口存在冲突,已被其它程序占用
- 弄混了 NoneBot 的 `host``port` 参数与 CQHTTP 配置中的 `host``port` 参数
- 使用旧版 CQHTTP 插件,且没有删除 `ws_reverse_api_url``ws_reverse_event_url`
- 使用旧版 CQHTTP 插件,且丢失了 `ws://127.0.0.1:8080/ws/` 结尾的 `/`
- `ws://` 错填为 `http://`
- 酷Q 或 CQHTTP 插件启动时遭到外星武器干扰
请尝试重启 CQHTTP、重启 酷Q、重启 NoneBot、更换端口、修改防火墙、重启系统、仔细阅读前面的文档及提示、更新 CQHTTP 和 NoneBot 到最新版本等方式来解决。
:::
现在,尝试向你的 QQ 机器人账号发送如下内容:
```
/echo 你好,世界
```
到这里如果一切 OK你应该会收到机器人给你回复了 `你好,世界`。这一历史性的对话标志着你已经成功地运行了一个 NoneBot 的最小实例,开始了编写更强大的 QQ 机器人的创意之旅!

View File

@ -1,41 +0,0 @@
# 安装
## NoneBot
:::warning 注意
请确保你的 Python 版本 >= 3.7。
:::
可以使用 pip 安装已发布的最新版本:
```bash
pip install nonebot
```
如果你需要使用最新的(可能尚未发布的)特性,可以克隆 Git 仓库后手动安装:
```bash
git clone https://github.com/richardchien/nonebot.git
cd nonebot
python setup.py install
```
以上命令中的 `pip``python` 可能需要根据情况换成 `pip3``python3`
## 酷Q
前往 酷Q 官方论坛的 [版本发布](https://cqp.cc/b/news) 页面根据需要下载最新版本的 酷Q Air 或 Pro解压后启动 `CQA.exe``CQP.exe` 并登录 QQ 机器人账号。
如果你的操作系统是 Linux 或 macOS可以使用版本发布页中 酷Q 官方提供的 Docker 镜像,也可以直接跳至下一个标题,使用 CQHTTP 插件官方提供的 Docker 镜像。
:::tip 提示
如果这是你第一次使用 酷Q建议完成它自带的新手教程以对 酷Q 的运行机制有所了解。
:::
## CQHTTP 插件
前往 [CQHTTP 插件文档](https://cqhttp.cc/docs/),按照其教程的「使用方法」安装插件。安装后,请先使用默认配置运行,并查看 酷Q 日志窗口的输出,以确定插件的加载、配置的生成和读取、插件版本等符合预期。
:::warning 注意
请确保你安装的插件版本 >= 4.8,通常建议插件在大版本内尽量及时升级至最新版本。
:::

View File

@ -1,266 +0,0 @@
# 编写自然语言处理器
在上一章中我们编写了一个天气查询命令,但它还具有非常强的局限性,用户必须发送固定格式的消息,它才能理解,即使它可以交互式地询问用户要查询的城市,用户仍然需要记住命令的名字。
本章将会介绍如何让插件能够理解用户的自然语言消息,例如:
```
今天南京天气怎么样?
```
:::tip 提示
本章的完整代码可以在 [awesome-bot-3](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-3) 查看。
:::
## 调整项目结构
在开始下一步之前,首先对项目结构再做一个调整,以方便后面的代码编写。
到目前为止 `weather.py` 中只有三个函数看起来还比较简单不过当我们再往里面添加更多功能之后它可能会变得比较杂乱。幸运的是NoneBot 除了支持加载 `.py` 文件Python 模块)形式的插件,还支持加载包含 `__init__.py` 的目录Python 包)。
下面我们对 `weather` 插件做一个调整,将 `get_weather_of_city()` 提取到单独的模块中(这个函数在实际应用中可能比较长,并且可能需要多个函数组合)。
首先创建 `weather` 目录,并将原来 `weather.py` 中的代码移动到 `weather/__init__.py` 文件(如果你使用 PyCharm 或 IDEA + Python 插件,可以右击 `weather.py` 并选择 Refactor - Convert to Python Package然后在 `weather` 目录中再创建 `data_source.py` 文件,将 `get_weather_of_city()` 函数移动进去。
经过这些步骤后,目录结构如下:
```
awesome-bot
├── awesome
│ └── plugins
│ └── weather
│ ├── __init__.py
│ └── data_source.py
├── bot.py
└── config.py
```
`weather/__init__.py` 内容如下:
```python
from nonebot import on_command, CommandSession
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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` 内容如下:
```python
async def get_weather_of_city(city: str) -> str:
return f'{city}的天气是……'
```
## 编写雏形
`weather/__init__.py` 文件添加内容如下:
```python {2,29-35}
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
return IntentCommand(90.0, 'weather')
```
代码中的注释已经进行了大部分解释,这里再详细介绍一下 `IntentCommand` 这个类。
在 NoneBot 中,自然语言处理器的工作方式就是将用户的自然语言消息解析成一个命令和命令所需的参数,由于自然语言消息的模糊性,在解析时不可能完全确定用户的意图,因此还需要返回一个置信度作为这个命令的确定程度。
:::warning 注意
这里的「置信度」与统计学中的置信度没有任何关系只表示对「当前用户输入的意图是触发某命令」这件事有多大把握应理解为普通意义的「confidence」。
:::
:::tip 提示
置信度的计算需要自然语言处理器的编写者进行恰当的设计,以确保各插件之间的功能不会互相冲突。
:::
在实际项目中,很多插件都会注册有自然语言处理器,其中每个都按照它的解析情况返回 `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` 命令。
现在运行 NoneBot尝试向机器人发送任何包含 `天气` 二字的消息,例如:
```
今天天气怎么样?
```
一切正常的话,它会询问你要查询的城市,这表示它正确的进入了 `weather` 命令的会话中。
## 安装结巴分词
下面我们将允许用户在消息中直接给出要查询的城市,要做到这一点,我们需要能够对消息进行分词和词性标注,以判断哪个词是城市名称。
到这里是真正的自然语言处理的领域了,我们为了简单起见,使用 [结巴分词](https://github.com/fxsjy/jieba) 来进行词性标注。
使用如下命令安装结巴分词:
```bash
pip install jieba
```
:::tip 提示
如果你没有使用过结巴分词,建议先前往它的 [项目主页](https://github.com/fxsjy/jieba) 查看代码示例以了解基本用法。
:::
## 完善自然语言处理器
有了结巴分词之后,扩充 `weather/__init__.py` 如下:
```python {3,35-49}
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from jieba import posseg
from .data_source import get_weather_of_city
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
city = session.get('city', prompt='你想查询哪个城市的天气呢?')
weather_report = await get_weather_of_city(city)
await session.send(weather_report)
@weather.args_parser
async def _(session: CommandSession):
stripped_arg = session.current_arg_text.strip()
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={'天气'})
async def _(session: NLPSession):
# 去掉消息首尾的空白符
stripped_msg = session.msg_text.strip()
# 对消息进行分词和词性标注
words = posseg.lcut(stripped_msg)
city = None
# 遍历 posseg.lcut 返回的列表
for word in words:
# 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性
if word.flag == 'ns':
# ns 词性表示地名
city = word.word
break
# 返回意图命令,前两个参数必填,分别表示置信度和意图命令名
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` 命令的参数传入 `IntentCommand`(如果 `city` 为空,则给 `current_arg` 传入空字符串)。
:::tip 提示
这里使用了 `current_arg`,因为之前编写的天气命令能够处理第一次运行时就附带了参数(城市名)的情况。
你也可以在你自己的功能中使用 `args` 传入更复杂的初始参数。
:::
现在运行 NoneBot尝试向机器人分别发送下面两句话
```
今天天气怎么样?
今天南京天气怎么样?
```
如果一切顺利,第一句它会问你要查询哪个城市,第二句会直接识别到城市。
## 理清自然语言处理器的逻辑
为了更好地理解自然语言处理器,这里再来尝试理清楚它的逻辑。
**自然语言处理器的核心功能,是从用户的任意消息中识别意图,并产生一个包含有初始参数的意图命令。**
比如上面例子中的自然语言处理器所做的事情,就是进行如下所示的意图识别:
```
今天天气怎么样? => /天气
今天南京天气怎么样? => /天气 南京
```
箭头左边是用户发送的**没有明确格式的任意消息**,右边是自然语言处理器从中识别出的**真正意图所对应的命令**。
## 优化群聊中的使用体验
到目前为止我们都只关注了私聊的情况,实际上我们的天气插件在群聊中也可以正常工作,但是有一个问题,我们必须 @ 机器人,它才会回复。一种解决办法是,给 `on_natural_language` 装饰器添加参数 `only_to_me=False`,这样的话,机器人将会响应所有群聊中含有 `天气` 关键词的消息,这对于某些功能的插件来说可能比较适用。另一种办法是通过配置项 `NICKNAME` 设置机器人的昵称,例如:
```python
NICKNAME = {'小明', '明明'}
```
`NICKNAME` 的值需要是一个 `Iterable`。设置了昵称之后,我们可以通过昵称来唤起机器人,例如:
```
小明,今天天气怎么样?
```
此处 `小明` 和 @ 的效果相同。
## 更精确的自然语言理解
如果你是一位自然语言处理领域的爱好者或从业人员,你可以在 NoneBot 中很方便地将你的理论研究应用到实例中,在自然语言处理器中使用更高级的 NLP 技术,并且,可以通过增加命令的参数,将自然语言的理解更加细化,以向用户提供更加顺畅的使用体验。

View File

@ -1,58 +0,0 @@
# 处理通知和请求
除了聊天消息酷Q 还提供了加群请求、加好友请求、出入群通知、管理员变动通知等很多其它事件,很多时候我们需要利用这些事件来实现群管功能,这也是 QQ 机器人除聊天之外的另一个很重要的应用之一。
本章将介绍如何在插件中处理通知和请求。
:::tip 提示
本章的完整代码可以在 [awesome-bot-5](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-5) 查看。
:::
## 自动同意加群请求
首先我们可能需要机器人根据条件自动同意加群请求,从而不再需要管理员手动操作。
新建 `awesome/plugins/group_admin.py`,编写代码如下:
```python
from nonebot import on_request, RequestSession
# 将函数注册为群请求处理器
@on_request('group')
async def _(session: RequestSession):
# 判断验证信息是否符合要求
if session.event.comment == '暗号':
# 验证信息正确,同意入群
await session.approve()
return
# 验证信息错误,拒绝入群
await session.reject('请说暗号')
```
这里首先 `on_request` 装饰器将函数注册为一个请求处理器,`group` 参数表示只处理群请求,这里各请求对应的参数值可以参考 [CQHTTP 插件的事件上报](https://cqhttp.cc/docs/#/Post?id=%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82) 的 `request_type` 字段,目前有 `group``friend` 两种。
接着判断 `session.event.comment` 是否是正确的暗号,这里 `session.event` 是一个 `aiocqhttp.Event` 对象,即 CQHTTP 上报来的事件的简单包装,`comment` 属性用于获取加群或加好友事件中的验证信息。
最后 `session.approve()``session.reject()` 分别用于同意和拒绝加群请求,如果都不调用,则忽略请求(其它管理员仍然可以处理请求)。
## 欢迎新成员
新成员入群之后,为了活跃气氛,我们可能希望机器人发一段欢迎消息。只需下面的代码即可实现:
```python
from nonebot import on_notice, NoticeSession
# 将函数注册为群成员增加通知处理器
@on_notice('group_increase')
async def _(session: NoticeSession):
# 发送欢迎消息
await session.send('欢迎新朋友~')
```
:::warning 注意
这里最好预先判断一下是不是你想发送的群(通过 `session.event.group_id`),否则机器人所在的任何群有新成员进入它都会欢迎。
:::
总的来说这些 `on_*` 装饰器用起来都是差不多的,这里的 `group_increase` 表示群成员增加,其它的通知类型可以参考 [CQHTTP 插件的事件上报](https://cqhttp.cc/docs/#/Post?id=%E7%BE%A4%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0) 的 `notice_type`

View File

@ -1,50 +0,0 @@
# 添加计划任务
实际应用中还经常会有定时执行任务的需求为了方便这类需求的开发NoneBot 可选地包含了计划任务功能。
:::tip 提示
本章的完整代码可以在 [awesome-bot-6](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-6) 查看。
:::
## 安装 `scheduler` 可选功能
计划任务功能在 NoneBot 中是可选功能,只有当同时安装了 [APScheduler](https://github.com/agronholm/apscheduler) 时,才会启用。
使用下面命令安装可选功能(会自动安装 APScheduler
```bash
pip install "nonebot[scheduler]"
```
安装成功之后就可以通过 `nonebot.scheduler` 访问 [`AsyncIOScheduler`](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/asyncio.html#apscheduler.schedulers.asyncio.AsyncIOScheduler) 对象。
## 定时发送消息
这里以一个整点报时的功能为例,来介绍定时任务的使用。
新建文件 `awesome/plugins/scheduler.py`,编写代码如下:
```python {8}
from datetime import datetime
import nonebot
import pytz
from aiocqhttp.exceptions import Error as CQHttpError
@nonebot.scheduler.scheduled_job('cron', hour='*')
async def _():
bot = nonebot.get_bot()
now = datetime.now(pytz.timezone('Asia/Shanghai'))
try:
await bot.send_group_msg(group_id=672076603,
message=f'现在{now.hour}点整啦!')
except CQHttpError:
pass
```
这里最主要的就是第 8 行,`nonebot.scheduler.scheduled_job()` 是一个装饰器,第一个参数是触发器类型(这里是 `cron`,表示使用 [Cron](https://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html#module-apscheduler.triggers.cron) 类型的触发参数)。这里 `hour='*'` 表示每小时都执行,`minute` 和 `second` 不填时默认为 `0`,也就是说装饰器所装饰的这个函数会在每小时的第一秒被执行。
除了 `cron`,还有两种触发器类型 `interval` 和 `date`。例如,你可以使用 `nonebot.scheduler.scheduled_job('interval', minutes=10)` 来每十分钟执行一次任务。
限于篇幅,这里无法给出太详细的接口介绍,`nonebot.scheduler` 是一个 APScheduler 的 `AsyncIOScheduler` 对象,因此关于它的更多使用方法,可以参考 [APScheduler 的官方文档](https://apscheduler.readthedocs.io/en/latest/userguide.html)。

View File

@ -1,254 +0,0 @@
# 接入图灵机器人
:::danger 重要
本章内容可能已经过时,即将更新。
:::
到目前为止我们已经编写了一个相对完整的天气查询插件,包括命令和自然语言处理器,除此之外,使用同样的方法,还可以编写更多功能的插件。
但这样的套路存在一个问题,如果我们不是专业的 NLP 工程师,开放话题的智能聊天仍然是我们无法自己完成的事情,用户只能通过特定插件所支持的句式来使用相应的功能,当用户试图使用我们暂时没有开发的功能时,我们的机器人显得似乎有些无能为力。
不过还是有解决方案的,市面上有一些提供智能聊天机器人接口的厂商,本章我们以 [图灵机器人](http://www.tuling123.com/) 为例,因为它的使用比较广泛,接入也比较简单,不过缺点是免费调用次数比较少。
:::tip 提示
本章的完整代码可以在 [awesome-bot-4](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-4) 查看。
:::
## 注册图灵机器人账号
首先前往 [图灵机器人官网](http://www.tuling123.com/) 注册账号,然后在「机器人管理」页根据它的提示创建机器人,可以设置机器人名字、属性、技能、语料库等。
:::warning 注意
图灵机器人的免费套餐现在需要实名认证后才可使用。
:::
注册完成后先放一边,或者如果有兴趣的话,在网页上的聊天窗口和它聊几句看看效果。
## 编写图灵机器人插件
新建 `awesome/plugins/tuling.py` 文件,编写如下内容:
```python
import json
from typing import Optional
import aiohttp
from aiocqhttp.message import escape
from nonebot import on_command, CommandSession
from nonebot import on_natural_language, NLPSession, IntentCommand
from nonebot.helpers import context_id, render_expression
# 定义无法获取图灵回复时的「表达Expression
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
# 注册一个仅内部使用的命令,不需要 aliases
@on_command('tuling')
async def tuling(session: CommandSession):
# 获取可选参数,这里如果没有 message 参数命令不会被中断message 变量会是 None
message = session.state.get('message')
# 通过封装的函数获取图灵机器人的回复
reply = await call_tuling_api(session, message)
if reply:
# 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户
# 转义会把消息中的某些特殊字符做转换,以避免 酷Q 将它们理解为 CQ 码
await session.send(escape(reply))
else:
# 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
# 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
@on_natural_language
async def _(session: NLPSession):
# 以置信度 60.0 返回 tuling 命令
# 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
# 调用图灵机器人的 API 获取回复
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None
```
上面这段代码比较长,而且有一些新出现的函数和概念,我们后面会慢慢地详解,不过现在先在 `config.py` 中添加一项:
```python
TULING_API_KEY = ''
```
`TULING_API_KEY` 的值填图灵机器人的「机器人设置」页面最下方提供的 API Key。
配置完成后来运行 NoneBot尝试给机器人随便发送一条消息看看它是不是正确地获取了图灵机器人的回复。
## 理解自然语言处理器
我们先来理解代码中最简单的部分:
```python {3}
@on_natural_language
async def _(session: NLPSession):
return IntentCommand(60.0, 'tuling', args={'message': session.msg_text})
```
根据我们前面一章中已经知道的用法,这里就是直接返回置信度为 60.0 的 `tuling` 命令。之所以返回置信度 60.0,是因为自然语言处理器所返回的结果最终会按置信度排序,取置信度最高且大于等于 60.0 的结果来执行。把置信度设为 60.0 可以保证一条消息无法被其它自然语言处理器理解的时候 fallback 到 `tuling` 命令。
## 理解图灵机器人接口的 HTTP 调用
图灵机器人接口的调用也非常简单,虽然看起来代码挺多,但新的概念并不多。
```python {7-23,26-37}
async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]:
if not text:
return None
url = 'http://openapi.tuling123.com/openapi/api/v2'
# 构造请求数据
payload = {
'reqType': 0,
'perception': {
'inputText': {
'text': text
}
},
'userInfo': {
'apiKey': session.bot.config.TULING_API_KEY,
'userId': context_id(session.ctx, use_hash=True)
}
}
group_unique_id = context_id(session.ctx, mode='group', use_hash=True)
if group_unique_id:
payload['userInfo']['groupId'] = group_unique_id
try:
async with aiohttp.ClientSession() as sess:
async with sess.post(url, json=payload) as response:
if response.status != 200:
# 如果 HTTP 响应状态码不是 200说明调用失败
return None
resp_payload = json.loads(await response.text())
if resp_payload['results']:
for result in resp_payload['results']:
if result['resultType'] == 'text':
# 返回文本类型的回复
return result['values']['text']
except (aiohttp.ClientError, json.JSONDecodeError, KeyError):
# 抛出上面任何异常,说明调用失败
return None
```
这里的代码主要需要参考 [图灵机器人的官方 API 文档](https://www.kancloud.cn/turing/www-tuling123-com/718227)。
### 构造请求数据
第一段高亮部分是根据图灵机器人的文档构造请求数据,其中有几个需要注意的地方:第 16、17 和 21 行。
第 16 行通过 `session.bot.config` 访问了 NoneBot 的配置对象,`session.bot` 就是当前正在运行的 NoneBot 对象,你在其它任何地方都可以这么用(前提是已经调用过 `nonebot.init()`)。
第 17 和 21 行调用了 `context_id()` 函数,这是 `nonebot.helpers` 模块中提供的一个函数,用于计算 Context 的独特 ID有三种模式可以选择通过 `mode` 参数传入):`default`、`group`、`user`,默认 `default`,它们的效果如下表:
| 模式 | 效果 |
| ------------ | --- |
| `default` | 每个用户在每个群、讨论组和私聊都对应不同的 ID |
| `group` | 每个群或讨论组内的成员共用一个 ID私聊仍按用户区分 |
| `user` | 每个用户对应不同的 ID但不区分用户是在私聊还是群或讨论组 |
`context_id()` 函数还提供 `use_hash` 参数可选地将计算出的 ID 进行 MD5 哈希,以适应某些应用场景。
### 发送请求
第二段高亮的代码是使用 [aiohttp](https://aiohttp.readthedocs.io/en/stable/) 发送 HTTP POST 请求给图灵机器人,并获取它的回复,这段其实没有什么跟 NoneBot 有关的东西,请参考前面给出的图灵机器人的官方 API 文档,里面详细解释了每个返回字段的含义。
## 理解命令处理器
命令处理器这部分虽然代码比较少,但引入了不少新的概念。
```python {1,3-8,13,16,18}
from aiocqhttp.message import escape
EXPR_DONT_UNDERSTAND = (
'我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!',
'我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛',
'其实我不太明白你的意思……',
'抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~'
)
@on_command('tuling')
async def tuling(session: CommandSession):
message = session.state.get('message')
reply = await call_tuling_api(session, message)
if reply:
await session.send(escape(reply))
else:
await session.send(render_expression(EXPR_DONT_UNDERSTAND))
```
### 可选参数
首先看第 13 行,`session.state.get()` 可用于获取命令的可选参数,也就是说,从 `session.state` 中尝试获取一个参数(还记得 `IntentCommand` 的 `args` 参数内容会全部进入 `CommandSession` 的 `state` 吗),如果没有,返回 `None`,但并不会中断命令的执行。其实这就是 `dict.get()` 方法。
### 消息转义
再看第 16 行,在调用 `session.send()` 之前先对 `reply` 调用了 `escape()`,这个 `escape()` 函数是 `aiocqhttp.message` 模块提供的,用于将字符串中的某些特殊字符进行转义。具体来说,这些特殊字符是 酷Q 看作是 CQ 码的一部分的那些字符,包括 `&`、`[`、`]`、`,`。
CQ 码是 酷Q 用来表示非文本消息的一种表示方法,形如 `[CQ:image,file=ABC.jpg]`。具体的格式规则,请参考 酷Q 文档的 [CQ 码](https://d.cqp.me/Pro/CQ%E7%A0%81) 和 CoolQ HTTP API 插件文档的 [CQ 码](https://cqhttp.cc/docs/#/CQCode)。
### 发送 Expression
第 18 行使用了 NoneBot 中 Expression 这个概念,或称为「表达」。
Expression 可以是一个 `str`、元素类型是 `str` 的序列(一般为 `list` 或 `tuple`)或返回类型为 `str` 的 `Callable`。
`render_expression()` 函数用于将 Expression 渲染成字符串。它首先判断 Expression 的类型,如果 Expression 是一个序列,则首先随机取其中的一个元素,如果是一个 `Callable`,则调用函数获取返回值。拿到最终的 `str` 类型的 Expression 之后,对它调用 `str.format()` 方法,格式化参数传入 `render_expression()` 函数的命名参数(`**kwargs`),最后返回格式化后的结果。特别地,如果 Expression 是个 `Callable`,在调用它获取返回值的时候,也会传入 `**kwargs`,以便函数根据参数来构造字符串。
你可以通过使用序列或 `Callable` 类型的 Expression 来让机器人的回复显得更加自然,甚至,可以利用更高级的人工智能技术来生成对话。

View File

@ -1,76 +0,0 @@
# 编写使用帮助
经过前面的部分,我们已经给机器人编写了天气查询和图灵聊天插件,当然,你可能已经另外编写了更多具有个性化功能的插件。
现在,为了让用户能够更方便的使用,是时候编写一个使用帮助了。
:::tip 提示
本章的完整代码可以在 [awesome-bot-7](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-7) 查看。
:::
## 给插件添加名称和用法
这里以天气查询和图灵聊天两个插件为例,分别在 `awesome/plugins/weather/__init__.py``awesome/plugins/tuling.py` 两个文件的开头,通过 `__plugin_name__``__plugin_usage__` 两个特殊变量设置插件的名称和使用方法,如下:
```python
# awesome/plugins/weather/__init__.py
# ... 各种 import
__plugin_name__ = '天气'
__plugin_usage__ = r"""
天气查询
天气 [城市名称]
"""
```
```python
# awesome/plugins/tuling.py
# ... 各种 import
__plugin_name__ = '智能聊天'
__plugin_usage__ = r"""
智能聊天
直接跟我聊天即可~
""".strip()
```
一旦使用 `__plugin_name__``__plugin_usage__` 特殊变量设置了插件的名称和使用方法NoneBot 在加载插件时就能够读取到这些内容,并存放在已加载插件的数据结构中。
## 编写使用帮助命令
新建插件 `awesome/plugins/usage.py`,编写内容如下:
```python {8,13-14,20}
import nonebot
from nonebot import on_command, CommandSession
@on_command('usage', aliases=['使用帮助', '帮助', '使用方法'])
async def _(session: CommandSession):
# 获取设置了名称的插件列表
plugins = list(filter(lambda p: p.name, nonebot.get_loaded_plugins()))
arg = session.current_arg_text.strip().lower()
if not arg:
# 如果用户没有发送参数,则发送功能列表
await session.send(
'我现在支持的功能有:\n\n' + '\n'.join(p.name for p in plugins))
return
# 如果发了参数则发送相应命令的使用帮助
for p in plugins:
if p.name.lower() == arg:
await session.send(p.usage)
```
这里高亮的内容是重点:
- `nonebot.get_loaded_plugins()` 函数用于获取所有已经加载的插件,**注意,由于可能存在插件没有设置 `__plugin_name__` 变量的情况,插件的名称有可能为空**,因此建议过滤一下
- 插件的 `name` 属性(`plugin.name`)用于获得插件模块的 `__plugin_name__` 特殊变量的值
- 插件的 `usage` 属性(`plugin.usage`)用于获得插件模块的 `__plugin_usage__` 特殊变量的值
到这里,使用帮助命令就已经编写完成了。如果愿意,可以继续按照自己的思路实现相对应的自然语言处理器,以优化使用体验。

View File

@ -1,88 +0,0 @@
# 发生了什么?
上一章中我们已经运行了一个最小的 NoneBot 实例,在看着 QQ 机器人回复了自己的消息的同时你可能想问这是如何实现的具体来说NoneBot、CQHTTP 插件、酷Q这三者是如何协同工作的本章将对这个问题做一个初步解答。
:::tip 提示
如果你已经有较丰富的 QQ 机器人开发经验,尤其是使用 CQHTTP 插件的经验,可以直接跳到 [NoneBot 出场](#nonebot-出场)。
:::
## 一切从 酷Q 开始
我们在 [概览](./README.md) 中提到过酷Q 扮演着「无头 QQ 客户端」的角色,一切的消息、通知、请求的发送和接收,最根本上都是由它来完成的,我们的最小 NoneBot 实例也不例外。
首先,我们向机器人发送的 `/echo 你好,世界` 进入腾讯的服务器,后者随后会把消息推送给 酷Q就像推送给一个真正的 QQ 客户端一样。到这里酷Q 就已经收到了我们发送的消息了。
## 进入 CQHTTP 插件
酷Q 在收到消息之后,按优先级依次将消息转交给已启用的各插件处理,在我们的例子中,只有一个插件,就是 CQHTTP 插件。
CQHTTP 插件收到消息后会将其包装为一个统一的事件格式并对消息内容进行一个初步的处理例如编码转换、数组化、CQ 码增强等,这里的细节目前为止不需要完全明白,在需要的时候,可以去参考 CQHTTP 插件的 [文档](https://cqhttp.cc/docs/)。
接着,插件把包装好的事件转换成 JSON 格式,并通过「反向 WebSocket」发送给 NoneBot。这里的「反向 WebSocket」连接的就是我们在 CQHTTP 插件的配置中指定的 `ws_reverse_url`,即 NoneBot 监听的 WebSocket 入口。
:::tip 提示
「反向 WebSocket」是 CQHTTP 插件的一种通信方式,表示插件作为客户端,主动去连接配置文件中指定的 `ws_reverse_url`。除此之外还有 HTTP、正向WebSocket 等方式。除了反向 WebSocketNoneBot 也支持通过 HTTP 与 CQHTTP 通信。
:::
## NoneBot 出场
CQHTTP 插件通过反向 WebSocket 将消息事件发送到 NoneBot 后NoneBot 就开始了它的处理流程。
### 初步处理
首先 NoneBot 利用底层的 aiocqhttp 区分事件类型,并通知到相应的函数,本例中,相应的函数就是负责处理消息的函数。
负责处理消息的函数会尝试把消息作为一个命令来解析,根据默认配置,它发现消息内容 `/echo 你好,世界` 符合命令的一个特征——以 `/` 开头,剥离掉这个起始字符之后,消息变为 `echo 你好,世界`,紧接着,它读取第一个空白字符之前的内容,即 `echo`,将其理解为命令的名字。
:::tip 提示
实际上,它还会使用配置中的分隔符对 `echo` 做一个分割,不过这里分割完也只有一个部分,所以实际命令名字为 `('echo',)`,形式是一个 Python 元组;而如果我们发送的命令是 `note.add`,分割之后就是 `('note', 'add')`
:::
### 理解最小实例的代码
到这里,我们先暂停一下对消息事件的行踪的描述,回头来说一下最小实例的代码:
```python {4-6}
import nonebot
if __name__ == '__main__':
nonebot.init()
nonebot.load_builtin_plugins()
nonebot.run(host='127.0.0.1', port=8080)
```
第 4 行的 `nonebot.init()` 首先初始化 `nonebot` 包,这是无论如何都需要写的一行代码,并且必须在使用 NoneBot 的任何功能之前调用。
随后,`nonebot.load_builtin_plugins()` 加载了 NoneBot 的内置插件,这一步不是必须的,尤其在你编写了自己的插件之后,可能不再需要内置插件。
NoneBot 的内置插件只包含了两个命令,`echo` 和 `say`,两者的功能都是重复发送者的话,区别在于,`echo` 命令任何人都可以调用(不限制权限),但只能原样重复消息,不能手动指定要发送的 CQ 码,`say` 命令只有超级用户(通常是你自己,需要在配置中指定,下一章会介绍)可以调用,可以在消息中指定要发送的 CQ 码,如下图:
<p style="text-align: center">
<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` 地址上,向 CQHTTP 插件提供 `/`、`/ws/` 等入口,在我们的反向 WebSocket 配置中,插件连接了 `/ws/`。
### 命令处理器
现在,我们知道了最小 NoneBot 实例中已经加载了 `echo` 和 `say` 两个命令,在 [初步处理](#初步处理) 中也知道了消息内容符合命令的格式,并且从中拿到了命令名(`echo`),这时候消息处理函数发现,这条消息中解析出来的命令确实是存在的,于是它将剩余部分(`你好,世界`)当做命令的参数,并通过命令名获取到对应的命令处理器,然后把参数、消息事件中附带的其它信息一起打包成一个 Session 对象(具体来说,是一个 `CommandSession` 类的对象),传给命令处理器来调用它。
`echo` 命令处理器的代码其实非常简单,如下:
```python
@on_command('echo')
async def echo(session: CommandSession):
await session.send(session.state.get('message') or session.current_arg)
```
你现在不用关心它是如何从 Session 中拿到参数的,只需看到,命令处理器中实际内容只有一行 `session.send()` 函数调用,这个调用会直接把参数中的消息内容原样发送。
## 再次进入 CQHTTP 插件
命令处理器在调用 `session.send()` 之后NoneBot 把消息内容发送给了 CQHTTP 插件那边已连接的反向 WebSocket 客户端同时告诉它要把消息发送到和收到消息相同的地方即接收到消息所在的群组、讨论组或私聊。CQHTTP 插件明白了 NoneBot 的要求之后,会对消息做一些必要的处理,然后按照指示调用 酷Q 提供的相应接口。
## 一切又在 酷Q 结束
酷Q 收到 CQHTTP 插件的接口调用之后,将消息内容发送给腾讯的服务器,就像一个真正的 QQ 客户端一样,于是你就收到了 QQ 机器人发来的消息了。
至此,我们已经理清楚了第一次对话中每一步到底都发生了些什么,以及 NoneBot 如何解析消息并调用到相应的命令处理器来进行回复。下面的几章中我们将一步一步地对最小 NoneBot 实例进行扩充,以实现一些非常棒的功能!

View File

@ -1,5 +0,0 @@
# 下一步做什么?
在阅读完前面的入门指南之后,你已经具备了实现具有复杂功能的 QQ 机器人的基本知识,可以开始编写完整的作品了。
在实际编写代码时,可能需要参考 [CQHTTP 文档](https://cqhttp.cc/docs/) 和 [aiocqhttp 文档](https://python-aiocqhttp.cqp.moe/);对于一些高级主题,可以参考本文档 [进阶](../advanced/README.md) 部分;另外,也可以参考 [cczu-osa/aki](https://github.com/cczu-osa/aki) 中的一些实践,比如模块划分、数据库访问等。