"""路由 DTO。 定义路由匹配相关的不可变值对象,包括匹配来源枚举、配置匹配来源枚举、 路由绑定、匹配层级、绑定上下文、配置回退链与嵌套白名单决策。所有 DTO 均为 ``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型, 用于路由匹配、绑定解析与白名单决策。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import StrEnum from typing import Literal from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.errors import ValidationError class MatchSource(StrEnum): """匹配来源。 标识路由绑定的匹配来源,用于路由决策追溯与审计。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: EXPLICIT: 显式绑定。 SESSION: 会话级匹配。 IDENTITY: 身份级匹配。 DEFAULT: 默认回退。 CHAT_TYPE: 会话类型匹配。 """ EXPLICIT = "explicit" SESSION = "session" IDENTITY = "identity" DEFAULT = "default" CHAT_TYPE = "chat_type" class ConfigMatchSource(StrEnum): """配置匹配来源。 标识配置层路由匹配的来源类型,用于配置回退链决策追溯。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: DIRECT: 直接匹配。 NORMALIZED_DIRECT: 归一化直接匹配。 PARENT: 父级匹配。 NORMALIZED_PARENT: 归一化父级匹配。 WILDCARD: 通配符匹配。 CHANNEL_BINDING: 渠道绑定。 """ DIRECT = "direct" NORMALIZED_DIRECT = "normalized_direct" PARENT = "parent" NORMALIZED_PARENT = "normalized_parent" WILDCARD = "wildcard" CHANNEL_BINDING = "channel_binding" @dataclass(frozen=True) class RouteBinding: """路由绑定。 描述路由匹配产出的绑定关系,关联渠道账户、可选渠道会话与 Agent 绑定(agent slug),并记录匹配来源用于审计。 字段: channel_account_id: 渠道账户 ID。 agent_binding: Agent 绑定(agent slug,即配置匹配值)。 match_source: 匹配来源。 channel_session_id: 渠道会话 ID(可选)。 matched_layer: 命中的匹配层级名称(FR-04 诊断元数据,AC-05)。 config_match_source: 配置匹配来源(FR-30 诊断元数据,AC-59)。 config_match_key: 配置匹配键(FR-30 诊断元数据,AC-58,配置回退 命中时记录匹配到的键;未经过配置回退时为 None)。 """ channel_account_id: str agent_binding: str match_source: MatchSource channel_session_id: str | None = None matched_layer: str | None = None config_match_source: ConfigMatchSource | None = None config_match_key: str | None = None def __post_init__(self) -> None: """校验 channel_account_id 与 agent_binding 非空。 ``channel_account_id`` 与 ``agent_binding`` 必须非空,在构造时即 抛出 ``ValidationError``,避免空账户 ID 或空 Agent 绑定导致路由绑定 无法定位(INV-8)。 """ if not self.channel_account_id: raise ValidationError("channel_account_id", "must not be empty") if not self.agent_binding: raise ValidationError("agent_binding", "must not be empty") @dataclass(frozen=True) class MatchTier: """匹配层级。 描述路由匹配层级的配置,包括名称、优先级、启用状态与匹配方法, 用于 FR-04 匹配层级注册与执行顺序控制。 字段: name: 层级名称。 priority: 优先级(越小越先执行)。 enabled: 是否启用(默认 True)。 match_method: 匹配方法标识(exact / prefix / regex)。 """ name: str priority: int match_method: Literal["exact", "prefix", "regex"] enabled: bool = True def __post_init__(self) -> None: """校验 name 非空、priority 非负与 match_method 取值合法。 ``name`` 必须非空,``priority`` 必须非负,``match_method`` 必须为 ``exact`` / ``prefix`` / ``regex`` 之一,在构造时即抛出 ``ValidationError``,避免非法层级定义破坏匹配执行顺序(INV-8)。 """ if not self.name: raise ValidationError("name", "must not be empty") if self.priority < 0: raise ValidationError("priority", "must not be negative") if self.match_method not in ("exact", "prefix", "regex"): raise ValidationError("match_method", "must be one of exact, prefix, regex") @dataclass(frozen=True) class BindingContext: """绑定上下文。 描述路由匹配的上下文信息,包括会话键、渠道类型、账户 ID、会话 类型、对端 ID 与可选的会话 / 身份关联,用于匹配层级执行。 字段: session_key: 会话键。 channel_type: 渠道类型。 account_id: 渠道账户 ID。 chat_type: 会话类型(p2p | group)。 peer_id: 对端 ID。 conversation_id: 关联的内部会话 ID(可选)。 unified_identity_id: 统一身份 ID(可选)。 """ session_key: str channel_type: ChannelType account_id: str chat_type: Literal["p2p", "group"] peer_id: str conversation_id: str | None = None unified_identity_id: str | None = None def __post_init__(self) -> None: """校验必填字符串非空与 chat_type 取值合法。 ``session_key`` / ``account_id`` / ``peer_id`` 必须非空,``chat_type`` 必须为 ``p2p`` / ``group`` 之一,在构造时即抛出 ``ValidationError``, 避免空定位字段或非法会话类型导致路由匹配错位(INV-8)。 """ if not self.session_key: raise ValidationError("session_key", "must not be empty") if not self.account_id: raise ValidationError("account_id", "must not be empty") if not self.peer_id: raise ValidationError("peer_id", "must not be empty") if self.chat_type not in ("p2p", "group"): raise ValidationError("chat_type", "must be one of p2p, group") @dataclass(frozen=True) class ConfigFallbackChain: """配置回退链。 描述配置层路由匹配的回退链节点,包括匹配键、匹配来源与匹配值, 用于配置回退决策追溯。 字段: match_key: 匹配的键。 match_source: 匹配来源。 match_value: 匹配值(可选)。 """ match_key: str match_source: ConfigMatchSource match_value: str | None = None def __post_init__(self) -> None: """校验 match_key 非空。 ``match_key`` 必须非空,在构造时即抛出 ``ValidationError``,避免空 匹配键导致配置回退决策追溯失效(INV-8)。 """ if not self.match_key: raise ValidationError("match_key", "must not be empty") @dataclass(frozen=True) class NestedWhitelistDecision: """嵌套白名单决策。 描述嵌套白名单的决策结果,包括外层配置与匹配状态以及内层决策, 用于 FR-30 嵌套白名单场景的决策追溯。 字段: outer_configured: 外层是否配置。 outer_matched: 外层是否匹配。 inner_decision: 内层决策(allow / deny,可选)。 """ outer_configured: bool outer_matched: bool inner_decision: Literal["allow", "deny"] | None = None @dataclass(frozen=True) class RouteBindingRule: """路由绑定规则 DTO(持久化层输出)。 字段命名对齐 ORM 模型与契约层 RouteBinding: - account_id 对应 ORM.account_id(运行时映射到 RouteBinding.channel_account_id) - agent_binding 直接对应 RouteBinding.agent_binding(slug 字符串) """ binding_id: str channel_type: ChannelType account_id: str match_source: str # 8 种 tier 名之一 match_value: str | None agent_binding: str enabled: bool description: str | None created_by: str | None updated_by: str | None created_at: datetime updated_at: datetime version: int # 乐观锁版本号,更新时回传 expected_version 校验 priority: int | None = None # 匹配层级优先级(由 match_source 派生,仅展示用) @dataclass(frozen=True) class SaveRouteBindingCmd: """创建路由绑定命令。 命名风格对齐现有 SaveChannelAccountCmd(contract/dtos/persistence.py)。 """ channel_type: ChannelType account_id: str match_source: str match_value: str | None agent_binding: str description: str | None = None @dataclass(frozen=True) class UpdateRouteBindingCmd: """更新路由绑定命令。 所有可选字段为 None 表示不更新。binding_id / expected_version 必填: expected_version 取自加载时的 version,用于乐观锁校验,并发冲突抛 ConflictError。 """ binding_id: str expected_version: int agent_binding: str | None = None match_value: str | None = None enabled: bool | None = None description: str | None = None @dataclass(frozen=True) class RouteBindingFilter: """路由绑定查询过滤条件。""" channel_type: ChannelType | None = None account_id: str | None = None match_source: str | None = None enabled: bool | None = None agent_binding: str | None = None