From 14f5842f10da5161fd6adc3d0d6d8aabbc237220 Mon Sep 17 00:00:00 2001 From: xuanerwa <58063798+xuanerwa@users.noreply.github.com> Date: Fri, 20 Jun 2025 19:08:06 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E9=87=8D=E6=9E=84=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=95=86=E5=BA=97=EF=BC=8C=E6=94=AF=E6=8C=81Gitee?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91=E5=92=8C=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=20(#1931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 + .../plugin_store/test_add_plugin.py | 2 +- .../plugin_store/test_search_plugin.py | 4 +- .../plugin_store/test_update_all_plugin.py | 4 +- .../plugin_store/test_update_plugin.py | 4 +- .../response/plugin_store/basic_plugins.json | 16 +- .../response/plugin_store/extra_plugins.json | 10 +- .../builtin_plugins/plugin_store/__init__.py | 14 +- .../builtin_plugins/plugin_store/config.py | 2 + .../plugin_store/data_source.py | 292 ++++++++++-------- .../builtin_plugins/plugin_store/models.py | 23 ++ .../web_ui/api/tabs/plugin_manage/store.py | 21 +- zhenxun/configs/utils/__init__.py | 4 +- zhenxun/utils/github_utils/const.py | 3 + zhenxun/utils/github_utils/func.py | 2 + zhenxun/utils/html_template/__init__.py | 1 + zhenxun/utils/html_template/component.py | 36 +++ .../utils/html_template/components/title.py | 15 + zhenxun/utils/html_template/container.py | 31 ++ .../manager/virtual_env_package_manager.py | 159 ++++++++++ 20 files changed, 472 insertions(+), 173 deletions(-) create mode 100644 zhenxun/utils/html_template/__init__.py create mode 100644 zhenxun/utils/html_template/component.py create mode 100644 zhenxun/utils/html_template/components/title.py create mode 100644 zhenxun/utils/html_template/container.py create mode 100644 zhenxun/utils/manager/virtual_env_package_manager.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b227fb6..e6830243 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,8 @@ "displayname", "flmt", "getbbox", + "gitcode", + "GITEE", "hibiapi", "httpx", "jsdelivr", diff --git a/tests/builtin_plugins/plugin_store/test_add_plugin.py b/tests/builtin_plugins/plugin_store/test_add_plugin.py index 3dd2ebbb..5a0edab8 100644 --- a/tests/builtin_plugins/plugin_store/test_add_plugin.py +++ b/tests/builtin_plugins/plugin_store/test_add_plugin.py @@ -359,7 +359,7 @@ async def test_add_plugin_exist( init_mocked_api(mocked_api=mocked_api) mocker.patch( - "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + "zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins", return_value=[("search_image", "0.1")], ) plugin_id = 1 diff --git a/tests/builtin_plugins/plugin_store/test_search_plugin.py b/tests/builtin_plugins/plugin_store/test_search_plugin.py index 8bc6876e..404fee5e 100644 --- a/tests/builtin_plugins/plugin_store/test_search_plugin.py +++ b/tests/builtin_plugins/plugin_store/test_search_plugin.py @@ -57,7 +57,7 @@ async def test_search_plugin_name( ) ctx.receive_event(bot=bot, event=event) mock_table_page.assert_awaited_once_with( - "插件列表", + "商店插件列表", "通过添加/移除插件 ID 来管理插件", ["-", "ID", "名称", "简介", "作者", "版本", "类型"], [ @@ -123,7 +123,7 @@ async def test_search_plugin_author( ) ctx.receive_event(bot=bot, event=event) mock_table_page.assert_awaited_once_with( - "插件列表", + "商店插件列表", "通过添加/移除插件 ID 来管理插件", ["-", "ID", "名称", "简介", "作者", "版本", "类型"], [ diff --git a/tests/builtin_plugins/plugin_store/test_update_all_plugin.py b/tests/builtin_plugins/plugin_store/test_update_all_plugin.py index 2a490da7..95360f6b 100644 --- a/tests/builtin_plugins/plugin_store/test_update_all_plugin.py +++ b/tests/builtin_plugins/plugin_store/test_update_all_plugin.py @@ -32,7 +32,7 @@ async def test_update_all_plugin_basic_need_update( new=tmp_path / "zhenxun", ) mocker.patch( - "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + "zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins", return_value=[("search_image", "0.0")], ) @@ -87,7 +87,7 @@ async def test_update_all_plugin_basic_is_new( new=tmp_path / "zhenxun", ) mocker.patch( - "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + "zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins", return_value=[("search_image", "0.1")], ) diff --git a/tests/builtin_plugins/plugin_store/test_update_plugin.py b/tests/builtin_plugins/plugin_store/test_update_plugin.py index 952191d6..2cb88d1b 100644 --- a/tests/builtin_plugins/plugin_store/test_update_plugin.py +++ b/tests/builtin_plugins/plugin_store/test_update_plugin.py @@ -32,7 +32,7 @@ async def test_update_plugin_basic_need_update( new=tmp_path / "zhenxun", ) mocker.patch( - "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + "zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins", return_value=[("search_image", "0.0")], ) @@ -87,7 +87,7 @@ async def test_update_plugin_basic_is_new( new=tmp_path / "zhenxun", ) mocker.patch( - "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + "zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins", return_value=[("search_image", "0.1")], ) diff --git a/tests/response/plugin_store/basic_plugins.json b/tests/response/plugin_store/basic_plugins.json index 7459e2ec..f0306836 100644 --- a/tests/response/plugin_store/basic_plugins.json +++ b/tests/response/plugin_store/basic_plugins.json @@ -1,5 +1,6 @@ -{ - "鸡汤": { +[ + { + "name": "鸡汤", "module": "jitang", "module_path": "plugins.alapi.jitang", "description": "喏,亲手为你煮的鸡汤", @@ -9,7 +10,8 @@ "plugin_type": "NORMAL", "is_dir": false }, - "识图": { + { + "name": "识图", "module": "search_image", "module_path": "plugins.search_image", "description": "以图搜图,看破本源", @@ -19,7 +21,8 @@ "plugin_type": "NORMAL", "is_dir": true }, - "网易云热评": { + { + "name": "网易云热评", "module": "comments_163", "module_path": "plugins.alapi.comments_163", "description": "生了个人,我很抱歉", @@ -29,7 +32,8 @@ "plugin_type": "NORMAL", "is_dir": false }, - "B站订阅": { + { + "name": "B站订阅", "module": "bilibili_sub", "module_path": "plugins.bilibili_sub", "description": "非常便利的B站订阅通知", @@ -39,4 +43,4 @@ "plugin_type": "NORMAL", "is_dir": true } -} +] diff --git a/tests/response/plugin_store/extra_plugins.json b/tests/response/plugin_store/extra_plugins.json index 9d92f859..ca5e7f0a 100644 --- a/tests/response/plugin_store/extra_plugins.json +++ b/tests/response/plugin_store/extra_plugins.json @@ -1,5 +1,6 @@ -{ - "github订阅": { +[ + { + "name": "github订阅", "module": "github_sub", "module_path": "github_sub", "description": "订阅github用户或仓库", @@ -10,7 +11,8 @@ "is_dir": true, "github_url": "https://github.com/xuanerwa/zhenxun_github_sub" }, - "Minecraft查服": { + { + "name": "Minecraft查服", "module": "mc_check", "module_path": "mc_check", "description": "Minecraft服务器状态查询,支持IPv6", @@ -21,4 +23,4 @@ "is_dir": true, "github_url": "https://github.com/molanp/zhenxun_check_Minecraft" } -} +] diff --git a/zhenxun/builtin_plugins/plugin_store/__init__.py b/zhenxun/builtin_plugins/plugin_store/__init__.py index 7e9f52a0..72d6d7dd 100644 --- a/zhenxun/builtin_plugins/plugin_store/__init__.py +++ b/zhenxun/builtin_plugins/plugin_store/__init__.py @@ -9,7 +9,7 @@ from zhenxun.utils.enum import PluginType from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import is_number -from .data_source import ShopManage +from .data_source import StoreManager __plugin_meta__ = PluginMetadata( name="插件商店", @@ -82,7 +82,7 @@ _matcher.shortcut( @_matcher.assign("$main") async def _(session: EventSession): try: - result = await ShopManage.get_plugins_info() + result = await StoreManager.get_plugins_info() logger.info("查看插件列表", "插件商店", session=session) await MessageUtils.build_message(result).send() except Exception as e: @@ -97,7 +97,7 @@ async def _(session: EventSession, plugin_id: str): await MessageUtils.build_message(f"正在添加插件 Id: {plugin_id}").send() else: await MessageUtils.build_message(f"正在添加插件 Module: {plugin_id}").send() - result = await ShopManage.add_plugin(plugin_id) + result = await StoreManager.add_plugin(plugin_id) except Exception as e: logger.error(f"添加插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) await MessageUtils.build_message( @@ -110,7 +110,7 @@ async def _(session: EventSession, plugin_id: str): @_matcher.assign("remove") async def _(session: EventSession, plugin_id: str): try: - result = await ShopManage.remove_plugin(plugin_id) + result = await StoreManager.remove_plugin(plugin_id) except Exception as e: logger.error(f"移除插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) await MessageUtils.build_message( @@ -123,7 +123,7 @@ async def _(session: EventSession, plugin_id: str): @_matcher.assign("search") async def _(session: EventSession, plugin_name_or_author: str): try: - result = await ShopManage.search_plugin(plugin_name_or_author) + result = await StoreManager.search_plugin(plugin_name_or_author) except Exception as e: logger.error( f"搜索插件 name: {plugin_name_or_author}失败", @@ -145,7 +145,7 @@ async def _(session: EventSession, plugin_id: str): await MessageUtils.build_message(f"正在更新插件 Id: {plugin_id}").send() else: await MessageUtils.build_message(f"正在更新插件 Module: {plugin_id}").send() - result = await ShopManage.update_plugin(plugin_id) + result = await StoreManager.update_plugin(plugin_id) except Exception as e: logger.error(f"更新插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) await MessageUtils.build_message( @@ -159,7 +159,7 @@ async def _(session: EventSession, plugin_id: str): async def _(session: EventSession): try: await MessageUtils.build_message("正在更新全部插件").send() - result = await ShopManage.update_all_plugin() + result = await StoreManager.update_all_plugin() except Exception as e: logger.error("更新全部插件失败", "插件商店", session=session, e=e) await MessageUtils.build_message(f"更新全部插件失败 e: {e}").finish() diff --git a/zhenxun/builtin_plugins/plugin_store/config.py b/zhenxun/builtin_plugins/plugin_store/config.py index dacaffec..dd48a5c7 100644 --- a/zhenxun/builtin_plugins/plugin_store/config.py +++ b/zhenxun/builtin_plugins/plugin_store/config.py @@ -9,3 +9,5 @@ DEFAULT_GITHUB_URL = "https://github.com/zhenxun-org/zhenxun_bot_plugins/tree/ma EXTRA_GITHUB_URL = "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/tree/index" """插件库索引github仓库地址""" + +LOG_COMMAND = "插件商店" diff --git a/zhenxun/builtin_plugins/plugin_store/data_source.py b/zhenxun/builtin_plugins/plugin_store/data_source.py index 1df053f9..6633d404 100644 --- a/zhenxun/builtin_plugins/plugin_store/data_source.py +++ b/zhenxun/builtin_plugins/plugin_store/data_source.py @@ -1,6 +1,5 @@ from pathlib import Path import shutil -import subprocess from aiocache import cached import ujson as json @@ -14,9 +13,15 @@ from zhenxun.utils.github_utils import GithubUtils from zhenxun.utils.github_utils.models import RepoAPI from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle +from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager from zhenxun.utils.utils import is_number -from .config import BASE_PATH, DEFAULT_GITHUB_URL, EXTRA_GITHUB_URL +from .config import ( + BASE_PATH, + DEFAULT_GITHUB_URL, + EXTRA_GITHUB_URL, + LOG_COMMAND, +) BAT_FILE = Path() / "win启动.bat" @@ -45,74 +50,69 @@ def install_requirement(plugin_path: Path): requirement_files = ["requirement.txt", "requirements.txt"] requirement_paths = [plugin_path / file for file in requirement_files] - existing_requirements = next( + if existing_requirements := next( (path for path in requirement_paths if path.exists()), None - ) - - if not existing_requirements: - logger.debug( - f"No requirement.txt found for plugin: {plugin_path.name}", "插件管理" - ) - return - - try: - command = WIN_COMMAND if BAT_FILE.exists() else DEFAULT_COMMAND - command.append(str(existing_requirements)) - result = subprocess.run( - command, - check=True, - capture_output=True, - text=True, - ) - logger.debug( - "Successfully installed dependencies for" - f" plugin: {plugin_path.name}. Output:\n{result.stdout}", - "插件管理", - ) - except subprocess.CalledProcessError: - logger.error( - f"Failed to install dependencies for plugin: {plugin_path.name}. " - " Error:\n{e.stderr}" - ) + ): + VirtualEnvPackageManager.install_requirement(existing_requirements) -class ShopManage: +class StoreManager: @classmethod - @cached(60) - async def get_data(cls) -> dict[str, StorePluginInfo]: - """获取插件信息数据 - - 异常: - ValueError: 访问请求失败 + async def get_github_plugins(cls) -> list[StorePluginInfo]: + """获取github插件列表信息 返回: - dict: 插件信息数据 + list[StorePluginInfo]: 插件列表数据 """ - default_github_repo = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL) - extra_github_repo = GithubUtils.parse_github_url(EXTRA_GITHUB_URL) - for repo_info in [default_github_repo, extra_github_repo]: - if await repo_info.update_repo_commit(): - logger.info(f"获取最新提交: {repo_info.branch}", "插件管理") - else: - logger.warning(f"获取最新提交失败: {repo_info}", "插件管理") - default_github_url = await default_github_repo.get_raw_download_urls( - "plugins.json" - ) - extra_github_url = await extra_github_repo.get_raw_download_urls("plugins.json") - res = await AsyncHttpx.get(default_github_url, check_status_code=200) - res2 = await AsyncHttpx.get(extra_github_url, check_status_code=200) + repo_info = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL) + if await repo_info.update_repo_commit(): + logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND) + else: + logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND) + default_github_url = await repo_info.get_raw_download_urls("plugins.json") + response = await AsyncHttpx.get(default_github_url, check_status_code=200) + if response.status_code == 200: + logger.info("获取github插件列表成功", LOG_COMMAND) + return [StorePluginInfo(**detail) for detail in json.loads(response.text)] + else: + logger.warning( + f"获取github插件列表失败: {response.status_code}", LOG_COMMAND + ) + return [] - # 检查请求结果 - if res.status_code != 200 or res2.status_code != 200: - raise ValueError(f"下载错误, code: {res.status_code}, {res2.status_code}") + @classmethod + async def get_extra_plugins(cls) -> list[StorePluginInfo]: + """获取额外插件列表信息 - # 解析并合并返回的 JSON 数据 - data1 = json.loads(res.text) - data2 = json.loads(res2.text) - return { - name: StorePluginInfo(**detail) - for name, detail in {**data1, **data2}.items() - } + 返回: + list[StorePluginInfo]: 插件列表数据 + """ + repo_info = GithubUtils.parse_github_url(EXTRA_GITHUB_URL) + if await repo_info.update_repo_commit(): + logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND) + else: + logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND) + extra_github_url = await repo_info.get_raw_download_urls("plugins.json") + response = await AsyncHttpx.get(extra_github_url, check_status_code=200) + if response.status_code == 200: + return [StorePluginInfo(**detail) for detail in json.loads(response.text)] + else: + logger.warning( + f"获取github扩展插件列表失败: {response.status_code}", LOG_COMMAND + ) + return [] + + @classmethod + @cached(60) + async def get_data(cls) -> list[StorePluginInfo]: + """获取插件信息数据 + + 返回: + list[StorePluginInfo]: 插件信息数据 + """ + plugins = await cls.get_github_plugins() + extra_plugins = await cls.get_extra_plugins() + return [*plugins, *extra_plugins] @classmethod def version_check(cls, plugin_info: StorePluginInfo, suc_plugin: dict[str, str]): @@ -120,7 +120,7 @@ class ShopManage: 参数: plugin_info: StorePluginInfo - suc_plugin: dict[str, str] + suc_plugin: 模块名: 版本号 返回: str: 版本号 @@ -140,7 +140,7 @@ class ShopManage: 参数: plugin_info: StorePluginInfo - suc_plugin: dict[str, str] + suc_plugin: 模块名: 版本号 返回: bool: 是否有更新 @@ -164,21 +164,21 @@ class ShopManage: 返回: BuildImage | str: 返回消息 """ - data: dict[str, StorePluginInfo] = await cls.get_data() + plugin_list: list[StorePluginInfo] = await cls.get_data() column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"] - plugin_list = await cls.get_loaded_plugins("module", "version") - suc_plugin = {p[0]: (p[1] or "0.1") for p in plugin_list} + db_plugin_list = await cls.get_loaded_plugins("module", "version") + suc_plugin = {p[0]: (p[1] or "0.1") for p in db_plugin_list} data_list = [ [ - "已安装" if plugin_info[1].module in suc_plugin else "", + "已安装" if plugin_info.module in suc_plugin else "", id, - plugin_info[0], - plugin_info[1].description, - plugin_info[1].author, - cls.version_check(plugin_info[1], suc_plugin), - plugin_info[1].plugin_type_name, + plugin_info.name, + plugin_info.description, + plugin_info.author, + cls.version_check(plugin_info, suc_plugin), + plugin_info.plugin_type_name, ] - for id, plugin_info in enumerate(data.items()) + for id, plugin_info in enumerate(plugin_list) ] return await ImageTemplate.table_page( "插件列表", @@ -198,15 +198,15 @@ class ShopManage: 返回: str: 返回消息 """ - data: dict[str, StorePluginInfo] = await cls.get_data() + plugin_list: list[StorePluginInfo] = await cls.get_data() try: plugin_key = await cls._resolve_plugin_key(plugin_id) except ValueError as e: return str(e) - plugin_list = await cls.get_loaded_plugins("module") - plugin_info = data[plugin_key] - if plugin_info.module in [p[0] for p in plugin_list]: - return f"插件 {plugin_key} 已安装,无需重复安装" + db_plugin_list = await cls.get_loaded_plugins("module") + plugin_info = next(p for p in plugin_list if p.module == plugin_key) + if plugin_info.module in [p[0] for p in db_plugin_list]: + return f"插件 {plugin_info.name} 已安装,无需重复安装" is_external = True if plugin_info.github_url is None: plugin_info.github_url = DEFAULT_GITHUB_URL @@ -215,34 +215,39 @@ class ShopManage: if len(version_split) > 1: github_url_split = plugin_info.github_url.split("/tree/") plugin_info.github_url = f"{github_url_split[0]}/tree/{version_split[1]}" - logger.info(f"正在安装插件 {plugin_key}...") + logger.info(f"正在安装插件 {plugin_info.name}...", LOG_COMMAND) await cls.install_plugin_with_repo( plugin_info.github_url, plugin_info.module_path, plugin_info.is_dir, is_external, ) - return f"插件 {plugin_key} 安装成功! 重启后生效" + return f"插件 {plugin_info.name} 安装成功! 重启后生效" @classmethod async def install_plugin_with_repo( - cls, github_url: str, module_path: str, is_dir: bool, is_external: bool = False + cls, + github_url: str, + module_path: str, + is_dir: bool, + is_external: bool = False, ): - files: list[str] repo_api: RepoAPI repo_info = GithubUtils.parse_github_url(github_url) if await repo_info.update_repo_commit(): - logger.info(f"获取最新提交: {repo_info.branch}", "插件管理") + logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND) else: - logger.warning(f"获取最新提交失败: {repo_info}", "插件管理") - logger.debug(f"成功获取仓库信息: {repo_info}", "插件管理") + logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND) + logger.debug(f"成功获取仓库信息: {repo_info}", LOG_COMMAND) for repo_api in GithubUtils.iter_api_strategies(): try: await repo_api.parse_repo_info(repo_info) break except Exception as e: logger.warning( - f"获取插件文件失败: {e} | API类型: {repo_api.strategy}", "插件管理" + f"获取插件文件失败 | API类型: {repo_api.strategy}", + LOG_COMMAND, + e=e, ) continue else: @@ -258,7 +263,7 @@ class ShopManage: base_path = BASE_PATH / "plugins" if is_external else BASE_PATH base_path = base_path if module_path else base_path / repo_info.repo download_paths: list[Path | str] = [base_path / file for file in files] - logger.debug(f"插件下载路径: {download_paths}", "插件管理") + logger.debug(f"插件下载路径: {download_paths}", LOG_COMMAND) result = await AsyncHttpx.gather_download_file(download_urls, download_paths) for _id, success in enumerate(result): if not success: @@ -273,12 +278,12 @@ class ShopManage: req_files.extend( repo_api.get_files(f"{replace_module_path}/requirement.txt", False) ) - logger.debug(f"获取插件依赖文件列表: {req_files}", "插件管理") + logger.debug(f"获取插件依赖文件列表: {req_files}", LOG_COMMAND) req_download_urls = [ await repo_info.get_raw_download_urls(file) for file in req_files ] req_paths: list[Path | str] = [plugin_path / file for file in req_files] - logger.debug(f"插件依赖文件下载路径: {req_paths}", "插件管理") + logger.debug(f"插件依赖文件下载路径: {req_paths}", LOG_COMMAND) if req_files: result = await AsyncHttpx.gather_download_file( req_download_urls, req_paths @@ -286,7 +291,7 @@ class ShopManage: for success in result: if not success: raise Exception("插件依赖文件下载失败") - logger.debug(f"插件依赖文件列表: {req_paths}", "插件管理") + logger.debug(f"插件依赖文件列表: {req_paths}", LOG_COMMAND) install_requirement(plugin_path) except ValueError as e: logger.warning("未获取到依赖文件路径...", e=e) @@ -303,12 +308,12 @@ class ShopManage: 返回: str: 返回消息 """ - data: dict[str, StorePluginInfo] = await cls.get_data() + plugin_list: list[StorePluginInfo] = await cls.get_data() try: plugin_key = await cls._resolve_plugin_key(plugin_id) except ValueError as e: return str(e) - plugin_info = data[plugin_key] + plugin_info = next(p for p in plugin_list if p.module == plugin_key) path = BASE_PATH if plugin_info.github_url: path = BASE_PATH / "plugins" @@ -317,14 +322,14 @@ class ShopManage: if not plugin_info.is_dir: path = Path(f"{path}.py") if not path.exists(): - return f"插件 {plugin_key} 不存在..." - logger.debug(f"尝试移除插件 {plugin_key} 文件: {path}", "插件管理") + return f"插件 {plugin_info.name} 不存在..." + logger.debug(f"尝试移除插件 {plugin_info.name} 文件: {path}", LOG_COMMAND) if plugin_info.is_dir: shutil.rmtree(path) else: path.unlink() await PluginInitManager.remove(f"zhenxun.{plugin_info.module_path}") - return f"插件 {plugin_key} 移除成功! 重启后生效" + return f"插件 {plugin_info.name} 移除成功! 重启后生效" @classmethod async def search_plugin(cls, plugin_name_or_author: str) -> BuildImage | str: @@ -336,25 +341,25 @@ class ShopManage: 返回: BuildImage | str: 返回消息 """ - data: dict[str, StorePluginInfo] = await cls.get_data() - plugin_list = await cls.get_loaded_plugins("module", "version") - suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list} + plugin_list: list[StorePluginInfo] = await cls.get_data() + db_plugin_list = await cls.get_loaded_plugins("module", "version") + suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list} filtered_data = [ (id, plugin_info) - for id, plugin_info in enumerate(data.items()) - if plugin_name_or_author.lower() in plugin_info[0].lower() - or plugin_name_or_author.lower() in plugin_info[1].author.lower() + for id, plugin_info in enumerate(plugin_list) + if plugin_name_or_author.lower() in plugin_info.name.lower() + or plugin_name_or_author.lower() in plugin_info.author.lower() ] data_list = [ [ - "已安装" if plugin_info[1].module in suc_plugin else "", + "已安装" if plugin_info.module in suc_plugin else "", id, - plugin_info[0], - plugin_info[1].description, - plugin_info[1].author, - cls.version_check(plugin_info[1], suc_plugin), - plugin_info[1].plugin_type_name, + plugin_info.name, + plugin_info.description, + plugin_info.author, + cls.version_check(plugin_info, suc_plugin), + plugin_info.plugin_type_name, ] for id, plugin_info in filtered_data ] @@ -362,7 +367,7 @@ class ShopManage: return "未找到相关插件..." column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"] return await ImageTemplate.table_page( - "插件列表", + "商店插件列表", "通过添加/移除插件 ID 来管理插件", column_name, data_list, @@ -379,20 +384,20 @@ class ShopManage: 返回: str: 返回消息 """ - data: dict[str, StorePluginInfo] = await cls.get_data() + plugin_list: list[StorePluginInfo] = await cls.get_data() try: plugin_key = await cls._resolve_plugin_key(plugin_id) except ValueError as e: return str(e) - logger.info(f"尝试更新插件 {plugin_key}", "插件管理") - plugin_info = data[plugin_key] - plugin_list = await cls.get_loaded_plugins("module", "version") - suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list} - if plugin_info.module not in [p[0] for p in plugin_list]: - return f"插件 {plugin_key} 未安装,无法更新" - logger.debug(f"当前插件列表: {suc_plugin}", "插件管理") + plugin_info = next(p for p in plugin_list if p.module == plugin_key) + logger.info(f"尝试更新插件 {plugin_info.name}", LOG_COMMAND) + db_plugin_list = await cls.get_loaded_plugins("module", "version") + suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list} + if plugin_info.module not in [p[0] for p in db_plugin_list]: + return f"插件 {plugin_info.name} 未安装,无法更新" + logger.debug(f"当前插件列表: {suc_plugin}", LOG_COMMAND) if cls.check_version_is_new(plugin_info, suc_plugin): - return f"插件 {plugin_key} 已是最新版本" + return f"插件 {plugin_info.name} 已是最新版本" is_external = True if plugin_info.github_url is None: plugin_info.github_url = DEFAULT_GITHUB_URL @@ -403,7 +408,7 @@ class ShopManage: plugin_info.is_dir, is_external, ) - return f"插件 {plugin_key} 更新成功! 重启后生效" + return f"插件 {plugin_info.name} 更新成功! 重启后生效" @classmethod async def update_all_plugin(cls) -> str: @@ -415,24 +420,33 @@ class ShopManage: 返回: str: 返回消息 """ - data: dict[str, StorePluginInfo] = await cls.get_data() - plugin_list = list(data.keys()) + plugin_list: list[StorePluginInfo] = await cls.get_data() + plugin_name_list = [p.name for p in plugin_list] update_failed_list = [] update_success_list = [] result = "--已更新{}个插件 {}个失败 {}个成功--" - logger.info(f"尝试更新全部插件 {plugin_list}", "插件管理") - for plugin_key in plugin_list: + logger.info(f"尝试更新全部插件 {plugin_name_list}", LOG_COMMAND) + for plugin_info in plugin_list: try: - plugin_info = data[plugin_key] - plugin_list = await cls.get_loaded_plugins("module", "version") - suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list} - if plugin_info.module not in [p[0] for p in plugin_list]: - logger.debug(f"插件 {plugin_key} 未安装,跳过", "插件管理") + db_plugin_list = await cls.get_loaded_plugins("module", "version") + suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list} + if plugin_info.module not in [p[0] for p in db_plugin_list]: + logger.debug( + f"插件 {plugin_info.name}({plugin_info.module}) 未安装,跳过", + LOG_COMMAND, + ) continue if cls.check_version_is_new(plugin_info, suc_plugin): - logger.debug(f"插件 {plugin_key} 已是最新版本,跳过", "插件管理") + logger.debug( + f"插件 {plugin_info.name}({plugin_info.module}) 已是最新版本" + ",跳过", + LOG_COMMAND, + ) continue - logger.info(f"正在更新插件 {plugin_key}", "插件管理") + logger.info( + f"正在更新插件 {plugin_info.name}({plugin_info.module})", + LOG_COMMAND, + ) is_external = True if plugin_info.github_url is None: plugin_info.github_url = DEFAULT_GITHUB_URL @@ -443,10 +457,14 @@ class ShopManage: plugin_info.is_dir, is_external, ) - update_success_list.append(plugin_key) + update_success_list.append(plugin_info.name) except Exception as e: - logger.error(f"更新插件 {plugin_key} 失败: {e}", "插件管理") - update_failed_list.append(plugin_key) + logger.error( + f"更新插件 {plugin_info.name}({plugin_info.module}) 失败", + LOG_COMMAND, + e=e, + ) + update_failed_list.append(plugin_info.name) if not update_success_list and not update_failed_list: return "全部插件已是最新版本" if update_success_list: @@ -468,13 +486,13 @@ class ShopManage: @classmethod async def _resolve_plugin_key(cls, plugin_id: str) -> str: - data: dict[str, StorePluginInfo] = await cls.get_data() + plugin_list: list[StorePluginInfo] = await cls.get_data() if is_number(plugin_id): idx = int(plugin_id) - if idx < 0 or idx >= len(data): + if idx < 0 or idx >= len(plugin_list): raise ValueError("插件ID不存在...") - return list(data.keys())[idx] + return plugin_list[idx].module elif isinstance(plugin_id, str): - if plugin_id not in [v.module for k, v in data.items()]: + if plugin_id not in [v.module for v in plugin_list]: raise ValueError("插件Module不存在...") - return {v.module: k for k, v in data.items()}[plugin_id] + return plugin_id diff --git a/zhenxun/builtin_plugins/plugin_store/models.py b/zhenxun/builtin_plugins/plugin_store/models.py index df65dd56..2bea1315 100644 --- a/zhenxun/builtin_plugins/plugin_store/models.py +++ b/zhenxun/builtin_plugins/plugin_store/models.py @@ -1,3 +1,5 @@ +from typing import Any, Literal + from nonebot.compat import model_dump from pydantic import BaseModel @@ -13,9 +15,30 @@ type2name: dict[str, str] = { } +class GiteeContents(BaseModel): + """Gitee Api内容""" + + type: Literal["file", "dir"] + """类型""" + size: Any + """文件大小""" + name: str + """文件名""" + path: str + """文件路径""" + url: str + """文件链接""" + html_url: str + """文件html链接""" + download_url: str + """文件raw链接""" + + class StorePluginInfo(BaseModel): """插件信息""" + name: str + """插件名""" module: str """模块名""" module_path: str 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 acff6356..9ee6ff41 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 @@ -1,6 +1,7 @@ from fastapi import APIRouter from fastapi.responses import JSONResponse from nonebot import require +from nonebot.compat import model_dump from zhenxun.models.plugin_info import PluginInfo from zhenxun.services.log import logger @@ -22,12 +23,12 @@ router = APIRouter(prefix="/store") async def _() -> Result[dict]: try: require("plugin_store") - from zhenxun.builtin_plugins.plugin_store import ShopManage + from zhenxun.builtin_plugins.plugin_store import StoreManager - data = await ShopManage.get_data() + data = await StoreManager.get_data() plugin_list = [ - {**data[name].to_dict(), "name": name, "id": idx} - for idx, name in enumerate(data) + {**model_dump(plugin), "name": plugin.name, "id": idx} + for idx, plugin in enumerate(data) ] modules = await PluginInfo.filter(load_status=True).values_list( "module", flat=True @@ -48,9 +49,9 @@ async def _() -> Result[dict]: async def _(param: PluginIr) -> Result: try: require("plugin_store") - from zhenxun.builtin_plugins.plugin_store import ShopManage + from zhenxun.builtin_plugins.plugin_store import StoreManager - result = await ShopManage.add_plugin(param.id) # type: ignore + result = await StoreManager.add_plugin(param.id) # type: ignore return Result.ok(info=result) except Exception as e: return Result.fail(f"安装插件失败: {type(e)}: {e}") @@ -66,9 +67,9 @@ async def _(param: PluginIr) -> Result: async def _(param: PluginIr) -> Result: try: require("plugin_store") - from zhenxun.builtin_plugins.plugin_store import ShopManage + from zhenxun.builtin_plugins.plugin_store import StoreManager - result = await ShopManage.update_plugin(param.id) # type: ignore + result = await StoreManager.update_plugin(param.id) # type: ignore return Result.ok(info=result) except Exception as e: return Result.fail(f"更新插件失败: {type(e)}: {e}") @@ -84,9 +85,9 @@ async def _(param: PluginIr) -> Result: async def _(param: PluginIr) -> Result: try: require("plugin_store") - from zhenxun.builtin_plugins.plugin_store import ShopManage + from zhenxun.builtin_plugins.plugin_store import StoreManager - result = await ShopManage.remove_plugin(param.id) # type: ignore + result = await StoreManager.remove_plugin(param.id) # type: ignore return Result.ok(info=result) except Exception as e: return Result.fail(f"移除插件失败: {type(e)}: {e}") diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index 03bc7331..5382d38d 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -454,9 +454,9 @@ class ConfigsManager: else config.default_value ) except Exception as e: - logger.warning( + logger.debug( f"配置项类型转换 MODULE: [{module}]" - " | KEY: [{key}]", + f" | KEY: [{key}] 将使用原始值", e=e, ) value = config.value or config.default_value diff --git a/zhenxun/utils/github_utils/const.py b/zhenxun/utils/github_utils/const.py index 23effa4c..68fffad9 100644 --- a/zhenxun/utils/github_utils/const.py +++ b/zhenxun/utils/github_utils/const.py @@ -21,6 +21,9 @@ CACHED_API_TTL = 300 RAW_CONTENT_FORMAT = "https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}" """raw content格式""" +GITEE_RAW_CONTENT_FORMAT = "https://gitee.com/{owner}/{repo}/raw/main/{path}" +"""gitee raw content格式""" + ARCHIVE_URL_FORMAT = "https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip" """archive url格式""" diff --git a/zhenxun/utils/github_utils/func.py b/zhenxun/utils/github_utils/func.py index b3f9a6f9..db3afa03 100644 --- a/zhenxun/utils/github_utils/func.py +++ b/zhenxun/utils/github_utils/func.py @@ -4,6 +4,7 @@ from zhenxun.utils.http_utils import AsyncHttpx from .const import ( ARCHIVE_URL_FORMAT, + GITEE_RAW_CONTENT_FORMAT, RAW_CONTENT_FORMAT, RELEASE_ASSETS_FORMAT, RELEASE_SOURCE_FORMAT, @@ -21,6 +22,7 @@ async def __get_fastest_formats(formats: dict[str, str]) -> list[str]: async def get_fastest_raw_formats() -> list[str]: """获取最快的raw下载地址格式""" formats: dict[str, str] = { + "https://gitee.com/": GITEE_RAW_CONTENT_FORMAT, "https://raw.githubusercontent.com/": RAW_CONTENT_FORMAT, "https://ghproxy.cc/": f"https://ghproxy.cc/{RAW_CONTENT_FORMAT}", "https://gh-proxy.com/": f"https://gh-proxy.com/{RAW_CONTENT_FORMAT}", diff --git a/zhenxun/utils/html_template/__init__.py b/zhenxun/utils/html_template/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/zhenxun/utils/html_template/__init__.py @@ -0,0 +1 @@ + diff --git a/zhenxun/utils/html_template/component.py b/zhenxun/utils/html_template/component.py new file mode 100644 index 00000000..c23ed503 --- /dev/null +++ b/zhenxun/utils/html_template/component.py @@ -0,0 +1,36 @@ +from abc import ABC +from typing import Literal + +from pydantic import BaseModel + + +class Style(BaseModel): + """常用样式""" + + padding: str = "0px" + margin: str = "0px" + border: str = "0px" + border_radius: str = "0px" + text_align: Literal["left", "right", "center"] = "left" + color: str = "#000" + font_size: str = "16px" + + +class Component(ABC): + def __init__(self, background_color: str = "#fff", is_container: bool = False): + self.extra_style = [] + self.style = Style() + self.background_color = background_color + self.is_container = is_container + self.children = [] + + def add_child(self, child: "Component | str"): + self.children.append(child) + + def set_style(self, style: Style): + self.style = style + + def add_style(self, style: str): + self.extra_style.append(style) + + def to_html(self) -> str: ... diff --git a/zhenxun/utils/html_template/components/title.py b/zhenxun/utils/html_template/components/title.py new file mode 100644 index 00000000..860ad17e --- /dev/null +++ b/zhenxun/utils/html_template/components/title.py @@ -0,0 +1,15 @@ +from ..component import Component, Style +from ..container import Row + + +class Title(Component): + def __init__(self, text: str, color: str = "#000"): + self.text = text + self.color = color + + def build(self): + row = Row() + style = Style(font_size="36px", color=self.color) + row.set_style(style) + + # def diff --git a/zhenxun/utils/html_template/container.py b/zhenxun/utils/html_template/container.py new file mode 100644 index 00000000..3d5341c0 --- /dev/null +++ b/zhenxun/utils/html_template/container.py @@ -0,0 +1,31 @@ +from .component import Component + + +class Row(Component): + def __init__(self, background_color: str = "#fff"): + super().__init__(background_color, True) + + +class Col(Component): + def __init__(self, background_color: str = "#fff"): + super().__init__(background_color, True) + + +class Container(Component): + def __init__(self, background_color: str = "#fff"): + super().__init__(background_color, True) + self.children = [] + + +class GlobalOverview: + def __init__(self, name: str): + self.name = name + self.class_name: dict[str, list[str]] = {} + self.content = None + + def set_content(self, content: Container): + self.content = content + + def add_class(self, class_name: str, contents: list[str]): + """全局样式""" + self.class_name[class_name] = contents diff --git a/zhenxun/utils/manager/virtual_env_package_manager.py b/zhenxun/utils/manager/virtual_env_package_manager.py new file mode 100644 index 00000000..5c4b16e5 --- /dev/null +++ b/zhenxun/utils/manager/virtual_env_package_manager.py @@ -0,0 +1,159 @@ +from pathlib import Path +import subprocess +from subprocess import CalledProcessError +from typing import ClassVar + +from zhenxun.services.log import logger + +BAT_FILE = Path() / "win启动.bat" + +LOG_COMMAND = "VirtualEnvPackageManager" + + +class VirtualEnvPackageManager: + WIN_COMMAND: ClassVar[list[str]] = [ + "./Python310/python.exe", + "-m", + "pip", + ] + + DEFAULT_COMMAND: ClassVar[list[str]] = ["poetry", "run", "pip"] + + @classmethod + def __get_command(cls) -> list[str]: + return cls.WIN_COMMAND if BAT_FILE.exists() else cls.DEFAULT_COMMAND + + @classmethod + def install(cls, package: list[str] | str): + """安装依赖包 + + 参数: + package: 安装依赖包名称或列表 + """ + if isinstance(package, str): + package = [package] + try: + command = cls.__get_command() + command.append("install") + command.append(" ".join(package)) + logger.info(f"执行虚拟环境安装包指令: {command}", LOG_COMMAND) + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + logger.debug( + f"安装虚拟环境包指令执行完成: {result.stdout}", + LOG_COMMAND, + ) + except CalledProcessError as e: + logger.error(f"安装虚拟环境包指令执行失败: {e.stderr}.", LOG_COMMAND) + + @classmethod + def uninstall(cls, package: list[str] | str): + """卸载依赖包 + + 参数: + package: 卸载依赖包名称或列表 + """ + if isinstance(package, str): + package = [package] + try: + command = cls.__get_command() + command.append("uninstall") + command.append(" ".join(package)) + logger.info(f"执行虚拟环境卸载包指令: {command}", LOG_COMMAND) + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + logger.debug( + f"卸载虚拟环境包指令执行完成: {result.stdout}", + LOG_COMMAND, + ) + except CalledProcessError as e: + logger.error(f"卸载虚拟环境包指令执行失败: {e.stderr}.", LOG_COMMAND) + + @classmethod + def update(cls, package: list[str] | str): + """更新依赖包 + + 参数: + package: 更新依赖包名称或列表 + """ + if isinstance(package, str): + package = [package] + try: + command = cls.__get_command() + command.append("install") + command.append("--upgrade") + command.append(" ".join(package)) + logger.info(f"执行虚拟环境更新包指令: {command}", LOG_COMMAND) + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + logger.debug(f"更新虚拟环境包指令执行完成: {result.stdout}", LOG_COMMAND) + except CalledProcessError as e: + logger.error(f"更新虚拟环境包指令执行失败: {e.stderr}.", LOG_COMMAND) + + @classmethod + def install_requirement(cls, requirement_file: Path): + """安装依赖文件 + + 参数: + requirement_file: requirement文件路径 + + 异常: + FileNotFoundError: 文件不存在 + """ + if not requirement_file.exists(): + raise FileNotFoundError(f"依赖文件 {requirement_file} 不存在", LOG_COMMAND) + try: + command = cls.__get_command() + command.append("install") + command.append("-r") + command.append(str(requirement_file.absolute())) + logger.info(f"执行虚拟环境安装依赖文件指令: {command}", LOG_COMMAND) + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + logger.debug( + f"安装虚拟环境依赖文件指令执行完成: {result.stdout}", + LOG_COMMAND, + ) + except CalledProcessError as e: + logger.error( + f"安装虚拟环境依赖文件指令执行失败: {e.stderr}.", + LOG_COMMAND, + ) + + @classmethod + def list(cls) -> str: + """列出已安装的依赖包""" + try: + command = cls.__get_command() + command.append("list") + logger.info(f"执行虚拟环境列出包指令: {command}", LOG_COMMAND) + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + logger.debug( + f"列出虚拟环境包指令执行完成: {result.stdout}", + LOG_COMMAND, + ) + return result.stdout + except CalledProcessError as e: + logger.error(f"列出虚拟环境包指令执行失败: {e.stderr}.", LOG_COMMAND) + return ""