Files
HeXi/hexi/plugins/nonebot_plugin_web_hub/dashboard.py
T
sansenhoshiandClaude 9371a28e35 refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to
Trigger(handlers) → Service(services) → Model(repository/models) + utils):

- Split monolithic __init__.py into handlers/services/utils across
  dailywife, deer_pipe, dice, galgame_card, helldivers_tools,
  huoziyinshua, learning_chat, makeaquote, mc_server_status,
  ncm_saying, picfinder_take, picstatus, random_jm_code, regif,
  steam_info, video_analysis, group_tools
- Move static assets under res/: deer_pipe font/img, makeaquote font,
  helldivers img/templates, huoziyinshua HuoZiYinShua
- Add config.py + register_config_items to ncm_saying, random_jm_code,
  group_tools; learning_chat unified config bridge
- Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in
  dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp
- Disable brash_general_supercredits_tools (stub comment only)
- bot.py: optional stdout/stderr redirect to log file for Web log viewer,
  force ANSI colorize on non-TTY sinks
- Move runtime data (jm_code.json) out of plugin dir into hexi/data
- Docs: plugin-audit-report.md; README reflects removed plugins

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-03 00:44:38 +08:00

295 lines
9.1 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()
# 网络速率采样:记录上一次累计计数与时间,用于计算实时 bytes/s 用量
_NET_LAST: dict = {"time": None, "sent": None, "recv": None}
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_network() -> dict:
"""采集网络流量累计值 + 实时速率(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,
}
async def _collect_backend() -> dict:
"""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
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, network, backend = await asyncio.gather(
_collect_cpu(),
_collect_memory(),
_collect_process(),
_collect_system(),
_collect_disk(),
_collect_bots(),
_collect_network(),
_collect_backend(),
)
return {
"bots": bots,
"system": system,
"cpu": cpu,
"memory": memory,
"process": process,
"disk": disk,
"network": network,
"backend": backend,
}