2026-09-01 13:13:40 +08:00
# 插件配置文件标准(Plugin Config Standard)
> HeXi 统一插件配置标准 —— 机器可读、带类型的 schema 声明 + 持久化值文件,
> 让 Web 管理台(/hub)能对任意插件**读取、展示、修改、热刷新、重启生效**。
>
> 插件整体骨架/目录/入口/职责划分见《插件结构标准》(docs/plugin-structure-standard.md)。
---
## 0. 目标与一句话总结
- **核心原则**:插件只声明 `schema` ,Web 自动生成表单;值统一写入值库;插件通过统一 API 读取生效值。
2026-09-03 15:40:43 +08:00
- **一句话**:插件在导入时调用 `register_plugin_config(plugin_id, schema, apply, getter)` 声明配置;值写入 `../../hexi/config/plugin_config.json` ; Web 端读 `{schema, values}` 渲染、保存后写回并热应用。
2026-09-01 13:13:40 +08:00
- **`plugin_id` 约定**:必须等于 NoneBot 插件模块名(如 `hexi.plugins.nonebot_plugin_helldivers_tools` )。Web 的 `/api/plugins/<id>/config` 才能命中,插件目录合表也用该 id。
---
## 1. 相关文件与位置
```text
hexi/
2026-09-08 14:25:32 +08:00
├── 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
2026-09-01 13:13:40 +08:00
├── config/
│ └── plugin_config.json # 统一值库:{ "<plugin_id>": { key: value, ... } }
└── web/ # /hub 前端(通用 schema 表单渲染器)
```
> 值库按**插件聚合**,不每个插件一个文件:Web「插件管理」一页聚合所有插件配置、统一热刷新、统一迁移。
---
## 2. Schema 字段标准
每个插件声明一份 schema:
```jsonc
{
"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` 才清除。
2026-09-03 15:40:43 +08:00
- 复杂类型(`list` /`path` /`object` /`object_set` )在 `../../.env` 里不易安全表达,**只做运行期热更新**,不落 `../../.env` ;重启需插件自行从值库读取。
2026-09-01 13:13:40 +08:00
---
## 4. 值文件存储标准
```jsonc
{
"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 一键接入(推荐)
```python
# 在插件 __init__.py 里
2026-09-08 14:25:32 +08:00
from hexi.web_hub.config_standard import register_model_config
2026-09-01 13:13:40 +08:00
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 插件手动注册
```python
2026-09-08 14:25:32 +08:00
from hexi.web_hub.web_config import register_plugin_config
2026-09-01 13:13:40 +08:00
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` (原子写)。
2026-09-03 15:40:43 +08:00
3. **写 .env** :非 list 字段写入 `os.environ` + `../../.env` (保证重启仍生效); list/path/object 跳过,避免 `str(list)` 破坏重启解析。
2026-09-01 13:13:40 +08:00
4. **调 `apply(values)`** :把值热应用到插件运行态对象(list/path/object 也在此生效)。
2026-09-08 14:25:32 +08:00
5. 若插件未提供 apply,则 `hot_reload(plugin_id)` 让插件重载;**library/非 application 插件不受热重载**(见《插件类型分类》,热拔插会破坏依赖它的插件)。
2026-09-01 13:13:40 +08:00
**为什么 apply 与 env 都做** : NoneBot 的 `get_driver().config` 在启动时即固定,重载插件也读不到新 env;所以运行期必须 apply,重启靠 env。
---
## 8. Web 端「读 / 改」标准接口(/hub)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/plugins` | 所有已注册 Web 插件 |
2026-09-08 14:25:32 +08:00
| GET | `/api/plugins/catalog` | 所有 application/library 插件(含带配置项的 library)+ `type/has_config/has_web/web_path` |
2026-09-01 13:13:40 +08:00
| 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):
```jsonc
{
"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" }
}
```
2026-09-03 15:40:43 +08:00
前端 `../../hexi/web/src/pages/plugins/index.tsx` 已按 `type` 自动渲染:bool→开关、enum→下拉、text→多行、password→掩码、int/float→数字、secret→脱敏。
2026-09-01 13:13:40 +08:00
---
## 9. 权限与安全
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
- 配置 POST 必须携带 GET 返回的 `revision` ;缺失返回 428,冲突返回 409。
- 只允许 `plugin_id` 存在于注册表,未注册返回 `ok:false` (防任意写入)。
- 部署时必须显式设置 Web 管理员凭据;禁止生产环境使用默认的 `admin/admin` 。
- `secret` 字段只返回 `****` ;提交 `****` 保持原值,提交 `null` 清除,写回明文仅限服务端受控存储。
- 写 .env 用「原行更新/追加」,保留注释,不整文件覆盖。
---
## 10. 版本与迁移
配置保存状态至少区分:`persisted` (值库已保存)、`applied` (运行态已应用)、`restart_required` (需重启)和 `apply_failed` (应用失败)。apply 失败时接口必须返回错误,且保留上一份可恢复配置。
```jsonc
{
"$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. 落地示例
```python
# hexi/plugins/nonebot_plugin_helldivers_tools/__init__.py
2026-09-08 14:25:32 +08:00
from hexi.web_hub.config_standard import register_model_config
2026-09-01 13:13:40 +08:00
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` 。
2026-09-08 14:25:32 +08:00
- **未接入(建议后续)**: `learning_chat` 、`group_daily_analysis` (已有独自 Web 配置,避免冲突)、`picfinder_take` 、`bf_bot` 、`group_tools` 、`hexi/core` (模块级常量/SUPERUSERS,运行期热更复杂)。
2026-09-01 13:13:40 +08:00
---
## 13. 插件内部配置项(来源无关)接入标准
2026-09-03 15:40:43 +08:00
> **不只是 `../../.env`**:插件内部自己的配置项(module 常量、YAML/JSON 配置、数据库里的开关等)同样遵循统一 schema,Web 可读可改。核心是让每个配置项自带 **getter/setter**,与来源解耦。
2026-09-01 13:13:40 +08:00
### 13.1 四种来源与接入方式
| 配置来源 | 接入方式 | getter/setter | 持久化 | 运行期生效点 |
|---|---|---|---|---|
2026-09-03 15:40:43 +08:00
| pydantic Config(`../../.env` 驱动) | `register_model_config` | 自动(读写 Config 对象) | 写值库 + 写 `../../.env` | apply 热更新对象 |
| 模块常量 | `register_config_items(..., store=模块)` | 自动(getattr/setattr) | 写值库(可选写 `../../.env` ) | 需插件用 `get_effective_value` 读取 |
2026-09-01 13:13:40 +08:00
| 配置文件(dict/JSON/YAML) | `register_config_items(..., store=dict)` | 自动(读写 dict) | 插件自身写回文件 / 值库 | 插件从 dict 读取时即生效 |
| 自定义(DB/运行态) | `register_config_items` 传 `getter` /`setter` | 自定义 | 自定义回写 | 自定义 |
### 13.2 通用注册 API(任意来源)
```python
2026-09-08 14:25:32 +08:00
from hexi.web_hub.config_standard import register_config_items
2026-09-01 13:13:40 +08:00
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:
```python
2026-09-08 14:25:32 +08:00
from hexi.web_hub.web_config import get_effective_value
2026-09-01 13:13:40 +08:00
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 形态
```jsonc
{
"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 值形态
```jsonc
[
{ "group_id" : "123" , "auto" : true , "plan" : "A" , "auto_link" :[ "xhs" ]},
{ "group_id" : "456" , "auto" : false , "plan" : null , "auto_link" :[]}
]
```
### 14.3 一键注册 API
```python
2026-09-08 14:25:32 +08:00
from hexi.web_hub.config_standard import register_object_set
2026-09-01 13:13:40 +08:00
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)。