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

288 lines
8.6 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。
定义跨层共享的不可变值对象,包括消息格式、操作人角色、原始事件、消息内容、
附件、操作人、失败详情、跳过详情等。所有 DTO 均为 ``dataclass(frozen=True)``
仅依赖标准库,禁止泄露领域实体引用。
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Any
from yuxi.channels.contract.errors import ValidationError
class MessageFormat(StrEnum):
"""消息内容格式。
用于 ``MessageContent.format`` 字段,标识消息文本的渲染格式,便于适配器
按格式转换为渠道侧对应的消息结构。
取值:
TEXT: 纯文本。
MARKDOWN: Markdown 文本。
RICH: 富消息(含结构化卡片 / 模板)。
"""
TEXT = "text"
MARKDOWN = "markdown"
RICH = "rich"
class OperatorRole(StrEnum):
"""操作人角色。
用于审计日志与权限校验,区分触发操作的用户类型。
取值:
REQUIRED_USER: 普通需求用户。
ADMIN_USER: 管理员用户。
SUPERADMIN_USER: 超级管理员用户。
"""
REQUIRED_USER = "required_user"
ADMIN_USER = "admin_user"
SUPERADMIN_USER = "superadmin_user"
@dataclass(frozen=True)
class RawEvent:
"""原始事件。
渠道适配器接收到外部事件webhook / SSE / polling后封装的原始数据
作为管道入口的统一输入,保留原始负载与请求头供后续签名校验与解析。
字段:
source: 事件来源webhook=渠道HTTP推送 / polling=Puller轮询 / stream=Streamer长连接
payload: 原始事件负载。
headers: 请求头(含签名、时间戳等)。
received_at: 事件接收时间。
"""
source: str
payload: dict[str, Any]
headers: dict[str, str]
received_at: datetime
@dataclass(frozen=True)
class Attachment:
"""消息附件。
描述消息中的非文本内容(图片 / 文件 / 音频 / 视频),由适配器按渠道协议
解析后填充。入站图片经 media-fetch 阶段下载二进制并预处理为 base64 后,
填充 content / base64_content / width / height 字段;视频仅透传 URL 与
元数据,不下载二进制。
字段:
type: 附件类型image | file | audio | video
url: 附件资源 URL。
mime_type: MIME 类型。
size: 附件字节数。
content: 二进制内容(入站由 media-fetch 阶段填充)。
base64_content: base64 编码内容(供多模态模型消费)。
filename: 文件名(含扩展名)。
width: 图片 / 视频宽度(像素)。
height: 图片 / 视频高度(像素)。
duration_ms: 音频 / 视频时长(毫秒)。
"""
type: str
url: str
mime_type: str | None = None
size: int | None = None
content: bytes | None = None
base64_content: str | None = None
filename: str | None = None
width: int | None = None
height: int | None = None
duration_ms: int | None = None
@dataclass(frozen=True)
class MessageContent:
"""消息内容。
统一描述渠道消息的文本与附件,跨层传递时保持不可变;附件字段使用 tuple
以保证 frozen dataclass 的不可变语义。
字段:
text: 文本内容。
format: 文本格式text | markdown | rich
attachments: 附件列表tuple 保证不可变)。
metadata: 渠道侧元数据。
"""
text: str
format: MessageFormat = MessageFormat.TEXT
attachments: tuple[Attachment, ...] = ()
metadata: dict[str, Any] | None = None
@classmethod
def from_dict(cls, data: dict[str, Any]) -> MessageContent:
"""从 dict 构造 MessageContent 实例。
将 dict 形态的消息内容(如 HTTP 请求体或外部序列化结构)转换为不可变
``MessageContent`` DTO递归构造 ``Attachment`` 元组。供驱动适配器层
(如 MSG-SEND-01 端点)将 Pydantic 字段委托至契约层 DTO 构造,
保证跨层传递的不可变语义。
@pre
- ``data`` 含 ``text`` 字段非空(``""`` / ``None`` / 缺失均视为非法)
- ``format`` 可选,默认 ``MessageFormat.TEXT``
- ``attachments`` 可选,默认空元组
- ``metadata`` 可选,默认 ``None``
@post
- 返回不可变 ``MessageContent`` 实例
- ``attachments`` 中 dict 元素已通过 ``Attachment(**a)`` 构造为
``Attachment`` 实例;已是 ``Attachment`` 实例的元素原样保留
@failure
- ``ValidationError(field="text", message="text is required and must not be empty")``
``text`` 为空或缺失
参数:
data: dict 形态的消息内容。
返回:
构造完成的 ``MessageContent`` 实例。
"""
text = data.get("text", "")
if not text:
raise ValidationError("text", "text is required and must not be empty")
fmt = data.get("format", MessageFormat.TEXT)
raw_attachments = data.get("attachments", [])
attachments = tuple(Attachment(**a) if isinstance(a, dict) else a for a in raw_attachments)
metadata = data.get("metadata")
return cls(text=text, format=fmt, attachments=attachments, metadata=metadata)
@dataclass(frozen=True)
class Operator:
"""操作人。
用于审计日志与权限校验,记录触发操作的用户身份与链路追踪信息。
字段:
user_id: 操作人用户 ID"system")。
role: 操作人角色。
ip: 来源 IP审计用
request_id: 请求 ID链路追踪用
"""
user_id: str
role: OperatorRole
ip: str | None = None
request_id: str | None = None
@dataclass(frozen=True)
class FailureDetail:
"""失败详情。
描述批量操作中单个目标的失败信息,用于结果聚合与重试策略决策。
字段:
target: 失败目标。
error_code: 错误码。
message: 人类可读错误信息。
retryable: 是否可重试。
details: 原始异常的业务字段(从 ``Error.details`` 提取),用于
反向重构异常时保留 ``resource`` / ``id`` / ``field`` / ``rule``
等字段,避免业务信息丢失。
"""
target: str
error_code: str
message: str
retryable: bool = False
details: dict[str, Any] | None = None
@dataclass(frozen=True)
class SkipDetail:
"""跳过详情。
描述批量操作中单个目标被跳过的原因与触发策略,用于结果聚合与审计。
字段:
target: 跳过目标。
reason: 跳过原因(如 "in_denylist")。
policy: 触发的策略。
"""
target: str
reason: str
policy: str
@dataclass(frozen=True)
class BatchOperationFailure:
"""批量操作失败条目(通用)。
描述 P1 批量操作(逐条独立事务模式 D中单条失败条目统一以 ``id``
字段承载失败目标标识session_id / account_id / pairing_id 等),
供各域批量结果 ``failed`` 列表共用。
字段:
id: 失败目标标识。
error_code: 错误码。
message: 人类可读错误信息。
"""
id: str
error_code: str
message: str
@dataclass(frozen=True)
class TrendDataPoint:
"""通用趋势数据点。
描述按时间粒度切片后的单个时间桶计数,供 outbox / pairing / analytics
等域趋势结果共用。``timestamp`` 序列化为 ISO 8601 字符串由调用方处理。
字段:
timestamp: 时间桶起始时间。
value: 计数值。
"""
timestamp: datetime
value: int
@dataclass(frozen=True)
class CategoryStat:
"""分类统计(通用)。
描述按分类分组的计数项,供 content review stats / analytics 等域
``by_category`` 列表共用。
字段:
category: 分类标识。
count: 计数。
"""
category: str
count: int
@dataclass(frozen=True)
class CleanExpiredResult:
"""清理过期结果(通用)。
描述 P1 ``clean-expired`` 操作(白名单 / 配对等)的统一返回结构,
单一事务批量清理模式下不区分逐条失败。
字段:
total: 清理条目总数。
cleaned: 已清理的目标 ID 元组。
"""
total: int
cleaned: tuple[str, ...]