- 后端
- 即时通讯
【免费下载链接】nonebot2
跨平台 Python 异步聊天机器人框架 / Asynchronous multi-platform chatbot framework written in Python
本篇教程聚焦 NoneBot2 插件开发的起点——插件是什么、如何创建、如何加载。你将学会区分单文件插件与包插件、通过nb plugin create或手动方式创建插件,并掌握load_plugin、load_plugins、load_all_plugins、load_from_json、load_from_toml以及内置插件加载等全部加载接口的适用场景与底层实现。读完即可在真实项目中独立组织插件目录并正确接入入口文件bot.py。
插件结构:插件本质上是一个特殊的 Python 模块
在 NoneBot 中,插件即是 Python 的一个模块(module)。NoneBot 会在导入时对这些模块做一些特殊处理,使其成为一个真正的插件。插件之间应尽量减少耦合,可以进行有限制的相互调用,NoneBot 能够正确解析插件间的依赖关系。
从源码实现来看,这一"特殊处理"发生在 nonebot/plugin/manager.py 的PluginLoader.exec_module中:模块执行前,框架会先通过_new_plugin创建Plugin对象并挂载为模块的__plugin__属性,随后进入插件上下文(_current_plugin)再真正执行模块代码;若执行抛出异常,则调用_revert_plugin回滚注册,避免留下脏数据。因此,一个普通.py文件在导入后,只要其__plugin__属性存在且为Plugin实例,就会被识别为插件——这就是"插件即模块"的底层依据。
单文件插件
一个普通的.py文件即可以作为一个插件。例如创建一个foo.py文件:
📂 plugins └── 📜 foo.py这个时候模块foo已经可以被称为一个插件了,尽管它还什么都没做。
包插件
一个包含__init__.py的文件夹即是一个常规 Python 包(package)。例如创建一个foo文件夹:
📂 plugins └── 📂 foo └── 📜 __init__.py这个时候包foo同样是一个合法的插件,插件内容可以在__init__.py文件中编写。
仓库佐证:
pkgutil.iter_modules扫描插件目录时,单文件插件与包含__init__.py的包插件都会被纳入搜索;在 nonebot/utils.py 的path_to_module_name中,若路径文件名是__init__,则取父目录作为模块名,这正是包插件能被正确转换为模块名的原因。
创建插件:nb-cli 交互式创建与手动创建
创建插件可以通过nb-cli命令从完整模板创建,也可以手动新建空白文件。通过以下命令创建一个名为weather的插件:
$ nb plugin create [?] 插件名称: weather [?] 使用嵌套插件? (y/N) N [?] 请输入插件存储位置: awesome_bot/pluginsnb-cli会在awesome_bot/plugins目录下创建一个名为weather的文件夹,其中包含的文件将在后续章节中用到:
📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins │ └── 📂 weather │ ├── 📜 __init__.py │ └── 📜 config.py ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md生成出的config.py是插件专属配置的声明文件,用于配合get_plugin_config从全局配置中提取插件所需配置项(见 nonebot/plugin/init.py),后续编写插件业务逻辑时可以在__init__.py中导入使用。
基于 bootstrap 模板项目的修改
如果在之前的快速上手章节中已经使用bootstrap模板创建了项目,那么需要做出如下修改:
在项目目录中创建一个两层文件夹
awesome_bot/plugins📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md修改
pyproject.toml文件中的nonebot配置项,在plugin_dirs中添加awesome_bot/plugins[tool.nonebot] plugin_dirs = ["awesome_bot/plugins"]plugin_dirs数组正是后续load_from_toml读取[tool.nonebot]Table 时所依赖的字段,它声明了机器人要扫描的本地插件目录。
基于手动创建项目的修改
如果在之前的创建项目章节中手动创建了相关文件,那么需要做出如下修改:
在项目目录中创建一个两层文件夹
awesome_bot/plugins📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins └── 📜 bot.py修改
bot.py文件中的加载插件部分,取消注释或者添加如下代码# 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 nonebot.load_plugins("awesome_bot/plugins") # 本地插件
加载插件:时机、约束与七种加载接口
加载时机与必须遵守的约束
加载插件是在机器人入口文件中完成的,需要在框架初始化之后、运行之前进行,即位于nonebot.init()与nonebot.run()之间:
import nonebot nonebot.init() # 加载插件 nonebot.run():::danger[警告] 请勿在插件被加载前import插件模块,这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。 :::
这条警告的根源在PluginLoader.create_module与PluginLoader.exec_module的实现逻辑(nonebot/plugin/manager.py):如果模块早已被普通import放入sys.modules,则加载器会直接复用已存在的模块而跳过"创建插件对象"的步骤,导致该模块没有__plugin__属性,最终在load_plugin中抛出Module ... is not loaded as a plugin!错误。
此外还需注意:加载的插件模块名称(插件文件名或文件夹名)不能相同,且每一个插件只能被加载一次,重复加载将会导致异常。这对应 nonebot/plugin/manager.py 中_prepare_plugins的去重校验——无论是独立插件名还是目录扫描出的插件,只要插件标识符已存在,就会抛出Plugin already exists: xxx! Check your plugin name。
如果你使用nb-cli管理插件,那么可以跳过本节,nb-cli会自动处理加载;如果使用自定义的入口文件bot.py,则需要手动加载。
加载插件的方式有多种,但底层的加载逻辑是一致的(所有接口最终都汇聚到PluginManager与importlib),以下是为加载插件提供的几种方式。
load_plugin:加载单个插件
通过点分割模块名称或使用pathlib的Path对象来加载插件,通常用于加载第三方插件或者项目插件。例如:
from pathlib import Path nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件 nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件:::warning[注意] 本地插件的路径应该为相对机器人**入口文件(通常为 bot.py)**可导入的,例如在项目plugins目录下。 :::
源码实现上(nonebot/plugin/load.py),传入Path时先经path_to_module_name转换为点分模块名,再交由PluginManager加载。仓库测试 tests/test_plugin/test_load.py 同时验证了"模块名加载"与"路径加载"两条路径,并确认加载不存在的插件会返回None。
load_plugins:加载目录下所有插件
加载传入插件目录中的所有插件,通常用于加载一系列本地编写的项目插件。例如:
nonebot.load_plugins("src/plugins", "path/to/your/plugins"):::warning[注意] 插件目录应该为相对机器人**入口文件(通常为 bot.py)**可导入的,例如在项目plugins目录下。 :::
底层通过PluginManager(search_path=plugin_dir)扫描目录,注意实现细节(nonebot/plugin/manager.py):以_开头的文件或文件夹不会被导入。这一点在测试中得到印证:assert "plugin._hidden" not in sys.modules(见 tests/test_plugin/test_load.py)——仓库的tests/plugins/_hidden.py正是用来验证下划线前缀插件会被忽略。
load_all_plugins:混合加载
这种加载方式是以上两种方式的混合,加载所有传入的插件模块名称,以及所有给定目录下的插件。例如:
nonebot.load_all_plugins(["path.to.your.plugin"], ["path/to/your/plugins"])签名对应源码load_all_plugins(module_path: Iterable[str], plugin_dir: Iterable[str])(nonebot/plugin/load.py),它把独立插件与目录插件统一交给一个PluginManager处理。
load_from_json:从 JSON 文件加载
通过 JSON 文件加载插件,是load_all_plugins的 JSON 变种,通过读取 JSON 文件中的plugins字段和plugin_dirs字段进行加载。例如:
{ "plugins": ["path.to.your.plugin"], "plugin_dirs": ["path/to/your/plugins"] }nonebot.load_from_json("plugin_config.json", encoding="utf-8")源码会校验 JSON 顶层必须是 dict,且plugins、plugin_dirs均为列表(nonebot/plugin/load.py),否则抛出TypeError或AssertionError。仓库中的测试样例 tests/plugins.json 与校验测试见 tests/test_plugin/test_load.py(非法 JSON 会触发TypeError)。
:::tip[提示] 如果 JSON 配置文件中的字段无法满足你的需求,可以使用load_all_plugins方法自行读取配置来加载插件。 :::
load_from_toml:从 TOML 文件加载
通过 TOML 文件加载插件,是load_all_plugins的 TOML 变种,通过读取 TOML 文件中的[tool.nonebot]Table 中的plugin_dirsArray 与[tool.nonebot.plugins]Table 中的多个 Array 进行加载。例如:
[tool.nonebot] plugin_dirs = ["path/to/your/plugins"] [tool.nonebot.plugins] "@local" = ["path.to.your.plugin"] # 本地插件等非插件商店来源的插件 "nonebot-plugin-someplugin" = ["nonebot_plugin_someplugin"] # 插件商店来源的插件nonebot.load_from_toml("plugin_config.toml", encoding="utf-8")源码解析逻辑(nonebot/plugin/load.py)值得留意:
- 若 TOML 中没有
[tool.nonebot]Table,直接抛出ValueError: Cannot find '[tool.nonebot]' in given toml file!(测试见 tests/test_plugin/test_load.py); [tool.nonebot]下既支持新版plugins作为 Table([tool.nonebot.plugins]下的多个 Array,按来源分组),也兼容旧版plugins作为 Array 的格式——旧格式会输出警告Legacy project format found! Upgrade withnb upgrade-format.;- 仓库测试样例 tests/plugins.toml(新版分组格式)与 tests/plugins.legacy.toml(旧版数组格式)对两者均有覆盖。
:::tip[提示] 如果 TOML 配置文件中的字段无法满足你的需求,可以使用load_all_plugins方法自行读取配置来加载插件。 :::
load_builtin_plugin:加载单个内置插件
加载一个内置插件,传入的插件名必须为 NoneBot 内置插件。该方法是load_plugin的封装。例如:
nonebot.load_builtin_plugin("echo")源码实现(nonebot/plugin/load.py)等价于load_plugin(f"nonebot.plugins.{name}"),即把内置插件当作nonebot.plugins包下的模块加载。仓库内置插件目录 nonebot/plugins/echo.py 定义了/echo命令——它通过on_command("echo", to_me())注册响应器并回复消息内容。
load_builtin_plugins:加载多个内置插件
加载传入插件列表中的所有内置插件。例如:
nonebot.load_builtin_plugins("echo", "single_session")源码实现(nonebot/plugin/load.py)等价于load_all_plugins([f"nonebot.plugins.{p}" for p in plugins], [])。第二个内置插件 nonebot/plugins/single_session.py 是"唯一会话"插件——加载后自动生效,通过@event_preprocessor限制同一会话内同时只能运行一个响应器。
其他加载方式
以上是面向入口文件的全部加载接口。除此之外,插件加载机制还覆盖两个进阶场景,可参考官方文档深入了解:
- 跨插件访问:通过
require(name)声明依赖并获取其他插件模块(实现见 nonebot/plugin/load.py),详见跨插件访问; - 嵌套插件:子插件以
父插件标识符:子插件名的形式注册(测试nested:nested_subplugin见 tests/test_plugin/test_load.py),详见嵌套插件。
加载流程小结与自检清单
所有加载接口最终都经由PluginManager(nonebot/plugin/manager.py)完成:先_prepare_plugins搜索并缓存可用插件(含重名校验),再由load_plugin触发importlib导入,导入过程被注册在sys.meta_path首位的PluginFinder拦截,改用PluginLoader执行模块并注入__plugin__属性,最终插件进入全局注册表_plugins(nonebot/plugin/init.py),可通过get_loaded_plugins()/get_plugin()查询。
完成本教程后,建议按以下清单自查:
- 插件目录(如
awesome_bot/plugins)已创建,且相对入口文件可导入; - 插件是普通
.py文件或含__init__.py的包,且插件名不与已加载插件重名; - 入口文件中
nonebot.init()之后、nonebot.run()之前调用加载接口; - 未在任何插件加载前手动
import插件模块; - 目录内以
_开头的文件/文件夹不会被当作插件加载(这是特性,不是 bug)。
- 后端
- 即时通讯
【免费下载链接】nonebot2
跨平台 Python 异步聊天机器人框架 / Asynchronous multi-platform chatbot framework written in Python
相关推荐
NoneBot2 嵌套插件:编写与加载子插件完全指南
NoneBot2 嵌套插件:编写与加载子插件完全指南 嵌套插件是 NoneBot2 提供的一种插件组织机制:一个插件可以包含其他插件,父插件通过调用框架的加载插
后端即时通讯NoneBot2插件开发指南:从创建到加载全流程解析
NoneBot2插件开发指南:从创建到加载全流程解析 前言 NoneBot2作为一款优秀的Python异步机器人框架,其插件系统是功能扩展的核心。本文将全面讲解
后端即时通讯NoneBot2插件开发指南:从创建到加载全流程解析
NoneBot2插件开发指南:从创建到加载全流程解析 前言 NoneBot2作为一款优秀的Python异步机器人框架,其插件系统是整个框架的核心功能之一。本文将
后端即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考