From 93539be492b916b7d99bef49b2996ae0c1c339e8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 26 Apr 2022 14:45:04 +0800 Subject: [PATCH] update v0.1.5.0 --- README.md | 12 +- __version__ | 2 +- basic_plugins/admin_help/data_source.py | 13 +- basic_plugins/chat_history/__init__.py | 42 +--- basic_plugins/chat_history/chat_message.py | 38 +++ .../chat_history/chat_message_handle.py | 108 +++++++++ basic_plugins/hooks/task_hook.py | 2 +- basic_plugins/super_cmd/manager_group.py | 2 +- models/chat_history.py | 113 ++++++++- .../genshin/query_user/_models/__init__.py | 4 +- .../query_user/genshin_sign/init_task.py | 22 +- .../query_user/query_role/draw_image.py | 1 - .../query_user/resin_remind/init_task.py | 8 +- plugins/send_setu_/_model.py | 1 - plugins/send_setu_/update_setu/data_source.py | 2 +- plugins/statistics/statistics_handle.py | 2 +- plugins/web_ui/api/__init__.py | 1 + plugins/web_ui/api/system.py | 219 ++++++++++++++++++ plugins/web_ui/config.py | 93 +++++++- utils/browser.py | 8 +- utils/image_utils.py | 34 +-- utils/message_builder.py | 58 ++++- 22 files changed, 670 insertions(+), 115 deletions(-) create mode 100644 basic_plugins/chat_history/chat_message.py create mode 100644 basic_plugins/chat_history/chat_message_handle.py create mode 100644 plugins/web_ui/api/system.py diff --git a/README.md b/README.md index 3fa890ed..8dfc015e 100644 --- a/README.md +++ b/README.md @@ -242,16 +242,24 @@ __Docker 最新版本由 [Sakuracio](https://github.com/Sakuracio) 提供__ ## 更新 +### 2022/4/26 + +* 修复了群白名单无法正确添加 +* 优化了管理员帮助图片,背景图层将位于最下层 +* 修复了树脂140时不断提醒(未测试 +* 新增了消息记录的消息排行 +* WebUI新增CPU,内存,磁盘监控 +* WebUI新增资源文件夹统计可视化 + ### 2022/4/12 * 修复b了命令私聊出错 -### 2022/4/10 \[v0.1.4.8] +### 2022/4/10 \[v0.1.4.7] * 新增消息记录模块 * 丰富处理请求操作提示 * web ui新增配置项修改 -* 修复chat_history阻断消息 ### 2022/4/9 diff --git a/__version__ b/__version__ index dd58c805..13bb97d6 100644 --- a/__version__ +++ b/__version__ @@ -1 +1 @@ -__version__: v0.1.4.8 +__version__: v0.1.5.0 \ No newline at end of file diff --git a/basic_plugins/admin_help/data_source.py b/basic_plugins/admin_help/data_source.py index e17fae08..a1106785 100755 --- a/basic_plugins/admin_help/data_source.py +++ b/basic_plugins/admin_help/data_source.py @@ -5,7 +5,6 @@ from utils.utils import get_matchers from utils.manager import group_manager from nonebot.adapters.onebot.v11 import Bot from nonebot import Driver -import asyncio import nonebot @@ -27,12 +26,10 @@ async def create_help_image(): """ 创建管理员帮助图片 """ - await asyncio.get_event_loop().run_in_executor( - None, _create_help_image - ) + await _create_help_image() -def _create_help_image(): +async def _create_help_image(): """ 创建管理员帮助图片 """ @@ -85,9 +82,9 @@ def _create_help_image(): height = len(help_str.split("\n")) * 33 A = BuildImage(width, height, font_size=24) _background = BuildImage(width, height, background=background) - A.text((150, 110), help_str) - A.paste(_background, alpha=True) - A.save(admin_help_image) + await A.apaste(_background, alpha=True) + await A.atext((150, 110), help_str) + await A.asave(admin_help_image) logger.info(f'已成功加载 {len(_plugin_name_list)} 条管理员命令') diff --git a/basic_plugins/chat_history/__init__.py b/basic_plugins/chat_history/__init__.py index f749937d..a67c745a 100644 --- a/basic_plugins/chat_history/__init__.py +++ b/basic_plugins/chat_history/__init__.py @@ -1,41 +1,3 @@ -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from models.chat_history import ChatHistory -from ._rule import rule -from configs.config import Config -from nonebot import on_message +import nonebot -__zx_plugin_name__ = "消息存储 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -Config.add_plugin_config( - "chat_history", - "FLAG", - True, - help_="是否开启消息自从存储", - name="消息存储", - default_value=True -) - - -chat_history = on_message(rule=rule, priority=1, block=False) - -# test = on_command("aa") - - -@chat_history.handle() -async def _(event: MessageEvent): - if isinstance(event, GroupMessageEvent): - await ChatHistory.add_chat_msg(event.user_id, event.group_id, str(event.get_message())) - else: - await ChatHistory.add_chat_msg(event.user_id, None, str(event.get_message())) - -# @test.handle() -# async def _(event: MessageEvent): -# print(await ChatHistory.get_user_msg(event.user_id, "private")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) -# print(await ChatHistory.get_user_msg(event.user_id, "group")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) -# print(await ChatHistory.get_group_msg(event.group_id)) -# print(await ChatHistory.get_group_msg_count(event.group_id)) +nonebot.load_plugins("basic_plugins/chat_history") diff --git a/basic_plugins/chat_history/chat_message.py b/basic_plugins/chat_history/chat_message.py new file mode 100644 index 00000000..a0042ef3 --- /dev/null +++ b/basic_plugins/chat_history/chat_message.py @@ -0,0 +1,38 @@ +from configs.config import Config +from models.chat_history import ChatHistory +from nonebot import on_message +from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent + +from ._rule import rule + +__zx_plugin_name__ = "消息存储 [Hidden]" +__plugin_version__ = 0.1 +__plugin_author__ = "HibiKier" + + +Config.add_plugin_config( + "chat_history", "FLAG", True, help_="是否开启消息自从存储", name="消息存储", default_value=True +) + + +chat_history = on_message(rule=rule, priority=1, block=False) + + +@chat_history.handle() +async def _(event: MessageEvent): + if isinstance(event, GroupMessageEvent): + await ChatHistory.add_chat_msg( + event.user_id, event.group_id, str(event.get_message()) + ) + else: + await ChatHistory.add_chat_msg(event.user_id, None, str(event.get_message())) + + +# @test.handle() +# async def _(event: MessageEvent): +# print(await ChatHistory.get_user_msg(event.user_id, "private")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) +# print(await ChatHistory.get_user_msg(event.user_id, "group")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) +# print(await ChatHistory.get_group_msg(event.group_id)) +# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/basic_plugins/chat_history/chat_message_handle.py b/basic_plugins/chat_history/chat_message_handle.py new file mode 100644 index 00000000..cd01d19d --- /dev/null +++ b/basic_plugins/chat_history/chat_message_handle.py @@ -0,0 +1,108 @@ +from datetime import datetime, timedelta + +import pytz +from models.chat_history import ChatHistory +from models.group_member_info import GroupInfoUser +from nonebot import on_regex +from nonebot.adapters.onebot.v11 import GroupMessageEvent +from nonebot.params import RegexGroup +from utils.image_utils import BuildImage, text2image +from utils.utils import is_number +from utils.message_builder import image +from typing import Tuple, Any + + +__zx_plugin_name__ = "消息统计" +__plugin_usage__ = """ +usage: + 发言记录统计 + regex:(周|月)?消息排行(des|DES)?(n=[0-9]{1,2})? + 指令: + 消息统计?(des)?(n=?) + 周消息统计?(des)?(n=?) + 月消息统计?(des)?(n=?) + 示例: + 消息统计 + 消息统计des + 消息统计DESn=15 + 消息统计n=15 +""".strip() +__plugin_des__ = "发言消息排行" +__plugin_cmd__ = [ + "消息统计", + "周消息统计", + "月消息统计" +] +__plugin_type__ = ("数据统计", 1) +__plugin_version__ = 0.1 +__plugin_author__ = "HibiKier" +__plugin_settings__ = { + "level": 5, + "cmd": ["消息统计"], +} + + +msg_handler = on_regex(r"(周|月)?消息统计(des|DES)?(n=[0-9]{1,2})?", priority=5, block=True) + + +@msg_handler.handle() +async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): + gid = event.group_id + date_scope = None + date, order, num = reg_group + num = num.split("=")[-1] or 10 + if num and is_number(num) and 10 < int(num) < 50: + num = int(num) + if date in ["周"]: + date_scope = (datetime.now() - timedelta(days=7), datetime.now()) + elif date in ["月"]: + date_scope = (datetime.now() - timedelta(days=30), datetime.now()) + if rank_data := await ChatHistory.get_group_msg_rank( + gid, num, order or "DESC", date_scope + ): + name = "昵称:\n\n" + num_str = "发言次数:\n\n" + idx = 1 + for uid, num in rank_data: + try: + user_name = (await GroupInfoUser.get_member_info(uid, gid)).user_name + except AttributeError: + user_name = uid + name += f"\t{idx}.{user_name} \n\n" + num_str += f"\t{num}\n\n" + idx += 1 + name_img = await text2image(name.strip(), padding=10, color="#f9f6f2") + num_img = await text2image(num_str.strip(), padding=10, color="#f9f6f2") + if not date_scope: + if date_scope := await ChatHistory.get_group_first_msg_datetime(gid): + date_scope = date_scope.astimezone( + pytz.timezone("Asia/Shanghai") + ).replace(microsecond=0) + else: + date_scope = datetime.now().replace(microsecond=0) + date_str = f"日期:{date_scope} - 至今" + else: + date_str = f"日期:{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}" + date_w = BuildImage(0, 0, font_size=15).getsize(date_str)[0] + img_w = date_w if date_w > name_img.w + num_img.w else name_img.w + num_img.w + A = BuildImage( + img_w + 15, + num_img.h + 30, + color="#f9f6f2", + font="CJGaoDeGuo.otf", + font_size=15, + ) + await A.atext((10, 10), date_str) + await A.apaste(name_img, (0, 30)) + await A.apaste(num_img, (name_img.w, 30)) + await msg_handler.send(image(b64=A.pic2bs4())) + + +# @test.handle() +# async def _(event: MessageEvent): +# print(await ChatHistory.get_user_msg(event.user_id, "private")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) +# print(await ChatHistory.get_user_msg(event.user_id, "group")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) +# print(await ChatHistory.get_group_msg(event.group_id)) +# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/basic_plugins/hooks/task_hook.py b/basic_plugins/hooks/task_hook.py index 7cd241e8..7488ce56 100644 --- a/basic_plugins/hooks/task_hook.py +++ b/basic_plugins/hooks/task_hook.py @@ -10,7 +10,7 @@ async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): r = None if ( ( - (api == "send_msg" and data["message_type"] == "group") + (api == "send_msg" and data.get("message_type") == "group") or api == "send_group_msg" ) and ( diff --git a/basic_plugins/super_cmd/manager_group.py b/basic_plugins/super_cmd/manager_group.py index 7ed821cd..27445265 100755 --- a/basic_plugins/super_cmd/manager_group.py +++ b/basic_plugins/super_cmd/manager_group.py @@ -136,7 +136,7 @@ async def _(): @manager_group_whitelist.handle() async def _(bot: Bot, cmd: Tuple[str, ...] = Command(), arg: Message = CommandArg()): cmd = cmd[0] - msg = arg.extract_plain_text().strip() + msg = arg.extract_plain_text().strip().split() all_group = [ g["group_id"] for g in await bot.get_group_list() ] diff --git a/models/chat_history.py b/models/chat_history.py index 4e5c0505..5198c371 100644 --- a/models/chat_history.py +++ b/models/chat_history.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import List, Literal, Optional +from typing import List, Literal, Optional, Tuple, Union from services.db_context import db @@ -36,6 +36,86 @@ class ChatHistory(db.Model): """ return await cls._get_msg(uid, None, "user", msg_type, days).gino.all() + @classmethod + async def get_group_user_msg( + cls, + uid: int, + gid: int, + limit: int = 10, + date_scope: Tuple[datetime, datetime] = None, + ) -> List["ChatHistory"]: + """ + 说明: + 获取群聊指定用户聊天记录 + 参数: + :param uid: qq + :param gid: 群号 + :param limit: 获取数量 + :param date_scope: 日期范围,默认None为全搜索 + """ + return ( + await cls._get_msg(uid, gid, "group", days=date_scope) + .limit(limit) + .gino.all() + ) + + @classmethod + async def get_group_user_msg_count(cls, uid: int, gid: int) -> Optional[int]: + """ + 说明: + 查询群聊指定用户的聊天记录数量 + 参数: + :param uid: qq + :param gid: 群号 + """ + if x := await db.first( + db.text( + f"SELECT COUNT(id) as sum FROM public.chat_history WHERE user_qq = {uid} AND group_id = {gid}" + ) + ): + return x[0] + return None + + @classmethod + async def get_group_msg_rank( + cls, + gid: int, + limit: int = 10, + order: str = "DESC", + date_scope: Optional[Tuple[datetime, datetime]] = None, + ) -> Optional[Tuple[int, int]]: + """ + 说明: + 获取排行数据 + 参数: + :param gid: 群号 + :param limit: 获取数量 + :param order: 排序类型,desc,des + :param date_scope: 日期范围 + """ + sql = f"SELECT user_qq, COUNT(id) as sum FROM public.chat_history WHERE group_id = {gid} " + if date_scope: + sql += f"AND create_time BETWEEN '{date_scope[0]}' AND '{date_scope[1]}' " + sql += f"GROUP BY user_qq ORDER BY sum {order if order and order.upper() != 'DES' else ''} LIMIT {limit}" + print(sql) + return await db.all(db.text(sql)) + + @classmethod + async def get_group_first_msg_datetime(cls, gid: int) -> Optional[datetime]: + """ + 说明: + 获取群第一条记录消息时间 + 参数: + :param gid: + """ + if ( + msg := await cls.query.where(cls.group_id == gid) + .order_by(cls.create_time) + .gino.first() + ): + return msg.create_time + return None + @classmethod async def get_user_msg_count( cls, @@ -51,7 +131,9 @@ class ChatHistory(db.Model): :param msg_type: 消息类型,私聊或群聊 :param days: 限制日期 """ - return (await cls._get_msg(uid, None, "user", msg_type, days, True).gino.first())[0] + return ( + await cls._get_msg(uid, None, "user", msg_type, days, True).gino.first() + )[0] @classmethod async def get_group_msg( @@ -81,7 +163,9 @@ class ChatHistory(db.Model): :param gid: 用户qq :param days: 限制日期 """ - return (await cls._get_msg(None, gid, "group", None, days, True).gino.first())[0] + return (await cls._get_msg(None, gid, "group", None, days, True).gino.first())[ + 0 + ] @classmethod def _get_msg( @@ -89,9 +173,9 @@ class ChatHistory(db.Model): uid: Optional[int], gid: Optional[int], type_: Literal["user", "group"], - msg_type: Optional[Literal["private", "group"]], - days: Optional[int], - is_select_count: bool = False + msg_type: Optional[Literal["private", "group"]] = None, + days: Optional[Union[int, Tuple[datetime, datetime]]] = None, + is_select_count: bool = False, ): """ 说明: @@ -104,8 +188,8 @@ class ChatHistory(db.Model): :param days: 限制日期 """ if is_select_count: - setattr(ChatHistory, 'count', db.func.count(cls.id).label('count')) - query = cls.select('count') + setattr(ChatHistory, "count", db.func.count(cls.id).label("count")) + query = cls.select("count") else: query = cls.query if type_ == "user": @@ -116,8 +200,15 @@ class ChatHistory(db.Model): query = query.where(cls.group_id != None) else: query = query.where(cls.group_id == gid) + if uid: + query = query.where(cls.user_qq == uid) if days: - query = query.where( - cls.create_time >= datetime.now() - timedelta(days=days) - ) + if isinstance(days, int): + query = query.where( + cls.create_time >= datetime.now() - timedelta(days=days) + ) + elif isinstance(days, tuple): + query = query.where(cls.create_time >= days[0]).where( + cls.create_time <= days[1] + ) return query diff --git a/plugins/genshin/query_user/_models/__init__.py b/plugins/genshin/query_user/_models/__init__.py index a60285e4..3c55910c 100644 --- a/plugins/genshin/query_user/_models/__init__.py +++ b/plugins/genshin/query_user/_models/__init__.py @@ -256,9 +256,7 @@ class Genshin(db.Model): if x: await cls._add_query_uid(uid, uid) return x.cookie - for u in [ - x for x in await cls.query.order_by(db.func.random()).gino.all() if x.cookie - ]: + for u in await cls.query.where(cls.cookie != "").order_by(db.func.random()).gino.all(): if not u.today_query_uid or len(u.today_query_uid[:-1].split()) < 30: await cls._add_query_uid(uid, u.uid) return u.cookie diff --git a/plugins/genshin/query_user/genshin_sign/init_task.py b/plugins/genshin/query_user/genshin_sign/init_task.py index 4a362eb7..8b979141 100644 --- a/plugins/genshin/query_user/genshin_sign/init_task.py +++ b/plugins/genshin/query_user/genshin_sign/init_task.py @@ -23,17 +23,17 @@ async def _(): g_list = await Genshin.get_all_auto_sign_user() for u in g_list: if u.auto_sign_time: - date = await Genshin.random_sign_time(u.uid) - scheduler.add_job( - _sign, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_auto_sign_{u.uid}_{u.user_qq}_0", - args=[u.user_qq, u.uid, 0], - ) - logger.info( - f"genshin_sign add_job:USER:{u.user_qq} UID:{u.uid} " f"{date} 原神自动签到" - ) + if date := await Genshin.random_sign_time(u.uid): + scheduler.add_job( + _sign, + "date", + run_date=date.replace(microsecond=0), + id=f"genshin_auto_sign_{u.uid}_{u.user_qq}_0", + args=[u.user_qq, u.uid, 0], + ) + logger.info( + f"genshin_sign add_job:USER:{u.user_qq} UID:{u.uid} " f"{date} 原神自动签到" + ) def add_job(user_id: int, uid: int, date: datetime): diff --git a/plugins/genshin/query_user/query_role/draw_image.py b/plugins/genshin/query_user/query_role/draw_image.py index b776f759..2de6c3cc 100644 --- a/plugins/genshin/query_user/query_role/draw_image.py +++ b/plugins/genshin/query_user/query_role/draw_image.py @@ -299,7 +299,6 @@ def get_country_data_image(world_data_dict: Dict) -> BuildImage: # 层岩巨渊 和 地下矿区 算一个 region = BuildImage(790, 267 * (len(world_data_dict) - 1), color="#F9F6F2") height = 0 - print(world_data_dict) for country in ["蒙德", "龙脊雪山", "璃月", "层岩巨渊", "稻妻", "渊下宫"]: x = BuildImage(790, 250, color="#3A4467") logo = BuildImage(180, 180, background=image_path / "logo" / f"{country}.png") diff --git a/plugins/genshin/query_user/resin_remind/init_task.py b/plugins/genshin/query_user/resin_remind/init_task.py index d22dde36..3f591bfa 100644 --- a/plugins/genshin/query_user/resin_remind/init_task.py +++ b/plugins/genshin/query_user/resin_remind/init_task.py @@ -20,6 +20,9 @@ driver: Driver = nonebot.get_driver() get_memo = require("query_memo").get_memo +global_map = {} + + class UserManager: def __init__(self, max_error_count: int = 3): self._data = [] @@ -146,8 +149,8 @@ async def _remind(user_id: int, uid: str): if current_resin < max_resin: user_manager.remove(uid) user_manager.remove_overflow(uid) - if max_resin - 40 <= current_resin <= max_resin - 20: - next_time = now + timedelta(minutes=(max_resin - 20 - current_resin) * 8, seconds=10) + if max_resin - 40 < current_resin <= max_resin - 20: + next_time = now + timedelta(minutes=(max_resin - 20 - current_resin + 1) * 8, seconds=10) elif current_resin < max_resin: next_time = now + timedelta(minutes=(max_resin - current_resin) * 8, seconds=10) elif current_resin == max_resin: @@ -189,6 +192,7 @@ async def _remind(user_id: int, uid: str): user_manager.remove_error_count(uid) await Genshin.set_user_resin_recovery_time(int(uid), next_time) scheduler.add_job( + _remind, _remind, "date", run_date=next_time, diff --git a/plugins/send_setu_/_model.py b/plugins/send_setu_/_model.py index 20286c59..e252b360 100644 --- a/plugins/send_setu_/_model.py +++ b/plugins/send_setu_/_model.py @@ -144,7 +144,6 @@ class Setu(db.Model): return _tmp_local_id return -1 - @classmethod async def update_setu_data( cls, diff --git a/plugins/send_setu_/update_setu/data_source.py b/plugins/send_setu_/update_setu/data_source.py index 771b0d83..309b426c 100755 --- a/plugins/send_setu_/update_setu/data_source.py +++ b/plugins/send_setu_/update_setu/data_source.py @@ -139,7 +139,7 @@ async def update_setu_img(flag: bool = False): f"--> /{path}/{image.local_id}.jpg" ) os.rename( - TEMP_PATH / f"{image.local_id}.jpg", + TEMP_PATH / f"/{image.local_id}.jpg", path / f"{image.local_id}.jpg", ) except FileNotFoundError: diff --git a/plugins/statistics/statistics_handle.py b/plugins/statistics/statistics_handle.py index 37c1e439..e3d3fede 100755 --- a/plugins/statistics/statistics_handle.py +++ b/plugins/statistics/statistics_handle.py @@ -53,7 +53,7 @@ __plugin_cmd__ = [ "我的周功能调用统计 ?[功能]", "我的月功能调用统计 ?[功能]", ] -__plugin_type__ = ("功能调用统计可视化", 1) +__plugin_type__ = ("数据统计", 1) __plugin_version__ = 0.1 __plugin_author__ = "HibiKier" __plugin_settings__ = { diff --git a/plugins/web_ui/api/__init__.py b/plugins/web_ui/api/__init__.py index 341445f2..0b61b3a0 100644 --- a/plugins/web_ui/api/__init__.py +++ b/plugins/web_ui/api/__init__.py @@ -1,3 +1,4 @@ from .group import * from .plugins import * from .request import * +from .system import * diff --git a/plugins/web_ui/api/system.py b/plugins/web_ui/api/system.py new file mode 100644 index 00000000..d7930d39 --- /dev/null +++ b/plugins/web_ui/api/system.py @@ -0,0 +1,219 @@ +import asyncio +import os +from pathlib import Path + +import psutil +import ujson as json +from configs.path_config import ( + DATA_PATH, + FONT_PATH, + IMAGE_PATH, + LOG_PATH, + RECORD_PATH, + TEMP_PATH, + TEXT_PATH, +) +from services.log import logger +from utils.http_utils import AsyncHttpx + +from ..auth import Depends, User, token_to_user +from ..config import * + +CPU_DATA_PATH = DATA_PATH / "system" / "cpu.json" +MEMORY_DATA_PATH = DATA_PATH / "system" / "memory.json" +DISK_DATA_PATH = DATA_PATH / "system" / "disk.json" +CPU_DATA_PATH.parent.mkdir(exist_ok=True, parents=True) +cpu_data = {"data": []} +memory_data = {"data": []} +disk_data = {"data": []} + + +@app.get("/webui/system") +async def _(user: User = Depends(token_to_user)) -> Result: + return await get_system_data() + + +@app.get("/webui/system/status") +async def _(user: User = Depends(token_to_user)) -> Result: + return Result( + code=200, + data=await asyncio.get_event_loop().run_in_executor(None, _get_system_status), + ) + + +@app.get("/webui/system/disk") +async def _(type_: Optional[str] = None, user: User = Depends(token_to_user)) -> Result: + return Result( + code=200, + data=await asyncio.get_event_loop().run_in_executor( + None, _get_system_disk, type_ + ), + ) + + +@app.get("/webui/system/statusList") +async def _(user: User = Depends(token_to_user)) -> Result: + global cpu_data, memory_data, disk_data + await asyncio.get_event_loop().run_in_executor(None, _get_system_status) + cpu_rst = cpu_data["data"][-10:] if len(cpu_data["data"]) > 10 else cpu_data["data"] + memory_rst = ( + memory_data["data"][-10:] + if len(memory_data["data"]) > 10 + else memory_data["data"] + ) + disk_rst = ( + disk_data["data"][-10:] if len(disk_data["data"]) > 10 else disk_data["data"] + ) + return Result( + code=200, + data=SystemStatusList( + cpu_data=cpu_rst, + memory_data=memory_rst, + disk_data=disk_rst, + ), + ) + + +async def get_system_data(): + """ + 说明: + 获取系统信息,资源文件大小,网络状态等 + """ + baidu = 200 + google = 200 + try: + await AsyncHttpx.get("https://www.baidu.com/", timeout=5) + except Exception as e: + logger.warning(f"访问BaiDu失败... {type(e)}: {e}") + baidu = 404 + try: + await AsyncHttpx.get("https://www.google.com/", timeout=5) + except Exception as e: + logger.warning(f"访问Google失败... {type(e)}: {e}") + google = 404 + network = SystemNetwork(baidu=baidu, google=google) + disk = await asyncio.get_event_loop().run_in_executor(None, _get_system_disk) + status = await asyncio.get_event_loop().run_in_executor(None, _get_system_status) + return Result( + code=200, + data=SystemResult( + status=status, + network=network, + disk=disk, + check_time=datetime.now().replace(microsecond=0), + ), + ) + + +def _get_system_status() -> SystemStatus: + """ + 说明: + 获取系统信息等 + """ + cpu = psutil.cpu_percent() + memory = psutil.virtual_memory().percent + disk = psutil.disk_usage("/").percent + save_system_data(cpu, memory, disk) + return SystemStatus( + cpu=cpu, + memory=memory, + disk=disk, + check_time=datetime.now().replace(microsecond=0), + ) + + +def _get_system_disk( + type_: Optional[str], +) -> Union[SystemFolderSize, Dict[str, Union[float, datetime]]]: + """ + 说明: + 获取资源文件大小等 + """ + if not type_: + disk = SystemFolderSize( + font_dir_size=_get_dir_size(FONT_PATH) / 1024 / 1024, + image_dir_size=_get_dir_size(IMAGE_PATH) / 1024 / 1024, + text_dir_size=_get_dir_size(TEXT_PATH) / 1024 / 1024, + record_dir_size=_get_dir_size(RECORD_PATH) / 1024 / 1024, + temp_dir_size=_get_dir_size(TEMP_PATH) / 1024 / 102, + data_dir_size=_get_dir_size(DATA_PATH) / 1024 / 1024, + log_dir_size=_get_dir_size(LOG_PATH) / 1024 / 1024, + check_time=datetime.now().replace(microsecond=0), + ) + return disk + else: + if type_ == "image": + dir_path = IMAGE_PATH + elif type_ == "font": + dir_path = FONT_PATH + elif type_ == "text": + dir_path = TEXT_PATH + elif type_ == "record": + dir_path = RECORD_PATH + elif type_ == "data": + dir_path = DATA_PATH + elif type_ == "temp": + dir_path = TEMP_PATH + else: + dir_path = LOG_PATH + dir_map = {} + other_file_size = 0 + for file in os.listdir(dir_path): + file = Path(dir_path / file) + if file.is_dir(): + dir_map[file.name] = _get_dir_size(file) / 1024 / 1024 + else: + other_file_size += os.path.getsize(file) / 1024 / 1024 + dir_map["其他文件"] = other_file_size + dir_map["check_time"] = datetime.now().replace(microsecond=0) + return dir_map + + +def _get_dir_size(dir_path: Path) -> float: + """ + 说明: + 获取文件夹大小 + 参数: + :param dir_path: 文件夹路径 + """ + size = 0 + for root, dirs, files in os.walk(dir_path): + size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) + return size + + +def save_system_data(cpu: float, memory: float, disk: float): + """ + 说明: + 保存一些系统信息 + 参数: + :param cpu: cpu + :param memory: memory + :param disk: disk + """ + global cpu_data, memory_data, disk_data + if CPU_DATA_PATH.exists() and not cpu_data["data"]: + with open(CPU_DATA_PATH, "r") as f: + cpu_data = json.load(f) + if MEMORY_DATA_PATH.exists() and not memory_data["data"]: + with open(MEMORY_DATA_PATH, "r") as f: + memory_data = json.load(f) + if DISK_DATA_PATH.exists() and not disk_data["data"]: + with open(DISK_DATA_PATH, "r") as f: + disk_data = json.load(f) + now = str(datetime.now().time().replace(microsecond=0)) + cpu_data["data"].append({"time": now, "data": cpu}) + memory_data["data"].append({"time": now, "data": memory}) + disk_data["data"].append({"time": now, "data": disk}) + if len(cpu_data["data"]) > 50: + cpu_data["data"] = cpu_data["data"][-50:] + if len(memory_data["data"]) > 50: + memory_data["data"] = memory_data["data"][-50:] + if len(disk_data["data"]) > 50: + disk_data["data"] = disk_data["data"][-50:] + with open(CPU_DATA_PATH, "w") as f: + json.dump(cpu_data, f, indent=4, ensure_ascii=False) + with open(MEMORY_DATA_PATH, "w") as f: + json.dump(memory_data, f, indent=4, ensure_ascii=False) + with open(DISK_DATA_PATH, "w") as f: + json.dump(disk_data, f, indent=4, ensure_ascii=False) diff --git a/plugins/web_ui/config.py b/plugins/web_ui/config.py index 7927aebc..93d7997e 100644 --- a/plugins/web_ui/config.py +++ b/plugins/web_ui/config.py @@ -1,6 +1,7 @@ -from typing import Optional, List, Any, Union +from typing import Optional, List, Any, Union, Dict from pydantic import BaseModel from fastapi.middleware.cors import CORSMiddleware +from datetime import datetime import nonebot @@ -18,6 +19,9 @@ app.add_middleware( class CdLimit(BaseModel): + """ + Cd 限制 + """ cd: int status: bool check_type: str @@ -26,6 +30,9 @@ class CdLimit(BaseModel): class BlockLimit(BaseModel): + """ + Block限制 + """ status: bool check_type: str limit_type: str @@ -33,6 +40,9 @@ class BlockLimit(BaseModel): class CountLimit(BaseModel): + """ + Count限制 + """ max_count: int status: bool limit_type: bool @@ -40,6 +50,9 @@ class CountLimit(BaseModel): class PluginManager(BaseModel): + """ + 插件信息 + """ plugin_name: str # 插件名称 status: Optional[bool] # 插件状态 error: Optional[bool] # 加载状态 @@ -49,6 +62,9 @@ class PluginManager(BaseModel): class PluginSettings(BaseModel): + """ + 插件基本设置 + """ level: Optional[int] # 群权限等级 default_status: Optional[bool] # 默认开关 limit_superuser: Optional[bool] # 是否限制超级用户 @@ -58,6 +74,9 @@ class PluginSettings(BaseModel): class PluginConfig(BaseModel): + """ + 插件配置项 + """ id: int key: str value: Optional[Any] @@ -66,6 +85,9 @@ class PluginConfig(BaseModel): class Plugin(BaseModel): + """ + 插件 + """ model: str # 模块 plugin_settings: Optional[PluginSettings] plugin_manager: Optional[PluginManager] @@ -76,6 +98,9 @@ class Plugin(BaseModel): class Group(BaseModel): + """ + 群组信息 + """ group_id: int group_name: str member_count: int @@ -83,12 +108,18 @@ class Group(BaseModel): class Task(BaseModel): + """ + 被动技能 + """ name: str nameZh: str status: bool class GroupResult(BaseModel): + """ + 群组返回数据 + """ group: Group level: int status: bool @@ -97,6 +128,9 @@ class GroupResult(BaseModel): class RequestResult(BaseModel): + """ + 好友/群组请求管理 + """ oid: str id: int flag: str @@ -111,11 +145,68 @@ class RequestResult(BaseModel): class RequestParma(BaseModel): + """ + 操作请求接收数据 + """ id: int handle: str type: str +class SystemStatus(BaseModel): + """ + 系统状态 + """ + cpu: int + memory: int + disk: int + check_time: datetime + + +class SystemNetwork(BaseModel): + """ + 系统网络状态 + """ + baidu: int + google: int + + +class SystemFolderSize(BaseModel): + """ + 资源文件占比 + """ + font_dir_size: float + image_dir_size: float + text_dir_size: float + record_dir_size: float + temp_dir_size: float + data_dir_size: float + log_dir_size: float + check_time: datetime + + +class SystemStatusList(BaseModel): + """ + 状态记录 + """ + cpu_data: List[Dict[str, Union[float, str]]] + memory_data: List[Dict[str, Union[float, str]]] + disk_data: List[Dict[str, Union[float, str]]] + + +class SystemResult(BaseModel): + """ + 系统api返回 + """ + status: SystemStatus + network: SystemNetwork + disk: SystemFolderSize + check_time: datetime + + class Result(BaseModel): + """ + 总体返回 + """ code: int data: Any diff --git a/utils/browser.py b/utils/browser.py index 28888df1..91d21a66 100755 --- a/utils/browser.py +++ b/utils/browser.py @@ -14,16 +14,20 @@ _browser: Optional[Browser] = None async def init(**kwargs) -> Optional[Browser]: + global _browser if platform.system() == "Windows": return None try: - global _browser browser = await async_playwright().start() _browser = await browser.chromium.launch(**kwargs) return _browser except NotImplementedError: logger.warning("win环境下 初始化playwright失败,相关功能将被限制....") - return None + except Exception as e: + logger.warning(f"启动chromium发生错误 {type(e)}:{e}") + if _browser: + await _browser.close() + return None async def get_browser(**kwargs) -> Browser: diff --git a/utils/image_utils.py b/utils/image_utils.py index c1c5f525..5641717c 100755 --- a/utils/image_utils.py +++ b/utils/image_utils.py @@ -1,17 +1,18 @@ import asyncio -from configs.path_config import IMAGE_PATH, FONT_PATH -from PIL import Image, ImageFile, ImageDraw, ImageFont, ImageFilter -from imagehash import ImageHash -from io import BytesIO -from matplotlib import pyplot as plt -from typing import Tuple, Optional, Union, List, Literal -from pathlib import Path -from math import ceil -import random -import cv2 import base64 -import imagehash +import random import re +from io import BytesIO +from math import ceil +from pathlib import Path +from typing import List, Literal, Optional, Tuple, Union + +import cv2 +import imagehash +from configs.path_config import FONT_PATH, IMAGE_PATH +from imagehash import ImageHash +from matplotlib import pyplot as plt +from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont ImageFile.LOAD_TRUNCATED_IMAGES = True Image.MAX_IMAGE_PIXELS = None @@ -65,9 +66,7 @@ def compressed_image( """ in_file = IMAGE_PATH / in_file if isinstance(in_file, str) else in_file if out_file: - out_file = ( - IMAGE_PATH / out_file if isinstance(out_file, str) else out_file - ) + out_file = IMAGE_PATH / out_file if isinstance(out_file, str) else out_file else: out_file = in_file h, w, d = cv2.imread(str(in_file.absolute())).shape @@ -673,9 +672,11 @@ class BuildImage: ellipse_box = [0, 0, r2 - 2, r2 - 2] mask = Image.new( size=[int(dim * antialias) for dim in self.markImg.size], - mode='L', color='black') + mode="L", + color="black", + ) draw = ImageDraw.Draw(mask) - for offset, fill in (width / -2.0, 'black'), (width / 2.0, 'white'): + for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"): left, top = [(value + offset) * antialias for value in ellipse_box[:2]] right, bottom = [(value - offset) * antialias for value in ellipse_box[2:]] draw.ellipse([left, top, right, bottom], fill=fill) @@ -1490,6 +1491,7 @@ async def text2image( height = 0 _tmp = BuildImage(0, 0, font_size=font_size) for x in text.split("\n"): + x = x if x.strip() else "正" w, h = _tmp.getsize(x) height += h width = width if width > w else w diff --git a/utils/message_builder.py b/utils/message_builder.py index 5a1b3780..1551b1d4 100755 --- a/utils/message_builder.py +++ b/utils/message_builder.py @@ -1,16 +1,16 @@ -from configs.path_config import IMAGE_PATH, RECORD_PATH -from nonebot.adapters.onebot.v11.message import MessageSegment -from configs.config import NICKNAME -from services.log import logger -from typing import Union, List from pathlib import Path -import os +from typing import List, Union + +from configs.config import NICKNAME +from configs.path_config import IMAGE_PATH, RECORD_PATH +from nonebot.adapters.onebot.v11.message import MessageSegment, Message +from services.log import logger def image( - file: Union[str, Path, bytes] = None, - path: str = None, - b64: str = None, + file: Union[str, Path, bytes] = None, + path: str = None, + b64: str = None, ) -> Union[MessageSegment, str]: """ 说明: @@ -63,7 +63,9 @@ def record(voice_name: str, path: str = None) -> MessageSegment or str: if len(voice_name.split(".")) == 1: voice_name += ".mp3" file = ( - Path(RECORD_PATH) / path / voice_name if path else Path(RECORD_PATH) / voice_name + Path(RECORD_PATH) / path / voice_name + if path + else Path(RECORD_PATH) / voice_name ) if "http" in voice_name: return MessageSegment.record(voice_name) @@ -96,7 +98,7 @@ def contact_user(qq: int) -> MessageSegment: def share( - url: str, title: str, content: str = None, image_url: str = None + url: str, title: str, content: str = None, image_url: str = None ) -> MessageSegment: """ 说明: @@ -155,7 +157,7 @@ def music(type_: str, id_: int) -> MessageSegment: def custom_forward_msg( - msg_list: List[str], uin: Union[int, str], name: str = f"这里是{NICKNAME}" + msg_list: List[str], uin: Union[int, str], name: str = f"这里是{NICKNAME}" ) -> List[dict]: """ 生成自定义合并消息 @@ -176,3 +178,35 @@ def custom_forward_msg( } mes_list.append(data) return mes_list + + +class MessageBuilder: + """ + MessageSegment构建工具 + """ + + def __init__(self, msg: Union[str, MessageSegment, Message]): + if msg: + if isinstance(msg, str): + self._msg = text(msg) + else: + self._msg = msg + else: + self._msg = text("") + + def text(self, msg: str): + return MessageBuilder(self._msg + text(msg)) + + def image( + self, + file: Union[str, Path, bytes] = None, + path: str = None, + b64: str = None, + ): + return MessageBuilder(self._msg + image(file, path, b64)) + + def at(self, qq: int): + return MessageBuilder(self._msg + at(qq)) + + def face(self, id_: int): + return MessageBuilder(self._msg + face(id_))