"""消息操作 DTO。 定义消息操作的枚举与不可变值对象,包括消息操作类型、消息操作能力、 消息操作定义与消息操作结果。所有 DTO 均为 ``dataclass(frozen=True)``, 仅依赖标准库与契约层内部类型,用于 FR-12 消息操作(编辑 / 撤回 / 反应 / 置顶 / 卡片更新等)的能力声明、操作定义、工具转换与结果回传。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import StrEnum from typing import Any from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator from yuxi.channels.contract.dtos.tools import ToolParameter from yuxi.channels.contract.errors import ValidationError class MessageOperation(StrEnum): """消息操作类型。 标识渠道侧支持的消息操作,用于消息操作能力声明与结果回传。继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。 取值: SEND: 发送消息。 EDIT: 编辑消息。 REACT: 添加反应。 UNREACT: 移除反应。 PIN: 置顶消息。 UNPIN: 取消置顶。 UPDATE_CARD: 更新卡片。 DELETE_CARD: 删除卡片。 RECALL: 撤回消息(24 小时内,多渠道通用)。 """ SEND = "send" EDIT = "edit" REACT = "react" UNREACT = "unreact" PIN = "pin" UNPIN = "unpin" UPDATE_CARD = "update_card" DELETE_CARD = "delete_card" RECALL = "recall" @dataclass(frozen=True) class MessageOperationCapability: """消息操作能力。 描述渠道适配器支持的消息操作能力,包括是否支持反应、置顶与卡片更新, 用于能力声明与降级决策。 字段: supports_reaction: 是否支持反应(默认 False)。 supports_pin: 是否支持置顶(默认 False)。 supports_card_update: 是否支持卡片更新(默认 False)。 has_side_effects: 操作是否产生不可逆副作用(编辑/撤回/置顶等修改 渠道侧状态的操作为 True,查询类操作为 False,默认 False)。 用于工具调用前置可信注入阶段决定是否强制校验可信发送者 (FR-25)。 requires_trusted_sender: 操作是否要求可信发送者声明。为 True 时, 非渠道 Agent Run(无可信发送者)**不得** 执行该操作,由 pre-tool-call 可信注入阶段强制校验(FR-25)。默认 True,对齐 PRD §FR-25 业务规则第 4 条"默认所有消息操作都需要可信发送者"。 """ supports_reaction: bool = False supports_pin: bool = False supports_card_update: bool = False has_side_effects: bool = False requires_trusted_sender: bool = True @dataclass(frozen=True) class MessageOperationDefinition: """消息操作定义。 描述单个消息操作的完整元信息,包括操作类型、工具名(不含统一前缀)、 描述、参数列表与操作粒度能力声明(副作用、可信发送者要求),由插件 的 ``getMessageOperations`` 返回,供消息操作执行器与工具转换机制 消费(FR-12)。 工具转换规则(PRD §FR-12 业务规则第 3 条): - 定义中的 ``tool_name`` 在合并到 ``ChannelTool`` 时附加统一前缀 ``MESSAGE_OPERATION_TOOL_PREFIX``(默认 ``渠道操作_``)。 - Agent 调用工具时通过工具名前缀路由到消息操作执行器,而非插件 的 ``executeTool``。 字段: operation: 消息操作类型。 tool_name: 工具名(不含统一前缀,如 ``表情反应``)。 description: 工具描述,供 Agent 理解操作用途。 parameters: 参数列表(默认空 tuple)。 has_side_effects: 操作是否产生不可逆副作用(默认 False)。用于 工具调用前置可信注入阶段决定是否强制校验可信发送者(FR-25)。 requires_trusted_sender: 操作是否要求可信发送者声明(默认 True)。 为 True 时,非渠道 Agent Run(无可信发送者)**不得** 执行该 操作,由 pre-tool-call 可信注入阶段强制校验(FR-25)。默认 True 对齐 PRD §FR-25 业务规则第 4 条"默认所有消息操作都需要 可信发送者"。 """ operation: MessageOperation tool_name: str description: str parameters: tuple[ToolParameter, ...] = () has_side_effects: bool = False requires_trusted_sender: bool = True @dataclass(frozen=True) class MessageOperationResult: """消息操作结果(FR-12)。 描述一次消息操作的执行结果,包括操作类型、是否成功、结果数据与错误 信息,用于消息操作结果回传与审计。 字段: operation: 消息操作类型。 success: 是否成功。 result_data: 结果数据(可选)。 error: 错误信息(可选)。 """ operation: MessageOperation success: bool result_data: dict[str, Any] | None = None error: str | None = None @dataclass(frozen=True) class ResendMessageCmd: """消息重发命令(MSG-RESEND)。 由 ``MessageManagementPort.resendMessage`` 引用,基于原消息重新构造并发送 一条新消息,需记录操作人以满足审计要求(FR-12)。 字段: channel_type: 渠道类型。 message_id: 原消息 ID。 operator: 操作人(审计用)。 target: 重发目标(可选,缺省时复用原消息目标)。 content_overrides: 内容覆盖(可选,按字段覆盖原消息内容)。 reason: 重发原因(审计用,可选)。 """ channel_type: ChannelType message_id: str operator: Operator target: str | None = None content_overrides: dict[str, Any] | None = None reason: str | None = None def __post_init__(self) -> None: """校验必填字段非空(MSG-RESEND)。 ``message_id`` 必须非空,在构造时即抛出 ``ValidationError``, adapter 不再做该校验(INV-8)。 """ if not self.message_id: raise ValidationError("message_id", "must not be empty") @dataclass(frozen=True) class ResendMessageResult: """消息重发结果(MSG-RESEND)。 描述重发操作的新旧消息映射关系,``new_message_id`` 为新生成消息的 ID。 字段: original_message_id: 原消息 ID。 new_message_id: 新消息 ID。 sent_at: 发送时间戳。 """ original_message_id: str new_message_id: str sent_at: datetime @dataclass(frozen=True) class BatchRecallCmd: """批量撤回命令(MSG-BATCH-RECALL)。 由 ``MessageManagementPort.batchRecallMessages`` 引用,逐条独立事务撤回 消息(模式 D),``message_ids`` 长度限制 1-500。 字段: message_ids: 待撤回消息 ID 元组(必填,1-500)。 operator: 操作人(审计用)。 reason: 撤回原因(审计用,可选)。 """ message_ids: tuple[str, ...] operator: Operator reason: str | None = None def __post_init__(self) -> None: """校验必填字段非空与 ``message_ids`` 长度范围(MSG-BATCH-RECALL)。 ``message_ids`` 必须非空且长度不超过 500(批量操作上限), 在构造时即抛出 ``ValidationError``,adapter 不再做该校验(INV-8)。 """ if not self.message_ids: raise ValidationError("message_ids", "must not be empty") if len(self.message_ids) > 500: raise ValidationError( "message_ids", "must not exceed 500 items", ) @dataclass(frozen=True) class BatchRecallResult: """批量撤回结果(MSG-BATCH-RECALL)。 描述逐条独立事务撤回消息的执行结果,``failed`` 使用通用 ``BatchOperationFailure``(``id`` 字段承载 message_id)。 ``succeeded`` 为每条撤回结果 dict(含 message_id / channel_msg_id / channel_recalled_at / im_side_recall_ack),供前端直接展示撤回详情。 字段: total: 待撤回消息总数。 succeeded: 成功撤回的结果 dict 元组。 failed: 失败条目元组。 """ total: int succeeded: tuple[dict[str, Any], ...] failed: tuple[BatchOperationFailure, ...] @dataclass(frozen=True) class ReadStatus: """消息已读状态。 描述 ``MessageOpsAdapter.getReadStatus`` 返回的消息已读回执信息,用于 业务方判断用户是否已读 AI 发送的重要通知。集合字段使用 tuple 以保证 frozen dataclass 的不可变语义。 字段: channel_msg_id: 渠道侧消息 ID。 read_count: 已读用户数。 total_count: 目标用户总数。 read_user_ids: 已读用户 ID 元组(渠道支持返回明细时填充, 不支持时为空 tuple,调用方据 ``read_count`` 判定)。 read_at: 最近一次已读时间戳(可选,渠道不支持时为 ``None``)。 """ channel_msg_id: str read_count: int total_count: int read_user_ids: tuple[str, ...] = () read_at: datetime | None = None # 消息操作工具统一前缀(PRD §FR-12 业务规则第 3 条)。 # 消息操作定义转换为渠道工具时附加本前缀,``ChannelToolExecutor`` 检测 # 前缀后路由到 ``MessageOperationExecutor`` 而非插件 ``executeTool``。 MESSAGE_OPERATION_TOOL_PREFIX = "渠道操作_"