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

272 lines
10 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。
定义跨层传递的领域事件值对象,供 ``EventPublisherPort`` 发布与订阅者消费。
事件载荷为不可变值对象(``dataclass(frozen=True)``),不泄露领域实体引用。
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Literal
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.outbox import OutboxStatus
from yuxi.channels.contract.dtos.plugin import DomainEvent
from yuxi.channels.contract.dtos.session import ChatType
from yuxi.channels.contract.errors import ValidationError
@dataclass(frozen=True)
class OutboxStateChangedEvent:
"""发件箱状态变更事件OBX-004
描述一条发件箱条目(``OutboxEntry``)的状态迁移,由应用层在 outbox
状态机推进时(``markSent`` / ``markFailed`` / ``markSuppressed`` /
``markSentUnconfirmed`` 等)产出,经 ``EventPublisherPort`` 分发给
订阅者。状态值取自 ``OutboxStatus`` 枚举的字符串形式,便于跨层序列化
与审计。
字段:
entry_id: 发件箱条目 ID``OutboxEntry.outbox_id``)。
old_state: 变更前状态(``OutboxStatus`` 字符串值)。
new_state: 变更后状态(``OutboxStatus`` 字符串值)。
reason: 变更原因(如投递成功、重试失败、栅栏抑制等)。
occurred_at: 事件发生时间。
"""
entry_id: str
old_state: OutboxStatus
new_state: OutboxStatus
reason: str
occurred_at: datetime
def __post_init__(self) -> None:
"""校验必填字段非空。
``entry_id`` / ``old_state`` / ``new_state`` / ``reason`` 必须非空,
在构造时即抛出 ``ValidationError``,避免空值事件传播到订阅者
INV-8
"""
if not self.entry_id:
raise ValidationError("entry_id", "must not be empty")
if not self.old_state:
raise ValidationError("old_state", "must not be empty")
if not self.new_state:
raise ValidationError("new_state", "must not be empty")
if not self.reason:
raise ValidationError("reason", "must not be empty")
@dataclass(frozen=True)
class OutboxEntryPurgedEvent:
"""发件箱条目被物理清理事件OBX-005
描述一批发件箱条目(``OutboxEntry``被物理删除purge的事件
调度器定时清理 dead / sent 终态条目时产出,经 ``EventPublisherPort``
分发给订阅者。``entry_ids`` 为本次被清理条目的业务 ID 列表,供下游
做审计、统计与缓存失效等处理。
字段:
entry_ids: 被物理清理的发件箱条目业务 ID 列表(``OutboxEntry.outbox_id``)。
occurred_at: 事件发生时间。
"""
entry_ids: tuple[str, ...]
occurred_at: datetime
def __post_init__(self) -> None:
"""校验 entry_ids 非空。
``entry_ids`` 必须为非空元组,空列表的清理事件无意义,在构造时即
抛出 ``ValidationError``避免空事件传播到订阅者INV-8
"""
if not self.entry_ids:
raise ValidationError("entry_ids", "must not be empty")
@dataclass(frozen=True)
class ChannelSessionUpdatedEvent:
"""渠道会话状态更新事件。
描述渠道会话生命周期变更(创建、关闭、合并、转移所有者),由入站流水线
或控制面处理器在事务提交后发布。
字段:
session_id: 渠道会话 ID。
channel_type: 渠道类型。
channel_account_id: 渠道账户 ID。
update_type: 更新类型created / closed / merged / transferred
occurred_at: 事件发生时间。
"""
session_id: str
channel_type: ChannelType
channel_account_id: str
update_type: Literal["created", "closed", "merged", "transferred"]
occurred_at: datetime
def __post_init__(self) -> None:
"""校验必填字段与枚举范围。"""
if not self.session_id:
raise ValidationError("session_id", "must not be empty")
if not self.channel_account_id:
raise ValidationError("channel_account_id", "must not be empty")
if self.channel_type is None or not self.channel_type:
raise ValidationError("channel_type", "must not be empty")
if self.update_type not in ("created", "closed", "merged", "transferred"):
raise ValidationError(
"update_type",
"must be one of: created, closed, merged, transferred",
)
if self.occurred_at is None:
raise ValidationError("occurred_at", "must not be None")
if not isinstance(self.occurred_at, datetime):
raise ValidationError("occurred_at", "must be a datetime")
def toDomainEvent(self) -> DomainEvent:
"""转换为契约层 ``DomainEvent``。"""
return DomainEvent(
event_id=str(uuid.uuid4()),
event_type="ChannelSessionUpdated",
payload={
"session_id": self.session_id,
"channel_type": str(self.channel_type),
"channel_account_id": self.channel_account_id,
"update_type": self.update_type,
"occurred_at": self.occurred_at.isoformat(),
},
timestamp=self.occurred_at,
trace_id=None,
)
@dataclass(frozen=True)
class ChannelMessageReceivedEvent:
"""渠道新用户消息到达事件。
描述入站流水线解析到用户消息后产生的事件,供 SSE 连接推送最新消息提醒。
字段:
session_id: 渠道会话 ID。
conversation_id: 内部会话 ID。
channel_type: 渠道类型。
chat_type: 会话类型p2p / group
content_preview: 脱敏后的消息内容预览(前 120 字符)。
occurred_at: 事件发生时间。
"""
session_id: str
conversation_id: str
channel_type: ChannelType
chat_type: ChatType
content_preview: str
occurred_at: datetime
def __post_init__(self) -> None:
"""校验必填字段与枚举范围。"""
if not self.session_id:
raise ValidationError("session_id", "must not be empty")
if not self.conversation_id:
raise ValidationError("conversation_id", "must not be empty")
if self.channel_type is None or not self.channel_type:
raise ValidationError("channel_type", "must not be empty")
if self.chat_type not in (ChatType.P2P, ChatType.GROUP):
raise ValidationError(
"chat_type",
"must be one of: p2p, group",
)
if self.content_preview is None or not self.content_preview:
raise ValidationError("content_preview", "must not be empty")
if self.occurred_at is None:
raise ValidationError("occurred_at", "must not be None")
if not isinstance(self.occurred_at, datetime):
raise ValidationError("occurred_at", "must be a datetime")
def toDomainEvent(self) -> DomainEvent:
"""转换为契约层 ``DomainEvent``。"""
return DomainEvent(
event_id=str(uuid.uuid4()),
event_type="ChannelMessageReceived",
payload={
"session_id": self.session_id,
"conversation_id": self.conversation_id,
"channel_type": str(self.channel_type),
"chat_type": self.chat_type.value,
"content_preview": self.content_preview,
"occurred_at": self.occurred_at.isoformat(),
},
timestamp=self.occurred_at,
trace_id=None,
)
@dataclass(frozen=True)
class ChannelMessageSentEvent:
"""渠道回复/管理员消息已发送事件。
描述出站流水线将 assistant 或 admin 消息持久化完成后产生的事件。
字段:
session_id: 渠道会话 ID。
conversation_id: 内部会话 ID。
message_id: 消息 ID。
role: 消息角色assistant / admin
channel_type: 渠道类型。
occurred_at: 事件发生时间。
"""
session_id: str
conversation_id: str
message_id: str
role: Literal["assistant", "admin"]
channel_type: ChannelType
occurred_at: datetime
def __post_init__(self) -> None:
"""校验必填字段与枚举范围。"""
if not self.session_id:
raise ValidationError("session_id", "must not be empty")
if not self.conversation_id:
raise ValidationError("conversation_id", "must not be empty")
if not self.message_id:
raise ValidationError("message_id", "must not be empty")
if self.role not in ("assistant", "admin"):
raise ValidationError(
"role",
"must be one of: assistant, admin",
)
if self.channel_type is None or not self.channel_type:
raise ValidationError("channel_type", "must not be empty")
if self.occurred_at is None:
raise ValidationError("occurred_at", "must not be None")
if not isinstance(self.occurred_at, datetime):
raise ValidationError("occurred_at", "must be a datetime")
def toDomainEvent(self) -> DomainEvent:
"""转换为契约层 ``DomainEvent``。"""
return DomainEvent(
event_id=str(uuid.uuid4()),
event_type="ChannelMessageSent",
payload={
"session_id": self.session_id,
"conversation_id": self.conversation_id,
"message_id": self.message_id,
"role": self.role,
"channel_type": str(self.channel_type),
"occurred_at": self.occurred_at.isoformat(),
},
timestamp=self.occurred_at,
trace_id=None,
)
__all__ = [
"OutboxStateChangedEvent",
"OutboxEntryPurgedEvent",
"ChannelSessionUpdatedEvent",
"ChannelMessageReceivedEvent",
"ChannelMessageSentEvent",
]