feat: revamp web hub — global connection status, array config editor, log streaming

- Global connection badge in app header (persistent dashboard SSE via ConnectionProvider)
- LogProvider keeps log data and SSE stream alive across page switches (no reload spinner)
- Config drawer: password reveal toggle, array-type row editor for list fields
- Plugin management: optimistic updates, race-free loading, reset all group overrides
- Logs: load-earlier pagination; backend tail-read optimization (before/end offsets)
- web_auth: Web password changes persist (sync_admin seeding only when .env has explicit key)
- Dashboard: collect in thread pool + 1s result cache
- ErrorBoundary + local Suspense: plugin chunk loading stays inside content area
- UI polish: table row hover, mobile header/breadcrumb fixes, wider config drawer
- config_standard: array type inference for list fields; array env skip
- random_jm_code / steam_info: array schema migration; README web frontend note

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-03 15:39:10 +08:00
co-authored by Claude Code
parent 9371a28e35
commit c53b210413
45 changed files with 1312 additions and 576 deletions
@@ -0,0 +1,92 @@
"""配置注册:按《插件配置文件标准》统一接入 Web 管理台。
此处演示三类:
1) register_model_config → 读 .env / NoneBot config 的 pydantic Config
2) register_config_items → 来源无关(模块常量)
3) register_object_set → 一组结构化条目(数组/对象集合)
"""
from __future__ import annotations
from nonebot import get_plugin_config
from pydantic import BaseModel
from hexi.config_standard import register_config_items, register_model_config, register_object_set
# plugin_id = NoneBot 插件模块名
_PLUGIN_ID = __package__
class Config(BaseModel):
enable: bool = True
reply: str = "你好,我是模板插件!"
config = get_plugin_config(Config)
# —— 来源无关示例:模块级常量(Web 可读可改) ——
_COLOR = "red"
def _get_color() -> str:
return _COLOR
def _set_color(v: str) -> None:
global _COLOR
_COLOR = v
# —— 对象集示例:一组条目(如服务器/关键词/白名单) ——
_ITEMS: dict[str, dict] = {}
def _obj_getter() -> list[dict]:
return [{"name": k, "value": v.get("value", "")} for k, v in _ITEMS.items()]
def _obj_setter(rows: list[dict]) -> None:
_ITEMS.clear()
for row in rows:
_ITEMS[str(row["name"])] = {"value": row.get("value", "")}
def register_config() -> None:
# 1) env/pydantic 配置
register_model_config(
_PLUGIN_ID,
config,
fields=["enable", "reply"],
labels={"enable": "启用", "reply": "回复内容"},
descriptions={"enable": "关闭后指令直接提示已停用", "reply": "模板指令的默认回复"},
)
# 2) 来源无关(模块常量)
register_config_items(
_PLUGIN_ID,
[
{
"key": "color",
"label": "主题色",
"type": "string",
"getter": _get_color,
"setter": _set_color,
"secret": False,
}
],
)
# 3) 对象集(数组/结构化条目)
register_object_set(
_PLUGIN_ID,
"dict_items",
[
{"key": "name", "label": "名称", "type": "string"},
{"key": "value", "label": "值", "type": "string"},
],
key_field="name",
getter=_obj_getter,
setter=_obj_setter,
label="条目表",
# nosave=True 表示权威源在插件自身(文件/DB);此处写进 plugin_config.json,故为 False
)