ForcePilot/backend/package/yuxi/channels/contract/dtos/plugin.py

426 lines
13 KiB
Python
Raw Normal View History

"""插件 DTO。
定义插件相关的枚举不可变值对象与 Protocol包括权限枚举环境变量
领域事件事件处理 Protocol控制 RPC 处理 Protocol配置加载 Protocol
与配置解密 Protocol所有枚举继承 ``str, Enum`` 以支持 JSON 序列化
DTO 均为 ``dataclass(frozen=True)``Protocol 使用 ``@runtime_checkable``
装饰以支持 ``isinstance`` 检查仅依赖标准库用于插件生命周期事件
分发控制 RPC 与配置加载解密
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import TYPE_CHECKING, Any, Literal, Protocol, runtime_checkable
from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator
from yuxi.channels.contract.errors import ValidationError
if TYPE_CHECKING:
from yuxi.channels.contract.dtos.capability import ChannelCapabilities
from yuxi.channels.contract.plugin.manifest import PluginManifest
# 插件生命周期状态字面量类型(对齐 LifecycleState 枚举值)。
# 供 PluginSummary / PluginCatalogItem / PluginDetail 的 state 字段与
# Router 层 state 查询参数共用,避免 11 个取值在多处重复声明。
PluginStateLiteral = Literal[
"discovered",
"resolved",
"loaded",
"initialized",
"started",
"paused",
"stopped",
"unloaded",
"failed",
"not_installed",
"installed",
]
class Permission(StrEnum):
"""权限等级。
标识插件或操作所需的用户权限等级用于权限校验与审计继承
``str, Enum`` 以支持 JSON 序列化与字符串比较
取值
REQUIRED_USER: 普通需求用户
ADMIN_USER: 管理员用户
SUPERADMIN_USER: 超级管理员用户
"""
REQUIRED_USER = "required_user"
ADMIN_USER = "admin_user"
SUPERADMIN_USER = "superadmin_user"
@dataclass(frozen=True)
class EnvVar:
"""环境变量声明。
描述插件所需的环境变量元信息包括名称描述是否必填默认值与
是否敏感用于插件安装向导的环境变量校验与脱敏渲染
字段
name: 环境变量名称
description: 环境变量描述
required: 是否必填默认 True
default: 默认值可选
sensitive: 是否敏感需脱敏默认 False
"""
name: str
description: str
required: bool = True
default: str | None = None
sensitive: bool = False
@dataclass(frozen=True)
class DomainEvent:
"""领域事件。
描述插件生命周期或业务流程中产生的领域事件包括事件 ID事件类型
负载时间戳与链路追踪 ID用于事件总线分发与审计
字段
event_id: 事件 ID
event_type: 事件类型PluginLoaded | PairingApproved | ConfigChanged | ...
payload: 事件负载
timestamp: 事件时间戳
trace_id: 链路追踪 ID可选
"""
event_id: str
event_type: str
payload: dict[str, Any]
timestamp: datetime
trace_id: str | None = None
@runtime_checkable
class EventHandler(Protocol):
"""事件处理 Protocol。
由插件实现用于订阅并处理领域事件使用 ``@runtime_checkable``
装饰以支持 ``isinstance`` 检查事件处理为异步方法避免阻塞
事件总线分发线程
"""
async def handle(self, event: DomainEvent) -> None:
"""处理领域事件。
参数
event: 领域事件
"""
...
@runtime_checkable
class ConfigLoader(Protocol):
"""配置加载 Protocol。
由插件实现用于加载插件配置并返回配置字典使用
``@runtime_checkable`` 装饰以支持 ``isinstance`` 检查配置加载
为同步方法适用于启动期一次性加载场景
"""
def load(self) -> dict[str, Any]:
"""加载插件配置。
返回:
配置字典
"""
...
@runtime_checkable
class ConfigDecryptor(Protocol):
"""配置解密 Protocol。
由插件实现用于解密敏感配置值使用 ``@runtime_checkable`` 装饰
以支持 ``isinstance`` 检查解密为同步方法适用于配置加载后
的就地解密场景
"""
def decrypt(self, value: str) -> str:
"""解密配置值。
参数:
value: 待解密的配置值
返回:
解密后的明文配置值
"""
...
@dataclass(frozen=True)
class PluginSummary:
"""插件摘要PLG-QUERY-01 列表项)。
字段
plugin_id: 插件唯一标识manifest.manifest.id
name: 插件显示名称manifest.manifest.name
version: 插件版本manifest.manifest.version
channel_type: 绑定渠道类型manifest.manifest.channel_type
state: 当前生命周期状态LifecycleState.value
"""
plugin_id: str
name: str
version: str
channel_type: str
state: PluginStateLiteral
@dataclass(frozen=True)
class PluginCatalogItem:
"""插件目录项PLG-CATALOG 卡片视图数据)。
``PluginSummary`` 的区别在于携带 ``capabilities`` 字段供前端
卡片网格展示能力摘要``capabilities`` ``ChannelCapabilities``
dataclass Router ``serialize_control_data`` 递归序列化
字段
plugin_id: 插件唯一标识
name: 插件显示名称
version: 插件版本
channel_type: 绑定渠道类型
state: 当前生命周期状态
capabilities: 渠道能力集合静态声明
"""
plugin_id: str
name: str
version: str
channel_type: str
state: PluginStateLiteral
capabilities: ChannelCapabilities
@dataclass(frozen=True)
class PluginDetail:
"""插件详情PLG-QUERY-02 响应)。
字段
plugin_id: 插件唯一标识
name: 插件显示名称
version: 插件版本
channel_type: 绑定渠道类型
state: 当前生命周期状态
manifest: 完整 PluginManifest含内嵌 ChannelManifest
adapters / stages / event_subscriptions / config_sources
ChannelManifest capabilities / provides / depends /
config_schema / failure_policy / resource_quota 等字段
"""
plugin_id: str
name: str
version: str
channel_type: str
state: PluginStateLiteral
manifest: PluginManifest
@dataclass(frozen=True)
class InstallPluginCmd:
"""安装插件命令PLG-INSTALL
``PluginManagementPort.installPlugin`` 引用从来源安装插件需记录
操作人以满足审计要求超级管理员
字段
operator: 操作人审计用
source_type: 来源类型``path`` / ``url`` / ``registry``
source: 来源路径或 URL
version: 指定版本可选
force: 是否强制覆盖已存在插件默认 False
"""
operator: Operator
source_type: Literal["path", "url", "registry"]
source: str
version: str | None = None
force: bool = False
def __post_init__(self) -> None:
"""校验必填字段非空与 source_type 取值PLG-INSTALL
``source`` 必须非空``source_type`` 必须为 ``path`` / ``url`` /
``registry`` 之一在构造时即抛出 ``ValidationError``adapter 不再做
该校验INV-8
"""
if not self.source:
raise ValidationError("source", "must not be empty")
if self.source_type not in ("path", "url", "registry"):
raise ValidationError(
"source_type",
"must be one of: path, url, registry",
)
@dataclass(frozen=True)
class InstallPluginResult:
"""安装插件结果PLG-INSTALL
字段
plugin_id: 插件 ID
version: 安装的插件版本
installed_at: 安装时间戳
requires_load: 是否需要手动加载默认 True
"""
plugin_id: str
version: str
installed_at: datetime
requires_load: bool = True
@dataclass(frozen=True)
class UninstallPluginResult:
"""卸载插件结果PLG-UNINSTALL-FILE
字段
plugin_id: 插件 ID
uninstalled_at: 卸载时间戳
files_removed: 已删除的文件列表
"""
plugin_id: str
uninstalled_at: datetime
files_removed: tuple[str, ...]
@dataclass(frozen=True)
class PluginConfigResult:
"""插件配置查询结果PLG-CONFIG
字段
plugin_id: 插件 ID
config: 当前配置值
schema: 配置 schema PluginManifest 获取
requires_restart: 是否需要重启生效
updated_at: 配置最后更新时间可选
"""
plugin_id: str
config: dict[str, Any]
schema: dict[str, Any]
requires_restart: bool
updated_at: datetime | None = None
@dataclass(frozen=True)
class UpdatePluginConfigCmd:
"""更新插件配置命令PLG-CONFIG-PUT
``PluginManagementPort.updatePluginConfig`` 引用 ``apply_mode``
判断是否需要重启需记录操作人以满足审计要求
字段
plugin_id: 插件 ID
operator: 操作人审计用
config: 配置值
apply_mode: 应用模式``hot`` / ``restart_required``
"""
plugin_id: str
operator: Operator
config: dict[str, Any]
apply_mode: Literal["hot", "restart_required"]
def __post_init__(self) -> None:
"""校验必填字段非空与 apply_mode 取值PLG-CONFIG-PUT
``plugin_id`` 必须非空``config`` 必须非空字典``apply_mode``
必须为 ``hot`` / ``restart_required`` 之一在构造时即抛出
``ValidationError``adapter 不再做该校验INV-8
"""
if not self.plugin_id:
raise ValidationError("plugin_id", "must not be empty")
if not self.config:
raise ValidationError("config", "must not be empty")
if self.apply_mode not in ("hot", "restart_required"):
raise ValidationError(
"apply_mode",
"must be one of: hot, restart_required",
)
@dataclass(frozen=True)
class BatchPluginLifecycleSuccessItem:
"""批量插件生命周期成功条目PLG-BATCH-LIFE
字段
plugin_id: 插件 ID
new_state: 新生命周期状态
"""
plugin_id: str
new_state: str
@dataclass(frozen=True)
class BatchPluginLifecycleCmd:
"""批量插件生命周期命令PLG-BATCH-LIFE
逐条独立事务执行批量启动/停止模式 D``plugin_ids`` ``filter``
二选一不可同时为空不可同时提供 ``__post_init__`` 校验
字段
operator: 操作人审计用
plugin_ids: 显式插件 ID 元组默认空元组
filter: 筛选条件 channel_type / state可选
"""
operator: Operator
plugin_ids: tuple[str, ...] = ()
filter: dict[str, Any] | None = None
def __post_init__(self) -> None:
"""校验 plugin_ids 与 filter 互斥性与非空PLG-BATCH-LIFE
``plugin_ids`` ``filter`` 二选一不可同时为空不可同时提供
``plugin_ids`` 各项不得为空字符串在构造时即抛出 ``ValidationError``
使非法输入在 Router 层即被拒绝INV-8
"""
has_ids = bool(self.plugin_ids)
has_filter = self.filter is not None
if not has_ids and not has_filter:
raise ValidationError(
"plugin_ids",
"either plugin_ids or filter must be provided",
)
if has_ids and has_filter:
raise ValidationError(
"plugin_ids",
"plugin_ids and filter are mutually exclusive",
)
if has_ids and not all(pid for pid in self.plugin_ids):
raise ValidationError(
"plugin_ids",
"plugin_ids must not contain empty strings",
)
@dataclass(frozen=True)
class BatchPluginLifecycleResult:
"""批量插件生命周期结果PLG-BATCH-LIFE
描述逐条独立事务执行批量启动/停止的执行结果``failed`` 使用通用
``BatchOperationFailure````id`` 字段承载 plugin_id
字段
total: 待操作插件总数
succeeded: 成功条目元组
failed: 失败条目元组
"""
total: int
succeeded: tuple[BatchPluginLifecycleSuccessItem, ...]
failed: tuple[BatchOperationFailure, ...]