"""多渠道网关 Bot 循环防护。""" from __future__ import annotations from collections import OrderedDict, deque from typing import TYPE_CHECKING from yuxi.channel.constants import InboundRejectionReason from .policy import SecurityCheckResult if TYPE_CHECKING: from .registry import SecurityContext class BotLoopDetector: """按 sender_id 维护近期 bot 消息比例,避免全局误杀。 使用 OrderedDict 实现 LRU 淘汰,当跟踪的 sender 数量超过 ``max_senders`` 时自动移除最久未活跃的条目,防止内存无限增长。 """ name = "bot_loop" default_priority = 300 def __init__(self, window_size: int = 10, max_bot_replies: int = 3, max_senders: int = 10000): self.window_size = window_size self.max_bot_replies = max_bot_replies self.max_senders = max_senders self._history: OrderedDict[str, deque[bool]] = OrderedDict() async def check(self, ctx: SecurityContext) -> SecurityCheckResult | None: """SecurityChecker 协议入口。""" actor_id = ctx.resolved_sender_id or ctx.inbound.sender_id or ctx.inbound.peer_id or "" is_bot = False if isinstance(ctx.inbound.raw_event, dict): is_bot = bool(ctx.inbound.raw_event.get("is_bot", False)) if self.is_allowed(actor_id, is_bot): return None return SecurityCheckResult( allowed=False, reason=InboundRejectionReason.BOT_LOOP_DETECTED, ) def is_allowed(self, sender_id: str, is_bot: bool) -> bool: """原有 check(sender_id, is_bot) 逻辑,改名以避免覆盖协议方法。""" if sender_id in self._history: self._history.move_to_end(sender_id) history = self._history.setdefault(sender_id, deque(maxlen=self.window_size)) history.append(is_bot) # LRU 淘汰:超出上限时移除最久未活跃的条目 while len(self._history) > self.max_senders: self._history.popitem(last=False) recent_bot_count = sum(history) return recent_bot_count <= self.max_bot_replies