本次提交包含多维度代码优化与功能增强: 1. 移除报告模块冗余导入与枚举,清理报表相关代码 2. 新增扫码登录支持方法与飞书适配器适配 3. 完善异常日志与健康检查信息 4. 扩展目录、配对管理、能力查询等接口 5. 优化出站管道与事务提交后钩子逻辑 6. 修复飞书消息解析与响应空值问题 7. 重构配置更新与服务账号创建逻辑 8. 统一传输错误分类契约与错误基类扩展
434 lines
14 KiB
Python
434 lines
14 KiB
Python
"""配置热更新 DTO。
|
||
|
||
定义配置热更新端口的命令与值对象,包括配置热更新模式、配置作用域、
|
||
配置版本、配置字段、更新 / 回滚配置命令与配置值。所有 DTO 均为
|
||
``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型,用于配置
|
||
热更新(FR-37)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from datetime import datetime
|
||
from enum import StrEnum
|
||
from typing import Any, Literal
|
||
|
||
from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
|
||
|
||
def _normalize_config_field_type(type_value: str) -> str:
|
||
"""将配置字段类型别名归一化为规范值。"""
|
||
mapping = {
|
||
"string": "str",
|
||
"integer": "int",
|
||
"boolean": "bool",
|
||
"enum": "str",
|
||
}
|
||
return mapping.get(type_value, type_value)
|
||
|
||
|
||
class ConfigHotReloadMode(StrEnum):
|
||
"""配置热更新模式。
|
||
|
||
标识配置变更的热更新策略,用于控制配置变更的生效方式。继承
|
||
``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
||
|
||
取值:
|
||
OFF: 不热更新。
|
||
HOT: 所有可热更新的配置项均热更新。
|
||
RESTART: 所有变更触发重启。
|
||
HYBRID: 混合模式(默认)。
|
||
"""
|
||
|
||
OFF = "off"
|
||
HOT = "hot"
|
||
RESTART = "restart"
|
||
HYBRID = "hybrid"
|
||
|
||
|
||
class ConfigScope(StrEnum):
|
||
"""配置作用域。
|
||
|
||
标识配置项的作用域层级,用于配置管理与作用域过滤。继承
|
||
``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
||
|
||
取值:
|
||
GLOBAL: 全局配置。
|
||
CHANNEL: 渠道级配置。
|
||
ACCOUNT: 账户级配置。
|
||
PLUGIN: 插件级配置(target 为 plugin_id)。
|
||
"""
|
||
|
||
GLOBAL = "global"
|
||
CHANNEL = "channel"
|
||
ACCOUNT = "account"
|
||
PLUGIN = "plugin"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigVersion:
|
||
"""配置版本。
|
||
|
||
描述配置项的版本信息,包括键、版本号与更新时间,用于乐观并发控制
|
||
与版本回溯。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
version: 版本号。
|
||
updated_at: 更新时间;配置不存在(version=0)时为 None,
|
||
避免误报"刚刚更新"。
|
||
"""
|
||
|
||
key: str
|
||
version: int
|
||
updated_at: datetime | None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigField:
|
||
"""配置字段。
|
||
|
||
描述配置项的字段元信息,包括键、类型、是否必填、默认值、是否可热
|
||
更新、是否敏感、作用域与约束,用于配置校验与热更新决策。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
type: 字段类型(str | int | bool | json)。
|
||
required: 是否必填(默认 False)。
|
||
default: 默认值(默认 None)。
|
||
hot_reloadable: 是否可热更新(默认 True)。
|
||
sensitive: 是否为敏感字段(默认 False)。敏感字段在审计日志、
|
||
配置变更事件序列化与配置导出时执行脱敏或过滤,由
|
||
``SensitiveFieldRegistry`` 派生为 ``key → sensitive`` 映射
|
||
供消费点查询(F-01)。
|
||
scope: 配置作用域(默认 None,向后兼容)。声明该 key 应当归属的
|
||
作用域层级,由 ``ConfigScopeRegistry`` 派生为
|
||
``key → ConfigScope`` 映射,供 ``RedisConfigAdapter._build_key``
|
||
在读写时校验作用域一致性——声明作用域与实际作用域不一致时产生
|
||
WARNING 日志,不阻断读写(F-02)。未声明(None)时不产生告警。
|
||
constraints: 约束(可选)。
|
||
title: 配置项中文显示名称(可选),供管理后台展示。
|
||
description: 配置项说明(可选),供管理后台展示帮助文案。
|
||
category: 配置项分组(可选),用于管理后台按功能分组展示。
|
||
"""
|
||
|
||
key: str
|
||
type: Literal["str", "int", "bool", "json"]
|
||
required: bool = False
|
||
default: Any = None
|
||
hot_reloadable: bool = True
|
||
sensitive: bool = False
|
||
scope: ConfigScope | None = None
|
||
constraints: dict[str, Any] | None = None
|
||
title: str | None = None
|
||
description: str | None = None
|
||
category: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""将非标准类型别名归一化为契约声明的规范值。
|
||
|
||
manifest.json 与部分插件入口可能使用 ``string`` / ``integer`` /
|
||
``boolean`` / ``enum`` 等别名;统一转为 ``str`` / ``int`` / ``bool``,
|
||
其中 ``enum`` 视为受约束的字符串,由 ``constraints.values`` 描述可选值。
|
||
"""
|
||
normalized = _normalize_config_field_type(self.type)
|
||
if normalized != self.type:
|
||
object.__setattr__(self, "type", normalized)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class UpdateConfigCmd:
|
||
"""更新配置命令(FR-37)。
|
||
|
||
由配置端口方法引用,描述一次配置更新请求,携带键、值、作用域、目标、
|
||
期望版本与操作人,用于配置更新与乐观并发控制。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
value: 配置值。
|
||
operator: 操作人(审计用)。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type / account_id / plugin_id,可选)。
|
||
expected_version: 期望版本(可选,用于乐观并发控制)。
|
||
"""
|
||
|
||
key: str
|
||
value: Any
|
||
operator: Operator
|
||
scope: ConfigScope = ConfigScope.GLOBAL
|
||
target: str | None = None
|
||
expected_version: int | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空与版本号合理性(FR-37)。
|
||
|
||
``key`` 必须非空,``expected_version`` 提供时必须为正整数,
|
||
``scope`` 为 ``channel`` / ``account`` / ``plugin`` 时 ``target``
|
||
必须非空。在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.key:
|
||
raise ValidationError("key", "must not be empty")
|
||
if self.expected_version is not None and self.expected_version <= 0:
|
||
raise ValidationError("expected_version", "must be positive")
|
||
if self.scope in (ConfigScope.CHANNEL, ConfigScope.ACCOUNT, ConfigScope.PLUGIN) and not self.target:
|
||
raise ValidationError(
|
||
"target",
|
||
"must not be empty when scope is channel, account or plugin",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RollbackConfigCmd:
|
||
"""回滚配置命令(FR-37)。
|
||
|
||
由配置端口方法引用,描述一次配置回滚请求,携带键、目标版本、作用域、
|
||
目标与操作人,用于配置版本回滚。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
target_version: 目标版本。
|
||
operator: 操作人(审计用)。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type / account_id / plugin_id,可选)。
|
||
"""
|
||
|
||
key: str
|
||
target_version: int
|
||
operator: Operator
|
||
scope: ConfigScope = ConfigScope.GLOBAL
|
||
target: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空与目标版本号合理性(FR-37)。
|
||
|
||
``key`` 必须非空,``target_version`` 必须为正整数,
|
||
``scope`` 为 ``channel`` / ``account`` / ``plugin`` 时 ``target``
|
||
必须非空。在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.key:
|
||
raise ValidationError("key", "must not be empty")
|
||
if self.target_version <= 0:
|
||
raise ValidationError("target_version", "must be positive")
|
||
if self.scope in (ConfigScope.CHANNEL, ConfigScope.ACCOUNT, ConfigScope.PLUGIN) and not self.target:
|
||
raise ValidationError(
|
||
"target",
|
||
"must not be empty when scope is channel, account or plugin",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigValue:
|
||
"""配置值。
|
||
|
||
描述配置项的当前值,包括键、值、版本与作用域,用于配置查询与通知。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
value: 配置值。
|
||
version: 版本号。
|
||
scope: 配置作用域。
|
||
"""
|
||
|
||
key: str
|
||
value: Any
|
||
version: int
|
||
scope: ConfigScope
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigHistoryEntry:
|
||
"""配置历史版本条目。
|
||
|
||
描述配置项的单个历史版本信息,包括版本号、值与更新时间,用于配置
|
||
版本历史查询(FR-37)。``updated_at`` 在历史条目未持久化时间戳时
|
||
为 ``None``(兼容历史数据)。
|
||
|
||
字段:
|
||
version: 版本号。
|
||
value: 配置值(已反序列化的 Python 对象)。
|
||
updated_at: 更新时间;历史数据未存储时间戳时为 ``None``。
|
||
"""
|
||
|
||
version: int
|
||
value: Any
|
||
updated_at: datetime | None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigExportCmd:
|
||
"""配置导出命令(CFG-EXPORT)。
|
||
|
||
由 ``ConfigManagementPort.exportConfig`` 引用,导出指定作用域的配置,
|
||
需记录操作人以满足审计要求(超级管理员)。
|
||
|
||
字段:
|
||
operator: 操作人(审计用)。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type 或 account_id,可选)。
|
||
"""
|
||
|
||
operator: Operator
|
||
scope: ConfigScope = ConfigScope.GLOBAL
|
||
target: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigExportResult:
|
||
"""配置导出结果(CFG-EXPORT)。
|
||
|
||
字段:
|
||
scope: 配置作用域。
|
||
config_data: 导出的配置数据。
|
||
version: 导出格式版本。
|
||
exported_at: 导出时间戳。
|
||
target: 目标(可选)。
|
||
"""
|
||
|
||
scope: ConfigScope
|
||
config_data: dict[str, Any]
|
||
version: int
|
||
exported_at: datetime
|
||
target: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ImportConfigCmd:
|
||
"""配置导入命令(CFG-IMPORT)。
|
||
|
||
由 ``ConfigManagementPort.importConfig`` 引用,逐条校验 schema 并写入,
|
||
需记录操作人以满足审计要求(超级管理员)。
|
||
|
||
字段:
|
||
operator: 操作人(审计用)。
|
||
config_data: 导入的配置数据。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(可选)。
|
||
overwrite: 是否覆盖已存在 key(默认 False)。
|
||
dry_run: 是否仅预校验不写入(默认 False)。
|
||
"""
|
||
|
||
operator: Operator
|
||
config_data: dict[str, Any]
|
||
scope: ConfigScope = ConfigScope.GLOBAL
|
||
target: str | None = None
|
||
overwrite: bool = False
|
||
dry_run: bool = False
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ImportConfigResult:
|
||
"""配置导入结果(CFG-IMPORT)。
|
||
|
||
字段:
|
||
imported_count: 成功导入数。
|
||
skipped_count: 跳过数(overwrite=false 时已存在)。
|
||
failed_count: 失败数。
|
||
failed_keys: 失败的配置 key 元组。
|
||
imported_at: 导入时间戳。
|
||
"""
|
||
|
||
imported_count: int
|
||
skipped_count: int
|
||
failed_count: int
|
||
failed_keys: tuple[str, ...]
|
||
imported_at: datetime
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigUpdateItem:
|
||
"""单条配置更新项(CFG-BATCH-UPDATE)。
|
||
|
||
描述批量更新中的单条配置项,含 key、value 与期望版本(乐观并发控制)。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
value: 配置值。
|
||
expected_version: 期望版本(可选,用于乐观并发控制)。
|
||
"""
|
||
|
||
key: str
|
||
value: Any
|
||
expected_version: int | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchConfigUpdateSuccessItem:
|
||
"""批量配置更新成功条目(CFG-BATCH-UPDATE)。
|
||
|
||
字段:
|
||
key: 配置键。
|
||
new_version: 更新后的新版本号。
|
||
"""
|
||
|
||
key: str
|
||
new_version: int
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchUpdateConfigCmd:
|
||
"""批量更新配置命令(CFG-BATCH-UPDATE)。
|
||
|
||
由 ``ConfigManagementPort.batchUpdateConfig`` 引用,逐条复用
|
||
config/update 逻辑(版本校验、hot-reload 检查),需记录操作人。
|
||
|
||
字段:
|
||
operator: 操作人(审计用)。
|
||
updates: 配置更新项元组(必填,1-50,key 不可重复)。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(可选)。
|
||
stop_on_error: 首条失败即终止(默认 False,逐条独立事务)。
|
||
"""
|
||
|
||
operator: Operator
|
||
updates: tuple[ConfigUpdateItem, ...]
|
||
scope: ConfigScope = ConfigScope.GLOBAL
|
||
target: str | None = None
|
||
stop_on_error: bool = False
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验 updates 非空、长度上限与 key 唯一性(FR-37)。
|
||
|
||
``updates`` 必须非空且长度不超过 50;同一批次内 ``key`` 不可重复,
|
||
否则 ``BatchExecutor`` 按 key 反查 update item 时会重复命中第一条,
|
||
导致后续同 key 条目的 value 被静默丢弃(INV-8)。
|
||
"""
|
||
if not self.updates:
|
||
raise ValidationError("updates", "must not be empty")
|
||
if len(self.updates) > 50:
|
||
raise ValidationError("updates", "must not exceed 50 items")
|
||
keys = [item.key for item in self.updates]
|
||
if not all(keys):
|
||
raise ValidationError("key", "must not be empty")
|
||
seen: set[str] = set()
|
||
for key in keys:
|
||
if key in seen:
|
||
raise ValidationError(
|
||
"updates",
|
||
f"duplicate key in batch update: {key}",
|
||
)
|
||
seen.add(key)
|
||
if self.scope in (ConfigScope.CHANNEL, ConfigScope.ACCOUNT, ConfigScope.PLUGIN) and not self.target:
|
||
raise ValidationError(
|
||
"target",
|
||
"must not be empty when scope is channel, account or plugin",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchUpdateConfigResult:
|
||
"""批量更新配置结果(CFG-BATCH-UPDATE)。
|
||
|
||
描述批量更新配置的执行结果,``failed`` 使用通用
|
||
``BatchOperationFailure``(``id`` 字段承载 key)。
|
||
|
||
字段:
|
||
total: 待更新配置总数。
|
||
succeeded: 成功条目元组。
|
||
failed: 失败条目元组。
|
||
"""
|
||
|
||
total: int
|
||
succeeded: tuple[BatchConfigUpdateSuccessItem, ...]
|
||
failed: tuple[BatchOperationFailure, ...]
|