2026-07-02 03:22:12 +08:00
|
|
|
|
"""消息操作 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
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from yuxi.channels.contract.errors import ValidationError
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MessageOperation(StrEnum):
|
|
|
|
|
|
"""消息操作类型。
|
|
|
|
|
|
|
|
|
|
|
|
标识渠道侧支持的消息操作,用于消息操作能力声明与结果回传。继承
|
|
|
|
|
|
``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
|
|
|
|
|
|
|
|
|
|
|
取值:
|
|
|
|
|
|
SEND: 发送消息。
|
|
|
|
|
|
EDIT: 编辑消息。
|
|
|
|
|
|
REACT: 添加反应。
|
|
|
|
|
|
UNREACT: 移除反应。
|
|
|
|
|
|
PIN: 置顶消息。
|
|
|
|
|
|
UNPIN: 取消置顶。
|
|
|
|
|
|
UPDATE_CARD: 更新卡片。
|
|
|
|
|
|
DELETE_CARD: 删除卡片。
|
2026-07-09 04:21:28 +08:00
|
|
|
|
RECALL: 撤回消息(24 小时内,多渠道通用)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
SEND = "send"
|
|
|
|
|
|
EDIT = "edit"
|
|
|
|
|
|
REACT = "react"
|
|
|
|
|
|
UNREACT = "unreact"
|
|
|
|
|
|
PIN = "pin"
|
|
|
|
|
|
UNPIN = "unpin"
|
|
|
|
|
|
UPDATE_CARD = "update_card"
|
|
|
|
|
|
DELETE_CARD = "delete_card"
|
2026-07-09 04:21:28 +08:00
|
|
|
|
RECALL = "recall"
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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:
|
2026-07-03 19:18:13 +08:00
|
|
|
|
"""消息操作结果(FR-12)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
描述一次消息操作的执行结果,包括操作类型、是否成功、结果数据与错误
|
|
|
|
|
|
信息,用于消息操作结果回传与审计。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
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)。
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
由 ``MessageManagementPort.resendMessage`` 引用,基于原消息重新构造并发送
|
2026-07-02 03:22:12 +08:00
|
|
|
|
一条新消息,需记录操作人以满足审计要求(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
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@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)。
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
由 ``MessageManagementPort.batchRecallMessages`` 引用,逐条独立事务撤回
|
2026-07-02 03:22:12 +08:00
|
|
|
|
消息(模式 D),``message_ids`` 长度限制 1-500。
|
|
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
message_ids: 待撤回消息 ID 元组(必填,1-500)。
|
|
|
|
|
|
operator: 操作人(审计用)。
|
|
|
|
|
|
reason: 撤回原因(审计用,可选)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
message_ids: tuple[str, ...]
|
|
|
|
|
|
operator: Operator
|
|
|
|
|
|
reason: str | None = None
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class BatchRecallResult:
|
|
|
|
|
|
"""批量撤回结果(MSG-BATCH-RECALL)。
|
|
|
|
|
|
|
|
|
|
|
|
描述逐条独立事务撤回消息的执行结果,``failed`` 使用通用
|
|
|
|
|
|
``BatchOperationFailure``(``id`` 字段承载 message_id)。
|
2026-07-03 19:18:13 +08:00
|
|
|
|
``succeeded`` 为每条撤回结果 dict(含 message_id / channel_msg_id /
|
|
|
|
|
|
channel_recalled_at / im_side_recall_ack),供前端直接展示撤回详情。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
字段:
|
|
|
|
|
|
total: 待撤回消息总数。
|
2026-07-03 19:18:13 +08:00
|
|
|
|
succeeded: 成功撤回的结果 dict 元组。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
failed: 失败条目元组。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total: int
|
2026-07-03 19:18:13 +08:00
|
|
|
|
succeeded: tuple[dict[str, Any], ...]
|
2026-07-02 03:22:12 +08:00
|
|
|
|
failed: tuple[BatchOperationFailure, ...]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 18:27:06 +08:00
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
# 消息操作工具统一前缀(PRD §FR-12 业务规则第 3 条)。
|
|
|
|
|
|
# 消息操作定义转换为渠道工具时附加本前缀,``ChannelToolExecutor`` 检测
|
|
|
|
|
|
# 前缀后路由到 ``MessageOperationExecutor`` 而非插件 ``executeTool``。
|
|
|
|
|
|
MESSAGE_OPERATION_TOOL_PREFIX = "渠道操作_"
|