Skip to content

Commit 5128ae9

Browse files
committed
refactor: 推进后端分层架构治理
1 parent 5d0baca commit 5128ae9

363 files changed

Lines changed: 34196 additions & 6036 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/adapters/external/market.py

Lines changed: 57 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@
3030

3131
from app.runtime.cache import cached, is_fresh
3232
from app.runtime.config import settings
33-
from app.db.oper.systemconfig import SystemConfigOper
3433
from app.adapters.system.package import PackageInstallRequest, build_package_install_strategies
3534
from app.runtime.log import logger
36-
from app.schemas.types import SystemConfigKey
3735
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
3836
from app.foundation.singleton import WeakSingleton
3937

@@ -59,6 +57,24 @@
5957
"v3": ["v2"],
6058
}
6159

60+
InstalledPluginsProvider = Callable[[], List[str]]
61+
62+
63+
def _empty_installed_plugins() -> List[str]:
64+
"""组合根尚未注入配置读取器时返回空安装清单。"""
65+
return []
66+
67+
68+
_installed_plugins_provider: InstalledPluginsProvider = _empty_installed_plugins
69+
70+
71+
def configure_installed_plugins_provider(
72+
provider: InstalledPluginsProvider,
73+
) -> None:
74+
"""由启动组合层注入已安装插件读取器,避免市场适配器访问数据库。"""
75+
global _installed_plugins_provider
76+
_installed_plugins_provider = provider
77+
6278

6379
def normalize_plugin_market_repo_url(repo_url: str) -> Optional[str]:
6480
"""规范化插件仓库地址,便于跨来源合并去重。"""
@@ -165,10 +181,6 @@ class PluginHelper(metaclass=WeakSingleton):
165181
"sqlalchemy, starlette, uvicorn; from pydantic import BaseModel, Field"
166182
)
167183

