from __future__ import annotations import time from dataclasses import dataclass from typing import Any from yuxi.utils.logging_config import logger DEFAULT_TTL_SECONDS = 3600 MAX_CACHE_ENTRIES = 10000 @dataclass class SentMessageEntry: message_id: str channel_user_id: str chat_id: str content_hash: str mode: str sent_at: float expires_at: float metadata: dict[str, Any] | None = None class WeChatSendCache: def __init__(self, ttl_seconds: int = DEFAULT_TTL_SECONDS): self._cache: dict[str, SentMessageEntry] = {} self._ttl = ttl_seconds self._id_index: dict[str, str] = {} @staticmethod def _content_key(content: str) -> str: import hashlib return hashlib.sha256(content.encode()).hexdigest()[:12] def store( self, message_id: str, channel_user_id: str, chat_id: str, content: str, mode: str = "wecom", metadata: dict[str, Any] | None = None, ) -> None: self._prune() now = time.time() entry = SentMessageEntry( message_id=message_id, channel_user_id=channel_user_id, chat_id=chat_id, content_hash=self._content_key(content), mode=mode, sent_at=now, expires_at=now + self._ttl, metadata=metadata, ) self._cache[message_id] = entry self._id_index[message_id] = message_id logger.debug(f"[WeChat/SendCache] Stored message_id={message_id}") def get(self, message_id: str) -> SentMessageEntry | None: entry = self._cache.get(message_id) if entry and entry.expires_at < time.time(): self._cache.pop(message_id, None) self._id_index.pop(message_id, None) return None return entry def get_attached_results(self, message_id: str) -> dict[str, Any]: entry = self.get(message_id) if entry is None: return {} return { "message_id": entry.message_id, "channel_user_id": entry.channel_user_id, "chat_id": entry.chat_id, "sent_at": entry.sent_at, "mode": entry.mode, **(entry.metadata or {}), } def remove(self, message_id: str) -> None: self._cache.pop(message_id, None) self._id_index.pop(message_id, None) def hit_rate(self) -> float: total = len(self._cache) if total == 0: return 1.0 now = time.time() valid = sum(1 for e in self._cache.values() if e.expires_at > now) return valid / total def _prune(self) -> None: if len(self._cache) < MAX_CACHE_ENTRIES: return now = time.time() expired = [mid for mid, e in self._cache.items() if e.expires_at < now] for mid in expired: self._cache.pop(mid, None) self._id_index.pop(mid, None) if expired: logger.info(f"[WeChat/SendCache] Pruned {len(expired)} expired entries") def clear(self) -> None: self._cache.clear() self._id_index.clear()