6. 给插件加配置
导读 这一章解决插件开发中几乎无法回避的问题:如何让插件的行为变得可配置。从一个最小开关配置出发,本章将完整介绍
BaseConfig、SectionBase、Field、config_section这套配置体系,并系统梳理其内置方法与属性。读完之后,你不只是会套用一个配置模板,而是真正理解这套设计背后的原因。
到了这里,插件终于要开始摆脱“写死行为”了。
前几章那个 echo_demo 虽然已经能跑,但它其实还有一个很明显的特点:它的行为几乎全写死在代码里。
这在入门阶段没有问题,甚至是故意这样安排的。因为一开始我们只想先打通“插件能被加载、组件能被注册、命令能被执行”这条主线。
但只要插件稍微往真实使用走一步,配置几乎就会立刻出现。你会开始想要:
- 临时关闭某个插件。
- 调整一些行为参数。
- 让用户不用改代码,只改配置就能改变插件表现。
这就是这一章要解决的问题。
我们要给
echo_demo加上第一个真正的插件配置。
这一章会先从一个最小开关配置开始,然后把配置系统本身是怎么组织的讲清楚。读完之后,你不只是会“抄一个配置类”,而是会开始明白:为什么 Neo-MoFox 的配置要长成这样。
6.1 什么时候应该给插件加配置
不是所有插件一开始都必须有配置,但只要你的插件开始出现下面这些需求,配置通常就该来了:
- 某些行为需要启用或关闭。
- 某些参数希望用户可调整。
- 你不希望用户每次改行为都去改 Python 代码。
哪怕只是一个最简单的 enabled 开关,也已经足以让插件从“代码样例”开始往“可使用功能”迈一步。
所以这一章我们不做复杂配置,先做一个最小但真实有意义的版本:
- 给
echo_demo增加一个enabled开关。 - 当配置关闭时,插件不注册任何组件。
这个例子虽然很小,但它已经把配置系统最关键的一条链路全走通了:
定义配置类
-> 在插件类里声明 configs
-> 启动时自动加载配置
-> 配置实例注入插件
-> 插件根据配置决定行为6.2 看最终目标
这一章做完之后,echo_demo 会变成这样:
- 插件目录里多一个
config.py。 - 插件类通过
configs声明配置类。 - 配置文件默认放在
config/plugins/echo_demo/config.toml。 - 如果这个文件还不存在,系统会在首次加载时自动生成默认配置。
- 如果配置里的
enabled = false,插件就不会注册命令组件。
6.3 新增 config.py
在 echo_demo 目录里新增一个 config.py:
from __future__ import annotations
from typing import ClassVar
from src.app.plugin_system.base import (
BaseConfig,
Field,
SectionBase,
config_section,
)
class EchoDemoConfig(BaseConfig):
"""EchoDemo 插件配置。"""
config_name: ClassVar[str] = "config"
config_description: ClassVar[str] = "EchoDemo 插件配置"
@config_section("plugin")
class PluginSection(SectionBase):
"""插件基础配置。"""
enabled: bool = Field(
default=True,
description="是否启用 echo_demo 插件",
)
plugin: PluginSection = Field(default_factory=PluginSection)如果你第一次看这段代码,可能会觉得“明明只是一个开关,为什么要写成这样”。
这个疑问很正常,而且值得认真回答。因为如果只看结果,一个布尔值确实很简单;但配置系统设计得稍微完整一些,目的从来不只是装下今天这一个布尔值,而是为明天的更多配置项留出空间。
所以接下来我们把这几个核心角色拆开讲。
6.4 BaseConfig 是什么
BaseConfig 是插件配置的基类。你可以先把它理解成一句话:
只要某个类继承自
BaseConfig,系统就会把它当成插件配置来理解。
它帮你处理了几件非常重要的事:
- 提供统一的配置模型基类。
- 约定默认配置路径。
- 支持默认配置文件生成。
- 支持按插件名加载配置。
也就是说,插件作者写配置类时,不需要从零开始解决“配置文件放哪”“找不到配置怎么办”“默认值怎么写回 TOML”这些问题。这些基础工作,BaseConfig 已经替你铺好了。
这也是为什么我们这章可以把重点放在“怎么描述配置”,而不是先去造一套自己的配置读写框架。
6.5 config_name 和 config_description 在干什么
这两个字段虽然简单,但很值得一开始就理解清楚。
config_name: ClassVar[str] = "config"
config_description: ClassVar[str] = "EchoDemo 插件配置"config_name
它决定这个配置文件的基本文件名。
在当前实现里,如果插件名是 echo_demo,config_name 是 config,那么默认配置路径就会是:
config/plugins/echo_demo/config.toml所以 config_name 并不是给人看的文案,而是会直接影响配置文件路径。
config_description
它更偏向配置本身的描述信息,主要是给系统和未来的配置展示层预留语义。
在入门阶段,你可以先把它理解成“这个配置类是干什么的”。
6.6 SectionBase 和 config_section 为什么要一起出现
现在来看最关键的一层:
@config_section("plugin")
class PluginSection(SectionBase):
enabled: bool = Field(default=True, description="是否启用 echo_demo 插件")这里有两个点需要一起看。
SectionBase
它表示“这是一段配置节”。
你可以把它想成 TOML 里的一块小节,比如:
[plugin]
enabled = true也就是说,SectionBase 不是整个配置文件,它只是配置文件里的某一块结构。
@config_section("plugin")
这个装饰器则是在告诉系统:
这一段配置节在配置文件里对应的名字叫
plugin。
所以这两者放在一起,表达的其实是:
我现在定义了一个名为
plugin的配置节,它里面会放插件级的基础配置。
这也是为什么后面我们还会再写:
plugin: PluginSection = Field(default_factory=PluginSection)它是在把这个配置节真正挂回整个配置模型里。
6.7 Field 到底是干什么的
在配置系统里,Field 主要有两类作用。
第一类作用:给字段默认值
比如:
enabled: bool = Field(default=True, description="是否启用 echo_demo 插件")这里最直接的含义就是:如果用户没有显式设置这个值,那它默认就是 True。
第二类作用:给字段附加说明信息
比如这里的 description,它并不只是为了好看。它本质上是在给这个配置项补充语义说明。
在更完整的插件里,你还会看到更多元数据,比如:
- label
- hint
- order
- tag
- input_type
这些信息在入门阶段暂时不用一次掌握完,但你至少要先知道:配置字段不是只有“类型”和“默认值”,它还可以携带一些帮助系统理解或展示这个字段的信息。
6.8 为什么最后还要写 plugin: PluginSection
这句:
plugin: PluginSection = Field(default_factory=PluginSection)非常容易被初学者忽略,因为它看起来像重复了一遍。
但其实它很重要。前面的内部类只是定义了一个配置节的结构,而这句是在告诉整个配置模型:
我这个配置文件里,真的有一个
plugin字段,它的类型就是刚才定义的PluginSection。
如果少了这句,你只是定义了一个内部结构,却没有把它真正纳入最终配置模型。
所以你可以把整个配置类理解成两层:
- 上层:配置文件整体长什么样。
- 下层:每个配置节内部长什么样。
6.9 现在修改 plugin.py
有了配置类之后,插件本身还要明确声明“我使用这个配置”。
把 plugin.py 改成这样:
from __future__ import annotations
from src.app.plugin_system.base import (
BaseCommand,
BasePlugin,
cmd_route,
register_plugin,
)
from .config import EchoDemoConfig
class EchoCommand(BaseCommand):
"""最小回显命令。"""
command_name = "echo"
command_description = "一个用于演示插件系统的最小回显命令"
command_prefix = "/"
@cmd_route("ping")
async def handle_ping(self) -> tuple[bool, str]:
"""检查命令是否已经正常工作。"""
return True, "pong"
@cmd_route("say")
async def handle_say(self, text: str) -> tuple[bool, str]:
"""回显一段文本。"""
return True, f"echo: {text}"
@register_plugin
class EchoDemoPlugin(BasePlugin):
"""最小回显插件。"""
plugin_name = "echo_demo"
plugin_description = "一个用于演示插件加载与命令执行的最小插件"
plugin_version = "1.0.0"
configs: list[type] = [EchoDemoConfig]
def get_components(self) -> list[type]:
"""返回当前插件包含的组件。"""
if isinstance(self.config, EchoDemoConfig) and not self.config.plugin.enabled:
return []
return [EchoCommand]这一改动看起来很小,但意义非常大。
第一处变化:声明 configs
configs: list[type] = [EchoDemoConfig]这句是在告诉插件管理器:
这个插件有配置类,而且在实例化插件之前,应该先把它加载好。
这也是当前实现里最关键的约定之一:配置不是靠 get_components() 返回的,而是靠插件类上的 configs 声明。
这一点一定要记住,因为它和其他组件的接入方式不一样。
第二处变化:根据配置决定是否注册组件
if isinstance(self.config, EchoDemoConfig) and not self.config.plugin.enabled:
return []这一段是在说:如果插件配置已经加载成功,而且 enabled 被关掉了,那这个插件就不注册任何组件。
你可以把它理解成一种很直接的插件级开关。
它的好处是很清楚:
- 配置打开,插件正常工作。
- 配置关闭,插件仍然存在于项目里,但不会对系统暴露功能。
对于很多社区插件来说,这都是一个非常实用的默认能力。
6.10 配置文件会放在哪里
在当前实现下,这个配置的默认位置是:
config/plugins/echo_demo/config.toml这个路径不是我们手写出来的,而是由插件名和 config_name 一起决定的。
这点很重要,因为它意味着不同插件的配置天然就会各自收在自己的目录里,不容易打架,也更方便用户查找。
6.11 第一次启动时会发生什么
当你现在再次启动项目:
uv run main.py系统大致会做下面这些事:
- 发现
echo_demo插件。 - 读取它的 manifest。
- 导入
plugin.py。 - 找到注册过的
EchoDemoPlugin。 - 看到它声明了
configs = [EchoDemoConfig]。 - 尝试加载
config/plugins/echo_demo/config.toml。 - 如果文件不存在,就生成默认配置文件。
- 把配置实例注入插件对象。
- 插件在
get_components()里根据配置决定是否返回组件。
这里有一个对新手很友好的点:配置文件不存在时,系统会自动生成默认配置。
不过这里不是所有问题都靠自动生成解决,只是在入门阶段,这个机制确实能让你少走很多弯路。至少你不用第一步就手写完整配置文件,才能验证插件机制本身是否通了。
6.12 一个最小配置文件大致会长什么样
首次生成之后,你最终看到的配置文件核心内容,大致会是这个样子:
[plugin]
enabled = true实际生成结果可能会带上更多注释或渲染细节,但对入门理解来说,这个核心结构已经够用了。
如果你把它改成:
[plugin]
enabled = false那么下一次加载插件时,echo_demo 就不会返回它的命令组件。
6.14 BaseConfig 基类速查
下面系统性地过一遍 BaseConfig 的定义,包括所有类属性和方法,供参考:
类属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
config_name | ClassVar[str] | "config" | 配置文件基本文件名(不含 .toml 扩展名),影响默认路径生成 |
config_description | ClassVar[str] | "" | 配置的可读描述,用于展示层和文档 |
_plugin_ | ClassVar[str] | 由插件管理器注入 | 所属插件名称,用于生成路径和签名,开发者无需手动填写 |
主要方法
| 方法 | 签名 | 说明 |
|---|---|---|
get_default_path() | cls → Path | None | 根据插件名和 config_name 计算默认配置文件路径,格式为 config/plugins/{plugin_name}/{config_name}.toml |
generate_default() | cls, path=None → None | 按配置模型的默认值生成 TOML 文件;path 为 None 时使用 get_default_path() |
get_signature() | cls → str | None | 返回组件签名,格式为 plugin_name:config:config_name |
BaseConfig继承自 kernel 层的ConfigBase(Pydantic 模型),因此也支持.model_validate()、.model_dump()等标准 Pydantic 方法。
关联辅助类型
SectionBase
所有配置节的基类。定义配置节结构的内部类须继承自 SectionBase。
@config_section(name: str)
将一个 SectionBase 子类标记为特定的配置节,参数 name 即 TOML 文件中对应的小节名。
Field(...)
为配置字段声明默认值和元数据:
from src.app.plugin_system.base import Field
enabled: bool = Field(default=True, description="是否启用")常用参数:default、description、label、hint、order、tag、input_type。
6.15 这一章最值得记住的几个点
到了这里,配置系统里最关键的几条关系应该已经比较清楚了:
BaseConfig表示这是一个插件配置类。config_name决定默认配置文件名。SectionBase+config_section用来定义配置节。Field用来声明默认值和字段元数据。- 插件通过
configs声明要加载哪个配置类。 - 插件实例会通过
self.config拿到配置对象。
如果你能把这几条关系记住,后面再看更复杂的插件配置,就不会觉得它们只是在“写很多很长的字段定义”了。你会知道它们其实是在用同一套模式,逐步把一个插件的可调行为描述出来。
6.16 为什么这一章故意先从 enabled 开始
你可能已经注意到了,这一章讲了不少配置系统本身的结构,但示例配置却只做了一个最简单的 enabled 开关。
这是故意的。
因为配置系统本身已经有一点抽象味道了。如果一开始再塞进一堆业务参数,读者很容易分不清:
- 哪部分是在学配置机制。
- 哪部分是在学某个插件自己的业务。
所以我们先用最简单的开关,把“配置如何定义、如何生成、如何注入、如何影响插件行为”这条链跑通。等后面再加更真实的参数时,你就会轻松很多。
6.17 这一章压缩成一句话
如果你想把这一章压缩成一句最值得带走的话,那就是:
配置类负责描述插件的可调行为,插件类通过
configs声明它,系统在加载插件前把配置准备好,再把结果注入给插件使用。
到这里为止,你的插件已经不再只是“能跑的代码”,而开始具备一点点“可使用、可调整”的味道了。
下一步,我们就要往前再走一步:从单组件,开始走向多组件。

