本次提交涵盖了近百处代码优化与功能补全,包括: 1. 完善配置与数据模型:新增expired_at配对记录字段、路由绑定乐观锁版本控制、会话路由信息追踪字段 2. 优化业务流程:添加幂等记录操作人审计、会话合并领域服务文档更新、媒体处理异步化改造 3. 新增功能能力:健康检查时间更新、会话路由信息更新接口、内容审核/幂等记录清理定时任务 4. 修复与简化:移除废弃的max_message_length属性、修复微信iLink适配器配置读取路径、简化配对过期扫描逻辑 5. 代码规范优化:统一敏感词检测工具导入、完善事务上下文处理注释、调整wechat_woc入站适配器sender回退逻辑
747 lines
27 KiB
Python
747 lines
27 KiB
Python
"""渠道类型 DTO。
|
||
|
||
定义渠道账户、会话、身份、消息等跨层共享的不可变值对象。所有 DTO 均为
|
||
``dataclass(frozen=True)``,仅依赖标准库,禁止泄露领域实体引用。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
from enum import StrEnum
|
||
from typing import Any, Literal
|
||
|
||
from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
|
||
|
||
class ChannelType(str):
|
||
"""渠道类型标识符。
|
||
|
||
由插件 manifest 声明,框架层不做白名单校验。每个插件应使用唯一的
|
||
channel_type 值,由 ``PluginRegistry`` 保证唯一性。运行时为 ``str``
|
||
子类,可直接作为字典 key、JSON 序列化、FastAPI 参数。
|
||
|
||
约束:
|
||
- 框架层(contract / core / application)**禁止** 在任何决策语句中
|
||
分支到具体 channel_type 值,渠道特性决策 **必须** 通过
|
||
``ChannelManifest`` 声明字段驱动。
|
||
- 新增渠道仅需在插件 manifest 中声明 channel_type 字符串,无需
|
||
修改本类。
|
||
- channel_type 值在系统中全局唯一(由 ``PluginRegistry.register``
|
||
校验),两个插件不得共用同一 channel_type。
|
||
"""
|
||
|
||
__slots__ = ()
|
||
|
||
@classmethod
|
||
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> Any:
|
||
"""Pydantic 核心 schema:以 ``str`` 校验后包装为 ``ChannelType``。
|
||
|
||
``ChannelType`` 为 ``str`` 子类(非 ``StrEnum``),Pydantic 默认
|
||
无法为其生成 schema,导致 ``ChannelType | None`` 作为 FastAPI
|
||
``Query`` / ``Field`` 参数或 Pydantic 模型字段时抛
|
||
``PydanticSchemaGenerationError``。本方法以 ``str_schema`` 校验
|
||
输入后用 ``cls`` 包装,保留 str 子类的开放值语义(支持
|
||
``ChannelType("wechat_ilink")`` 等动态值,无需预定义枚举成员)。
|
||
|
||
``pydantic_core`` 在方法内延迟导入,避免在 contract 层引入硬依赖,
|
||
保持 DTO 模块"仅依赖标准库"的约束(无 Pydantic 环境下本类仍可正常
|
||
使用,仅失去 Pydantic 字段类型支持)。
|
||
"""
|
||
from pydantic_core import core_schema
|
||
|
||
return core_schema.no_info_after_validator_function(
|
||
cls,
|
||
core_schema.str_schema(),
|
||
)
|
||
|
||
|
||
class AccountStatus(StrEnum):
|
||
"""渠道账户状态枚举。
|
||
|
||
聚合根 ``ChannelAccount`` 的状态机枚举。状态转换规则参见 ``ChannelAccount``
|
||
聚合根:
|
||
|
||
- ``ACTIVE`` ↔ ``DISABLED``:通过 ``enable()`` / ``disable()`` 切换。
|
||
- ``ACTIVE`` → ``DEGRADED``:通过 ``degrade()`` 切换(不可从 ``DISABLED`` 降级)。
|
||
- ``DEGRADED`` → ``ACTIVE``:通过 ``recover()`` 切换(不可直接 ``enable``)。
|
||
- ``DISABLED`` 不可直接 ``recover``,需先 ``enable`` 回到 ``ACTIVE``。
|
||
"""
|
||
|
||
ACTIVE = "active"
|
||
DISABLED = "disabled"
|
||
DEGRADED = "degraded"
|
||
|
||
|
||
class OnboardingStatus(StrEnum):
|
||
"""渠道账号接入态枚举。
|
||
|
||
描述渠道账号 onboarding 生命周期状态,独立于运行态 ``AccountStatus``
|
||
(``ACTIVE`` / ``DISABLED`` / ``DEGRADED``):onboarding 状态刻画账号
|
||
从创建到可用的接入流程进度,运行态刻画已上线账号的实时可用性。两者
|
||
解耦后,账号下线(``OFFLINE``)与运行态禁用(``DISABLED``)可独立流转,
|
||
避免 onboarding 流程被运行态切换误覆盖。
|
||
|
||
取值:
|
||
PENDING: 账号已创建,凭证未配置。
|
||
CONFIGURED: 凭证已落库,未验证连通性。
|
||
VERIFIED: 连通性验证通过,未上线。
|
||
ONLINE: 已上线(运行态 ``ACTIVE``)。
|
||
OFFLINE: 已下线(运行态 ``DISABLED``)。
|
||
FAILED: 接入失败(凭证失效 / 验证失败)。
|
||
|
||
继承 ``StrEnum`` 以兼容 JSON 序列化:``str(OnboardingStatus.PENDING)``
|
||
即 ``"pending"``,可直接用于 ``json.dumps`` 与持久化层字符串字段,
|
||
无需额外的 ``__str__`` 或 serializer 适配。
|
||
"""
|
||
|
||
PENDING = "pending"
|
||
CONFIGURED = "configured"
|
||
VERIFIED = "verified"
|
||
ONLINE = "online"
|
||
OFFLINE = "offline"
|
||
FAILED = "failed"
|
||
|
||
|
||
class SessionStatus(StrEnum):
|
||
"""渠道会话状态枚举。
|
||
|
||
标识渠道会话的生命周期状态,用于会话关闭与消息投递决策。继承
|
||
``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
||
|
||
取值:
|
||
ACTIVE: 活跃会话,可正常收发消息。
|
||
CLOSED: 已关闭会话,停止接收新消息。
|
||
"""
|
||
|
||
ACTIVE = "active"
|
||
CLOSED = "closed"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ChannelAccount:
|
||
"""渠道账户。
|
||
|
||
描述一个渠道账户的完整配置,用于账户管理与消息路由。config 字段为脱敏后
|
||
的渠道配置,不包含原始密钥。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账户 ID(全局唯一)。
|
||
display_name: 显示名称。
|
||
config: 渠道配置(脱敏后)。
|
||
enabled: 是否启用。
|
||
status: 账户状态机值(active/disabled/degraded)。
|
||
created_at: 创建时间。
|
||
updated_at: 更新时间。
|
||
transport_cursor: 传输游标,Puller类型使用。
|
||
last_rotated_at: 凭据最近轮换时间(可选)。
|
||
service_user_uid: 关联服务账号 User.uid(user_type='service'),可选。
|
||
用于渠道账户以服务账号身份执行内部操作。
|
||
onboarding_status: 接入态状态机值(pending/configured/verified/
|
||
online/offline/failed)。描述账号 onboarding 生命周期,与运行态
|
||
``status`` 解耦。必填,无默认值(kw_only)。
|
||
credential_ref: 凭证引用(可选),指向凭证存储中的版本化凭证记录。
|
||
credential_version: 凭证版本号(默认 0),与 ``credential_ref`` 共同
|
||
定位具体凭证版本。
|
||
transport_mode: 传输模式(默认 ``both``)。声明账号支持的传输方式:
|
||
``pull``(仅拉取)、``stream``(仅流式)、``both``(两者皆可),
|
||
供入站/出站链路决策拉取器与流式通道启用策略。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
display_name: str
|
||
config: dict[str, Any]
|
||
enabled: bool = True
|
||
status: AccountStatus = AccountStatus.ACTIVE
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
transport_cursor: str = ""
|
||
last_rotated_at: datetime | None = None
|
||
service_user_uid: str | None = None
|
||
transport_mode: Literal["pull", "stream", "both"] = "both"
|
||
onboarding_status: OnboardingStatus = field(kw_only=True)
|
||
credential_ref: str | None = field(kw_only=True, default=None)
|
||
credential_version: int = field(kw_only=True, default=0)
|
||
last_error: str | None = field(kw_only=True, default=None)
|
||
plugin_status: str = field(kw_only=True, default="stopped")
|
||
version: int = field(kw_only=True, default=1)
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空。
|
||
|
||
``channel_type`` 不可为 ``None``,``account_id`` 与 ``display_name``
|
||
必须非空,在构造时即抛出 ``ValidationError``,adapter 不再做该校验
|
||
(INV-8)。
|
||
"""
|
||
if self.channel_type is None:
|
||
raise ValidationError("channel_type", "must not be None")
|
||
if not self.account_id:
|
||
raise ValidationError("account_id", "must not be empty")
|
||
if not self.display_name:
|
||
raise ValidationError("display_name", "must not be empty")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ChannelAccountSummary:
|
||
"""渠道账户摘要。
|
||
|
||
用于账户列表查询等场景,仅包含展示必要字段,不泄露配置详情。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账户 ID。
|
||
display_name: 显示名称。
|
||
enabled: 是否启用。
|
||
status: 账户状态机值(active/disabled/degraded)。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
display_name: str
|
||
enabled: bool
|
||
status: AccountStatus = AccountStatus.ACTIVE
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ChannelSession:
|
||
"""渠道会话。
|
||
|
||
描述渠道侧会话状态,关联会话与统一身份、主会话所有者,支持临时会话标记
|
||
与软删除。
|
||
|
||
字段:
|
||
session_id: 会话 ID。
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账户 ID。
|
||
peer_id: 对端 ID。
|
||
chat_type: 会话类型("p2p" | "group")。
|
||
conversation_id: 关联的内部会话 ID。
|
||
unified_identity_id: 统一身份 ID。
|
||
owner_peer_id: 主会话所有者对端 ID(FR-26)。
|
||
is_temporary: 临时会话标记(FR-27)。
|
||
created_at: 创建时间。
|
||
updated_at: 更新时间。
|
||
deleted_at: 软删除时间。
|
||
closed_at: 会话关闭时间(可选)。
|
||
last_message_at: 最近一次消息时间(可选,用于会话列表排序与
|
||
inactive 临时会话清理,FR-27)。
|
||
last_route_at: 最近路由更新时间(可选,FR-04/FR-26,用于跨渠道
|
||
会话排序与路由诊断)。
|
||
route_match_source: 最近路由匹配来源(可选,取值见
|
||
``ConfigMatchSource`` 枚举,用于路由审计)。
|
||
version: 乐观锁版本号(默认 1,用于并发更新校验)。
|
||
"""
|
||
|
||
session_id: str
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
peer_id: str
|
||
chat_type: Literal["p2p", "group"]
|
||
conversation_id: str | None = None
|
||
unified_identity_id: str | None = None
|
||
owner_peer_id: str | None = None
|
||
is_temporary: bool = False
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
deleted_at: datetime | None = None
|
||
closed_at: datetime | None = None
|
||
last_message_at: datetime | None = None
|
||
last_route_at: datetime | None = None
|
||
route_match_source: str | None = None
|
||
version: int = 1
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class UserIdentity:
|
||
"""用户身份。
|
||
|
||
描述用户在渠道侧或外部身份系统的身份信息,用于身份解析与统一身份关联。
|
||
``user_id`` 为可空字段但需显式传入,以区分"未关联用户"与"使用默认值"。
|
||
|
||
字段:
|
||
identity_id: 统一身份 ID。
|
||
user_id: 关联用户表 ID(可空)。
|
||
identity_type: 身份类型(邮箱 / 手机 / 组织员工 ID)。
|
||
identity_value: 身份值。
|
||
channel_type: 渠道类型。
|
||
channel_sender_id: 渠道侧发送者 ID。
|
||
source: 身份来源。
|
||
channel_bindings: 渠道绑定映射 ``{channel_type: [peer_id, ...]}``,
|
||
记录该身份在各渠道的对端 ID,用于跨渠道身份关联与反查。
|
||
merged_from: 合并来源 identity_id 列表,记录被合并进本身份的其它身份。
|
||
confidence: 身份置信度等级(``low`` / ``medium`` / ``high``),
|
||
反映身份解析结果的可信程度,用于路由决策与审计。
|
||
created_at: 创建时间。
|
||
updated_at: 更新时间。
|
||
version: 乐观锁版本号(P3 渐进式绑定,用于 updateUserIdentity 并发控制)。
|
||
"""
|
||
|
||
identity_id: str
|
||
user_id: str | None
|
||
identity_type: str
|
||
identity_value: str
|
||
channel_type: ChannelType | None = None
|
||
channel_sender_id: str | None = None
|
||
source: str | None = None
|
||
channel_bindings: dict[str, list[str]] = field(default_factory=dict)
|
||
merged_from: list[str] = field(default_factory=list)
|
||
confidence: str = "low"
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
version: int = 1
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SessionInfo:
|
||
"""会话信息。
|
||
|
||
渠道适配器解析原始事件后产出的会话定位信息,用于核心层解析或创建会话。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账户 ID。
|
||
peer_id: 对端 ID。
|
||
chat_type: 会话类型("p2p" | "group")。
|
||
group_id: 群组 ID。
|
||
topic_id: 话题 ID。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
peer_id: str
|
||
chat_type: Literal["p2p", "group"]
|
||
group_id: str | None = None
|
||
topic_id: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Message:
|
||
"""消息。
|
||
|
||
描述一条消息的完整状态,包括内部字段与渠道侧状态字段(已读、撤回、编辑
|
||
等),用于消息持久化与渠道侧状态同步。
|
||
|
||
字段:
|
||
message_id: 消息 ID。
|
||
conversation_id: 会话 ID。
|
||
role: 角色(user | assistant | admin)。
|
||
content: 消息文本内容。
|
||
channel_status: 渠道侧状态(FR-09)。
|
||
channel_msg_id: 渠道侧消息 ID。
|
||
ref_channel_msg_id: 引用的渠道消息 ID(编辑/回复场景)。
|
||
channel_status_history: 渠道侧状态事件历史数组(FR-09)。
|
||
operations_history: 消息操作历史数组(FR-12),每项记录一次消息操作
|
||
的执行信息(操作类型、执行者、时间戳、是否成功)。
|
||
channel_read_at: 渠道侧已读时间。
|
||
channel_recalled_at: 渠道侧撤回时间。
|
||
channel_edited_at: 渠道侧编辑时间。
|
||
created_at: 创建时间。
|
||
channel_type: 渠道类型(本期新增,由持久化层从会话关联填充,供 _messageStatus 返回)。
|
||
"""
|
||
|
||
message_id: str
|
||
conversation_id: str
|
||
role: Literal["user", "assistant", "admin"]
|
||
content: str
|
||
channel_status: str | None = None
|
||
channel_msg_id: str | None = None
|
||
ref_channel_msg_id: str | None = None
|
||
channel_status_history: tuple[dict, ...] | None = None
|
||
operations_history: tuple[dict, ...] | None = None
|
||
channel_read_at: datetime | None = None
|
||
channel_recalled_at: datetime | None = None
|
||
channel_edited_at: datetime | None = None
|
||
created_at: datetime | None = None
|
||
channel_type: ChannelType | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空与角色取值。
|
||
|
||
``message_id`` 与 ``conversation_id`` 必须非空,``role`` 必须为
|
||
``user`` / ``assistant`` / ``admin`` 之一,在构造时即抛出
|
||
``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.message_id:
|
||
raise ValidationError("message_id", "must not be empty")
|
||
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 RotateCredentialsResult:
|
||
"""凭据轮换结果。
|
||
|
||
描述账户凭据轮换操作的返回结果,包括轮换时间与是否成功撤销旧凭据。
|
||
|
||
字段:
|
||
account_id: 渠道账户 ID。
|
||
rotated_at: 轮换完成时间。
|
||
old_credentials_revoked: 旧凭据是否已成功撤销。
|
||
"""
|
||
|
||
account_id: str
|
||
rotated_at: datetime
|
||
old_credentials_revoked: bool
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConnectionCheckResult:
|
||
"""连接检查结果。
|
||
|
||
描述单次渠道账户连接连通性检查的结果。
|
||
|
||
字段:
|
||
success: 连接是否成功。
|
||
checked_at: 检查时间。
|
||
latency_ms: 连接延迟(毫秒,可选)。
|
||
error: 错误信息(失败时填充,可选)。
|
||
"""
|
||
|
||
success: bool
|
||
checked_at: datetime
|
||
latency_ms: int | None = None
|
||
error: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class TestConnectionResult:
|
||
"""测试连接结果。
|
||
|
||
描述渠道账户连接测试的完整结果,包括连通性检查与凭据验证两部分。
|
||
|
||
字段:
|
||
account_id: 渠道账户 ID。
|
||
connection: 连通性检查结果。
|
||
credentials_valid: 凭据是否有效。
|
||
tested_at: 测试时间。
|
||
"""
|
||
|
||
account_id: str
|
||
connection: ConnectionCheckResult
|
||
credentials_valid: bool
|
||
tested_at: datetime
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class MessageSearchItem:
|
||
"""消息搜索结果项。
|
||
|
||
描述消息全文搜索的单条匹配结果,包含消息核心字段与匹配高亮信息。
|
||
|
||
字段:
|
||
message_id: 消息 ID。
|
||
conversation_id: 会话 ID。
|
||
channel_type: 渠道类型。
|
||
role: 消息角色(user | assistant | admin)。
|
||
content: 消息内容。
|
||
created_at: 创建时间。
|
||
channel_session_id: 渠道会话 ID(可选)。
|
||
channel_account_id: 渠道账户业务 ID(可选)。
|
||
peer_id: 对端 ID(可选)。
|
||
conversation_title: 会话标题(可选)。
|
||
snippet: 匹配片段(高亮后,可选)。
|
||
"""
|
||
|
||
message_id: str
|
||
conversation_id: str
|
||
channel_type: ChannelType
|
||
role: Literal["user", "assistant", "admin"]
|
||
content: str
|
||
created_at: datetime
|
||
channel_session_id: str | None = None
|
||
channel_account_id: str | None = None
|
||
peer_id: str | None = None
|
||
conversation_title: str | None = None
|
||
snippet: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空与角色取值。
|
||
|
||
``message_id`` / ``conversation_id`` / ``content`` 必须非空,
|
||
``channel_type`` 不可为 ``None``,``role`` 必须为 ``user`` /
|
||
``assistant`` / ``admin`` 之一,在构造时即抛出 ``ValidationError``,
|
||
adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if not self.message_id:
|
||
raise ValidationError("message_id", "must not be empty")
|
||
if not self.conversation_id:
|
||
raise ValidationError("conversation_id", "must not be empty")
|
||
if self.channel_type is None:
|
||
raise ValidationError("channel_type", "must not be None")
|
||
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 WebhookTestCmd:
|
||
"""Webhook 测试命令(WHK-TEST)。
|
||
|
||
由 ``AccountManagementPort.testWebhook`` 引用,触发对指定渠道账户的
|
||
webhook 测试事件投递。``account_id`` 缺省时由 dispatch handler 取该
|
||
渠道首个账户。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
operator: 操作人(审计用)。
|
||
account_id: 渠道账户 ID(可选,缺省取首个账户)。
|
||
event_type: 测试事件类型(默认 ``test_event``)。
|
||
payload: 测试事件负载(可选,``None`` 归一化为空 dict)。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
operator: Operator
|
||
account_id: str | None = None
|
||
event_type: str = "test_event"
|
||
payload: dict[str, Any] | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空并归一化 payload(WHK-TEST)。
|
||
|
||
``channel_type`` 不可为 ``None``,``event_type`` 必须为非空字符串;
|
||
违规抛 ``ValidationError``,在构造时即拦截,避免空值传播到控制面
|
||
管道后才暴露(INV-8)。``payload`` 为 ``None`` 时归一化为空 dict,
|
||
与 ``WebhookTestAdapter.testWebhook`` 协议签名(非 Optional)对齐,
|
||
避免适配器解引用 ``None`` 触发 ``TypeError``。
|
||
"""
|
||
if self.channel_type is None:
|
||
raise ValidationError("channel_type", "must not be None")
|
||
if not self.event_type:
|
||
raise ValidationError("event_type", "must not be empty")
|
||
if self.payload is None:
|
||
object.__setattr__(self, "payload", {})
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class WebhookTestResult:
|
||
"""Webhook 测试结果(WHK-TEST)。
|
||
|
||
描述测试事件投递的结果,由插件 ``WebhookTestAdapter.testWebhook`` 返回。
|
||
|
||
字段:
|
||
test_id: 测试事件 ID。
|
||
delivered: 是否投递成功。
|
||
http_status: 渠道侧返回的 HTTP 状态码(投递失败时可能为 None)。
|
||
response_time_ms: 响应延迟(毫秒,投递失败时可能为 None)。
|
||
error: 错误信息(投递失败时填充,可选)。
|
||
"""
|
||
|
||
test_id: str
|
||
delivered: bool
|
||
http_status: int | None = None
|
||
response_time_ms: int | None = None
|
||
error: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchStateChangeCmd:
|
||
"""批量启停账户命令(ACC-BATCH-STATE)。
|
||
|
||
由 ``AccountManagementPort.batchEnableAccounts`` /
|
||
``batchDisableAccounts`` 引用,支持显式 ID 列表或筛选条件两种模式。
|
||
``action`` 取值 ``enable`` / ``disable``,由 dispatch handler 决定。
|
||
|
||
字段:
|
||
action: 操作类型(``enable`` / ``disable``)。
|
||
operator: 操作人(审计用)。
|
||
account_ids: 显式账户 ID 元组(默认空元组)。
|
||
filter: 筛选条件(含 channel_type / status,可选)。
|
||
reason: 操作原因(审计用,可选)。
|
||
"""
|
||
|
||
action: Literal["enable", "disable"]
|
||
operator: Operator
|
||
account_ids: tuple[str, ...] = ()
|
||
filter: dict[str, Any] | None = None
|
||
reason: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验 action 取值与至少一个目标模式(ACC-BATCH-STATE)。
|
||
|
||
``action`` 必须为 ``enable`` / ``disable`` 之一,``account_ids`` 与
|
||
``filter`` 至少提供一个,避免无目标的批量操作,在构造时即抛出
|
||
``ValidationError``,adapter 不再做该校验(INV-8)。
|
||
"""
|
||
if self.action not in ("enable", "disable"):
|
||
raise ValidationError(
|
||
"action",
|
||
"must be one of: enable, disable",
|
||
)
|
||
if not self.account_ids and self.filter is None:
|
||
raise ValidationError(
|
||
"filter",
|
||
"at least one of account_ids or filter must be provided",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class BatchStateChangeResult:
|
||
"""批量启停账户结果(ACC-BATCH-STATE)。
|
||
|
||
描述逐条独立事务启停账户的执行结果,``failed`` 使用通用
|
||
``BatchOperationFailure``(``id`` 字段承载 account_id)。
|
||
|
||
字段:
|
||
total: 待操作账户总数。
|
||
succeeded: 成功操作的账户 ID 元组。
|
||
failed: 失败条目元组。
|
||
"""
|
||
|
||
total: int
|
||
succeeded: tuple[str, ...]
|
||
failed: tuple[BatchOperationFailure, ...]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AccountExportResult:
|
||
"""账户配置导出结果(ACC-EXPORT)。
|
||
|
||
描述账户配置导出的返回内容,``secrets_included=false`` 时 ``raw_config``
|
||
中敏感字段已通过 MaskingPort 脱敏。
|
||
|
||
字段:
|
||
account_id: 渠道账户 ID。
|
||
channel_type: 渠道类型。
|
||
display_name: 账户显示名。
|
||
raw_config: 原始配置(脱敏后或含凭据)。
|
||
exported_at: 导出时间。
|
||
secrets_included: 是否包含敏感凭据。
|
||
"""
|
||
|
||
account_id: str
|
||
channel_type: ChannelType
|
||
display_name: str
|
||
raw_config: dict[str, Any]
|
||
exported_at: datetime
|
||
secrets_included: bool
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CloneAccountCmd:
|
||
"""克隆账户命令(ACC-CLONE)。
|
||
|
||
由 ``AccountManagementPort.cloneAccount`` 引用,基于源账户配置创建
|
||
新账户。``include_credentials=false`` 时清除凭据字段。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
account_id: 源账户 ID。
|
||
new_display_name: 新账户显示名(必填)。
|
||
operator: 操作人(审计用)。
|
||
new_raw_config_overrides: 配置覆盖项(可选,默认空 dict)。
|
||
include_credentials: 是否克隆凭据(默认 False)。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
new_display_name: str
|
||
operator: Operator
|
||
new_raw_config_overrides: dict[str, Any] | None = None
|
||
include_credentials: bool = False
|
||
|
||
def __post_init__(self) -> None:
|
||
"""校验必填字段非空(ACC-CLONE)。
|
||
|
||
``new_display_name`` 必须非空字符串,在构造时即抛出
|
||
``ValidationError``,避免空值传播到聚合根 ``clone()`` 后才暴露
|
||
(INV-8)。
|
||
"""
|
||
if not self.new_display_name:
|
||
raise ValidationError("new_display_name", "new_display_name must not be empty")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CloneAccountResult:
|
||
"""克隆账户结果(ACC-CLONE)。
|
||
|
||
描述克隆操作返回的新账户信息,新账户默认为 DISABLED 状态,需手动 enable。
|
||
|
||
字段:
|
||
new_account_id: 新账户 ID。
|
||
cloned_from: 源账户 ID。
|
||
status: 新账户状态(默认 DISABLED)。
|
||
created_at: 创建时间。
|
||
"""
|
||
|
||
new_account_id: str
|
||
cloned_from: str
|
||
status: AccountStatus
|
||
created_at: datetime
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AccountFilter:
|
||
"""渠道账户筛选条件值对象(ACC-BATCH-STATE)。
|
||
|
||
供 ``ChannelAccountRepositoryPort.findAccountsByFilter`` 使用,替换原
|
||
``dict[str, Any]`` 弱类型参数。所有字段均可选,但 ``__post_init__``
|
||
强制至少提供一个筛选条件,避免全表扫描。
|
||
|
||
字段:
|
||
channel_type: 渠道类型筛选(可选)。
|
||
status: 账户状态筛选(可选)。
|
||
"""
|
||
|
||
channel_type: ChannelType | None = None
|
||
status: AccountStatus | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
if self.channel_type is None and self.status is None:
|
||
raise ValidationError(
|
||
"filter",
|
||
"at least one of channel_type or status must be provided",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SessionFilter:
|
||
"""渠道会话筛选条件值对象(SES-BATCH-CLOSE-01)。
|
||
|
||
供 ``ChannelSessionRepositoryPort.findSessionsByFilter`` 使用,替换原
|
||
``dict[str, Any]`` 弱类型参数。所有字段均可选,但 ``__post_init__``
|
||
强制至少提供一个筛选条件,避免全表扫描。
|
||
|
||
字段:
|
||
channel_type: 渠道类型筛选(可选)。
|
||
unified_identity_id: 统一身份 ID 精确匹配(P3 渐进式绑定,
|
||
跨渠道会话合并场景,可选)。
|
||
inactive_before: 非活跃截止时间;``last_message_at`` 为 NULL
|
||
或早于该时间均视为非活跃(可选)。
|
||
status: 会话状态筛选(可选)。
|
||
"""
|
||
|
||
channel_type: ChannelType | None = None
|
||
unified_identity_id: str | None = None
|
||
inactive_before: datetime | None = None
|
||
status: SessionStatus | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
has_filter = any(
|
||
field is not None
|
||
for field in (
|
||
self.channel_type,
|
||
self.unified_identity_id,
|
||
self.inactive_before,
|
||
self.status,
|
||
)
|
||
)
|
||
if not has_filter:
|
||
raise ValidationError(
|
||
"filter",
|
||
"at least one filter field must be provided",
|
||
)
|