Files
HeXi/hexi/plugins/nonebot_plugin_web_hub/dashboard.py
T
sansenhoshiandClaude b61d09f09f 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>
2026-09-01 13:13:40 +08:00

199 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""统一 Web 管理台 Dashboard 数据采集。
数据来源与 nonebot_plugin_picstatus 类似:
- bot 账号 / WS 连通性:从 OneBot V11 适配器的 bots / connections 读取
- host 性能:psutil(CPU/内存/磁盘/进程)+ platform + nonebot 版本
仅在 /hub/api/dashboard 被调用时执行,不做常驻采样。
"""
from __future__ import annotations
import asyncio
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()
async def _collect_cpu() -> dict:
try:
per_core = await asyncio.to_thread(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)
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
async def _collect_memory() -> dict:
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),
}
async def _collect_process() -> dict:
try:
proc = psutil.Process()
cpu = await asyncio.to_thread(proc.cpu_percent, 0.2)
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
async def _collect_system() -> dict:
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 ""
async def _collect_disk() -> list:
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:
try:
login = await bot.get_login_info()
item["nick"] = login.get("nickname") or item["nick"]
except Exception as e: # noqa: BLE001
logger.warning(
f"获取登录信息失败({bot.self_id}): {type(e).__name__}: {e}"
)
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:
"""聚合采集,失败字段降级,不阻塞整体返回。"""
cpu, memory, process, system, disk, bots = await asyncio.gather(
_collect_cpu(),
_collect_memory(),
_collect_process(),
_collect_system(),
_collect_disk(),
_collect_bots(),
)
return {
"bots": bots,
"system": system,
"cpu": cpu,
"memory": memory,
"process": process,
"disk": disk,
}