2026-07-02 03:22:12 +08:00
|
|
|
|
"""配对 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
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from yuxi.channels.contract.dtos.common import Operator
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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 状态时填充,可选)。
|
2026-07-08 03:57:05 +08:00
|
|
|
|
expired_at: 过期生效时间(expired 状态时填充,由定时任务或审批时回填,可选)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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
|
2026-07-08 03:57:05 +08:00
|
|
|
|
expired_at: datetime | None = None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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: 配对状态(可选)。
|
2026-07-02 18:27:06 +08:00
|
|
|
|
created_after: 申请时间下界(可选,含,按 ``requested_at`` 过滤)。
|
|
|
|
|
|
created_before: 申请时间上界(可选,含,按 ``requested_at`` 过滤)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
limit: 分页大小(默认 100)。
|
|
|
|
|
|
offset: 分页偏移(默认 0)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: ChannelType | None = None
|
|
|
|
|
|
account_id: str | None = None
|
|
|
|
|
|
peer_id: str | None = None
|
|
|
|
|
|
status: PairingStatus | None = None
|
2026-07-02 18:27:06 +08:00
|
|
|
|
created_after: datetime | None = None
|
|
|
|
|
|
created_before: datetime | None = None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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:
|
2026-07-03 19:18:13 +08:00
|
|
|
|
"""创建配对结果(FR-33)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
描述创建配对请求操作的返回结果,包含新创建的配对记录信息。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
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`` /
|
2026-07-03 19:18:13 +08:00
|
|
|
|
``batchRejectPairings`` 引用,操作类型由调用的方法名隐式确定
|
|
|
|
|
|
(``batchApprovePairings`` → approve,``batchRejectPairings`` → reject)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
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, ...]
|