Files
NeoBot/plugins/bot_status.py
K2cr2O1 d458413e4b feat: 添加状态监控插件和Redis原子操作支持
- 新增 `/status` 指令,展示机器人运行状态和系统指标
- 实现Redis Lua脚本支持原子化计数器操作
- 添加消息收发统计功能
- 完善文档,包括插件开发和性能优化指南
- 重构WebSocket连接池,增加健康检查机制
- 移除旧版编译脚本,优化项目结构
2026-01-23 15:54:45 +08:00

144 lines
5.5 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.
"""
Bot 状态查询插件
提供 /status 指令,以图片形式展示机器人当前的综合运行状态。
"""
import os
import psutil
import time
from datetime import datetime, timedelta
from core.bot import Bot
from core.managers.command_manager import matcher
from core.managers.image_manager import image_manager
from core.managers.redis_manager import redis_manager
from core.utils.executor import run_in_thread_pool
from core.utils.logger import logger
from models.events.message import MessageEvent, MessageSegment
from models.objects import LoginInfo, Status, VersionInfo
__plugin_meta__ = {
"name": "bot_status",
"description": "以图片形式展示机器人当前的综合运行状态",
"usage": "/status 或 /状态",
}
# 记录机器人启动时间
START_TIME = time.time()
# 获取当前进程
PROCESS = psutil.Process(os.getpid())
def _get_system_info():
"""
同步函数:使用 psutil 获取系统信息,避免阻塞事件循环。
"""
# interval=1 会阻塞1秒必须在线程池中运行
cpu_percent = psutil.cpu_percent(interval=1)
mem_info = psutil.virtual_memory()
bot_mem_mb = PROCESS.memory_info().rss / (1024 * 1024)
return {
"cpu_percent": f"{cpu_percent:.1f}",
"mem_percent": f"{mem_info.percent:.1f}",
"bot_mem_mb": f"{bot_mem_mb:.2f}",
}
@matcher.command("status", "状态")
async def handle_status(bot: Bot, event: MessageEvent, args: list[str]):
"""
处理 status 指令,生成并回复机器人状态图片。
"""
logger.info(f"收到用户 {event.user_id} 的状态查询指令,开始生成状态图...")
try:
# 1. 获取API信息 (增加独立错误处理)
try:
# 优先使用 get_stranger_info 获取自身信息,比 get_login_info 更轻量
stranger_info = await bot.get_stranger_info(user_id=bot.self_id)
nickname = stranger_info.nickname
except Exception as e:
logger.warning(f"获取 stranger_info 失败: {e}, 将回退到 login_info")
try:
login_info = await bot.get_login_info()
nickname = login_info.nickname
except Exception as e2:
logger.warning(f"获取 login_info 也失败了: {e2}")
nickname = "获取失败"
# 状态信息:如果能响应此命令,说明机器人必然在线且状态良好
# 这避免了依赖可能超时或未实现的 get_status API
logger.debug("正在推断机器人状态...")
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. 获取统计数据
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
)
# 4. 异步获取系统信息
system_data = await run_in_thread_pool(_get_system_info)
# 5. 准备模板所需的所有数据
template_data = {
"bot_info": bot_info_data,
"status_info": status_info,
"version_info": version_info,
"stats": stats_data,
"system": system_data,
"command_stats": command_stats_data,
}
# 6. 渲染图片
base64_str = await image_manager.render_template_to_base64(
template_name="status.html",
data=template_data,
output_name="status.png",
image_type="png"
)
if base64_str:
await event.reply(MessageSegment.image(base64_str))
else:
# 如果渲染失败image_manager 内部会记录错误,这里给用户一个通用提示
await event.reply("状态图片生成失败,可能是渲染服务出现问题,请联系管理员。")
except Exception as e:
logger.exception(f"生成状态图时发生意外错误, 用户: {event.user_id}")
await event.reply(f"获取状态信息时发生未知错误,请稍后再试或联系管理员。")