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

20 KiB
Raw Permalink Blame History

插件配置文件标准(Plugin Config Standard)

HeXi 统一插件配置标准 —— 机器可读、带类型的 schema 声明 + 持久化值文件, 让 Web 管理台(/hub)能对任意插件读取、展示、修改、热刷新、重启生效。

插件整体骨架/目录/入口/职责划分见《插件结构标准》(docs/plugin-structure-standard.md)。


0. 目标与一句话总结

  • 核心原则:插件只声明 schema,Web 自动生成表单;值统一写入值库;插件通过统一 API 读取生效值。

  • 一句话:插件在导入时调用 register_plugin_config(plugin_id, schema, apply, getter) 声明配置;值写入 ../../hexi/config/plugin_config.json;Web 端读 {schema, values} 渲染、保存后写回并热应用。

  • plugin_id 约定:必须等于 NoneBot 插件模块名(如 hexi.plugins.nonebot_plugin_helldivers_tools)。Web 的 /api/plugins/<id>/config 才能命中,插件目录合表也用该 id。


1. 相关文件与位置

hexi/
├── web_hub/                    # 统一 Web 管理台 / 鉴权 / 配置标准
│   ├── web_config.py           # 配置标准核心:schema 注册 / 值库 / 保存热刷新
│   ├── config_standard.py      # pydantic Config 一键接入的辅助封装
│   ├── web_auth.py             # 统一 Web 鉴权(OAuth2 + SQLite)
│   ├── web_plugin_registry.py  # 统一 Web 插件注册中心
│   └── web_hub_auth.py         # 兼容 shim
├── config/
│   └── plugin_config.json      # 统一值库:{ "<plugin_id>": { key: value, ... } }
└── web/                        # /hub 前端(通用 schema 表单渲染器)

值库按插件聚合,不每个插件一个文件:Web「插件管理」一页聚合所有插件配置、统一热刷新、统一迁移。


2. Schema 字段标准

每个插件声明一份 schema:

{
  "version": 1,                 // 插件配置 schema 版本(用于迁移)
  "fields": [
    {
      "key": "hd2_api_base_url",        // 必填,插件读取的字段名(snake_case)
      "label": "API 根地址",             // 必填,前端显示名
      "type": "string",                // 必填,见 §3
      "default": "https://api.helldivers2.dev",
      "description": "自托管 API 根地址;不填用公共社区 API。",
      "secret": false,                 // true → 前端脱敏/掩码
      "required": false,
      "placeholder": "http://192.168.2.15:18080",
      "env": "HD2_API_BASE_URL",       // 写回 .env 的变量名;缺省 = key.upper()
      "options": [                     // type=enum 时必填
        { "value": "auto", "label": "自动" },
        { "value": "manual", "label": "手动" }
      ],
      "min": 0, "max": 86400,           // int/float 范围(可选)
      "pattern": "^https?://",          // 字符串校验(可选)
      "group": "API 连接",              // 表单分组(可选)
      "depends_on": { "field": "enabled", "value": true }, // 条件显示(可选)
      "item_type": "int",              // type=list 的元素类型 int/float/str/bool(可选)
      "newline_list": true             // type=list 时:多行/逗号分隔(可选)
    }
  ]
}

字段说明

字段 类型 必填 说明
key string 是 插件读取字段名,snake_case,唯一
label string 是 前端显示名(中文)
type string 是 见 §3
default any 否 用户未改时的回填默认
description string 否 前端提示
secret bool 否 敏感字段前端掩码,返回时脱敏
required bool 否 保存时校验非空
options array enum 必填 [{value,label}]
env string 否 写 .env 变量名;缺省 key.upper()
min / max number 否 数值范围
pattern string 否 字符串正则
item_type string list 推荐 元素类型
newline_list / group / depends_on - 否 展示/校验增强

3. 支持的 type

