diff --git a/liteyuki/liteyuki_main/runtime.py b/liteyuki/liteyuki_main/runtime.py index be3366e..0eb64cf 100644 --- a/liteyuki/liteyuki_main/runtime.py +++ b/liteyuki/liteyuki_main/runtime.py @@ -48,7 +48,7 @@ for lang in get_all_lang(): } -@scheduler.scheduled_job("cron", second="*/20") +@scheduler.scheduled_job("cron", minute="*/20") async def _(): nonebot.logger.info("数据已刷新") for lang_code in get_all_lang(): diff --git a/liteyuki/plugins/liteyuki_status/api.py b/liteyuki/plugins/liteyuki_status/api.py index e69de29..a820831 100644 --- a/liteyuki/plugins/liteyuki_status/api.py +++ b/liteyuki/plugins/liteyuki_status/api.py @@ -0,0 +1,195 @@ +import platform +import time + +import nonebot +import psutil +from cpuinfo import cpuinfo +from liteyuki.utils import __NAME__, __VERSION__ +from liteyuki.utils.base.data_manager import TempConfig, common_db +from liteyuki.utils.base.language import Language +from liteyuki.utils.base.resource import get_path +from liteyuki.utils.message.html_tool import template2image + +protocol_names = { + 0: "iPad", + 1: "Android Phone", + 2: "Android Watch", + 3: "Mac", + 5: "iPad", + 6: "Android Pad", +} + +""" +Universal Interface +data +- bot + - name: str + icon: str + id: int + protocol_name: str + groups: int + friends: int + message_sent: int + message_received: int + app_name: str +- hardware + - cpu + - percent: float + - name: str + - mem + - percent: float + - total: int + - used: int + - free: int + - swap + - percent: float + - total: int + - used: int + - free: int + - disk: list + - name: str + - percent: float + - total: int + +""" + + +async def generate_status_card(bot: dict, hardware: dict, liteyuki: dict, lang="zh-CN", bot_id="0") -> bytes: + return await template2image( + get_path("templates/status.html", abs_path=True), + { + "data": { + "bot" : bot, + "hardware" : hardware, + "liteyuki" : liteyuki, + "localization": get_local_data(lang) + } + }, + debug=True + ) + + +def get_local_data(lang_code) -> dict: + lang = Language(lang_code) + return { + "friends" : lang.get("status.friends"), + "groups" : lang.get("status.groups"), + "plugins" : lang.get("status.plugins"), + "message_sent" : lang.get("status.message_sent"), + "message_received": lang.get("status.message_received"), + "cpu" : lang.get("status.cpu"), + "memory" : lang.get("status.memory"), + "swap" : lang.get("status.swap"), + "disk" : lang.get("status.disk"), + + "usage" : lang.get("status.usage"), + "total" : lang.get("status.total"), + "used" : lang.get("status.used"), + "free" : lang.get("status.free"), + } + + +async def get_bots_data(self_id: str = "0") -> dict: + """获取当前所有机器人数据 + Returns: + """ + result = { + "self_id": self_id, + "bots" : [], + } + for bot_id, bot in nonebot.get_bots().items(): + groups = 0 + friends = 0 + status = {} + bot_name = bot_id + version_info = {} + try: + # API fetch + bot_name = (await bot.get_login_info())["nickname"] + groups = len(await bot.get_group_list()) + friends = len(await bot.get_friend_list()) + status = await bot.get_status() + version_info = await bot.get_version_info() + except Exception: + pass + + statistics = status.get("stat", {}) + app_name = version_info.get("app_name", "UnknownImplementation") + if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock"]: + icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640" + else: + icon = None + bot_data = { + "name" : bot_name, + "icon" : icon, + "id" : bot_id, + "protocol_name" : protocol_names.get(version_info.get("protocol_name"), "Online"), + "groups" : groups, + "friends" : friends, + "message_sent" : statistics.get("message_sent"), + "message_received": statistics.get("message_received"), + "app_name" : app_name + } + result["bots"].append(bot_data) + + return result + + +async def get_hardware_data() -> dict: + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown") + if "AMD" in cpu_brand_raw: + brand = "AMD" + elif "Intel" in cpu_brand_raw: + brand = "Intel" + else: + brand = "Unknown" + result = { + "cpu" : { + "percent": psutil.cpu_percent(), + "name" : f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}" + }, + "mem" : { + "percent": mem.percent, + "total" : mem.total, + "used" : mem.used, + "free" : mem.free + }, + "swap": { + "percent": swap.percent, + "total" : swap.total, + "used" : swap.used, + "free" : swap.free + }, + "disk": [], + } + + for disk in psutil.disk_partitions(all=True): + try: + disk_usage = psutil.disk_usage(disk.mountpoint) + result["disk"].append({ + "name" : disk.mountpoint, + "percent": disk_usage.percent, + "total" : disk_usage.total, + "used" : disk_usage.used, + "free" : disk_usage.free + }) + except: + pass + + return result + + +async def get_liteyuki_data() -> dict: + temp_data: TempConfig = common_db.first(TempConfig(), default=TempConfig()) + result = { + "name" : __NAME__, + "version": __VERSION__, + "plugins": len(nonebot.get_loaded_plugins()), + "nonebot": f"{nonebot.__version__}", + "python" : f"{platform.python_implementation()} {platform.python_version()}", + "system" : f"{platform.system()} {platform.release()}", + "runtime": time.time() - temp_data.data.get("start_time", time.time()), # 运行时间秒数 + } + return result diff --git a/liteyuki/plugins/liteyuki_status/status.py b/liteyuki/plugins/liteyuki_status/status.py index 66e2faf..a2844df 100644 --- a/liteyuki/plugins/liteyuki_status/status.py +++ b/liteyuki/plugins/liteyuki_status/status.py @@ -1,12 +1,18 @@ from nonebot import require +from liteyuki.utils.base.resource import get_path +from liteyuki.utils.message.html_tool import template2image +from liteyuki.utils.base.language import get_user_lang +from .api import * +from ...utils.base.ly_typing import T_Bot, T_MessageEvent + require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma +from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma, UniMessage status_alc = on_alconna( - aliases={"status"}, + aliases={"#状态"}, command=Alconna( - "status", + "#status", Subcommand( "memory", alias={"mem", "m", "内存"}, @@ -19,6 +25,19 @@ status_alc = on_alconna( ) +@status_alc.handle() +async def _(event: T_MessageEvent, bot: T_Bot): + ulang = get_user_lang(event.user_id) + image = await generate_status_card( + bot=await get_bots_data(), + hardware=await get_hardware_data(), + liteyuki=await get_liteyuki_data(), + lang=ulang.lang_code, + bot_id=bot.self_id + ) + await status_alc.finish(UniMessage.image(raw=image)) + + @status_alc.assign("memory") async def _(): print("memory") diff --git a/liteyuki/resources/lang/zh-CN.lang b/liteyuki/resources/lang/zh-CN.lang index 1a48df3..2e54ad2 100644 --- a/liteyuki/resources/lang/zh-CN.lang +++ b/liteyuki/resources/lang/zh-CN.lang @@ -133,3 +133,17 @@ rpm.move_top=置顶 weather.city_not_found=未找到城市 {CITY} weather.weather_not_found=未找到城市 {CITY} 的天气信息 weather.no_key=未设置天气api key,请在配置文件添加weather_key + +status.friends=好友 +status.groups=群 +status.plugins=插件 +status.message_sent=发送消息 +status.message_received=接收消息 +status.cpu=处理器 +status.memory=内存 +status.swap=交换空间 +status.disk=磁盘 +status.usage=使用率 +status.total=总计 +status.used=已用 +status.free=空闲 \ No newline at end of file diff --git a/liteyuki/resources/templates/js/status.js b/liteyuki/resources/templates/js/status.js index 288f492..803e57b 100644 --- a/liteyuki/resources/templates/js/status.js +++ b/liteyuki/resources/templates/js/status.js @@ -1,9 +1,84 @@ const data = JSON.parse(document.getElementById('data').innerText); +const bot_data = data['bot']; // 机器人数据 +const hardware_data = data['hardware']; // 硬件数据 +const liteyuki_data = data['liteyuki']; // LiteYuki数据 +const local_data = data['localization']; // 本地化语言数据 -function createPieChartOption(title, data){ +console.log(data) + +/** + * 创建饼图 + * @param title + * @param {Array<{name: string, value: number}>} data 数据 + */ +function createPieChartOption(title, data) { // data为各项占比列表 + return { + animation: false, + title: { + text: title, + left: 'center', + top: 'center', + textStyle: { + color: '#fff', + fontSize: 30 + } + }, + tooltip: { + show: true, + trigger: 'item', + backgroundColor: '#fff', + }, + color: data.length === 3 ? ['#00a6ff', '#a2d8f4', "#ffffff44"] : ['#a2d8f4', '#ffffff44'], + series: [ + { + name: 'info', + type: 'pie', + radius: ['80%', '100%'], + center: ['50%', '50%'], + itemStyle: { + normal: { + label: { + show: false + }, + labelLine: { + show: false + } + }, + emphasis: { + label: { + show: true, + textStyle: { + fontSize: '50', + fontWeight: 'bold' + } + } + } + }, + data: data + } + ] + } } -function createBarChartOption(title, percent){ +/** + * 创建柱状图 + * @param title + * @param percent 数据 + */ +function createBarChartOption(title, percent) { // percent为百分比,最大值为100 } + +// 主函数 +function main() { + bot_data['bots'].forEach( + (bot, index) => { + let botInfoDiv = document.importNode(document.getElementById('bot-template').content, true) + document.body.insertBefore(botInfoDiv, document.getElementById('hardware-info')) + botInfoDiv.className = 'info-box bot-info' + } + ) +} + +main() \ No newline at end of file diff --git a/liteyuki/resources/templates/status.html b/liteyuki/resources/templates/status.html index 811f751..7f26ac4 100644 --- a/liteyuki/resources/templates/status.html +++ b/liteyuki/resources/templates/status.html @@ -7,9 +7,24 @@ +
{{ data | tojson }}
-
+
diff --git a/liteyuki/utils/__init__.py b/liteyuki/utils/__init__.py index 5dbd1fa..d0f227c 100644 --- a/liteyuki/utils/__init__.py +++ b/liteyuki/utils/__init__.py @@ -2,6 +2,7 @@ import json import os.path import platform import sys +import time import nonebot @@ -12,7 +13,7 @@ import requests from liteyuki.utils.base.config import load_from_yaml, config from liteyuki.utils.base.log import init_log -from liteyuki.utils.base.data_manager import auto_migrate +from liteyuki.utils.base.data_manager import TempConfig, auto_migrate, common_db major, minor, patch = map(int, __VERSION__.split(".")) __VERSION_I__ = major * 10000 + minor * 100 + patch @@ -54,6 +55,10 @@ def init(): if sys.version_info < (3, 10): nonebot.logger.error("This project requires Python3.10+ to run, please upgrade your Python Environment.") exit(1) + temp_data: TempConfig = common_db.first(TempConfig(), default=TempConfig()) + temp_data.data["start_time"] = time.time() + common_db.upsert(temp_data) + auto_migrate() # 在加载完成语言后再初始化日志 nonebot.logger.info("Liteyuki is initializing...") diff --git a/liteyuki/utils/base/data_manager.py b/liteyuki/utils/base/data_manager.py index 77b21a7..2a737cd 100644 --- a/liteyuki/utils/base/data_manager.py +++ b/liteyuki/utils/base/data_manager.py @@ -6,10 +6,10 @@ from .data import Database, LiteModel, Database DATA_PATH = "data/liteyuki" -user_db = Database(os.path.join(DATA_PATH, "users.ldb")) -group_db = Database(os.path.join(DATA_PATH, "groups.ldb")) -plugin_db = Database(os.path.join(DATA_PATH, "plugins.ldb")) -common_db = Database(os.path.join(DATA_PATH, "common.ldb")) +user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb")) +group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb")) +plugin_db: Database = Database(os.path.join(DATA_PATH, "plugins.ldb")) +common_db: Database = Database(os.path.join(DATA_PATH, "common.ldb")) class User(LiteModel): diff --git a/liteyuki/utils/base/resource.py b/liteyuki/utils/base/resource.py index 21d636d..bfec758 100644 --- a/liteyuki/utils/base/resource.py +++ b/liteyuki/utils/base/resource.py @@ -47,7 +47,7 @@ def load_resource_from_dir(path: str): _loaded_resource_packs.insert(0, ResourceMetadata(**metadata)) -def get_path(path: str, abs_path: bool = False, default: Any = None, debug: bool=False) -> str | Any: +def get_path(path: str, abs_path: bool = True, default: Any = None, debug: bool=False) -> str | Any: """ 获取资源包中的文件 Args: diff --git a/liteyuki/utils/message/html_tool.py b/liteyuki/utils/message/html_tool.py index 8f258e2..4a0a01e 100644 --- a/liteyuki/utils/message/html_tool.py +++ b/liteyuki/utils/message/html_tool.py @@ -62,7 +62,6 @@ async def template2image( if debug: # 重载资源 - load_resources() raw_html = await template_to_html( template_name=template_name, diff --git a/main.py b/main.py index 7682997..861704d 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,6 @@ from liteyuki.utils.base.ly_api import liteyuki_api init() store_config: dict = common_db.first(StoredConfig(), default=StoredConfig()).config - static_config = load_from_yaml("config.yml") store_config.update(static_config) nonebot.init(**store_config)