2026-09-01 13:13:40 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""统一 Web 管理台 Dashboard 数据采集。
|
|
|
|
|
|
|
|
|
|
|
|
数据来源与 nonebot_plugin_picstatus 类似:
|
|
|
|
|
|
- bot 账号 / WS 连通性:从 OneBot V11 适配器的 bots / connections 读取
|
2026-09-22 14:23:52 +08:00
|
|
|
|
- 协议端接入信息:driver.config 的 host/port + 驱动实际注册的反向 WS 路由 + 本机网卡 IP
|
2026-09-01 13:13:40 +08:00
|
|
|
|
- host 性能:psutil(CPU/内存/磁盘/进程)+ platform + nonebot 版本
|
|
|
|
|
|
仅在 /hub/api/dashboard 被调用时执行,不做常驻采样。
|
2026-09-03 15:39:10 +08:00
|
|
|
|
同步采集(psutil 等可能阻塞的调用)整体放入线程池执行,避免阻塞事件循环;
|
2026-09-14 18:59:01 +08:00
|
|
|
|
结果做短缓存,多个前端标签页(含 SSE 循环)不至于各自重复采集。
|
|
|
|
|
|
CPU 采样走非阻塞模式(interval=None,与上一次调用求差):带 interval 的
|
|
|
|
|
|
psutil.cpu_percent/Process.cpu_percent 会真的 sleep 住调用线程,而 SSE 循环
|
|
|
|
|
|
每轮都调用一次,会凭空常驻烧掉约四分之一个核。
|
2026-09-01 13:13:40 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
2026-09-22 14:23:52 +08:00
|
|
|
|
import os
|
2026-09-01 13:13:40 +08:00
|
|
|
|
import platform
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
import psutil
|
|
|
|
|
|
from nonebot import get_bots, logger
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from cpuinfo import get_cpu_info
|
|
|
|
|
|
except Exception: # pragma: no cover
|
|
|
|
|
|
get_cpu_info = None
|
|
|
|
|
|
|
|
|
|
|
|
_MODULE_START = time.time()
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
# 网络速率采样:记录上一次累计计数与时间,用于计算实时 bytes/s 用量
|
|
|
|
|
|
_NET_LAST: dict = {"time": None, "sent": None, "recv": None}
|
|
|
|
|
|
|
2026-09-14 18:59:01 +08:00
|
|
|
|
# 采集结果短缓存(多标签页 / SSE 循环共用一份结果)
|
2026-09-03 15:39:10 +08:00
|
|
|
|
_DASH_CACHE: dict = {"time": 0.0, "data": None}
|
2026-09-14 18:59:01 +08:00
|
|
|
|
_DASH_CACHE_TTL = 2.0
|
|
|
|
|
|
|
|
|
|
|
|
# 进程对象与 CPU 采样必须跨调用复用:psutil 的非阻塞模式(interval=None)
|
|
|
|
|
|
# 是「与上一次调用」求差,换对象或首次调用只会拿到 0.0。
|
|
|
|
|
|
_PROC = psutil.Process()
|
|
|
|
|
|
psutil.cpu_percent(None, True) # 预热 CPU 采样(首次调用恒为 0.0)
|
|
|
|
|
|
_PROC.cpu_percent(None) # 预热进程采样
|
|
|
|
|
|
|
|
|
|
|
|
# bot 昵称(get_login_info)基本不变,长缓存避免每轮都打一次 OneBot API
|
|
|
|
|
|
_LOGIN_CACHE: dict[str, tuple[float, str]] = {}
|
|
|
|
|
|
_LOGIN_TTL = 600.0
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
|
|
|
|
|
|
def _cpu_sync() -> dict:
|
2026-09-01 13:13:40 +08:00
|
|
|
|
try:
|
2026-09-14 18:59:01 +08:00
|
|
|
|
per_core = psutil.cpu_percent(None, True)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
except TypeError:
|
|
|
|
|
|
# 兼容部分平台不接受 percpu 参数
|
|
|
|
|
|
per_core = []
|
|
|
|
|
|
if per_core:
|
|
|
|
|
|
percent = sum(per_core) / len(per_core)
|
|
|
|
|
|
else:
|
2026-09-14 18:59:01 +08:00
|
|
|
|
percent = psutil.cpu_percent(None)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
info: dict = {
|
|
|
|
|
|
"percent": round(float(percent), 1),
|
|
|
|
|
|
"per_core": [round(float(x), 1) for x in (per_core or [])],
|
|
|
|
|
|
"logical": psutil.cpu_count(),
|
|
|
|
|
|
"physical": psutil.cpu_count(logical=False),
|
|
|
|
|
|
"brand": "",
|
|
|
|
|
|
}
|
|
|
|
|
|
if get_cpu_info:
|
|
|
|
|
|
try:
|
|
|
|
|
|
brand = get_cpu_info().get("brand_raw", "")
|
|
|
|
|
|
info["brand"] = brand.split("@", maxsplit=1)[0].strip()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
info["brand"] = ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
info["load_avg"] = [round(float(x), 2) for x in psutil.getloadavg()]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
info["load_avg"] = None
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
def _memory_sync() -> dict:
|
2026-09-01 13:13:40 +08:00
|
|
|
|
mem = psutil.virtual_memory()
|
|
|
|
|
|
try:
|
|
|
|
|
|
proc = psutil.Process()
|
|
|
|
|
|
rss = proc.memory_info().rss
|
|
|
|
|
|
proc_mem_percent = proc.memory_percent()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
rss = 0
|
|
|
|
|
|
proc_mem_percent = 0.0
|
|
|
|
|
|
return {
|
|
|
|
|
|
"percent": round(float(mem.percent), 1),
|
|
|
|
|
|
"used": int(mem.used),
|
|
|
|
|
|
"total": int(mem.total),
|
|
|
|
|
|
"available": int(mem.available),
|
|
|
|
|
|
"process_rss": int(rss),
|
|
|
|
|
|
"process_percent": round(float(proc_mem_percent), 1),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
def _process_sync() -> dict:
|
2026-09-01 13:13:40 +08:00
|
|
|
|
try:
|
2026-09-14 18:59:01 +08:00
|
|
|
|
proc = _PROC
|
|
|
|
|
|
cpu = proc.cpu_percent(None)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
info = {
|
|
|
|
|
|
"pid": proc.pid,
|
|
|
|
|
|
"name": proc.name(),
|
|
|
|
|
|
"rss": int(proc.memory_info().rss),
|
|
|
|
|
|
"cpu_percent": round(float(cpu), 1),
|
|
|
|
|
|
"memory_percent": round(float(proc.memory_percent()), 1),
|
|
|
|
|
|
"create_time": int(proc.create_time()),
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"采集进程信息失败: {type(e).__name__}: {e}")
|
|
|
|
|
|
info = {}
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
def _network_sync() -> dict:
|
2026-09-03 00:44:38 +08:00
|
|
|
|
"""采集网络流量累计值 + 实时速率(bytes/s)。"""
|
|
|
|
|
|
global _NET_LAST
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
try:
|
|
|
|
|
|
io = psutil.net_io_counters(pernic=False)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"sent": 0,
|
|
|
|
|
|
"recv": 0,
|
|
|
|
|
|
"sent_rate": 0,
|
|
|
|
|
|
"recv_rate": 0,
|
|
|
|
|
|
"packets_sent": 0,
|
|
|
|
|
|
"packets_recv": 0,
|
|
|
|
|
|
"interfaces": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
sent = int(io.bytes_sent)
|
|
|
|
|
|
recv = int(io.bytes_recv)
|
|
|
|
|
|
sent_rate = recv_rate = 0.0
|
|
|
|
|
|
last = _NET_LAST
|
|
|
|
|
|
if last["time"] is not None and last["sent"] is not None:
|
|
|
|
|
|
dt = max(0.001, now - float(last["time"]))
|
|
|
|
|
|
sent_rate = max(0.0, (sent - int(last["sent"])) / dt)
|
|
|
|
|
|
recv_rate = max(0.0, (recv - int(last["recv"])) / dt)
|
|
|
|
|
|
_NET_LAST = {"time": now, "sent": sent, "recv": recv}
|
|
|
|
|
|
|
|
|
|
|
|
# 逐网卡明细(含 IP / 友好名称)
|
|
|
|
|
|
interfaces: list[dict] = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
pernic = psutil.net_io_counters(pernic=True)
|
|
|
|
|
|
addrs = psutil.net_if_addrs()
|
|
|
|
|
|
stats = psutil.net_if_stats()
|
|
|
|
|
|
for name, cnt in pernic.items():
|
|
|
|
|
|
if not cnt.bytes_sent and not cnt.bytes_recv:
|
|
|
|
|
|
continue
|
|
|
|
|
|
ips = []
|
|
|
|
|
|
for addr in addrs.get(name, []):
|
|
|
|
|
|
ip = (addr.address or "").strip()
|
|
|
|
|
|
# 只保留 IPv4(带点分十进制),忽略 MAC / IPv6
|
|
|
|
|
|
if ip and ":" not in ip and ip.count(".") >= 3:
|
|
|
|
|
|
ips.append(ip)
|
|
|
|
|
|
up = bool(stats.get(name).isup) if name in stats else True
|
|
|
|
|
|
interfaces.append({
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"up": up,
|
|
|
|
|
|
"sent": int(cnt.bytes_sent),
|
|
|
|
|
|
"recv": int(cnt.bytes_recv),
|
|
|
|
|
|
"ip": ", ".join(ips) if ips else "",
|
|
|
|
|
|
})
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
interfaces = []
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"sent": sent,
|
|
|
|
|
|
"recv": recv,
|
|
|
|
|
|
"sent_rate": round(sent_rate, 1),
|
|
|
|
|
|
"recv_rate": round(recv_rate, 1),
|
|
|
|
|
|
"packets_sent": int(io.packets_sent),
|
|
|
|
|
|
"packets_recv": int(io.packets_recv),
|
|
|
|
|
|
"interfaces": interfaces,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
def _backend_sync() -> dict:
|
2026-09-03 00:44:38 +08:00
|
|
|
|
"""BOT 后端信息:NoneBot 版本 / 驱动方式 / 装载插件数。"""
|
|
|
|
|
|
from nonebot import get_loaded_plugins
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
plugin_count = len(get_loaded_plugins())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
plugin_count = 0
|
|
|
|
|
|
info: dict = {
|
|
|
|
|
|
"nonebot_version": _nonebot_version(),
|
|
|
|
|
|
"plugin_count": plugin_count,
|
|
|
|
|
|
"python": f"{platform.python_implementation()} {platform.python_version()}",
|
|
|
|
|
|
"driver_type": "",
|
|
|
|
|
|
"driver": "",
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
from nonebot import get_driver
|
|
|
|
|
|
|
|
|
|
|
|
driver = get_driver()
|
|
|
|
|
|
info["driver_type"] = getattr(driver, "type", "") or ""
|
|
|
|
|
|
info["driver"] = getattr(driver.config, "driver", "") or ""
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-22 14:23:52 +08:00
|
|
|
|
def _is_private_ip(ip: str) -> bool:
|
|
|
|
|
|
"""RFC1918 私有网段(192.168/10/172.16-31),用于把局域网地址排在前面。"""
|
|
|
|
|
|
parts = ip.split(".")
|
|
|
|
|
|
if len(parts) != 4:
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
a, b = int(parts[0]), int(parts[1])
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return a == 10 or (a == 192 and b == 168) or (a == 172 and 16 <= b <= 31)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _local_ips() -> list[str]:
|
|
|
|
|
|
"""本机可被协议端访问的 IPv4 列表(私有网段优先,排除环回/链路本地)。"""
|
|
|
|
|
|
ips: list[str] = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
addrs = psutil.net_if_addrs()
|
|
|
|
|
|
stats = psutil.net_if_stats()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return ips
|
|
|
|
|
|
for name, addr_list in addrs.items():
|
|
|
|
|
|
st = stats.get(name)
|
|
|
|
|
|
if st is not None and not st.isup:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for addr in addr_list:
|
|
|
|
|
|
ip = (addr.address or "").strip()
|
|
|
|
|
|
# 只保留 IPv4(忽略 MAC / IPv6),排除环回与自动分配地址
|
|
|
|
|
|
if not ip or ":" in ip or ip.count(".") < 3:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if ip.startswith(("127.", "169.254.")):
|
|
|
|
|
|
continue
|
|
|
|
|
|
if ip not in ips:
|
|
|
|
|
|
ips.append(ip)
|
|
|
|
|
|
return sorted(ips, key=lambda ip: (not _is_private_ip(ip), ip))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ws_path() -> str:
|
|
|
|
|
|
"""反向 WS 路径:以驱动实际注册的 websocket 路由为准(跟随适配器版本)。"""
|
|
|
|
|
|
default = "/onebot/v11/ws"
|
|
|
|
|
|
try:
|
|
|
|
|
|
from nonebot import get_driver
|
|
|
|
|
|
|
|
|
|
|
|
routes = getattr(get_driver().server_app, "routes", None) or []
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return default
|
|
|
|
|
|
paths = [
|
|
|
|
|
|
str(getattr(route, "path", "") or "")
|
|
|
|
|
|
for route in routes
|
|
|
|
|
|
if "WebSocket" in type(route).__name__
|
|
|
|
|
|
]
|
|
|
|
|
|
paths = [p for p in paths if "/onebot/" in p]
|
|
|
|
|
|
if not paths:
|
|
|
|
|
|
return default
|
|
|
|
|
|
# 同一条路由注册了根路径与尾斜杠别名,优先规范路径 /onebot/v11/ws
|
|
|
|
|
|
return sorted(paths, key=lambda p: (not p.rstrip("/").endswith("/ws"), len(p)))[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _access_token() -> str:
|
|
|
|
|
|
"""反向 WS 授权令牌(OneBot V11 适配器配置;未设置时协议端无需带 Authorization)。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
for bot in get_bots().values():
|
|
|
|
|
|
adapter = getattr(bot, "adapter", None)
|
|
|
|
|
|
cfg = getattr(adapter, "onebot_config", None)
|
|
|
|
|
|
token = getattr(cfg, "onebot_access_token", None)
|
|
|
|
|
|
if token:
|
|
|
|
|
|
return str(token)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return os.environ.get("ONEBOT_V11_ACCESS_TOKEN", "") or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _endpoint_sync() -> dict:
|
|
|
|
|
|
"""协议端接入信息:监听地址 / 端口 / 反向 WS 路径 / 授权令牌 / 可用 IP。
|
|
|
|
|
|
|
|
|
|
|
|
供 Web 首页展示「协议端配对」信息,只读 driver.config 与驱动的路由表,不额外起服务。
|
|
|
|
|
|
"""
|
|
|
|
|
|
host = ""
|
|
|
|
|
|
port = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
from nonebot import get_driver
|
|
|
|
|
|
|
|
|
|
|
|
config = get_driver().config
|
|
|
|
|
|
host = str(getattr(config, "host", "") or "")
|
|
|
|
|
|
raw_port = getattr(config, "port", None)
|
|
|
|
|
|
port = int(raw_port) if raw_port else None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return {
|
|
|
|
|
|
"host": host,
|
|
|
|
|
|
"port": port,
|
|
|
|
|
|
"ws_path": _ws_path(),
|
|
|
|
|
|
"access_token": _access_token(),
|
|
|
|
|
|
"ips": _local_ips(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
def _system_sync() -> dict:
|
2026-09-01 13:13:40 +08:00
|
|
|
|
boot = psutil.boot_time()
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"hostname": platform.node(),
|
|
|
|
|
|
"os": platform.platform(),
|
|
|
|
|
|
"python": f"{platform.python_implementation()} {platform.python_version()}",
|
|
|
|
|
|
"nonebot": _nonebot_version(),
|
|
|
|
|
|
"boot_time": int(boot),
|
|
|
|
|
|
"system_uptime_seconds": int(now - boot),
|
|
|
|
|
|
"bot_uptime_seconds": int(now - _MODULE_START),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nonebot_version() -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
import nonebot
|
|
|
|
|
|
|
|
|
|
|
|
return nonebot.__version__
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:39:10 +08:00
|
|
|
|
def _disk_sync() -> list:
|
2026-09-01 13:13:40 +08:00
|
|
|
|
parts = []
|
|
|
|
|
|
for part in psutil.disk_partitions():
|
|
|
|
|
|
try:
|
|
|
|
|
|
usage = psutil.disk_usage(part.mountpoint)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
continue
|
|
|
|
|
|
parts.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"mount": part.mountpoint,
|
|
|
|
|
|
"device": part.device,
|
|
|
|
|
|
"percent": round(float(usage.percent), 1),
|
|
|
|
|
|
"used": int(usage.used),
|
|
|
|
|
|
"total": int(usage.total),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return parts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _collect_bots() -> list:
|
|
|
|
|
|
try:
|
|
|
|
|
|
bots = get_bots().values()
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(f"获取 bot 列表失败: {type(e).__name__}: {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for bot in bots:
|
|
|
|
|
|
adapter = getattr(bot, "adapter", None)
|
|
|
|
|
|
connections = getattr(adapter, "connections", {}) or {}
|
|
|
|
|
|
ws_connected = bool(connections.get(bot.self_id))
|
|
|
|
|
|
item = {
|
|
|
|
|
|
"self_id": bot.self_id,
|
|
|
|
|
|
"adapter": adapter.get_name() if adapter else "",
|
|
|
|
|
|
"nick": bot.self_id,
|
|
|
|
|
|
"avatar": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640",
|
|
|
|
|
|
"ws_connected": ws_connected,
|
|
|
|
|
|
"online": None,
|
|
|
|
|
|
"msg_recv": None,
|
|
|
|
|
|
"msg_sent": None,
|
|
|
|
|
|
}
|
|
|
|
|
|
if ws_connected:
|
2026-09-14 18:59:01 +08:00
|
|
|
|
cached = _LOGIN_CACHE.get(bot.self_id)
|
|
|
|
|
|
if cached and time.time() - cached[0] < _LOGIN_TTL:
|
|
|
|
|
|
item["nick"] = cached[1]
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
login = await bot.get_login_info()
|
|
|
|
|
|
item["nick"] = login.get("nickname") or item["nick"]
|
|
|
|
|
|
_LOGIN_CACHE[bot.self_id] = (time.time(), item["nick"])
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"获取登录信息失败({bot.self_id}): {type(e).__name__}: {e}"
|
|
|
|
|
|
)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
try:
|
|
|
|
|
|
status = await bot.get_status()
|
|
|
|
|
|
item["online"] = status.get("online")
|
|
|
|
|
|
stat = status.get("stat") or {}
|
|
|
|
|
|
item["msg_recv"] = stat.get("message_received") or stat.get(
|
|
|
|
|
|
"MessageReceived"
|
|
|
|
|
|
)
|
|
|
|
|
|
item["msg_sent"] = stat.get("message_sent") or stat.get("MessageSent")
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"获取连接状态失败({bot.self_id}): {type(e).__name__}: {e}"
|
|
|
|
|
|
)
|
|
|
|
|
|
result.append(item)
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def collect_dashboard() -> dict:
|
2026-09-03 15:39:10 +08:00
|
|
|
|
"""聚合采集,失败字段降级,不阻塞整体返回。
|
|
|
|
|
|
|
|
|
|
|
|
同步采集整体放线程池(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"]
|
2026-09-22 14:23:52 +08:00
|
|
|
|
cpu, memory, process, system, disk, network, backend, endpoint, 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),
|
|
|
|
|
|
asyncio.to_thread(_endpoint_sync),
|
|
|
|
|
|
_collect_bots(), # 需要走 bot API,必须留在事件循环
|
|
|
|
|
|
)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
)
|
2026-09-03 15:39:10 +08:00
|
|
|
|
data = {
|
2026-09-01 13:13:40 +08:00
|
|
|
|
"bots": bots,
|
|
|
|
|
|
"system": system,
|
|
|
|
|
|
"cpu": cpu,
|
|
|
|
|
|
"memory": memory,
|
|
|
|
|
|
"process": process,
|
|
|
|
|
|
"disk": disk,
|
2026-09-03 00:44:38 +08:00
|
|
|
|
"network": network,
|
|
|
|
|
|
"backend": backend,
|
2026-09-22 14:23:52 +08:00
|
|
|
|
"endpoint": endpoint,
|
2026-09-01 13:13:40 +08:00
|
|
|
|
}
|
2026-09-03 15:39:10 +08:00
|
|
|
|
_DASH_CACHE["time"] = now
|
|
|
|
|
|
_DASH_CACHE["data"] = data
|
|
|
|
|
|
return data
|