import asyncio import logging from datetime import datetime, timedelta, UTC from yuxi.channel.extensions.jd.client import JOSClient logger = logging.getLogger(__name__) class SessionEntry: __slots__ = ("chat_id", "user_id", "messages", "turn_count", "created_at", "updated_at", "status") def __init__(self, chat_id: str, user_id: str): self.chat_id = chat_id self.user_id = user_id self.messages: list[dict] = [] self.turn_count = 0 self.created_at = datetime.now(UTC) self.updated_at = datetime.now(UTC) self.status = "active" class JDSession: MAX_HISTORY = 50 SESSION_TIMEOUT_SECONDS = 1800 CLEANUP_INTERVAL = 600 def __init__(self, client: JOSClient | None = None): self._sessions: dict[str, SessionEntry] = {} self._client = client self._cleanup_task: asyncio.Task | None = None async def start(self): self._cleanup_task = asyncio.create_task(self._cleanup_loop()) async def stop(self): if self._cleanup_task: self._cleanup_task.cancel() try: await self._cleanup_task except asyncio.CancelledError: pass self._cleanup_task = None def add_message(self, chat_id: str, user_id: str, msg: dict): session = self._sessions.get(chat_id) if session is None: session = SessionEntry(chat_id, user_id) self._sessions[chat_id] = session session.messages.append({**msg, "_timestamp": datetime.now(UTC).isoformat()}) if len(session.messages) > self.MAX_HISTORY: session.messages.pop(0) if session.status != "active": session.status = "active" session.turn_count = 1 else: session.turn_count += 1 session.updated_at = datetime.now(UTC) def get_history(self, chat_id: str, limit: int = 20) -> list[dict]: session = self._sessions.get(chat_id) if session is None: return [] return session.messages[-limit:] def get_session_context(self, chat_id: str) -> dict | None: session = self._sessions.get(chat_id) if session is None: return None return { "chat_id": session.chat_id, "user_id": session.user_id, "turn_count": session.turn_count, "status": session.status, "created_at": session.created_at.isoformat(), "updated_at": session.updated_at.isoformat(), "message_count": len(session.messages), } def close_session(self, chat_id: str): session = self._sessions.get(chat_id) if session: session.status = "closed" session.updated_at = datetime.now(UTC) def clear_session(self, chat_id: str): self._sessions.pop(chat_id, None) async def fetch_remote_history( self, chat_id: str, to_id: str, access_token: str, page: int = 1, page_size: int = 20 ) -> list[dict]: if self._client is None: return [] biz_params = { "chat_id": chat_id, "to_id": to_id, "page": str(page), "page_size": str(page_size), } try: data = await self._client.call("jingdong.im.getChatLog", biz_params, access_token) messages = data.get("jingdong_im_getChatLog_responce", {}).get("result", {}).get("messages", []) return messages if isinstance(messages, list) else [] except Exception: logger.exception("Failed to fetch JD chat history for %s", chat_id) return [] async def _cleanup_loop(self): while True: try: await asyncio.sleep(self.CLEANUP_INTERVAL) self._expire_sessions() except asyncio.CancelledError: break except Exception: logger.exception("JD session cleanup loop error") def _expire_sessions(self): cutoff = datetime.now(UTC) - timedelta(seconds=self.SESSION_TIMEOUT_SECONDS) expired = [chat_id for chat_id, session in self._sessions.items() if session.updated_at < cutoff] for chat_id in expired: session = self._sessions.pop(chat_id) logger.debug("JD session expired: chat_id=%s turns=%d", session.chat_id, session.turn_count)