ForcePilot/backend/package/yuxi/channels/contract/dtos/config.py
Kris a484844ea1 chore: 汇总完成多模块功能迭代与缺陷修复
此提交包含了大量跨模块的优化与修复:
1.  配置与依赖调整:调整账号列表分页默认值、重试上限、内容审核长度限制,新增出站文件支持
2.  代码结构优化:重构白名单缓存逻辑、移除冗余日志依赖、清理过时TODO注释
3.  功能增强:添加死信计数接口、会话绑定审计、SSE订阅限流、凭据验证服务
4.  契约更新:修正DTO字段命名、完善类型定义、更新审计操作类型
5.  错误处理:优化限流清理逻辑、添加异常捕获与告警
6.  文档与校验:补充字段校验规则、完善注释与文档说明
2026-07-11 21:37:16 +08:00

463 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""配置热更新 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:
"""将配置字段类型别名归一化为规范值。
兼容 JSON Schema 常见别名(``number`` / ``object``)与插件 manifest
可能使用的 ``string`` / ``integer`` / ``boolean`` / ``enum`` 别名,
统一转为契约层规范的 ``str`` / ``int`` / ``float`` / ``bool`` /
``dict``,其中 ``enum`` 视为受约束的字符串,由 ``constraints.values``
描述可选值。
"""
mapping = {
"string": "str",
"integer": "int",
"number": "float",
"boolean": "bool",
"object": "dict",
"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 | float | bool | json | dict
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", "float", "bool", "json", "dict"]
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`` / ``number`` / ``object`` 等别名;统一转为
``str`` / ``int`` / ``bool`` / ``float`` / ``dict``,其中 ``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 ConfigImportFailure:
"""配置导入失败条目CFG-IMPORT
字段:
key: 失败的配置键。
reason: 失败原因(如 "key not declared" / "not hot reloadable" /
"type mismatch: expected int got str" / "version conflict")。
"""
key: str
reason: str
@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[ConfigImportFailure, ...]
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
#: ``batch_update`` 单次更新条目数上限CFG-BATCH-UPDATE
#: 作为 handler 与 DTO 校验的单一真相源,避免双重声明导致不一致。
BATCH_UPDATE_LIMIT: int = 50
@dataclass(frozen=True)
class BatchUpdateConfigCmd:
"""批量更新配置命令CFG-BATCH-UPDATE
由 ``ConfigManagementPort.batchUpdateConfig`` 引用,逐条复用
config/update 逻辑版本校验、hot-reload 检查),需记录操作人。
字段:
operator: 操作人(审计用)。
updates: 配置更新项元组必填1-BATCH_UPDATE_LIMITkey 不可重复)。
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`` 必须非空且长度不超过 ``BATCH_UPDATE_LIMIT``;同一批次
内 ``key`` 不可重复,否则 ``BatchExecutor`` 按 key 反查 update item
时会重复命中第一条,导致后续同 key 条目的 value 被静默丢弃INV-8
"""
if not self.updates:
raise ValidationError("updates", "must not be empty")
if len(self.updates) > BATCH_UPDATE_LIMIT:
raise ValidationError("updates", f"must not exceed {BATCH_UPDATE_LIMIT} 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, ...]