168-
def __init__(self):
169-
"""初始化插件仓库配置访问器。"""
170-
self.systemconfig = SystemConfigOper()
171-
172184
@staticmethod
173185
def is_local_repo_url(repo_url: Optional[str]) -> bool:
174186
"""
@@ -1173,7 +1185,7 @@ def __collect_plugin_wheels_dirs(self) -> List[Path]:
11731185
try:
11741186
install_plugins = {
11751187
plugin_id.lower()
1176-
for plugin_id in self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or []
1188+
for plugin_id in _installed_plugins_provider() or []
11771189
}
11781190
for plugin_id in install_plugins:
11791191
wheels_dir = PLUGIN_DIR / plugin_id / "wheels"
@@ -2099,68 +2111,26 @@ def __install_from_release(self, pid: str, user_repo: str, release_tag: str) ->
20992111
return False, f"解压 Release 压缩包失败:{e}"
21002112

21012113
def find_missing_dependencies(self) -> List[str]:
2102-
"""
2103-
收集所有需要安装或更新的依赖项
2104-
1. 收集所有插件的依赖项,合并版本约束
2105-
2. 获取已安装的包及其版本
2106-
3. 比较已安装的包与所需的依赖项,找出需要安装或升级的包
2107-
:return: 需要安装或更新的依赖项列表,例如 ["package1>=1.0.0", "package2"]
2108-
"""
2109-
try:
2110-
# 收集所有插件的依赖项
2111-
plugin_dependencies = self.__find_plugin_dependencies() # 返回格式为 {package_name: version_specifier}
2112-
# 获取已安装的包及其版本
2113-
installed_packages = self.__get_installed_packages() # 返回格式为 {package_name: Version}
2114-
# 需要安装或更新的依赖项列表
2115-
dependencies_to_install = []
2116-
for pkg_name, version_specifier in plugin_dependencies.items():
2117-
spec_set = SpecifierSet(version_specifier)
2118-
installed_version = installed_packages.get(pkg_name)
2119-
if installed_version is None:
2120-
# 包未安装,需要安装
2121-
if version_specifier:
2122-
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
2123-
else:
2124-
dependencies_to_install.append(pkg_name)
2125-
elif not spec_set.contains(installed_version, prereleases=True):
2126-
# 已安装的版本不满足版本约束,需要升级或降级
2127-
if version_specifier:
2128-
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
2129-
else:
2130-
dependencies_to_install.append(pkg_name)
2131-
# 已安装的版本满足要求,无需操作
2132-
return dependencies_to_install
2133-
except Exception as e:
2134-
logger.error(f"收集所有需要安装或更新的依赖项时发生错误:{e}")
2135-
return []
2114+
"""兼容旧市场入口,转发到独立依赖适配器。"""
2115+
installer = importlib.import_module(
2116+
"app.adapters.system.plugin.dependency"
2117+
).PluginDependencyInstaller
2118+
return installer(
2119+
self,
2120+
installed_plugins_provider=_installed_plugins_provider,
2121+
plugin_dir=PLUGIN_DIR,
2122+
).find_missing()
21362123

21372124
def install_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
2138-
"""
2139-
安装指定的依赖项列表
2140-
:param dependencies: 需要安装或更新的依赖项列表
2141-
:return: (success, message)
2142-
"""
2143-
if not dependencies:
2144-
return False, "没有传入需要安装的依赖项"
2145-
2146-
try:
2147-
logger.debug(f"需要安装或更新的依赖项:{dependencies}")
2148-
# 创建临时的 requirements.txt 文件用于批量安装
2149-
requirements_temp_file = Path(settings.TEMP_PATH) / "plugin_dependencies" / "requirements.txt"
2150-
requirements_temp_file.parent.mkdir(parents=True, exist_ok=True)
2151-
with open(requirements_temp_file, "w", encoding="utf-8") as f:
2152-
for dep in dependencies:
2153-
f.write(dep + "\n")
2154-
try:
2155-
# 使用自动降级策略安装依赖
2156-
wheels_dirs = self.__collect_plugin_wheels_dirs()
2157-
return self.pip_install_with_fallback(requirements_temp_file, wheels_dirs)
2158-
finally:
2159-
# 删除临时文件
2160-
requirements_temp_file.unlink()
2161-
except Exception as e:
2162-
logger.error(f"安装依赖项时发生错误:{e}")
2163-
return False, f"安装依赖项时发生错误:{e}"
2125+
"""兼容旧市场入口,转发到独立依赖适配器。"""
2126+
installer = importlib.import_module(
2127+
"app.adapters.system.plugin.dependency"
2128+
).PluginDependencyInstaller
2129+
return installer(
2130+
self,
2131+
installed_plugins_provider=_installed_plugins_provider,
2132+
plugin_dir=PLUGIN_DIR,
2133+
).install(dependencies)
21642134

21652135
@classmethod
21662136
def __get_installed_packages(cls) -> Dict[str, Version]:
@@ -2203,9 +2173,7 @@ def __find_plugin_dependencies(self) -> Dict[str, str]:
22032173
try:
22042174
install_plugins = {
22052175
plugin_id.lower() # 对应插件的小写目录名
2206-
for plugin_id in SystemConfigOper().get(
2207-
SystemConfigKey.UserInstalledPlugins
2208-
) or []
2176+
for plugin_id in _installed_plugins_provider() or []
22092177
}
22102178
for plugin_dir in PLUGIN_DIR.iterdir():
22112179
if plugin_dir.is_dir():
@@ -2739,34 +2707,15 @@ async def __async_install_dependencies_if_required(self, pid: str) -> Tuple[bool
27392707
return False, False, "不存在依赖"
27402708

27412709
async def async_install_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
2742-
"""
2743-
异步安装指定的依赖项列表
2744-
:param dependencies: 需要安装或更新的依赖项列表
2745-
:return: (success, message)
2746-
"""
2747-
if not dependencies:
2748-
return False, "没有传入需要安装的依赖项"
2749-
2750-
try:
2751-
logger.debug(f"需要安装或更新的依赖项:{dependencies}")
2752-
# 创建临时的 requirements.txt 文件用于批量安装
2753-
requirements_temp_file = AsyncPath(settings.TEMP_PATH) / "plugin_dependencies" / "requirements.txt"
2754-
await requirements_temp_file.parent.mkdir(parents=True, exist_ok=True)
2755-
2756-
async with aiofiles.open(requirements_temp_file, "w", encoding="utf-8") as f:
2757-
for dep in dependencies:
2758-
await f.write(dep + "\n")
2759-
2760-
try:
2761-
# 使用自动降级策略安装依赖
2762-
wheels_dirs = self.__collect_plugin_wheels_dirs()
2763-
return await self.__async_pip_install_with_fallback(Path(requirements_temp_file), wheels_dirs)
2764-
finally:
2765-
# 删除临时文件
2766-
await requirements_temp_file.unlink()
2767-
except Exception as e:
2768-
logger.error(f"安装依赖项时发生错误:{e}")
2769-
return False, f"安装依赖项时发生错误:{e}"
2710+
"""兼容旧异步市场入口,转发到独立依赖适配器。"""
2711+
installer = importlib.import_module(
2712+
"app.adapters.system.plugin.dependency"
2713+
).PluginDependencyInstaller
2714+
return await installer(
2715+
self,
2716+
installed_plugins_provider=_installed_plugins_provider,
2717+
plugin_dir=PLUGIN_DIR,
2718+
).async_install(dependencies)
27702719

27712720
async def __async_find_plugin_dependencies(self) -> Dict[str, str]:
27722721
"""
@@ -2779,9 +2728,7 @@ async def __async_find_plugin_dependencies(self) -> Dict[str, str]:
27792728
try:
27802729
install_plugins = {
27812730
plugin_id.lower() # 对应插件的小写目录名
2782-
for plugin_id in SystemConfigOper().get(
2783-
SystemConfigKey.UserInstalledPlugins
2784-
) or []
2731+
for plugin_id in _installed_plugins_provider() or []
27852732
}
27862733

