feat(plugin): add source identity foundation (#6454)

This commit is contained in:
InfinityPacer
2026-08-25 19:58:56 +08:00
committed by GitHub
parent e7f5a92cdb
commit f28e8b1538
11 changed files with 1427 additions and 4 deletions
+145
View File
@@ -0,0 +1,145 @@
"""插件来源身份 Application Port 的 SQLAlchemy 实现。"""
from collections.abc import Callable
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from app.application.plugin.identity import (
PluginBindingBasis,
PluginPayloadSourceType,
PluginIdentity,
PluginIdentityConflictError,
TrustedPluginSourceType,
WritePluginIdentityCommand,
normalize_physical_plugin_id,
)
from app.db.models.pluginidentity import PluginIdentity as IdentityModel
from app.db.oper.pluginidentity import PluginIdentityOper
from app.db.uow import SqlAlchemyUnitOfWork
def _parse_datetime(value: str | None) -> datetime | None:
"""把数据库 ISO 时间还原为带时区应用值。"""
return datetime.fromisoformat(value) if value else None
def _to_record(model: IdentityModel) -> PluginIdentity:
"""把持久化模型映射为已校验的应用身份。"""
return PluginIdentity(
plugin_id=model.plugin_id,
normalized_plugin_id=model.normalized_plugin_id,
trusted_source_type=TrustedPluginSourceType(model.trusted_source_type),
trusted_source_key=model.trusted_source_key,
binding_basis=PluginBindingBasis(model.binding_basis),
payload_source_type=PluginPayloadSourceType(model.payload_source_type),
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,
payload_receipt=model.payload_receipt,
revision=model.revision,
created_at=datetime.fromisoformat(model.created_at),
updated_at=datetime.fromisoformat(model.updated_at),
bound_at=_parse_datetime(model.bound_at),
payload_applied_at=_parse_datetime(model.payload_applied_at),
)
def _to_model(identity: PluginIdentity) -> IdentityModel:
"""把应用身份映射为不拥有事务的持久化模型。"""
return IdentityModel(
plugin_id=identity.plugin_id,
normalized_plugin_id=identity.normalized_plugin_id,
trusted_source_type=identity.trusted_source_type.value,
trusted_source_key=identity.trusted_source_key,
binding_basis=identity.binding_basis.value,
payload_source_type=identity.payload_source_type.value,
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,
payload_receipt=identity.payload_receipt,
revision=identity.revision,
created_at=identity.created_at.isoformat(),
updated_at=identity.updated_at.isoformat(),
bound_at=identity.bound_at.isoformat() if identity.bound_at else None,
payload_applied_at=(
identity.payload_applied_at.isoformat()
if identity.payload_applied_at
else None
),
)
class _SqlAlchemyIdentityRepository:
"""绑定一个调用方 Session 的来源身份仓储。"""
def __init__(self, session: Session) -> None:
"""保存由事务适配器拥有的 Session。"""
self._oper = PluginIdentityOper(session)
def get(self, plugin_id: str) -> PluginIdentity | None:
"""读取并映射指定来源身份。"""
model = self._oper.get_by_plugin_id(plugin_id)
return _to_record(model) if model else None
def stage_create(self, identity: PluginIdentity) -> None:
"""暂存首次身份。"""
try:
self._oper.stage_create(_to_model(identity))
except IntegrityError as error:
raise PluginIdentityConflictError(
f"插件 {identity.plugin_id} 的来源身份已存在"
) from error
def stage_replace(
self,
identity: PluginIdentity,
*,
expected_revision: int,
) -> bool:
"""按 revision 条件暂存替换。"""
return self._oper.stage_replace(
_to_model(identity),
expected_revision=expected_revision,
)
class TransactionalPluginIdentityStore:
"""为每次来源身份读写创建独占同步数据库会话。"""
def __init__(self, session_factory: Callable[[], Session]) -> None:
"""保存由组合根提供的同步 Session 工厂。"""
self._session_factory = session_factory
def get(self, plugin_id: str) -> PluginIdentity | None:
"""在短会话内读取指定物理插件身份。"""
session = self._session_factory()
try:
return _SqlAlchemyIdentityRepository(session).get(
normalize_physical_plugin_id(plugin_id)
)
finally:
session.close()
def compare_and_set(
self,
identity: PluginIdentity,
*,
expected_revision: int | None,
) -> PluginIdentity:
"""在一个事务内执行首次创建或 revision 条件替换。"""
session = self._session_factory()
try:
return WritePluginIdentityCommand(
repository=_SqlAlchemyIdentityRepository(session),
unit_of_work=SqlAlchemyUnitOfWork(session),
).execute(identity, expected_revision=expected_revision)
finally:
session.close()
+4
View File
@@ -18,6 +18,10 @@ _MODEL_EXPORTS = {
"OutboxMessage": ("app.db.models.outbox", "OutboxMessage"),
"PassKey": ("app.db.models.passkey", "PassKey"),
"PluginData": ("app.db.models.plugindata", "PluginData"),
"PluginIdentity": (
"app.db.models.pluginidentity",
"PluginIdentity",
),
"Site": ("app.db.models.site", "Site"),
"SiteIcon": ("app.db.models.siteicon", "SiteIcon"),
"SiteStatistic": ("app.db.models.sitestatistic", "SiteStatistic"),
+45
View File
@@ -0,0 +1,45 @@
"""已安装物理插件来源身份模型。"""
from typing import Optional
from sqlalchemy import Boolean, CheckConstraint, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, get_id_column
class PluginIdentity(Base):
"""持久化一份大小写无关、可条件更新的物理插件来源身份。"""
id = get_id_column()
plugin_id: Mapped[str] = mapped_column(String(128), nullable=False)
normalized_plugin_id: Mapped[str] = mapped_column(String(128), nullable=False)
trusted_source_type: Mapped[str] = mapped_column(String(20), nullable=False)
trusted_source_key: Mapped[Optional[str]] = mapped_column(String(255))
binding_basis: Mapped[str] = mapped_column(String(32), nullable=False)
payload_source_type: Mapped[str] = mapped_column(String(20), nullable=False)
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)
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)
updated_at: Mapped[str] = mapped_column(String(40), nullable=False)
bound_at: Mapped[Optional[str]] = mapped_column(String(40))
payload_applied_at: Mapped[Optional[str]] = mapped_column(String(40))
__table_args__ = (
UniqueConstraint(
"normalized_plugin_id",
name="uq_pluginidentity_normalized_plugin_id",
),
CheckConstraint(
"normalized_plugin_id <> '' "
"AND normalized_plugin_id = lower(normalized_plugin_id)",
name="ck_pluginidentity_normalized_plugin_id",
),
CheckConstraint("revision >= 1", name="ck_pluginidentity_revision"),
)
+58
View File
@@ -0,0 +1,58 @@
"""插件来源身份的数据访问原语。"""
from sqlalchemy import select, update
from sqlalchemy.orm import Session
from app.db.base import DbOper, execute_dml
from app.db.models.pluginidentity import PluginIdentity
class PluginIdentityOper(DbOper):
"""在调用方 Session 中查询并条件暂存插件来源身份。"""
def get_by_plugin_id(self, plugin_id: str) -> PluginIdentity | None:
"""按规范化物理插件 ID 查询唯一身份。"""
return self._execute_sync_query(
lambda session: session.execute(
select(PluginIdentity).where(
PluginIdentity.normalized_plugin_id == plugin_id
)
).scalar_one_or_none()
)
def stage_create(self, identity: PluginIdentity) -> None:
"""暂存首次身份并立即暴露数据库唯一键竞争。"""
def stage(session: Session) -> None:
"""加入并 flush 当前调用方事务。"""
session.add(identity)
session.flush()
self._execute_sync_write(stage)
def stage_replace(
self,
identity: PluginIdentity,
*,
expected_revision: int,
) -> bool:
"""仅在当前 revision 匹配时替换整份审计事实。"""
values = {
column.name: getattr(identity, column.name)
for column in PluginIdentity.__table__.columns
if column.name != "id"
}
return bool(
self._execute_sync_write(
lambda session: execute_dml(
session,
update(PluginIdentity)
.where(
PluginIdentity.normalized_plugin_id
== identity.normalized_plugin_id,
PluginIdentity.revision == expected_revision,
)
.values(**values),
execution_options={"synchronize_session": False},
)
)
)