"""多渠道网关跨渠道身份关联。""" from __future__ import annotations import asyncio from yuxi.repositories.channel_identity_repository import ChannelIdentityRepository from yuxi.storage.postgres.manager import pg_manager class IdentityLinkResolver: def __init__( self, identity_links: dict[str, list[str]] | None = None, identity_repository: ChannelIdentityRepository | None = None, ): # { "canonical_name": ["feishu:ou_xxx", "dingtalk:12345", "telegram:67890"] } self._links = identity_links or {} self._reverse_index: dict[str, str] = {} self._repository = identity_repository self._lock = asyncio.Lock() for canonical, ids in self._links.items(): for linked_id in ids: self._reverse_index[linked_id] = canonical @classmethod async def create(cls, identity_repository: ChannelIdentityRepository | None = None) -> IdentityLinkResolver: """从数据库加载全量绑定关系并构建内存索引。 若未提供 repository,则使用 pg_manager 新建会话加载; 运行期 link/unlink 会复用初始化时传入的 repository,否则每次新建会话。 """ if identity_repository is not None: records = await identity_repository.load_all() return cls._from_records(records, identity_repository) async with pg_manager.get_async_session_context() as db: repo = ChannelIdentityRepository(db) records = await repo.load_all() return cls._from_records(records, repo) @classmethod def _from_records( cls, records: list, identity_repository: ChannelIdentityRepository | None, ) -> IdentityLinkResolver: links: dict[str, list[str]] = {} for record in records: canonical = record.platform_user_id linked_id = f"{record.channel_type}:{record.channel_sender_id}" links.setdefault(canonical, []).append(linked_id) return cls(identity_links=links, identity_repository=identity_repository) def resolve(self, channel_type: str, peer_id: str) -> str: key = f"{channel_type}:{peer_id}" if key in self._reverse_index: return self._reverse_index[key] if peer_id in self._reverse_index: return self._reverse_index[peer_id] return peer_id async def link( self, channel_type: str, channel_sender_id: str, platform_user_id: str, account_id: str, paired_by: str = "qr", ) -> None: linked_id = f"{channel_type}:{channel_sender_id}" async with self._lock: if self._repository is not None: await self._repository.create( channel_type=channel_type, account_id=account_id, channel_sender_id=channel_sender_id, platform_user_id=platform_user_id, paired_by=paired_by, ) else: async with pg_manager.get_async_session_context() as db: repo = ChannelIdentityRepository(db) await repo.create( channel_type=channel_type, account_id=account_id, channel_sender_id=channel_sender_id, platform_user_id=platform_user_id, paired_by=paired_by, ) self._links.setdefault(platform_user_id, []).append(linked_id) self._reverse_index[linked_id] = platform_user_id async def unlink( self, channel_type: str, account_id: str, channel_sender_id: str, ) -> bool: linked_id = f"{channel_type}:{channel_sender_id}" async with self._lock: if self._repository is not None: deleted = await self._repository.delete_by_sender( channel_type=channel_type, account_id=account_id, channel_sender_id=channel_sender_id, ) else: async with pg_manager.get_async_session_context() as db: repo = ChannelIdentityRepository(db) deleted = await repo.delete_by_sender( channel_type=channel_type, account_id=account_id, channel_sender_id=channel_sender_id, ) if deleted: self._reverse_index.pop(linked_id, None) for canonical, ids in list(self._links.items()): if linked_id in ids: ids.remove(linked_id) if not ids: self._links.pop(canonical, None) break return deleted