ForcePilot/backend/package/yuxi/channels/contract/dtos/conversation.py
Kris b88c0ae29e feat(channels): 批量新增多渠道网关限界上下文基础代码与契约
新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
2026-07-02 03:22:12 +08:00

299 lines
11 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
from datetime import datetime
from typing import Any
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:
"""保存消息命令。
由端口方法 ``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: str
content: str
channel_msg_id: str | None = None
trace_id: str | None = None
channel_status: str | None = None
ref_channel_msg_id: str | None = None
channel_status_history: list[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:
"""解析会话命令。
由端口方法 ``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)。
"""
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
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:
"""关联会话命令。
由端口方法 ``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:
"""合并会话命令。
由端口方法 ``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:
"""合并结果。
描述会话合并操作的执行结果,包括迁移消息数量、源会话软删除标记与
审计日志 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: str
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")