type 前端控件 存储 说明
string 单行输入 字符串 通用文本
text 多行文本域 字符串(支持 \n) 长文本/提示词/模板
password 密码框(掩码) 字符串 自动 secret:true
int 数字输入 整数
float 数字输入 浮点数
bool 开关 布尔
enum 下拉 options[].value 必须提供 options
list 多行文本(每行一项) 数组 需 item_type;兼容逗号分隔
json/object 代码文本域 JSON 对象(校验后存) 高级
path 单行输入 路径字符串 运行期转 Path
object_set 通用表格(增删改) list[dict] 一组结构化条目;需 item_schema + key_field,见 §14

约定

  • key 一律 snake_case。
  • secret 字段只返回 ****,绝不返回明文;提交未修改的 **** 表示保持原值,只有提交 null 才清除。
  • 复杂类型(list/path/object/object_set)在 ../../.env 里不易安全表达,只做运行期热更新,不落 ../../.env;重启需插件自行从值库读取。

4. 值文件存储标准

{
  "hexi.plugins.nonebot_plugin_helldivers_tools": {
    "hd2_api_base_url": "http://192.168.2.15:18080",
    "hd2_api_language": "zh-Hans",
    "hd2_cache_ttl": 60,
    "hd2_super_client": "hexi-bot.local",
    "hd2_super_contact": "https://github.com/..."
  },
  "hexi.plugins.nonebot_plugin_steam_info": {
    "steam_api_key": ["key1", "key2"],
    "steam_request_interval": 300,
    "steam_broadcast_type": "part",
    "steam_playtime_theme": "light",
    "proxy": null
  }
}
  • 键 = plugin_id;值 = 仅用户显式设置过的字段(未设置不写死,避免覆盖 .env)。
  • null 表示「清空/恢复默认」。
  • 原子写入:先写临时文件再 os.replace;启动读取失败时保留原文件并告警,不得静默覆盖为空。
  • 读取优先级:用户值库 > getter 生效值 > schema default。
  • 保存校验失败返回 HTTP 422,不能静默把非法输入替换成默认值。
  • 值库按插件保存 revision;保存必须携带当前 revision,冲突返回 HTTP 409。

5. 插件读取配置的标准 API

方式 A:pydantic Config 一键接入(推荐)

# 在插件 __init__.py 里
from hexi.web_hub.config_standard import register_model_config
from .config import config

register_model_config(
    __name__,                          # = NoneBot 插件模块名
    config,                            # 模块级 pydantic Config 实例
    fields=["hd2_api_base_url", "hd2_cache_ttl", ...],
    labels={"hd2_api_base_url": "API 根地址", ...},
    descriptions={...},
    options={"mode": [{"value": "auto", "label": "自动"}]},
    types={"mode": "enum"},            # 覆盖自动推断的 type
)

运行期插件直接读 config.hd2_api_base_url。

register_model_config 自动:

  • 推断字段类型(bool/int/float/string/enum/text);
  • Literal[...] → 下拉枚举;list[str]/list[int] → 多行文本并按元素类型转换;
  • 内置 getter(回填当前生效值) 与 apply(保存后热更新 pydantic 对象,无需重启);
  • 密码/key/token 类字段自动 secret=True。

方式 B:非 pydantic 插件手动注册

from hexi.web_hub.web_config import register_plugin_config

def _get():  # 返回当前生效值 dict
    return {"field": get_my_cur_value("field")}

def _apply(values):  # 保存后热应用
    set_my_config(values)

register_plugin_config(
    __name__,
    {"fields": [{"key": "field", "label": "字段", "type": "string", "default": ""}]},
    apply=_apply,
    getter=_get,
)

同一插件可多次注册:register_plugin_config / register_model_config / register_config_items / register_object_set 对同一 plugin_id 重复调用时,字段按 key 自动合并到一个 schema,getter 合并取值,apply 按注册顺序依次应用(不覆盖)。所以 config.py 里分多段注册(如 temp 清理 / S3 / 群分组 / object_set)是合理且推荐的。