27872734
plugin_dir_path = AsyncPath(PLUGIN_DIR)
@@ -2838,40 +2785,15 @@ async def __async_parse_requirements(self, requirements_file: AsyncPath) -> Dict
28382785
return {}
28392786

28402787
async def async_find_missing_dependencies(self) -> List[str]:
2841-
"""
2842-
异步收集所有需要安装或更新的依赖项
2843-
1. 收集所有插件的依赖项,合并版本约束
2844-
2. 获取已安装的包及其版本
2845-
3. 比较已安装的包与所需的依赖项,找出需要安装或升级的包
2846-
:return: 需要安装或更新的依赖项列表,例如 ["package1>=1.0.0", "package2"]
2847-
"""
2848-
try:
2849-
# 收集所有插件的依赖项
2850-
plugin_dependencies = await self.__async_find_plugin_dependencies() # 返回格式为 {package_name: version_specifier}
2851-
# 获取已安装的包及其版本
2852-
installed_packages = self.__get_installed_packages() # 返回格式为 {package_name: Version}
2853-
# 需要安装或更新的依赖项列表
2854-
dependencies_to_install = []
2855-
for pkg_name, version_specifier in plugin_dependencies.items():
2856-
spec_set = SpecifierSet(version_specifier)
2857-
installed_version = installed_packages.get(pkg_name)
2858-
if installed_version is None:
2859-
# 包未安装,需要安装
2860-
if version_specifier:
2861-
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
2862-
else:
2863-
dependencies_to_install.append(pkg_name)
2864-
elif not spec_set.contains(installed_version, prereleases=True):
2865-
# 已安装的版本不满足版本约束,需要升级或降级
2866-
if version_specifier:
2867-
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
2868-
else:
2869-
dependencies_to_install.append(pkg_name)
2870-
# 已安装的版本满足要求,无需操作
2871-
return dependencies_to_install
2872-
except Exception as e:
2873-
logger.error(f"收集所有需要安装或更新的依赖项时发生错误:{e}")
2874-
return []
2788+
"""兼容旧异步市场入口,转发到独立依赖适配器。"""
2789+
installer = importlib.import_module(
2790+
"app.adapters.system.plugin.dependency"
2791+
).PluginDependencyInstaller
2792+
return await installer(
2793+
self,
2794+
installed_plugins_provider=_installed_plugins_provider,
2795+
plugin_dir=PLUGIN_DIR,
2796+
).async_find_missing()
28752797

