本次提交包含多维度代码优化与功能增强: 1. 移除报告模块冗余导入与枚举,清理报表相关代码 2. 新增扫码登录支持方法与飞书适配器适配 3. 完善异常日志与健康检查信息 4. 扩展目录、配对管理、能力查询等接口 5. 优化出站管道与事务提交后钩子逻辑 6. 修复飞书消息解析与响应空值问题 7. 重构配置更新与服务账号创建逻辑 8. 统一传输错误分类契约与错误基类扩展
433 lines
13 KiB
Python
433 lines
13 KiB
Python
"""插件 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`` 与 ``icon`` 字段,
|
||
供前端卡片网格展示能力摘要与渠道图标。``capabilities`` 为
|
||
``ChannelCapabilities`` dataclass,由 Router 层
|
||
``serialize_control_data`` 递归序列化。
|
||
|
||
字段:
|
||
plugin_id: 插件唯一标识。
|
||
name: 插件显示名称。
|
||
version: 插件版本。
|
||
channel_type: 绑定渠道类型。
|
||
state: 当前生命周期状态。
|
||
capabilities: 渠道能力集合(静态声明)。
|
||
icon: 渠道展示图标名(lucide 图标名,缺失时前端回退默认图标)。
|
||
来源为 ``ChannelManifest.icon``,供前端卡片/筛选/标签等场景
|
||
统一渲染渠道图标,避免在前端硬编码渠道清单。
|
||
"""
|
||
|
||
plugin_id: str
|
||
name: str
|
||
version: str
|
||
channel_type: str
|
||
state: PluginStateLiteral
|
||
capabilities: ChannelCapabilities
|
||
icon: str | None = None
|
||
|
||
|
||
@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 等字段。
|
||
last_error: 最近一次生命周期失败错误信息(可选),供详情页排障展示。
|
||
"""
|
||
|
||
plugin_id: str
|
||
name: str
|
||
version: str
|
||
channel_type: str
|
||
state: PluginStateLiteral
|
||
manifest: PluginManifest
|
||
last_error: str | None = None
|
||
|
||
|
||
@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, ...]
|