2026-07-02 03:22:12 +08:00
|
|
|
|
"""Dashboard 聚合视图域 DTO。
|
|
|
|
|
|
|
|
|
|
|
|
定义跨子域 KPI 聚合视图的不可变值对象,供 dashboard_router 端点返回。
|
|
|
|
|
|
所有 DTO 均为 ``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
|
|
|
|
from yuxi.channels.contract.dtos.outbox import OutboxStats
|
2026-07-02 18:27:06 +08:00
|
|
|
|
from yuxi.channels.contract.errors import ValidationError
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class AccountStats:
|
|
|
|
|
|
"""账户统计快照(DSB-02)。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
total: 账户总数(仅含 is_deleted=0)。
|
2026-07-09 04:21:28 +08:00
|
|
|
|
by_channel: 按渠道类型分组的计数 dict(key 为 ChannelType)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
by_status: 按账户状态分组的计数 dict(key ∈ {active, disabled, degraded})。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total: int
|
|
|
|
|
|
by_channel: dict[str, int]
|
|
|
|
|
|
by_status: dict[str, int]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class MessageStats:
|
|
|
|
|
|
"""消息统计快照(DSB-03)。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
total: 消息总数(满足时间过滤条件)。
|
|
|
|
|
|
by_channel: 按渠道类型分组的计数 dict(channel_type 通过 conversation 关联填充)。
|
|
|
|
|
|
by_role: 按发送角色分组的计数 dict(key ∈ {user, assistant, system, tool})。
|
|
|
|
|
|
by_delivery_status: 按渠道侧投递状态分组的计数 dict
|
|
|
|
|
|
(key ∈ {sent, delivered, read, recalled, edited})。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total: int
|
|
|
|
|
|
by_channel: dict[str, int]
|
|
|
|
|
|
by_role: dict[str, int]
|
|
|
|
|
|
by_delivery_status: dict[str, int]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class SessionStats:
|
|
|
|
|
|
"""会话统计快照(DSB-04)。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
total: 未软删除会话总数。
|
|
|
|
|
|
by_channel: 按渠道类型分组的计数 dict。
|
|
|
|
|
|
by_is_temporary: 按是否临时会话分组的计数 dict(key 为 "true" / "false")。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total: int
|
|
|
|
|
|
by_channel: dict[str, int]
|
|
|
|
|
|
by_is_temporary: dict[str, int]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DashboardOverview:
|
|
|
|
|
|
"""全局总览快照(DSB-01)。
|
|
|
|
|
|
|
|
|
|
|
|
聚合四类子域计数,供管理后台首页看板渲染。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
accounts: 账户总览。
|
|
|
|
|
|
messages: 消息总览。
|
|
|
|
|
|
sessions: 会话总览。
|
|
|
|
|
|
outbox: 投递总览(复用 OutboxStats)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
accounts: AccountStats
|
|
|
|
|
|
messages: MessageStats
|
|
|
|
|
|
sessions: SessionStats
|
|
|
|
|
|
outbox: OutboxStats
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DashboardOverviewQuery:
|
|
|
|
|
|
"""全局总览查询(DSB-01)。
|
|
|
|
|
|
|
|
|
|
|
|
描述全局总览查询条件,按渠道类型过滤;``start_time`` / ``end_time``
|
|
|
|
|
|
仅作用于 ``messages`` 子域(消息数为时间窗口内事件),``accounts`` /
|
|
|
|
|
|
``sessions`` / ``outbox`` 为当前状态快照,不受时间范围影响。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
channel_type: 渠道类型过滤(可选)。
|
|
|
|
|
|
start_time: 消息统计起始时间(可选,须为 aware datetime)。
|
|
|
|
|
|
end_time: 消息统计截止时间(可选,须为 aware datetime)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: ChannelType | None = None
|
|
|
|
|
|
start_time: datetime | None = None
|
|
|
|
|
|
end_time: datetime | None = None
|
|
|
|
|
|
|
|
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
|
"""校验时间区间合法性与时区约束(DSB-01)。
|
|
|
|
|
|
|
|
|
|
|
|
与 ``DashboardDeliveryQuery`` 保持一致,避免区间反转与 naive
|
|
|
|
|
|
datetime 导致的数据偏移(INV-DASHBOARD-OVERVIEW-RANGE)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.start_time is not None and self.start_time.tzinfo is None:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"start_time",
|
|
|
|
|
|
"start_time must be timezone-aware datetime",
|
|
|
|
|
|
)
|
|
|
|
|
|
if self.end_time is not None and self.end_time.tzinfo is None:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"end_time",
|
|
|
|
|
|
"end_time must be timezone-aware datetime",
|
|
|
|
|
|
)
|
|
|
|
|
|
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 not be later than end_time",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DashboardDeliveryQuery:
|
|
|
|
|
|
"""投递总览查询(DSB-DELIVERY)。
|
|
|
|
|
|
|
|
|
|
|
|
描述投递总览查询条件,按渠道类型与时间范围过滤;所有字段可选,
|
|
|
|
|
|
缺省时返回全局聚合。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
channel_type: 渠道类型过滤(可选)。
|
2026-07-02 18:27:06 +08:00
|
|
|
|
start_time: 起始时间(可选,须为 aware datetime)。
|
|
|
|
|
|
end_time: 截止时间(可选,须为 aware datetime)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: ChannelType | None = None
|
|
|
|
|
|
start_time: datetime | None = None
|
|
|
|
|
|
end_time: datetime | None = None
|
|
|
|
|
|
|
2026-07-02 18:27:06 +08:00
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
|
"""校验时间区间合法性与时区约束(DSB-DELIVERY)。
|
|
|
|
|
|
|
|
|
|
|
|
- ``start_time`` / ``end_time`` 同时提供时,``start_time`` 必须
|
|
|
|
|
|
不晚于 ``end_time``,否则区间反转会导致 adapter 层 SQL 静默
|
|
|
|
|
|
返回空集(INV-DASHBOARD-DELIVERY-RANGE)。
|
|
|
|
|
|
- 任一时间字段提供时必须为 aware datetime(``tzinfo`` 非空),
|
|
|
|
|
|
与各表时间列的 UTC 存储语义对齐,避免 naive datetime 被按
|
|
|
|
|
|
本地时区解释导致偏移(INV-DASHBOARD-DELIVERY-TZ)。
|
|
|
|
|
|
|
|
|
|
|
|
校验在构造时即抛出 ``ValidationError``,避免非法值传播到
|
|
|
|
|
|
adapter 层后才暴露。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.start_time is not None and self.start_time.tzinfo is None:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"start_time",
|
|
|
|
|
|
"start_time must be timezone-aware datetime",
|
|
|
|
|
|
)
|
|
|
|
|
|
if self.end_time is not None and self.end_time.tzinfo is None:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"end_time",
|
|
|
|
|
|
"end_time must be timezone-aware datetime",
|
|
|
|
|
|
)
|
2026-07-03 19:18:13 +08:00
|
|
|
|
if self.start_time is not None and self.end_time is not None and self.start_time > self.end_time:
|
2026-07-02 18:27:06 +08:00
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"time_range",
|
|
|
|
|
|
"start_time must not be later than end_time",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class ChannelDeliveryStat:
|
|
|
|
|
|
"""渠道投递统计(DSB-DELIVERY by_channel 条目)。
|
|
|
|
|
|
|
|
|
|
|
|
描述单渠道投递计数与成功率,用于投递总览的按渠道切片展示。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
channel_type: 渠道类型字符串值。
|
|
|
|
|
|
sent: 该渠道投递总数。
|
|
|
|
|
|
success_rate: 成功率(0-100)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: str
|
|
|
|
|
|
sent: int
|
2026-07-06 20:49:35 +08:00
|
|
|
|
success_rate: float | None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DashboardDeliveryResult:
|
|
|
|
|
|
"""投递总览结果(DSB-DELIVERY)。
|
|
|
|
|
|
|
|
|
|
|
|
描述投递总览聚合结果,包含总计数、成功率、延迟分位数、队列深度、
|
|
|
|
|
|
死信数与按渠道切片。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
total_sent: 投递总数。
|
|
|
|
|
|
success_count: 成功数。
|
|
|
|
|
|
failed_count: 失败数。
|
2026-07-06 20:49:35 +08:00
|
|
|
|
success_rate: 成功率(0-100);无投递样本时为 ``None``,避免
|
|
|
|
|
|
前端将 ``0.0%`` 误解为低投递率。
|
|
|
|
|
|
avg_latency_ms: 平均延迟(毫秒);无投递样本时为 ``None``。
|
|
|
|
|
|
p95_latency_ms: P95 延迟(毫秒);无投递样本时为 ``None``。
|
|
|
|
|
|
p99_latency_ms: P99 延迟(毫秒);无投递样本时为 ``None``。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
current_queue_depth: 当前队列深度。
|
|
|
|
|
|
dead_letter_count: 死信总数。
|
|
|
|
|
|
by_channel: 按渠道切片的统计元组。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total_sent: int
|
|
|
|
|
|
success_count: int
|
|
|
|
|
|
failed_count: int
|
2026-07-06 20:49:35 +08:00
|
|
|
|
success_rate: float | None
|
|
|
|
|
|
avg_latency_ms: float | None
|
|
|
|
|
|
p95_latency_ms: float | None
|
|
|
|
|
|
p99_latency_ms: float | None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
current_queue_depth: int
|
|
|
|
|
|
dead_letter_count: int
|
|
|
|
|
|
by_channel: tuple[ChannelDeliveryStat, ...]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class DashboardTodoResult:
|
|
|
|
|
|
"""工作台待办计数聚合结果(DSB-TODOS)。
|
|
|
|
|
|
|
|
|
|
|
|
将分散在配对审批、内容审核、死信积压三个子域的待办计数聚合为
|
|
|
|
|
|
一次请求返回,确保看板角标与跳转页数据口径一致。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
pending_pairings: 待审批配对数量。
|
|
|
|
|
|
pending_reviews: 待人工复核的内容审核数量。
|
|
|
|
|
|
dead_letter_count: 当前死信积压数量(按渠道过滤时仅统计该渠道)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
pending_pairings: int
|
|
|
|
|
|
pending_reviews: int
|
|
|
|
|
|
dead_letter_count: int
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class RealtimeQuery:
|
|
|
|
|
|
"""实时监控查询(DSB-REALTIME)。
|
|
|
|
|
|
|
|
|
|
|
|
描述实时监控查询条件,按渠道类型与滑动窗口过滤;``window_seconds``
|
|
|
|
|
|
范围 10-300,默认 60。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
channel_type: 渠道类型过滤(可选)。
|
2026-07-02 18:27:06 +08:00
|
|
|
|
window_seconds: 滑动窗口秒数(默认 60,范围 10-300)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: ChannelType | None = None
|
|
|
|
|
|
window_seconds: int = 60
|
|
|
|
|
|
|
2026-07-02 18:27:06 +08:00
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
|
"""校验 ``window_seconds`` 取值范围(DSB-REALTIME)。
|
|
|
|
|
|
|
|
|
|
|
|
``window_seconds`` 必须在 ``[10, 300]`` 区间内:过小会导致
|
|
|
|
|
|
滑动窗口采样不足,过大会使内存计数器 deque 膨胀。校验在构造时
|
|
|
|
|
|
即抛出 ``ValidationError``,与 Router 层 ``Query(ge=10, le=300)``
|
|
|
|
|
|
形成双层防御,覆盖非 HTTP 调用方(如内部 API 直接构造 DTO)的
|
|
|
|
|
|
兜底场景(INV-DASHBOARD-REALTIME-WINDOW)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.window_seconds < 10 or self.window_seconds > 300:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"window_seconds",
|
|
|
|
|
|
f"window_seconds must be in [10, 300], got {self.window_seconds}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class ChannelRealtimeStat:
|
|
|
|
|
|
"""渠道实时统计(DSB-REALTIME by_channel 条目)。
|
|
|
|
|
|
|
|
|
|
|
|
描述单渠道实时消息速率与活跃会话数,用于实时监控的按渠道切片展示。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
channel_type: 渠道类型字符串值。
|
|
|
|
|
|
mps: 每秒消息数(messages per second)。
|
|
|
|
|
|
active_sessions: 活跃会话数。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
channel_type: str
|
|
|
|
|
|
mps: float
|
|
|
|
|
|
active_sessions: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class RealtimeMetricsResult:
|
|
|
|
|
|
"""实时监控结果(DSB-REALTIME)。
|
|
|
|
|
|
|
|
|
|
|
|
描述实时监控纯内存态聚合结果,包含全局消息速率、活跃会话/会话数、
|
2026-07-03 19:18:13 +08:00
|
|
|
|
队列深度、worker 利用率与按渠道切片。高频查询场景使用,业务不开启
|
|
|
|
|
|
DB 事务,审计走 INDEPENDENT 独立事务 best-effort 写入。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
window_seconds: 滑动窗口秒数。
|
|
|
|
|
|
messages_per_second: 全局每秒消息数。
|
|
|
|
|
|
active_sessions: 活跃会话总数。
|
|
|
|
|
|
active_conversations: 活跃会话总数(与 active_sessions 同义,由
|
|
|
|
|
|
不同来源聚合:sessions 来自 QueuePort,conversations 来自会话仓库)。
|
|
|
|
|
|
queue_depth: 当前队列深度。
|
2026-07-03 19:18:13 +08:00
|
|
|
|
worker_utilization: worker 利用率(0.0-1.0,与 ``WorkerStatus``
|
2026-07-06 20:49:35 +08:00
|
|
|
|
同源;ARQ 适配器当前不采集真实利用率,返回 ``None`` 表示未采集)。
|
2026-07-03 19:18:13 +08:00
|
|
|
|
by_channel: 按渠道切片的实时统计元组(全局查询时遍历已注册渠道
|
|
|
|
|
|
填充,单渠道过滤时仅含该渠道一条)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
window_seconds: int
|
|
|
|
|
|
messages_per_second: float
|
|
|
|
|
|
active_sessions: int
|
|
|
|
|
|
active_conversations: int
|
|
|
|
|
|
queue_depth: int
|
2026-07-06 20:49:35 +08:00
|
|
|
|
worker_utilization: float | None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
by_channel: tuple[ChannelRealtimeStat, ...]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
|
|
"AccountStats",
|
|
|
|
|
|
"ChannelDeliveryStat",
|
|
|
|
|
|
"ChannelRealtimeStat",
|
|
|
|
|
|
"DashboardDeliveryQuery",
|
|
|
|
|
|
"DashboardDeliveryResult",
|
|
|
|
|
|
"DashboardOverview",
|
2026-07-06 20:49:35 +08:00
|
|
|
|
"DashboardTodoResult",
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"MessageStats",
|
|
|
|
|
|
"RealtimeMetricsResult",
|
|
|
|
|
|
"RealtimeQuery",
|
|
|
|
|
|
"SessionStats",
|
|
|
|
|
|
]
|