本次提交涵盖了近百处代码优化与功能补全,包括: 1. 完善配置与数据模型:新增expired_at配对记录字段、路由绑定乐观锁版本控制、会话路由信息追踪字段 2. 优化业务流程:添加幂等记录操作人审计、会话合并领域服务文档更新、媒体处理异步化改造 3. 新增功能能力:健康检查时间更新、会话路由信息更新接口、内容审核/幂等记录清理定时任务 4. 修复与简化:移除废弃的max_message_length属性、修复微信iLink适配器配置读取路径、简化配对过期扫描逻辑 5. 代码规范优化:统一敏感词检测工具导入、完善事务上下文处理注释、调整wechat_woc入站适配器sender回退逻辑
390 lines
12 KiB
Python
390 lines
12 KiB
Python
"""配对 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, ...]
|