"""配对 DTO。 定义 DM 安全与配对审批的不可变值对象,包括配对 ID、配对状态、DM 决策、 DM 策略、配对审批、配对记录与机器人循环预算。所有 DTO 均为 ``dataclass(frozen=True)``,仅依赖标准库,用于 DM 安全策略决策与配对 审批流程。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import StrEnum from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.dtos.common import Operator from yuxi.channels.contract.errors import ValidationError @dataclass(frozen=True) class PairingId: """配对 ID。 标识一次配对审批流程的全局唯一 ID,用于配对记录关联与审计。 字段: value: 配对 ID 字符串。 """ value: str class PairingStatus(StrEnum): """配对状态。 标识配对审批流程的当前状态,用于配对记录查询与状态机流转。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: PENDING: 待审批。 APPROVED: 已批准。 REJECTED: 已拒绝。 EXPIRED: 已过期。 REVOKED: 已撤销(FR-33,已批准的配对被主动撤销)。 """ PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" EXPIRED = "expired" REVOKED = "revoked" class DmDecision(StrEnum): """DM 决策。 描述 DM 消息的处置决策结果,用于入站管道的 DM 安全策略执行。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: ALLOW: 允许。 DENY: 拒绝。 PENDING_PAIRING: 待配对审批。 WHITELIST: 白名单放行。 """ ALLOW = "allow" DENY = "deny" PENDING_PAIRING = "pending_pairing" WHITELIST = "whitelist" class DmPolicy(StrEnum): """DM 策略。 描述渠道账户的 DM 安全策略,用于 DM 决策引擎的策略匹配。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: ALLOW: 允许所有 DM。 DENY: 拒绝所有 DM。 PAIRING_REQUIRED: 需配对审批。 WHITELIST: 白名单。 """ ALLOW = "allow" DENY = "deny" PAIRING_REQUIRED = "pairing_required" WHITELIST = "whitelist" @dataclass(frozen=True) class PairingApproval: """配对审批。 描述一次配对审批的结果,携带配对 ID、渠道账户 ID、对端 ID、状态、 审批人与过期时间,用于审批结果回传与审计。 字段: pairing_id: 配对 ID。 channel_account_id: 渠道账户 ID。 peer_id: 对端 ID。 status: 配对状态。 approver_id: 审批人 ID(待审批时为 None)。 approved_at: 审批时间(待审批时为 None)。 expires_at: 过期时间(可选)。 """ pairing_id: str channel_account_id: str peer_id: str status: PairingStatus approver_id: str | None = None approved_at: datetime | None = None expires_at: datetime | None = None @dataclass(frozen=True) class PairingRecord: """配对记录。 描述一条配对审批的持久化记录,携带配对 ID、渠道账户业务 ID、渠道类型、 对端信息、状态、审批人与全量时间戳,用于管理员审批列表查询(FR-33)、 状态机流转与审计追溯。 ``channel_type`` 与 ``account_id`` 由适配器层在查询时通过 ``channel_accounts`` 表关联填充,供管理后台展示与降级检查使用;``listExpiredPendingPairings`` 等内部场景可不填充(保持 ``None``)。 字段: pairing_id: 配对 ID(业务标识,UUID)。 channel_account_id: 渠道账户业务 ID(字符串,与 ``channel_type`` 共同确定账户作用域)。 channel_type: 渠道类型(可选,适配器层关联填充)。 peer_id: 对端用户 ID。 peer_name: 对端名称(冗余展示字段,可选)。 status: 配对状态。 approver_id: 审批人 ID(待审批时为 ``None``)。 approved_at: 批准时间(approved 状态时填充,可选)。 rejected_at: 拒绝时间(rejected 状态时填充,可选)。 expired_at: 过期生效时间(expired 状态时填充,由定时任务或审批时回填,可选)。 revoked_at: 撤销时间(revoked 状态时填充,FR-33,可选)。 reason: 审批原因 / 拒绝原因 / 撤销原因(可选)。 created_at: 创建时间(可选)。 updated_at: 更新时间(可选)。 expires_at: 过期时间(可选,供过期扫描器重建聚合根调用 ``expireIfOverdue``)。 requested_at: 申请时间(可选,供管理后台按申请时间排序展示)。 version: 乐观锁版本号(默认 1)。 """ pairing_id: str channel_account_id: str peer_id: str status: PairingStatus channel_type: ChannelType | None = None peer_name: str | None = None approver_id: str | None = None approved_at: datetime | None = None rejected_at: datetime | None = None expired_at: datetime | None = None revoked_at: datetime | None = None reason: str | None = None created_at: datetime | None = None updated_at: datetime | None = None expires_at: datetime | None = None requested_at: datetime | None = None version: int = 1 @dataclass(frozen=True) class PairingQuery: """配对查询。 描述配对审批记录的查询条件,支持按渠道类型、账户、对端用户、状态过滤 与分页,用于管理员配对审批列表查询(FR-33 ``pairing/list`` 操作)。 ``channel_type`` 与 ``account_id`` 共同确定渠道账户作用域,仅提供其一 时按提供的条件过滤。 字段: channel_type: 渠道类型(可选)。 account_id: 渠道账户业务 ID(可选)。 peer_id: 对端用户 ID(可选)。 status: 配对状态(可选)。 created_after: 申请时间下界(可选,含,按 ``requested_at`` 过滤)。 created_before: 申请时间上界(可选,含,按 ``requested_at`` 过滤)。 limit: 分页大小(默认 100)。 offset: 分页偏移(默认 0)。 """ channel_type: ChannelType | None = None account_id: str | None = None peer_id: str | None = None status: PairingStatus | None = None created_after: datetime | None = None created_before: datetime | None = None limit: int = 100 offset: int = 0 @dataclass(frozen=True) class BotLoopBudget: """机器人循环预算。 描述机器人回复的速率限制预算,包括每小时最大回复数、冷却时间、当前计数 与上次回复时间,用于防止机器人循环与速率限制。 字段: max_replies_per_hour: 每小时最大回复数。 cooldown_seconds: 冷却时间(秒)。 current_count: 当前计数(默认 0)。 last_reply_at: 上次回复时间(可选)。 """ max_replies_per_hour: int cooldown_seconds: int current_count: int = 0 last_reply_at: datetime | None = None @dataclass(frozen=True) class CreatePairingResult: """创建配对结果(FR-33)。 描述创建配对请求操作的返回结果,包含新创建的配对记录信息。 字段: pairing_id: 配对 ID。 channel_account_id: 渠道账户 ID。 peer_id: 对端 ID。 status: 配对状态(应为 PENDING)。 expires_at: 过期时间。 created_at: 创建时间。 """ pairing_id: str channel_account_id: str peer_id: str status: PairingStatus expires_at: datetime created_at: datetime @dataclass(frozen=True) class BatchPairingCmd: """批量配对命令(PRG-BATCH-ACT)。 由 ``PairingManagementPort.batchApprovePairings`` / ``batchRejectPairings`` 引用,操作类型由调用的方法名隐式确定 (``batchApprovePairings`` → approve,``batchRejectPairings`` → reject)。 字段: pairing_ids: 配对 ID 元组(必填)。 operator: 操作人(审计用)。 reason: 操作原因(可选,最长 512)。 """ pairing_ids: tuple[str, ...] operator: Operator reason: str | None = None def __post_init__(self) -> None: """校验业务规则(PRG-BATCH-ACT)。 - ``pairing_ids`` 非空且长度 ≤ 500。 - ``reason`` 非空时长度 ≤ 512。 在构造时即抛出 ``ValidationError``,避免非法值传播到 dispatch handler 后才暴露(INV-8)。 """ if not self.pairing_ids: raise ValidationError("pairing_ids", "pairing_ids must not be empty") if len(self.pairing_ids) > 500: raise ValidationError( "pairing_ids", f"pairing_ids length must be <= 500, got {len(self.pairing_ids)}", ) if self.reason is not None and len(self.reason) > 512: raise ValidationError( "reason", f"reason length must be <= 512, got {len(self.reason)}", ) @dataclass(frozen=True) class CleanExpiredPairingsCmd: """清理过期配对命令(PRG-CLEAN-EXPIRED)。 由 ``PairingManagementPort.cleanExpiredPairings`` 引用,单一事务批量 更新过期配对为 EXPIRED 状态。 字段: operator: 操作人(审计用)。 channel_type: 渠道类型过滤(可选)。 older_than: 过期时间上界(ISO 8601,缺省取所有过期)。 max_count: 单次最大清理数(默认 500,1-1000)。 """ operator: Operator channel_type: ChannelType | None = None older_than: datetime | None = None max_count: int = 500 def __post_init__(self) -> None: """校验业务规则(PRG-CLEAN-EXPIRED)。 ``max_count`` 必须在 ``[1, 1000]`` 区间,在构造时即抛出 ``ValidationError``,避免非法值传播到 dispatch handler 后才暴露 (INV-8)。 """ if self.max_count < 1 or self.max_count > 1000: raise ValidationError( "max_count", f"max_count must be between 1 and 1000, got {self.max_count}", ) @dataclass(frozen=True) class PairingStatsQuery: """配对统计查询(PRG-STATS)。 描述配对统计的查询条件,由 ``PairingRepositoryPort.getPairingStats`` 消费。``granularity`` 取值 ``hour`` / ``day`` / ``week``,默认 ``day``。 字段: channel_type: 渠道类型过滤(可选)。 start_time: 起始时间(可选,含)。 end_time: 结束时间(可选,含)。 granularity: 时间粒度(默认 ``day``)。 """ channel_type: ChannelType | None = None start_time: datetime | None = None end_time: datetime | None = None granularity: str = "day" def __post_init__(self) -> None: """校验业务规则(PRG-STATS)。 ``granularity`` 必须为 ``hour`` / ``day`` / ``week``,在构造时即 抛出 ``ValidationError``,避免非法值传播到仓储层后才暴露(INV-8)。 """ if self.granularity not in ("hour", "day", "week"): raise ValidationError( "granularity", f"granularity must be 'hour', 'day' or 'week', got {self.granularity!r}", ) @dataclass(frozen=True) class PairingTrendPoint: """配对趋势点(PRG-STATS)。 描述按时间粒度切片的配对趋势数据点,含申请数与批准数。 字段: timestamp: 时间桶起始时间。 requested: 申请数。 approved: 批准数。 """ timestamp: datetime requested: int approved: int @dataclass(frozen=True) class PairingStatsResult: """配对统计结果(PRG-STATS)。 描述配对审批的聚合统计指标,由 ``getPairingStats`` 返回。 字段: total_requested: 申请总数。 approved_count: 批准数。 rejected_count: 拒绝数。 revoked_count: 撤销数。 expired_count: 过期数。 approve_rate: 批准率(0-100)。 avg_approval_seconds: 平均审批时长(秒)。 trend: 配对趋势数据点元组。 """ total_requested: int approved_count: int rejected_count: int revoked_count: int expired_count: int approve_rate: float avg_approval_seconds: float trend: tuple[PairingTrendPoint, ...]