refactor ai_auto_response plugin architecture
This commit is contained in:
19
plugins/ai_auto_response/safety/__init__.py
Normal file
19
plugins/ai_auto_response/safety/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .dedup import DedupManager
|
||||
from .filters import (
|
||||
is_coding_work_request,
|
||||
is_prompt_attack,
|
||||
is_targeting_other_user,
|
||||
should_ignore,
|
||||
strip_at_prefix,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DedupManager",
|
||||
"is_coding_work_request",
|
||||
"is_prompt_attack",
|
||||
"is_targeting_other_user",
|
||||
"should_ignore",
|
||||
"strip_at_prefix",
|
||||
]
|
||||
53
plugins/ai_auto_response/safety/dedup.py
Normal file
53
plugins/ai_auto_response/safety/dedup.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Dict, Set
|
||||
|
||||
|
||||
class DedupManager:
|
||||
def __init__(self):
|
||||
self.inflight_message_keys: Set[str] = set()
|
||||
self.recent_message_keys: Dict[str, float] = {}
|
||||
self.recent_reply_signatures: Dict[str, float] = {}
|
||||
|
||||
def begin_message_processing(self, message_key: str, expiry_sec: int) -> bool:
|
||||
if not message_key:
|
||||
return True
|
||||
now = time.time()
|
||||
stale_keys = [key for key, ts in self.recent_message_keys.items() if now - ts > expiry_sec]
|
||||
for key in stale_keys:
|
||||
self.recent_message_keys.pop(key, None)
|
||||
if message_key in self.inflight_message_keys:
|
||||
return False
|
||||
if message_key in self.recent_message_keys:
|
||||
return False
|
||||
self.inflight_message_keys.add(message_key)
|
||||
return True
|
||||
|
||||
def finish_message_processing(self, message_key: str) -> None:
|
||||
if not message_key:
|
||||
return
|
||||
self.inflight_message_keys.discard(message_key)
|
||||
self.recent_message_keys[message_key] = time.time()
|
||||
|
||||
def should_skip_duplicate_reply(
|
||||
self,
|
||||
*,
|
||||
room_id: str,
|
||||
sender: str,
|
||||
reply_text: str,
|
||||
expiry_sec: int,
|
||||
scope: str = "sender",
|
||||
) -> bool:
|
||||
text = str(reply_text or "").strip()
|
||||
if not text:
|
||||
return False
|
||||
now = time.time()
|
||||
stale_keys = [key for key, ts in self.recent_reply_signatures.items() if now - ts > expiry_sec]
|
||||
for key in stale_keys:
|
||||
self.recent_reply_signatures.pop(key, None)
|
||||
signature = f"{room_id}:{text}" if scope == "room" else f"{room_id}:{sender}:{text}"
|
||||
if signature in self.recent_reply_signatures:
|
||||
return True
|
||||
self.recent_reply_signatures[signature] = now
|
||||
return False
|
||||
66
plugins/ai_auto_response/safety/filters.py
Normal file
66
plugins/ai_auto_response/safety/filters.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
PROMPT_ATTACK_PATTERNS = [
|
||||
r"(?i)\bprompt\b",
|
||||
r"(?i)\bignore\b",
|
||||
r"(?i)\bsystem\b",
|
||||
r"(?i)\brole\b",
|
||||
r"(?i)\bjailbreak\b",
|
||||
r"(?i)提示词",
|
||||
r"(?i)越狱",
|
||||
r"(?i)扮演",
|
||||
r"(?i)现在你是",
|
||||
r"(?i)你是.+?(机器人|助手|模型|ai)",
|
||||
r"(?i)忘记(之前|上面|所有|设定|规则)",
|
||||
r"(?i)重置(设定|规则|系统|人格)",
|
||||
]
|
||||
|
||||
CODING_WORK_PATTERNS = [
|
||||
r"(?i)写(个|一段|一下|一份)?.{0,8}(代码|脚本|程序|插件|接口|爬虫|sql|配置)",
|
||||
r"(?i)(帮我|给我|直接).{0,8}(写|做|实现|生成|改).{0,12}(代码|脚本|程序|插件|接口|sql|配置)",
|
||||
r"(?i)(实现|开发|编写|重构|修改|修复).{0,16}(插件|代码|脚本|程序|接口|功能)",
|
||||
r"(?i)(给我|帮我).{0,10}(搞个|整一个).{0,12}(机器人|插件|脚本|程序)",
|
||||
r"(?i)\bdebug\b",
|
||||
r"(?i)\bfix\b",
|
||||
r"(?i)\brefactor\b",
|
||||
r"(?i)\bimplement\b",
|
||||
]
|
||||
|
||||
|
||||
def strip_at_prefix(content: str) -> str:
|
||||
return re.sub(r"@.*?[\u2005\s]+", "", str(content or "")).strip()
|
||||
|
||||
|
||||
def should_ignore(content: str, filters: Dict[str, Any]) -> bool:
|
||||
content = str(content or "").strip()
|
||||
filters = filters or {}
|
||||
if len(content) < int(filters.get("min_text_length", 1)):
|
||||
return True
|
||||
if content in set(filters.get("ignore_exact", [])):
|
||||
return True
|
||||
return any(content.startswith(prefix) for prefix in filters.get("ignore_prefixes", []))
|
||||
|
||||
|
||||
def is_prompt_attack(content: str) -> bool:
|
||||
text = str(content or "").strip()
|
||||
if not text:
|
||||
return False
|
||||
return any(re.search(pattern, text) for pattern in PROMPT_ATTACK_PATTERNS)
|
||||
|
||||
|
||||
def is_coding_work_request(content: str) -> bool:
|
||||
text = str(content or "").strip()
|
||||
if not text:
|
||||
return False
|
||||
return any(re.search(pattern, text) for pattern in CODING_WORK_PATTERNS)
|
||||
|
||||
|
||||
def is_targeting_other_user(message: Dict[str, Any]) -> bool:
|
||||
if message.get("is_at", False):
|
||||
return False
|
||||
raw_content = str(message.get("content", "") or "")
|
||||
return "@" in raw_content
|
||||
Reference in New Issue
Block a user