2026-09-01 13:13:40 +08:00
|
|
|
|
# 插件结构标准(Plugin Structure Standard)
|
|
|
|
|
|
|
|
|
|
|
|
> HeXi 所有自定义插件的统一骨架:包命名、目录布局、入口/配置/工具/服务/数据/Web 职责划分。
|
|
|
|
|
|
> 配合《插件配置文件标准》(plugin-config-standard.md) 使用:结构标准管「怎么组织代码」,配置标准管「配置如何统一并暴露给 Web」。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 0. 目标
|
|
|
|
|
|
|
|
|
|
|
|
- 让新插件**开箱即用**:照骨架抄一遍即可,天然接入统一配置 + Web 管理台。
|
|
|
|
|
|
- 让旧插件**逐步收敛**到同一结构,避免「入口一大坨、配置散落、数据读写各自为政」。
|
|
|
|
|
|
- 让 **Web 管理台能统一读/改所有插件的配置**(不是只针对 env)。
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 包命名与位置
|
|
|
|
|
|
|
2026-09-03 15:40:43 +08:00
|
|
|
|
- 位置:`hexi/plugins/nonebot_plugin_<name>/`(`../../bot.py` 用 `nonebot.load_plugins("hexi")` 加载)。
|
2026-09-01 13:13:40 +08:00
|
|
|
|
- 包名 = 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 依赖方向(单向、向下)
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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. 目录骨架模板
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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` 标准职责(固定顺序)
|
|
|
|
|
|
|
|
|
|
|
|
1. `__plugin_meta__ = PluginMetadata(name=..., description=..., usage=..., type="application")`。
|
2026-09-08 14:25:32 +08:00
|
|
|
|
> **type 取值(NoneBot 发布规范)**:`application`(向机器人用户提供功能,支持热插拔/热重载)或 `library`(为其他插件提供能力,不可热拔插)。新插件默认 `application`。本仓库的 `hexi/core`、`hexi/web_hub` 已从插件目录移出,作为机器人核心模块(**非插件**),直接不参与热拔插。
|
2026-09-01 13:13:40 +08:00
|
|
|
|
2. 声明依赖 `require("nonebot_plugin_alconna")` 等。
|
|
|
|
|
|
3. 显式导入子模块(`from . import handlers, services, utils`),注册 matcher/handler。
|
|
|
|
|
|
4. **配置注册**(在 config 就绪后):`register_model_config`(pydantic Config)或 `register_config_items` / `register_object_set`(来源无关)。
|
|
|
|
|
|
5. 可选 `register_web_plugin("id", "名称", "icon", lambda: build_app(), module_name=__name__)`。
|
|
|
|
|
|
6. 启动钩子 `@driver.on_startup`:初始化 DB / 加载 data 文件 / 注册定时任务。
|
|
|
|
|
|
7. 业务命令尽量放 `handlers/`,不要把大逻辑堆在 `__init__.py`。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. config.py 标准
|
|
|
|
|
|
|
2026-09-03 15:40:43 +08:00
|
|
|
|
### 4.1 环境驱动型(读 `../../.env` / NoneBot config)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
```python
|
|
|
|
|
|
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)
|
|
|
|
|
|
```python
|
2026-09-08 14:25:32 +08:00
|
|
|
|
from hexi.web_hub.config_standard import register_config_items, register_object_set
|
2026-09-01 13:13:40 +08:00
|
|
|
|
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`。
|
2026-09-08 14:25:32 +08:00
|
|
|
|
- 命令触发:需要 @ 用 `rule=to_me()`;全局命令注意与其它插件冲突;高开销命令加冷却/限频(`hexi/core` 的 `cooldown/rate_limit`)。
|
2026-09-01 13:13:40 +08:00
|
|
|
|
- 外部 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 驱动表单)。
|
2026-09-08 14:25:32 +08:00
|
|
|
|
- 鉴权统一用 `hexi.web_hub.web_auth.require_admin`(OAuth2 + SQLite),不要自造一套。
|
2026-09-01 13:13:40 +08:00
|
|
|
|
- 敏感字段 `secret=True`,前端掩码;写回允许明文。
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 插件迁移检查清单
|
|
|
|
|
|
|
2026-09-03 15:40:43 +08:00
|
|
|
|
- [ ] 包名改为 `nonebot_plugin_*`,位置在 `../../hexi/plugins`。
|
2026-09-08 14:25:32 +08:00
|
|
|
|
- [ ] 有 `__plugin_meta__`,`type="application"`(核心/框架插件标 `library`,不参与热拔插)。
|
2026-09-01 13:13:40 +08:00
|
|
|
|
- [ ] 配置已接入统一标准(Web 能读能改,运行期/重启生效)。
|
|
|
|
|
|
- [ ] 命令在 `handlers/`,业务在 `services/`,工具在 `utils/`。
|
|
|
|
|
|
- [ ] 数据访问集中(repository / services),原子写 / 事务。
|
|
|
|
|
|
- [ ] 无 `*` 导入、裸 `except`、`print`;无同步阻塞混入 async。
|
|
|
|
|
|
- [ ] 运行数据写入统一数据目录,不写入插件源码目录;配置读取点支持标准热更新语义。
|
|
|
|
|
|
- [ ] 外部请求有超时/重试/失败提示;命令有权限/限流。
|
|
|
|
|
|
- [ ] 需要 Web 的已 `register_web_plugin`;鉴权走统一 `web_auth`。
|