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

438 lines
20 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 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. 相关文件与位置
```text
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:
```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` 才清除。
- 复杂类型(`list`/`path`/`object`/`object_set`)在 `../../.env` 里不易安全表达,**只做运行期热更新**,不落 `../../.env`;重启需插件自行从值库读取。
---
## 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 里
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 插件手动注册
```python
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):
```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" }
}
```
前端 `../../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 失败时接口必须返回错误,且保留上一份可恢复配置。
```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
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(任意来源)
```python
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:
```python
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 形态
```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
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)。