6. 值读取优先级

get_config(plugin_id) 对每个字段按序取值:

  1. 用户已保存值(值库 plugin_config.json);
  2. getter 返回的当前生效值(插件运行态);
  3. schema.default;
  4. None。

这样 Web 表单打开时显示的是插件当前真正生效的配置,而不是只看默认值。


7. 保存流程(热刷新)

save_config(plugin_id, values):

  1. 按 schema 字段的 type/item_type 做 类型校验与转换(string/int/float/bool/enum/list)。
  2. 写值库 plugin_config.json(原子写)。
  3. 写 .env:非 list 字段写入 os.environ + ../../.env(保证重启仍生效);list/path/object 跳过,避免 str(list) 破坏重启解析。
  4. 调 apply(values):把值热应用到插件运行态对象(list/path/object 也在此生效)。
  5. 若插件未提供 apply,则 hot_reload(plugin_id) 让插件重载;library/非 application 插件不受热重载(见《插件类型分类》,热拔插会破坏依赖它的插件)。

为什么 apply 与 env 都做:NoneBot 的 get_driver().config 在启动时即固定,重载插件也读不到新 env;所以运行期必须 apply,重启靠 env。


8. Web 端「读 / 改」标准接口(/hub)

方法 路径 说明
GET /api/plugins 所有已注册 Web 插件
GET /api/plugins/catalog 所有 application/library 插件(含带配置项的 library)+ type/has_config/has_web/web_path
GET /api/plugins/{id}/config 读 {schema, values, revision}(登录);secret 值只返回 ****
POST /api/plugins/{id}/config 保存 { "revision": n, "values": { key: value } };冲突 409,校验失败 422
DELETE /api/plugins/{id}/config 清空该插件覆盖,恢复默认(可选)

响应示例(GET):

{
  "ok": true,
  "revision": 3,
  "schema": { "fields": [ { "key": "hd2_cache_ttl", "type": "int", "default": 60, ... } ] },
  "values": { "hd2_cache_ttl": 60, "hd2_api_base_url": "http://192.168.2.15:18080" }
}

前端 ../../hexi/web/src/pages/plugins/index.tsx 已按 type 自动渲染:bool→开关、enum→下拉、text→多行、password→掩码、int/float→数字、secret→脱敏。


9. 权限与安全

  • 全部配置接口走 hexi.web_hub.web_auth.require_admin(OAuth2 + SQLite)。
  • 配置 POST 必须携带 GET 返回的 revision;缺失返回 428,冲突返回 409。
  • 只允许 plugin_id 存在于注册表,未注册返回 ok:false(防任意写入)。
  • 部署时必须显式设置 Web 管理员凭据;禁止生产环境使用默认的 admin/admin。
  • secret 字段只返回 ****;提交 **** 保持原值,提交 null 清除,写回明文仅限服务端受控存储。
  • 写 .env 用「原行更新/追加」,保留注释,不整文件覆盖。

10. 版本与迁移

配置保存状态至少区分:persisted(值库已保存)、applied(运行态已应用)、restart_required(需重启)和 apply_failed(应用失败)。apply 失败时接口必须返回错误,且保留上一份可恢复配置。

{
  "$schema_version": 3,
  "plugins": {
    "hexi.plugins.xxx": {
      "schema_version": 1,
      "values": { "...": "..." }
    }
  }
}
  • 升级 schema 时保留 values:新增字段取 default,删除字段清理。
  • 提供 migrate(plugin_id, old_schema, new_schema) 钩子,支持字段改名/类型升级/str 列表迁 list[int];迁移必须幂等,失败时保留旧值文件并阻止该插件配置加载。
  • 读到未知插件条目时保留并上报,不强制删除。

11. 落地示例

# hexi/plugins/nonebot_plugin_helldivers_tools/__init__.py
from hexi.web_hub.config_standard import register_model_config
from .config import config as _hd2_config

