mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""插件应用端口与用例。"""
|
||||
@@ -0,0 +1,275 @@
|
||||
"""插件市场目录应用服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
MarketLoader = Callable[[str, Optional[str], bool], Optional[dict[str, dict]]]
|
||||
AsyncMarketLoader = Callable[
|
||||
[str, Optional[str], bool],
|
||||
Awaitable[Optional[dict[str, dict]]],
|
||||
]
|
||||
PluginMapper = Callable[[str, dict, str, list[str], int, Optional[str]], Any]
|
||||
ProgressCallback = Callable[..., Any]
|
||||
|
||||
|
||||
class PluginCatalogService:
|
||||
"""负责插件市场索引映射、并发收集、代际合并和来源去重。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
market_loader: MarketLoader,
|
||||
async_market_loader: AsyncMarketLoader,
|
||||
installed_plugins_provider: Callable[[], list[str]],
|
||||
plugin_mapper: PluginMapper,
|
||||
is_local_repo: Callable[[Optional[str]], bool],
|
||||
version_compare: Callable[[str, str, str], bool],
|
||||
warning: Callable[[str], Any],
|
||||
error: Callable[[str], Any],
|
||||
) -> None:
|
||||
"""保存市场读取、插件映射和版本比较端口。"""
|
||||
self._market_loader = market_loader
|
||||
self._async_market_loader = async_market_loader
|
||||
self._installed_plugins_provider = installed_plugins_provider
|
||||
self._plugin_mapper = plugin_mapper
|
||||
self._is_local_repo = is_local_repo
|
||||
self._version_compare = version_compare
|
||||
self._warning = warning
|
||||
self._error = error
|
||||
|
||||
def load(
|
||||
self,
|
||||
market: str,
|
||||
package_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> list[Any]:
|
||||
"""同步读取并映射指定市场和插件代际。"""
|
||||
if not market:
|
||||
return []
|
||||
online_plugins = self._market_loader(market, package_version, force)
|
||||
if online_plugins is None:
|
||||
self._warning(
|
||||
f"获取{package_version if package_version else ''}插件库失败:"
|
||||
f"{market},请检查 GitHub 网络连接"
|
||||
)
|
||||
return []
|
||||
return self._map_plugins(online_plugins, market, package_version)
|
||||
|
||||
async def async_load(
|
||||
self,
|
||||
market: str,
|
||||
package_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> list[Any]:
|
||||
"""异步读取并映射指定市场和插件代际。"""
|
||||
if not market:
|
||||
return []
|
||||
online_plugins = await self._async_market_loader(
|
||||
market,
|
||||
package_version,
|
||||
force,
|
||||
)
|
||||
if online_plugins is None:
|
||||
self._warning(
|
||||
f"获取{package_version if package_version else ''}插件库失败:"
|
||||
f"{market},请检查 GitHub 网络连接"
|
||||
)
|
||||
return []
|
||||
return self._map_plugins(online_plugins, market, package_version)
|
||||
|
||||
def collect(
|
||||
self,
|
||||
*,
|
||||
markets: list[str],
|
||||
compatible_flags: list[str],
|
||||
force: bool,
|
||||
loader: Callable[[str, Optional[str], bool], list[Any]],
|
||||
) -> list[Any]:
|
||||
"""并发读取多个市场和代际,并按稳定优先级合并。"""
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures_meta: dict[
|
||||
concurrent.futures.Future,
|
||||
tuple[int, bool, int],
|
||||
] = {}
|
||||
for market_index, market in enumerate(markets):
|
||||
base_future = executor.submit(loader, market, None, force)
|
||||
futures_meta[base_future] = (market_index, False, 0)
|
||||
for flag_priority, flag in enumerate(compatible_flags):
|
||||
higher_future = executor.submit(loader, market, flag, force)
|
||||
futures_meta[higher_future] = (
|
||||
market_index,
|
||||
True,
|
||||
flag_priority,
|
||||
)
|
||||
|
||||
collected = []
|
||||
for future in concurrent.futures.as_completed(futures_meta):
|
||||
plugins = future.result()
|
||||
market_index, is_higher, flag_priority = futures_meta[future]
|
||||
collected.append((
|
||||
market_index,
|
||||
is_higher,
|
||||
flag_priority,
|
||||
plugins or [],
|
||||
))
|
||||
|
||||
collected.sort(key=lambda item: (item[0], 0 if item[1] else 1, item[2]))
|
||||
higher_plugins = []
|
||||
base_plugins = []
|
||||
for _market_index, is_higher, _flag_priority, plugins in collected:
|
||||
(higher_plugins if is_higher else base_plugins).extend(plugins)
|
||||
return self.merge(higher_plugins, base_plugins, markets)
|
||||
|
||||
async def async_collect(
|
||||
self,
|
||||
*,
|
||||
markets: list[str],
|
||||
compatible_flags: list[str],
|
||||
force: bool,
|
||||
loader: Callable[
|
||||
[str, Optional[str], bool],
|
||||
Awaitable[list[Any]],
|
||||
],
|
||||
progress_callback: Optional[ProgressCallback] = None,
|
||||
) -> list[Any]:
|
||||
"""异步读取多个市场和代际,并持续报告稳定进度。"""
|
||||
async def fetch(
|
||||
market: str,
|
||||
package_version: Optional[str],
|
||||
result_version: str,
|
||||
task_index: int,
|
||||
) -> tuple[int, str, list[Any]]:
|
||||
"""读取一个市场代际并保留创建时的稳定任务序号。"""
|
||||
plugins = await loader(market, package_version, force)
|
||||
return task_index, result_version, plugins or []
|
||||
|
||||
tasks = []
|
||||
for market in markets:
|
||||
tasks.append(asyncio.create_task(
|
||||
fetch(market, None, "base_version", len(tasks))
|
||||
))
|
||||
for flag in compatible_flags:
|
||||
tasks.append(asyncio.create_task(
|
||||
fetch(market, flag, "higher_version", len(tasks))
|
||||
))
|
||||
|
||||
higher_plugins = []
|
||||
base_plugins = []
|
||||
if tasks:
|
||||
total_tasks = len(tasks)
|
||||
finished_tasks = 0
|
||||
task_results = {}
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
value=0,
|
||||
text=f"开始刷新插件市场,共 {total_tasks} 个请求 ...",
|
||||
data={"total": total_tasks, "finished": 0},
|
||||
)
|
||||
for completed_task in asyncio.as_completed(tasks):
|
||||
try:
|
||||
task_index, version, plugins = await completed_task
|
||||
task_results[task_index] = (version, plugins)
|
||||
except Exception as err:
|
||||
self._error(f"获取插件市场数据失败:{str(err)}")
|
||||
finished_tasks += 1
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
value=finished_tasks / total_tasks * 100,
|
||||
text=(
|
||||
f"插件市场请求({finished_tasks}/{total_tasks})"
|
||||
"处理完成"
|
||||
),
|
||||
data={"total": total_tasks, "finished": finished_tasks},
|
||||
)
|
||||
for task_index in sorted(task_results):
|
||||
version, plugins = task_results[task_index]
|
||||
(higher_plugins if version == "higher_version" else base_plugins).extend(
|
||||
plugins
|
||||
)
|
||||
|
||||
result = self.merge(higher_plugins, base_plugins, markets)
|
||||
if progress_callback:
|
||||
progress_callback(value=100, text="插件市场缓存刷新完成")
|
||||
return result
|
||||
|
||||
def merge(
|
||||
self,
|
||||
higher_plugins: list[Any],
|
||||
base_plugins: list[Any],
|
||||
markets: list[str],
|
||||
) -> list[Any]:
|
||||
"""按代际、来源顺序和版本合并插件目录。"""
|
||||
all_plugins = list(higher_plugins)
|
||||
higher_keys = {
|
||||
f"{plugin.id}{plugin.plugin_version}"
|
||||
for plugin in higher_plugins
|
||||
}
|
||||
all_plugins.extend(
|
||||
plugin
|
||||
for plugin in base_plugins
|
||||
if f"{plugin.id}{plugin.plugin_version}" not in higher_keys
|
||||
)
|
||||
|
||||
def repo_order(plugin: Any) -> int:
|
||||
"""本地来源排在远程市场之后,远程来源保持配置顺序。"""
|
||||
if self._is_local_repo(plugin.repo_url):
|
||||
return len(markets) + 1
|
||||
if plugin.repo_url in markets:
|
||||
return markets.index(plugin.repo_url)
|
||||
return len(markets)
|
||||
|
||||
deduplicated = {}
|
||||
for plugin in sorted(all_plugins, key=repo_order):
|
||||
key = f"{plugin.id}{plugin.plugin_version}"
|
||||
exists = deduplicated.get(key)
|
||||
if not exists or (
|
||||
self._is_local_repo(exists.repo_url)
|
||||
and not self._is_local_repo(plugin.repo_url)
|
||||
):
|
||||
deduplicated[key] = plugin
|
||||
|
||||
result_by_id = {}
|
||||
for plugin in sorted(deduplicated.values(), key=repo_order):
|
||||
exists = result_by_id.get(plugin.id)
|
||||
if not exists \
|
||||
or self._version_compare(
|
||||
plugin.plugin_version,
|
||||
">",
|
||||
exists.plugin_version,
|
||||
) \
|
||||
or (
|
||||
plugin.plugin_version == exists.plugin_version
|
||||
and self._is_local_repo(exists.repo_url)
|
||||
and not self._is_local_repo(plugin.repo_url)
|
||||
):
|
||||
result_by_id[plugin.id] = plugin
|
||||
return list(result_by_id.values())
|
||||
|
||||
def _map_plugins(
|
||||
self,
|
||||
online_plugins: dict[str, dict],
|
||||
market: str,
|
||||
package_version: Optional[str],
|
||||
) -> list[Any]:
|
||||
"""把一个市场索引映射为宿主插件 DTO。"""
|
||||
installed_plugins = self._installed_plugins_provider()
|
||||
result = []
|
||||
add_time = len(online_plugins)
|
||||
for plugin_id, plugin_info in online_plugins.items():
|
||||
plugin = self._plugin_mapper(
|
||||
plugin_id,
|
||||
plugin_info,
|
||||
market,
|
||||
installed_plugins,
|
||||
add_time,
|
||||
package_version,
|
||||
)
|
||||
if plugin:
|
||||
result.append(plugin)
|
||||
add_time -= 1
|
||||
return result
|
||||
@@ -0,0 +1,59 @@
|
||||
"""插件配置保存、重置和运行态重建应用用例。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginConfigResult:
|
||||
"""描述插件配置写操作是否成功及提示信息。"""
|
||||
|
||||
success: bool
|
||||
message: str = ""
|
||||
|
||||
|
||||
class PluginConfigCommand:
|
||||
"""协调插件配置持久化、实例初始化和运行时注册刷新。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
save_config: Callable[[str, dict, bool], bool],
|
||||
initialize: Callable[[str, dict], Any],
|
||||
stop: Callable[[str], Any],
|
||||
delete_config: Callable[[str, bool], bool],
|
||||
delete_data: Callable[[str, bool], bool],
|
||||
reload_runtime: Callable[[str], Any],
|
||||
publish_reset: Callable[[str], Any],
|
||||
refresh_registrations: Callable[[str], Any],
|
||||
) -> None:
|
||||
"""保存插件管理 Facade 和运行时注册刷新端口。"""
|
||||
self._save_config = save_config
|
||||
self._initialize = initialize
|
||||
self._stop = stop
|
||||
self._delete_config = delete_config
|
||||
self._delete_data = delete_data
|
||||
self._reload_runtime = reload_runtime
|
||||
self._publish_reset = publish_reset
|
||||
self._refresh_registrations = refresh_registrations
|
||||
|
||||
def update(self, plugin_id: str, config: dict) -> PluginConfigResult:
|
||||
"""保存配置并按既有顺序重新初始化实例及运行时注册。"""
|
||||
if not self._save_config(plugin_id, config, False):
|
||||
return PluginConfigResult(False, "插件配置保存失败")
|
||||
self._initialize(plugin_id, config)
|
||||
self._refresh_registrations(plugin_id)
|
||||
return PluginConfigResult(True)
|
||||
|
||||
def reset(self, plugin_id: str) -> PluginConfigResult:
|
||||
"""通知插件补偿后停止实例、删除配置数据并重建运行态。"""
|
||||
self._publish_reset(plugin_id)
|
||||
self._stop(plugin_id)
|
||||
self._delete_config(plugin_id, True)
|
||||
self._delete_data(plugin_id, True)
|
||||
self._reload_runtime(plugin_id)
|
||||
self._refresh_registrations(plugin_id)
|
||||
return PluginConfigResult(True)
|
||||
@@ -0,0 +1,384 @@
|
||||
"""插件安装应用用例。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
InstalledPluginsReader = Callable[[], list[str]]
|
||||
InstalledPluginsWriter = Callable[[list[str]], Awaitable[object]]
|
||||
PluginIdsProvider = Callable[[], list[str]]
|
||||
CompatibilityChecker = Callable[[str, str], Awaitable[Optional[str]]]
|
||||
PackageInstaller = Callable[
|
||||
[str, str, Optional[str], bool],
|
||||
Awaitable[tuple[bool, str]],
|
||||
]
|
||||
PackageCheckpointer = Callable[[str], Awaitable[Any]]
|
||||
PackageCheckpointAction = Callable[[Any], Awaitable[object]]
|
||||
InstallReporter = Callable[[str, Optional[str]], Awaitable[object]]
|
||||
PluginReloader = Callable[[str], Awaitable[object]]
|
||||
PluginRegistrationRefresher = Callable[[str], Awaitable[object]]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginInstallRollback:
|
||||
"""描述失败安装中各类可补偿副作用的恢复结果。"""
|
||||
|
||||
file_attempted: bool = False
|
||||
file_restored: bool = False
|
||||
installed_list_attempted: bool = False
|
||||
installed_list_restored: bool = False
|
||||
runtime_attempted: bool = False
|
||||
runtime_restored: bool = False
|
||||
registrations_attempted: bool = False
|
||||
registrations_restored: bool = False
|
||||
dependency_supported: bool = False
|
||||
errors: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginInstallResult:
|
||||
"""描述插件安装结果、失败阶段和可观察补偿状态。"""
|
||||
|
||||
success: bool
|
||||
message: str = ""
|
||||
refreshed_only: bool = False
|
||||
package_installed: bool = False
|
||||
installed_list_persisted: bool = False
|
||||
runtime_reloaded: bool = False
|
||||
registrations_refreshed: bool = False
|
||||
reported: bool = False
|
||||
report_error: str = ""
|
||||
failure_stage: Optional[str] = None
|
||||
checkpoint_cleanup_error: str = ""
|
||||
rollback: PluginInstallRollback = field(default_factory=PluginInstallRollback)
|
||||
|
||||
|
||||
class PluginInstallCommand:
|
||||
"""协调插件检查、包事务、持久化、运行态刷新和安装上报。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
installed_plugins_reader: InstalledPluginsReader,
|
||||
installed_plugins_writer: InstalledPluginsWriter,
|
||||
plugin_ids_provider: PluginIdsProvider,
|
||||
compatibility_checker: CompatibilityChecker,
|
||||
package_installer: PackageInstaller,
|
||||
package_checkpointer: PackageCheckpointer,
|
||||
package_committer: PackageCheckpointAction,
|
||||
package_rollback: PackageCheckpointAction,
|
||||
install_reporter: InstallReporter,
|
||||
plugin_reloader: PluginReloader,
|
||||
registration_refresher: PluginRegistrationRefresher,
|
||||
) -> None:
|
||||
"""保存安装用例所需端口,不绑定数据库、网络或运行时实现。"""
|
||||
self._installed_plugins_reader = installed_plugins_reader
|
||||
self._installed_plugins_writer = installed_plugins_writer
|
||||
self._plugin_ids_provider = plugin_ids_provider
|
||||
self._compatibility_checker = compatibility_checker
|
||||
self._package_installer = package_installer
|
||||
self._package_checkpointer = package_checkpointer
|
||||
self._package_committer = package_committer
|
||||
self._package_rollback = package_rollback
|
||||
self._install_reporter = install_reporter
|
||||
self._plugin_reloader = plugin_reloader
|
||||
self._registration_refresher = registration_refresher
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
*,
|
||||
plugin_id: str,
|
||||
repo_url: Optional[str],
|
||||
release_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> PluginInstallResult:
|
||||
"""执行插件安装,并在关键阶段失败时恢复可补偿状态。"""
|
||||
installed_plugins = list(self._installed_plugins_reader() or [])
|
||||
refreshed_only = not force and plugin_id in self._plugin_ids_provider()
|
||||
if refreshed_only:
|
||||
return await self._refresh_existing(
|
||||
plugin_id=plugin_id,
|
||||
repo_url=repo_url,
|
||||
)
|
||||
if not repo_url:
|
||||
return PluginInstallResult(
|
||||
success=False,
|
||||
message="没有传入仓库地址,无法正确安装插件,请检查配置",
|
||||
failure_stage="validation",
|
||||
)
|
||||
|
||||
try:
|
||||
checkpoint = await self._package_checkpointer(plugin_id)
|
||||
except Exception as err:
|
||||
return PluginInstallResult(
|
||||
success=False,
|
||||
message=f"创建插件安装快照失败:{err}",
|
||||
failure_stage="package_checkpoint",
|
||||
)
|
||||
|
||||
try:
|
||||
state, message = await self._package_installer(
|
||||
plugin_id,
|
||||
repo_url,
|
||||
release_version,
|
||||
force,
|
||||
)
|
||||
except Exception as err:
|
||||
return await self._failure(
|
||||
plugin_id=plugin_id,
|
||||
original_plugins=installed_plugins,
|
||||
checkpoint=checkpoint,
|
||||
stage="package_install",
|
||||
message=str(err),
|
||||
package_installed=False,
|
||||
)
|
||||
if not state:
|
||||
return await self._failure(
|
||||
plugin_id=plugin_id,
|
||||
original_plugins=installed_plugins,
|
||||
checkpoint=checkpoint,
|
||||
stage="package_install",
|
||||
message=message,
|
||||
package_installed=False,
|
||||
)
|
||||
|
||||
installed_list_persisted = False
|
||||
if plugin_id not in installed_plugins:
|
||||
updated_plugins = [*installed_plugins, plugin_id]
|
||||
try:
|
||||
await self._installed_plugins_writer(updated_plugins)
|
||||
installed_list_persisted = True
|
||||
except Exception as err:
|
||||
return await self._failure(
|
||||
plugin_id=plugin_id,
|
||||
original_plugins=installed_plugins,
|
||||
checkpoint=checkpoint,
|
||||
stage="installed_list_persistence",
|
||||
message=str(err),
|
||||
package_installed=True,
|
||||
)
|
||||
|
||||
try:
|
||||
await self._plugin_reloader(plugin_id)
|
||||
except Exception as err:
|
||||
return await self._failure(
|
||||
plugin_id=plugin_id,
|
||||
original_plugins=installed_plugins,
|
||||
checkpoint=checkpoint,
|
||||
stage="runtime_reload",
|
||||
message=str(err),
|
||||
package_installed=True,
|
||||
installed_list_persisted=installed_list_persisted,
|
||||
runtime_touched=True,
|
||||
)
|
||||
|
||||
try:
|
||||
await self._registration_refresher(plugin_id)
|
||||
except Exception as err:
|
||||
return await self._failure(
|
||||
plugin_id=plugin_id,
|
||||
original_plugins=installed_plugins,
|
||||
checkpoint=checkpoint,
|
||||
stage="registration_refresh",
|
||||
message=str(err),
|
||||
package_installed=True,
|
||||
installed_list_persisted=installed_list_persisted,
|
||||
runtime_touched=True,
|
||||
registrations_touched=True,
|
||||
)
|
||||
|
||||
checkpoint_cleanup_error = ""
|
||||
try:
|
||||
await self._package_committer(checkpoint)
|
||||
except Exception as err:
|
||||
checkpoint_cleanup_error = str(err)
|
||||
|
||||
reported = False
|
||||
report_error = ""
|
||||
try:
|
||||
report_result = await self._install_reporter(plugin_id, repo_url)
|
||||
reported = report_result is not False
|
||||
if not reported:
|
||||
report_error = "安装上报未确认"
|
||||
except Exception as err:
|
||||
report_error = str(err)
|
||||
|
||||
result_message = message or "插件安装成功"
|
||||
if checkpoint_cleanup_error:
|
||||
result_message = f"{result_message};临时安装快照清理失败"
|
||||
if report_error:
|
||||
result_message = f"{result_message};安装上报失败,不影响本地安装"
|
||||
return PluginInstallResult(
|
||||
success=True,
|
||||
message=result_message,
|
||||
package_installed=True,
|
||||
installed_list_persisted=installed_list_persisted,
|
||||
runtime_reloaded=True,
|
||||
registrations_refreshed=True,
|
||||
reported=reported,
|
||||
report_error=report_error,
|
||||
checkpoint_cleanup_error=checkpoint_cleanup_error,
|
||||
)
|
||||
|
||||
async def _refresh_existing(
|
||||
self,
|
||||
*,
|
||||
plugin_id: str,
|
||||
repo_url: Optional[str],
|
||||
) -> PluginInstallResult:
|
||||
"""刷新已存在插件,不触碰包文件和已安装列表。"""
|
||||
if repo_url:
|
||||
compatible_message = await self._compatibility_checker(
|
||||
plugin_id,
|
||||
repo_url,
|
||||
)
|
||||
if compatible_message:
|
||||
return PluginInstallResult(
|
||||
success=False,
|
||||
message=compatible_message,
|
||||
refreshed_only=True,
|
||||
failure_stage="compatibility",
|
||||
)
|
||||
failure_stage = "runtime_reload"
|
||||
try:
|
||||
await self._plugin_reloader(plugin_id)
|
||||
failure_stage = "registration_refresh"
|
||||
await self._registration_refresher(plugin_id)
|
||||
except Exception as err:
|
||||
rollback_errors = []
|
||||
runtime_restored = False
|
||||
registrations_restored = False
|
||||
try:
|
||||
await self._plugin_reloader(plugin_id)
|
||||
runtime_restored = True
|
||||
except Exception as rollback_err:
|
||||
rollback_errors.append(f"运行态恢复失败:{rollback_err}")
|
||||
if runtime_restored:
|
||||
try:
|
||||
await self._registration_refresher(plugin_id)
|
||||
registrations_restored = True
|
||||
except Exception as rollback_err:
|
||||
rollback_errors.append(f"路由和服务注册恢复失败:{rollback_err}")
|
||||
return PluginInstallResult(
|
||||
success=False,
|
||||
message=f"刷新插件运行态失败:{err}",
|
||||
refreshed_only=True,
|
||||
failure_stage=failure_stage,
|
||||
rollback=PluginInstallRollback(
|
||||
runtime_attempted=True,
|
||||
runtime_restored=runtime_restored,
|
||||
registrations_attempted=True,
|
||||
registrations_restored=registrations_restored,
|
||||
errors=tuple(rollback_errors),
|
||||
),
|
||||
)
|
||||
|
||||
reported = False
|
||||
report_error = ""
|
||||
try:
|
||||
report_result = await self._install_reporter(plugin_id, repo_url)
|
||||
reported = report_result is not False
|
||||
if not reported:
|
||||
report_error = "安装上报未确认"
|
||||
except Exception as err:
|
||||
report_error = str(err)
|
||||
return PluginInstallResult(
|
||||
success=True,
|
||||
message=(
|
||||
"插件已存在,已刷新加载"
|
||||
if not report_error
|
||||
else "插件已存在,已刷新加载;安装上报失败,不影响本地刷新"
|
||||
),
|
||||
refreshed_only=True,
|
||||
runtime_reloaded=True,
|
||||
registrations_refreshed=True,
|
||||
reported=reported,
|
||||
report_error=report_error,
|
||||
)
|
||||
|
||||
async def _failure(
|
||||
self,
|
||||
*,
|
||||
plugin_id: str,
|
||||
original_plugins: list[str],
|
||||
checkpoint: Any,
|
||||
stage: str,
|
||||
message: str,
|
||||
package_installed: bool,
|
||||
installed_list_persisted: bool = False,
|
||||
runtime_touched: bool = False,
|
||||
registrations_touched: bool = False,
|
||||
) -> PluginInstallResult:
|
||||
"""按持久化、文件、运行态顺序补偿失败安装并记录结果。"""
|
||||
errors = []
|
||||
installed_list_restored = False
|
||||
if installed_list_persisted:
|
||||
try:
|
||||
await self._installed_plugins_writer(list(original_plugins))
|
||||
installed_list_restored = True
|
||||
except Exception as err:
|
||||
errors.append(f"已安装列表恢复失败:{err}")
|
||||
|
||||
file_restored = False
|
||||
try:
|
||||
await self._package_rollback(checkpoint)
|
||||
file_restored = True
|
||||
except Exception as err:
|
||||
errors.append(f"插件文件恢复失败:{err}")
|
||||
|
||||
runtime_restored = False
|
||||
registrations_restored = False
|
||||
if runtime_touched:
|
||||
try:
|
||||
await self._plugin_reloader(plugin_id)
|
||||
runtime_restored = True
|
||||
except Exception as err:
|
||||
errors.append(f"插件运行态恢复失败:{err}")
|
||||
if runtime_restored:
|
||||
try:
|
||||
await self._registration_refresher(plugin_id)
|
||||
registrations_restored = True
|
||||
except Exception as err:
|
||||
errors.append(f"插件路由和服务注册恢复失败:{err}")
|
||||
|
||||
rollback = PluginInstallRollback(
|
||||
file_attempted=True,
|
||||
file_restored=file_restored,
|
||||
installed_list_attempted=installed_list_persisted,
|
||||
installed_list_restored=installed_list_restored,
|
||||
runtime_attempted=runtime_touched,
|
||||
runtime_restored=runtime_restored,
|
||||
registrations_attempted=runtime_touched or registrations_touched,
|
||||
registrations_restored=registrations_restored,
|
||||
dependency_supported=False,
|
||||
errors=tuple(errors),
|
||||
)
|
||||
rollback_message = []
|
||||
rollback_message.append("插件文件已恢复" if file_restored else "插件文件恢复失败")
|
||||
if installed_list_persisted:
|
||||
rollback_message.append(
|
||||
"已安装列表已恢复"
|
||||
if installed_list_restored
|
||||
else "已安装列表恢复失败"
|
||||
)
|
||||
if runtime_touched:
|
||||
rollback_message.append(
|
||||
"旧运行态已恢复" if runtime_restored else "旧运行态恢复失败"
|
||||
)
|
||||
rollback_message.append(
|
||||
"旧路由和服务注册已恢复"
|
||||
if registrations_restored
|
||||
else "旧路由和服务注册恢复失败"
|
||||
)
|
||||
rollback_message.append("Python依赖变更不支持自动回滚")
|
||||
return PluginInstallResult(
|
||||
success=False,
|
||||
message=f"{message};{';'.join(rollback_message)}",
|
||||
package_installed=package_installed,
|
||||
installed_list_persisted=installed_list_persisted,
|
||||
failure_stage=stage,
|
||||
rollback=rollback,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""动态插件路由应用端口。"""
|
||||
|
||||
from typing import Optional, Protocol
|
||||
|
||||
|
||||
class DynamicRouteRegistry(Protocol):
|
||||
"""插件生命周期操作动态 HTTP 路由所需的最小端口。"""
|
||||
|
||||
def update(self, plugin_id: Optional[str], action: str) -> None:
|
||||
"""新增或移除指定插件的动态路由。"""
|
||||
...
|
||||
|
||||
def remove(self, plugin_id: str) -> bool:
|
||||
"""移除指定插件的全部动态路由。"""
|
||||
...
|
||||
Reference in New Issue
Block a user