ForcePilot/backend/package/yuxi/channels/contract/dtos/pairing.py

390 lines
12 KiB
Python
Raw Normal View History

"""配对 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: 单次最大清理数默认 5001-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, ...]