"""UI 自动化幂等缓存:防止短时间内重复发送相同消息。 key 由 (flow_name, to_wxid, content, client_request_id) 经 SHA256 完整哈希生成, 5 分钟 TTL,最多 1000 条。命中时返回缓存的 SendResult,避免重复发送。 LRU 淘汰 + TTL 过期双策略,保证内存占用有上限且旧数据自动失效。 """ from __future__ import annotations import hashlib import logging import time from collections import OrderedDict from typing import Any, Optional logger = logging.getLogger("woc-bridge") class IdemCache: """幂等缓存:LRU + TTL 双策略。 Attributes: ttl: 缓存存活秒数,默认 300(5 分钟) max_size: 最大缓存条目数,默认 1000 _cache: OrderedDict[key_str, (value, expire_at)] """ def __init__(self, ttl: float = 300.0, max_size: int = 1000) -> None: """初始化幂等缓存。 Args: ttl: 缓存存活秒数 max_size: 最大缓存条目数 """ self.ttl = ttl self.max_size = max_size self._cache: OrderedDict[str, tuple[Any, float]] = OrderedDict() @staticmethod def _make_key( flow_name: str, to_wxid: str, content: str, client_request_id: str = "", ) -> str: """生成缓存 key:完整 SHA256(不截断)。 Args: flow_name: 流程名(如 "send_text") to_wxid: 目标 wxid content: 消息内容 client_request_id: 客户端请求 ID(可选,用于显式幂等) Returns: 64 字符 hex SHA256 摘要 """ raw = f"{flow_name}|{to_wxid}|{content}|{client_request_id}" return hashlib.sha256(raw.encode("utf-8")).hexdigest() def get( self, flow_name: str, to_wxid: str, content: str, client_request_id: str = "", ) -> Optional[Any]: """查询缓存。 命中且未过期:move_to_end(LRU),返回 value。 命中但已过期:删除,返回 None。 未命中:返回 None。 Args: flow_name: 流程名 to_wxid: 目标 wxid content: 消息内容 client_request_id: 客户端请求 ID Returns: 缓存的 value 或 None """ key = self._make_key(flow_name, to_wxid, content, client_request_id) entry = self._cache.get(key) if entry is None: return None value, expire_at = entry if time.monotonic() >= expire_at: # TTL 过期,删除 del self._cache[key] logger.debug("[idem] key expired: %s", key[:12]) return None # LRU:移到末尾 self._cache.move_to_end(key) logger.debug("[idem] hit: %s", key[:12]) return value def set( self, flow_name: str, to_wxid: str, content: str, value: Any, client_request_id: str = "", ) -> None: """写入缓存。 若 key 已存在则更新 value 并 move_to_end。 超限 max_size 时 popitem(last=False) 删最旧。 Args: flow_name: 流程名 to_wxid: 目标 wxid content: 消息内容 value: 缓存的值(通常为 FlowResult) client_request_id: 客户端请求 ID """ key = self._make_key(flow_name, to_wxid, content, client_request_id) expire_at = time.monotonic() + self.ttl if key in self._cache: self._cache.move_to_end(key) self._cache[key] = (value, expire_at) # LRU 淘汰 while len(self._cache) > self.max_size: self._cache.popitem(last=False) logger.debug("[idem] set: %s (size=%d)", key[:12], len(self._cache)) def clear(self) -> None: """清空缓存。""" self._cache.clear() logger.info("[idem] cache cleared")