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

8.9 KiB
Raw Permalink Blame History

插件结构标准(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 标准职责(固定顺序)

  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)

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.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。