"""插件市场候选事实与来源选择策略。""" from __future__ import annotations from collections.abc import Iterable, Sequence from dataclasses import dataclass, field from enum import StrEnum from typing import Any, TypeAlias from urllib.parse import unquote, urlsplit from app.application.plugin.identity import ( PluginIdentity, PluginPayloadSourceType, TrustedPluginSourceType, normalize_physical_plugin_id, validate_online_source_key, ) from app.application.plugin.identity import ( PluginSourceCandidate as IdentitySourceCandidate, ) from app.foundation.version import compare_version PLUGIN_GENERATIONS = ("v1", "v2", "v3") class MarketReadStatus(StrEnum): """一次市场索引读取的最终状态。""" PRESENT = "present" ABSENT = "absent" FAILED = "failed" class PluginSelectionStatus(StrEnum): """插件候选选择的可观察结果。""" SELECTED = "selected" UNAVAILABLE = "unavailable" CONFLICT = "conflict" INCOMPLETE = "incomplete" class PluginSourceSelectionError(RuntimeError): """插件来源选择的策略错误。""" @dataclass(frozen=True, slots=True) class PluginMarketCandidate: """一个在线市场条目的原始候选事实。""" plugin_id: str source_key: str source_type: TrustedPluginSourceType repo_url: str package_generation: str plugin_version: str | None dto: Any = None normalized_plugin_id: str = field(init=False) def __post_init__(self) -> None: """校验候选身份,并把外部来源键归一为持久化合同使用的形式。""" normalized_id = normalize_physical_plugin_id(self.plugin_id) source_type = _coerce_online_source_type(self.source_type) source_key = validate_online_source_key(self.source_key) # IdentitySourceCandidate 复用官方仓库与来源类型的双向约束。 IdentitySourceCandidate(source_type=source_type, source_key=source_key) repo_url = _normalize_repo_url(self.repo_url) package_generation = normalize_package_generation(self.package_generation) plugin_version = _normalize_plugin_version(self.plugin_version) object.__setattr__(self, "normalized_plugin_id", normalized_id) object.__setattr__(self, "source_type", source_type) object.__setattr__(self, "source_key", source_key) object.__setattr__(self, "repo_url", repo_url) object.__setattr__(self, "package_generation", package_generation) object.__setattr__(self, "plugin_version", plugin_version) @property def payload_source_type(self) -> PluginPayloadSourceType: """返回用于载荷审计的在线来源类型。""" return PluginPayloadSourceType(self.source_type.value) def public_dict(self) -> dict[str, Any]: """生成不携带原始元数据的公共候选投影。""" return { "plugin_id": self.plugin_id, "source_key": self.source_key, "source_type": self.source_type.value, "repo_url": self.repo_url, "package_generation": self.package_generation, "plugin_version": self.plugin_version, } @dataclass(frozen=True, slots=True) class PluginLocalCandidate: """一个本地插件载荷候选,与在线来源身份保持独立。""" plugin_id: str repo_url: str package_generation: str plugin_version: str | None dto: Any = None normalized_plugin_id: str = field(init=False) def __post_init__(self) -> None: """校验本地载荷的插件与版本事实。""" normalized_id = normalize_physical_plugin_id(self.plugin_id) repo_url = _normalize_repo_url(self.repo_url) package_generation = normalize_package_generation(self.package_generation) plugin_version = _normalize_plugin_version(self.plugin_version) object.__setattr__(self, "normalized_plugin_id", normalized_id) object.__setattr__(self, "repo_url", repo_url) object.__setattr__(self, "package_generation", package_generation) object.__setattr__(self, "plugin_version", plugin_version) @property def payload_source_type(self) -> PluginPayloadSourceType: """返回本地载荷类型,不把本地路径伪装成在线来源。""" return PluginPayloadSourceType.LOCAL @property def source_type(self) -> PluginPayloadSourceType: """返回独立的本地载荷类型,不伪造在线可信来源。""" return PluginPayloadSourceType.LOCAL @property def source_key(self) -> None: """本地载荷没有可绑定的在线来源键。""" return None def public_dict(self) -> dict[str, Any]: """生成本地候选的公共投影,永不暴露仓库路径或原始 metadata。""" return { "plugin_id": self.plugin_id, "source_type": PluginPayloadSourceType.LOCAL.value, "package_generation": self.package_generation, "plugin_version": self.plugin_version, } @dataclass(frozen=True, slots=True) class MarketRead: """记录一个配置市场的读取状态及其全部在线候选。""" market: str status: MarketReadStatus candidates: tuple[PluginMarketCandidate, ...] = () error: str | None = None package_generation: str = "v1" def __post_init__(self) -> None: """拒绝把失败读取伪装成空成功结果。""" market = _normalize_market(self.market) status = MarketReadStatus(self.status) candidates = tuple(self.candidates) package_generation = normalize_package_generation(self.package_generation) if status is MarketReadStatus.FAILED: if candidates: raise ValueError("失败的插件市场读取不能携带候选") if not self.error or not self.error.strip(): raise ValueError("失败的插件市场读取必须保留错误说明") else: if self.error: raise ValueError("已判定的插件市场读取不能携带错误说明") if status is MarketReadStatus.ABSENT and candidates: raise ValueError("不存在的插件市场索引不能携带候选") if any(not isinstance(candidate, PluginMarketCandidate) for candidate in candidates): raise TypeError("市场读取候选必须是在线插件候选") object.__setattr__(self, "market", market) object.__setattr__(self, "status", status) object.__setattr__(self, "candidates", candidates) object.__setattr__(self, "error", self.error.strip() if self.error else None) object.__setattr__(self, "package_generation", package_generation) @classmethod def present( cls, market: str, candidates: Iterable[PluginMarketCandidate] = (), *, package_generation: str = "v1", ) -> "MarketRead": """构造存在的索引读取;真实空索引可以没有候选。""" return cls( market=market, status=MarketReadStatus.PRESENT, candidates=tuple(candidates), package_generation=package_generation, ) @classmethod def absent( cls, market: str, *, package_generation: str = "v1", ) -> "MarketRead": """构造已确认不存在的代际索引。""" return cls( market=market, status=MarketReadStatus.ABSENT, package_generation=package_generation, ) @classmethod def failure( cls, market: str, error: str, *, package_generation: str = "v1", ) -> "MarketRead": """构造失败读取,并保留可诊断但不用于选择的错误说明。""" return cls( market=market, status=MarketReadStatus.FAILED, error=error, package_generation=package_generation, ) @property def succeeded(self) -> bool: """判断该索引是否得到存在或不存在的确定结论。""" return self.status is not MarketReadStatus.FAILED @property def present_index(self) -> bool: """判断该代际索引是否真实存在。""" return self.status is MarketReadStatus.PRESENT def public_dict(self) -> dict[str, Any]: """生成市场读取的脱敏投影,保留状态、代际和候选事实。""" return { "market": self.market, "package_generation": self.package_generation, "status": self.status.value, "error": self.error, "candidates": [candidate.public_dict() for candidate in self.candidates], } class LocalCandidateReadStatus(StrEnum): """一次本地插件仓库扫描的可观察终态。""" PRESENT = "present" ABSENT = "absent" FAILED = "failed" @dataclass(frozen=True, slots=True) class LocalCandidateRead: """记录本地候选扫描状态,避免扫描失败伪装成空仓库。""" status: LocalCandidateReadStatus candidates: tuple[PluginLocalCandidate, ...] = () error: str | None = None def __post_init__(self) -> None: """保证本地扫描状态、候选和错误说明相互一致。""" status = LocalCandidateReadStatus(self.status) candidates = tuple(self.candidates) if any(not isinstance(candidate, PluginLocalCandidate) for candidate in candidates): raise TypeError("本地扫描候选必须是 PluginLocalCandidate") if status is LocalCandidateReadStatus.FAILED: if candidates: raise ValueError("失败的本地扫描不能携带候选") if not self.error or not self.error.strip(): raise ValueError("失败的本地扫描必须保留错误说明") else: if self.error: raise ValueError("已判定的本地扫描不能携带错误说明") if status is LocalCandidateReadStatus.ABSENT and candidates: raise ValueError("不存在的本地扫描不能携带候选") object.__setattr__(self, "status", status) object.__setattr__(self, "candidates", candidates) object.__setattr__(self, "error", self.error.strip() if self.error else None) @classmethod def present( cls, candidates: Iterable[PluginLocalCandidate] = (), ) -> "LocalCandidateRead": """构造扫描成功的本地候选快照,空结果仍表示扫描成功。""" return cls( status=LocalCandidateReadStatus.PRESENT, candidates=tuple(candidates), ) @classmethod def absent(cls) -> "LocalCandidateRead": """构造没有配置本地仓库的结果。""" return cls(status=LocalCandidateReadStatus.ABSENT) @classmethod def failure(cls, error: str) -> "LocalCandidateRead": """构造无法完成本地扫描的结果。""" return cls(status=LocalCandidateReadStatus.FAILED, error=error) def public_dict(self) -> dict[str, Any]: """生成不泄漏本地路径的扫描投影。""" return { "status": self.status.value, "error": self.error, "candidates": [candidate.public_dict() for candidate in self.candidates], } @dataclass(frozen=True, slots=True) class CandidateInventory: """一次短生命周期市场快照,保留配置市场状态和全部候选。""" market_reads: tuple[MarketRead, ...] local_candidates: tuple[PluginLocalCandidate, ...] = () expected_markets: tuple[str, ...] | None = None expected_generations: tuple[str, ...] | None = None local_read: LocalCandidateRead | None = None def __post_init__(self) -> None: """冻结快照输入,避免后续市场刷新改变选择依据。""" market_reads = tuple(self.market_reads) local_candidates = tuple(self.local_candidates) expected_markets = ( tuple(_normalize_market(market) for market in self.expected_markets) if self.expected_markets is not None else None ) expected_generations = ( tuple(normalize_package_generation(generation) for generation in self.expected_generations) if self.expected_generations is not None else None ) local_read = self.local_read if local_read is None: local_read = ( LocalCandidateRead.present(local_candidates) if local_candidates else LocalCandidateRead.absent() ) if not isinstance(local_read, LocalCandidateRead): raise TypeError("候选清单的本地读取必须由 LocalCandidateRead 组成") if local_read.candidates != local_candidates: raise ValueError("候选清单的本地读取与本地候选必须一致") if any(not isinstance(read, MarketRead) for read in market_reads): raise TypeError("候选清单必须由 MarketRead 组成") if any(not isinstance(candidate, PluginLocalCandidate) for candidate in local_candidates): raise TypeError("本地候选清单必须由 PluginLocalCandidate 组成") read_keys = [(read.market, read.package_generation) for read in market_reads] if len(read_keys) != len(set(read_keys)): raise ValueError("候选清单不能重复记录同一个市场代际") if expected_markets is not None and len(expected_markets) != len(set(expected_markets)): raise ValueError("候选清单的预期市场不能重复") if expected_generations is not None: if not expected_generations or len(expected_generations) != len(set(expected_generations)): raise ValueError("候选清单的预期代际必须唯一且非空") object.__setattr__(self, "market_reads", market_reads) object.__setattr__(self, "local_candidates", local_candidates) object.__setattr__(self, "expected_markets", expected_markets) object.__setattr__(self, "expected_generations", expected_generations) object.__setattr__(self, "local_read", local_read) @property def configured_markets(self) -> tuple[str, ...]: """按配置顺序返回本轮预期读取的市场。""" if self.expected_markets is not None: return self.expected_markets return tuple(dict.fromkeys(read.market for read in self.market_reads)) def reads_for(self, market: str) -> tuple[MarketRead, ...]: """返回一个市场的全部代际读取事实。""" normalized_market = _normalize_market(market) return tuple(read for read in self.market_reads if read.market == normalized_market) def read_for(self, market: str, package_generation: str) -> MarketRead | None: """返回一个市场和代际的读取事实。""" normalized_generation = normalize_package_generation(package_generation) return next( ( read for read in self.reads_for(market) if read.package_generation == normalized_generation ), None, ) @property def complete(self) -> bool: """只有预期市场与代际均可证明已成功读取时才算完整。""" if not self.market_reads or not all(read.succeeded for read in self.market_reads): return False expected_markets = self.expected_markets expected_generations = self.expected_generations if (expected_markets is None) != (expected_generations is None): return False if expected_markets is None or expected_generations is None: return True expected = { (market, generation) for market in expected_markets for generation in expected_generations } actual = {(read.market, read.package_generation) for read in self.market_reads} return expected <= actual @property def can_use_for_tofu(self) -> bool: """判断快照是否足以证明唯一第三方来源。""" local_read = self.local_read return ( self.complete and local_read is not None and local_read.status is not LocalCandidateReadStatus.FAILED ) @property def online_candidates(self) -> tuple[PluginMarketCandidate, ...]: """按配置市场顺序返回全部在线候选,不按 ID 或版本去重。""" return tuple( candidate for read in self.market_reads if read.present_index for candidate in read.candidates ) def candidates_for(self, plugin_id: str) -> tuple[PluginMarketCandidate, ...]: """读取一个插件 ID 的全部在线候选。""" normalized_id = normalize_physical_plugin_id(plugin_id) return tuple( candidate for candidate in self.online_candidates if candidate.normalized_plugin_id == normalized_id ) def local_candidates_for(self, plugin_id: str) -> tuple[PluginLocalCandidate, ...]: """读取一个插件 ID 的全部本地候选。""" normalized_id = normalize_physical_plugin_id(plugin_id) return tuple( candidate for candidate in self.local_candidates if candidate.normalized_plugin_id == normalized_id ) def public_dict(self) -> dict[str, Any]: """生成完整库存的脱敏投影,不泄漏本地路径或原始 DTO。""" local_read = self.local_read if local_read is None: raise RuntimeError("候选清单缺少本地读取终态") return { "markets": [read.public_dict() for read in self.market_reads], "local_candidates": [candidate.public_dict() for candidate in self.local_candidates], "local_read": local_read.public_dict(), "complete": self.complete, } @dataclass(frozen=True, slots=True) class PluginSelection: """候选选择结果,冲突和不完整状态均不降级为静默空值。""" status: PluginSelectionStatus candidate: PluginMarketCandidate | PluginLocalCandidate | None = None conflict_source_keys: tuple[str, ...] = () reason: str = "" def __post_init__(self) -> None: """保证选择状态与载荷及冲突信息相互一致。""" status = PluginSelectionStatus(self.status) conflict_source_keys = tuple(sorted(set(self.conflict_source_keys))) if status is PluginSelectionStatus.SELECTED and self.candidate is None: raise ValueError("selected 结果必须携带候选") if status is not PluginSelectionStatus.SELECTED and self.candidate is not None: raise ValueError("未选中结果不能携带候选") if status is not PluginSelectionStatus.CONFLICT and conflict_source_keys: raise ValueError("只有 conflict 结果能携带冲突来源") object.__setattr__(self, "status", status) object.__setattr__(self, "conflict_source_keys", conflict_source_keys) @property def selected(self) -> bool: """判断是否已经选择出一个载荷候选。""" return self.status is PluginSelectionStatus.SELECTED def public_dict(self) -> dict[str, Any]: """生成安全的选择结果投影,不透传本地路径或原始 DTO。""" result: dict[str, Any] = { "status": self.status.value, "reason": self.reason, } if self.conflict_source_keys: result["conflict_source_keys"] = list(self.conflict_source_keys) if self.candidate is not None: result["candidate"] = self.candidate.public_dict() return result Candidate: TypeAlias = PluginMarketCandidate | PluginLocalCandidate def normalize_package_generation(package_generation: str) -> str: """校验并归一插件包代际。""" value = str(package_generation).strip().lower() if value not in PLUGIN_GENERATIONS: raise ValueError("插件包代际必须为 v1、v2 或 v3") return value def _select_local_candidate( inventory: CandidateInventory, *, plugin_id: str, normalized_id: str, generation_order: tuple[str, ...], local_candidates: Iterable[PluginLocalCandidate] | None, ) -> PluginSelection | None: """优先选择本地载荷;读取失败时阻止自动降级到在线来源。""" local = ( tuple(local_candidates) if local_candidates is not None else inventory.local_candidates_for(plugin_id) ) if any(not isinstance(candidate, PluginLocalCandidate) for candidate in local): raise TypeError("本地候选必须是 PluginLocalCandidate") if any(candidate.normalized_plugin_id != normalized_id for candidate in local): raise ValueError("本地候选的插件 ID 必须与选择目标一致") local_read = inventory.local_read if ( not local and local_read is not None and local_read.status is LocalCandidateReadStatus.FAILED ): return PluginSelection( status=PluginSelectionStatus.INCOMPLETE, reason="本地插件仓库读取失败,不能自动选择在线载荷", ) if not local: return None selected_local = _select_best(local, generation_order) if selected_local is None: return PluginSelection( status=PluginSelectionStatus.UNAVAILABLE, reason="本地候选没有符合当前运行代际的版本", ) return PluginSelection( status=PluginSelectionStatus.SELECTED, candidate=selected_local, reason="优先使用本地载荷", ) def select_plugin_candidate( inventory: CandidateInventory, *, plugin_id: str, generations: Sequence[str], identity: PluginIdentity | None = None, local_candidates: Iterable[PluginLocalCandidate] | None = None, requested_source_key: str | None = None, explicit_source: bool = False, allow_source_change: bool = False, ) -> PluginSelection: """ 按允许来源、运行代际和同源版本选择一个插件载荷。 :param inventory: 本轮市场读取快照 :param plugin_id: 要选择的物理插件 ID :param generations: 调用方按优先级传入的代际顺序 :param identity: 已安装插件来源身份;为空表示未安装 :param local_candidates: 可选的本地载荷候选,优先于在线候选 :param requested_source_key: 调用方提供的规范在线来源;非显式调用不能绕过本地载荷 :param explicit_source: 本次调用是否代表管理员明确选源 :param allow_source_change: 是否是带 revision 的显式换源命令 :return: 带明确冲突或不完整状态的选择结果 """ normalized_id = normalize_physical_plugin_id(plugin_id) generation_order = _normalize_generation_order(generations) requested_source = ( validate_online_source_key(requested_source_key) if requested_source_key is not None else None ) if requested_source is None or not (explicit_source or allow_source_change): local_selection = _select_local_candidate( inventory, plugin_id=plugin_id, normalized_id=normalized_id, generation_order=generation_order, local_candidates=local_candidates, ) if local_selection is not None: return local_selection online = inventory.candidates_for(plugin_id) if not online: return PluginSelection( status=PluginSelectionStatus.UNAVAILABLE, reason=f"没有找到插件 {plugin_id} 的在线候选", ) allowed_source = _allowed_source(identity, normalized_id) if requested_source is not None: requested_online = tuple( candidate for candidate in online if candidate.source_key == requested_source ) if not requested_online: return PluginSelection( status=PluginSelectionStatus.UNAVAILABLE, reason="明确选择的在线来源没有当前插件候选", ) if allowed_source is not None: _source_type, allowed_key = allowed_source if requested_source != allowed_key and not allow_source_change: return PluginSelection( status=PluginSelectionStatus.CONFLICT, conflict_source_keys=(allowed_key, requested_source), reason="普通安装不能改变已绑定的在线来源", ) if explicit_source or allow_source_change: selected_requested = _select_best(requested_online, generation_order) if selected_requested is None: return PluginSelection( status=PluginSelectionStatus.UNAVAILABLE, reason="明确选择的来源没有符合当前运行代际的版本", ) return PluginSelection( status=PluginSelectionStatus.SELECTED, candidate=selected_requested, reason=( "按显式换源目标选择在线载荷" if allow_source_change else "按管理员明确选择的来源安装在线载荷" ), ) 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="在线候选没有符合当前运行代际的版本", ) return PluginSelection( status=PluginSelectionStatus.SELECTED, candidate=selected_online, reason="按已绑定来源选择在线载荷", ) if identity is not None: return PluginSelection( status=PluginSelectionStatus.INCOMPLETE, reason="插件来源身份尚未绑定,不能自动选择在线载荷", ) source_pairs = {(candidate.source_type, candidate.source_key) for candidate in online} if len(source_pairs) > 1: return PluginSelection( status=PluginSelectionStatus.CONFLICT, conflict_source_keys=tuple(source_key for _source_type, source_key in source_pairs), reason="未安装插件存在多个在线来源,不能静默选择", ) source_type = next(iter(source_pairs))[0] if source_type is TrustedPluginSourceType.THIRD_PARTY and not inventory.can_use_for_tofu: return PluginSelection( status=PluginSelectionStatus.INCOMPLETE, reason="市场读取不完整,不能建立唯一第三方来源的 TOFU", ) selected_online = _select_best(online, generation_order) if selected_online is None: return PluginSelection( status=PluginSelectionStatus.UNAVAILABLE, reason="在线候选没有符合当前运行代际的版本", ) return PluginSelection( status=PluginSelectionStatus.SELECTED, candidate=selected_online, reason="唯一在线来源候选", ) def list_effective_online_candidates( inventory: CandidateInventory, *, plugin_id: str, generations: Sequence[str], ) -> tuple[PluginMarketCandidate, ...]: """按来源列出当前运行代际实际可安装的最高版本候选,官方来源始终置顶。""" generation_order = _normalize_generation_order(generations) grouped: dict[ tuple[TrustedPluginSourceType, str], list[PluginMarketCandidate], ] = {} for candidate in inventory.candidates_for(plugin_id): grouped.setdefault( (candidate.source_type, candidate.source_key), [], ).append(candidate) selected: list[PluginMarketCandidate] = [] for candidates in grouped.values(): selected_candidate = _select_best(candidates, generation_order) if isinstance(selected_candidate, PluginMarketCandidate): selected.append(selected_candidate) selected.sort( key=lambda candidate: candidate.source_type is not TrustedPluginSourceType.OFFICIAL ) return tuple(selected) def get_effective_local_candidate( inventory: CandidateInventory, *, plugin_id: str, generations: Sequence[str], ) -> PluginLocalCandidate | None: """返回本地插件目录中当前运行代际优先级最高的安全候选。""" candidate = _select_best( inventory.local_candidates_for(plugin_id), _normalize_generation_order(generations), ) return candidate if isinstance(candidate, PluginLocalCandidate) else None def parse_local_plugin_reference(repo_url: str) -> str | None: """从不透明本地来源标识中提取插件 ID,不读取或暴露宿主路径。""" if not str(repo_url).startswith("local://"): return None try: parsed = urlsplit(repo_url) plugin_id = unquote(parsed.netloc or parsed.path.strip("/")) except (TypeError, ValueError): return None return plugin_id or None def _coerce_online_source_type(source_type: TrustedPluginSourceType) -> TrustedPluginSourceType: """把外部字符串来源类型转换为可信在线来源枚举。""" value = TrustedPluginSourceType(source_type) if value is TrustedPluginSourceType.UNKNOWN: raise ValueError("未知来源不能作为在线市场候选") return value def _normalize_market(market: str) -> str: """校验市场标识,保留其作为本轮快照的显示值。""" value = str(market).strip() if not value: raise ValueError("插件市场标识不能为空") return value def _normalize_repo_url(repo_url: str) -> str: """保留仓库地址作为安装事实,但移除无意义的外围空白。""" value = str(repo_url).strip() if not value: raise ValueError("插件仓库地址不能为空") return value def _normalize_plugin_version(plugin_version: str | None) -> str | None: """标准化可选插件声明版本,并保持缺失版本可观察。""" if plugin_version is None: return None value = str(plugin_version).strip() if not value: raise ValueError("插件声明版本不能为空字符串") if len(value) > 64: raise ValueError("插件声明版本长度不能超过 64") return value def _normalize_generation_order(generations: Sequence[str]) -> tuple[str, ...]: """校验调用方提供的代际优先序,并拒绝重复项。""" normalized = tuple(normalize_package_generation(generation) for generation in generations) if not normalized: raise ValueError("至少需要一个当前运行代际") if len(normalized) != len(set(normalized)): raise ValueError("当前运行代际优先序不能重复") return normalized def _allowed_source( identity: PluginIdentity | None, normalized_plugin_id: str, ) -> tuple[TrustedPluginSourceType, str] | None: """从已安装身份读取不可变的允许在线来源。""" if identity is None: return None if identity.normalized_plugin_id != normalized_plugin_id: raise ValueError("来源身份的插件 ID 与选择目标不一致") if identity.trusted_source_type is TrustedPluginSourceType.UNKNOWN: return None if not identity.trusted_source_key: raise PluginSourceSelectionError("已绑定来源身份缺少规范来源键") return identity.trusted_source_type, identity.trusted_source_key def _select_best( candidates: Sequence[Candidate], generation_order: Sequence[str], ) -> Candidate | None: """在已完成来源过滤后按代际和同源版本选择最高候选。""" for generation in generation_order: generation_candidates = tuple( candidate for candidate in candidates if candidate.package_generation == generation ) if generation_candidates: return _select_highest_version(generation_candidates) return None def _select_highest_version(candidates: Sequence[Candidate]) -> Candidate: """使用宿主既有版本比较语义选择同源最高版本,平级保留先读候选。""" selected = candidates[0] for candidate in candidates[1:]: selected_version = selected.plugin_version or "0" candidate_version = candidate.plugin_version or "0" if compare_version(candidate_version, ">", selected_version): selected = candidate return selected