更新插件商店功能,支持通过模块名操作插件(#1670)

* 更新插件商店功能,支持通过模块名操作插件

- 扩展插件添加、移除和更新功能,支持使用插件ID或模块名
- 增加更新全部插件的功能
- 优化插件商店的命令使用说明
- 修复了一些与插件模块名相关的逻辑问题

* 优化插件更新和加载机制,提供测试函数

- 修复了插件更新函数中的条件判断逻辑

* 优化插件更新通知的格式

调整了插件更新通知的文本格式,去掉了多余的换行符,使消息内容更加紧凑和清晰。

* 更新测试用例中的消息格式,将插件更新通知中的空格改为换行符

* 移除版本号更新

* 重构插件管理器的数据源解析逻辑

- 将插件ID和模块名的检查逻辑移至单独的私有方法 _resolve_plugin_key
- 简化了 get_info 和 update_plugin 方法中的逻辑
- 提高了代码的可读性和可维护性

* 优化插件商店数据源类的插件查询逻辑

简化了ShopManage类中查询插件信息的逻辑。通过新增的_resolve_plugin_key类方法来解析插件ID或模块名,如果解析失败则捕获ValueError异常并返回错误信息。这样可以更清晰地处理插件查询逻辑,并避免冗余代码。

* 移除更新全部插件日志中的f-string

更新全部插件功能中,移除了日志记录中的f-string,简化了日志消息的格式。这个更改可能是为了统一日志记录的风格或者减少不必要的字符串格式化操作。

* Revert "移除版本号更新"

This reverts commit 2bcaa6f12e.

---------

Co-authored-by: molanp <molanp@users.noreply.github.com>
Co-authored-by: HibiKier <45528451+HibiKier@users.noreply.github.com>
Co-authored-by: AkashiCoin <l1040186796@gmail.com>
This commit is contained in:
moelanp 2024-10-02 18:32:21 +08:00 committed by GitHub
parent d3a4a5bbf7
commit 42b6e94564
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 231 additions and 27 deletions

View File

@ -1 +1 @@
__version__: v0.2.3-a39d7a3
__version__: v0.2.3-ea00860

View File

@ -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

View File

@ -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()

View File

@ -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]