From a80ebf964faeed327d8de32783a58dd5d5f985f6 Mon Sep 17 00:00:00 2001 From: Flern Date: Fri, 27 Dec 2024 17:26:27 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20=E8=B5=84=E6=BA=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=8D=95=E7=8B=AC=E4=B8=8B=E8=BD=BD=EF=BC=8C=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E8=A2=AB=E5=8A=A8=E4=BB=BB=E5=8A=A1=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 2 + zhenxun/builtin_plugins/init/init_plugin.py | 45 +---- zhenxun/builtin_plugins/init/init_task.py | 164 ++++++++++++++++++ zhenxun/builtin_plugins/sign_in/utils.py | 9 +- .../web_ui/api/menu/__init__.py | 2 +- .../web_ui/api/tabs/dashboard/__init__.py | 8 +- .../web_ui/api/tabs/manage/__init__.py | 5 +- .../web_ui/api/tabs/plugin_manage/__init__.py | 4 +- .../web_ui/api/tabs/plugin_manage/store.py | 8 +- zhenxun/configs/utils/__init__.py | 36 +++- zhenxun/models/task_info.py | 3 + zhenxun/utils/http_utils.py | 1 + zhenxun/utils/manager/resource_manager.py | 79 +++++++++ 13 files changed, 302 insertions(+), 64 deletions(-) create mode 100644 zhenxun/builtin_plugins/init/init_task.py create mode 100644 zhenxun/utils/manager/resource_manager.py diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index aaa8306d..28395c96 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -16,6 +16,7 @@ from zhenxun.models.sign_user import SignUser from zhenxun.models.user_console import UserConsole from zhenxun.services.log import logger from zhenxun.utils.decorator.shop import shop_register +from zhenxun.utils.manager.resource_manager import ResourceManager from zhenxun.utils.platform import PlatformUtils driver: Driver = nonebot.get_driver() @@ -65,6 +66,7 @@ from public.bag_users t1 @driver.on_startup async def _(): + await ResourceManager.init_resources() """签到与用户的数据迁移""" if goods_list := await GoodsInfo.filter(uuid__isnull=True).all(): for goods in goods_list: diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index 366df312..1257d6e0 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -34,7 +34,6 @@ async def _handle_setting( plugin: Plugin, plugin_list: list[PluginInfo], limit_list: list[PluginLimit], - task_list: list[tuple[bool, TaskInfo]], ): """处理插件设置 @@ -91,20 +90,6 @@ async def _handle_setting( ) for limit in extra_data.limits ) - if extra_data.tasks: - task_list.extend( - ( - task.create_status, - TaskInfo( - module=task.module, - name=task.name, - status=task.status, - run_time=task.run_time, - default_status=task.default_status, - ), - ) - for task in extra_data.tasks - ) @driver.on_startup @@ -114,14 +99,13 @@ async def _(): """ plugin_list: list[PluginInfo] = [] limit_list: list[PluginLimit] = [] - task_list = [] module2id = {} load_plugin = [] if module_list := await PluginInfo.all().values("id", "module_path"): module2id = {m["module_path"]: m["id"] for m in module_list} for plugin in get_loaded_plugins(): load_plugin.append(plugin.module_name) - await _handle_setting(plugin, plugin_list, limit_list, task_list) + await _handle_setting(plugin, plugin_list, limit_list) create_list = [] update_list = [] for plugin in plugin_list: @@ -170,33 +154,6 @@ async def _(): # limit_create.append(limit) # if limit_create: # await PluginLimit.bulk_create(limit_create, 10) - if task_list: - module_dict = { - t[1]: t[0] for t in await TaskInfo.all().values_list("id", "module") - } - create_list = [] - update_list = [] - for status, task in task_list: - if task.module not in module_dict: - create_list.append((status, task)) - else: - task.id = module_dict[task.module] - update_list.append(task) - if create_list: - _create_list = [t[1] for t in create_list] - await TaskInfo.bulk_create(_create_list, 10) - if block := [t[1].module for t in create_list if not t[0]]: - block_task = ",".join(block) + "," - if group_list := await GroupConsole.all(): - for group in group_list: - group.block_task += block_task - await GroupConsole.bulk_update(group_list, ["block_task"], 10) - if update_list: - await TaskInfo.bulk_update( - update_list, - ["run_time", "name"], - 10, - ) await data_migration() await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False) diff --git a/zhenxun/builtin_plugins/init/init_task.py b/zhenxun/builtin_plugins/init/init_task.py new file mode 100644 index 00000000..cead7d72 --- /dev/null +++ b/zhenxun/builtin_plugins/init/init_task.py @@ -0,0 +1,164 @@ +import nonebot +from nonebot import get_loaded_plugins +from nonebot.drivers import Driver +from nonebot.plugin import Plugin +from nonebot.utils import is_coroutine_callable +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.configs.utils import PluginExtraData, Task +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.common_utils import CommonUtils + +driver: Driver = nonebot.get_driver() + + +async def _handle_setting( + plugin: Plugin, + task_info_list: list[tuple[bool, TaskInfo]], + task_list: list[Task], +): + """处理插件设置 + + 参数: + plugin: Plugin + task_info_list: 被动技能db数据列表 + task_list: 被动技能列表 + """ + metadata = plugin.metadata + if not metadata: + return + extra = metadata.extra + extra_data = PluginExtraData(**extra) + if extra_data.tasks: + task_info_list.extend( + ( + task.create_status, + TaskInfo( + module=task.module, + name=task.name, + status=task.status, + default_status=task.default_status, + ), + ) + for task in extra_data.tasks + ) + task_list.extend(extra_data.tasks) + + +async def update_to_group(create_list: list[tuple[bool, TaskInfo]]): + """根据创建时状态对群组进行被动技能更新 + + 参数: + create_list: 被动技能创建列表 + """ + if blocks := [t[1].module for t in create_list if not t[0]]: + if group_list := await GroupConsole.all(): + for group in group_list: + block_tasks = list( + set(CommonUtils.convert_module_format(group.block_task) + blocks) + ) + group.block_task = CommonUtils.convert_module_format(block_tasks) + await GroupConsole.bulk_update(group_list, ["block_task"], 10) + + +async def to_db( + load_task: list[str], + create_list: list[tuple[bool, TaskInfo]], + update_list: list[TaskInfo], +): + """将被动技能保存至数据库 + + 参数: + load_task: 已加载的被动技能模块 + create_list: 被动技能创建列表 + update_list: 被动技能更新列表 + """ + if create_list: + _create_list = [t[1] for t in create_list] + await TaskInfo.bulk_create(_create_list, 10) + await update_to_group(create_list) + if update_list: + await TaskInfo.bulk_update( + update_list, + ["run_time", "name"], + 10, + ) + if load_task: + await TaskInfo.filter(module__in=load_task).update(load_status=True) + await TaskInfo.filter(module__not_in=load_task).update(load_status=False) + + +async def get_run_task(task: Task, *args, **kwargs): + is_run = False + if task.check: + if is_coroutine_callable(task.check): + if await task.check(*task.check_args): + is_run = True + elif task.check(*task.check_args): + is_run = True + else: + bot = task.check_args[0] + group_id = task.check_args[1] + if not await CommonUtils.task_is_block(bot, task.module, group_id): + is_run = True + if is_run and task.run_func: + if is_coroutine_callable(task.run_func): + await task.run_func(*args, **kwargs) + else: + task.run_func(*args, **kwargs) + + +async def create_schedule(task: Task): + scheduler_model = task.scheduler + if not scheduler_model or not task.run_func: + return + try: + scheduler.add_job( + get_run_task, + scheduler_model.trigger, + run_date=scheduler_model.run_date, + hour=scheduler_model.hour, + minute=scheduler_model.minute, + second=scheduler_model.second, + id=scheduler_model.id, + max_instances=scheduler_model.max_instances, + args=scheduler_model.args, + kwargs=scheduler_model.kwargs, + ) + logger.debug(f"成功动态创建定时任务: {task.name}({task.module})") + except Exception as e: + logger.error(f"动态创建定时任务 {task.name}({task.module}) 失败", e=e) + + +@driver.on_startup +async def _(): + """ + 初始化插件数据配置 + """ + task_list: list[Task] = [] + task_info_list: list[tuple[bool, TaskInfo]] = [] + for plugin in get_loaded_plugins(): + await _handle_setting(plugin, task_info_list, task_list) + if not task_info_list: + await TaskInfo.all().update(load_status=False) + return + module_dict = {t[1]: t[0] for t in await TaskInfo.all().values_list("id", "module")} + load_task = [] + create_list = [] + update_list = [] + for status, task in task_info_list: + if task.module not in module_dict: + create_list.append((status, task)) + else: + task.id = module_dict[task.module] + update_list.append(task) + load_task.append(task.module) + await to_db(load_task, create_list, update_list) + # db_task = await TaskInfo.filter(load_status=True, status=True).values_list( + # "module", flat=True + # ) + # task_list = [t for t in task_list if t.module in db_task] + # for task in task_list: + # create_schedule(task) diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index 6fc3dbf8..3e224f60 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -57,7 +57,7 @@ LG_MESSAGE = [ async def init_image(): SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) - await generate_progress_bar_pic() + # await generate_progress_bar_pic() clear_sign_data_pic() @@ -86,6 +86,7 @@ async def get_card( 返回: Path: 卡片路径 """ + await generate_progress_bar_pic() user_id = user.user_id date = datetime.now().date() _type = "view" if is_card_view else "sign" @@ -296,6 +297,10 @@ async def generate_progress_bar_pic(): """ 初始化进度条图片 """ + bar_white_file = SIGN_RESOURCE_PATH / "bar_white.png" + if bar_white_file.exists(): + return + bg_2 = (254, 1, 254) bg_1 = (0, 245, 246) @@ -335,7 +340,7 @@ async def generate_progress_bar_pic(): await bk.paste(img_y, (0, 0)) await bk.paste(A, (25, 0)) await bk.paste(img_x, (975, 0)) - await bk.save(SIGN_RESOURCE_PATH / "bar_white.png") + await bk.save(bar_white_file) def get_level_and_next_impression(impression: float) -> tuple[str, int | float, int]: diff --git a/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py b/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py index aca8baef..8753281a 100644 --- a/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py @@ -16,7 +16,7 @@ router = APIRouter(prefix="/menu") dependencies=[authentication()], response_model=Result[list[MenuData]], response_class=JSONResponse, - deprecated="获取菜单列表", # type: ignore + description="获取菜单列表", ) async def _() -> Result[list[MenuData]]: try: diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py index 81b66185..fa719cdf 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py @@ -23,7 +23,7 @@ driver = nonebot.get_driver() dependencies=[authentication()], response_model=Result[list[BotInfo]], response_class=JSONResponse, - deprecated="获取bot列表", # type: ignore + description="获取bot列表", # type: ignore ) async def _() -> Result[list[BotInfo]]: try: @@ -74,7 +74,7 @@ async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]: dependencies=[authentication()], response_model=Result[ChatCallMonthCount], response_class=JSONResponse, - deprecated="获取聊天/调用记录的一个月数量", # type: ignore + description="获取聊天/调用记录的一个月数量", # type: ignore ) async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]: try: @@ -91,7 +91,7 @@ async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]: dependencies=[authentication()], response_model=Result[BaseResultModel], response_class=JSONResponse, - deprecated="获取Bot连接记录", # type: ignore + description="获取Bot连接记录", # type: ignore ) async def _(query: QueryModel) -> Result[BaseResultModel]: try: @@ -106,7 +106,7 @@ async def _(query: QueryModel) -> Result[BaseResultModel]: dependencies=[authentication()], response_model=Result[Config], response_class=JSONResponse, - deprecated="获取nb配置", # type: ignore + description="获取nb配置", # type: ignore ) async def _() -> Result[Config]: return Result.ok(driver.config) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py index 87288465..dfa9dd31 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py @@ -85,9 +85,6 @@ async def _(group: UpdateGroup) -> Result[str]: description="获取好友列表", ) async def _(bot_id: str) -> Result[list[Friend]]: - """ - 获取群信息 - """ try: bot = nonebot.get_bot(bot_id) friend_list, _ = await PlatformUtils.get_friend_list(bot) @@ -95,7 +92,7 @@ async def _(bot_id: str) -> Result[list[Friend]]: for f in friend_list: ava_url = AVA_URL.format(f.user_id) result_list.append( - Friend(user_id=f.user_id, nickname=f.nickname, ava_url=ava_url) + Friend(user_id=f.user_id, nickname=f.user_name or "", ava_url=ava_url) ) return Result.ok( result_list, diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py index 3e45ad03..86d08057 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -24,7 +24,7 @@ router = APIRouter(prefix="/plugin") dependencies=[authentication()], response_model=Result[list[PluginInfo]], response_class=JSONResponse, - deprecated="获取插件列表", # type: ignore + description="获取插件列表", # type: ignore ) async def _( plugin_type: list[PluginType] = Query(None), menu_type: str | None = None @@ -43,7 +43,7 @@ async def _( dependencies=[authentication()], response_model=Result[int], response_class=JSONResponse, - deprecated="获取插件数量", # type: ignore + description="获取插件数量", # type: ignore ) async def _() -> Result[int]: try: diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/store.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/store.py index 068c9ee5..84599e51 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/store.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/store.py @@ -16,7 +16,7 @@ router = APIRouter(prefix="/store") dependencies=[authentication()], response_model=Result[dict], response_class=JSONResponse, - deprecated="获取插件商店插件信息", # type: ignore + description="获取插件商店插件信息", # type: ignore ) async def _() -> Result[dict]: try: @@ -41,7 +41,7 @@ async def _() -> Result[dict]: dependencies=[authentication()], response_model=Result, response_class=JSONResponse, - deprecated="安装插件", # type: ignore + description="安装插件", # type: ignore ) async def _(param: PluginIr) -> Result: try: @@ -59,7 +59,7 @@ async def _(param: PluginIr) -> Result: dependencies=[authentication()], response_model=Result, response_class=JSONResponse, - deprecated="更新插件", # type: ignore + description="更新插件", # type: ignore ) async def _(param: PluginIr) -> Result: try: @@ -77,7 +77,7 @@ async def _(param: PluginIr) -> Result: dependencies=[authentication()], response_model=Result, response_class=JSONResponse, - deprecated="移除插件", # type: ignore + description="移除插件", # type: ignore ) async def _(param: PluginIr) -> Result: try: diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index 5518e1c3..f4e1c48b 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -1,7 +1,8 @@ from collections.abc import Callable import copy +from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Literal import cattrs from pydantic import BaseModel @@ -157,6 +158,29 @@ class PluginSetting(BaseModel): """调用插件花费金币""" +class SchedulerModel(BaseModel): + trigger: Literal["date", "interval", "cron"] + """trigger""" + day: int | None = None + """天数""" + hour: int | None = None + """小时""" + minute: int | None = None + """分钟""" + second: int | None = None + """秒""" + run_date: datetime | None = None + """运行日期""" + id: str | None = None + """id""" + max_instances: int | None = None + """最大运行实例""" + args: list | None = None + """参数""" + kwargs: dict | None = None + """参数""" + + class Task(BaseBlock): module: str """被动技能模块名""" @@ -168,8 +192,14 @@ class Task(BaseBlock): """初次加载默认开关状态""" default_status: bool = True """进群时默认状态""" - run_time: str | None = None - """运行时间""" + scheduler: SchedulerModel | None = None + """定时任务配置""" + run_func: Callable | None = None + """运行函数""" + check: Callable | None = None + """检查函数""" + check_args: list = [] + """检查函数参数""" class PluginExtraData(BaseModel): diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py index 56cd7015..bcb4bb65 100644 --- a/zhenxun/models/task_info.py +++ b/zhenxun/models/task_info.py @@ -12,6 +12,8 @@ class TaskInfo(Model): """被动技能名称""" status = fields.BooleanField(default=True, description="全局开关状态") """全局开关状态""" + load_status = fields.BooleanField(default=True, description="进群默认开关状态") + """加载状态""" default_status = fields.BooleanField(default=True, description="进群默认开关状态") """全局开关状态""" run_time = fields.CharField(255, null=True, description="运行时间") @@ -27,5 +29,6 @@ class TaskInfo(Model): async def _run_script(cls): return [ "ALTER TABLE task_info ADD default_status boolean DEFAULT true;", + "ALTER TABLE task_info ADD load_status boolean DEFAULT false;", # 默认状态 ] diff --git a/zhenxun/utils/http_utils.py b/zhenxun/utils/http_utils.py index 00c4fe28..52bf4461 100644 --- a/zhenxun/utils/http_utils.py +++ b/zhenxun/utils/http_utils.py @@ -285,6 +285,7 @@ class AsyncHttpx: response.raise_for_status() logger.info( f"开始下载 {path.name}.. " + f"Url: {u}.. " f"Path: {path.absolute()}" ) async with aiofiles.open(path, "wb") as wf: diff --git a/zhenxun/utils/manager/resource_manager.py b/zhenxun/utils/manager/resource_manager.py new file mode 100644 index 00000000..ea4d63c3 --- /dev/null +++ b/zhenxun/utils/manager/resource_manager.py @@ -0,0 +1,79 @@ +import os +from pathlib import Path +import shutil +import zipfile + +from zhenxun.configs.path_config import FONT_PATH +from zhenxun.services.log import logger +from zhenxun.utils.github_utils import GithubUtils +from zhenxun.utils.http_utils import AsyncHttpx + +CMD_STRING = "ResourceManager" + + +class ResourceManager: + GITHUB_URL = "https://github.com/zhenxun-org/zhenxun-bot-resources/tree/main" + + RESOURCE_PATH = Path() / "resources" + + TMP_PATH = Path() / "_resource_tmp" + + ZIP_FILE = TMP_PATH / "resources.zip" + + UNZIP_PATH = None + + @classmethod + async def init_resources(cls): + if FONT_PATH.exists() and os.listdir(FONT_PATH): + return + if cls.TMP_PATH.exists(): + logger.debug( + "resources临时文件夹已存在,移除resources临时文件夹", CMD_STRING + ) + shutil.rmtree(cls.TMP_PATH) + cls.TMP_PATH.mkdir(parents=True, exist_ok=True) + try: + await cls.__download_resources() + cls.file_handle() + except Exception as e: + logger.error("获取resources资源包失败", CMD_STRING, e=e) + if cls.TMP_PATH.exists(): + logger.debug("移除resources临时文件夹", CMD_STRING) + shutil.rmtree(cls.TMP_PATH) + + @classmethod + def file_handle(cls): + if not cls.UNZIP_PATH: + return + cls.__recursive_folder(cls.UNZIP_PATH, "resources") + + @classmethod + def __recursive_folder(cls, dir: Path, parent_path: str): + for file in dir.iterdir(): + if file.is_dir(): + cls.__recursive_folder(file, f"{parent_path}/{file.name}") + else: + res_file = Path(parent_path) / file.name + if res_file.exists(): + res_file.unlink() + res_file.parent.mkdir(parents=True, exist_ok=True) + file.rename(res_file) + + @classmethod + async def __download_resources(cls): + """获取resources文件夹""" + repo_info = GithubUtils.parse_github_url(cls.GITHUB_URL) + url = await repo_info.get_archive_download_urls() + logger.debug("开始下载resources资源包...", CMD_STRING) + if not await AsyncHttpx.download_file(url, cls.ZIP_FILE, stream=True): + raise Exception("下载resources资源包失败...") + logger.debug("下载resources资源文件压缩包完成...", CMD_STRING) + tf = zipfile.ZipFile(cls.ZIP_FILE) + tf.extractall(cls.TMP_PATH) + logger.debug("解压文件压缩包完成...", CMD_STRING) + download_file_path = cls.TMP_PATH / next( + x for x in os.listdir(cls.TMP_PATH) if (cls.TMP_PATH / x).is_dir() + ) + cls.UNZIP_PATH = download_file_path / "resources" + if tf: + tf.close()