clean up files
37
.editorconfig
Normal file
@ -0,0 +1,37 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
# The JSON files contain newlines inconsistently
|
||||
[*.json]
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified JavaScript files shouldn't be changed
|
||||
[**.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Makefiles always use tabs for indentation
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Batch files use tabs for indentation
|
||||
[*.bat]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
1
.gitignore
vendored
@ -186,4 +186,3 @@ typings/
|
||||
.idea
|
||||
.vscode
|
||||
dev
|
||||
doc
|
||||
|
2
.style.yapf
Normal file
@ -0,0 +1,2 @@
|
||||
[style]
|
||||
based_on_style = google
|
@ -1,80 +0,0 @@
|
||||
module.exports = {
|
||||
title: 'NoneBot',
|
||||
description: '基于 酷Q 的 Python 异步 QQ 机器人框架',
|
||||
markdown: {
|
||||
lineNumbers: true
|
||||
},
|
||||
head: [
|
||||
['link', { rel: 'icon', href: `/logo.png` }],
|
||||
['link', { rel: 'manifest', href: '/manifest.json' }],
|
||||
['meta', { name: 'theme-color', content: '#ffffff' }],
|
||||
['meta', { name: 'application-name', content: 'NoneBot' }],
|
||||
['meta', { name: 'apple-mobile-web-app-title', content: 'NoneBot' }],
|
||||
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
|
||||
['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon.png` }],
|
||||
['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#5bbad5' }],
|
||||
['meta', { name: 'msapplication-TileImage', content: '/icons/mstile-150x150.png' }],
|
||||
['meta', { name: 'msapplication-TileColor', content: '#00aba9' }]
|
||||
],
|
||||
ga: 'UA-115509121-2',
|
||||
themeConfig: {
|
||||
repo: 'nonebot/nonebot',
|
||||
docsDir: 'docs',
|
||||
editLinks: true,
|
||||
editLinkText: '在 GitHub 上编辑此页',
|
||||
lastUpdated: '上次更新',
|
||||
activeHeaderLinks: false,
|
||||
nav: [
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: '进阶', link: '/advanced/' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
{ text: '术语表', link: '/glossary.md' },
|
||||
{ text: '更新日志', link: '/changelog.md' },
|
||||
],
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
title: '指南',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'',
|
||||
'installation',
|
||||
'getting-started',
|
||||
'whats-happened',
|
||||
'basic-configuration',
|
||||
'command',
|
||||
'nl-processor',
|
||||
'tuling',
|
||||
'notice-and-request',
|
||||
'cqhttp',
|
||||
'scheduler',
|
||||
'usage',
|
||||
'whats-next',
|
||||
]
|
||||
}
|
||||
],
|
||||
'/advanced/': [
|
||||
{
|
||||
title: '进阶',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'',
|
||||
'command-session',
|
||||
'command-argument',
|
||||
'command-group',
|
||||
'message',
|
||||
'permission',
|
||||
'decorator',
|
||||
'database',
|
||||
'server-app',
|
||||
'scheduler',
|
||||
'logging',
|
||||
'configuration',
|
||||
'larger-application',
|
||||
'deployment',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
$accentColor = #d32f2f
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/icons/mstile-150x150.png"/>
|
||||
<TileColor>#00aba9</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 520 B |
Before Width: | Height: | Size: 886 B |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.7 KiB |
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="500.000000pt" height="500.000000pt" viewBox="0 0 500.000000 500.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2389 4496 c-2 -2 -40 -6 -84 -9 -433 -33 -888 -249 -1216 -579 -302
|
||||
-304 -493 -675 -565 -1103 -24 -140 -24 -471 0 -615 71 -429 261 -803 562
|
||||
-1104 251 -250 575 -435 917 -522 184 -46 323 -62 532 -58 105 1 206 6 225 9
|
||||
19 3 60 10 90 16 123 21 237 51 342 90 552 206 986 641 1186 1190 36 100 78
|
||||
254 88 324 3 28 8 59 11 70 18 91 24 363 10 490 -15 140 -28 209 -72 375 -16
|
||||
60 -71 202 -112 290 -190 404 -509 729 -914 931 -258 129 -535 196 -840 204
|
||||
-86 3 -158 3 -160 1z m406 -51 c429 -69 790 -251 1090 -549 304 -303 492 -676
|
||||
562 -1121 22 -137 21 -425 -1 -570 -88 -578 -417 -1076 -915 -1385 -218 -135
|
||||
-488 -231 -766 -271 -107 -16 -434 -16 -520 -1 -33 6 -80 14 -105 17 -69 10
|
||||
-245 60 -346 98 -470 180 -870 552 -1076 1002 -82 179 -147 389 -163 530 -4
|
||||
28 -10 82 -15 120 -8 72 -6 379 3 412 3 10 8 38 11 63 10 71 15 99 42 200 191
|
||||
726 777 1284 1510 1435 71 15 165 30 209 34 122 11 368 4 480 -14z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 8.8 KiB |
@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "NoneBot",
|
||||
"short_name": "NoneBot",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", "Open Sans", "Helvetica Neue", "Noto Sans CJK SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", sans-serif;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
---
|
||||
home: true
|
||||
heroImage: /logo.png
|
||||
actionText: 开始使用
|
||||
actionLink: /guide/
|
||||
features:
|
||||
- title: 简洁
|
||||
details: 提供极其简洁易懂的 API,使你可以毫无压力地开始验证你的绝佳创意,只需编写最少量的代码,即可实现丰富的功能。
|
||||
- title: 易于扩展
|
||||
details: 精心设计的消息处理流程使得你可以很方便地将原型扩充为具有大量实用功能的完整聊天机器人,并持续保证扩展性。
|
||||
- title: 高性能
|
||||
details: 采用异步 I/O,利用 WebSocket 进行通信,以获得极高的性能;同时,支持使用多账号同时接入,减少业务宕机的可能。
|
||||
footer: MIT Licensed | Copyright © 2020 NoneBot Team
|
||||
---
|
@ -1,3 +0,0 @@
|
||||
# 概览
|
||||
|
||||
在 [指南](../guide/) 中,我们已经介绍了 NoneBot 的基础用法,利用这些基础用法已经可以编写很多有趣的功能。而在进阶这一部分,我们将介绍 NoneBot 的高级特性、特殊用法和最佳实践,以帮助你编写更加复杂、功能更丰富的机器人。
|
@ -1,110 +0,0 @@
|
||||
# 命令参数
|
||||
|
||||
## `session.get()` 和参数解析器
|
||||
|
||||
## 类 Shell 参数解析
|
||||
|
||||
`nonebot.argparse` 模块主要继承自 Python 内置的同名模块(`argparse`),用于解析命令的参数。在需要编写类 shell 语法的命令的时候,使用此模块可以大大提高开发效率。
|
||||
|
||||
「类 shell 语法」指的是形如 `some-command --verbose -n 3 --name=some-name argument1 argument2` 的类似于 shell 命令的语法。
|
||||
|
||||
下面给出一个使用 `argparse` 模块的实际例子:
|
||||
|
||||
```python {1-15}
|
||||
@on_command('schedule', shell_like=True)
|
||||
async def _(session: CommandSession):
|
||||
parser = ArgumentParser(session=session, usage=USAGE)
|
||||
parser.add_argument('-S', '--second')
|
||||
parser.add_argument('-M', '--minute')
|
||||
parser.add_argument('-H', '--hour')
|
||||
parser.add_argument('-d', '--day')
|
||||
parser.add_argument('-m', '--month')
|
||||
parser.add_argument('-w', '--day-of-week')
|
||||
parser.add_argument('-f', '--force', action='store_true', default=False)
|
||||
parser.add_argument('-v', '--verbose', action='store_true', default=False)
|
||||
parser.add_argument('--name', required=True)
|
||||
parser.add_argument('commands', nargs='+')
|
||||
|
||||
args = parser.parse_args(session.argv)
|
||||
|
||||
if not re.match(r'[_a-zA-Z][_a-zA-Z0-9]*', args.name):
|
||||
await session.send('计划任务名必须仅包含字母、数字、下划线,且以字母或下划线开头')
|
||||
return
|
||||
|
||||
parsed_commands: List[ScheduledCommand] = []
|
||||
invalid_commands: List[str] = []
|
||||
|
||||
if args.verbose:
|
||||
parsed_commands.append(
|
||||
ScheduledCommand(('echo',), f'开始执行计划任务 {args.name}……'))
|
||||
|
||||
for cmd_str in args.commands:
|
||||
cmd, current_arg = parse_command(session.bot, cmd_str)
|
||||
if cmd:
|
||||
tmp_session = CommandSession(session.bot, session.ctx, cmd,
|
||||
current_arg=current_arg)
|
||||
if await cmd.run(tmp_session, dry=True):
|
||||
parsed_commands.append(ScheduledCommand(cmd.name, current_arg))
|
||||
continue
|
||||
invalid_commands.append(cmd_str)
|
||||
if invalid_commands:
|
||||
invalid_commands_joined = '\n'.join(
|
||||
[f'- {c}' for c in invalid_commands])
|
||||
await session.send(f'计划任务添加失败,'
|
||||
f'因为下面的 {len(invalid_commands)} 个命令无法被运行'
|
||||
f'(命令不存在或权限不够):\n'
|
||||
f'{invalid_commands_joined}')
|
||||
return
|
||||
|
||||
trigger_args = {k: v for k, v in args.__dict__.items()
|
||||
if k in {'second', 'minute', 'hour', 'day', 'month', 'day_of_week'}}
|
||||
try:
|
||||
job = await scheduler.add_scheduled_commands(
|
||||
parsed_commands,
|
||||
job_id=scheduler.make_job_id(PLUGIN_NAME, context_id(session.ctx), args.name),
|
||||
ctx=session.ctx,
|
||||
trigger='cron', **trigger_args,
|
||||
replace_existing=args.force
|
||||
)
|
||||
except scheduler.JobIdConflictError:
|
||||
# a job with same name exists
|
||||
await session.send(f'计划任务 {args.name} 已存在,'
|
||||
f'若要覆盖请使用 --force 参数')
|
||||
return
|
||||
|
||||
await session.send(f'计划任务 {args.name} 添加成功')
|
||||
await session.send(format_job(args.name, job))
|
||||
|
||||
|
||||
USAGE = r"""
|
||||
添加计划任务
|
||||
|
||||
使用方法:
|
||||
schedule.add [OPTIONS] --name NAME COMMAND [COMMAND ...]
|
||||
|
||||
OPTIONS:
|
||||
-h, --help 显示本使用帮助
|
||||
-S SECOND, --second SECOND 定时器的秒参数
|
||||
-M MINUTE, --minute MINUTE 定时器的分参数
|
||||
-H HOUR, --hour HOUR 定时器的时参数
|
||||
-d DAY, --day DAY 定时器 的日参数
|
||||
-m MONTH, --month MONTH 定时器的月参数
|
||||
-w DAY_OF_WEEK, --day-of-week DAY_OF_WEEK 定时器的星期参数
|
||||
-f, --force 强制覆盖已有的同名计划任务
|
||||
-v, --verbose 在执行计划任务时输出更多信息
|
||||
|
||||
NAME:
|
||||
计划任务名称
|
||||
|
||||
COMMAND:
|
||||
要计划执行的命令,如果有空格或特殊字符,需使用引号括起来
|
||||
""".strip()
|
||||
```
|
||||
|
||||
上面的例子出自 [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` 属性来快速获得。
|
||||
|
||||
命令处理函数中,使用 `nonebot.argparse` 模块包装后的 `ArgumentParser` 类来解析参数,具体 `ArgumentParser` 添加参数的方法,请参考 [`argparse`](https://docs.python.org/3/library/argparse.html)。在使用 `add_argument()` 方法添加需要解析的参数后,使用 `parse_args()` 方法最终将 `argv` 解析为 `argparse.Namespace` 对象。
|
||||
|
||||
特别地,`parse_args()` 方法如果遇到需要打印帮助或报错并退出程序的情况(具体可以通过使用 Python 内置的 `argparse.ArgumentParser` 来体验),行为会更改为发送消息给当前 session 对应的上下文。注意到,`ArgumentParser` 类初始化时传入了 `session` 和 `usage` 参数,分别用于发送消息和使用帮助的内容。
|
@ -1 +0,0 @@
|
||||
# 命令组
|
@ -1,9 +0,0 @@
|
||||
# 命令会话
|
||||
|
||||
## 生命周期
|
||||
|
||||
## 状态数据
|
||||
|
||||
## 暂停、终止
|
||||
|
||||
## 切换上下文
|
@ -1 +0,0 @@
|
||||
# 配置
|
@ -1 +0,0 @@
|
||||
# 数据库
|
@ -1 +0,0 @@
|
||||
# `on_*` 装饰器
|
@ -1,80 +0,0 @@
|
||||
# 部署
|
||||
|
||||
## 基本部署
|
||||
|
||||
NoneBot 所基于的 python-aiocqhttp 库使用的 web 框架是 Quart,因此 NoneBot 的部署方法和 Quart 一致([Deploying Quart](https://pgjones.gitlab.io/quart/tutorials/deployment.html))。
|
||||
|
||||
Quart 官方建议使用 Hypercorn 来部署,这需要一个 ASGI app 对象,在 NoneBot 中,可使用 `nonebot.get_bot().asgi` 获得 ASGI app 对象。
|
||||
|
||||
具体地,通常在项目根目录下创建一个 `run.py` 文件如下:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
import nonebot
|
||||
|
||||
import config
|
||||
|
||||
nonebot.init(config)
|
||||
bot = nonebot.get_bot()
|
||||
app = bot.asgi
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.run()
|
||||
```
|
||||
|
||||
然后使用下面命令部署:
|
||||
|
||||
```python
|
||||
hypercorn run:app
|
||||
```
|
||||
|
||||
另外,NoneBot 配置文件的 `DEBUG` 项默认为 `True`,在生产环境部署时请注意修改为 `False` 以提高性能。
|
||||
|
||||
## 使用 Docker Compose 与 酷Q 同时部署
|
||||
|
||||
Docker Compose 是 Docker 官方提供的一个命令行工具,用来定义和运行由多个容器组成的应用。通过建立一个名为 `docker-compose.yml` 的文件,可以将部署过程中需要的参数记录在其中,并由单个命令完成应用的创建和启动。
|
||||
|
||||
`docker-compose.yml` 文件的示例如下:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
cqhttp:
|
||||
image: richardchien/cqhttp:latest
|
||||
volumes:
|
||||
- "./coolq:/home/user/coolq" # 用于保存COOLQ文件的目录
|
||||
environment:
|
||||
- COOLQ_ACCOUNT=123456 # 指定要登陆的QQ号,用于自动登录
|
||||
- FORCE_ENV=true
|
||||
- CQHTTP_USE_HTTP=false
|
||||
- CQHTTP_USE_WS=false
|
||||
- CQHTTP_USE_WS_REVERSE=true
|
||||
- CQHTTP_WS_REVERSE_API_URL=ws://nonebot:8080/ws/api/
|
||||
- CQHTTP_WS_REVERSE_EVENT_URL=ws://nonebot:8080/ws/event/
|
||||
depends_on:
|
||||
- nonebot
|
||||
ports: 9000:9000 # noVNC 端口,用于从浏览器控制 酷Q
|
||||
|
||||
nonebot:
|
||||
build: ./nonebot # 构建nonebot执行环境,Dockerfile见下面的例子
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- "./qbot:/root/qbot" # 项目文件所在目录
|
||||
command: python3 /root/qbot/bot.py
|
||||
```
|
||||
|
||||
部分说明见注释。NoneBot 运行环境由文件 `./nonebot/Dockerfile` 控制构建。如果项目中使用了第三方库,可以在这一步骤进行安装。`Dockerfile` 内容例如:
|
||||
|
||||
```Dockerfile
|
||||
FROM alpine
|
||||
RUN apk add --no-cache tzdata python3 py3-multidict py3-yarl && \
|
||||
pip3 install --no-cache-dir "nonebot[scheduler]"
|
||||
```
|
||||
|
||||
上述文件编辑完成后,输入命令 `docker-compose up` 即可一次性启动酷Q和 NoneBot(可通过 `docker-compose up -d` 在后台启动。更多 Docker Compose 用法见 [官方文档](https://docs.docker.com/compose/reference/overview/)。
|
@ -1,7 +0,0 @@
|
||||
# 大型应用的最佳实践
|
||||
|
||||
## 使用独立 Logger
|
||||
|
||||
## 项目结构
|
||||
|
||||
## 根据运行环境加载不同的配置
|
@ -1,23 +0,0 @@
|
||||
# 日志
|
||||
|
||||
`nonebot.log` 模块提供了一个 `logger` 对象,可用于日志。
|
||||
|
||||
使用 `nonebot.init()` 配置 NoneBot 时,`logger` 对象的日志级别会随 `DEBUG` 配置项的不同而不同,如果 `DEBUG` 为 `True`,则日志级别为 `DEBUG`,否则为 `INFO`。你也可以在 `nonebot.init()` 调用之后自行设置 `logger` 的日志级别。
|
||||
|
||||
举例:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
import nonebot
|
||||
from nonebot.log import logger
|
||||
|
||||
import config
|
||||
|
||||
|
||||
nonebot.init(config)
|
||||
# logger.setLevel(logging.WARNING)
|
||||
|
||||
logger.info('Starting')
|
||||
nonebot.run()
|
||||
```
|
@ -1,5 +0,0 @@
|
||||
# 消息处理
|
||||
|
||||
## CQ 码和消息段
|
||||
|
||||
## Expression
|
@ -1 +0,0 @@
|
||||
# 权限控制
|
@ -1,161 +0,0 @@
|
||||
# 计划任务
|
||||
|
||||
NoneBot 可选地内置了计划任务功能,在指南的 [添加计划任务](../guide/scheduler.md) 已经进行了简单的介绍。这里列出更多常见的用法。
|
||||
|
||||
## 固定的计划任务
|
||||
|
||||
可以利用固定的*触发器*(trigger)来触发某些任务。
|
||||
|
||||
### 一次性任务
|
||||
|
||||
[`date` 触发器](https://apscheduler.readthedocs.io/en/stable/modules/triggers/date.html#module-apscheduler.triggers.date) 固定时间触发,仅触发一次:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
@nonebot.scheduler.scheduled_job(
|
||||
'date',
|
||||
run_date=datetime(2021, 1, 1, 0, 0),
|
||||
# timezone=None,
|
||||
)
|
||||
async def _():
|
||||
await bot.send_group_msg(group_id=123456,
|
||||
message="2021,新年快乐!")
|
||||
```
|
||||
|
||||
### 定期任务
|
||||
|
||||
[`cron` 触发器](https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html#module-apscheduler.triggers.cron) 从 `start_date` 开始到 `end_date` 结束,根据类似 [Cron](https://wiki.archlinux.org/index.php/Cron_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)) 的规则触发任务:
|
||||
|
||||
```python
|
||||
@nonebot.scheduler.scheduled_job(
|
||||
'cron',
|
||||
# year=None,
|
||||
# month=None,
|
||||
# day=None,
|
||||
# week=None,
|
||||
day_of_week="mon,tue,wed,thu,fri",
|
||||
hour=7,
|
||||
# minute=None,
|
||||
# second=None,
|
||||
# start_date=None,
|
||||
# end_date=None,
|
||||
# timezone=None,
|
||||
)
|
||||
async def _():
|
||||
await bot.send_group_msg(group_id=123456,
|
||||
message="起床啦!")
|
||||
```
|
||||
|
||||
### 间隔任务
|
||||
|
||||
[`interval` 触发器](https://apscheduler.readthedocs.io/en/stable/modules/triggers/interval.html#module-apscheduler.triggers.interval) 从 `start_date` 开始,每间隔一段时间触发,到 `end_date` 结束:
|
||||
|
||||
```python
|
||||
@nonebot.scheduler.scheduled_job(
|
||||
'interval',
|
||||
# weeks=0,
|
||||
# days=0,
|
||||
# hours=0,
|
||||
minutes=5,
|
||||
# seconds=0,
|
||||
# start_date=time.now(),
|
||||
# end_date=None,
|
||||
)
|
||||
async def _():
|
||||
has_new_item = check_new_item()
|
||||
if has_new_item:
|
||||
await bot.send_group_msg(group_id=123456,
|
||||
message="XX有更新啦!")
|
||||
```
|
||||
|
||||
## 动态的计划任务
|
||||
|
||||
有时,我们需要机器人在运行的过程中,添加或删除计划任务,那么我们就需要 `scheduler.add_job` 来帮忙。这里,我们以*一次性任务*为例,其他类型的任务可以用相同的方法:
|
||||
|
||||
```python
|
||||
import datetime
|
||||
|
||||
from apscheduler.triggers.date import DateTrigger # 一次性触发器
|
||||
# from apscheduler.triggers.cron import CronTrigger # 定期触发器
|
||||
# from apscheduler.triggers.interval import IntervalTrigger # 间隔触发器
|
||||
from nonebot import on_command, scheduler
|
||||
|
||||
@on_command('赖床')
|
||||
async def _(session: CommandSession):
|
||||
await session.send('我会在5分钟后再喊你')
|
||||
|
||||
# 制作一个“5分钟后”触发器
|
||||
delta = datetime.timedelta(minutes=5)
|
||||
trigger = DateTrigger(
|
||||
run_date=datetime.datetime.now() + delta
|
||||
)
|
||||
|
||||
# 添加任务
|
||||
scheduler.add_job(
|
||||
func=session.send, # 要添加任务的函数,不要带参数
|
||||
trigger=trigger, # 触发器
|
||||
args=('不要再赖床啦!',), # 函数的参数列表,注意:只有一个值时,不能省略末尾的逗号
|
||||
# kwargs=None,
|
||||
misfire_grace_time=60, # 允许的误差时间,建议不要省略
|
||||
# jobstore='default', # 任务储存库,在下一小节中说明
|
||||
)
|
||||
```
|
||||
|
||||
<!-- ## 持久化存储任务
|
||||
|
||||
有时,我们动态添加的一些计划任务需要持久化储存,否则任务会在重启后丢失,这时我们就需要 `jobstore` 来帮忙。
|
||||
|
||||
APScheduler 可以将任务存储在内存中或数据库中,默认 jobstore 将所有任务储存在内存中,关闭后即丢失。这里,我们以 SQLite 为例,将任务添加到数据库中。
|
||||
|
||||
我们先创建一个数据库:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||
from nonebot import on_command, scheduler, get_bot
|
||||
|
||||
database_path = os.path.join(os.path.dirname(__file__), 'job_store.db')
|
||||
|
||||
store = SQLAlchemyJobStore(url='sqlite:///'+database_path)
|
||||
|
||||
scheduler.add_jobstore(store, alias='my_job_store')
|
||||
```
|
||||
|
||||
之后,我们在添加新任务时,可以指定任务的储存库
|
||||
|
||||
```python
|
||||
async def alarm(*args, **kwargs):
|
||||
bot = get_bot()
|
||||
await bot.send(*args, **kwargs)
|
||||
|
||||
@on_command('提醒收菜')
|
||||
async def _(session: CommandSession):
|
||||
await session.send('我会在一天后提醒你收菜')
|
||||
delta = datetime.timedelta(days=1)
|
||||
trigger = DateTrigger(
|
||||
run_date=datetime.datetime.now() + delta
|
||||
)
|
||||
|
||||
# 添加任务
|
||||
scheduler.add_job(
|
||||
func=alarm,
|
||||
trigger=trigger,
|
||||
kwargs = {
|
||||
'context': session.ctx,
|
||||
'message': '起床收菜啦!',
|
||||
},
|
||||
misfire_grace_time=60,
|
||||
jobstore='my_job_store', # 任务储存库,指定为刚才创建的储存库
|
||||
)
|
||||
```
|
||||
|
||||
**踩坑预警:**
|
||||
|
||||
由于 `apscheduler` 自带的 jobstore 无法将协程任务储存进数据库,
|
||||
所以必须将任务转化为同步任务再储存。
|
||||
并且 `apscheduler` 中的 `AsyncIOScheduler` 在执行同步任务时,会新建一个执行器(executor),
|
||||
导致这个任务里无法使用 `asyncio.get_running_loop()` 来获取事件循环,
|
||||
只能使用 `asyncio.run(...)` 来运行异步函数。 -->
|
@ -1,39 +0,0 @@
|
||||
# Server App
|
||||
|
||||
如果需要对 web 框架进行更详细的控制,可以通过 `bot.server_app` 访问到内部的 Quart 对象,之后可以像使用 Quart 的 app 对象一样添加路由、设置生命周期处理函数等。
|
||||
|
||||
:::tip 提示
|
||||
Quart 是一个与 Flask 具有相同 API 的异步 web 框架,其用法可以参考 [官方文档](https://pgjones.gitlab.io/quart/)。
|
||||
:::
|
||||
|
||||
## 自定义路由
|
||||
|
||||
这里以一个简单的管理页面为例:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bot = nonebot.get_bot() # 在此之前必须已经 init
|
||||
|
||||
@bot.server_app.route('/admin')
|
||||
async def admin():
|
||||
await bot.send_private_msg(12345678, '你的主页被访问了')
|
||||
return '欢迎来到管理页面'
|
||||
```
|
||||
|
||||
启动 NoneBot 后访问 <http://127.0.0.1:8080/admin>,你会看见管理页面的欢迎词,并收到机器人的提醒。
|
||||
|
||||
## 处理生命周期事件
|
||||
|
||||
有时可能需要在 NoneBot 启动时初始化数据库连接池,例如:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bot = nonebot.get_bot() # 在此之前必须已经 init
|
||||
|
||||
@bot.server_app.before_serving
|
||||
async def init_db():
|
||||
# 这会在 NoneBot 启动后立即运行
|
||||
pass
|
||||
```
|
3219
docs/api.md
Before Width: | Height: | Size: 30 KiB |
@ -1,233 +0,0 @@
|
||||
---
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# 更新日志
|
||||
|
||||
## v1.6.0
|
||||
|
||||
- 新增 `PluginManager` `CommandManager` `NLPManager` 管理类,用于插件的开启与关闭
|
||||
- 修改 `message_preprocessor` 在消息预处理阶段可以进行针对该消息的插件开启与关闭
|
||||
- 移动 `on_command` `on_natural_language` `on_notice` `on_request` 装饰器至 plugin 模块
|
||||
|
||||
## v1.5.0
|
||||
|
||||
- 新增 `nonebot.on_startup` 装饰器,用于注册 NoneBot 启动时回调函数
|
||||
- 新增 `nonebot.on_websocket_connect` 装饰器,用于注册 CQHTTP 反向 WebSocket 连接时回调函数(要求 CQHTTP v4.14+)
|
||||
- 弃用 `session.ctx` 属性,请使用 `session.event` 替代,该对象类型为 `aiocqhttp.Event`,可通过 property 访问内容
|
||||
- 弃用 `nonebot.tying.Context_T`,请使用 `aiocqhttp.Event` 替代
|
||||
- 修复 `@on_command` 装饰后命令处理函数 docstring 丢失问题
|
||||
|
||||
## v1.4.2
|
||||
|
||||
- 修复 `CommandSession` 的部分方法在多线程条件下出错
|
||||
- 优化日志输出多行消息的方法
|
||||
|
||||
## v1.4.1
|
||||
|
||||
- `on_command` 装饰器的 `aliases` 参数现支持字符串类型
|
||||
- 在命令注册失败时,给出警告信息
|
||||
- 修复 `helpers.render_expression` 的 bug
|
||||
|
||||
## v1.4.0
|
||||
|
||||
- 提升 aiocqhttp 依赖版本至 1.2,提升最低 Python 版本至 3.7
|
||||
- 修复 `command.group` 的 stub 文件问题
|
||||
- 修复 `helpers.render_expression` 没有转义位置参数的 bug
|
||||
- 修复 `argparse.ArgumentParser` 在没有必填参数时不能正确使用的 bug
|
||||
|
||||
## v1.3.1
|
||||
|
||||
- `on_natural_language` 装饰器的 `keywords` 参数现可直接传字符串
|
||||
|
||||
## v1.3.0
|
||||
|
||||
- 允许机器人昵称和消息主体之间不使用空格或逗号分隔,即支持 `奶茶帮我查下天气` 这种用法
|
||||
- 在处理命令之前检查机器人昵称,即在不编写自然语言处理器的情况下可以通过 `奶茶,echo 喵` 触发 `echo` 命令,而不再强制需要 at,其它命令同理
|
||||
- 新增一种命令参数过滤器——控制器,在 `nonebot.command.argfilter.controllers` 模块,用于在过滤命令参数时对命令会话进行控制,内置了 `handle_cancellation()` 控制器允许用户取消正在进行的命令
|
||||
- 新增命令参数验证失败次数的检查,可通过配置项 `MAX_VALIDATION_FAILURES` 和 `TOO_MANY_VALIDATION_FAILURES_EXPRESSION` 来配置最大失败次数和失败过多后的提示
|
||||
|
||||
## v1.2.3
|
||||
|
||||
- 修复 `nonebot.scheduler` 过早启动导致使用 Hypercorn 部署时计划任务无法运行的问题
|
||||
|
||||
同时使用计划任务功能和 Hypercorn 部署的用户请务必升级到此版本!
|
||||
|
||||
## v1.2.2
|
||||
|
||||
- 修复 `nonebot.natual_language.IntentCommand` 类 `current_arg` 参数默认为 `None` 导致的 bug
|
||||
- `nonebot.helpers.render_expression` 函数新增 `*args` 用于向 Expression 传递位置参数
|
||||
|
||||
## v1.2.1
|
||||
|
||||
- 修复 `nonebot.helpers.context_id` 的 `group` 模式无法正确产生私聊用户 ID 的 bug
|
||||
|
||||
## v1.2.0
|
||||
|
||||
#### 新增
|
||||
|
||||
- 新增 `nonebot.natual_language.IntentCommand` 类,用于替代旧的 `NLPResult`(后者已弃用),使该类的意义更明确,并新增 `current_arg` 属性,可用于设置 `IntentCommand` 被调用时的 `current_arg`
|
||||
- `nonebot` 模块新增了 `nonebot.helpers.context_id` 的快捷导入,以后可以直接通过 `nonebot.context_id` 使用
|
||||
- `CommandSession` 类新增 `state` 属性,用于替代旧的 `args`(后者已弃用),明确其用于维持 session 状态的作用,本质上和原来的 `args` 等价
|
||||
- `CommandSession` 类的 `get()` 方法新增 `arg_filters` 参数,表示正在询问用户的参数的过滤器,用于避免为每个参数编写 `args_parser`(一旦在 `get()` 时使用了 `arg_filters`,命令全局的 `args_parser` 将不会对这个参数运行),具体请参考 API 文档中的示例
|
||||
- 新增 `nonebot.command.argfilter` 模块,内置了几种常用的参数过滤器,分别在 `extractors`、`validators`、`converters` 子模块
|
||||
- 新增配置项 `DEFAULT_VALIDATION_FAILURE_EXPRESSION`,用于设置命令参数验证失败时的默认提示消息
|
||||
- `nonebot.typing` 模块新增 `State_T` 和 `Filter_T`
|
||||
|
||||
#### 变更
|
||||
|
||||
- `CommandSession` 类的 `current_arg_text` 和 `current_arg_images` 现变更为只读属性
|
||||
- 当使用 `CommandSession#get()` 方法获取参数后,若没有编写 `args_parser` 也没有传入 `arg_filters`,现在将会默认把用户输入直接当做参数,避免不断重复询问
|
||||
|
||||
#### 修复
|
||||
|
||||
- 修复交互式对话中,`ctx['to_me']` 没有置为 `True` 的 bug
|
||||
|
||||
#### 弃用
|
||||
|
||||
下述弃用内容可能会在若干版本后彻底移除,请适当做迁移。
|
||||
|
||||
- 弃用 `nonebot.natual_language.NLPResult` 类,请使用 `IntentCommand` 类替代
|
||||
- 弃用 `CommandSession` 类的 `args` 属性,请使用 `state` 属性替代
|
||||
- 弃用 `CommandSession` 类的 `get_optional()` 方法,请使用 `state.get()` 替代
|
||||
|
||||
#### 例子
|
||||
|
||||
以一个例子总结本次更新:
|
||||
|
||||
```python
|
||||
from nonebot import *
|
||||
from nonebot.command.argfilter import validators, extractors, ValidateError
|
||||
|
||||
|
||||
async def my_custom_validator(value):
|
||||
if len(value) < 8:
|
||||
raise ValidateError('长度必须至少是 8 哦')
|
||||
return value
|
||||
|
||||
|
||||
@on_command('demo')
|
||||
async def demo(session: CommandSession):
|
||||
arg1_derived = session.state.get('arg1_derived') # 从会话状态里尝试获取
|
||||
if arg1_derived is None:
|
||||
arg1: int = session.get(
|
||||
'arg1',
|
||||
prompt='请输入参数1',
|
||||
arg_filters=[
|
||||
extractors.extract_text, # 提取纯文本部分
|
||||
str.strip, # 去掉两边的空白
|
||||
validators.not_empty(),
|
||||
validators.match_regex(r'[0-9a-zA-Z]{6,20}', '必须为6~20位字符'),
|
||||
my_custom_validator, # 自定义验证器
|
||||
int, # 转换成 int
|
||||
validators.ensure_true(lambda x: x > 20000000, '必须大于2000000')
|
||||
],
|
||||
at_sender=True,
|
||||
)
|
||||
arg1_derived = arg1 + 42
|
||||
session.state['arg1_derived'] = arg1_derived # 修改会话状态
|
||||
|
||||
arg2 = session.get(
|
||||
'arg2',
|
||||
prompt='请输入参数2',
|
||||
arg_filters=[
|
||||
extractors.extract_image_urls, # 提取图片 URL 列表
|
||||
'\n'.join, # 使用换行符拼接 URL 列表
|
||||
validators.not_empty('请至少发送一张图片'),
|
||||
]
|
||||
)
|
||||
|
||||
arg3 = session.get('arg3', prompt='你的arg3是什么呢?')
|
||||
|
||||
reply = f'arg1_derived:\n{arg1_derived}\n\narg2:\n{arg2}\n\narg3:\n{arg3}'
|
||||
session.finish(reply)
|
||||
|
||||
|
||||
@demo.args_parser
|
||||
async def _(session: CommandSession):
|
||||
if session.is_first_run and session.current_arg_text.strip():
|
||||
# 第一次运行,如果有参数,则设置给 arg3
|
||||
session.state['arg3'] = session.current_arg_text.strip()
|
||||
|
||||
# 如果不需要对参数进行特殊处理,则不用再手动加入 state,NoneBot 会自动放进去
|
||||
|
||||
|
||||
@on_natural_language(keywords={'demo'})
|
||||
async def _(session: NLPSession):
|
||||
return IntentCommand(90.0, 'demo', current_arg='这是我的arg3')
|
||||
```
|
||||
|
||||
## v1.1.0
|
||||
|
||||
- 插件模块现可通过 `__plugin_name__` 和 `__plugin_usage__` 来分别指定插件名称和插件使用方法(两者均不强制,若不设置则默认为 `None`)
|
||||
- 新增 `nonebot.plugin.get_loaded_plugins()` 函数用于获取所有已加载的插件集合
|
||||
- `BaseSession.send()` 方法和 `nonebot.helpers.send()` 函数现返回 API 调用返回值(即 CQHTTP 插件的返回结果的 `data` 字段)
|
||||
- `BaseSession` 新增 `self_id` 属性,可通过 `session.self_id` 代替 `session.ctx['self_id']` 来获取当前机器人账号
|
||||
- `only_to_me` 的命令现可以通过在消息结尾 @ 机器人触发,而不必在开头
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- 更改包名为 `nonebot`,请注意修改导入语句,原先 `import none` 改为 `import nonebot`,`from none import something` 改为 `from nonebot import something`,`none.something` 改为 `nonebot.something`,如果代码量比较大,可以使用 `import nonebot as none`,以避免过多更改
|
||||
- `nonebot.command.kill_current_session()` 方法去掉了 `bot` 参数,现只需传入 `ctx`
|
||||
|
||||
## v0.5.3
|
||||
|
||||
- 修复使用多级命令时,命令查找会出现异常的情况
|
||||
- 调整 `none.load_plugins()` 等方法,返回加载成功的插件数量,并新增 `none.load_plugin()` 方法用于加载单个插件模块
|
||||
|
||||
## v0.5.2
|
||||
|
||||
- 修复自然语言处理器匹配机器人昵称时的 bug
|
||||
- 修复一些与异常处理有关的小问题
|
||||
|
||||
## v0.5.1
|
||||
|
||||
- 给所有发送消息的函数和方法(`BaseSession.send()`、`CommandSession.pause()`、`CommandSession.finish()` 等)新增了 `**kwargs`,并将此参数继续传递给 python-aiocqhttp 的 `CQHttp.send()` 方法,从而支持 `at_sender` 参数(默认 `False`),**注意,此功能需要安装 `aiocqhttp>=0.6.7`**
|
||||
- `BaseSession.send()` 方法新增 `ensure_private` 参数,类型 `bool`,默认 `False`,可用于确保发送消息到私聊(对于群消息,会私聊发送给发送人)
|
||||
|
||||
## v0.5.0
|
||||
|
||||
- 修复调用不存在的多级命令(例如 `/echo/nonexist`)时,抛出异常导致 WebSocket 连接断开的问题
|
||||
- 调整 Expression 相关接口:移除了所有 `send_expr()` 函数和方法,移除了 `CommandSession.get()` 方法的 `prompt_expr` 参数,移除了 `none.expression` 模块,原 `render()` 函数移动到 `none.helpers` 模块并改名为 `render_expression()`
|
||||
- 修改 `none.argparse.ArgumentParser` 类的构造方法和 `parse_args()` 方法:构造方法新增 `session` 参数,可传入 `CommandSession` 对象;`parse_args()` 方法可直接用于解析参数,用户输入的参数错误,会发送错误提示或使用帮助
|
||||
- `on_command` 装饰器新增 `shell_like` 参数,设为 `True`(默认 `False`)将自动以类 shell 语法分割命令参数 `current_arg`(不再需要自行编写 args parser),并将分割后的参数列表放入 `session.args['argv']`
|
||||
- `CommandSession` 类新增 `argv` 只读属性,用于获取 `session.args['argv']`,如不存在,则返回空列表
|
||||
|
||||
## v0.4.3
|
||||
|
||||
- 自然语言处理器支持响应只有机器人昵称而没有实际内容的消息,通过 `on_natural_language` 的 `allow_empty_message` 参数开启(默认关闭)
|
||||
|
||||
## v0.4.2
|
||||
|
||||
- 修复命令处理器中抛出异常导致运行超时 `SESSION_RUN_TIMEOUT` 不生效的问题
|
||||
|
||||
## v0.4.1
|
||||
|
||||
- `load_plugins()` 导入模块失败时打印错误信息,且日志级别从 warning 改为 error
|
||||
- 修复 `CommandName_T` 的问题
|
||||
- 修复特权命令在不满足 `to_me` 条件时没有被当做现有 session 的新参数的问题
|
||||
|
||||
## v0.4.0
|
||||
|
||||
- `message_preprocessor` 装饰器现要求被装饰函数接收 `bot` 和 `ctx` 两个参数
|
||||
- 调整了 Type Hint,使其更准确,并新增 `none.typing` 模块,提供部分常用类型
|
||||
- 规范部分模块的导入,现可通过 `none.Message` 访问 `aiocqhttp.Message`,通过 `none.CQHttpError` 访问 `aiocqhttp.Error`
|
||||
|
||||
## v0.3.2
|
||||
|
||||
- `none.message` 模块现已导入所有 `aiocqhttp.message` 中的内容,因此不必再从后者导入 `Message`、`escape` 等类和函数
|
||||
- 命令的运行加入了超时机制,可通过 `SESSION_RUN_TIMEOUT` 配置,类型为 `datetime.timedelta`,默认为 `None` 表示永不超时
|
||||
- `on_command` 装饰器新增 `privileged` 参数,可将命令设置为特权命令,特权命令即使在已存在其它 CommandSession 的情况下也会运行,但它不会覆盖当前 CommandSession
|
||||
- 新增 `none.command.kill_current_session()` 函数用于杀死当前已存在的 CommandSession(不会终止已经在运行的命令,但会移除 session 对象)
|
||||
|
||||
## v0.3.1
|
||||
|
||||
- 调整计划任务的启动时间,修复创建任务后无法立即获取下次运行时间的 bug
|
||||
|
||||
## v0.3.0
|
||||
|
||||
- 内置可选的计划任务功能(需要安装 APScheduler)
|
||||
|
||||
## v0.2.2
|
||||
|
||||
- 修复快速的连续消息导致报错问题 [#5](https://github.com/richardchien/nonebot/issues/5)
|
140
docs/glossary.md
@ -1,140 +0,0 @@
|
||||
---
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# 术语表
|
||||
|
||||
## 酷Q
|
||||
|
||||
[酷Q](https://cqp.cc) 是一个易语言编写的 QQ 机器人平台,其本身没有任何具体的功能,只是负责实现 QQ 协议,并以 DLL 导出函数的形式向插件提供 API 和事件上报。
|
||||
|
||||
## CQHTTP 插件
|
||||
|
||||
[CQHTTP 插件](https://cqhttp.cc/)即 CoolQ HTTP API 插件,是 酷Q 的一个第三方插件,用于将 酷Q 所提供的所有 DLL 接口转换为 HTTP 或 WebSocket 的 web 形式,从而使利用任意语言编写 酷Q 插件成为可能。
|
||||
|
||||
有时被称为 cqhttp、CQHTTP、酷Q HTTP API 等。
|
||||
|
||||
## aiocqhttp
|
||||
|
||||
[aiocqhttp](https://github.com/richardchien/python-aiocqhttp)(或称 python-aiocqhttp)是 CQHTTP 插件的一个 Python 异步 SDK,基于 asyncio,在 Quart 的基础上封装了与 CQHTTP 插件的网络交互。
|
||||
|
||||
## asyncio
|
||||
|
||||
[asyncio](https://docs.python.org/3/library/asyncio.html) 是 Python 3.4 引入的一个模块,实际上它是 Python 中整个基于事件循环(Event Loop)的异步 I/O 编程机制。
|
||||
|
||||
## Quart
|
||||
|
||||
[Quart](https://pgjones.gitlab.io/quart/) 是一个基于异步 I/O 的 web 框架,支持 HTTP 和 WebSocket,是 aiocqhttp 的基础。
|
||||
|
||||
## 异步 I/O
|
||||
|
||||
有时直接称为「异步」,是一种对 I/O 操作的处理方式,它可以在单个线程内实现非阻塞 I/O,即在 I/O 操作进行时,仍可以调度程序的其它部分。在 Python 3.4+ 中,asyncio 模块提供的异步 I/O 调度的基本单位是「协程(Coroutine)」,通过 `await` 关键字即可在进行 I/O 操作时将程序的执行权转移给其它协程,直到 I/O 结束再次被唤起。
|
||||
|
||||
## 通信方式
|
||||
|
||||
CQHTTP 插件中的一个术语,表示其与通过 web 技术编写的 酷Q 插件之间通信的手段。
|
||||
|
||||
目前 CQHTTP 插件支持 HTTP、WebSocket、反向 WebSocket 三种通信方式,见 [通信方式](https://cqhttp.cc/docs/#/CommunicationMethods),NoneBot 支持其中的 HTTP 和反向 WebSocket。
|
||||
|
||||
## 负载均衡
|
||||
|
||||
多个 QQ 连接到同一个后端,使用同一套逻辑分别服务不同的用户和群,以防止单个 QQ 无法承受过大的消息量或被腾讯封禁。
|
||||
|
||||
## 命令
|
||||
|
||||
NoneBot 主要支持的插件形式之一,主要用于处理符合特定格式的、意图明确的用户消息,例如:
|
||||
|
||||
```
|
||||
天气 南京 明天
|
||||
/echo 喵喵喵
|
||||
note.add 这是一条笔记
|
||||
```
|
||||
|
||||
上面的每行都符合一种固定的格式,消息的第一个空格左边是命令的名字(可能包含命令的起始符和分隔符),右边是命令所需的参数,可能以空格分隔,或是完全作为单个参数。
|
||||
|
||||
你可以将命令理解为操作系统中的命令行程序,NoneBot 执行命令就像在 Shell 中运行程序一样:
|
||||
|
||||
```bash
|
||||
docker run hello-world
|
||||
```
|
||||
|
||||
## 可交互命令
|
||||
|
||||
能够和用户「对话」的命令,称为可交互命令。
|
||||
|
||||
## 命令处理器
|
||||
|
||||
或称为「命令处理函数」,有时也简称为「命令」,是 NoneBot 插件中实际用于实现某个命令功能的函数。
|
||||
|
||||
通过 `nonebot.on_command` 装饰器可以将一个函数注册为命令处理器,例如:
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
|
||||
@on_command('echo')
|
||||
async def echo(session):
|
||||
pass
|
||||
```
|
||||
|
||||
## 自然语言处理器
|
||||
|
||||
或称为「自然语言处理函数」,是 NoneBot 插件中用于将用户的自然语言消息解析为命令和参数的函数。
|
||||
|
||||
通过 `nonebot.on_natural_language` 装饰器可以将一个函数注册为自然语言处理器,例如:
|
||||
|
||||
```python
|
||||
from nonebot import on_natural_language
|
||||
|
||||
@on_natural_language
|
||||
async def _(session):
|
||||
pass
|
||||
```
|
||||
|
||||
## 会话(Session)
|
||||
|
||||
是命令处理器、自然语言处理器等插件形式被调用时传入的一个包含有当前消息上下文的对象,它根据当前的插件形式的不同而不同,例如命令处理器拿到的 Session 是 `CommandSession` 类型,而自然语言处理器拿到的是 `NLPSession` 类型,不同类型的 Session 包含的属性不太一样,能进行的操作也有所区别。
|
||||
|
||||
特别地,命令的 Session 在需要和用户交互的情况下,会一直保留到下一次调用,以保证命令的多次交互能够共享数据。
|
||||
|
||||
## 表达(Expression)
|
||||
|
||||
是 NoneBot 支持的一种消息渲染的机制,可以通过随机选择或函数生成+字符串格式化的方式根据参数生成出自然的、不固定的消息回复,提升用户体验。
|
||||
|
||||
Expression 可以是一个 `str`、元素类型是 `str` 的序列(一般为 `list` 或 `tuple`)或返回类型为 `str` 的 `Callable`。
|
||||
|
||||
## CQ 码
|
||||
|
||||
是 酷Q 用来表示非文本消息的一种表示方法,形如 `[CQ:image,file=ABC.jpg]`。具体的格式规则,请参考 酷Q 文档的 [CQ 码](https://d.cqp.me/Pro/CQ%E7%A0%81) 和 CQHTTP 插件文档的 [CQ 码](https://cqhttp.cc/docs/#/CQCode)。
|
||||
|
||||
## 消息段
|
||||
|
||||
是 CQHTTP 定义的、和 CQ 码可以互相转换的一个消息表示格式,具体表示方式见 [消息格式](https://cqhttp.cc/docs/#/Message)。
|
||||
|
||||
除了纯文本消息段之外,每一个消息段都和一个 CQ 码对应,例如下面这个消息段:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "face",
|
||||
"data": {
|
||||
"id": "14"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对应的 CQ 码表示形式就是:
|
||||
|
||||
```
|
||||
[CQ:face,id=14]
|
||||
```
|
||||
|
||||
具体的,NoneBot 中使用 `MessageSegment` 类来表示消息段(继承自 aiocqhttp),例如,要创建上面这个消息段,可以使用如下代码:
|
||||
|
||||
```python
|
||||
seg = MessageSegment(type="face", data={"id": "14"})
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```python
|
||||
seg = MessageSegment.face(14)
|
||||
```
|
@ -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 通信方式
|
||||
- 支持命令、自然语言处理器等多种插件形式
|
||||
- 支持多个机器人账号负载均衡
|
||||
- 提供直观的交互式会话接口
|
||||
- 命令和自然语言处理器提供权限控制机制
|
||||
- 支持在命令会话运行过程中切换到其它命令或自然语言处理器
|
||||
- 多种方式渲染要发送的消息内容,使对话足够自然
|
Before Width: | Height: | Size: 19 KiB |
@ -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
|
||||
```
|
@ -1,8 +0,0 @@
|
||||
import nonebot
|
||||
|
||||
import config
|
||||
|
||||
if __name__ == '__main__':
|
||||
nonebot.init(config)
|
||||
nonebot.load_builtin_plugins()
|
||||
nonebot.run()
|
@ -1,7 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
@ -1 +0,0 @@
|
||||
nonebot>=1.1.0
|
@ -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}的天气是……'
|
@ -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()
|
@ -1,7 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
@ -1 +0,0 @@
|
||||
nonebot>=1.1.0
|
@ -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 '')
|
@ -1,2 +0,0 @@
|
||||
async def get_weather_of_city(city: str) -> str:
|
||||
return f'{city}的天气是……'
|
@ -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()
|
@ -1,8 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
||||
NICKNAME = {'小明', '明明'}
|
@ -1,2 +0,0 @@
|
||||
nonebot>=1.1.0
|
||||
jieba
|
@ -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
|
@ -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 '')
|
@ -1,2 +0,0 @@
|
||||
async def get_weather_of_city(city: str) -> str:
|
||||
return f'{city}的天气是……'
|
@ -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()
|
@ -1,10 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
||||
NICKNAME = {'小明', '明明'}
|
||||
|
||||
TULING_API_KEY = ''
|
@ -1,3 +0,0 @@
|
||||
nonebot>=1.1.0
|
||||
jieba
|
||||
aiohttp
|
@ -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('欢迎新朋友~')
|
@ -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
|
@ -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 '')
|
@ -1,2 +0,0 @@
|
||||
async def get_weather_of_city(city: str) -> str:
|
||||
return f'{city}的天气是……'
|
@ -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()
|
@ -1,10 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
||||
NICKNAME = {'小明', '明明'}
|
||||
|
||||
TULING_API_KEY = ''
|
@ -1,3 +0,0 @@
|
||||
nonebot>=1.1.0
|
||||
jieba
|
||||
aiohttp
|
@ -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('欢迎新朋友~')
|
@ -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
|
@ -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
|
@ -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 '')
|
@ -1,2 +0,0 @@
|
||||
async def get_weather_of_city(city: str) -> str:
|
||||
return f'{city}的天气是……'
|
@ -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()
|
@ -1,10 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
||||
NICKNAME = {'小明', '明明'}
|
||||
|
||||
TULING_API_KEY = ''
|
@ -1,4 +0,0 @@
|
||||
nonebot>=1.1.0
|
||||
jieba
|
||||
aiohttp
|
||||
pytz
|
@ -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('欢迎新朋友~')
|
@ -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
|
@ -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
|
@ -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)
|
@ -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 '')
|
@ -1,2 +0,0 @@
|
||||
async def get_weather_of_city(city: str) -> str:
|
||||
return f'{city}的天气是……'
|
@ -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()
|
@ -1,10 +0,0 @@
|
||||
from nonebot.default_config import *
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 8080
|
||||
|
||||
SUPERUSERS = {12345678}
|
||||
COMMAND_START = {'', '/', '!', '/', '!'}
|
||||
NICKNAME = {'小明', '明明'}
|
||||
|
||||
TULING_API_KEY = ''
|
@ -1,4 +0,0 @@
|
||||
nonebot>=1.1.0
|
||||
jieba
|
||||
aiohttp
|
||||
pytz
|
@ -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 就可以真正投入使用了!
|
@ -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-列表)。
|
@ -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 机器人的创意之旅!
|
@ -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,通常建议插件在大版本内尽量及时升级至最新版本。
|
||||
:::
|
@ -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 技术,并且,可以通过增加命令的参数,将自然语言的理解更加细化,以向用户提供更加顺畅的使用体验。
|
@ -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`。
|
@ -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)。
|
@ -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 来让机器人的回复显得更加自然,甚至,可以利用更高级的人工智能技术来生成对话。
|
@ -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__` 特殊变量的值
|
||||
|
||||
到这里,使用帮助命令就已经编写完成了。如果愿意,可以继续按照自己的思路实现相对应的自然语言处理器,以优化使用体验。
|
@ -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 等方式。除了反向 WebSocket,NoneBot 也支持通过 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 实例进行扩充,以实现一些非常棒的功能!
|
@ -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) 中的一些实践,比如模块划分、数据库访问等。
|
@ -28,7 +28,8 @@ class Matcher:
|
||||
self.parser = self._args_parser or self._default_parser
|
||||
|
||||
@classmethod
|
||||
def new(cls,
|
||||
def new(
|
||||
cls,
|
||||
rule: Rule = Rule(),
|
||||
scope: Scope = "ALL",
|
||||
permission: str = "ALL",
|
||||
@ -38,7 +39,8 @@ class Matcher:
|
||||
*,
|
||||
default_state: dict = {},
|
||||
default_parser: Optional[Callable[[Event, dict], None]] = None,
|
||||
args_parser: Optional[Callable[[Event, dict], None]] = None):
|
||||
args_parser: Optional[Callable[[Event, dict], None]] = None
|
||||
) -> Type["Matcher"]:
|
||||
|
||||
# class NewMatcher(cls):
|
||||
# rule: Rule = rule
|
||||
@ -51,7 +53,7 @@ class Matcher:
|
||||
# _default_state = default_state
|
||||
|
||||
NewMatcher = type(
|
||||
"Matcher", (cls,), {
|
||||
"Matcher", (Matcher,), {
|
||||
"rule": rule,
|
||||
"scope": scope,
|
||||
"permission": permission,
|
||||
@ -178,20 +180,19 @@ def on_message(rule: Rule,
|
||||
default_state=state)
|
||||
|
||||
|
||||
def on_startswith(msg,
|
||||
start: int = None,
|
||||
end: int = None,
|
||||
rule: Optional[Rule] = None,
|
||||
**kwargs) -> Type[Matcher]:
|
||||
return on_message(startswith(msg, start, end) &
|
||||
rule, **kwargs) if rule else on_message(
|
||||
startswith(msg, start, end), **kwargs)
|
||||
# def on_startswith(msg,
|
||||
# start: int = None,
|
||||
# end: int = None,
|
||||
# rule: Optional[Rule] = None,
|
||||
# **kwargs) -> Type[Matcher]:
|
||||
# return on_message(startswith(msg, start, end) &
|
||||
# rule, **kwargs) if rule else on_message(
|
||||
# startswith(msg, start, end), **kwargs)
|
||||
|
||||
|
||||
def on_regex(pattern,
|
||||
flags: Union[int, re.RegexFlag] = 0,
|
||||
rule: Optional[Rule] = None,
|
||||
**kwargs) -> Type[Matcher]:
|
||||
return on_message(regex(pattern, flags) &
|
||||
rule, **kwargs) if rule else on_message(
|
||||
regex(pattern, flags), **kwargs)
|
||||
# def on_regex(pattern,
|
||||
# flags: Union[int, re.RegexFlag] = 0,
|
||||
# rule: Optional[Rule] = None,
|
||||
# **kwargs) -> Type[Matcher]:
|
||||
# return on_message(regex(pattern, flags) &
|
||||
# rule, **kwargs) if rule else on_message(
|
||||
# regex(pattern, flags), **kwargs)
|
||||
|
4
nonebot_test/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# TODO: test running nonebot
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"docs:dev": "vuepress dev --host 127.0.0.1 -p 8888 --debug docs",
|
||||
"docs:build": "vuepress build docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vuepress": "^0.14.8"
|
||||
}
|
||||
}
|
115
poetry.lock
generated
Normal file
@ -0,0 +1,115 @@
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "In-process task scheduler with Cron-like capabilities"
|
||||
name = "apscheduler"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
version = "3.6.3"
|
||||
|
||||
[package.dependencies]
|
||||
pytz = "*"
|
||||
setuptools = ">=0.7"
|
||||
six = ">=1.4.0"
|
||||
tzlocal = ">=1.2"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["trollius"]
|
||||
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||
gevent = ["gevent"]
|
||||
mongodb = ["pymongo (>=2.8)"]
|
||||
redis = ["redis (>=3.0)"]
|
||||
rethinkdb = ["rethinkdb (>=2.4.0)"]
|
||||
sqlalchemy = ["sqlalchemy (>=0.8)"]
|
||||
testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"]
|
||||
tornado = ["tornado (>=4.3)"]
|
||||
twisted = ["twisted"]
|
||||
zookeeper = ["kazoo"]
|
||||
|
||||
[package.source]
|
||||
reference = "aliyun"
|
||||
type = "legacy"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
name = "pytz"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
version = "2020.1"
|
||||
|
||||
[package.source]
|
||||
reference = "aliyun"
|
||||
type = "legacy"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
name = "six"
|
||||
optional = true
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[package.source]
|
||||
reference = "aliyun"
|
||||
type = "legacy"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "tzinfo object for the local timezone"
|
||||
name = "tzlocal"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
version = "2.1"
|
||||
|
||||
[package.dependencies]
|
||||
pytz = "*"
|
||||
|
||||
[package.source]
|
||||
reference = "aliyun"
|
||||
type = "legacy"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A formatter for Python code."
|
||||
name = "yapf"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.30.0"
|
||||
|
||||
[package.source]
|
||||
reference = "aliyun"
|
||||
type = "legacy"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
|
||||
[extras]
|
||||
scheduler = ["apscheduler"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "1cf1db5856a5f3e46484122795ab0da8f579d1ca907ce62b67869f77196f640f"
|
||||
python-versions = "^3.7"
|
||||
|
||||
[metadata.files]
|
||||
apscheduler = [
|
||||
{file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"},
|
||||
{file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
|
||||
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
tzlocal = [
|
||||
{file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"},
|
||||
{file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"},
|
||||
]
|
||||
yapf = [
|
||||
{file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"},
|
||||
{file = "yapf-0.30.0.tar.gz", hash = "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427"},
|
||||
]
|
42
pyproject.toml
Normal file
@ -0,0 +1,42 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot"
|
||||
version = "2.0.0"
|
||||
description = "An asynchronous QQ bot framework."
|
||||
authors = ["Richard Chien <richardchienthebest@gmail.com>", "yanyongyu <yanyongyu_1@126.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://nonebot.cqp.moe/"
|
||||
repository = "https://github.com/nonebot/nonebot"
|
||||
documentation = "https://nonebot.cqp.moe/"
|
||||
keywords = ["bot", "qq", "qqbot", "cqhttp", "coolq"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: Robot Framework",
|
||||
"Framework :: Robot Framework :: Library",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3"
|
||||
]
|
||||
packages = [
|
||||
{ include = "nonebot" },
|
||||
{ include = "nonebot_test" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
apscheduler = { version = "^3.6.3", optional = true }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
yapf = "^0.30.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
scheduler = ["apscheduler"]
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "aliyun"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
default = true
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
36
setup.py
@ -1,36 +0,0 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open('README.md', 'r', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
packages = find_packages(include=('nonebot', 'nonebot.*'))
|
||||
|
||||
setup(
|
||||
name='nonebot',
|
||||
version='1.6.0',
|
||||
url='https://github.com/richardchien/nonebot',
|
||||
license='MIT License',
|
||||
author='Richard Chien',
|
||||
author_email='richardchienthebest@gmail.com',
|
||||
description='An asynchronous QQ bot framework based on CoolQ.',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
packages=packages,
|
||||
package_data={
|
||||
'': ['*.pyi'],
|
||||
},
|
||||
install_requires=['aiocqhttp>=1.2,<1.3', 'aiocache>=0.10,<1.0'],
|
||||
extras_require={
|
||||
'scheduler': ['apscheduler'],
|
||||
},
|
||||
python_requires='>=3.7',
|
||||
platforms='any',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Framework :: Robot Framework',
|
||||
'Framework :: Robot Framework :: Library',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3',
|
||||
],
|
||||
)
|