ForcePilot/backend/package/yuxi/channels/contract/dtos/content_review.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

548 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""内容审核 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:
"""审核适配器输出 DTOContentModerationAdapter.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, ...]