本次提交包含多项代码优化与规范修正: 1. 文档与注释优化:修正注释术语、补充注解与FR编号 2. 代码格式调整:统一空格、换行与缩进规范 3. 类型与接口完善:补充__all__导出、修正返回类型注解 4. 错误处理增强:新增领域错误类与校验逻辑 5. 依赖与导入调整:修复路径引用、统一时区导入 6. 协议与契约更新:完善接口文档与一致性注解
548 lines
18 KiB
Python
548 lines
18 KiB
Python
"""内容审核 DTO。
|
||
|
||
定义内容审核域的不可变值对象,包括审核结论枚举、严重级别枚举、来源枚举、
|
||
资源类型枚举,以及审核请求、命中片段、审核结论、完整审核记录、历史查询
|
||
过滤与命令等 DTO。所有 DTO 均为 ``dataclass(frozen=True)``,仅依赖标准库
|
||
与契约层内部类型,用于内容审核域(CR-01 预审核 / CR-02 历史列表 /
|
||
CR-03 历史详情)的跨层传递。
|
||
|
||
设计要点:
|
||
- 4 个 ``StrEnum`` 不得用 ``@dataclass(frozen=True)`` 装饰(``StrEnum``
|
||
本身已是不可变枚举,``@dataclass`` 装饰枚举会引发 ``TypeError``)。
|
||
- 拆分 ``ContentModerationOutcome``(适配器输出)与 ``ContentReviewRecord``
|
||
(持久化记录),使 ``review_id`` 生成责任明确归属 ``dispatch_stage``
|
||
(符项目惯例:业务 ID 由编排层生成)。
|
||
- ``ContentReviewHit.position`` 为 ``[start, end)`` 字符偏移区间,基于
|
||
``content`` 字段的 0-based 字符索引,左闭右开。
|
||
- ``ContentReviewRecord.content_preview`` 长度约束 ``<= 200``,由
|
||
``dispatch_stage`` 在构造时截断(``content[:200]``),DTO 不做截断。
|
||
- ``reviewer`` 字段来源为 ``operator.user_id``,由 ``dispatch_stage``
|
||
填充。
|
||
- ``ContentReviewRecord`` 与 ``ContentReviewDetail`` 为概念对应关系
|
||
(字段重叠),非继承关系:前者为持久化记录(含 ``outcome`` 嵌套),
|
||
后者为查询返回(将 ``outcome`` 展平为 ``verdict`` / ``confidence`` /
|
||
``categories`` / ``detail`` 四个字段)。
|
||
"""
|
||
|
||
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.dtos.common import BatchOperationFailure, CategoryStat, Operator
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
|
||
|
||
class ContentReviewVerdict(StrEnum):
|
||
"""审核结论枚举。
|
||
|
||
取值:
|
||
PASS: 内容合规,可投递。
|
||
REVIEW: 存在疑似违规,需人工复核。
|
||
BLOCK: 明确违规,禁止投递。
|
||
"""
|
||
|
||
PASS = "pass"
|
||
REVIEW = "review"
|
||
BLOCK = "block"
|
||
|
||
|
||
class ContentReviewSeverity(StrEnum):
|
||
"""审核命中严重级别。
|
||
|
||
取值:
|
||
LOW: 低风险(如擦边、隐喻)。
|
||
MEDIUM: 中风险(如辱骂、不当言论)。
|
||
HIGH: 高风险(如涉政、涉黄、涉暴)。
|
||
"""
|
||
|
||
LOW = "low"
|
||
MEDIUM = "medium"
|
||
HIGH = "high"
|
||
|
||
|
||
class ContentReviewSource(StrEnum):
|
||
"""审核来源(标识审核触发路径)。
|
||
|
||
取值:
|
||
MANUAL_PREVIEW: 管理员手动预审核(本期使用)。
|
||
INBOUND_PIPELINE: 入站管道自动审核(预留,本期不使用)。
|
||
OUTBOUND_PIPELINE: 出站管道自动审核(预留,本期不使用)。
|
||
"""
|
||
|
||
MANUAL_PREVIEW = "manual_preview"
|
||
INBOUND_PIPELINE = "inbound_pipeline"
|
||
OUTBOUND_PIPELINE = "outbound_pipeline"
|
||
|
||
|
||
class ContentReviewResourceType(StrEnum):
|
||
"""审核资源类型。
|
||
|
||
取值:
|
||
MESSAGE_TEXT: 消息文本。
|
||
MESSAGE_ATTACHMENT: 消息附件(图片 / 文件 / 音视频)。
|
||
USER_PROFILE: 用户资料(昵称 / 简介 / 头像)。
|
||
"""
|
||
|
||
MESSAGE_TEXT = "message_text"
|
||
MESSAGE_ATTACHMENT = "message_attachment"
|
||
USER_PROFILE = "user_profile"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewRequest:
|
||
"""审核请求 DTO(传入 ContentModerationAdapter.review)。
|
||
|
||
不含 ``review_id``:``review_id`` 由 ``dispatch_stage`` 统一生成,
|
||
适配器仅返回审核结论(``ContentModerationOutcome``)。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
account_id: 账户 ID。
|
||
resource_type: 资源类型(ContentReviewResourceType 枚举)。
|
||
content: 待审核文本。
|
||
peer_id: 对端 ID(可选,用于上下文)。
|
||
trace_id: 链路追踪 ID(可选)。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
resource_type: ContentReviewResourceType
|
||
content: str
|
||
peer_id: str | None = None
|
||
trace_id: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewHit:
|
||
"""审核命中片段。
|
||
|
||
字段:
|
||
snippet: 命中的文本片段。
|
||
position: 字符偏移区间 ``[start, end)``,基于 ``content`` 字段
|
||
的 0-based 字符索引,左闭右开。
|
||
category: 命中分类(厂商自定义,如 ``"politics"`` / ``"violence"``)。
|
||
severity: 严重级别(ContentReviewSeverity 枚举)。
|
||
"""
|
||
|
||
snippet: str
|
||
position: tuple[int, int]
|
||
category: str
|
||
severity: ContentReviewSeverity
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentModerationOutcome:
|
||
"""审核适配器输出 DTO(ContentModerationAdapter.review 返回)。
|
||
|
||
不含 ``review_id`` / ``reviewed_at`` / ``source`` 等持久化字段:
|
||
这些字段由 ``dispatch_stage`` 统一填充,避免 adapter 实现差异。
|
||
|
||
字段:
|
||
verdict: 审核结论(ContentReviewVerdict 枚举)。
|
||
confidence: 置信度 ``[0.0, 1.0]``。
|
||
categories: 命中分类元组(无命中时为空元组)。
|
||
detail: 命中片段元组(无命中时为空元组)。
|
||
"""
|
||
|
||
verdict: ContentReviewVerdict
|
||
confidence: float
|
||
categories: tuple[str, ...]
|
||
detail: tuple[ContentReviewHit, ...]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewRecord:
|
||
"""完整审核记录(持久化与查询返回)。
|
||
|
||
由 ``dispatch_stage`` 构造:``ContentModerationOutcome`` + 上下文元数据。
|
||
作为 ``ContentReviewRepositoryPort.saveReviewResult`` 的入参。与
|
||
``ContentReviewDetail``(查询返回 DTO)为概念对应关系,字段重叠但
|
||
非继承:本类保留 ``outcome`` 嵌套结构,``ContentReviewDetail`` 将其
|
||
展平为 ``verdict`` / ``confidence`` / ``categories`` / ``detail``。
|
||
|
||
字段:
|
||
review_id: 审核记录 ID(由 ``dispatch_stage`` 生成)。
|
||
channel_type: 渠道类型。
|
||
account_id: 账户 ID。
|
||
resource_type: 资源类型。
|
||
content_preview: 内容预览(前 200 字符,由 ``dispatch_stage`` 截断)。
|
||
outcome: 审核结论(ContentModerationOutcome)。
|
||
reviewed_at: 审核时间戳(UTC,由 ``dispatch_stage`` 生成)。
|
||
reviewer: 审核人(``operator.user_id``)。
|
||
source: 审核来源(ContentReviewSource 枚举)。
|
||
trace_id: 链路追踪 ID(可选)。
|
||
"""
|
||
|
||
review_id: str
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
resource_type: ContentReviewResourceType
|
||
content_preview: str
|
||
outcome: ContentModerationOutcome
|
||
reviewed_at: datetime
|
||
reviewer: str
|
||
source: ContentReviewSource
|
||
trace_id: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewHistoryFilter:
|
||
"""审核历史查询过滤器。
|
||
|
||
字段:
|
||
channel_type: 渠道类型过滤(可选)。
|
||
account_id: 账户 ID 过滤(可选)。
|
||
verdict: 审核结论过滤(可选)。
|
||
start_time: 起始时间过滤(可选,含)。
|
||
end_time: 结束时间过滤(可选,含)。
|
||
"""
|
||
|
||
channel_type: ChannelType | None = None
|
||
account_id: str | None = None
|
||
verdict: ContentReviewVerdict | None = None
|
||
start_time: datetime | None = None
|
||
end_time: datetime | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewHistoryQueryCmd:
|
||
"""历史列表查询命令(CR-02)。
|
||
|
||
数据面查询无副作用,不写审计日志,故不携带 ``operator`` 字段
|
||
(对比 ``ContentReviewDetailQueryCmd`` 需 ``operator.request_id``
|
||
作 ``NotFoundError.trace_id``)。
|
||
|
||
字段:
|
||
channel_type: 渠道类型过滤(可选)。
|
||
account_id: 账户 ID 过滤(可选)。
|
||
verdict: 审核结论过滤(可选)。
|
||
start_time: 起始时间过滤(可选)。
|
||
end_time: 结束时间过滤(可选)。
|
||
limit: 每页数量(1-100)。
|
||
offset: 偏移量(>= 0)。
|
||
"""
|
||
|
||
channel_type: ChannelType | None
|
||
account_id: str | None
|
||
verdict: ContentReviewVerdict | None
|
||
start_time: datetime | None
|
||
end_time: datetime | None
|
||
limit: int
|
||
offset: int
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验业务规则(CR-02)。
|
||
|
||
- ``limit`` 1-100、``offset`` >= 0。
|
||
- ``start_time`` / ``end_time`` 同时提供时需满足
|
||
``start_time < end_time``(与 ``ContentReviewStatsQuery`` 同源,
|
||
避免非法范围静默返回空结果,INV-8)。
|
||
"""
|
||
if self.limit < 1 or self.limit > 100:
|
||
raise ValidationError("limit", f"limit must be 1-100, got {self.limit}")
|
||
if self.offset < 0:
|
||
raise ValidationError("offset", f"offset must be >= 0, got {self.offset}")
|
||
if self.start_time is not None and self.end_time is not None and self.start_time >= self.end_time:
|
||
raise ValidationError(
|
||
"time_range",
|
||
"start_time must be earlier than end_time",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewHistoryItem:
|
||
"""历史列表条目。
|
||
|
||
字段:
|
||
review_id: 审核记录 ID。
|
||
channel_type: 渠道类型。
|
||
account_id: 账户 ID。
|
||
resource_type: 资源类型。
|
||
verdict: 审核结论。
|
||
confidence: 置信度(``[0.0, 1.0]``)。
|
||
categories: 命中分类元组。
|
||
reviewed_at: 审核时间戳。
|
||
reviewer: 审核人。
|
||
source: 审核来源(``manual_preview`` / ``inbound_pipeline`` /
|
||
``outbound_pipeline``)。
|
||
"""
|
||
|
||
review_id: str
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
resource_type: ContentReviewResourceType
|
||
verdict: ContentReviewVerdict
|
||
confidence: float
|
||
categories: tuple[str, ...]
|
||
reviewed_at: datetime
|
||
reviewer: str
|
||
source: ContentReviewSource
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewHistoryList:
|
||
"""历史列表结果(CR-02)。
|
||
|
||
字段:
|
||
total: 匹配条目总数。
|
||
limit: 每页数量。
|
||
offset: 偏移量。
|
||
items: 条目元组。
|
||
"""
|
||
|
||
total: int
|
||
limit: int
|
||
offset: int
|
||
items: tuple[ContentReviewHistoryItem, ...]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewDetailQueryCmd:
|
||
"""详情查询命令(CR-03)。
|
||
|
||
字段:
|
||
review_id: 审核记录 ID。
|
||
operator: 操作人。
|
||
"""
|
||
|
||
review_id: str
|
||
operator: Operator
|
||
|
||
def __post_init__(self) -> None:
|
||
if not self.review_id:
|
||
raise ValidationError("review_id", "review_id must not be empty")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewDetail:
|
||
"""审核详情(CR-03 返回)。
|
||
|
||
字段:
|
||
review_id: 审核记录 ID。
|
||
channel_type: 渠道类型。
|
||
account_id: 账户 ID。
|
||
resource_type: 资源类型。
|
||
content_preview: 内容预览(前 200 字符)。
|
||
verdict: 审核结论。
|
||
confidence: 置信度。
|
||
categories: 命中分类元组。
|
||
detail: 命中片段元组。
|
||
reviewed_at: 审核时间戳。
|
||
reviewer: 审核人。
|
||
source: 审核来源。
|
||
trace_id: 链路追踪 ID(可选)。
|
||
"""
|
||
|
||
review_id: str
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
resource_type: ContentReviewResourceType
|
||
content_preview: str
|
||
verdict: ContentReviewVerdict
|
||
confidence: float
|
||
categories: tuple[str, ...]
|
||
detail: tuple[ContentReviewHit, ...]
|
||
reviewed_at: datetime
|
||
reviewer: str
|
||
source: ContentReviewSource
|
||
trace_id: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewStatsQuery:
|
||
"""审核统计查询(CR-STATS-01)。
|
||
|
||
描述审核统计的查询条件,由 ``ContentReviewRepositoryPort.getReviewStats``
|
||
消费。``granularity`` 取值 ``hour`` / ``day`` / ``week``,默认 ``day``。
|
||
|
||
字段:
|
||
channel_type: 渠道类型过滤(可选)。
|
||
account_id: 账户 ID 过滤(可选)。
|
||
start_time: 起始时间(可选,含)。
|
||
end_time: 结束时间(可选,含)。
|
||
granularity: 时间粒度(默认 ``day``)。
|
||
"""
|
||
|
||
channel_type: ChannelType | None = None
|
||
account_id: str | None = None
|
||
start_time: datetime | None = None
|
||
end_time: datetime | None = None
|
||
granularity: str = "day"
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验业务规则(CR-STATS-01)。
|
||
|
||
- ``granularity`` 必须为 ``hour`` / ``day`` / ``week``。
|
||
- ``start_time`` / ``end_time`` 同时提供时需满足
|
||
``start_time < end_time``。
|
||
|
||
在构造时即抛出 ``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}",
|
||
)
|
||
if self.start_time is not None and self.end_time is not None and self.start_time >= self.end_time:
|
||
raise ValidationError(
|
||
"time_range",
|
||
"start_time must be earlier than end_time",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ReviewTrendPoint:
|
||
"""审核趋势点(CR-STATS-01)。
|
||
|
||
描述按时间粒度切片的审核趋势数据点,含通过与拦截计数。
|
||
|
||
字段:
|
||
timestamp: 时间桶起始时间。
|
||
pass_count: 通过数。
|
||
block_count: 拦截数。
|
||
"""
|
||
|
||
timestamp: datetime
|
||
pass_count: int
|
||
block_count: int
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContentReviewStatsResult:
|
||
"""审核统计结果(CR-STATS-01)。
|
||
|
||
描述审核记录的聚合统计指标,由 ``getReviewStats`` 返回。
|
||
|
||
字段:
|
||
total_reviews: 审核总数。
|
||
pass_count: 通过数。
|
||
review_count: 待复核数。
|
||
block_count: 拦截数。
|
||
pass_rate: 通过率(0-100)。
|
||
block_rate: 拦截率(0-100)。
|
||
manual_intervention_rate: 人工介入率(0-100)。
|
||
avg_decision_seconds: 平均决策时长(秒)。
|
||
by_category: 按分类分组的统计元组。
|
||
trend: 审核趋势数据点元组。
|
||
"""
|
||
|
||
total_reviews: int
|
||
pass_count: int
|
||
review_count: int
|
||
block_count: int
|
||
pass_rate: float
|
||
block_rate: float
|
||
manual_intervention_rate: float
|
||
avg_decision_seconds: float
|
||
by_category: tuple[CategoryStat, ...]
|
||
trend: tuple[ReviewTrendPoint, ...]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ReviewDecisionItem:
|
||
"""单条审核决定条目(CR-DECISION-BATCH)。
|
||
|
||
描述批量审核决定中单条决定的输入,``decision`` 取值 ``pass`` / ``block``。
|
||
|
||
字段:
|
||
review_id: 审核记录 ID。
|
||
decision: 决定结果(``pass`` / ``block``)。
|
||
reason: 决定原因(可选)。
|
||
categories: 命中分类元组(可选,默认空元组)。
|
||
"""
|
||
|
||
review_id: str
|
||
decision: Literal["pass", "block"]
|
||
reason: str | None = None
|
||
categories: tuple[str, ...] = ()
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验 ``decision`` 取值(INV-8)。
|
||
|
||
``decision`` 必须为 ``pass`` / ``block``,在构造时即抛出
|
||
``ValidationError``,避免非法值传播到 dispatch handler 后才暴露。
|
||
|
||
注:``ContentReviewVerdict`` 枚举含 ``REVIEW``,但人工决定仅允许
|
||
``pass`` / ``block``(人工覆盖要么放行要么拦截,不存在"待复核"
|
||
语义),故不复用该枚举。
|
||
"""
|
||
if self.decision not in ("pass", "block"):
|
||
raise ValidationError(
|
||
"decision",
|
||
f"decision must be 'pass' or 'block', got {self.decision!r}",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchReviewDecisionCmd:
|
||
"""批量审核决定命令(CR-DECISION-BATCH)。
|
||
|
||
由 ``ContentReviewPort.batchReviewDecision`` 引用,``decisions`` 数量
|
||
限制 1-100,由 dispatch handler 校验。
|
||
|
||
字段:
|
||
decisions: 决定条目元组(1-100 项)。
|
||
operator: 操作人(审计用)。
|
||
apply_to_pending_messages: 是否关联处理待审消息(默认 False)。
|
||
"""
|
||
|
||
decisions: tuple[ReviewDecisionItem, ...]
|
||
operator: Operator
|
||
apply_to_pending_messages: bool = False
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验业务规则(CR-DECISION-BATCH)。
|
||
|
||
- ``decisions`` 非空。
|
||
- ``decisions`` 长度 ≤ 100。
|
||
|
||
在构造时即抛出 ``ValidationError``,避免非法值传播到 dispatch
|
||
handler 后才暴露(INV-8)。单条 ``decision`` 取值 ``pass`` / ``block``
|
||
的校验由 ``ReviewDecisionItem`` 约束,dispatch handler 二次校验。
|
||
"""
|
||
if not self.decisions:
|
||
raise ValidationError("decisions", "decisions must not be empty")
|
||
if len(self.decisions) > 100:
|
||
raise ValidationError(
|
||
"decisions",
|
||
f"decisions length must be <= 100, got {len(self.decisions)}",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchDecisionSuccessItem:
|
||
"""批量审核决定成功条目(CR-DECISION-BATCH)。
|
||
|
||
描述单条审核决定成功后的返回信息,含最终审核结论。
|
||
|
||
字段:
|
||
review_id: 审核记录 ID。
|
||
current_verdict: 当前审核结论(``pass`` / ``block``)。
|
||
"""
|
||
|
||
review_id: str
|
||
current_verdict: Literal["pass", "block"]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchDecisionResult:
|
||
"""批量审核决定结果(CR-DECISION-BATCH)。
|
||
|
||
描述逐条独立事务审核决定的执行结果,``failed`` 使用通用
|
||
``BatchOperationFailure``(``id`` 字段承载 review_id)。
|
||
|
||
字段:
|
||
total: 决定条目总数。
|
||
succeeded: 成功条目元组。
|
||
failed: 失败条目元组。
|
||
"""
|
||
|
||
total: int
|
||
succeeded: tuple[BatchDecisionSuccessItem, ...]
|
||
failed: tuple[BatchOperationFailure, ...]
|