- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
8.5 KiB
8.5 KiB
插件结构标准(Plugin Structure Standard)
HeXi 所有自定义插件的统一骨架:包命名、目录布局、入口/配置/工具/服务/数据/Web 职责划分。 配合《插件配置文件标准》(plugin-config-standard.md) 使用:结构标准管「怎么组织代码」,配置标准管「配置如何统一并暴露给 Web」。
0. 目标
- 让新插件开箱即用:照骨架抄一遍即可,天然接入统一配置 + Web 管理台。
- 让旧插件逐步收敛到同一结构,避免「入口一大坨、配置散落、数据读写各自为政」。
- 让 Web 管理台能统一读/改所有插件的配置(不是只针对 env)。
1. 包命名与位置
- 位置:
hexi/plugins/nonebot_plugin_<name>/(bot.py用nonebot.load_plugins("hexi")加载)。 - 包名 = NoneBot 模块名 =
hexi.plugins.nonebot_plugin_<name>。 plugin_id约定:用于配置 schema / 插件控制 / Web 挂载的值,一律等于模块名(__name__)。
2. 架构分层:MTSS(Model–Trigger–Service–View)
本插件结构可用一种「事件驱动的 MVC」来理解:Controller 被换成 Trigger(触发器),并多出一个双 View(聊天 + 管理端)。我们称之为 MTSS。
2.1 四层职责
| 层 | 目录 | 职责 |
|---|---|---|
| Trigger | handlers/* |
on_command/on_message/on_notice、定时任务、Web 按钮 → 统一入口适配器,接触发后调 service、出 View |
| Service | services/* |
业务编排:一个业务动作一个方法;不关心是哪个触发器进来的,可被多触发器/定时/Web 复用 |
| Model | models.py + repository.py/storage/* + data/* |
数据(ORM/JSON/SQLite/S3)与数据访问;唯一读写入口 |
| View | 聊天出图/UniMessage;Web 端 web/ + schema 表单 |
双 View:a) 发给聊天用户(文本/图片/转发);b) 管理端(Web REST + 前端) |
2.2 依赖方向(单向、向下)
Trigger(handlers) → Service(services) → Model(repository/storage)
↘ 外部 API / LLM
Trigger/View → 依赖 nonebot / OneBot / 平台
Service/Model → 尽量不 import nonebot(可单测、未来可换框架)
↑ 配置标准 config.py 是横切模块,各层都能读
2.3 关键规则
- Trigger 只做:接触发 → 鉴权/校验 → 调 service → 出 View;不要放大逻辑。
- View 有两个出口:
chat(发给用户) 与web(管理端),可共用同一个 Service 的结果(类似 MVVM 的「一个 ViewModel 两个 View」)。 - service/repository 不 import nonebot,便于单测与切换框架;只依赖 Model 与配置标准。
- 触发器来源不限:群消息 / 私聊 / at / 定时 / Web 按钮。想加「Web 手动触发」,只需在 Trigger 层加一个入口复用同一个 service。
2.4 示例(video_analysis)
Trigger → handlers/entry.py 的 auto/active matcher(card/文本链接)
Service → dispatch_url 平台分派(可下沉到 services/)
Model → list_proc.py(list.json)、storage/s3.py、models.py(异常)
View → chat: UniMessage 文本/图片; web: config schema + 未来的 ObjectSetEditor
Config → config.py 统一注册(cleanup/S3/group_config)
3. 目录骨架模板
nonebot_plugin_xxx/
├── __init__.py # 入口:元数据 + 命令注册 + 配置注册 + 启动钩子
├── config.py # 配置标准:Config(BaseModel) / register_object_set / register_config_items
├── models.py # ORM / dataclass 模型(可选)
├── repository.py # 数据访问层:唯一读写入口(可选)
├── handlers/ # 命令/消息处理(按域拆分)
│ ├── __init__.py
│ └── chat.py management.py notices.py ...
├── services/ # 业务编排层(被多个 handler/定时任务复用)
│ └── __init__.py
├── utils/ # 纯工具:无副作用、尽量无 nonebot 依赖、可单测
│ └── __init__.py
├── data/ # 运行时数据(或统一放 hexi/data/)
├── res/ # 静态资源:图片/字体/模板
├── web/ # 可选:插件自带 Web 前端(build 产物挂载)
├── README.md # 可选
└── CLAUDE.md # 可选:插件内开发文档(在此插件内开发时优先读)
4. __init__.py 标准职责(固定顺序)
__plugin_meta__ = PluginMetadata(name=..., description=..., usage=..., type="application")。- 声明依赖
require("nonebot_plugin_alconna")等。 - 显式导入子模块(
from . import handlers, services, utils),注册 matcher/handler。 - 配置注册(在 config 就绪后):
register_model_config(pydantic Config)或register_config_items/register_object_set(来源无关)。 - 可选
register_web_plugin("id", "名称", "icon", lambda: build_app(), module_name=__name__)。 - 启动钩子
@driver.on_startup:初始化 DB / 加载 data 文件 / 注册定时任务。 - 业务命令尽量放
handlers/,不要把大逻辑堆在__init__.py。
5. config.py 标准
4.1 环境驱动型(读 .env / NoneBot config)
from nonebot import get_plugin_config
from pydantic import BaseModel
class Config(BaseModel):
enable: bool = True
api_key: str = ""
config = get_plugin_config(Config)
然后 register_model_config(__name__, config, fields=[...], labels={...}) 接入 Web。
4.2 来源无关型(模块常量 / 配置文件 / DB)
from hexi.config_standard import register_config_items, register_object_set
register_object_set(__name__, "groups", [ {item_schema...} ], key_field="group_id",
getter=get_groups, setter=set_groups, nosave=True)
详见《plugin-config-standard.md》§5/§13/§14。
4.3 约定
- 敏感字段
type="password"且secret=True; - 默认值写进 schema,供 Web 回填;
nosave=True表示权威源在插件自身(文件/DB),Web 修改只 apply 到来源,不写 plugin_config.json。
6. 数据层标准
- 读写统一走
repository.py(或 services 内封装的仓储方法),避免 SQL/文件读写散落各处。 - ORM:用
nonebot_plugin_orm的get_session();模型在models.py。 - 文件:原子写(tmp +
os.replace),避免并发/崩溃写坏 JSON。 - SQLite:开 WAL、用事务;不要在循环里逐条写。
- 路径:统一用插件
get_data_dir()/get_plugin_cache_dir()或hexi的路径工具,不硬编码绝对路径。
7. 工具/服务标准
utils/:纯函数、无状态、尽量不 import nonebot/driver,便于单测。services/:业务编排,可依赖 repository / 外部 API;把「一个业务动作」收敛到一个方法。- 禁止
from x import *、except: pass、print();用logger。 - async handler 内避免同步阻塞(
requests/time.sleep/ 同步爬虫);需要就asyncio.to_thread。 - 命令触发:需要 @ 用
rule=to_me();全局命令注意与其它插件冲突;高开销命令加冷却/限频(hexi_core的cooldown/rate_limit)。 - 外部 API:统一超时 + 重试 + 失败降级/用户提示。
8. Web 接入标准
- 需要独立 Web 页:
register_web_plugin(id, name, icon, lambda: build_app(), module_name=__name__),hub 启动自动挂载/api/<id>。 - 需要 Web 配置:
register_plugin_config/register_model_config/register_config_items/register_object_set(schema 驱动表单)。 - 鉴权统一用
hexi.web_auth.require_admin(OAuth2 + SQLite),不要自造一套。 - 敏感字段
secret=True,前端掩码;写回允许明文。
9. 插件迁移检查清单
- 包名改为
nonebot_plugin_*,位置在hexi/plugins/。 - 有
__plugin_meta__,type="application"。 - 配置已接入统一标准(Web 能读能改,运行期/重启生效)。
- 命令在
handlers/,业务在services/,工具在utils/。 - 数据访问集中(repository / services),原子写 / 事务。
- 无
*导入、裸except、print;无同步阻塞混入 async。 - 运行数据写入统一数据目录,不写入插件源码目录;配置读取点支持标准热更新语义。
- 外部请求有超时/重试/失败提示;命令有权限/限流。
- 需要 Web 的已
register_web_plugin;鉴权走统一web_auth。