124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
"""多层垃圾检测引擎 — 缓存 → 硬性正则 → 正常特征 → AI 兜底"""
|
||
import re
|
||
import logging
|
||
|
||
import httpx
|
||
|
||
import config
|
||
from knowledge import OLLAMA_URL, OLLAMA_MODEL
|
||
|
||
logger = logging.getLogger("spam_guard")
|
||
|
||
# ============ 硬性垃圾正则 ============
|
||
|
||
HARD_SPAM_PATTERNS = [
|
||
r"菠菜|赌博|博彩|赌场|棋牌|彩票|开奖|中奖|赔率|下注",
|
||
r"色情|约炮|外围|楼凤|一夜情|裸聊|成人视频",
|
||
r"刷单|传销|杀猪盘|资金盘|庞氏|传销盘",
|
||
r"(做USDT|U ?S ?D ?T|搬砖).{0,15}(看主页|看签名|进群|加我|找我)",
|
||
r"(看主页|看签名|加我|找我|进群).{0,15}(做USDT|U ?S ?D ?T|搬砖|赚钱|日入|月入)",
|
||
r"(一天|日入|月入|赚).{0,10}(万|w|W).{0,8}(项目|路子|渠道|带|做)",
|
||
r"(换|开|提).{0,6}(迈巴赫|宝马|奔驰|保时捷|法拉利|兰博基尼)",
|
||
r"(小白可带|手把手带|带你赚|带你做)",
|
||
r"(t\.me|telegram\.me).{0,30}(join|group|channel)",
|
||
]
|
||
|
||
# 软性信号(需 AI 二次判定)
|
||
SOFT_SPAM_SIGNALS = [
|
||
r"(免费领|免费送|限时领|点击领取|点击链接|复制链接)",
|
||
r"(加微信|wx|加我|私聊|私我|dm|PM)",
|
||
r"(赚钱|赚快钱|躺赚|副业|兼职日结)",
|
||
r"(开户|入金|出金|充值返|提现)",
|
||
r"(代理|加盟|推广返|分润|佣金)",
|
||
]
|
||
|
||
# 正常消息特征
|
||
NORMAL_SIGNALS = [
|
||
r"^.{0,8}$",
|
||
r"[??]",
|
||
r"(怎么|如何|为什么|能不能|请问|求|帮忙|谢谢|好的|收到|OK|ok|嗯|是的|不是|可以|不行|对|错|嗯嗯)",
|
||
r"(收割机|harvest|签到|站点|cookie|docker|安装|配置|更新|升级|bug|报错|失败|成功)",
|
||
r"^\d+$",
|
||
r"^[a-zA-Z0-9\s]{1,20}$",
|
||
]
|
||
|
||
SPAM_PROMPT = """判断以下消息是否为垃圾广告。同时检查消息内容和发送者信息。
|
||
|
||
消息内容: {text}
|
||
发送者用户名: {username}
|
||
发送者昵称: {first_name}
|
||
|
||
判断标准(满足任一即为垃圾):
|
||
1. 包含色情、赌博、诈骗、菠菜等违法推广内容
|
||
2. 包含明显广告推销(加群、加微信、免费领、点击链接等)
|
||
3. 纯转发的营销内容
|
||
|
||
请先给出结论,包含"垃圾"或"正常",再简短说明原因。"""
|
||
|
||
|
||
def normalize(text: str) -> str:
|
||
"""去除零宽字符、规范空白"""
|
||
t = re.sub(r'[\u200b\u200c\u200d\u2060\u200e\u200f\ufeff\u00a0]', '', text)
|
||
t = re.sub(r'\s+', ' ', t).strip()
|
||
return t
|
||
|
||
|
||
def match_patterns(text: str, patterns: list[str]) -> bool:
|
||
for pat in patterns:
|
||
if re.search(pat, text, re.IGNORECASE):
|
||
return True
|
||
return False
|
||
|
||
|
||
async def detect(text: str, username: str = "", first_name: str = "") -> tuple[bool, str]:
|
||
"""
|
||
多层垃圾检测。
|
||
返回 (is_spam, reason)
|
||
"""
|
||
norm = normalize(text)
|
||
|
||
# 第一层:硬性正则
|
||
if match_patterns(norm, HARD_SPAM_PATTERNS):
|
||
return True, "规则命中(硬性垃圾)"
|
||
|
||
# 短消息放行
|
||
if len(norm) < 20 and not match_patterns(norm, SOFT_SPAM_SIGNALS):
|
||
return False, "短消息跳过"
|
||
|
||
# 正常特征
|
||
if match_patterns(norm, NORMAL_SIGNALS) and not match_patterns(norm, SOFT_SPAM_SIGNALS):
|
||
return False, "正常消息特征匹配"
|
||
|
||
# AI 兜底
|
||
prompt = SPAM_PROMPT.format(text=text, username=username, first_name=first_name)
|
||
try:
|
||
async with httpx.AsyncClient(timeout=30, proxy=None) as client:
|
||
resp = await client.post(
|
||
f"{OLLAMA_URL}/api/chat",
|
||
json={
|
||
"model": OLLAMA_MODEL,
|
||
"messages": [{"role": "user", "content": prompt}],
|
||
"stream": False,
|
||
"options": {"num_predict": 50, "temperature": 0.1},
|
||
"think": False,
|
||
},
|
||
)
|
||
resp.raise_for_status()
|
||
reply = resp.json()["message"]["content"]
|
||
logger.info(f"🤖 AI回复: {reply[:100]}")
|
||
|
||
reply_upper = reply.upper()
|
||
spam_words = ["垃圾", "广告", "诈骗", "菠菜", "赌博", "色情", "YES"]
|
||
normal_words = ["正常", "非垃圾", "不是垃圾", "NO"]
|
||
if any(w in reply for w in normal_words) or "NO" in reply_upper:
|
||
return False, f"AI判定: {reply}"
|
||
if any(w in reply for w in spam_words) or "YES" in reply_upper:
|
||
return True, f"AI判定: {reply}"
|
||
return False, f"AI判定不明确: {reply}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ Ollama 调用失败: {e}")
|
||
if match_patterns(norm, SOFT_SPAM_SIGNALS):
|
||
return True, "关键词兜底(AI不可用)"
|
||
return False, f"检测异常: {e}"
|