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:
+33
-7
@@ -11,7 +11,7 @@
|
||||
"default": ..., # 默认值
|
||||
"description": "...", # 提示
|
||||
"env": "WEB_PASSWORD", # 环境变量名(缺省=key.upper())
|
||||
"secret": true, # 是否敏感(前端脱敏/掩码)
|
||||
"secret": true, # 是否敏感(前端默认掩码显示,回传明文)
|
||||
"options": [...], # enum 可选项
|
||||
"required": false,
|
||||
}
|
||||
@@ -28,6 +28,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
@@ -39,7 +40,7 @@ DATA_DIR = _ROOT / "config"
|
||||
STORE_PATH = DATA_DIR / "plugin_config.json"
|
||||
ENV_PATH = _ROOT.parent / ".env" # 仓库根 .env
|
||||
|
||||
_TYPES = {"string", "text", "password", "int", "float", "bool", "enum", "json", "object", "path", "object_set"}
|
||||
_TYPES = {"string", "text", "password", "int", "float", "bool", "enum", "json", "object", "path", "object_set", "array"}
|
||||
|
||||
# plugin_id -> schema
|
||||
_schemas: dict[str, dict[str, Any]] = {}
|
||||
@@ -209,6 +210,23 @@ def _coerce(field: dict[str, Any], raw: Any) -> Any:
|
||||
return float(raw)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError(f"字段 {field['key']} 必须是数字") from e
|
||||
if typ == "array":
|
||||
item_type = str(field.get("item_type") or "str")
|
||||
if isinstance(raw, str):
|
||||
# 防御:兼容旧前端提交的多行/逗号分隔文本
|
||||
raw = [p.strip() for p in re.split(r"[,\n]+", raw) if p.strip()]
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError(f"字段 {field['key']} 必须是数组")
|
||||
out = []
|
||||
for item in raw:
|
||||
if item_type in {"int", "float"}:
|
||||
try:
|
||||
out.append(int(item) if item_type == "int" else float(item))
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError(f"字段 {field['key']} 含非法数字: {item}") from e
|
||||
else:
|
||||
out.append(str(item))
|
||||
return out
|
||||
if typ in {"string", "text", "password"}:
|
||||
return "" if raw is None else str(raw)
|
||||
if typ == "enum":
|
||||
@@ -240,9 +258,16 @@ def get_config(plugin_id: str) -> Optional[dict[str, Any]]:
|
||||
values[key] = f["default"]
|
||||
else:
|
||||
values[key] = None
|
||||
for field in schema.get("fields", []):
|
||||
if field.get("secret") and values.get(field["key"]) not in (None, ""):
|
||||
values[field["key"]] = MASKED_SECRET
|
||||
# 密钥类字段回传明文:该管理台仅本地局域网/本机使用,无外泄面;
|
||||
# 明文由前端默认以掩码(小圆点)形式展示,用户点「眼睛」才可查看。
|
||||
# array 字段兼容旧存储:text 时期保存的是换行/逗号分隔字符串,归一化为数组
|
||||
for f in schema.get("fields", []):
|
||||
key = f["key"]
|
||||
if f.get("type") == "array" and isinstance(values.get(key), str):
|
||||
try:
|
||||
values[key] = _coerce(f, values[key])
|
||||
except ValueError:
|
||||
values[key] = []
|
||||
return {"schema": schema, "values": values, "revision": _revisions.get(plugin_id, 0)}
|
||||
|
||||
|
||||
@@ -272,9 +297,9 @@ def _update_env(plugin_id: str, values: dict[str, Any]) -> None:
|
||||
key = f["key"]
|
||||
if key not in values:
|
||||
continue
|
||||
# text/json/object/path 这类非标量不适合写进 .env(重启解析会出错),
|
||||
# text/json/object/path/array 这类非标量不适合写进 .env(重启解析会出错),
|
||||
# 这类字段仅在运行期热更新,不落盘 .env(见 config_standard 说明)。
|
||||
if f.get("type") in {"text", "json", "object", "path"}:
|
||||
if f.get("type") in {"text", "json", "object", "path", "array"}:
|
||||
continue
|
||||
env_name = _field_env(f)
|
||||
raw = values[key]
|
||||
@@ -340,6 +365,7 @@ def save_config(
|
||||
if key not in values:
|
||||
continue
|
||||
raw = values[key]
|
||||
# 兼容旧版本前端回传的掩码占位:不覆盖已保存的明文
|
||||
if f.get("secret") and raw == MASKED_SECRET:
|
||||
continue
|
||||
if raw in (None, ""):
|
||||
|
||||
Reference in New Issue
Block a user