feat(plugin): persist declared metadata snapshots (#6467)

This commit is contained in:
InfinityPacer
2026-08-26 13:15:12 +08:00
committed by GitHub
parent bd0795082a
commit ac2a2b1129
26 changed files with 1374 additions and 147 deletions
+43 -22
View File
@@ -28,6 +28,7 @@ from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.commands import init_commands
from app.application.configuration import get_api_runtime_config_snapshot, get_configured_system_config
from app.application.plugin.catalog import apply_declared_metadata_fallback
from app.application.plugin.config import PluginConfigCommand
from app.application.plugin.folders import remove_plugin_from_folders
from app.application.plugin.gateway import get_plugin_install_service
@@ -234,14 +235,21 @@ async def _get_plugin_history_detail(
installed_plugin = next(
(
plugin
for plugin in plugin_manager.get_local_plugins()
if plugin.id == plugin_id and plugin.installed
for plugin in plugin_manager.get_installed_plugins()
if plugin.id == plugin_id
),
None,
)
if not installed_plugin:
return None
identity = await get_plugin_persistence().get_identity(plugin_id)
if identity is not None:
installed_plugin = apply_declared_metadata_fallback(
[installed_plugin],
{identity.normalized_plugin_id: identity},
)[0]
local_repo_plugin = next(
(plugin for plugin in plugin_manager.get_local_repo_plugins() if plugin.id == plugin_id),
None,
@@ -249,27 +257,35 @@ async def _get_plugin_history_detail(
if local_repo_plugin:
return _merge_plugin_market_metadata(installed_plugin, local_repo_plugin)
if installed_plugin.repo_url:
trusted_repo_url = None
if identity is not None and identity.trusted_source_key:
owner_repo = identity.trusted_source_key.removeprefix("github:")
trusted_repo_url = f"https://github.com/{owner_repo}"
if trusted_repo_url:
market_plugin = await _get_market_plugin_from_repo(
plugin_manager, plugin_id, installed_plugin.repo_url, force
plugin_manager, plugin_id, trusted_repo_url, force
)
if not market_plugin:
logger.debug(f"插件 {plugin_id} 未从来源仓库获取到更新说明,返回本地插件信息")
return installed_plugin
return _merge_plugin_market_metadata(installed_plugin, market_plugin)
market_plugin = next(
(
plugin
for plugin in await plugin_manager.async_get_online_plugins(force=force)
if plugin.id == plugin_id
),
None,
)
if not market_plugin:
return installed_plugin
return installed_plugin
return _merge_plugin_market_metadata(installed_plugin, market_plugin)
async def _installed_plugins_with_declared_metadata(
plugin_manager: PluginRuntime,
) -> list[_SchemaPlugin]:
"""批量读取身份,并为未加载插件补齐已提交的展示声明。"""
plugins = plugin_manager.get_installed_plugins()
identities = await get_plugin_persistence().list_identities(
[plugin.id for plugin in plugins if plugin.id]
)
return apply_declared_metadata_fallback(
plugins,
{identity.normalized_plugin_id: identity for identity in identities},
)
@router.get("/", summary="所有插件", response_model=List[_SchemaPlugin])
@@ -281,14 +297,13 @@ async def all_plugins(
"""
查询所有插件清单,包括本地插件和在线插件,插件状态:installed, market, all
"""
# 本地插件
plugin_manager = get_plugin_manager()
local_plugins = plugin_manager.get_local_plugins()
# 已安装插件
installed_plugins = [plugin for plugin in local_plugins if plugin.installed]
installed_plugins = plugin_manager.get_installed_plugins()
if state == "installed":
return plugin_manager.get_installed_plugins()
return await _installed_plugins_with_declared_metadata(plugin_manager)
# 本地插件
local_plugins = plugin_manager.get_local_plugins()
# 未安装的本地插件
not_installed_plugins = [plugin for plugin in local_plugins if not plugin.installed]
# 本地插件仓库目录中的插件
@@ -305,7 +320,10 @@ async def all_plugins(
if state == "market":
# 返回未安装的本地插件
return not_installed_plugins
return local_plugins
return (
await _installed_plugins_with_declared_metadata(plugin_manager)
+ not_installed_plugins
)
# 插件市场插件清单
market_plugins = []
@@ -328,7 +346,10 @@ async def all_plugins(
return market_plugins
# 返回所有插件
return installed_plugins + market_plugins
return (
await _installed_plugins_with_declared_metadata(plugin_manager)
+ market_plugins
)
@router.get("/installed", summary="已安装插件", response_model=List[str])
+8 -8
View File
@@ -6,6 +6,7 @@ from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
from app.application.plugin.declaration import PluginDeclaredMetadata
from app.application.plugin.identity import (
PluginBindingBasis,
PluginIdentity,
@@ -60,14 +61,13 @@ class PluginInstallAdmission:
payload_receipt: str,
applied_at: datetime,
declared_version: str | None = None,
manifest_matches_payload: bool = True,
) -> PluginIdentity:
"""在载荷落盘并生成收据后构造唯一数据库提交目标。"""
current = self.identity_before
plugin_id = current.plugin_id if current else self.candidate.plugin_id
metadata = self.candidate.dto if isinstance(self.candidate.dto, Mapping) else {}
system_version = metadata.get("system_version")
supports_v3 = metadata.get("v3")
supports_v3t = metadata.get("v3t")
installed_version = declared_version or self.candidate.plugin_version
source_binding_changed = (
self.trusted_source_type is not TrustedPluginSourceType.UNKNOWN
and (
@@ -89,13 +89,13 @@ class PluginInstallAdmission:
if isinstance(self.candidate, PluginLocalCandidate)
else self.candidate.source_key
),
declared_version=declared_version or self.candidate.plugin_version,
declared_version=installed_version,
package_generation=self.candidate.package_generation,
system_version=(
system_version if isinstance(system_version, str) else None
declared_metadata=PluginDeclaredMetadata.from_package(
metadata,
declaration_version=self.candidate.plugin_version,
manifest_matches_payload=manifest_matches_payload,
),
supports_v3=supports_v3 if isinstance(supports_v3, bool) else None,
supports_v3t=supports_v3t if isinstance(supports_v3t, bool) else None,
payload_receipt=payload_receipt,
revision=(current.revision + 1) if current else 1,
created_at=current.created_at if current else applied_at,
+41 -1
View File
@@ -4,9 +4,11 @@ from __future__ import annotations
import asyncio
import concurrent.futures
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Mapping, Sequence
from typing import Any, Optional
from app.application.plugin.identity import PluginIdentity
from app.schemas.plugin import Plugin
MarketLoader = Callable[[str, Optional[str], bool], Optional[dict[str, dict]]]
AsyncMarketLoader = Callable[
@@ -17,6 +19,44 @@ PluginMapper = Callable[[str, dict, str, list[str], int, Optional[str]], Any]
ProgressCallback = Callable[..., Any]
def apply_declared_metadata_fallback(
plugins: Sequence[Plugin],
identities: Mapping[str, PluginIdentity],
) -> list[Plugin]:
"""用已提交快照补齐加载失败插件,不覆盖真实运行态字段。"""
result: list[Plugin] = []
for plugin in plugins:
identity = identities.get((plugin.id or "").lower())
if (
identity is None
or identity.declared_metadata is None
or identity.declared_version is None
):
result.append(plugin)
continue
fallback = identity.declared_metadata.display_fallback(
installed_version=identity.declared_version
)
updates: dict[str, str] = {}
if not plugin.plugin_version:
updates["plugin_version"] = fallback["plugin_version"]
if (
(not plugin.plugin_name or plugin.plugin_name == plugin.id)
and "plugin_name" in fallback
):
updates["plugin_name"] = fallback["plugin_name"]
if not plugin.plugin_desc and "plugin_desc" in fallback:
updates["plugin_desc"] = fallback["plugin_desc"]
if not plugin.plugin_icon and "plugin_icon" in fallback:
updates["plugin_icon"] = fallback["plugin_icon"]
if not plugin.plugin_author and "plugin_author" in fallback:
updates["plugin_author"] = fallback["plugin_author"]
if not plugin.plugin_label and "plugin_label" in fallback:
updates["plugin_label"] = fallback["plugin_label"]
result.append(plugin.model_copy(update=updates) if updates else plugin)
return result
class PluginCatalogService:
"""负责插件市场索引映射、并发收集、代际合并和来源去重。"""
+207
View File
@@ -0,0 +1,207 @@
"""已提交插件载荷的版本化 package 声明快照。"""
from __future__ import annotations
import json
import re
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Any
_SCHEMA_VERSION = 1
_RUNTIME_FIELD_PATTERN = re.compile(r"^v[1-9][0-9]*t?$")
_MANIFEST_TEXT_FIELDS = (
"name",
"description",
"icon",
"author",
"system_version",
)
def _optional_text(
value: object,
*,
max_length: int | None = None,
) -> str | None:
"""把外部可选文本规范为非空字符串。"""
if not isinstance(value, str):
return None
normalized = value.strip()
if max_length is not None and len(normalized) > max_length:
raise ValueError(f"插件声明文本长度不能超过 {max_length}")
return normalized or None
def _labels(value: object) -> list[str]:
"""把历史逗号字符串和字符串数组统一为有序标签列表。"""
if isinstance(value, str):
items = value.split(",")
elif isinstance(value, list):
items = value
else:
return []
return [
normalized
for item in items
if isinstance(item, str) and (normalized := item.strip())
]
def _normalize_manifest(value: object) -> dict[str, object]:
"""仅保留当前载荷展示和声明消费者需要的 package 字段。"""
if not isinstance(value, Mapping):
return {}
manifest: dict[str, object] = {}
for field_name in _MANIFEST_TEXT_FIELDS:
if normalized := _optional_text(
value.get(field_name),
max_length=128 if field_name == "system_version" else None,
):
manifest[field_name] = normalized
labels = _labels(value.get("labels"))
if labels:
manifest["labels"] = labels
level = value.get("level")
if isinstance(level, int) and not isinstance(level, bool):
manifest["level"] = level
release = value.get("release")
if isinstance(release, bool):
manifest["release"] = release
return manifest
def _normalize_runtime(value: object) -> dict[str, bool]:
"""保留可向后扩展的宿主代际和运行时变体布尔声明。"""
if not isinstance(value, Mapping):
return {}
return {
str(field_name): field_value
for field_name, field_value in value.items()
if (
isinstance(field_name, str)
and _RUNTIME_FIELD_PATTERN.fullmatch(field_name)
and isinstance(field_value, bool)
)
}
def _encode(value: Mapping[str, object]) -> str:
"""生成不可从外部原地修改的稳定内部表示。"""
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
@dataclass(frozen=True, slots=True)
class PluginDeclaredMetadata:
"""封装一份受限、版本化且不参与来源授信的 package 声明。"""
_encoded: str = field(repr=False)
def __post_init__(self) -> None:
"""拒绝绕过工厂构造的非规范内部状态。"""
normalized = self._normalize_storage(json.loads(self._encoded))
object.__setattr__(self, "_encoded", _encode(normalized))
@classmethod
def from_package(
cls,
package: Mapping[str, Any],
*,
declaration_version: str | None,
manifest_matches_payload: bool,
) -> "PluginDeclaredMetadata":
"""从安装候选生成与载荷一起提交的受限声明快照。"""
runtime = _normalize_runtime(package)
value = {
"schema_version": _SCHEMA_VERSION,
"declaration_version": _optional_text(declaration_version),
"manifest_matches_payload": manifest_matches_payload,
"manifest": _normalize_manifest(package),
"runtime": runtime,
}
return cls(_encode(value))
@classmethod
def from_storage(cls, value: object) -> "PluginDeclaredMetadata":
"""校验数据库 JSON,并忽略当前合同未消费的额外字段。"""
return cls(_encode(cls._normalize_storage(value)))
@staticmethod
def _normalize_storage(value: object) -> dict[str, object]:
"""验证快照结构,同时宽容丢弃非法可选展示字段。"""
if not isinstance(value, Mapping):
raise ValueError("插件声明快照必须是 JSON 对象")
if value.get("schema_version") != _SCHEMA_VERSION:
raise ValueError("插件声明快照 schema_version 不受支持")
manifest_matches_payload = value.get("manifest_matches_payload")
if not isinstance(manifest_matches_payload, bool):
raise ValueError("插件声明快照必须说明 manifest 是否对应当前载荷")
return {
"schema_version": _SCHEMA_VERSION,
"declaration_version": _optional_text(
value.get("declaration_version")
),
"manifest_matches_payload": manifest_matches_payload,
"manifest": _normalize_manifest(value.get("manifest")),
"runtime": _normalize_runtime(value.get("runtime")),
}
@property
def declaration_version(self) -> str | None:
"""返回生成该快照的 package 条目版本。"""
return _optional_text(self.to_json().get("declaration_version"))
@property
def manifest_matches_payload(self) -> bool:
"""说明该 package 声明是否与当前已提交载荷对应。"""
return bool(self.to_json()["manifest_matches_payload"])
def runtime_support(self, runtime_name: str) -> bool | None:
"""读取一个规范运行时声明;缺省保持未声明语义。"""
if not _RUNTIME_FIELD_PATTERN.fullmatch(runtime_name):
raise ValueError("运行时声明必须使用 v<数字> 或 v<数字>t")
runtime = self.to_json().get("runtime")
if not isinstance(runtime, dict):
return None
value = runtime.get(runtime_name)
return value if isinstance(value, bool) else None
def display_fallback(self, *, installed_version: str) -> dict[str, str]:
"""生成插件加载失败时可安全补齐的展示字段。"""
manifest = self.to_json().get("manifest")
fallback = {
"plugin_version": installed_version,
}
if not isinstance(manifest, dict):
return fallback
display_fields = {
"name": "plugin_name",
"description": "plugin_desc",
"icon": "plugin_icon",
"author": "plugin_author",
}
for source_name, target_name in display_fields.items():
value = manifest.get(source_name)
if isinstance(value, str):
fallback[target_name] = value
labels = manifest.get("labels")
if isinstance(labels, list):
label = ",".join(item for item in labels if isinstance(item, str))
if label:
fallback["plugin_label"] = label
return fallback
def to_json(self) -> dict[str, object]:
"""返回可持久化且与内部状态相互隔离的 JSON 副本。"""
value: object = json.loads(self._encoded)
if not isinstance(value, dict):
raise ValueError("插件声明快照内部状态必须是 JSON 对象")
return {
str(field_name): field_value
for field_name, field_value in value.items()
}
+11 -15
View File
@@ -3,12 +3,14 @@
from __future__ import annotations
import re
from collections.abc import Callable
from collections.abc import Callable, Sequence
from dataclasses import dataclass, replace
from datetime import datetime
from enum import StrEnum
from typing import Protocol
from app.application.plugin.declaration import PluginDeclaredMetadata
_PLUGIN_ID_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9]{0,127}$")
_ONLINE_SOURCE_KEY_PATTERN = re.compile(
r"^github:[a-z0-9](?:[a-z0-9-]{0,38})/"
@@ -116,9 +118,7 @@ class PluginIdentity:
payload_source_key: str | None
declared_version: str | None
package_generation: str | None
system_version: str | None
supports_v3: bool | None
supports_v3t: bool | None
declared_metadata: PluginDeclaredMetadata | None
payload_receipt: str | None
revision: int
created_at: datetime
@@ -200,9 +200,7 @@ class PluginIdentity:
if any((
self.declared_version,
self.package_generation,
self.system_version,
self.supports_v3 is not None,
self.supports_v3t is not None,
self.declared_metadata,
self.payload_receipt,
self.payload_applied_at,
)):
@@ -210,6 +208,8 @@ class PluginIdentity:
else:
if not self.declared_version or not self.package_generation:
raise ValueError("已知载荷必须携带声明版本和包代际")
if self.declared_metadata is None:
raise ValueError("已知载荷必须携带已提交 package 声明快照")
if self.payload_applied_at is None or self.payload_receipt is None:
raise ValueError("已知载荷必须携带应用时间和内容收据")
if (
@@ -234,11 +234,6 @@ class PluginIdentity:
field_name="插件声明版本",
max_length=64,
)
_validate_optional_text(
self.system_version,
field_name="插件系统版本要求",
max_length=128,
)
object.__setattr__(self, "trusted_source_key", trusted_key)
object.__setattr__(self, "payload_source_key", payload_key)
object.__setattr__(self, "payload_receipt", _validate_receipt(self.payload_receipt))
@@ -320,9 +315,7 @@ def plan_legacy_plugin_identity(
payload_source_key=None,
declared_version=None,
package_generation=None,
system_version=None,
supports_v3=None,
supports_v3t=None,
declared_metadata=None,
payload_receipt=None,
revision=1,
created_at=now,
@@ -356,6 +349,9 @@ class PluginIdentityStore(Protocol):
def get(self, plugin_id: str) -> PluginIdentity | None:
"""读取一个物理插件的来源身份。"""
def list(self, plugin_ids: Sequence[str]) -> list[PluginIdentity]:
"""批量读取指定物理插件的来源身份。"""
def compare_and_set(
self,
identity: PluginIdentity,
+4
View File
@@ -356,6 +356,10 @@ class PluginInstallCommand:
payload_receipt=receipt,
applied_at=self.__clock(),
declared_version=release_version,
manifest_matches_payload=(
release_version is None
or release_version == candidate.plugin_version
),
)
await self.__await_side_effect(
self.__persistence.set_installation_target(
+13
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import re
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
@@ -166,6 +167,9 @@ class PluginIdentityPersistence(Protocol):
def get(self, plugin_id: str) -> PluginIdentity | None:
"""读取一个物理插件的来源身份。"""
def list(self, plugin_ids: Sequence[str]) -> list[PluginIdentity]:
"""批量读取指定物理插件的来源身份。"""
def compare_and_set(
self,
identity: PluginIdentity,
@@ -202,6 +206,15 @@ class PluginPersistenceService:
"""在数据库 worker 中读取插件来源身份。"""
return await self.__executor.run(partial(self.__identities.get, plugin_id))
async def list_identities(
self,
plugin_ids: Sequence[str],
) -> list[PluginIdentity]:
"""在一次数据库任务中批量读取插件来源身份。"""
return await self.__executor.run(
partial(self.__identities.list, tuple(plugin_ids))
)
async def migrate_identity(
self,
identity: PluginIdentity,
+33 -7
View File
@@ -1,11 +1,12 @@
"""插件来源身份 Application Port 的 SQLAlchemy 实现。"""
from collections.abc import Callable
from collections.abc import Callable, Sequence
from datetime import datetime
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.application.plugin.declaration import PluginDeclaredMetadata
from app.application.plugin.identity import (
BindLocalPluginIdentityCommand,
BindOnlinePluginIdentityCommand,
@@ -40,9 +41,11 @@ def _to_record(model: IdentityModel) -> PluginIdentity:
payload_source_key=model.payload_source_key,
declared_version=model.declared_version,
package_generation=model.package_generation,
system_version=model.system_version,
supports_v3=model.supports_v3,
supports_v3t=model.supports_v3t,
declared_metadata=(
PluginDeclaredMetadata.from_storage(model.declared_metadata)
if model.declared_metadata is not None
else None
),
payload_receipt=model.payload_receipt,
revision=model.revision,
created_at=datetime.fromisoformat(model.created_at),
@@ -64,9 +67,11 @@ def _to_model(identity: PluginIdentity) -> IdentityModel:
payload_source_key=identity.payload_source_key,
declared_version=identity.declared_version,
package_generation=identity.package_generation,
system_version=identity.system_version,
supports_v3=identity.supports_v3,
supports_v3t=identity.supports_v3t,
declared_metadata=(
identity.declared_metadata.to_json()
if identity.declared_metadata is not None
else None
),
payload_receipt=identity.payload_receipt,
revision=identity.revision,
created_at=identity.created_at.isoformat(),
@@ -92,6 +97,13 @@ class _SqlAlchemyIdentityRepository:
model = self._oper.get_by_plugin_id(plugin_id)
return _to_record(model) if model else None
def list(self, plugin_ids: Sequence[str]) -> list[PluginIdentity]:
"""批量读取并映射指定来源身份。"""
return [
_to_record(model)
for model in self._oper.list_by_plugin_ids(plugin_ids)
]
def stage_create(self, identity: PluginIdentity) -> None:
"""暂存首次身份。"""
try:
@@ -131,6 +143,20 @@ class TransactionalPluginIdentityStore:
finally:
session.close()
def list(self, plugin_ids: Sequence[str]) -> list[PluginIdentity]:
"""在一个短会话内批量读取规范化插件身份。"""
normalized_ids = tuple(
dict.fromkeys(
normalize_physical_plugin_id(plugin_id)
for plugin_id in plugin_ids
)
)
session = self._session_factory()
try:
return _SqlAlchemyIdentityRepository(session).list(normalized_ids)
finally:
session.close()
def compare_and_set(
self,
identity: PluginIdentity,
+11 -6
View File
@@ -10,6 +10,7 @@ from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.application.plugin.declaration import PluginDeclaredMetadata
from app.application.plugin.identity import (
PluginBindingBasis,
PluginIdentity,
@@ -50,9 +51,11 @@ def _identity_from_model(model: IdentityModel) -> PluginIdentity:
payload_source_key=model.payload_source_key,
declared_version=model.declared_version,
package_generation=model.package_generation,
system_version=model.system_version,
supports_v3=model.supports_v3,
supports_v3t=model.supports_v3t,
declared_metadata=(
PluginDeclaredMetadata.from_storage(model.declared_metadata)
if model.declared_metadata is not None
else None
),
payload_receipt=model.payload_receipt,
revision=model.revision,
created_at=datetime.fromisoformat(model.created_at),
@@ -82,9 +85,11 @@ def _identity_model_values(identity: PluginIdentity) -> dict[str, object]:
"payload_source_key": identity.payload_source_key,
"declared_version": identity.declared_version,
"package_generation": identity.package_generation,
"system_version": identity.system_version,
"supports_v3": identity.supports_v3,
"supports_v3t": identity.supports_v3t,
"declared_metadata": (
identity.declared_metadata.to_json()
if identity.declared_metadata is not None
else None
),
"payload_receipt": identity.payload_receipt,
"revision": identity.revision,
"created_at": identity.created_at.isoformat(),
+5 -5
View File
@@ -1,8 +1,8 @@
"""已安装物理插件来源身份模型。"""
from typing import Optional
from typing import Any, Optional
from sqlalchemy import Boolean, CheckConstraint, Integer, String, UniqueConstraint
from sqlalchemy import JSON, CheckConstraint, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, get_id_column
@@ -21,9 +21,9 @@ class PluginIdentity(Base):
payload_source_key: Mapped[Optional[str]] = mapped_column(String(255))
declared_version: Mapped[Optional[str]] = mapped_column(String(64))
package_generation: Mapped[Optional[str]] = mapped_column(String(8))
system_version: Mapped[Optional[str]] = mapped_column(String(128))
supports_v3: Mapped[Optional[bool]] = mapped_column(Boolean)
supports_v3t: Mapped[Optional[bool]] = mapped_column(Boolean)
declared_metadata: Mapped[Optional[dict[str, Any]]] = mapped_column(
JSON(none_as_null=True)
)
payload_receipt: Mapped[Optional[str]] = mapped_column(String(71))
revision: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
created_at: Mapped[str] = mapped_column(String(40), nullable=False)
+18
View File
@@ -1,5 +1,6 @@
"""插件来源身份的数据访问原语。"""
from collections.abc import Sequence
from typing import cast
from sqlalchemy import select, update
@@ -25,6 +26,23 @@ class PluginIdentityOper(DbOper):
)
)
def list_by_plugin_ids(
self,
plugin_ids: Sequence[str],
) -> list[PluginIdentity]:
"""批量读取规范化物理插件 ID 对应的身份。"""
if not plugin_ids:
return []
return list(
self._execute_sync_query(
lambda session: session.execute(
select(PluginIdentity).where(
PluginIdentity.normalized_plugin_id.in_(plugin_ids)
)
).scalars()
)
)
def stage_create(self, identity: PluginIdentity) -> None:
"""暂存首次身份并立即暴露数据库唯一键竞争。"""
def stage(session: Session) -> None: