Add HeXi bot codebase: custom plugins, web frontends, tests
- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
# 插件配置文件标准(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_config.py # 配置标准核心:schema 注册 / 值库 / 保存热刷新
|
||||
├── config_standard.py # pydantic Config 一键接入的辅助封装
|
||||
├── 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.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_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)` 让插件重载。
|
||||
|
||||
**为什么 apply 与 env 都做**:NoneBot 的 `get_driver().config` 在启动时即固定,重载插件也读不到新 env;所以运行期必须 apply,重启靠 env。
|
||||
|
||||
---
|
||||
|
||||
## 8. Web 端「读 / 改」标准接口(/hub)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/plugins` | 所有已注册 Web 插件 |
|
||||
| GET | `/api/plugins/catalog` | 所有应用插件 + `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_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.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.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_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.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)。
|
||||
@@ -0,0 +1,156 @@
|
||||
# 插件结构标准(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")`。
|
||||
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.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_auth.require_admin`(OAuth2 + SQLite),不要自造一套。
|
||||
- 敏感字段 `secret=True`,前端掩码;写回允许明文。
|
||||
|
||||
## 9. 插件迁移检查清单
|
||||
|
||||
- [ ] 包名改为 `nonebot_plugin_*`,位置在 `hexi/plugins/`。
|
||||
- [ ] 有 `__plugin_meta__`,`type="application"`。
|
||||
- [ ] 配置已接入统一标准(Web 能读能改,运行期/重启生效)。
|
||||
- [ ] 命令在 `handlers/`,业务在 `services/`,工具在 `utils/`。
|
||||
- [ ] 数据访问集中(repository / services),原子写 / 事务。
|
||||
- [ ] 无 `*` 导入、裸 `except`、`print`;无同步阻塞混入 async。
|
||||
- [ ] 运行数据写入统一数据目录,不写入插件源码目录;配置读取点支持标准热更新语义。
|
||||
- [ ] 外部请求有超时/重试/失败提示;命令有权限/限流。
|
||||
- [ ] 需要 Web 的已 `register_web_plugin`;鉴权走统一 `web_auth`。
|
||||
Reference in New Issue
Block a user