本次提交包含多项代码优化与规范修正: 1. 文档与注释优化:修正注释术语、补充注解与FR编号 2. 代码格式调整:统一空格、换行与缩进规范 3. 类型与接口完善:补充__all__导出、修正返回类型注解 4. 错误处理增强:新增领域错误类与校验逻辑 5. 依赖与导入调整:修复路径引用、统一时区导入 6. 协议与契约更新:完善接口文档与一致性注解
310 lines
12 KiB
Python
310 lines
12 KiB
Python
"""会话命令 DTO。
|
||
|
||
定义会话端口方法引用的命令与结果值对象,包括会话 ID、消息 ID、保存消息
|
||
命令、解析会话命令、关联会话命令、合并会话命令与合并结果。所有 DTO 均为
|
||
``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型,用于会话端口
|
||
的命令传递与结果返回。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
from typing import Any, Literal
|
||
|
||
from yuxi.channels.contract.dtos.channel import ChannelType
|
||
from yuxi.channels.contract.dtos.common import Operator
|
||
from yuxi.channels.contract.dtos.session import ChatType
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConversationId:
|
||
"""会话 ID。
|
||
|
||
标识内部会话的唯一 ID,用于会话定位、消息持久化与路由绑定。
|
||
|
||
字段:
|
||
value: 会话 ID 字符串。
|
||
"""
|
||
|
||
value: str
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class MessageId:
|
||
"""消息 ID。
|
||
|
||
标识一条消息的唯一 ID,用于消息持久化、状态同步与审计关联。
|
||
|
||
字段:
|
||
value: 消息 ID 字符串。
|
||
"""
|
||
|
||
value: str
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SaveMessageCmd:
|
||
"""保存消息命令(FR-09)。
|
||
|
||
由端口方法 ``ConversationPort.saveMessage`` 引用,用于将一条消息持久化
|
||
至指定会话,携带角色、内容、渠道侧消息 ID、追踪 ID 与渠道侧初始状态
|
||
字段。渠道侧初始状态字段用于出站消息创建时一次性填充渠道扩展字段
|
||
(FR-09),后续状态变更走 ``ConversationPort.updateMessageChannelStatus``。
|
||
|
||
字段:
|
||
conversation_id: 目标会话 ID。
|
||
role: 角色(user | assistant | admin)。
|
||
content: 消息文本内容。
|
||
channel_msg_id: 渠道侧消息 ID(可选)。
|
||
trace_id: 追踪 ID(可选)。
|
||
channel_status: 渠道侧初始状态(可选,如 ``sent``)。
|
||
ref_channel_msg_id: 引用的渠道消息 ID(可选,编辑/回复场景)。
|
||
channel_status_history: 初始状态历史数组(可选)。
|
||
"""
|
||
|
||
conversation_id: str
|
||
role: Literal["user", "assistant", "admin"]
|
||
content: str
|
||
channel_msg_id: str | None = None
|
||
trace_id: str | None = None
|
||
channel_status: Literal["sent", "delivered", "read", "recalled", "edited"] | None = None
|
||
ref_channel_msg_id: str | None = None
|
||
channel_status_history: tuple[dict, ...] | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空与角色取值。
|
||
|
||
``conversation_id`` 与 ``content`` 必须非空,``role`` 必须为
|
||
``user`` / ``assistant`` / ``admin`` 之一,在构造时即抛出
|
||
``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.conversation_id:
|
||
raise ValidationError("conversation_id", "must not be empty")
|
||
if not self.content:
|
||
raise ValidationError("content", "must not be empty")
|
||
if self.role not in ("user", "assistant", "admin"):
|
||
raise ValidationError(
|
||
"role",
|
||
"must be one of: user, assistant, admin",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ResolveConversationCmd:
|
||
"""解析会话命令(FR-06)。
|
||
|
||
由端口方法 ``ConversationPort.resolveConversation`` 引用,根据对端 ID、
|
||
渠道类型、账户 ID 与会话类型解析或创建会话,可选携带统一身份 ID。
|
||
|
||
字段:
|
||
peer_id: 对端 ID。
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账户 ID。
|
||
chat_type: 会话类型。
|
||
unified_identity_id: 统一身份 ID(可选)。
|
||
非空且策略为"关联"时,适配器按此 ID 查询已有跨渠道会话,
|
||
命中则复用,未命中则创建新会话并写入该 ID(FR-06)。
|
||
create_if_not_found: 未找到会话时是否创建新会话(默认 False)。
|
||
为 True 时适配器在未找到已绑定会话的情况下创建新 Conversation
|
||
并返回其 ID,不再抛出 NotFoundError(FR-06)。
|
||
user_id: 真实用户 ID(可选,FR-06 语义变更)。
|
||
身份解析到真实用户时传入,创建新会话时写入 Conversation.uid;
|
||
为空时由 ``__post_init__`` 回退到 ``peer_id``,构造完成后保证非空(FR-06 §7.1.2)。
|
||
new_conversation_status: 新建会话状态(可选,默认 ``"active"``)。
|
||
由应用层调 ``Conversation.create()`` 聚合根工厂填充,适配器
|
||
直接消费不再 import core.model(§4.6 / INV-8)。
|
||
new_conversation_title: 新建会话标题(可选,默认 ``"New Conversation"``)。
|
||
new_conversation_agent_id: 新建会话绑定代理 ID(可选,默认空字符串)。
|
||
new_conversation_extra_metadata: 新建会话附加元数据(可选,默认
|
||
``{"attachments": []}``)。
|
||
"""
|
||
|
||
peer_id: str
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
chat_type: ChatType
|
||
unified_identity_id: str | None = None
|
||
create_if_not_found: bool = False
|
||
user_id: str | None = None
|
||
new_conversation_status: str = "active"
|
||
new_conversation_title: str = "New Conversation"
|
||
new_conversation_agent_id: str = ""
|
||
new_conversation_extra_metadata: dict[str, Any] = field(default_factory=lambda: {"attachments": []})
|
||
|
||
def __post_init__(self) -> None:
|
||
"""应用 FR-06 §7.1.2 身份回退规则。
|
||
|
||
``user_id`` 为 None 时回退到 ``peer_id``,构造完成后保证非空。
|
||
适配器直接消费 ``cmd.user_id``,不再做条件分支(INV-8)。
|
||
"""
|
||
if self.user_id is None:
|
||
object.__setattr__(self, "user_id", self.peer_id)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AssociateConversationCmd:
|
||
"""关联会话命令(FR-06)。
|
||
|
||
由端口方法 ``ConversationPort.associateConversation`` 引用,将统一身份
|
||
与路由绑定关联至指定渠道会话,支持身份级路由匹配。
|
||
|
||
字段:
|
||
unified_identity_id: 统一身份 ID。
|
||
route_binding: 路由绑定信息。
|
||
channel_session_id: 渠道会话 ID(可选)。
|
||
"""
|
||
|
||
unified_identity_id: str
|
||
route_binding: dict[str, Any]
|
||
channel_session_id: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空。
|
||
|
||
``unified_identity_id`` 必须非空,在构造时即抛出
|
||
``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.unified_identity_id:
|
||
raise ValidationError("unified_identity_id", "must not be empty")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class MergeConversationCmd:
|
||
"""合并会话命令(FR-07)。
|
||
|
||
由端口方法 ``ConversationPort.mergeConversations`` 引用,将源会话合并
|
||
至目标会话,需记录操作人与合并原因以满足审计要求。
|
||
|
||
字段:
|
||
source_conversation_id: 源会话 ID。
|
||
target_conversation_id: 目标会话 ID。
|
||
operator: 操作人(审计用)。
|
||
reason: 合并原因(审计用)。
|
||
"""
|
||
|
||
source_conversation_id: str
|
||
target_conversation_id: str
|
||
operator: Operator
|
||
reason: str
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验源/目标会话不相同(FR-07)。
|
||
|
||
源与目标相同将导致消息迁移至自身、源会话被软删除的逻辑矛盾,
|
||
在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if self.source_conversation_id == self.target_conversation_id:
|
||
raise ValidationError(
|
||
"target_conversation_id",
|
||
"source conversation and target conversation must not be the same",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class MergeResult:
|
||
"""合并结果(FR-07)。
|
||
|
||
描述会话合并操作的执行结果,包括迁移消息数量、源会话软删除标记与
|
||
审计日志 ID,用于结果聚合与审计追溯。
|
||
|
||
字段:
|
||
migrated_message_count: 迁移的消息数量。
|
||
source_soft_deleted: 源会话是否已软删除。
|
||
audit_log_id: 审计日志 ID(可选)。
|
||
"""
|
||
|
||
migrated_message_count: int
|
||
source_soft_deleted: bool
|
||
audit_log_id: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class UpdateMessageChannelStatusCmd:
|
||
"""渠道侧消息状态回写命令(FR-09)。
|
||
|
||
由端口方法 ``ConversationPort.updateMessageChannelStatus`` 引用,用于
|
||
渠道回调或状态回写阶段更新消息的渠道侧状态。命令携带目标消息 ID、新
|
||
状态、追加事件条目与按状态填充的时间戳。
|
||
|
||
状态变更语义:
|
||
- ``channel_status`` 取值:``sent`` / ``delivered`` / ``read`` /
|
||
``recalled`` / ``edited``。
|
||
- ``event`` 追加到 ``channel_status_history`` 数组,应包含 ``status`` /
|
||
``at`` 等字段。
|
||
- 时间戳字段按状态填充:``read`` → ``read_at``,``recalled`` →
|
||
``recalled_at``,``edited`` → ``edited_at``。
|
||
- ``channel_msg_id`` 为可选字段,用于"持久化记录创建消息"场景(FR-09
|
||
增强):当通过 OutboxEntry 反查到 Message 但 Message 的
|
||
``channel_msg_id`` 未初始化时,由状态处理器传入以补全字段。
|
||
|
||
字段:
|
||
message_id: 目标消息 ID。
|
||
channel_status: 新的渠道状态。
|
||
event: 追加到 ``channel_status_history`` 的事件条目。
|
||
read_at: 已读时间(可选,``channel_status='read'`` 时填充)。
|
||
recalled_at: 撤回时间(可选,``channel_status='recalled'`` 时填充)。
|
||
edited_at: 编辑时间(可选,``channel_status='edited'`` 时填充)。
|
||
channel_msg_id: 渠道侧消息 ID(可选,用于补全 Message 的 channel_msg_id 字段)。
|
||
trace_id: 追踪 ID(可选,DTO 层透传,用于状态回写链路追踪,6-P0-03)。
|
||
"""
|
||
|
||
message_id: str
|
||
channel_status: Literal["sent", "delivered", "read", "recalled", "edited"]
|
||
event: dict[str, Any]
|
||
read_at: datetime | None = None
|
||
recalled_at: datetime | None = None
|
||
edited_at: datetime | None = None
|
||
channel_msg_id: str | None = None
|
||
trace_id: str | None = None # 追踪 ID,用于状态回写链路追踪(DTO 层透传)
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空与渠道状态取值(FR-09)。
|
||
|
||
``message_id`` 必须非空,``channel_status`` 必须为 ``sent`` /
|
||
``delivered`` / ``read`` / ``recalled`` / ``edited`` 之一,在构造
|
||
时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.message_id:
|
||
raise ValidationError("message_id", "must not be empty")
|
||
if self.channel_status not in (
|
||
"sent",
|
||
"delivered",
|
||
"read",
|
||
"recalled",
|
||
"edited",
|
||
):
|
||
raise ValidationError(
|
||
"channel_status",
|
||
"must be one of: sent, delivered, read, recalled, edited",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AppendOperationHistoryCmd:
|
||
"""追加消息操作历史命令(FR-12)。
|
||
|
||
由端口方法 ``ConversationPort.appendOperationHistory`` 引用,用于消息
|
||
操作执行器在操作完成后将操作记录追加到目标消息的 ``operations_history``
|
||
JSON 数组,满足 PRD §FR-12 业务规则第 5 条"操作历史记录到消息元数据的
|
||
操作列表"。
|
||
|
||
字段:
|
||
message_id: 目标消息 ID。
|
||
entry: 操作历史条目,应包含 ``operation`` / ``at`` / ``success`` /
|
||
``sender_id`` 等字段。
|
||
"""
|
||
|
||
message_id: str
|
||
entry: dict[str, Any]
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空(FR-12)。
|
||
|
||
``message_id`` 必须非空,在构造时即抛出 ``ValidationError``,
|
||
adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.message_id:
|
||
raise ValidationError("message_id", "must not be empty")
|