"""LinuxAwesomeBot 入口:启动 OneBot 反向 WS 服务端 + 命令处理 + 高负载告警。""" import asyncio import logging import sys import monitor from alerter import Alerter from onebot import OneBotServer logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", datefmt="%H:%M:%S", ) log = logging.getLogger("bot") # ---------- 配置 ---------- def load_config() -> dict: import tomllib import shutil try: with open("config.toml", "rb") as f: return tomllib.load(f) except FileNotFoundError: shutil.copy("config.example.toml", "config.toml") log.warning("未找到 config.toml,已从模板生成,请修改后再运行(先退出)") sys.exit(1) # ---------- 消息格式化 ---------- def _fmt_uptime(seconds: float) -> str: d, r = divmod(int(seconds), 86400) h, r = divmod(r, 3600) m = r // 60 return f"{d}天{h}小时{m}分" if d else f"{h}小时{m}分" def fmt_status(s: dict) -> str: p, c, m = s["power"], s["cpu"], s["memory"] lines = ["📊 服务器状态"] power_str = "不支持" if p["total_watts"] is not None: src = {"battery": "🔋电池", "rapl": "RAPL"}.get(p["source"], p["source"]) extra = f" (电池 {p['battery']}W)" if p["battery"] is not None and p["source"] == "rapl" else "" power_str = f"{p['total_watts']}W [{src}]{extra}" lines.append(f"⚡ 功耗: {power_str}") load = c["load"] lines.append(f"🖥 CPU: {c['percent']}% 核心 {c['cores']} 负载 {load[0]:.2f}/{load[1]:.2f}/{load[2]:.2f}") lines.append(f"💾 内存: {m['used_gb']}/{m['total_gb']}GB ({m['percent']}%) Swap {m['swap_percent']}%") if s["temps"]: flat = [t for entries in s["temps"].values() for t in entries] hottest = max(flat, key=lambda t: t["current"]) lines.append(f"🌡 温度: 最高 {hottest['label']} {hottest['current']}°C") if s["net"]["up_mbps"] is not None: lines.append(f"🌐 网络: ↑{s['net']['up_mbps']}Mbps ↓{s['net']['down_mbps']}Mbps") lines.append(f"⏱ 运行: {_fmt_uptime(s['uptime_s'])}") if s["disk"]: d = s["disk"][0] lines.append(f"🗄 磁盘 {d['mount']}: {d['used_gb']}/{d['total_gb']}GB ({d['percent']}%)") return "\n".join(lines) def fmt_power(s: dict) -> str: p = s["power"] if p["total_watts"] is None and p["battery"] is None: return ("⚡ 未获取到功耗数据。\n" "支持的来源:笔记本电池 /sys/class/power_supply," "Intel RAPL /sys/class/powercap(AMD/部分虚拟机不支持)。") lines = ["⚡ 功耗详情"] if p["battery"] is not None: lines.append(f"🔋 电池功率: {p['battery']}W") if p["domains"]: lines.append("RAPL 域:") for name, watts in p["domains"].items(): lines.append(f" {name}: {watts}W") return "\n".join(lines) def fmt_top(s: dict) -> str: if not s["top"]: return "🔝 未获取到进程数据" lines = ["🔝 CPU 占用 TOP10:"] for i, p in enumerate(s["top"], 1): lines.append(f"{i}. {p['name']} (PID {p['pid']}) CPU {p['cpu']}% 内存 {p['mem_mb']}MB") return "\n".join(lines) def fmt_temps(s: dict) -> str: if not s["temps"]: return "🌡 未获取到温度数据(需要 lm-sensors 或 psutil 支持,虚拟机通常不支持)" lines = ["🌡 温度:"] for name, entries in s["temps"].items(): for t in entries: lines.append(f" {name}/{t['label']}: {t['current']}°C") return "\n".join(lines) HELP = """📖 可用命令(/ 或 ! 前缀均可): /status 状态 - 总览(功耗/CPU/内存/温度/网络) /power 功耗 - 功耗详情(RAPL/电池) /top 进程 - CPU TOP10 进程 /temp 温度 - 传感器温度 /net 网络 - 网络速率 /help 帮助 - 本帮助""" # ---------- 命令处理 ---------- class Bot: def __init__(self, cfg: dict): self.cfg = cfg self.ob = OneBotServer( cfg["server"]["host"], cfg["server"]["port"], cfg["server"].get("access_token", ""), ) async def on_event(self, ev: dict): if ev.get("post_type") != "message": return if ev.get("message_type") == "group": text = self._extract_text(ev.get("message")) # 群聊需 @ 或带前缀 if not self._is_command(text) and not self._at_me(ev.get("message")): return elif ev.get("message_type") == "private": text = self._extract_text(ev.get("message")) else: return reply = await self.handle_command(text) if reply is None: return if ev.get("message_type") == "group": await self.ob.send_group_msg(ev["group_id"], reply) else: await self.ob.send_private_msg(ev["user_id"], reply) @staticmethod def _extract_text(message) -> str: if isinstance(message, str): return message.strip() if isinstance(message, list): # CQ 段数组 return "".join( seg.get("data", {}).get("text", "") for seg in message if isinstance(seg, dict) and seg.get("type") == "text" ).strip() return "" @staticmethod def _at_me(message) -> bool: if isinstance(message, list): return any( seg.get("type") == "at" for seg in message if isinstance(seg, dict) ) return "[CQ:at" in (message or "") @staticmethod def _is_command(text: str) -> bool: return text.startswith("/") or text.startswith("!") async def handle_command(self, text: str) -> str | None: cmd = text.lstrip("/!").strip().lower() cmd = cmd.split()[0] if cmd else "" try: if cmd in ("status", "状态", ""): return fmt_status(monitor.get_status()) if cmd in ("power", "功耗"): return fmt_power({"power": monitor.get_power()}) if cmd in ("top", "进程"): return fmt_top({"top": monitor.get_top_processes(10)}) if cmd in ("temp", "温度"): return fmt_temps({"temps": monitor.get_temps()}) if cmd in ("net", "网络"): net = monitor.get_net(interval=1.0) return f"🌐 网络速率: ↑{net['up_mbps']}Mbps ↓{net['down_mbps']}Mbps" if cmd in ("help", "帮助"): return HELP return None except Exception: log.exception("处理命令失败: %s", text) return "❌ 命令执行出错,请查看服务端日志。" async def run(self): await self.ob.start(self.on_event) alerter = Alerter(self.ob, self.cfg.get("alert", {})) alerter.start() log.info("机器人就绪,等待 OneBot 实现端连接...") try: await asyncio.Event().wait() except (KeyboardInterrupt, asyncio.CancelledError): pass finally: await alerter.stop() await self.ob.stop() if __name__ == "__main__": if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) cfg = load_config() asyncio.run(Bot(cfg).run())