From 42b6e94564b55816f3e271702bf46b86391d908b Mon Sep 17 00:00:00 2001 From: moelanp <104612722+molanp@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:32:21 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=9B=B4=E6=96=B0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=95=86=E5=BA=97=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=80=9A=E8=BF=87=E6=A8=A1=E5=9D=97=E5=90=8D=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=8F=92=E4=BB=B6(#1670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 更新插件商店功能,支持通过模块名操作插件 - 扩展插件添加、移除和更新功能,支持使用插件ID或模块名 - 增加更新全部插件的功能 - 优化插件商店的命令使用说明 - 修复了一些与插件模块名相关的逻辑问题 * 优化插件更新和加载机制,提供测试函数 - 修复了插件更新函数中的条件判断逻辑 * 优化插件更新通知的格式 调整了插件更新通知的文本格式,去掉了多余的换行符,使消息内容更加紧凑和清晰。 * 更新测试用例中的消息格式,将插件更新通知中的空格改为换行符 * 移除版本号更新 * 重构插件管理器的数据源解析逻辑 - 将插件ID和模块名的检查逻辑移至单独的私有方法 _resolve_plugin_key - 简化了 get_info 和 update_plugin 方法中的逻辑 - 提高了代码的可读性和可维护性 * 优化插件商店数据源类的插件查询逻辑 简化了ShopManage类中查询插件信息的逻辑。通过新增的_resolve_plugin_key类方法来解析插件ID或模块名,如果解析失败则捕获ValueError异常并返回错误信息。这样可以更清晰地处理插件查询逻辑,并避免冗余代码。 * 移除更新全部插件日志中的f-string 更新全部插件功能中,移除了日志记录中的f-string,简化了日志消息的格式。这个更改可能是为了统一日志记录的风格或者减少不必要的字符串格式化操作。 * Revert "移除版本号更新" This reverts commit 2bcaa6f12e61136dc6bf2ca5b9fde29583a50c12. --------- Co-authored-by: molanp Co-authored-by: HibiKier <45528451+HibiKier@users.noreply.github.com> Co-authored-by: AkashiCoin --- __version__ | 2 +- .../plugin_store/test_update_all_plugin.py | 120 ++++++++++++++++++ .../builtin_plugins/plugin_store/__init__.py | 50 ++++++-- .../plugin_store/data_source.py | 86 ++++++++++--- 4 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 tests/builtin_plugins/plugin_store/test_update_all_plugin.py diff --git a/__version__ b/__version__ index a71fac09..4f3304af 100644 --- a/__version__ +++ b/__version__ @@ -1 +1 @@ -__version__: v0.2.3-a39d7a3 +__version__: v0.2.3-ea00860 diff --git a/tests/builtin_plugins/plugin_store/test_update_all_plugin.py b/tests/builtin_plugins/plugin_store/test_update_all_plugin.py new file mode 100644 index 00000000..d9205765 --- /dev/null +++ b/tests/builtin_plugins/plugin_store/test_update_all_plugin.py @@ -0,0 +1,120 @@ +from typing import cast +from pathlib import Path +from collections.abc import Callable + +from nonebug import App +from respx import MockRouter +from pytest_mock import MockerFixture +from nonebot.adapters.onebot.v11 import Bot +from nonebot.adapters.onebot.v11.message import Message +from nonebot.adapters.onebot.v11.event import GroupMessageEvent + +from tests.utils import _v11_group_message_event +from tests.config import BotId, UserId, GroupId, MessageId +from tests.builtin_plugins.plugin_store.utils import init_mocked_api + + +async def test_update_all_plugin_basic_need_update( + app: App, + mocker: MockerFixture, + mocked_api: MockRouter, + create_bot: Callable, + tmp_path: Path, +) -> None: + """ + 测试更新基础插件,插件需要更新 + """ + from zhenxun.builtin_plugins.plugin_store import _matcher + + init_mocked_api(mocked_api=mocked_api) + mock_base_path = mocker.patch( + "zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH", + new=tmp_path / "zhenxun", + ) + mocker.patch( + "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + return_value=[("search_image", "0.0")], + ) + + + async with app.test_matcher(_matcher) as ctx: + bot = create_bot(ctx) + bot: Bot = cast(Bot, bot) + raw_message = "更新全部插件" + event: GroupMessageEvent = _v11_group_message_event( + message=raw_message, + self_id=BotId.QQ_BOT, + user_id=UserId.SUPERUSER, + group_id=GroupId.GROUP_ID_LEVEL_5, + message_id=MessageId.MESSAGE_ID, + to_me=True, + ) + ctx.receive_event(bot=bot, event=event) + ctx.should_call_send( + event=event, + message=Message(message="正在更新全部插件"), + result=None, + bot=bot, + ) + ctx.should_call_send( + event=event, + message=Message(message="已更新插件 识图\n共计1个插件! 重启后生效"), + result=None, + bot=bot, + ) + assert mocked_api["basic_plugins"].called + assert mocked_api["extra_plugins"].called + assert mocked_api["search_image_plugin_file_init"].called + assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file() + + +async def test_update_all_plugin_basic_is_new( + app: App, + mocker: MockerFixture, + mocked_api: MockRouter, + create_bot: Callable, + tmp_path: Path, +) -> None: + """ + 测试更新基础插件,插件是最新版 + """ + from zhenxun.builtin_plugins.plugin_store import _matcher + + init_mocked_api(mocked_api=mocked_api) + mocker.patch( + "zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH", + new=tmp_path / "zhenxun", + ) + mocker.patch( + "zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins", + return_value=[("search_image", "0.1")], + ) + + + async with app.test_matcher(_matcher) as ctx: + bot = create_bot(ctx) + bot: Bot = cast(Bot, bot) + raw_message = "更新全部插件" + event: GroupMessageEvent = _v11_group_message_event( + message=raw_message, + self_id=BotId.QQ_BOT, + user_id=UserId.SUPERUSER, + group_id=GroupId.GROUP_ID_LEVEL_5, + message_id=MessageId.MESSAGE_ID, + to_me=True, + ) + ctx.receive_event(bot=bot, event=event) + ctx.should_call_send( + event=event, + message=Message(message="正在更新全部插件"), + result=None, + bot=bot, + ) + ctx.should_call_send( + event=event, + message=Message(message="全部插件已是最新版本"), + result=None, + bot=bot, + ) + assert mocked_api["basic_plugins"].called + assert mocked_api["extra_plugins"].called diff --git a/zhenxun/builtin_plugins/plugin_store/__init__.py b/zhenxun/builtin_plugins/plugin_store/__init__.py index 4ff8c940..06e0b481 100644 --- a/zhenxun/builtin_plugins/plugin_store/__init__.py +++ b/zhenxun/builtin_plugins/plugin_store/__init__.py @@ -15,10 +15,11 @@ __plugin_meta__ = PluginMetadata( description="插件商店", usage=""" 插件商店 : 查看当前的插件商店 - 添加插件 id : 添加插件 - 移除插件 id : 移除插件 + 添加插件 id or module : 添加插件 + 移除插件 id or module : 移除插件 搜索插件 name or author : 搜索插件 - 更新插件 id : 更新插件 + 更新插件 id or module : 更新插件 + 更新全部插件 : 更新全部插件 """.strip(), extra=PluginExtraData( author="HibiKier", @@ -30,10 +31,11 @@ __plugin_meta__ = PluginMetadata( _matcher = on_alconna( Alconna( "插件商店", - Subcommand("add", Args["plugin_id", int]), - Subcommand("remove", Args["plugin_id", int]), + Subcommand("add", Args["plugin_id", int | str]), + Subcommand("remove", Args["plugin_id", int | str]), Subcommand("search", Args["plugin_name_or_author", str]), - Subcommand("update", Args["plugin_id", int]), + Subcommand("update", Args["plugin_id", int | str]), + Subcommand("update_all"), ), permission=SUPERUSER, priority=1, @@ -68,6 +70,13 @@ _matcher.shortcut( prefix=True, ) +_matcher.shortcut( + r"更新全部插件", + command="插件商店", + arguments=["update_all"], + prefix=True, +) + @_matcher.assign("$main") async def _(session: EventSession): @@ -81,9 +90,12 @@ async def _(session: EventSession): @_matcher.assign("add") -async def _(session: EventSession, plugin_id: int): +async def _(session: EventSession, plugin_id: int | str): try: - await MessageUtils.build_message(f"正在添加插件 Id: {plugin_id}").send() + if isinstance(plugin_id, str): + await MessageUtils.build_message(f"正在添加插件 Module: {plugin_id}").send() + else: + await MessageUtils.build_message(f"正在添加插件 Id: {plugin_id}").send() result = await ShopManage.add_plugin(plugin_id) except Exception as e: logger.error(f"添加插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) @@ -95,7 +107,7 @@ async def _(session: EventSession, plugin_id: int): @_matcher.assign("remove") -async def _(session: EventSession, plugin_id: int): +async def _(session: EventSession, plugin_id: int | str): try: result = await ShopManage.remove_plugin(plugin_id) except Exception as e: @@ -126,9 +138,12 @@ async def _(session: EventSession, plugin_name_or_author: str): @_matcher.assign("update") -async def _(session: EventSession, plugin_id: int): +async def _(session: EventSession, plugin_id: int | str): try: - await MessageUtils.build_message(f"正在更新插件 Id: {plugin_id}").send() + if isinstance(plugin_id, str): + await MessageUtils.build_message(f"正在更新插件 Module: {plugin_id}").send() + else: + await MessageUtils.build_message(f"正在更新插件 Id: {plugin_id}").send() result = await ShopManage.update_plugin(plugin_id) except Exception as e: logger.error(f"更新插件 Id: {plugin_id}失败", "插件商店", session=session, e=e) @@ -137,3 +152,16 @@ async def _(session: EventSession, plugin_id: int): ).finish() logger.info(f"更新插件 Id: {plugin_id}", "插件商店", session=session) await MessageUtils.build_message(result).send() + +@_matcher.assign("update_all") +async def _(session: EventSession): + try: + await MessageUtils.build_message("正在更新全部插件").send() + result = await ShopManage.update_all_plugin() + except Exception as e: + logger.error("更新全部插件失败", "插件商店", session=session, e=e) + await MessageUtils.build_message( + f"更新全部插件失败 e: {e}" + ).finish() + logger.info("更新全部插件", "插件商店", session=session) + await MessageUtils.build_message(result).send() \ No newline at end of file diff --git a/zhenxun/builtin_plugins/plugin_store/data_source.py b/zhenxun/builtin_plugins/plugin_store/data_source.py index 26c3edff..d63309b8 100644 --- a/zhenxun/builtin_plugins/plugin_store/data_source.py +++ b/zhenxun/builtin_plugins/plugin_store/data_source.py @@ -175,19 +175,20 @@ class ShopManage: ) @classmethod - async def add_plugin(cls, plugin_id: int) -> str: + async def add_plugin(cls, plugin_id: int | str) -> str: """添加插件 参数: - plugin_id: 插件id + plugin_id: 插件id或模块名 返回: str: 返回消息 """ data: dict[str, StorePluginInfo] = await cls.get_data() - if plugin_id < 0 or plugin_id >= len(data): - return "插件ID不存在..." - plugin_key = list(data.keys())[plugin_id] + 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]: @@ -265,20 +266,21 @@ class ShopManage: raise Exception("插件下载失败") @classmethod - async def remove_plugin(cls, plugin_id: int) -> str: + async def remove_plugin(cls, plugin_id: int | str) -> str: """移除插件 参数: - plugin_id: 插件id + plugin_id: 插件id或模块名 返回: str: 返回消息 """ data: dict[str, StorePluginInfo] = await cls.get_data() - if plugin_id < 0 or plugin_id >= len(data): - return "插件ID不存在..." - plugin_key = list(data.keys())[plugin_id] - plugin_info = data[plugin_key] # type: ignore + try: + plugin_key = await cls._resolve_plugin_key(plugin_id) + except ValueError as e: + return str(e) + plugin_info = data[plugin_key] path = BASE_PATH if plugin_info.github_url: path = BASE_PATH / "plugins" @@ -340,7 +342,7 @@ class ShopManage: ) @classmethod - async def update_plugin(cls, plugin_id: int) -> str: + async def update_plugin(cls, plugin_id: int | str) -> str: """更新插件 参数: @@ -350,9 +352,10 @@ class ShopManage: str: 返回消息 """ data: dict[str, StorePluginInfo] = await cls.get_data() - if plugin_id < 0 or plugin_id >= len(data): - return "插件ID不存在..." - plugin_key = list(data.keys())[plugin_id] + 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") @@ -373,3 +376,56 @@ class ShopManage: is_external, ) return f"插件 {plugin_key} 更新成功! 重启后生效" + + @classmethod + async def update_all_plugin(cls) -> str: + """更新插件 + + 参数: + plugin_id: 插件id + + 返回: + str: 返回消息 + """ + data: dict[str, StorePluginInfo] = await cls.get_data() + plugin_list = list(data.keys()) + update_list = [] + logger.info(f"尝试更新全部插件 {plugin_list}", "插件管理") + for plugin_key in plugin_list: + 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} 未安装,跳过", "插件管理") + continue + if cls.check_version_is_new(plugin_info, suc_plugin): + logger.debug(f"插件 {plugin_key} 已是最新版本,跳过", "插件管理") + continue + logger.info(f"正在更新插件 {plugin_key}", "插件管理") + is_external = True + if plugin_info.github_url is None: + plugin_info.github_url = DEFAULT_GITHUB_URL + is_external = False + await cls.install_plugin_with_repo( + plugin_info.github_url, + plugin_info.module_path, + plugin_info.is_dir, + is_external, + ) + update_list.append(plugin_key) + if len(update_list) == 0: + return "全部插件已是最新版本" + return "已更新插件 {}\n共计{}个插件! 重启后生效".format( + "\n- ".join(update_list), len(update_list) + ) + @classmethod + async def _resolve_plugin_key(cls, plugin_id: int | str) -> str: + data: dict[str, StorePluginInfo] = await cls.get_data() + if isinstance(plugin_id, int): + if plugin_id < 0 or plugin_id >= len(data): + raise ValueError("插件ID不存在...") + return list(data.keys())[plugin_id] + elif isinstance(plugin_id, str): + if plugin_id not in [v.module for k, v in data.items()]: + raise ValueError("插件Module不存在...") + return {v.module: k for k, v in data.items()}[plugin_id]