From daf67dc71a09da4f141f9c4d63388b2f7085a07d Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 29 Aug 2026 08:47:25 +0800 Subject: [PATCH] fix(plugin): restore compatible history lookup --- app/api/endpoints/plugin.py | 53 +++++++++++++++++------------------ tests/test_plugin_endpoint.py | 40 ++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index 67862a4c4..fa3fc281c 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -9,7 +9,7 @@ from fastapi import Depends, Header, HTTPException, Security from starlette import status from starlette.responses import StreamingResponse -from app.adapters.external.market import PluginHelper +from app.adapters.external.market import VERSION_BACKWARD_COMPATIBLE_FLAGS, PluginHelper from app.adapters.external.server import MoviePilotServerHelper from app.adapters.web.security.access import ( resource_token_cookie, @@ -101,33 +101,32 @@ async def _get_market_plugin_from_repo( force: bool, ) -> Optional[_SchemaPlugin]: """ - 只读取指定插件仓库的市场元数据,避免单插件详情触发全部市场刷新。 - """ - market_plugins = await plugin_manager.async_get_plugins_from_market( - repo_url, get_api_runtime_config_snapshot().version_flag, force - ) - market_plugin = next( - ( - plugin - for plugin in market_plugins or [] - if plugin.id == plugin_id - ), - None, - ) - if market_plugin or not get_api_runtime_config_snapshot().version_flag: - return market_plugin + 按当前代际、向后兼容代际和基础索引顺序读取指定仓库的市场元数据。 - compatible_plugins = await plugin_manager.async_get_plugins_from_market( - repo_url, None, force - ) - return next( - ( - plugin - for plugin in compatible_plugins or [] - if plugin.id == plugin_id - ), - None, - ) + 单插件详情只访问可信绑定仓库,避免触发全部市场刷新;代际顺序必须与 + 全量插件目录保持一致,否则仅存在于 package.v2.json 的插件会丢失历史说明。 + """ + version_flag = get_api_runtime_config_snapshot().version_flag + package_versions = [version_flag] if version_flag else [] + package_versions.extend(VERSION_BACKWARD_COMPATIBLE_FLAGS.get(version_flag, [])) + package_versions.append(None) + + for package_version in dict.fromkeys(package_versions): + market_plugins = await plugin_manager.async_get_plugins_from_market( + repo_url, package_version, force + ) + market_plugin = next( + ( + plugin + for plugin in market_plugins or [] + if plugin.id == plugin_id + ), + None, + ) + if market_plugin: + return market_plugin + + return None async def _refresh_plugin_release_versions(plugin_id: str, repo_url: str) -> None: diff --git a/tests/test_plugin_endpoint.py b/tests/test_plugin_endpoint.py index 3274c66f7..3d2aabfaa 100644 --- a/tests/test_plugin_endpoint.py +++ b/tests/test_plugin_endpoint.py @@ -286,6 +286,41 @@ def test_plugin_history_merges_remote_metadata(): ) +def test_plugin_history_falls_back_to_backward_compatible_package(): + """已绑定来源的插件在当前索引缺失时仍读取向后兼容代际的历史。""" + installed_plugin = schemas.Plugin( + id="DemoPlugin", + plugin_name="Demo Plugin", + plugin_version="1.0.0", + installed=True, + history={}, + ) + market_plugin = schemas.Plugin( + id="DemoPlugin", + repo_url=SOURCE_URL, + history={"v2.0.0": "兼容代际更新说明"}, + ) + plugin_manager = MagicMock() + plugin_manager.get_installed_plugins.return_value = [installed_plugin] + plugin_manager.get_local_repo_plugins.return_value = [] + plugin_manager.async_get_plugins_from_market = AsyncMock( + side_effect=[[], [market_plugin]] + ) + persistence = _persistence(_plugin_identity()) + + with ( + patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.get_plugin_persistence", return_value=persistence), + ): + result = asyncio.run(plugin_history("DemoPlugin", None, True)) + + assert result.history == {"v2.0.0": "兼容代际更新说明"} + assert plugin_manager.async_get_plugins_from_market.await_args_list == [ + ((SOURCE_URL, settings.VERSION_FLAG, True), {}), + ((SOURCE_URL, "v2", True), {}), + ] + + def test_runtime_status_reports_pending_and_terminal_counts(): """插件页摘要区分后台收敛、准备态和终态失败。""" plugin_manager = MagicMock() @@ -506,7 +541,7 @@ def test_plugin_releases_does_not_mutate_cached_release_items(monkeypatch): def test_plugin_releases_falls_back_to_compatible_base_package(monkeypatch): """ - 当前版本 package 未包含插件时,再读取基础 package 兼容项,不扫描其他市场。 + 当前版本和向后兼容 package 未包含插件时,再读取基础 package 兼容项。 """ market_plugin = schemas.Plugin( id="DemoPlugin", @@ -516,7 +551,7 @@ def test_plugin_releases_falls_back_to_compatible_base_package(monkeypatch): ) plugin_manager = MagicMock() plugin_manager.async_get_plugins_from_market = AsyncMock( - side_effect=[[], [market_plugin]] + side_effect=[[], [], [market_plugin]] ) plugin_manager.get_local_plugin_version.return_value = None plugin_helper = MagicMock() @@ -534,6 +569,7 @@ def test_plugin_releases_falls_back_to_compatible_base_package(monkeypatch): assert result["latest_version"] == "1.2.3" assert plugin_manager.async_get_plugins_from_market.await_args_list == [ (("https://github.com/demo/plugins", settings.VERSION_FLAG, False), {}), + (("https://github.com/demo/plugins", "v2", False), {}), (("https://github.com/demo/plugins", None, False), {}), ]