"""目录查询 DTO。 定义渠道通讯录目录查询相关的不可变值对象,包括目录条目类型枚举、 渠道用户、渠道群组、群组成员、目录条目、目录查询与目录搜索结果。所有 DTO 均为 ``dataclass(frozen=True)``,仅依赖标准库,用于渠道通讯录 查询与用户 / 群组检索(FR-14)。集合字段使用 tuple 以保证 frozen dataclass 的不可变语义。 """ 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.channel import ChannelType from yuxi.channels.contract.dtos.common import Operator from yuxi.channels.contract.errors import ValidationError class DirectoryEntryType(StrEnum): """目录条目类型。 标识目录条目的类型,用于区分用户与群组。继承 ``str, Enum`` 以 支持 JSON 序列化与字符串比较。 取值: USER: 用户。 GROUP: 群组。 """ USER = "user" GROUP = "group" @dataclass(frozen=True) class ChannelUser: """渠道用户。 描述渠道侧用户信息,包括对端 ID、名称与可选的头像、邮箱、电话, 用于目录查询结果展示与身份关联。 字段: peer_id: 对端 ID。 name: 名称。 avatar_url: 头像 URL(可选)。 email: 邮箱(可选)。 phone: 电话(可选)。 """ peer_id: str name: str avatar_url: str | None = None email: str | None = None phone: str | None = None @dataclass(frozen=True) class ChannelGroup: """渠道群组。 描述渠道侧群组信息,包括群组 ID、名称与成员数,用于目录查询 结果展示。 字段: group_id: 群组 ID。 name: 名称。 member_count: 成员数(默认 0)。 """ group_id: str name: str member_count: int = 0 @dataclass(frozen=True) class DirectoryEntry: """目录条目。 描述目录查询的统一条目,包括类型、ID、名称与可选元数据,用于 目录搜索结果的统一展示。 字段: type: 条目类型(user | group)。 id: 条目 ID。 name: 名称。 metadata: 渠道侧元数据(可选)。 avatar_url: 头像 URL(可选)。 alias: 别名(可选)。 remark: 备注(可选)。 tags: 标签列表(可选)。 member_count: 成员数(可选)。 owner_id: 所有者 ID(可选)。 extra_metadata: 扩展元数据(可选)。 """ type: DirectoryEntryType id: str name: str metadata: dict[str, Any] | None = None avatar_url: str | None = None alias: str | None = None remark: str | None = None tags: tuple[str, ...] | None = None member_count: int | None = None owner_id: str | None = None extra_metadata: dict[str, Any] | None = None @dataclass(frozen=True) class DirectoryQuery: """目录查询。 描述目录查询请求,包括关键词、游标与分页限制,用于 FR-14 目录 查询的分页检索。 字段: keyword: 关键词(可选)。 cursor: 游标(可选)。 limit: 分页限制(默认 20)。 sort_by: 排序字段(可选)。 sort_order: 排序方向(可选,asc 或 desc)。 """ keyword: str | None = None cursor: str | None = None limit: int = 20 sort_by: str | None = None sort_order: Literal["asc", "desc"] | None = None def __post_init__(self) -> None: """校验 limit 为正整数与 sort_order 取值。 ``limit`` 必须为正整数,``sort_order`` 必须为 ``asc`` / ``desc`` / ``None`` 之一,在构造时即抛出 ``ValidationError``, adapter 不再做该校验(INV-8)。 """ if self.limit <= 0: raise ValidationError("limit", "must be a positive integer") if self.sort_order is not None and self.sort_order not in ("asc", "desc"): raise ValidationError("sort_order", "must be one of: asc, desc") @dataclass(frozen=True) class DirectorySearchResult: """目录搜索结果。 描述目录查询的结果,包括条目列表与可选的下一页游标,用于 FR-14 目录查询的分页返回。集合字段使用 tuple 以保证不可变。 字段: entries: 条目列表。 next_cursor: 下一页游标(可选)。 """ entries: tuple[DirectoryEntry, ...] next_cursor: str | None = None @dataclass(frozen=True) class DirectorySearchSuggestionQuery: """目录搜索建议查询。 描述目录搜索建议请求,包括关键词与返回数量限制,用于根据关键词 快速返回匹配的目录条目建议。 字段: keyword: 关键词。 limit: 返回数量限制(默认 10)。 """ keyword: str limit: int = 10 def __post_init__(self) -> None: """校验 keyword 非空且 limit 为正整数。""" if not self.keyword: raise ValidationError("keyword", "must not be empty") if self.limit <= 0: raise ValidationError("limit", "must be a positive integer") @dataclass(frozen=True) class DirectorySearchSuggestionResult: """目录搜索建议结果。 描述目录搜索建议的返回结果,包括匹配的目录条目列表。集合字段 使用 tuple 以保证不可变。 字段: entries: 条目列表。 """ entries: tuple[DirectoryEntry, ...] @dataclass(frozen=True) class GroupMember: """群组成员。 描述渠道群组中的单个成员信息,包括用户 ID、群组 ID、角色与加入时间, 用于 ``DirectoryAdapter.getGroupMembers`` 方法的返回元素(FR-14)。 字段: user_id: 用户 ID。 group_id: 群组 ID。 role: 成员角色(可选,如 "owner" / "admin" / "member")。 joined_at: 加入时间(可选)。 """ user_id: str group_id: str role: Literal["owner", "admin", "member"] | None = None joined_at: datetime | None = None @dataclass(frozen=True) class GroupMemberResult: """群组成员查询结果。 描述群组成员查询的分页结果,包括成员列表与可选的下一页游标,用于 ``DirectoryAdapter.getGroupMembers`` 方法的返回值(FR-14)。集合字段 使用 tuple 以保证不可变。 字段: members: 成员列表。 next_cursor: 下一页游标(可选)。 """ members: tuple[GroupMember, ...] next_cursor: str | None = None @dataclass(frozen=True) class BatchDirectoryProfilesQuery: """批量目录资料查询。 用于路由绑定列表等场景,把多个 peer_id(可能是用户或群组 ID) 一次性翻译成可读的名称、头像等资料,减少前端并发请求。 字段: peer_ids: 待查询的对端 ID 集合(去重后传入,顺序无关)。 types: 按条目类型过滤(可选)。 """ peer_ids: tuple[str, ...] types: tuple[DirectoryEntryType, ...] | None = None def __post_init__(self) -> None: """校验 peer_ids 非空且元素均为非空字符串。""" if not self.peer_ids: raise ValidationError("peer_ids", "must not be empty") if any(not isinstance(pid, str) or not pid for pid in self.peer_ids): raise ValidationError("peer_ids", "all peer_ids must be non-empty strings") @dataclass(frozen=True) class BatchDirectoryProfilesResult: """批量目录资料查询结果。 profiles 以 peer_id 为 key;value 为统一目录条目,type 区分 user/group, metadata 携带用户/群组的原始字段。未命中的 peer_id 不会出现在结果中。 """ profiles: dict[str, DirectoryEntry] @dataclass(frozen=True) class ClearDirectoryCacheCmd: """清理目录缓存命令(DIR-CACHE-CLEAR)。 由 ``DirectoryPort.clearDirectoryCache`` 引用,清理指定范围的目录缓存, 需记录操作人以满足审计要求。 字段: channel_type: 渠道类型。 account_id: 渠道账户 ID。 operator: 操作人(审计用)。 scope: 清理范围(``users`` / ``groups`` / ``all``)。 """ channel_type: ChannelType account_id: str operator: Operator scope: Literal["users", "groups", "all"] def __post_init__(self) -> None: """校验必填字段非空与 scope 取值(DIR-CACHE-CLEAR)。 ``account_id`` 与 ``scope`` 必须非空,``scope`` 必须为 ``users`` / ``groups`` / ``all`` 之一,在构造时即抛出 ``ValidationError``, adapter 不再做该校验(INV-8)。 """ if not self.account_id: raise ValidationError("account_id", "must not be empty") if not self.scope: raise ValidationError("scope", "must not be empty") if self.scope not in ("users", "groups", "all"): raise ValidationError( "scope", "must be one of: users, groups, all", ) @dataclass(frozen=True) class ClearDirectoryCacheResult: """清理目录缓存结果(DIR-CACHE-CLEAR)。 字段: cleared_keys: 清理的缓存 key 数量。 cleared_at: 清理时间戳。 """ cleared_keys: int cleared_at: datetime