feat(status): 优化状态查询功能,增加系统、网络和操作系统信息展示

This commit is contained in:
2026-02-01 23:55:00 +08:00
parent f2bf1e4ca5
commit 7666ef42a8
2 changed files with 362 additions and 121 deletions

View File

@@ -7,7 +7,11 @@ import os
import psutil import psutil
import time import time
import asyncio import asyncio
import socket
import platform
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import lru_cache
from typing import Optional
from core.bot import Bot from core.bot import Bot
from core.managers.command_manager import matcher from core.managers.command_manager import matcher
@@ -16,7 +20,7 @@ from core.managers.redis_manager import redis_manager
from core.utils.executor import run_in_thread_pool from core.utils.executor import run_in_thread_pool
from core.utils.logger import logger from core.utils.logger import logger
from models.events.message import MessageEvent, MessageSegment from models.events.message import MessageEvent, MessageSegment
from models.objects import LoginInfo, Status, VersionInfo from models.objects import Status, VersionInfo
__plugin_meta__ = { __plugin_meta__ = {
"name": "bot_status", "name": "bot_status",
@@ -28,14 +32,17 @@ __plugin_meta__ = {
START_TIME = time.time() START_TIME = time.time()
# 获取当前进程 # 获取当前进程
PROCESS = psutil.Process(os.getpid()) PROCESS = psutil.Process(os.getpid())
# 缓存bot昵称12小时过期
_nickname_cache: dict[str, tuple[str, float]] = {}
def _get_system_info(): def _get_system_info():
""" """
同步函数:使用 psutil 获取系统信息,避免阻塞事件循环。 同步函数:使用 psutil 获取系统信息,避免阻塞事件循环。
优化:使用 interval=None 获取自上次调用以来的平均 CPU 使用率
""" """
try: try:
# interval=1 会阻塞1秒必须在线程池中运行 # interval=None 会返回自上次调用以来的平均值,不会阻塞
cpu_percent = psutil.cpu_percent(interval=1) cpu_percent = psutil.cpu_percent(interval=None)
mem_info = psutil.virtual_memory() mem_info = psutil.virtual_memory()
bot_mem_mb = PROCESS.memory_info().rss / (1024 * 1024) bot_mem_mb = PROCESS.memory_info().rss / (1024 * 1024)
@@ -45,159 +52,333 @@ def _get_system_info():
# 网络信息 # 网络信息
net_io = psutil.net_io_counters() net_io = psutil.net_io_counters()
# 进程数
process_count = len(psutil.pids())
# CPU核心数
cpu_count = psutil.cpu_count(logical=True)
cpu_count_physical = psutil.cpu_count(logical=False)
return { return {
"cpu_percent": f"{cpu_percent:.1f}", "cpu_percent": f"{cpu_percent:.1f}",
"cpu_count": cpu_count,
"cpu_count_physical": cpu_count_physical,
"mem_percent": f"{mem_info.percent:.1f}", "mem_percent": f"{mem_info.percent:.1f}",
"mem_total": f"{mem_info.total / (1024**3):.1f}", "mem_total": f"{mem_info.total / (1024**3):.1f}",
"mem_used": f"{mem_info.used / (1024**3):.1f}", "mem_used": f"{mem_info.used / (1024**3):.1f}",
"mem_available": f"{mem_info.available / (1024**3):.1f}",
"bot_mem_mb": f"{bot_mem_mb:.2f}", "bot_mem_mb": f"{bot_mem_mb:.2f}",
"disk_percent": f"{disk_usage.percent:.1f}", "disk_percent": f"{disk_usage.percent:.1f}",
"disk_total": f"{disk_usage.total / (1024**3):.1f}", "disk_total": f"{disk_usage.total / (1024**3):.1f}",
"disk_used": f"{disk_usage.used / (1024**3):.1f}", "disk_used": f"{disk_usage.used / (1024**3):.1f}",
"disk_free": f"{disk_usage.free / (1024**3):.1f}",
"net_sent": f"{net_io.bytes_sent / (1024**2):.1f}", "net_sent": f"{net_io.bytes_sent / (1024**2):.1f}",
"net_recv": f"{net_io.bytes_recv / (1024**2):.1f}", "net_recv": f"{net_io.bytes_recv / (1024**2):.1f}",
"process_count": process_count,
} }
except Exception as e: except Exception as e:
logger.error(f"获取系统信息失败: {e}") logger.error(f"获取系统信息失败: {e}")
return _create_error_system_info("N/A")
async def _get_bot_nickname(bot: Bot) -> str:
"""
异步获取bot昵称带缓存机制12小时过期
"""
cache_key = f"bot_{bot.self_id}"
now = time.time()
# 检查缓存是否有效
if cache_key in _nickname_cache:
nickname, timestamp = _nickname_cache[cache_key]
if now - timestamp < 43200: # 12小时
return nickname
# 优先使用 get_stranger_info更轻量
try:
stranger_info = await bot.get_stranger_info(user_id=bot.self_id)
nickname = stranger_info.nickname
except Exception:
try:
login_info = await bot.get_login_info()
nickname = login_info.nickname
except Exception as e:
logger.warning(f"获取bot昵称失败: {e}")
nickname = "获取失败"
_nickname_cache[cache_key] = (nickname, now)
return nickname
async def _get_bot_info(bot: Bot, start_time: float) -> dict:
"""
收集bot信息id、昵称、头像、启动时间等
"""
nickname = await _get_bot_nickname(bot)
uptime_seconds = int(time.time() - start_time)
uptime_delta = timedelta(seconds=uptime_seconds)
days = uptime_delta.days
hours, remainder = divmod(uptime_delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}{hours:02}:{minutes:02}:{seconds:02}"
return {
"user_id": bot.self_id,
"nickname": nickname,
"avatar_url": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640",
"start_time": datetime.fromtimestamp(start_time).strftime("%Y-%m-%d %H:%M:%S"),
"uptime": uptime_str,
}
async def _get_version_info(bot: Bot) -> dict:
"""
获取版本信息,失败时返回默认值
"""
try:
version_info = await bot.get_version_info()
return { return {
"cpu_percent": "N/A", "app_name": version_info.app_name,
"mem_percent": "N/A", "app_version": version_info.app_version,
"mem_total": "N/A", "protocol_version": version_info.protocol_version,
"mem_used": "N/A", }
"bot_mem_mb": "N/A", except Exception as e:
"disk_percent": "N/A", logger.warning(f"获取版本信息失败: {e}")
"disk_total": "N/A", return {
"disk_used": "N/A", "app_name": "获取失败",
"net_sent": "N/A", "app_version": "N/A",
"net_recv": "N/A", "protocol_version": "N/A",
}
async def _get_stats(redis_manager) -> tuple[dict, list]:
"""
获取统计数据和命令排行
"""
try:
msgs_recv = await redis_manager.get("neobot:stats:messages_received") or 0
msgs_sent = await redis_manager.get("neobot:stats:messages_sent") or 0
command_stats_raw = await redis_manager.redis.hgetall("neobot:command_stats")
total_commands = sum(int(v) for v in command_stats_raw.values()) if command_stats_raw else 0
stats_data = {
"messages_received": int(msgs_recv),
"messages_sent": int(msgs_sent),
"total_commands": total_commands,
}
command_stats_data = sorted(
[{"name": k, "count": int(v)} for k, v in command_stats_raw.items()],
key=lambda x: x["count"],
reverse=True
) if command_stats_raw else []
return stats_data, command_stats_data
except Exception as e:
logger.error(f"获取统计数据失败: {e}")
return {
"messages_received": 0,
"messages_sent": 0,
"total_commands": 0,
}, []
async def _get_system_info_async(timeout: float = 3.0) -> dict:
"""
异步获取系统信息,带超时控制
"""
try:
system_data = await asyncio.wait_for(
run_in_thread_pool(_get_system_info),
timeout=timeout
)
return system_data
except asyncio.TimeoutError:
logger.error("获取系统信息超时")
return _create_error_system_info("Timeout")
except Exception as e:
logger.error(f"获取系统信息异常: {e}")
return _create_error_system_info("Error")
async def _get_network_info_async() -> dict:
"""
异步获取网络信息
"""
try:
return await asyncio.wait_for(
run_in_thread_pool(_get_network_info),
timeout=2.0
)
except Exception as e:
logger.error(f"获取网络信息异常: {e}")
return {
"hostname": "获取失败",
"local_ip": "获取失败",
"public_ip": "获取失败",
}
async def _get_os_info_async() -> dict:
"""
异步获取操作系统信息
"""
try:
return await asyncio.wait_for(
run_in_thread_pool(_get_os_info),
timeout=2.0
)
except Exception as e:
logger.error(f"获取操作系统信息异常: {e}")
return {
"os_name": "获取失败",
"os_version": "获取失败",
"os_arch": "获取失败",
"python_version": "获取失败",
}
def _create_error_system_info(error_msg: str = "N/A") -> dict:
"""
创建错误状态的系统信息字典
"""
return {
"cpu_percent": error_msg,
"cpu_count": error_msg,
"cpu_count_physical": error_msg,
"mem_percent": error_msg,
"mem_total": error_msg,
"mem_used": error_msg,
"mem_available": error_msg,
"bot_mem_mb": error_msg,
"disk_percent": error_msg,
"disk_total": error_msg,
"disk_used": error_msg,
"disk_free": error_msg,
"net_sent": error_msg,
"net_recv": error_msg,
"process_count": error_msg,
}
def _get_network_info():
"""
获取网络信息IP地址、主机名等
"""
try:
hostname = socket.gethostname()
# 获取本地IP
try:
local_ip = socket.gethostbyname(hostname)
except:
local_ip = "获取失败"
# 尝试获取公网IP通过连接外部DNS
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
public_ip = s.getsockname()[0]
s.close()
except:
public_ip = "无法获取"
return {
"hostname": hostname,
"local_ip": local_ip,
"public_ip": public_ip,
}
except Exception as e:
logger.error(f"获取网络信息失败: {e}")
return {
"hostname": "获取失败",
"local_ip": "获取失败",
"public_ip": "获取失败",
}
def _get_os_info():
"""
获取操作系统信息
"""
try:
os_name = platform.system()
os_version = platform.release()
os_arch = platform.machine()
python_version = platform.python_version()
return {
"os_name": os_name,
"os_version": os_version,
"os_arch": os_arch,
"python_version": python_version,
}
except Exception as e:
logger.error(f"获取操作系统信息失败: {e}")
return {
"os_name": "获取失败",
"os_version": "获取失败",
"os_arch": "获取失败",
"python_version": "获取失败",
} }
@matcher.command("status", "状态") @matcher.command("status", "状态")
async def handle_status(bot: Bot, event: MessageEvent, args: list[str]): async def handle_status(bot: Bot, event: MessageEvent, args: list[str]):
""" """
处理 status 指令,生成并回复机器人状态图片。 处理 status 指令,生成并回复机器人状态图片。
优化:并发获取各项数据,提升响应速度
""" """
logger.info(f"收到用户 {event.user_id} 的状态查询指令,开始生成状态图...") logger.info(f"收到用户 {event.user_id} 的状态查询指令,开始生成状态图...")
try: try:
# 1. 获取API信息 (增加独立错误处理) # 并发获取所有数据,提升性能
# 尝试获取或更新 bot.nickname bot_info, version_info, stats_result, system_data, network_info, os_info = await asyncio.gather(
if not hasattr(bot, "nickname") or not bot.nickname: _get_bot_info(bot, START_TIME),
try: _get_version_info(bot),
# 优先使用 get_stranger_info 获取自身信息,比 get_login_info 更轻量 _get_stats(redis_manager),
stranger_info = await bot.get_stranger_info(user_id=bot.self_id) _get_system_info_async(timeout=3.0),
bot.nickname = stranger_info.nickname _get_network_info_async(),
except Exception as e: _get_os_info_async(),
logger.warning(f"获取 stranger_info 失败: {e}, 将回退到 login_info") return_exceptions=False
try: )
login_info = await bot.get_login_info()
bot.nickname = login_info.nickname
except Exception as e2:
logger.warning(f"获取 login_info 也失败了: {e2}")
bot.nickname = "获取失败"
nickname = bot.nickname # 处理 _get_stats 返回的元组
if isinstance(stats_result, Exception):
logger.error(f"获取统计数据失败: {stats_result}")
stats_data, command_stats_data = {"messages_received": 0, "messages_sent": 0, "total_commands": 0}, []
else:
stats_data, command_stats_data = stats_result
# 处理异常返回值
if isinstance(system_data, Exception):
logger.error(f"获取系统信息失败: {system_data}")
system_data = _create_error_system_info("Error")
if isinstance(network_info, Exception):
logger.error(f"获取网络信息失败: {network_info}")
network_info = {
"hostname": "获取失败",
"local_ip": "获取失败",
"public_ip": "获取失败",
}
if isinstance(os_info, Exception):
logger.error(f"获取操作系统信息失败: {os_info}")
os_info = {
"os_name": "获取失败",
"os_version": "获取失败",
"os_arch": "获取失败",
"python_version": "获取失败",
}
# 状态信息:如果能响应此命令说明机器人必然在线且状态良好 # 推断机器人状态(能响应此命令说明在线且状态良好
# 这避免了依赖可能超时或未实现的 get_status API
logger.debug("正在推断机器人状态...")
status_info = Status(online=True, good=True) status_info = Status(online=True, good=True)
logger.debug(f"推断状态成功: online={status_info.online}, good={status_info.good}")
try: # 准备模板数据
version_info = await bot.get_version_info()
except Exception as e:
logger.warning(f"获取 version_info 失败: {e}")
version_info = VersionInfo(app_name="获取失败", app_version="N/A", protocol_version="N/A")
# 2. 计算运行时长
uptime_seconds = int(time.time() - START_TIME)
uptime_delta = timedelta(seconds=uptime_seconds)
days = uptime_delta.days
hours, remainder = divmod(uptime_delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}{hours:02}:{minutes:02}:{seconds:02}"
bot_info_data = {
"user_id": bot.self_id,
"nickname": nickname,
"avatar_url": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640",
"start_time": datetime.fromtimestamp(START_TIME).strftime("%Y-%m-%d %H:%M:%S"),
"uptime": uptime_str,
}
# 3. 获取统计数据
try:
msgs_recv = await redis_manager.get("neobot:stats:messages_received") or 0
msgs_sent = await redis_manager.get("neobot:stats:messages_sent") or 0
command_stats_raw = await redis_manager.redis.hgetall("neobot:command_stats")
total_commands = sum(int(v) for v in command_stats_raw.values())
stats_data = {
"messages_received": int(msgs_recv),
"messages_sent": int(msgs_sent),
"total_commands": total_commands,
}
command_stats_data = sorted(
[{"name": k, "count": int(v)} for k, v in command_stats_raw.items()],
key=lambda x: x["count"],
reverse=True
)
except Exception as e:
logger.error(f"获取Redis统计数据失败: {e}")
stats_data = {
"messages_received": 0,
"messages_sent": 0,
"total_commands": 0,
}
command_stats_data = []
# 4. 异步获取系统信息
# 设置超时,防止 psutil 阻塞过久
try:
system_data = await asyncio.wait_for(run_in_thread_pool(_get_system_info), timeout=5.0)
except asyncio.TimeoutError:
logger.error("获取系统信息超时")
system_data = {
"cpu_percent": "Timeout",
"mem_percent": "Timeout",
"mem_total": "Timeout",
"mem_used": "Timeout",
"bot_mem_mb": "Timeout",
"disk_percent": "Timeout",
"disk_total": "Timeout",
"disk_used": "Timeout",
"net_sent": "Timeout",
"net_recv": "Timeout",
}
except Exception as e:
logger.error(f"获取系统信息异常: {e}")
system_data = {
"cpu_percent": "Error",
"mem_percent": "Error",
"mem_total": "Error",
"mem_used": "Error",
"bot_mem_mb": "Error",
"disk_percent": "Error",
"disk_total": "Error",
"disk_used": "Error",
"net_sent": "Error",
"net_recv": "Error",
}
# 5. 准备模板所需的所有数据
template_data = { template_data = {
"bot_info": bot_info_data, "bot_info": bot_info,
"status_info": status_info, "status_info": status_info,
"version_info": version_info, "version_info": version_info,
"stats": stats_data, "stats": stats_data,
"system": system_data, "system": system_data,
"network": network_info,
"os": os_info,
"command_stats": command_stats_data, "command_stats": command_stats_data,
} }
# 6. 渲染图片 # 渲染图片
try: try:
base64_str = await image_manager.render_template_to_base64( base64_str = await image_manager.render_template_to_base64(
template_name="status.html", template_name="status.html",
@@ -209,8 +390,7 @@ async def handle_status(bot: Bot, event: MessageEvent, args: list[str]):
if base64_str: if base64_str:
await event.reply(MessageSegment.image(base64_str)) await event.reply(MessageSegment.image(base64_str))
else: else:
# 如果渲染失败image_manager 内部会记录错误,这里给用户一个通用提示 await event.reply("状态图片生成失败,请稍后重试或联系管理员。")
await event.reply("状态图片生成失败,可能是渲染服务出现问题,请联系管理员。")
except Exception as e: except Exception as e:
logger.error(f"渲染图片失败: {e}") logger.error(f"渲染图片失败: {e}")
await event.reply("状态图片渲染过程中发生错误。") await event.reply("状态图片渲染过程中发生错误。")

View File

@@ -141,6 +141,11 @@
column-count: 2; column-count: 2;
column-gap: 24px; column-gap: 24px;
} }
.multi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.footer { .footer {
margin-top: auto; margin-top: auto;
padding: 32px 40px; padding: 32px 40px;
@@ -221,10 +226,18 @@
<span class="info-label">CPU 占用</span> <span class="info-label">CPU 占用</span>
<span class="info-value">{{ system.cpu_percent }}%</span> <span class="info-value">{{ system.cpu_percent }}%</span>
</li> </li>
<li class="info-item">
<span class="info-label">CPU 核心</span>
<span class="info-value">{{ system.cpu_count }} ({{ system.cpu_count_physical }} 物理)</span>
</li>
<li class="info-item"> <li class="info-item">
<span class="info-label">内存占用</span> <span class="info-label">内存占用</span>
<span class="info-value">{{ system.mem_percent }}% ({{ system.mem_used }}G / {{ system.mem_total }}G)</span> <span class="info-value">{{ system.mem_percent }}% ({{ system.mem_used }}G / {{ system.mem_total }}G)</span>
</li> </li>
<li class="info-item">
<span class="info-label">可用内存</span>
<span class="info-value">{{ system.mem_available }}G</span>
</li>
<li class="info-item"> <li class="info-item">
<span class="info-label">Bot 进程内存</span> <span class="info-label">Bot 进程内存</span>
<span class="info-value">{{ system.bot_mem_mb }} MB</span> <span class="info-value">{{ system.bot_mem_mb }} MB</span>
@@ -233,10 +246,18 @@
<span class="info-label">磁盘占用</span> <span class="info-label">磁盘占用</span>
<span class="info-value">{{ system.disk_percent }}% ({{ system.disk_used }}G / {{ system.disk_total }}G)</span> <span class="info-value">{{ system.disk_percent }}% ({{ system.disk_used }}G / {{ system.disk_total }}G)</span>
</li> </li>
<li class="info-item">
<span class="info-label">磁盘可用</span>
<span class="info-value">{{ system.disk_free }}G</span>
</li>
<li class="info-item"> <li class="info-item">
<span class="info-label">网络流量 (↑/↓)</span> <span class="info-label">网络流量 (↑/↓)</span>
<span class="info-value">{{ system.net_sent }}MB / {{ system.net_recv }}MB</span> <span class="info-value">{{ system.net_sent }}MB / {{ system.net_recv }}MB</span>
</li> </li>
<li class="info-item">
<span class="info-label">进程总数</span>
<span class="info-value">{{ system.process_count }}</span>
</li>
</ul> </ul>
</div> </div>
@@ -257,6 +278,46 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="status-card">
<h2 class="card-title">网络信息 (Network)</h2>
<ul class="info-list">
<li class="info-item">
<span class="info-label">主机名</span>
<span class="info-value">{{ network.hostname }}</span>
</li>
<li class="info-item">
<span class="info-label">本地 IP</span>
<span class="info-value">{{ network.local_ip }}</span>
</li>
<li class="info-item">
<span class="info-label">公网 IP</span>
<span class="info-value">{{ network.public_ip }}</span>
</li>
</ul>
</div>
<div class="status-card">
<h2 class="card-title">操作系统 (OS)</h2>
<ul class="info-list">
<li class="info-item">
<span class="info-label">系统</span>
<span class="info-value">{{ os.os_name }}</span>
</li>
<li class="info-item">
<span class="info-label">版本</span>
<span class="info-value">{{ os.os_version }}</span>
</li>
<li class="info-item">
<span class="info-label">架构</span>
<span class="info-value">{{ os.os_arch }}</span>
</li>
<li class="info-item">
<span class="info-label">Python 版本</span>
<span class="info-value">{{ os.python_version }}</span>
</li>
</ul>
</div>
</div> </div>
<div class="status-card"> <div class="status-card">