feat: add plugin history endpoint to fetch remote update details

This commit is contained in:
jxxghp
2026-06-02 07:12:13 +08:00
parent db522e8829
commit 1c41d9f253
3 changed files with 151 additions and 0 deletions

View File

@@ -146,6 +146,62 @@ def register_plugin(plugin_id: str):
register_plugin_api(plugin_id)
def _merge_plugin_market_metadata(
plugin: schemas.Plugin, market_plugin: schemas.Plugin
) -> schemas.Plugin:
"""
合并插件市场中的远端元数据,供已安装插件按需展示更新说明。
"""
plugin.repo_url = market_plugin.repo_url or plugin.repo_url
plugin.history = market_plugin.history or {}
plugin.has_update = market_plugin.has_update
plugin.system_version = market_plugin.system_version or plugin.system_version
plugin.system_version_compatible = market_plugin.system_version_compatible
plugin.system_version_message = (
market_plugin.system_version_message or plugin.system_version_message
)
return plugin
async def _get_plugin_history_detail(
plugin_id: str, force: bool = True
) -> Optional[schemas.Plugin]:
"""
按需获取插件远端元数据,避免插件列表加载时批量访问网络。
"""
plugin_manager = PluginManager()
installed_plugin = next(
(
plugin
for plugin in plugin_manager.get_local_plugins()
if plugin.id == plugin_id and plugin.installed
),
None,
)
if not installed_plugin:
return None
local_repo_plugin = next(
(plugin for plugin in plugin_manager.get_local_repo_plugins() if plugin.id == plugin_id),
None,
)
if local_repo_plugin:
return _merge_plugin_market_metadata(installed_plugin, local_repo_plugin)
market_plugin = next(
(
plugin
for plugin in await plugin_manager.async_get_online_plugins(force=force)
if plugin.id == plugin_id
),
None,
)
if not market_plugin:
return installed_plugin
return _merge_plugin_market_metadata(installed_plugin, market_plugin)
@router.get("/", summary="所有插件", response_model=List[schemas.Plugin])
async def all_plugins(
_: User = Depends(get_current_active_superuser_async),
@@ -213,6 +269,24 @@ async def installed(_: User = Depends(get_current_active_superuser_async)) -> An
return SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
@router.get("/history/{plugin_id}", summary="获取插件更新说明", response_model=schemas.Plugin)
async def plugin_history(
plugin_id: str,
_: User = Depends(get_current_active_superuser_async),
force: bool = True,
) -> schemas.Plugin:
"""
按需获取指定插件的更新说明。
"""
plugin = await _get_plugin_history_detail(plugin_id=plugin_id, force=force)
if not plugin:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"插件 {plugin_id} 不存在或未安装",
)
return plugin
@router.get("/statistic", summary="插件安装统计", response_model=dict)
async def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""

View File

@@ -74,6 +74,12 @@ MoviePilot 实现了标准的 **Model Context Protocol (MCP)**,允许 AI 智
## 6. RESTful API
所有工具相关的API端点都在 `/api/v1/mcp` 路径下(保持向后兼容)。
### 插件补充接口
**GET** `/api/v1/plugin/history/{plugin_id}`
按需读取指定已安装插件的最新远端更新说明。该接口用于前端在用户点击“查看更新说明”时再实时访问插件仓库,避免加载已安装插件列表时批量请求网络。
### 1. 列出所有工具
**GET** `/api/v1/mcp/tools`

View File

@@ -0,0 +1,71 @@
import asyncio
from unittest import TestCase
from unittest.mock import AsyncMock, MagicMock, patch
class PluginEndpointTest(TestCase):
def test_plugin_history_merges_remote_metadata(self):
"""
已安装插件点击更新说明时,接口会按需合并远端仓库中的更新记录。
"""
try:
from app import schemas
from app.api.endpoints.plugin import plugin_history
except ModuleNotFoundError as exc:
self.skipTest(f"missing dependency: {exc}")
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="https://github.com/demo/plugins",
history={"v1.1.0": "- 新增更新说明"},
system_version=">=2.0.0",
system_version_compatible=True,
has_update=True,
)
plugin_manager = MagicMock()
plugin_manager.get_local_plugins.return_value = [installed_plugin]
plugin_manager.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugins = AsyncMock(return_value=[market_plugin])
with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager):
result = asyncio.run(plugin_history("DemoPlugin", None, True))
self.assertEqual("https://github.com/demo/plugins", result.repo_url)
self.assertEqual({"v1.1.0": "- 新增更新说明"}, result.history)
self.assertEqual(">=2.0.0", result.system_version)
self.assertTrue(result.has_update)
def test_plugin_history_returns_installed_plugin_when_remote_missing(self):
"""
远端仓库不可用时,接口仍返回本地已安装插件信息,前端可继续展示兜底状态。
"""
try:
from app import schemas
from app.api.endpoints.plugin import plugin_history
except ModuleNotFoundError as exc:
self.skipTest(f"missing dependency: {exc}")
installed_plugin = schemas.Plugin(
id="DemoPlugin",
plugin_name="Demo Plugin",
plugin_version="1.0.0",
installed=True,
)
plugin_manager = MagicMock()
plugin_manager.get_local_plugins.return_value = [installed_plugin]
plugin_manager.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugins = AsyncMock(return_value=[])
with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager):
result = asyncio.run(plugin_history("DemoPlugin", None, True))
self.assertEqual("DemoPlugin", result.id)
self.assertEqual({}, result.history)