register_model_config(
    __name__,
    _hd2_config,
    fields=["hd2_api_base_url", "hd2_api_language", "hd2_cache_ttl",
            "hd2_super_client", "hd2_super_contact"],
    labels={"hd2_api_base_url": "API 根地址", "hd2_cache_ttl": "数据缓存秒数", ...},
    descriptions={"hd2_api_base_url": "自托管 API 根地址;不填用公共社区 API", ...},
)

12. 已接入与待接入

  • 已接入:helldivers_tools、mc_server_status、video_analysis、steam_info、picstatus、galgame_card。
  • 未接入(建议后续):learning_chat、group_daily_analysis(已有独自 Web 配置,避免冲突)、picfinder_take、bf_bot、group_tools、hexi/core(模块级常量/SUPERUSERS,运行期热更复杂)。

13. 插件内部配置项(来源无关)接入标准

不只是 ../../.env:插件内部自己的配置项(module 常量、YAML/JSON 配置、数据库里的开关等)同样遵循统一 schema,Web 可读可改。核心是让每个配置项自带 getter/setter,与来源解耦。

13.1 四种来源与接入方式

配置来源 接入方式 getter/setter 持久化 运行期生效点
pydantic Config(../../.env 驱动) register_model_config 自动(读写 Config 对象) 写值库 + 写 ../../.env apply 热更新对象
模块常量 register_config_items(..., store=模块) 自动(getattr/setattr) 写值库(可选写 ../../.env) 需插件用 get_effective_value 读取
配置文件(dict/JSON/YAML) register_config_items(..., store=dict) 自动(读写 dict) 插件自身写回文件 / 值库 插件从 dict 读取时即生效
自定义(DB/运行态) register_config_items 传 getter/setter 自定义 自定义回写 自定义

13.2 通用注册 API(任意来源)

from hexi.web_hub.config_standard import register_config_items

register_config_items(
    __name__,  # = NoneBot 插件模块名
    [
        {"key": "DAILY_LIMIT", "label": "每日限额", "type": "int", "default": 50},
        {"key": "CHECK", "label": "开启截屏判定", "type": "bool", "default": True},
        {"key": "SAUCENAO_KEY", "label": "API Key", "type": "password", "secret": True},
        # 也可为某条目自定义 getter/setter
        {"key": "custom", "label": "自定义", "type": "string",
         "getter": lambda: my_get(), "setter": lambda v: my_set(v)},
    ],
    store=config_module,  # dict 或模块;未写 getter/setter 时默认读写这里
    apply_extra=None,     # 可选:保存后做额外持久化/热加载
    getter_extra=None,   # 可选:getter 额外返回非 items 的值
)

13.3 关键规则:运行期要生效,插件必须读生效值

Web 保存后 apply 会把新值写回来源(模块属性/dict/自定义),但若插件在别处是用 from .config import X 值拷贝进来的量,不受影响。要真正运行期生效,插件在读配置处改用统一 API:

from hexi.web_hub.web_config import get_effective_value

limit = get_effective_value("hexi.plugins.nonebot_plugin_picfinder_take", "DAILY_LIMIT", 50)
if not check_quota(limit): ...

优先级:用户值库 > getter 生效值 > default。这样 Web 改完即可生效,重启也不丢。

13.4 已接入的非 env 示例

  • picfinder_take:把 SAUCENAO_KEY / DAILY_LIMIT / SEARCH_TIMEOUT / CHECK / THUMB_ON / CHAIN_REPLY / IGNORE_STAMP / threshold 这些内部常量注册为 Web 可读可改配置项(store=config 模块)。

  • video_analysis:把原本硬编码在 storage/s3.py 的 S3 连接配置(PLANA/PLANB/PLANC/公网 endpoint/access/secret/bucket/region/secure/domain)注册为 Web 可读可改项;apply 时清空懒加载 S3 客户端缓存,且 s3 客户端创建时经 get_effective_value 读取,因此运行期与重启均生效。另用 type=json 暴露 data/list.json 的群分组配置(groups + blacklist),自定义 getter/setter 同步读写,并标记 nosave(不写入 plugin_config.json,权威源是 list.json),避免与 添加白名单 等指令互相覆盖为陈旧值。

  • 生效说明:注册后 Web 能读、能改、会持久化;若希望改完立刻反映到插件行为,需把插件内对应读取点改成 get_effective_value(见 §13.3),或让 apply_extra 里 hot_reload 并让 config 模块从值库初始化。


