fix: 协调本地插件与绑定仓库的版本选择 (#6482)

* fix(plugin): reconcile local and bound payload versions

* fix(plugin): preserve bound source on updates

* fix(plugin): normalize source reconciliation ids

* fix(plugin): defer local payload activation

* fix(plugin): centralize startup source admission

* fix(plugin): derive sync mode from admitted source
This commit is contained in:
InfinityPacer
2026-08-27 20:03:30 +08:00
committed by GitHub
parent 2b1f590c4d
commit 011ebfbe5f
16 changed files with 1030 additions and 77 deletions
+116 -24
View File
@@ -553,13 +553,13 @@ def select_plugin_candidate(
allow_source_change: bool = False,
) -> PluginSelection:
"""
允许来源、运行代际和同源版本选择一个插件载荷。
在线绑定、本地候选、运行代际和版本选择一个插件载荷。
:param inventory: 本轮市场读取快照
:param plugin_id: 要选择的物理插件 ID
:param generations: 调用方按优先级传入的代际顺序
:param identity: 已安装插件来源身份;为空表示未安装
:param local_candidates: 可选的本地载荷候选,优先于在线候选
:param local_candidates: 可选的本地载荷候选;已有在线绑定时参与代际和版本比较
:param requested_source_key: 调用方提供的规范在线来源;非显式调用不能绕过本地载荷
:param explicit_source: 本次调用是否代表管理员明确选源
:param allow_source_change: 是否是带 revision 的显式换源命令
@@ -572,6 +572,7 @@ def select_plugin_candidate(
if requested_source_key is not None
else None
)
local_selection: PluginSelection | None = None
if requested_source is None or not (explicit_source or allow_source_change):
local_selection = _select_local_candidate(
inventory,
@@ -580,11 +581,16 @@ def select_plugin_candidate(
generation_order=generation_order,
local_candidates=local_candidates,
)
if local_selection is not None:
if (
local_selection is not None
and local_selection.status is PluginSelectionStatus.INCOMPLETE
):
return local_selection
online = inventory.candidates_for(plugin_id)
if not online:
if local_selection is not None:
return local_selection
return PluginSelection(
status=PluginSelectionStatus.UNAVAILABLE,
reason=f"没有找到插件 {plugin_id} 的可用安装包",
@@ -627,35 +633,25 @@ def select_plugin_candidate(
),
)
if allowed_source is not None:
source_type, source_key = allowed_source
online = tuple(
candidate
for candidate in online
if candidate.source_type is source_type and candidate.source_key == source_key
)
if not online:
return PluginSelection(
status=PluginSelectionStatus.UNAVAILABLE,
reason="已绑定仓库中暂无可用插件包",
)
selected_online = _select_best(online, generation_order)
if selected_online is None:
return PluginSelection(
status=PluginSelectionStatus.UNAVAILABLE,
reason="已绑定仓库没有适用于当前 MoviePilot 版本的插件包",
)
return PluginSelection(
status=PluginSelectionStatus.SELECTED,
candidate=selected_online,
reason="已使用绑定仓库中的插件包",
return _select_bound_source_candidate(
online=online,
allowed_source=allowed_source,
local_selection=local_selection,
identity=identity,
generation_order=generation_order,
)
if identity is not None:
if local_selection is not None:
return local_selection
return PluginSelection(
status=PluginSelectionStatus.INCOMPLETE,
reason="当前插件尚未绑定仓库",
)
if local_selection is not None:
return local_selection
source_pairs = {(candidate.source_type, candidate.source_key) for candidate in online}
if len(source_pairs) > 1:
return PluginSelection(
@@ -683,6 +679,102 @@ def select_plugin_candidate(
)
def _select_bound_source_candidate(
*,
online: tuple[PluginMarketCandidate, ...],
allowed_source: tuple[TrustedPluginSourceType, str],
local_selection: PluginSelection | None,
identity: PluginIdentity | None,
generation_order: tuple[str, ...],
) -> PluginSelection:
"""只在已绑定仓库范围内选取在线候选,并与可用本地载荷协调。"""
if identity is None:
raise PluginSourceSelectionError("已绑定来源选择缺少插件身份")
source_type, source_key = allowed_source
allowed_online = tuple(
candidate
for candidate in online
if candidate.source_type is source_type and candidate.source_key == source_key
)
if not allowed_online:
if (
local_selection is not None
and local_selection.status is PluginSelectionStatus.SELECTED
):
return local_selection
return PluginSelection(
status=PluginSelectionStatus.UNAVAILABLE,
reason="已绑定仓库中暂无可用插件包",
)
selected_online = _select_best(allowed_online, generation_order)
if selected_online is None:
if (
local_selection is not None
and local_selection.status is PluginSelectionStatus.SELECTED
):
return local_selection
return PluginSelection(
status=PluginSelectionStatus.UNAVAILABLE,
reason="已绑定仓库没有适用于当前 MoviePilot 版本的插件包",
)
return _select_bound_or_local_candidate(
local_selection=local_selection,
online_candidate=selected_online,
identity=identity,
generation_order=generation_order,
)
def _select_bound_or_local_candidate(
*,
local_selection: PluginSelection | None,
online_candidate: Candidate,
identity: PluginIdentity,
generation_order: tuple[str, ...],
) -> PluginSelection:
"""在已绑定在线候选和本地候选之间选择代际、版本更高的载荷。"""
if (
local_selection is None
or local_selection.status is not PluginSelectionStatus.SELECTED
or local_selection.candidate is None
):
return PluginSelection(
status=PluginSelectionStatus.SELECTED,
candidate=online_candidate,
reason="已使用绑定仓库中的插件包",
)
local_candidate = local_selection.candidate
local_generation = generation_order.index(local_candidate.package_generation)
online_generation = generation_order.index(online_candidate.package_generation)
if local_generation < online_generation:
return local_selection
if online_generation < local_generation:
return PluginSelection(
status=PluginSelectionStatus.SELECTED,
candidate=online_candidate,
reason="绑定仓库提供了更高代际的插件包",
)
local_version = local_candidate.plugin_version or "0"
online_version = online_candidate.plugin_version or "0"
if compare_version(local_version, ">", online_version):
return local_selection
if compare_version(online_version, ">", local_version):
return PluginSelection(
status=PluginSelectionStatus.SELECTED,
candidate=online_candidate,
reason="绑定仓库提供了更高版本的插件包",
)
if identity.payload_source_type is PluginPayloadSourceType.LOCAL:
return local_selection
return PluginSelection(
status=PluginSelectionStatus.SELECTED,
candidate=online_candidate,
reason="当前继续使用绑定仓库中的插件包",
)
def list_effective_online_candidates(
inventory: CandidateInventory,
*,