diff --git a/app/runtime/extensions/plugin/metadata.py b/app/runtime/extensions/plugin/metadata.py index e0c0eae02..55cbb7b43 100644 --- a/app/runtime/extensions/plugin/metadata.py +++ b/app/runtime/extensions/plugin/metadata.py @@ -86,6 +86,7 @@ class PluginMetadataMapper: plugin.history = info.get("history") or {} plugin.release = bool(info.get("release")) plugin.repo_url = market + plugin.package_version = package_version plugin.is_local = False plugin.add_time = add_time return plugin diff --git a/app/schemas/plugin.py b/app/schemas/plugin.py index d3c4d25c0..ffd5570af 100644 --- a/app/schemas/plugin.py +++ b/app/schemas/plugin.py @@ -1,7 +1,7 @@ from enum import Enum as _Enum -from typing import Literal, Optional, List, Dict, Union +from typing import Dict, List, Literal, Optional, Union -from pydantic import BaseModel, Field, RootModel, field_validator +from pydantic import BaseModel, Field, PrivateAttr, RootModel, field_validator from app.schemas.common import JsonData @@ -62,6 +62,8 @@ class Plugin(BaseModel): """ 插件信息 """ + _package_version: Optional[str] = PrivateAttr(default=None) + id: str = None # 插件名称 plugin_name: Optional[str] = None @@ -124,6 +126,16 @@ class Plugin(BaseModel): # 实例实现模式;存量物理分身为空 instance_mode: Optional[str] = None + @property + def package_version(self) -> Optional[str]: + """读取仅供宿主内部候选选择使用的插件包代际。""" + return self._package_version + + @package_version.setter + def package_version(self, value: Optional[str]) -> None: + """保存插件包代际,但不把它暴露到 API 响应模型。""" + self._package_version = value + class PluginRuntimeSummary(BaseModel): """插件后台收敛状态和前端刷新代次。""" diff --git a/tests/test_plugin_catalog_service.py b/tests/test_plugin_catalog_service.py index 1749fb195..477cb3e34 100644 --- a/tests/test_plugin_catalog_service.py +++ b/tests/test_plugin_catalog_service.py @@ -1,11 +1,12 @@ import asyncio -from types import SimpleNamespace from unittest.mock import Mock import pytest from packaging.version import Version from app.application.plugin.catalog import PluginCatalogService +from app.runtime.extensions.plugin.metadata import PluginMetadataMapper +from app.schemas.plugin import Plugin def _plugin( @@ -13,14 +14,15 @@ def _plugin( version: str, repo_url: str, package_version: str | None = "v3", -): +) -> Plugin: """构造目录合并测试使用的最小插件 DTO。""" - return SimpleNamespace( + plugin = Plugin( id=plugin_id, plugin_version=version, repo_url=repo_url, - package_version=package_version, ) + plugin.package_version = package_version + return plugin def _service(**overrides) -> PluginCatalogService: @@ -132,6 +134,33 @@ def test_load_maps_market_entries_with_installed_snapshot(): assert mapper.call_args_list[1].args[3:] == (["Installed"], 1, "v3") +def test_metadata_mapper_preserves_package_generation_without_serializing_it(): + """市场映射必须保留候选代际,同时不能扩张插件 API 响应。""" + mapper = PluginMetadataMapper( + plugin_instance=lambda _plugin_id: None, + plugin_class=lambda _plugin_id: None, + annotate_system_version=lambda info: info, + is_package_compatible=lambda _info, _version: True, + auth_checker=lambda _plugin, _source: True, + version_compare=lambda _left, _operator, _right: False, + log=Mock(), + ) + + plugin = mapper.map( + plugin_id="Demo", + plugin_info={"name": "Demo", "version": "3.0.0"}, + market="https://market-a", + installed_plugins=[], + add_time=1, + package_version="v3", + ) + + assert plugin is not None + assert plugin.package_version == "v3" + assert "package_version" not in plugin.model_dump() + assert "package_version" not in Plugin.model_json_schema()["properties"] + + @pytest.mark.asyncio async def test_async_collect_isolates_failure_and_completes_progress(): """异步市场单任务失败时保留成功结果,并把进度推进到完成态。"""