新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
516 lines
16 KiB
Python
516 lines
16 KiB
Python
"""渠道类型 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.dtos.common import BatchOperationFailure, Operator
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
|
||
|
||
class ChannelType(StrEnum):
|
||
"""渠道类型。
|
||
|
||
标识多渠道网关支持的外部渠道种类,用于路由匹配、账户管理与插件解析。
|
||
继承 ``str, Enum`` 以支持 JSON 序列化与字符串比较。
|
||
|
||
取值:
|
||
FEISHU: 飞书。
|
||
DINGTALK: 钉钉。
|
||
WECOM: 企业微信。
|
||
WEBCHAT: 内置 Web 渠道(FR-31)。
|
||
TELEGRAM: Telegram。
|
||
DISCORD: Discord。
|
||
WHATSAPP: WhatsApp。
|
||
CUSTOM: 自定义渠道。
|
||
|
||
约束:
|
||
框架层(contract / core / application)**禁止** 在任何决策语句中
|
||
分支到具体枚举值(仅插件自身代码可分支),渠道特性决策 **必须**
|
||
通过 ``ChannelManifest`` 声明字段驱动(如 ``critical`` /
|
||
``requires_dm_pairing`` / ``requires_outbound_delivery``)。
|
||
新增渠道优先用 ``CUSTOM`` + manifest 声明,避免修改框架契约层枚举。
|
||
"""
|
||
|
||
FEISHU = "feishu"
|
||
DINGTALK = "dingtalk"
|
||
WECOM = "wecom"
|
||
WEBCHAT = "webchat"
|
||
TELEGRAM = "telegram"
|
||
DISCORD = "discord"
|
||
WHATSAPP = "whatsapp"
|
||
CUSTOM = "custom"
|
||
|
||
|
||
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 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: 凭据最近轮换时间(可选)。
|
||
"""
|
||
|
||
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
|
||
|
||
|
||
@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)。
|
||
"""
|
||
|
||
session_id: str
|
||
channel_type: ChannelType
|
||
account_id: str
|
||
peer_id: str
|
||
chat_type: str
|
||
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
|
||
|
||
|
||
@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: 身份来源。
|
||
created_at: 创建时间。
|
||
updated_at: 更新时间。
|
||
"""
|
||
|
||
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
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
|
||
@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: str
|
||
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: str
|
||
content: str
|
||
channel_status: str | None = None
|
||
channel_msg_id: str | None = None
|
||
ref_channel_msg_id: str | None = None
|
||
channel_status_history: list[dict] | None = None
|
||
operations_history: list[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
|
||
|
||
|
||
@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_session_id: 渠道会话 ID(可选)。
|
||
channel_type: 渠道类型。
|
||
role: 消息角色(user | assistant | admin)。
|
||
content: 消息内容。
|
||
created_at: 创建时间。
|
||
snippet: 匹配片段(高亮后,可选)。
|
||
"""
|
||
|
||
message_id: str
|
||
conversation_id: str
|
||
channel_type: ChannelType
|
||
role: str
|
||
content: str
|
||
created_at: datetime
|
||
channel_session_id: str | None = None
|
||
snippet: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class WebhookTestCmd:
|
||
"""Webhook 测试命令(WHK-TEST)。
|
||
|
||
由 ``AccountManagementPort.testWebhook`` 引用,触发对指定渠道账户的
|
||
webhook 测试事件投递。``account_id`` 缺省时由 dispatch handler 取该
|
||
渠道首个账户。
|
||
|
||
字段:
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账户 ID(可选,缺省取首个账户)。
|
||
event_type: 测试事件类型(默认 ``test_event``)。
|
||
payload: 测试事件负载(可选)。
|
||
operator: 操作人(审计用)。
|
||
"""
|
||
|
||
channel_type: ChannelType
|
||
operator: Operator
|
||
account_id: str | None = None
|
||
event_type: str = "test_event"
|
||
payload: dict[str, Any] | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class WebhookTestResult:
|
||
"""Webhook 测试结果(WHK-TEST)。
|
||
|
||
描述测试事件投递的结果,由插件 ``WebhookTestable.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: str
|
||
operator: Operator
|
||
account_ids: tuple[str, ...] = ()
|
||
filter: dict[str, Any] | None = None
|
||
reason: str | None = None
|
||
|
||
|
||
@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
|