59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""多渠道网关 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
|