mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
refactor(plugin): centralize market composition owners
This commit is contained in:
3
app/adapters/external/plugin/client.py
vendored
3
app/adapters/external/plugin/client.py
vendored
@@ -30,6 +30,7 @@ from app.domain.plugin import (
|
||||
parse_local_plugin_reference,
|
||||
)
|
||||
from app.foundation.environment import is_free_threaded_runtime
|
||||
from app.foundation.singleton import WeakSingleton
|
||||
from app.foundation.url import UrlUtils
|
||||
from app.foundation.version import compare_version
|
||||
from app.runtime.cache import async_fresh, cached, fresh, is_fresh
|
||||
@@ -208,7 +209,7 @@ def merge_plugin_market_repos(
|
||||
seen_repos.add(normalized_repo.lower())
|
||||
return merged_repos
|
||||
|
||||
class PluginMarketTransport:
|
||||
class PluginMarketTransport(metaclass=WeakSingleton):
|
||||
"""负责插件市场、本地仓库和 GitHub 元数据读取。"""
|
||||
|
||||
_base_url = "https://raw.githubusercontent.com/{user}/{repo}/main/"
|
||||
|
||||
70
app/startup/composition/plugin.py
Normal file
70
app/startup/composition/plugin.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""插件市场技术依赖的唯一启动组合 owner。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from app.adapters.external.plugin.client import (
|
||||
PluginMarketClient,
|
||||
PluginMarketTransport,
|
||||
PluginPackageSourceClient,
|
||||
)
|
||||
from app.adapters.system.plugin.dependency import PluginDependencyInstaller
|
||||
from app.adapters.system.plugin.health import PluginRuntimeHealth
|
||||
from app.adapters.system.plugin.package import PluginPackageManager
|
||||
from app.runtime.settings import get_runtime_setting
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginMarketComposition:
|
||||
"""保存插件市场相关 Transport、Client、Package 和 Dependency owner。"""
|
||||
|
||||
transport: PluginMarketTransport
|
||||
client: PluginMarketClient
|
||||
package: PluginPackageManager
|
||||
dependency: PluginDependencyInstaller
|
||||
|
||||
|
||||
_market_client: Optional[PluginMarketClient] = None
|
||||
|
||||
|
||||
def compose_plugin_market(
|
||||
*,
|
||||
installed_plugins_provider: Callable[[], list[str]],
|
||||
) -> PluginMarketComposition:
|
||||
"""一次性构造插件市场技术依赖,供同一 lifespan 内所有用例复用。"""
|
||||
global _market_client
|
||||
root_path = Path(get_runtime_setting("ROOT_PATH"))
|
||||
plugin_root = root_path / "app" / "plugins"
|
||||
transport = PluginMarketTransport.get_existing_instance() or PluginMarketTransport()
|
||||
client = PluginMarketClient(transport)
|
||||
composition = PluginMarketComposition(
|
||||
transport=transport,
|
||||
client=client,
|
||||
package=PluginPackageManager(
|
||||
source=PluginPackageSourceClient(transport),
|
||||
plugin_root=plugin_root,
|
||||
),
|
||||
dependency=PluginDependencyInstaller(
|
||||
PluginRuntimeHealth(),
|
||||
installed_plugins_provider=installed_plugins_provider,
|
||||
plugin_dir=plugin_root,
|
||||
),
|
||||
)
|
||||
_market_client = client
|
||||
return composition
|
||||
|
||||
|
||||
def get_composed_plugin_market_client() -> PluginMarketClient:
|
||||
"""返回当前 lifespan 由组合根构造的唯一插件市场 Client。"""
|
||||
if _market_client is None:
|
||||
raise RuntimeError("插件市场 Client 尚未由启动组合根装配")
|
||||
return _market_client
|
||||
|
||||
|
||||
def reset_plugin_market_composition() -> None:
|
||||
"""撤销当前 lifespan 的插件市场 Client 投影。"""
|
||||
global _market_client
|
||||
_market_client = None
|
||||
@@ -13,15 +13,11 @@ from app.adapters.external.market import (
|
||||
from app.adapters.external.plugin.client import (
|
||||
VERSION_BACKWARD_COMPATIBLE_FLAGS,
|
||||
PluginMarketClient,
|
||||
PluginMarketTransport,
|
||||
PluginPackageSourceClient,
|
||||
split_plugin_market_repo_urls,
|
||||
)
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.adapters.system.host import SystemUtils
|
||||
from app.adapters.system.plugin.dependency import PluginDependencyInstaller
|
||||
from app.adapters.system.plugin.manifest import dependency_manifest_status
|
||||
from app.adapters.system.plugin.package import PluginPackageManager
|
||||
from app.application.commands import init_commands
|
||||
from app.application.configuration import (
|
||||
get_api_runtime_config_snapshot,
|
||||
@@ -123,6 +119,11 @@ from app.runtime.settings import get_runtime_setting
|
||||
from app.schemas.exception import PluginMutationRejectedError
|
||||
from app.schemas.plugin import PluginRuntimeStatus
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.startup.composition.plugin import (
|
||||
compose_plugin_market,
|
||||
get_composed_plugin_market_client,
|
||||
reset_plugin_market_composition,
|
||||
)
|
||||
|
||||
|
||||
async def _async_write_plugin_config(key, value):
|
||||
@@ -180,12 +181,15 @@ def configure_plugin_runtime_services() -> None:
|
||||
|
||||
def configure_plugin_services() -> None:
|
||||
"""在模块持久化端口就绪后装配完整插件应用服务。"""
|
||||
market_transport = PluginMarketTransport()
|
||||
market_client = PluginMarketClient(market_transport)
|
||||
package_manager = PluginPackageManager(
|
||||
source=PluginPackageSourceClient(market_transport),
|
||||
plugin_root=Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins",
|
||||
market_composition = compose_plugin_market(
|
||||
installed_plugins_provider=lambda: get_configured_system_config().get(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
)
|
||||
or []
|
||||
)
|
||||
market_transport = market_composition.transport
|
||||
market_client = market_composition.client
|
||||
package_manager = market_composition.package
|
||||
plugin_manager = get_plugin_manager()
|
||||
inventory_reader = PluginCandidateInventoryReader(
|
||||
market_loader=market_client.get_plugin_index_result,
|
||||
@@ -371,12 +375,7 @@ def configure_plugin_services() -> None:
|
||||
configure_plugin_system(PluginSystemServices(
|
||||
market=market_client,
|
||||
package=package_manager,
|
||||
dependency=PluginDependencyInstaller(
|
||||
installed_plugins_provider=lambda: get_configured_system_config().get(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or [],
|
||||
plugin_dir=Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins",
|
||||
),
|
||||
dependency=market_composition.dependency,
|
||||
dependency_manifest_status=dependency_manifest_status,
|
||||
compatible_flags=lambda flag: (
|
||||
[flag] + VERSION_BACKWARD_COMPATIBLE_FLAGS.get(flag, [])
|
||||
@@ -502,7 +501,7 @@ def _run_plugin_install_sync(
|
||||
|
||||
def _build_plugin_catalog(plugin_mapper: Callable[..., Any]) -> PluginCatalogService:
|
||||
"""在组合根连接目录用例、市场客户端、持久化读取和插件 DTO 映射。"""
|
||||
client = PluginMarketClient()
|
||||
client = get_composed_plugin_market_client()
|
||||
return PluginCatalogService(
|
||||
market_loader=client.get_plugins,
|
||||
async_market_loader=client.async_get_plugins,
|
||||
@@ -776,3 +775,4 @@ def stop_plugins() -> bool:
|
||||
reset_plugin_catalog_query()
|
||||
reset_plugin_release_service()
|
||||
reset_plugin_rating_service()
|
||||
reset_plugin_market_composition()
|
||||
|
||||
@@ -2374,6 +2374,60 @@ def test_system_nettest_is_owned_by_application_service():
|
||||
assert concrete_adapter_imports == set()
|
||||
|
||||
|
||||
def test_plugin_market_construction_is_owned_by_startup_composition():
|
||||
"""插件 initializer 只能消费组合根创建的市场依赖,禁止隐式构造第二套 owner。"""
|
||||
initializer_path = APP_ROOT / "startup" / "initializers" / "plugins.py"
|
||||
initializer_tree = ast.parse(
|
||||
initializer_path.read_text(encoding="utf-8-sig"),
|
||||
filename=str(initializer_path),
|
||||
)
|
||||
initializer_imports = {
|
||||
alias.name
|
||||
for node in ast.walk(initializer_tree)
|
||||
if isinstance(node, ast.ImportFrom)
|
||||
for alias in node.names
|
||||
}
|
||||
assert "compose_plugin_market" in initializer_imports
|
||||
forbidden_names = {
|
||||
"PluginMarketTransport",
|
||||
"PluginPackageSourceClient",
|
||||
"PluginPackageManager",
|
||||
"PluginDependencyInstaller",
|
||||
}
|
||||
assert forbidden_names.isdisjoint(initializer_imports)
|
||||
assert not any(
|
||||
isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id in forbidden_names
|
||||
for node in ast.walk(initializer_tree)
|
||||
)
|
||||
|
||||
composition_path = APP_ROOT / "startup" / "composition" / "plugin.py"
|
||||
composition_tree = ast.parse(
|
||||
composition_path.read_text(encoding="utf-8-sig"),
|
||||
filename=str(composition_path),
|
||||
)
|
||||
composition_calls = [
|
||||
node.func.id
|
||||
for node in ast.walk(composition_tree)
|
||||
if isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id in forbidden_names
|
||||
]
|
||||
assert composition_calls.count("PluginMarketTransport") == 1
|
||||
assert composition_calls.count("PluginPackageSourceClient") == 1
|
||||
assert composition_calls.count("PluginPackageManager") == 1
|
||||
assert composition_calls.count("PluginDependencyInstaller") == 1
|
||||
|
||||
|
||||
def test_plugin_market_transport_is_process_shared_across_compat_and_composition():
|
||||
"""兼容门面和组合根不得物化两套插件市场传输 owner。"""
|
||||
from app.adapters.external.market import _plugin_market_transport
|
||||
from app.adapters.external.plugin.client import PluginMarketTransport
|
||||
|
||||
assert PluginMarketTransport.get_existing_instance() is _plugin_market_transport
|
||||
|
||||
|
||||
PROCESS_LEVEL_ROOTS = (
|
||||
"app.api",
|
||||
"app.chain",
|
||||
|
||||
Reference in New Issue
Block a user