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:
@@ -5,6 +5,8 @@
|
||||
- bot 账号 / WS 连通性:从 OneBot V11 适配器的 bots / connections 读取
|
||||
- host 性能:psutil(CPU/内存/磁盘/进程)+ platform + nonebot 版本
|
||||
仅在 /hub/api/dashboard 被调用时执行,不做常驻采样。
|
||||
同步采集(psutil 等可能阻塞的调用)整体放入线程池执行,避免阻塞事件循环;
|
||||
结果做 1 秒短缓存,多个前端标签页(含 SSE 循环)不至于各自重复采集。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -26,17 +28,21 @@ _MODULE_START = time.time()
|
||||
# 网络速率采样:记录上一次累计计数与时间,用于计算实时 bytes/s 用量
|
||||
_NET_LAST: dict = {"time": None, "sent": None, "recv": None}
|
||||
|
||||
# 采集结果短缓存(多标签页 / SSE 每 2s 循环共用一份结果)
|
||||
_DASH_CACHE: dict = {"time": 0.0, "data": None}
|
||||
_DASH_CACHE_TTL = 1.0
|
||||
|
||||
async def _collect_cpu() -> dict:
|
||||
|
||||
def _cpu_sync() -> dict:
|
||||
try:
|
||||
per_core = await asyncio.to_thread(psutil.cpu_percent, 0.3, True)
|
||||
per_core = psutil.cpu_percent(0.3, True)
|
||||
except TypeError:
|
||||
# 兼容部分平台不接受 percpu 参数
|
||||
per_core = []
|
||||
if per_core:
|
||||
percent = sum(per_core) / len(per_core)
|
||||
else:
|
||||
percent = await asyncio.to_thread(psutil.cpu_percent, 0.3)
|
||||
percent = psutil.cpu_percent(0.3)
|
||||
info: dict = {
|
||||
"percent": round(float(percent), 1),
|
||||
"per_core": [round(float(x), 1) for x in (per_core or [])],
|
||||
@@ -57,7 +63,7 @@ async def _collect_cpu() -> dict:
|
||||
return info
|
||||
|
||||
|
||||
async def _collect_memory() -> dict:
|
||||
def _memory_sync() -> dict:
|
||||
mem = psutil.virtual_memory()
|
||||
try:
|
||||
proc = psutil.Process()
|
||||
@@ -76,10 +82,10 @@ async def _collect_memory() -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def _collect_process() -> dict:
|
||||
def _process_sync() -> dict:
|
||||
try:
|
||||
proc = psutil.Process()
|
||||
cpu = await asyncio.to_thread(proc.cpu_percent, 0.2)
|
||||
cpu = proc.cpu_percent(0.2)
|
||||
info = {
|
||||
"pid": proc.pid,
|
||||
"name": proc.name(),
|
||||
@@ -94,7 +100,7 @@ async def _collect_process() -> dict:
|
||||
return info
|
||||
|
||||
|
||||
async def _collect_network() -> dict:
|
||||
def _network_sync() -> dict:
|
||||
"""采集网络流量累计值 + 实时速率(bytes/s)。"""
|
||||
global _NET_LAST
|
||||
now = time.time()
|
||||
@@ -157,7 +163,7 @@ async def _collect_network() -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def _collect_backend() -> dict:
|
||||
def _backend_sync() -> dict:
|
||||
"""BOT 后端信息:NoneBot 版本 / 驱动方式 / 装载插件数。"""
|
||||
from nonebot import get_loaded_plugins
|
||||
|
||||
@@ -183,7 +189,7 @@ async def _collect_backend() -> dict:
|
||||
return info
|
||||
|
||||
|
||||
async def _collect_system() -> dict:
|
||||
def _system_sync() -> dict:
|
||||
boot = psutil.boot_time()
|
||||
now = time.time()
|
||||
return {
|
||||
@@ -206,7 +212,7 @@ def _nonebot_version() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
async def _collect_disk() -> list:
|
||||
def _disk_sync() -> list:
|
||||
parts = []
|
||||
for part in psutil.disk_partitions():
|
||||
try:
|
||||
@@ -271,18 +277,25 @@ async def _collect_bots() -> list:
|
||||
|
||||
|
||||
async def collect_dashboard() -> dict:
|
||||
"""聚合采集,失败字段降级,不阻塞整体返回。"""
|
||||
cpu, memory, process, system, disk, bots, network, backend = await asyncio.gather(
|
||||
_collect_cpu(),
|
||||
_collect_memory(),
|
||||
_collect_process(),
|
||||
_collect_system(),
|
||||
_collect_disk(),
|
||||
_collect_bots(),
|
||||
_collect_network(),
|
||||
_collect_backend(),
|
||||
"""聚合采集,失败字段降级,不阻塞整体返回。
|
||||
|
||||
同步采集整体放线程池(psutil 的 disk_usage 等可能秒级阻塞),
|
||||
结果带 1 秒短缓存(SSE 每 2s 循环与多标签页共用)。
|
||||
"""
|
||||
now = time.time()
|
||||
if _DASH_CACHE["data"] is not None and now - _DASH_CACHE["time"] < _DASH_CACHE_TTL:
|
||||
return _DASH_CACHE["data"]
|
||||
cpu, memory, process, system, disk, network, backend, bots = await asyncio.gather(
|
||||
asyncio.to_thread(_cpu_sync),
|
||||
asyncio.to_thread(_memory_sync),
|
||||
asyncio.to_thread(_process_sync),
|
||||
asyncio.to_thread(_system_sync),
|
||||
asyncio.to_thread(_disk_sync),
|
||||
asyncio.to_thread(_network_sync),
|
||||
asyncio.to_thread(_backend_sync),
|
||||
_collect_bots(), # 需要走 bot API,必须留在事件循环
|
||||
)
|
||||
return {
|
||||
data = {
|
||||
"bots": bots,
|
||||
"system": system,
|
||||
"cpu": cpu,
|
||||
@@ -292,3 +305,6 @@ async def collect_dashboard() -> dict:
|
||||
"network": network,
|
||||
"backend": backend,
|
||||
}
|
||||
_DASH_CACHE["time"] = now
|
||||
_DASH_CACHE["data"] = data
|
||||
return data
|
||||
|
||||
Reference in New Issue
Block a user