ForcePilot/backend/package/yuxi/channels/contract/dtos/config.py
Kris 742299cb07 chore: 批量清理报告相关代码并完成多项功能迭代
本次提交包含多维度代码优化与功能增强:
1.  移除报告模块冗余导入与枚举,清理报表相关代码
2.  新增扫码登录支持方法与飞书适配器适配
3.  完善异常日志与健康检查信息
4.  扩展目录、配对管理、能力查询等接口
5.  优化出站管道与事务提交后钩子逻辑
6.  修复飞书消息解析与响应空值问题
7.  重构配置更新与服务账号创建逻辑
8.  统一传输错误分类契约与错误基类扩展
2026-07-06 20:49:35 +08:00

434 lines
14 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:
"""将配置字段类型别名归一化为规范值。"""
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-50key 不可重复)。
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, ...]