14. 通用对象集标准(object_set)

当插件要暴露「一组结构化条目」时(群分组、测速站点、服务器列表、白/黑名单成员、装备表等),用 object_set 类型声明 item_schema + key_field,Web 端通用渲染成可增删改的表格,后端通用按子字段校验/去重。

14.1 Schema 形态

{
  "key": "groups",
  "label": "群分组配置",
  "type": "object_set",
  "key_field": "group_id",              // 子字段主键(定位/去重/删除行)
  "item_schema": {
    "fields": [
      {"key": "group_id", "label": "群号", "type": "string", "required": true},
      {"key": "auto", "label": "自动解析", "type": "bool", "default": false},
      {"key": "plan", "label": "存储方案", "type": "enum",
       "options": [{"value":"A","label":"A"},{"value":"B","label":"B"}]},
      {"key": "auto_link", "label": "自动链接关键词", "type": "text", "item_type": "str"}
    ]
  },
  "default": [],
  "nosave": true      // 权威源在插件自身文件/DB 时填 true
}

14.2 值形态

[
  {"group_id":"123", "auto":true, "plan":"A", "auto_link":["xhs"]},
  {"group_id":"456", "auto":false, "plan":null, "auto_link":[]}
]

14.3 一键注册 API

from hexi.web_hub.config_standard import register_object_set

register_object_set(
    __name__,                          # NoneBot 插件模块名
    "groups",                        # 对象集配置 key
    [                                 # item_schema 子字段
        {"key": "group_id", "label": "群号", "type": "string"},
        {"key": "auto", "label": "自动解析", "type": "bool", "default": True},
        {"key": "plan", "label": "方案", "type": "enum",
         "options": [{"value":"A","label":"A"},{"value":"B","label":"B"}]},
        {"key": "auto_link", "label": "关键词", "type": "text", "item_type": "str"},
    ],
    key_field="group_id",
    getter=get_groups,        # () -> list[dict]
    setter=set_groups,        # (list[dict]) -> None(插件自行写回来源)
    label="群分组配置",
    nosave=True,             # 权威源在 list.json/DB,不写 plugin_config.json
)

框架自动做:按 item_schema 逐字段类型转换(bool/int/float/string/enum/text/list/json)、触发 _coerce_value 的值转成目标类型、按 key_field 去重并丢弃缺主键行,再把归一整形的 list 交给你的 setter。

14.4 Web 端通用表格组件(建议)

  • 前端新增 ObjectSetEditor 组件,读 field.item_schema.fields 渲染表头 + 每行输入(与 §3 type 一一对应);
  • 支持:添加行、删除行、编辑单元格、上移/下移(可选);主键列默认唯一校验;
  • 保存时把整个 list 提交到既有 POST /api/plugins/{id}/config,无需新接口;
  • 也可按需加细粒度:POST /api/plugins/{id}/config/{key} 单条 upsert、DELETE .../{key}/{key_value} 删除单行(后端需在 apply 里实现 upsert/delete)。

14.5 可接入示范(候选)

  • video_analysis:list.json 的 groups 群分组(从纯 JSON 升级为结构化 object_set,Key=group_id);
  • picstatus:ps_test_sites 测速站点列表(Key=name);
  • mc_server_status:var.group_list 每群服务器列表(Key=群号);
  • helldivers_tools:战备数据(可只读展示,来源 SQLite,Key=id);
  • learning_chat:分群配置(Key=group_id)。