Files
HeXi/dev/docs/plugin-structure-standard.md
sansenhoshi 131b92b319 结构调整
视频解析多图/多媒体结构 消息体适配
2026-09-08 14:25:32 +08:00

158 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件结构标准(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 依赖方向(单向、向下)
```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")`。
> **type 取值(NoneBot 发布规范)**:`application`(向机器人用户提供功能,支持热插拔/热重载)或 `library`(为其他插件提供能力,不可热拔插)。新插件默认 `application`。本仓库的 `hexi/core`、`hexi/web_hub` 已从插件目录移出,作为机器人核心模块(**非插件**),直接不参与热拔插。
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 标准
### 4.1 环境驱动型(读 `../../.env` / NoneBot config)
```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
from hexi.web_hub.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_hub.web_auth.require_admin`(OAuth2 + SQLite),不要自造一套。
- 敏感字段 `secret=True`,前端掩码;写回允许明文。
## 9. 插件迁移检查清单
- [ ] 包名改为 `nonebot_plugin_*`,位置在 `../../hexi/plugins`。
- [ ] 有 `__plugin_meta__`,`type="application"`(核心/框架插件标 `library`,不参与热拔插)。
- [ ] 配置已接入统一标准(Web 能读能改,运行期/重启生效)。
- [ ] 命令在 `handlers/`,业务在 `services/`,工具在 `utils/`。
- [ ] 数据访问集中(repository / services),原子写 / 事务。
- [ ] 无 `*` 导入、裸 `except`、`print`;无同步阻塞混入 async。
- [ ] 运行数据写入统一数据目录,不写入插件源码目录;配置读取点支持标准热更新语义。
- [ ] 外部请求有超时/重试/失败提示;命令有权限/限流。
- [ ] 需要 Web 的已 `register_web_plugin`;鉴权走统一 `web_auth`。