ForcePilot/backend/package/yuxi/channels/contract/dtos/session.py
Kris b8ac375e8e feat: 新增多渠道客服会话、身份合并与重试能力等功能
本次提交包含多项核心功能迭代与优化:
1. 新增KF客服会话类型,完善聊天类型枚举
2. 新增消息撤回操作类型与身份置信度排序方法
3. 新增控制面结果DTO与敏感字段注册表端口
4. 新增身份合并回滚、重试失败投递目标等业务能力
5. 优化Outbox投递逻辑与熔断器状态判断
6. 修复部分代码冗余与类型不匹配问题
7. 新增数据库索引并发创建与路由绑定清理逻辑
8. 优化会话关闭服务与插件重载并发控制
2026-07-09 04:21:28 +08:00

269 lines
8.0 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。
定义渠道会话相关的不可变值对象,包括渠道会话 ID、对端 ID、会话类型、
会话所有者、所有者转移命令与临时会话模式。所有 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.dtos.common import BatchOperationFailure, Operator
from yuxi.channels.contract.errors import ValidationError
@dataclass(frozen=True)
class ChannelSessionId:
"""渠道会话 ID。
标识渠道侧一次会话的唯一 ID用于会话定位与路由匹配。
字段:
value: 渠道会话 ID 字符串。
"""
value: str
@dataclass(frozen=True)
class PeerId:
"""对端 ID。
标识渠道侧会话对端(用户或群组)的唯一 ID用于会话解析与身份关联。
字段:
value: 对端 ID 字符串。
"""
value: str
class ChatType(StrEnum):
"""会话类型。
标识渠道侧会话的拓扑类型,用于路由匹配与会话所有者策略。继承
``str, Enum`` 以支持 JSON 序列化与字符串比较。
取值:
P2P: 单聊(点对点)。
GROUP: 群聊。
KF: 客服会话(微信客服等渠道的客服窗口,独立于群聊)。
"""
P2P = "p2p"
GROUP = "group"
KF = "kf"
@dataclass(frozen=True)
class SessionOwner:
"""会话所有者。
描述会话的所有者信息,用于 FR-26 所有者保护策略,确保仅所有者可触发
关键操作。
字段:
conversation_id: 所属会话 ID。
owner_peer_id: 所有者对端 ID。
created_at: 所有者关系建立时间。
"""
conversation_id: str
owner_peer_id: str
created_at: datetime
@dataclass(frozen=True)
class OwnerTransferCmd:
"""所有者转移命令FR-26
由端口方法 ``ConversationPort.transferSessionOwner`` 引用,用于将会话
所有者从当前对端转移至新对端需记录操作人以满足审计要求FR-26
字段:
conversation_id: 目标会话 ID。
new_owner_id: 新所有者对端 ID。
operator: 操作人(审计用)。
"""
conversation_id: str
new_owner_id: str
operator: Operator
def __post_init__(self) -> None:
"""校验必填字段非空FR-26
``conversation_id`` 与 ``new_owner_id`` 必须非空,在构造时即抛出
``ValidationError``adapter 不再做该校验INV-8
"""
if not self.conversation_id:
raise ValidationError("conversation_id", "must not be empty")
if not self.new_owner_id:
raise ValidationError("new_owner_id", "must not be empty")
@dataclass(frozen=True)
class TemporarySessionPattern:
"""临时会话模式。
描述临时会话的匹配模式FR-27用于识别周期性任务产生的临时会话
支持 cron 等模式表达式。
字段:
pattern: 模式表达式(如 ``cron:<任务 ID>:<运行 ID>``)。
description: 模式描述。
"""
pattern: str
description: str
@dataclass(frozen=True)
class CloseSessionCmd:
"""关闭会话命令FR-27
由 ``SessionManagementPort.closeSession`` 端口方法引用,用于关闭指定
渠道会话,停止接收新消息。需记录操作人以满足审计要求。
字段:
session_id: 要关闭的会话 ID。
reason: 关闭原因(审计用,可选)。
operator: 操作人(审计用)。
"""
session_id: str
operator: Operator
reason: str | None = None
def __post_init__(self) -> None:
"""校验必填字段非空。
``session_id`` 必须非空,在构造时即抛出 ``ValidationError``
adapter 不再做该校验INV-8
"""
if not self.session_id:
raise ValidationError("session_id", "must not be empty")
@dataclass(frozen=True)
class SessionMessageItem:
"""会话消息列表项。
描述渠道会话消息列表中的单条消息,包含消息核心字段与渠道侧状态。
字段:
message_id: 消息 ID。
conversation_id: 会话 ID。
channel_type: 渠道类型。
role: 消息角色user | assistant | admin
content: 消息内容。
channel_msg_id: 渠道侧消息 ID可选
channel_status: 渠道侧消息状态(可选)。
created_at: 创建时间。
"""
message_id: str
conversation_id: str
channel_type: str
role: Literal["user", "assistant", "admin"]
content: str
created_at: datetime
channel_msg_id: str | None = None
channel_status: str | None = None
@dataclass(frozen=True)
class SessionStatsResult:
"""会话统计结果SES-STATS-01
描述单会话的统计指标,由 dispatch handler 组合 ConversationPort 消息
查询后计算,不在核心层引入统计聚合根。
字段:
message_count: 消息总数。
user_message_count: 用户消息数。
assistant_message_count: 助手消息数。
started_at: 会话开始时间。
last_activity_at: 最近活动时间。
duration_seconds: 会话时长(秒)。
first_response_seconds: 首条用户消息到首条助手消息的间隔(秒,
无助手消息时为 None
avg_response_seconds: 平均响应间隔(秒,无助手消息时为 None
"""
message_count: int
user_message_count: int
assistant_message_count: int
started_at: datetime
last_activity_at: datetime
duration_seconds: int
first_response_seconds: int | None = None
avg_response_seconds: float | None = None
@dataclass(frozen=True)
class BatchCloseSessionsCmd:
"""批量关闭会话命令SES-BATCH-CLOSE-01
由 ``SessionManagementPort.batchCloseSessions`` 引用,支持显式 ID 列表
或筛选条件两种模式(二者不可同时空)。``session_ids`` 与 ``filter``
同时为空、``max_count`` 越界均在 ``__post_init__`` 中校验adapter 不再
重复校验INV-8
字段:
session_ids: 显式会话 ID 元组(默认空元组)。
filter: 筛选条件(含 channel_type / inactive_before / status
可选)。
max_count: 单次最大关闭数(默认 1001-1000
reason: 关闭原因(审计用,可选)。
operator: 操作人(审计用)。
"""
operator: Operator
session_ids: tuple[str, ...] = ()
filter: dict[str, Any] | None = None
max_count: int = 100
reason: str | None = None
def __post_init__(self) -> None:
"""校验必填字段与业务规则SES-BATCH-CLOSE-01
- ``session_ids`` 与 ``filter`` 不可同时为空
- ``max_count`` ∈ [1, 1000]
在构造时即抛出 ``ValidationError``adapter 不再做该校验INV-8
"""
if not self.session_ids and not self.filter:
raise ValidationError(
"session_ids",
"either session_ids or filter must be provided",
)
if self.max_count < 1 or self.max_count > 1000:
raise ValidationError(
"max_count",
f"max_count must be in [1, 1000], got {self.max_count}",
)
@dataclass(frozen=True)
class BatchCloseResult:
"""批量关闭会话结果SES-BATCH-CLOSE-01
描述逐条独立事务关闭会话的执行结果,``failed`` 使用通用
``BatchOperationFailure````id`` 字段承载 session_id
字段:
total: 待关闭会话总数。
closed: 成功关闭的会话 ID 元组。
failed: 失败条目元组。
"""
total: int
closed: tuple[str, ...]
failed: tuple[BatchOperationFailure, ...]