diff --git a/plugins/ai_auto_response/config.toml b/plugins/ai_auto_response/config.toml index 255eef2..e902b7e 100644 --- a/plugins/ai_auto_response/config.toml +++ b/plugins/ai_auto_response/config.toml @@ -35,31 +35,31 @@ casual_topic = 0.35 [flow] enable_flow_state = true -flow_decay_per_minute = 12 -idle_threshold = 30 -warming_threshold = 48 -engaged_threshold = 78 +flow_decay_per_minute = 16 +idle_threshold = 36 +warming_threshold = 60 +engaged_threshold = 96 at_bot_boost = 40 question_boost = 30 -followup_boost = 20 -topic_boost = 10 -returning_member_boost = 6 -response_accepted_boost = 10 -ignored_reply_penalty = 20 -over_reply_penalty = 22 +followup_boost = 12 +topic_boost = 4 +returning_member_boost = 3 +response_accepted_boost = 6 +ignored_reply_penalty = 26 +over_reply_penalty = 32 night_penalty = 30 -max_bot_reply_streak = 2 +max_bot_reply_streak = 1 [cooldown] -group_reply_cooldown_sec = 90 -same_user_followup_cooldown_sec = 18 +group_reply_cooldown_sec = 150 +same_user_followup_cooldown_sec = 28 at_mention_min_interval_sec = 5 at_mention_burst_window_sec = 90 -at_mention_burst_limit = 5 +at_mention_burst_limit = 4 at_mention_silent_sec = 180 directed_burst_window_sec = 240 -directed_burst_limit = 4 -directed_burst_silent_sec = 480 +directed_burst_limit = 3 +directed_burst_silent_sec = 600 night_silent_hours = ["01:00-07:30"] [memory] @@ -102,6 +102,11 @@ ignore_prefixes = ["/", "#"] ignore_exact = ["收到", "好的", "嗯", "哦", "6", "1", "?", "?"] min_text_length = 1 +[spam_guard] +repeat_window_sec = 10 +repeat_threshold = 2 +repeat_min_length = 4 + [logging] debug = true diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index a6d908c..2f2a0d3 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -130,6 +130,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): self.cooldown_config = self._config.get("cooldown", {}) or {} self.cooldown = CooldownManager(self.cooldown_config) self.image_config = self._config.get("image", {}) or {} + self.spam_config = self._config.get("spam_guard", {}) or {} self._synced_member_context_versions: Dict[str, str] = {} self.log_debug = bool((self._config.get("logging", {}) or {}).get("debug", True)) self.LOG.debug(f"[{self.name}] 初始化完成") @@ -197,6 +198,23 @@ class AIAutoResponsePlugin(MessagePluginInterface): reply_mode="defense", ) return False, "ignored_prompt_attack" + if self.dedup.should_skip_repeated_room_content( + room_id=room_id, + content=content, + window_sec=int(self.spam_config.get("repeat_window_sec", 45) or 45), + repeat_threshold=int(self.spam_config.get("repeat_threshold", 3) or 3), + min_length=int(self.spam_config.get("repeat_min_length", 4) or 4), + ): + self._log_event( + "skip", + room_id=room_id, + sender=sender, + reason="repeated_room_content", + trigger_type="spam_guard", + reply_mode="guard", + topic="-", + ) + return False, "repeated_room_content" coding_work_request = is_coding_work_request(content) if coding_work_request and not is_at: return False, "skip_coding_work" diff --git a/plugins/ai_auto_response/safety/dedup.py b/plugins/ai_auto_response/safety/dedup.py index 3c89783..6ae57d2 100644 --- a/plugins/ai_auto_response/safety/dedup.py +++ b/plugins/ai_auto_response/safety/dedup.py @@ -1,7 +1,8 @@ from __future__ import annotations +import re import time -from typing import Dict, Set +from typing import Dict, List, Set, Tuple class DedupManager: @@ -9,6 +10,7 @@ class DedupManager: self.inflight_message_keys: Set[str] = set() self.recent_message_keys: Dict[str, float] = {} self.recent_reply_signatures: Dict[str, float] = {} + self.recent_room_content_hits: Dict[str, List[Tuple[float, str]]] = {} def begin_message_processing(self, message_key: str, expiry_sec: int) -> bool: if not message_key: @@ -51,3 +53,31 @@ class DedupManager: return True self.recent_reply_signatures[signature] = now return False + + def should_skip_repeated_room_content( + self, + *, + room_id: str, + content: str, + window_sec: int, + repeat_threshold: int, + min_length: int = 4, + ) -> bool: + text = self._normalize_room_content(content) + if not room_id or not text or len(text) < max(int(min_length or 4), 1): + return False + now = time.time() + window_sec = max(int(window_sec or 0), 1) + repeat_threshold = max(int(repeat_threshold or 0), 2) + room_items = self.recent_room_content_hits.get(room_id, []) + room_items = [(ts, item_text) for ts, item_text in room_items if now - ts <= window_sec] + same_count = sum(1 for _, item_text in room_items if item_text == text) + room_items.append((now, text)) + self.recent_room_content_hits[room_id] = room_items[-80:] + return same_count + 1 >= repeat_threshold + + @staticmethod + def _normalize_room_content(content: str) -> str: + text = str(content or "").strip().lower() + text = re.sub(r"\s+", "", text) + return text