"""会话 DTO。 定义渠道会话相关的不可变值对象,包括渠道会话 ID、对端 ID、会话类型、 会话所有者、所有者转移命令与临时会话模式。所有 DTO 均为 ``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型,用于会话 解析、所有者管理与临时会话识别。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import StrEnum from typing import Any, Literal from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator from yuxi.channels.contract.errors import ValidationError @dataclass(frozen=True) class ChannelSessionId: """渠道会话 ID。 标识渠道侧一次会话的唯一 ID,用于会话定位与路由匹配。 字段: value: 渠道会话 ID 字符串。 """ value: str @dataclass(frozen=True) class PeerId: """对端 ID。 标识渠道侧会话对端(用户或群组)的唯一 ID,用于会话解析与身份关联。 字段: value: 对端 ID 字符串。 """ value: str class ChatType(StrEnum): """会话类型。 标识渠道侧会话的拓扑类型,用于路由匹配与会话所有者策略。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: P2P: 单聊(点对点)。 GROUP: 群聊。 KF: 客服会话(微信客服等渠道的客服窗口,独立于群聊)。 """ P2P = "p2p" GROUP = "group" KF = "kf" @dataclass(frozen=True) class SessionOwner: """会话所有者。 描述会话的所有者信息,用于 FR-26 所有者保护策略,确保仅所有者可触发 关键操作。 字段: conversation_id: 所属会话 ID。 owner_peer_id: 所有者对端 ID。 created_at: 所有者关系建立时间。 """ conversation_id: str owner_peer_id: str created_at: datetime @dataclass(frozen=True) class OwnerTransferCmd: """所有者转移命令(FR-26)。 由端口方法 ``ConversationPort.transferSessionOwner`` 引用,用于将会话 所有者从当前对端转移至新对端,需记录操作人以满足审计要求(FR-26)。 字段: conversation_id: 目标会话 ID。 new_owner_id: 新所有者对端 ID。 operator: 操作人(审计用)。 """ conversation_id: str new_owner_id: str operator: Operator def __post_init__(self) -> None: """校验必填字段非空(FR-26)。 ``conversation_id`` 与 ``new_owner_id`` 必须非空,在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。 """ if not self.conversation_id: raise ValidationError("conversation_id", "must not be empty") if not self.new_owner_id: raise ValidationError("new_owner_id", "must not be empty") @dataclass(frozen=True) class TemporarySessionPattern: """临时会话模式。 描述临时会话的匹配模式(FR-27),用于识别周期性任务产生的临时会话, 支持 cron 等模式表达式。 字段: pattern: 模式表达式(如 ``cron:<任务 ID>:<运行 ID>``)。 description: 模式描述。 """ pattern: str description: str @dataclass(frozen=True) class CloseSessionCmd: """关闭会话命令(FR-27)。 由 ``SessionManagementPort.closeSession`` 端口方法引用,用于关闭指定 渠道会话,停止接收新消息。需记录操作人以满足审计要求。 字段: session_id: 要关闭的会话 ID。 reason: 关闭原因(审计用,可选)。 operator: 操作人(审计用)。 """ session_id: str operator: Operator reason: str | None = None def __post_init__(self) -> None: """校验必填字段非空。 ``session_id`` 必须非空,在构造时即抛出 ``ValidationError``, adapter 不再做该校验(INV-8)。 """ if not self.session_id: raise ValidationError("session_id", "must not be empty") @dataclass(frozen=True) class SessionMessageItem: """会话消息列表项。 描述渠道会话消息列表中的单条消息,包含消息核心字段与渠道侧状态。 字段: message_id: 消息 ID。 conversation_id: 会话 ID。 channel_type: 渠道类型。 role: 消息角色(user | assistant | admin)。 content: 消息内容。 channel_msg_id: 渠道侧消息 ID(可选)。 channel_status: 渠道侧消息状态(可选)。 created_at: 创建时间。 """ message_id: str conversation_id: str channel_type: str role: Literal["user", "assistant", "admin"] content: str created_at: datetime channel_msg_id: str | None = None channel_status: str | None = None @dataclass(frozen=True) class SessionStatsResult: """会话统计结果(SES-STATS-01)。 描述单会话的统计指标,由 dispatch handler 组合 ConversationPort 消息 查询后计算,不在核心层引入统计聚合根。 字段: message_count: 消息总数。 user_message_count: 用户消息数。 assistant_message_count: 助手消息数。 started_at: 会话开始时间。 last_activity_at: 最近活动时间。 duration_seconds: 会话时长(秒)。 first_response_seconds: 首条用户消息到首条助手消息的间隔(秒, 无助手消息时为 None)。 avg_response_seconds: 平均响应间隔(秒,无助手消息时为 None)。 """ message_count: int user_message_count: int assistant_message_count: int started_at: datetime last_activity_at: datetime duration_seconds: int first_response_seconds: int | None = None avg_response_seconds: float | None = None @dataclass(frozen=True) class BatchCloseSessionsCmd: """批量关闭会话命令(SES-BATCH-CLOSE-01)。 由 ``SessionManagementPort.batchCloseSessions`` 引用,支持显式 ID 列表 或筛选条件两种模式(二者不可同时空)。``session_ids`` 与 ``filter`` 同时为空、``max_count`` 越界均在 ``__post_init__`` 中校验,adapter 不再 重复校验(INV-8)。 字段: session_ids: 显式会话 ID 元组(默认空元组)。 filter: 筛选条件(含 channel_type / inactive_before / status, 可选)。 max_count: 单次最大关闭数(默认 100,1-1000)。 reason: 关闭原因(审计用,可选)。 operator: 操作人(审计用)。 """ operator: Operator session_ids: tuple[str, ...] = () filter: dict[str, Any] | None = None max_count: int = 100 reason: str | None = None def __post_init__(self) -> None: """校验必填字段与业务规则(SES-BATCH-CLOSE-01)。 - ``session_ids`` 与 ``filter`` 不可同时为空 - ``max_count`` ∈ [1, 1000] 在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。 """ if not self.session_ids and not self.filter: raise ValidationError( "session_ids", "either session_ids or filter must be provided", ) if self.max_count < 1 or self.max_count > 1000: raise ValidationError( "max_count", f"max_count must be in [1, 1000], got {self.max_count}", ) @dataclass(frozen=True) class BatchCloseResult: """批量关闭会话结果(SES-BATCH-CLOSE-01)。 描述逐条独立事务关闭会话的执行结果,``failed`` 使用通用 ``BatchOperationFailure``(``id`` 字段承载 session_id)。 字段: total: 待关闭会话总数。 closed: 成功关闭的会话 ID 元组。 failed: 失败条目元组。 """ total: int closed: tuple[str, ...] failed: tuple[BatchOperationFailure, ...]