"""聚合视图域 analytics 子域契约 DTO。 定义聚合视图域 analytics 子域 9 个跨子域深度分析端点(ANL-01/02/04/05/06/07 /ACCOUNTS/PEERS/CR)返回的不可变值对象。所有 DTO 均为 ``dataclass(frozen=True)``, 仅依赖标准库,用于按时间 / 类型 / 状态切片的细分聚合展示,供管理后台运营审计与 运维排障使用。 与 dashboard 子域的区分:dashboard 提供总览计数,analytics 提供按维度切片的 细分聚合;与单子域 stats(如 OBX-02 outbox stats)的区分:单子域统计归各子域 router,本域仅承载跨子域深度分析视图。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Any, Literal from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.dtos.common import CategoryStat, TrendDataPoint from yuxi.channels.contract.errors import ValidationError from yuxi.utils.datetime_utils import format_utc_datetime @dataclass(frozen=True) class TimeSeriesPoint: """时间序列数据点。 描述按时间粒度切片后的单个时间桶计数,供消息与会话分析的趋势展示使用。 字段: timestamp: 时间桶起始时间(ISO 8601 字符串)。 value: 计数值。 """ timestamp: str value: int def to_dict(self) -> dict[str, Any]: return {"timestamp": self.timestamp, "value": self.value} @dataclass(frozen=True) class MessageAnalytics: """消息深度分析结果(ANL-01)。 描述时间范围内消息量按时间 / 渠道 / 角色 / 投递状态切片的细分聚合,由 ``getMessageAnalytics`` 返回,供运营审计按多维切片定位消息量趋势。 字段: total: 时间范围内消息总数。 timeseries: 按时间粒度切片的消息量趋势。 by_channel: 按渠道类型分组的计数 dict(key 为 channel_type 字符串值)。 by_role: 按发送角色分组的计数 dict(key 为 user / assistant / system / tool)。 by_delivery_status: 按投递状态分组的计数 dict。 """ total: int timeseries: tuple[TimeSeriesPoint, ...] by_channel: dict[str, int] by_role: dict[str, int] by_delivery_status: dict[str, int] def to_dict(self) -> dict[str, Any]: """序列化为 dict,递归处理嵌套 ``TimeSeriesPoint`` 为 dict 列表。""" return { "total": self.total, "timeseries": [point.to_dict() for point in self.timeseries], "by_channel": self.by_channel, "by_role": self.by_role, "by_delivery_status": self.by_delivery_status, } @dataclass(frozen=True) class MessageDistribution: """消息类型分布结果(ANL-02)。 描述时间范围内消息按类型分组的计数与占比,由 ``getMessageDistribution`` 返回, 供内容运营按类型(text / card / attachment / mixed)定位消息构成。 字段: total: 时间范围内消息总数。 by_type: 按消息类型分组的计数 dict。 by_type_percent: 按消息类型分组的占比 dict(0-100)。 """ total: int by_type: dict[str, int] by_type_percent: dict[str, float] def to_dict(self) -> dict[str, Any]: """序列化为 dict,供控制面结果返回与日志输出使用。""" return { "total": self.total, "by_type": self.by_type, "by_type_percent": self.by_type_percent, } @dataclass(frozen=True) class SessionAnalytics: """会话分析结果(ANL-04)。 描述时间范围内会话量按时间 / 渠道 / 临时性 / 消息数区间切片的细分聚合,由 ``getSessionAnalytics`` 返回,供运营审计按多维切片定位会话量趋势。 字段: total: 时间范围内活跃会话总数。 timeseries: 按时间粒度切片的会话量趋势。 by_channel: 按渠道类型分组的计数 dict(key 为 channel_type 字符串值)。 by_is_temporary: 按是否临时会话分组的计数 dict(key 为 "true" / "false")。 by_message_count_bucket: 按消息数区间分组的计数 dict (key 为 "0-10" / "11-50" / "51-100" / "100+")。 """ total: int timeseries: tuple[TimeSeriesPoint, ...] by_channel: dict[str, int] by_is_temporary: dict[str, int] by_message_count_bucket: dict[str, int] def to_dict(self) -> dict[str, Any]: """序列化为 dict,递归处理嵌套 ``TimeSeriesPoint`` 为 dict 列表。""" return { "total": self.total, "timeseries": [point.to_dict() for point in self.timeseries], "by_channel": self.by_channel, "by_is_temporary": self.by_is_temporary, "by_message_count_bucket": self.by_message_count_bucket, } @dataclass(frozen=True) class DeliveryAnalytics: """投递链路分析结果(ANL-05)。 与现有 ``OutboxStats``(按状态分组计数)的关系:``DeliveryAnalytics`` 是 ``OutboxStats`` 的扩展,新增 success_rate / failure_rate / retry_distribution / avg_latency_ms 四个聚合维度,由 ``getDeliveryAnalytics`` 返回,供运维排障定位投递成功率与重试分布。 字段: total: 时间范围内投递条目总数。 success_rate: 投递成功率(0-100)。 failure_rate: 投递失败率(0-100)。 retry_distribution: 按重试次数分组的计数 dict (key 为 "0" / "1" / "2" / "3+" / "max_reached")。 avg_latency_ms: 平均投递延迟(毫秒),无 latency_ms 数据时为 None。 """ total: int success_rate: float failure_rate: float retry_distribution: dict[str, int] avg_latency_ms: float | None def to_dict(self) -> dict[str, Any]: """序列化为 dict,``avg_latency_ms`` 为 None 时返回 None。""" return { "total": self.total, "success_rate": self.success_rate, "failure_rate": self.failure_rate, "retry_distribution": self.retry_distribution, "avg_latency_ms": self.avg_latency_ms, } @dataclass(frozen=True) class DeliveryLatencyDistribution: """投递延迟分布结果(ANL-06)。 描述投递延迟的分位数与直方图分布,由 ``getDeliveryLatencyDistribution`` 返回,供运维排障定位长尾延迟。所有分位数字段无 latency_ms 数据时为 None。 字段: p50_ms: P50 延迟(毫秒),无数据时为 None。 p90_ms: P90 延迟(毫秒),无数据时为 None。 p99_ms: P99 延迟(毫秒),无数据时为 None。 max_ms: 最大延迟(毫秒),无数据时为 None。 histogram: 按延迟区间分组的计数 dict (key 为 "0-100ms" / "100-500ms" / "500ms-1s" / "1-5s" / "5s+")。 """ p50_ms: float | None p90_ms: float | None p99_ms: float | None max_ms: float | None histogram: dict[str, int] def to_dict(self) -> dict[str, Any]: """序列化为 dict,分位数字段为 None 时返回 None。""" return { "p50_ms": self.p50_ms, "p90_ms": self.p90_ms, "p99_ms": self.p99_ms, "max_ms": self.max_ms, "histogram": self.histogram, } @dataclass(frozen=True) class DeliveryFunnel: """投递漏斗分析结果(ANL-07)。 描述投递管道各节点的条目数与相对 enter 的转化率,由 ``getDeliveryFunnel`` 返回,供运维排障定位投递链路损耗节点。 字段: enter: 进入投递管道的条目数。 sent: 投递成功数。 suppressed: 被抑制数。 failed: 失败数。 dead: 死信数。 conversion_rate: 各节点相对 enter 的转化率 dict (key 为 "sent" / "suppressed" / "failed" / "dead",value 为 0-100)。 """ enter: int sent: int suppressed: int failed: int dead: int conversion_rate: dict[str, float] def to_dict(self) -> dict[str, Any]: """序列化为 dict,供控制面结果返回与日志输出使用。""" return { "enter": self.enter, "sent": self.sent, "suppressed": self.suppressed, "failed": self.failed, "dead": self.dead, "conversion_rate": self.conversion_rate, } @dataclass(frozen=True) class AccountAnalyticsQuery: """账户活跃度分析查询(ANL-ACCOUNTS)。 描述账户活跃度分析查询条件,按时间范围(必填)与渠道类型过滤, 支持按粒度切片。 字段: start_time: 起始时间(必填)。 end_time: 截止时间(必填)。 granularity: 时间粒度(默认 day,可选 hour/day/week)。 channel_type: 渠道类型过滤(可选)。 """ start_time: datetime end_time: datetime granularity: Literal["hour", "day", "week"] = "day" channel_type: ChannelType | None = None def __post_init__(self) -> None: """校验时间范围与粒度取值。 ``start_time`` 必须早于 ``end_time``,``granularity`` 必须为 ``hour`` / ``day`` / ``week`` 之一,在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。 """ if self.start_time >= self.end_time: raise ValidationError( "time_range", "start_time must be earlier than end_time", ) if self.granularity not in ("hour", "day", "week"): raise ValidationError( "granularity", "must be one of: hour, day, week", ) @dataclass(frozen=True) class AccountActivityStat: """账户活跃统计(ANL-ACCOUNTS by_account 条目)。 描述单账户在时间范围内的活跃度指标,由 MessageRepositoryPort 与 ChannelSessionRepositoryPort 聚合计算。 字段: account_id: 账户 ID。 channel_type: 渠道类型字符串值。 message_count: 消息总数。 session_count: 会话总数。 active_days: 活跃天数。 avg_daily_messages: 日均消息数。 """ account_id: str channel_type: str message_count: int session_count: int active_days: int avg_daily_messages: float def to_dict(self) -> dict[str, Any]: """序列化为 dict,供控制面结果返回与日志输出使用。""" return { "account_id": self.account_id, "channel_type": self.channel_type, "message_count": self.message_count, "session_count": self.session_count, "active_days": self.active_days, "avg_daily_messages": self.avg_daily_messages, } @dataclass(frozen=True) class AccountAnalyticsResult: """账户活跃度分析结果(ANL-ACCOUNTS)。 描述账户活跃度分析聚合结果,包含按账户切片的统计与活跃账户数趋势。 ``trend`` 复用通用 ``TrendDataPoint``,``value`` 承载该时间桶的活跃账户数, 序列化时由 ``to_dict`` 映射为 ``active_accounts`` 字段名;``timestamp`` 为 ``datetime``,序列化为 ISO 8601 UTC 字符串。 字段: by_account: 按账户切片的统计元组。 trend: 活跃账户数趋势元组(value=active_accounts)。 """ by_account: tuple[AccountActivityStat, ...] trend: tuple[TrendDataPoint, ...] def to_dict(self) -> dict[str, Any]: """序列化为 dict。 - ``by_account`` 递归调用 ``AccountActivityStat.to_dict``。 - ``trend`` 将 ``TrendDataPoint.timestamp``(datetime)格式化为 ISO 8601 UTC 字符串,并将 ``value`` 映射为 ``active_accounts`` 字段名。 """ return { "by_account": [stat.to_dict() for stat in self.by_account], "trend": [ { "timestamp": format_utc_datetime(point.timestamp), "active_accounts": point.value, } for point in self.trend ], } @dataclass(frozen=True) class PeerAnalyticsQuery: """对端活跃度分析查询(ANL-PEERS)。 描述对端活跃度分析查询条件,按时间范围(必填)与渠道类型/账户过滤, 返回按消息数降序的 Top N 对端。 字段: start_time: 起始时间(必填)。 end_time: 截止时间(必填)。 channel_type: 渠道类型过滤(可选)。 account_id: 账户 ID 过滤(可选)。 limit: Top N 上限(默认 100,1-500)。 """ start_time: datetime end_time: datetime channel_type: ChannelType | None = None account_id: str | None = None limit: int = 100 def __post_init__(self) -> None: """校验时间范围与 limit 取值。 ``start_time`` 必须早于 ``end_time``,``limit`` 必须在 1-500 之间, 在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。 """ if self.start_time >= self.end_time: raise ValidationError( "time_range", "start_time must be earlier than end_time", ) if self.limit < 1 or self.limit > 500: raise ValidationError( "limit", "limit must be in [1, 500]", ) @dataclass(frozen=True) class PeerActivityStat: """对端活跃统计(ANL-PEERS top_active 条目)。 描述单对端在时间范围内的活跃度指标,由 MessageRepositoryPort 聚合计算。 字段: peer_id: 对端 ID。 channel_type: 渠道类型字符串值。 message_count: 消息总数。 session_count: 会话总数。 first_seen: 首次出现时间(可选,无数据时为 None)。 last_seen: 最后出现时间(可选,无数据时为 None)。 """ peer_id: str channel_type: str message_count: int session_count: int first_seen: datetime | None last_seen: datetime | None def to_dict(self) -> dict[str, Any]: """序列化为 dict。 ``first_seen`` / ``last_seen`` 为 ``datetime``,序列化为 ISO 8601 UTC 字符串;为 ``None`` 时原样返回 ``None``。 """ return { "peer_id": self.peer_id, "channel_type": self.channel_type, "message_count": self.message_count, "session_count": self.session_count, "first_seen": format_utc_datetime(self.first_seen), "last_seen": format_utc_datetime(self.last_seen), } @dataclass(frozen=True) class PeerAnalyticsResult: """对端活跃度分析结果(ANL-PEERS)。 描述对端活跃度分析聚合结果,按 ``message_count`` 降序返回 Top N 对端。 字段: top_active: 按消息数降序的对端统计元组。 """ top_active: tuple[PeerActivityStat, ...] def to_dict(self) -> dict[str, Any]: """序列化为 dict,递归调用 ``PeerActivityStat.to_dict``。""" return { "top_active": [stat.to_dict() for stat in self.top_active], } @dataclass(frozen=True) class ContentReviewAnalyticsQuery: """内容审核分析查询(ANL-CR)。 描述内容审核分析查询条件,按时间范围(必填)与渠道类型过滤, 支持按粒度切片。 字段: start_time: 起始时间(必填)。 end_time: 截止时间(必填)。 granularity: 时间粒度(默认 day,可选 hour/day/week)。 channel_type: 渠道类型过滤(可选)。 """ start_time: datetime end_time: datetime granularity: Literal["hour", "day", "week"] = "day" channel_type: ChannelType | None = None def __post_init__(self) -> None: """校验时间范围与粒度取值。 ``start_time`` 必须早于 ``end_time``,``granularity`` 必须为 ``hour`` / ``day`` / ``week`` 之一,在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。 """ if self.start_time >= self.end_time: raise ValidationError( "time_range", "start_time must be earlier than end_time", ) if self.granularity not in ("hour", "day", "week"): raise ValidationError( "granularity", "must be one of: hour, day, week", ) @dataclass(frozen=True) class ContentReviewAnalyticsResult: """内容审核分析结果(ANL-CR)。 描述内容审核分析聚合结果,包含总审核/拦截计数、拦截率、按分类切片与 审核趋势。``by_category`` 复用通用 ``CategoryStat``,序列化时由 ``to_dict`` 按 ``count / total_reviews * 100`` 计算并补充 ``rate`` 字段; ``trend`` 为多值时间序列(``reviewed`` / ``blocked`` 双计数),无法用单一 ``value`` 字段承载,故使用 ``dict[str, Any]`` 元组,由适配器层按 ``{timestamp, reviewed, blocked}`` 结构组装,序列化时将 ``timestamp`` (datetime)格式化为 ISO 8601 UTC 字符串。 字段命名与 ``ContentReviewStatsResult`` 对齐:``total_reviews`` / ``block_count`` 共享同一命名约定(``total_X`` 表示总数,``X_count`` 表示分类计数)。 字段: total_reviews: 审核总数。 block_count: 拦截数。 block_rate: 拦截率(0-100)。 by_category: 按分类切片的统计元组(rate 由 to_dict 补充)。 trend: 审核趋势元组(每项含 timestamp/reviewed/blocked)。 """ total_reviews: int block_count: int block_rate: float by_category: tuple[CategoryStat, ...] trend: tuple[dict[str, Any], ...] def to_dict(self) -> dict[str, Any]: """序列化为 dict。 - ``by_category`` 递归展开 ``CategoryStat``,并按 ``count / total_reviews * 100`` 计算补充 ``rate`` 字段(``total_reviews`` 为 0 时 ``rate=0.0``)。 - ``trend`` 将每项的 ``timestamp``(datetime)格式化为 ISO 8601 UTC 字符串, 保留 ``reviewed`` / ``blocked`` 双计数。 """ return { "total_reviews": self.total_reviews, "block_count": self.block_count, "block_rate": self.block_rate, "by_category": [ { "category": stat.category, "count": stat.count, "rate": (stat.count / self.total_reviews * 100) if self.total_reviews > 0 else 0.0, } for stat in self.by_category ], "trend": [ { "timestamp": format_utc_datetime(item["timestamp"]), "reviewed": item["reviewed"], "blocked": item["blocked"], } for item in self.trend ], } __all__ = [ "AccountActivityStat", "AccountAnalyticsQuery", "AccountAnalyticsResult", "ContentReviewAnalyticsQuery", "ContentReviewAnalyticsResult", "DeliveryAnalytics", "DeliveryFunnel", "DeliveryLatencyDistribution", "MessageAnalytics", "MessageDistribution", "PeerActivityStat", "PeerAnalyticsQuery", "PeerAnalyticsResult", "SessionAnalytics", "TimeSeriesPoint", ]