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

310 lines
12 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。
定义会话端口方法引用的命令与结果值对象,包括会话 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 查询已有跨渠道会话,
命中则复用,未命中则创建新会话并写入该 IDFR-06
create_if_not_found: 未找到会话时是否创建新会话(默认 False
为 True 时适配器在未找到已绑定会话的情况下创建新 Conversation
并返回其 ID不再抛出 NotFoundErrorFR-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")