mirror of
				https://github.com/nonebot/nonebot2.git
				synced 2025-10-31 06:56:39 +00:00 
			
		
		
		
	clean up files
This commit is contained in:
		| @@ -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 | ||||
| ``` | ||||
		Reference in New Issue
	
	Block a user