28762798
async def async_install(self, pid: str, repo_url: str, package_version: Optional[str] = None,
28772799
release_version: Optional[str] = None,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""插件市场外部适配器。"""
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""插件市场查询客户端。"""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import Any, Optional
7+
8+
from app.adapters.external.market import PluginHelper as _PluginHelper
9+
from app.runtime.cache import async_fresh, fresh
10+
11+
12+
class PluginMarketClient:
13+
"""把插件市场、版本元数据和本地仓库查询隔离为只读客户端。"""
14+
15+
def __init__(self, helper: Optional[_PluginHelper] = None) -> None:
16+
"""复用旧 PluginHelper 实现,保持缓存和弱单例身份不变。"""
17+
self._helper = helper or _PluginHelper()
18+
19+
def get_plugins(
20+
self,
21+
repo_url: str,
22+
package_version: Optional[str] = None,
23+
force: bool = False,
24+
) -> Optional[dict[str, dict]]:
25+
"""同步读取指定仓库和代际的插件索引。"""
26+
with fresh(force):
27+
return self._helper.get_plugins(repo_url, package_version)
28+
29+
async def async_get_plugins(
30+
self,
31+
repo_url: str,
32+
package_version: Optional[str] = None,
33+
force: bool = False,
34+
) -> Optional[dict[str, dict]]:
35+
"""异步读取指定仓库和代际的插件索引。"""
36+
async with async_fresh(force):
37+
return await self._helper.async_get_plugins(repo_url, package_version)
38+
39+
def get_local_candidates(self) -> dict[str, dict]:
40+
"""返回全部本地插件仓库候选。"""
41+
return self._helper.get_local_plugin_candidates()
42+
43+
def get_local_candidate(
44+
self,
45+
plugin_id: str,
46+
package_version: Optional[str] = None,
47+
repo_path: Optional[Path] = None,
48+
**kwargs: Any,
49+
) -> Optional[dict]:
50+
"""返回指定插件的本地仓库候选。"""
51+
return self._helper.get_local_plugin_candidate(
52+
pid=plugin_id,
53+
package_version=package_version,
54+
repo_path=repo_path,
55+
**kwargs,
56+
)
57+
58+
@staticmethod
59+
def get_local_repo_paths() -> list[Path]:
60+
"""返回配置中有效的本地插件仓库目录。"""
61+
return _PluginHelper.get_local_repo_paths()
62+
63+
@staticmethod
64+
def make_local_repo_url(
65+
plugin_id: str,
66+
repo_path: Optional[object] = None,
67+
package_version: Optional[str] = None,
68+
) -> str:
69+
"""生成兼容旧入口的本地插件来源标识。"""
70+
return _PluginHelper.make_local_repo_url(
71+
plugin_id,
72+
repo_path,
73+
package_version,
74+
)
75+
76+
@staticmethod
77+
def is_local_repo_url(repo_url: Optional[str]) -> bool:
78+
"""判断插件来源是否为本地仓库标识。"""
79+
return _PluginHelper.is_local_repo_url(repo_url)
80+
81+
@staticmethod
82+
def annotate_system_version(plugin_info: dict) -> dict:
83+
"""补充插件所需 MoviePilot 版本兼容状态。"""
84+
return _PluginHelper.annotate_plugin_system_version(plugin_info)
85+
86+
@staticmethod
87+
def is_package_compatible(
88+
plugin_info: dict,
89+
package_version: Optional[str],
90+
) -> bool:
91+
"""判断插件条目是否兼容目标插件包代际。"""
92+
return _PluginHelper.is_package_plugin_compatible(
93+
plugin_info,
94+
package_version,
95+
)

0 commit comments

Comments
 (0)