"""渠道会话管理器""" import uuid from hashlib import md5 from sqlalchemy import select, text from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from yuxi.channel.plugins.protocol import ( BindingRoute, ChannelPlugin, InboundMessage, SessionConversationRef, ) from yuxi.channel.routing.router import BindingRouter from yuxi.channel.session.user_mapping import build_channel_user_uid from yuxi.repositories.channel_session_repository import ChannelSessionRepository from yuxi.repositories.conversation_repository import ConversationRepository from yuxi.storage.postgres.model_channel import ChannelSession from yuxi.storage.postgres.models_business import User class SessionManager: def __init__( self, binding_router: BindingRouter, ) -> None: self.router = binding_router async def resolve( self, config: dict, plugin: ChannelPlugin, inbound: InboundMessage, db_session: AsyncSession, ) -> tuple[ChannelSession, BindingRoute | None]: """解析渠道会话,返回会话与首次解析出的路由(新建会话时)。 若会话已存在,则路由为 None,由调用方按需复用缓存或重新解析。 """ # 1. 插件自定义解析 ref = plugin.resolve_session_conversation( inbound.chat_type, inbound.peer_id or inbound.sender_id or "", ) # 2. 通用回退 if ref is None: ref = self._fallback_resolve(plugin, inbound) # 3. 解析插件级回复与线程策略 reply_to_mode = plugin.resolve_reply_to_mode(config, inbound) auto_thread_id = plugin.resolve_auto_thread_id(config, inbound) if reply_to_mode is not None: ref.channel_metadata["reply_to_mode"] = reply_to_mode if auto_thread_id: if auto_thread_id not in ref.parent_conversation_candidates: ref.parent_conversation_candidates.append(auto_thread_id) ref.channel_metadata["auto_thread_id"] = auto_thread_id if ref.parent_conversation_candidates: ref.channel_metadata["parent_conversation_candidates"] = ref.parent_conversation_candidates session_repo = ChannelSessionRepository(db_session) # 4. Postgres 下对 session_key 加事务级咨询锁,串行化新建会话,避免并发冲突 await self._acquire_session_key_lock(db_session, ref.session_key) # 5. 在锁保护下重新查询已有 ChannelSession session = await session_repo.get_by_session_key(ref.session_key) if session is not None: updates: dict = {} if reply_to_mode is not None: updates["reply_to_mode"] = reply_to_mode if auto_thread_id: updates["auto_thread_id"] = auto_thread_id if ref.parent_conversation_candidates: updates["parent_conversation_candidates"] = ref.parent_conversation_candidates if updates: session.update_channel_metadata(updates) await db_session.flush() return session, None # 6. 先确定 agent_id(必须在创建 Conversation 前) route = await self.router.resolve_runtime(config, plugin, ref, db_session) agent_id = route.agent_id or config.get("default_agent_id") if not agent_id: raise ValueError(f"Unable to resolve agent_id for session {ref.session_key}") # 7. 创建/获取账户级虚拟用户(复用同一事务) channel_user = await self._get_or_create_channel_user( channel_type=config["channel_type"], account_id=config["account_id"], db_session=db_session, ) # 8. 创建 Conversation channel_metadata = { "account_id": config["account_id"], "sender_id": inbound.sender_id, "sender_name": inbound.sender_name, } conversation_repo = ConversationRepository(db_session) conversation = await conversation_repo.create_conversation( uid=channel_user.uid, agent_id=agent_id, thread_id=self._session_key_to_thread_id(ref.session_key), metadata={ "channel_type": config["channel_type"], "channel_session_id": ref.session_key, "channel_metadata": channel_metadata, }, channel_type=config["channel_type"], channel_session_id=ref.session_key, channel_metadata=channel_metadata, auto_commit=False, ) # 9. 创建 ChannelSession(非 Postgres 环境仍可能冲突,兜底重取并丢弃当前路由) try: session = await session_repo.create( session_key=ref.session_key, channel_type=config["channel_type"], account_id=config["account_id"], chat_type=inbound.chat_type, channel_sender_id=inbound.sender_id, conversation_id=conversation.id, channel_metadata=ref.channel_metadata, auto_commit=False, ) except IntegrityError: await db_session.rollback() session = await session_repo.get_by_session_key(ref.session_key) if session is None: raise await db_session.refresh(session) return session, None return session, route async def _acquire_session_key_lock(self, db_session: AsyncSession, session_key: str) -> None: """在 Postgres 下对 session_key 加事务级咨询锁,避免并发新建同一会话。 非 Postgres 方言不执行任何操作,依赖外层 IntegrityError 兜底。 """ bind = db_session.bind if bind is None or bind.dialect.name != "postgresql": return lock_id = self._session_key_lock_id(session_key) await db_session.execute(text("SELECT pg_advisory_xact_lock(:lock_id)").bindparams(lock_id=lock_id)) def _session_key_lock_id(self, session_key: str) -> int: """把 session_key 映射为 64 位有符号整数,供 pg_advisory_xact_lock 使用。""" return int(md5(session_key.encode()).hexdigest()[:15], 16) async def record_message( self, session: ChannelSession, inbound: InboundMessage, db_session: AsyncSession, ) -> None: """更新 ChannelSession 最后消息时间等轻量元数据。""" session_repo = ChannelSessionRepository(db_session) await session_repo.update_last_message_at( session, channel_message_id=inbound.channel_message_id, ) async def _get_or_create_channel_user( self, channel_type: str, account_id: str, db_session: AsyncSession, ) -> User: uid = build_channel_user_uid(channel_type, account_id) result = await db_session.execute(select(User).where(User.uid == uid)) user = result.scalar_one_or_none() if user is not None: return user user = User( uid=uid, username=f"{channel_type}:{account_id}", password_hash="", role="user", is_channel_user=True, channel_type=channel_type, ) db_session.add(user) try: await db_session.flush() except IntegrityError: await db_session.rollback() result = await db_session.execute(select(User).where(User.uid == uid)) user = result.scalar_one_or_none() if user is None: raise return user def _fallback_resolve(self, plugin: ChannelPlugin, inbound: InboundMessage) -> SessionConversationRef: return SessionConversationRef( session_key=plugin.parse_session_key(inbound), chat_type=inbound.chat_type, channel_sender_id=inbound.sender_id, channel_metadata={"fallback": True}, ) def _session_key_to_thread_id(self, session_key: str) -> str: return uuid.uuid5(uuid.NAMESPACE_OID, session_key).hex