ForcePilot/backend/package/yuxi/channels/contract/dtos/common.py
Kris 742299cb07 chore: 批量清理报告相关代码并完成多项功能迭代
本次提交包含多维度代码优化与功能增强:
1.  移除报告模块冗余导入与枚举,清理报表相关代码
2.  新增扫码登录支持方法与飞书适配器适配
3.  完善异常日志与健康检查信息
4.  扩展目录、配对管理、能力查询等接口
5.  优化出站管道与事务提交后钩子逻辑
6.  修复飞书消息解析与响应空值问题
7.  重构配置更新与服务账号创建逻辑
8.  统一传输错误分类契约与错误基类扩展
2026-07-06 20:49:35 +08:00

346 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。
定义跨层共享的不可变值对象,包括消息格式、操作人角色、原始事件、消息内容、
附件、操作人、失败详情、跳过详情等。所有 DTO 均为 ``dataclass(frozen=True)``
仅依赖标准库,禁止泄露领域实体引用。
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
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: 事件接收时间。
account_id: 渠道账号 ID由框架层 ``BaseTransportWorker._deliverMessage``
自动填充,供 ``InboundAdapter.downloadAttachment`` 等下游组件
获取账号上下文,避免在 ``payload`` 中注入私有字段)。
"""
source: Literal["webhook", "polling", "stream"]
payload: dict[str, Any]
headers: dict[str, str]
received_at: datetime
account_id: str | None = None
def __post_init__(self) -> None:
"""校验 source 取值合法。
``source`` 必须为 ``webhook`` / ``polling`` / ``stream`` 之一,在
构造时即抛出 ``ValidationError``避免非法来源传播到管道INV-8
"""
if self.source not in ("webhook", "polling", "stream"):
raise ValidationError(
"source",
"must be one of: webhook, polling, stream",
)
@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: Literal["image", "file", "audio", "video"]
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
def __post_init__(self) -> None:
"""校验 type 取值与 url 非空。
``type`` 必须为 ``image`` / ``file`` / ``audio`` / ``video`` 之一,
``url`` 必须非空,在构造时即抛出 ``ValidationError``,避免非法附件
类型传播到渲染层INV-8
"""
if self.type not in ("image", "file", "audio", "video"):
raise ValidationError(
"type",
"must be one of: image, file, audio, video",
)
if not self.url:
raise ValidationError("url", "must not be empty")
@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
def __post_init__(self) -> None:
"""校验 text 非空。
``text`` 必须非空,与 ``from_dict`` 的校验保持一致,在构造时即抛出
``ValidationError``避免空文本消息传播到出站管道INV-8
"""
if not self.text:
raise ValidationError("text", "text is required and must not be empty")
@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``;非缺省值必须为
``MessageFormat`` 合法成员(``text`` / ``markdown`` / ``rich``
- ``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`` 为空或缺失
- ``ValidationError(field="format", message="unsupported format: ...")``
``format`` 非合法 ``MessageFormat`` 成员
参数:
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)
valid_formats = {f.value for f in MessageFormat}
if fmt not in valid_formats:
raise ValidationError(
"format",
f"unsupported format: {fmt}, must be one of: {', '.join(f.value for f in MessageFormat)}",
)
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
def __post_init__(self) -> None:
"""校验 user_id 非空。
``user_id`` 必须非空(或为 ``"system"``),在构造时即抛出
``ValidationError``避免空操作人传播到审计日志INV-8
"""
if not self.user_id:
raise ValidationError("user_id", "must not be empty")
@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, ...]