ForcePilot/backend/package/yuxi/channel/security/bot_loop.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

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