Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3f78158
text and reason mismatch
rurudo-ruo May 19, 2026
57cd638
fix: include context in notification for sensitive messages
rurudo-ruo May 19, 2026
070be91
feat: add configuration options for message context in sensitive monitor
rurudo-ruo May 24, 2026
59e7fc4
feat: improve context fetching for LLM judgment and enhance command v…
rurudo-ruo May 26, 2026
1b3a2be
fix: always provide context to LLM judgment, gate only notification d…
Windsland52 May 26, 2026
9ae77b2
fix: extract clean text from structured messages instead of raw CQ codes
Windsland52 May 26, 2026
6ad21dd
Merge branch 'main' of https://github.com/rurudo-ruo/37Bot
rurudo-ruo Jun 4, 2026
5473ec0
feat: implement notification cooldown for sensitive messages
rurudo-ruo Jun 4, 2026
0679b27
fix: enhance context handling in LLM judgment and improve sensitive m…
rurudo-ruo Jun 4, 2026
8cda53e
fix: improve regex for sensitive content detection in LLM responses
rurudo-ruo Jun 4, 2026
6de8628
删除 llm_20260605003558.py
rurudo-ruo Jun 4, 2026
8d4e73f
删除 llm_20260605003540.py
rurudo-ruo Jun 4, 2026
7e19d8c
Merge branch 'main' into main
rurudo-ruo Jun 4, 2026
f0e5e87
feat: add Skland sign-in plugin with SMS and QR login support
rurudo-ruo Jun 4, 2026
3fab9f0
feat: Implement sensitive message monitoring plugin with configuratio…
rurudo-ruo Jun 4, 2026
dd5990a
Remove outdated SensitiveMonitorPlugin implementations from history
rurudo-ruo Jun 4, 2026
5e395e5
Merge branch 'main' of https://github.com/rurudo-ruo/37Bot
rurudo-ruo Jun 4, 2026
1058701
feat: Add sensitive message monitoring plugin with configuration and …
rurudo-ruo Jun 4, 2026
d4fce02
refactor: Remove outdated sensitive message monitoring plugin impleme…
rurudo-ruo Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions plugins/_ai/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import json
import re
import time
import httpx
from ncatbot.utils import get_log
Expand Down Expand Up @@ -270,9 +271,16 @@ async def judge_sensitive(self, message_text: str, context: str = "") -> tuple[b
return is_sensitive, reason
except json.JSONDecodeError:
logger.warning(f"LLM 返回非 JSON,fallback 到旧规则: {reply[:100]}")
is_sensitive = reply.strip().startswith("是")
reason = reply.strip()
return is_sensitive, reason
cleaned = reply.strip()
# Match leading 「是」/「否」 anchored at start; handles brackets like(是)「否」
m = re.match(r'^\s*[「((]?\s*([是否])\s*[」))]?', cleaned)
if m:
is_sensitive = m.group(1) == "是"
else:
# Fallback: check first non-whitespace character
first_char = cleaned.lstrip()[:1] if cleaned else ""
is_sensitive = first_char == "是"
return is_sensitive, cleaned[:200]

async def judge_question(self, project: str, message_text: str, context: str = "") -> bool:
"""判断消息是否在询问项目相关问题。返回 True/False。"""
Expand Down
1 change: 1 addition & 0 deletions plugins/arkrec/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .api import (
fetch_bundle_ext,
fetch_exclusive_operators,
fetch_menu,
fetch_menu_tree,
fetch_open_episodes,
fetch_operation_info,
Expand Down
7 changes: 4 additions & 3 deletions plugins/mirrorchyan/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
from pathlib import Path
from dataclasses import asdict
from typing import Optional

from ncatbot.plugin_system import NcatBotPlugin, command_registry, param
from ncatbot.core.event import GroupMessageEvent, PrivateMessageEvent
Expand Down Expand Up @@ -448,9 +449,9 @@ async def cmd_config(
event: GroupMessageEvent,
rid: str,
type: int = 1,
interval: int = None,
auto: bool = None,
channel: str = None,
interval: Optional[int] = None,
auto: Optional[bool] = None,
channel: Optional[str] = None,
):
"""更新配置 用法: /mirror_config <资源ID> [类型0/1] [检查间隔秒] [自动上传]"""
if not await self._is_group_admin(event.group_id, event.user_id):
Expand Down
2 changes: 0 additions & 2 deletions plugins/qa_helper/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,6 @@ async def _fetch_releases(self, project: str) -> str | None:

@staticmethod
def _looks_like_question(text: str) -> bool:
import re

# 1. 含问号直接放行(由 LLM 层做精判)
if "?" in text or "?" in text:
return True
Expand Down
3 changes: 3 additions & 0 deletions plugins/sensitive_monitor/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ class SensitiveGroupConfig:
max_context_messages: int = 5
# Maximum total characters for the appended context
max_context_chars: int = 800
# Cooldown seconds: after a sensitive notification, suppress further
# notifications from this group for this duration to avoid cascade
notify_cooldown_seconds: int = 120
87 changes: 66 additions & 21 deletions plugins/sensitive_monitor/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""敏感消息监听插件"""

import asyncio
import json
import time

from ncatbot.plugin_system import NcatBotPlugin, command_registry, param, on_message
from ncatbot.core.event import GroupMessageEvent, PrivateMessageEvent
Expand All @@ -15,23 +17,10 @@
RECENT_SENSITIVE: set[str] = set()
MAX_RECENT = 500
MIN_TEXT_LENGTH = 4


def _build_clean_message(message) -> str:
"""Convert a MessageArray to clean text, replacing non-text segments with summaries."""
parts = []
for seg in message:
if seg.msg_seg_type == "text":
parts.append(seg.text)
elif seg.msg_seg_type == "at":
parts.append(f"@{seg.qq}" if seg.qq != "all" else "@全体成员")
elif seg.msg_seg_type == "reply":
continue
else:
summary = seg.get_summary()
if summary and summary != "该消息不支持预览":
parts.append(summary)
return "".join(parts)
# Per-group cooldown: tracks the last notification time for each group
LAST_NOTIFY_TIME: dict[str, float] = {}
# Prune LAST_NOTIFY_TIME entries older than this (seconds)
NOTIFY_COOLDOWN_PRUNE_AGE = 3600 # 1 hour


class SensitiveMonitorPlugin(NcatBotPlugin):
Expand All @@ -56,6 +45,7 @@ def _load_config(self) -> dict[str, SensitiveGroupConfig]:
append_context=g.get("append_context", False),
max_context_messages=g.get("max_context_messages", 5),
max_context_chars=g.get("max_context_chars", 800),
notify_cooldown_seconds=g.get("notify_cooldown_seconds", 120),
)
for gid, g in data.items()
}
Expand All @@ -74,6 +64,7 @@ def _save_config(self):
"append_context": g.append_context,
"max_context_messages": g.max_context_messages,
"max_context_chars": g.max_context_chars,
"notify_cooldown_seconds": g.notify_cooldown_seconds,
}
for gid, g in self.groups.items()
},
Expand Down Expand Up @@ -113,8 +104,19 @@ async def _on_message(self, event):
return
RECENT_PROCESSED.add(str(event.message_id))
if len(RECENT_PROCESSED) > MAX_RECENT:
RECENT_PROCESSED.clear()
RECENT_SENSITIVE.clear()
# Sliding window: discard oldest half, but keep RECENT_SENSITIVE intact
# to prevent re-detection of already-flagged messages
keep_count = MAX_RECENT // 2
RECENT_PROCESSED = set(list(RECENT_PROCESSED)[-keep_count:])
# Prune stale notification cooldown entries
now = time.time()
stale_keys = [
gid
for gid, last in LAST_NOTIFY_TIME.items()
if now - last > NOTIFY_COOLDOWN_PRUNE_AGE
]
for gid in stale_keys:
del LAST_NOTIFY_TIME[gid]

if not is_llm_configured():
return
Expand All @@ -141,13 +143,30 @@ async def _on_message(self, event):
except Exception as e:
logger.error(f"获取消息上下文失败: {e}")

is_sensitive, reason = await get_llm().judge_sensitive(text, context)
try:
is_sensitive, reason = await asyncio.wait_for(
get_llm().judge_sensitive(text, context), timeout=15
)
except asyncio.TimeoutError:
logger.warning(f"LLM 敏感判断超时 (group={group_id}),跳过")
return
if is_sensitive:
RECENT_SENSITIVE.add(str(event.message_id))
logger.info(
f"敏感消息: group={group_id}, user={event.user_id}, reason={reason}"
)
await self._notify(cfg, group_id, str(event.user_id), text, reason, context)
# Cooldown check: suppress notification if we recently sent one in this group
now = time.time()
last = LAST_NOTIFY_TIME.get(group_id, 0)
if now - last < cfg.notify_cooldown_seconds:
logger.info(
f"群 {group_id} 处于通知冷却中 (剩余 {int(cfg.notify_cooldown_seconds - (now - last))}s),跳过通知"
)
else:
await self._notify(
cfg, group_id, str(event.user_id), text, reason, context
)
LAST_NOTIFY_TIME[group_id] = time.time()

async def _notify(
self,
Expand Down Expand Up @@ -271,9 +290,35 @@ async def cmd_status(self, event: GroupMessageEvent):
lines.append(f" 附加对话背景: {'是' if cfg.append_context else '否'}")
lines.append(f" 对话消息数上限: {cfg.max_context_messages}")
lines.append(f" 对话字符上限: {cfg.max_context_chars}")
lines.append(f" 通知冷却: {cfg.notify_cooldown_seconds}秒")
llm_cfg = load_llm_config()
lines.append(f"LLM: {'已配置' if llm_cfg.base_url else '未配置'}")
await event.reply("\n".join(lines))

@command_registry.command(
"sensitive_cooldown", description="[管理员] 设置通知冷却秒数"
)
@param(name="seconds", default="120", help="冷却秒数,设为0禁用冷却")
async def cmd_cooldown(self, event: GroupMessageEvent, seconds: str = "120"):
if not await self._is_group_admin(event.group_id, event.user_id):
await event.reply("需要群主或管理员权限")
return
try:
val = int(seconds)
if val < 0:
await event.reply("冷却秒数不能为负数")
return
except ValueError:
await event.reply("请输入有效的数字")
return
group_id = str(event.group_id)
cfg = self._get_cfg(group_id)
cfg.notify_cooldown_seconds = val
self._save_config()
if val == 0:
await event.reply("通知冷却已禁用")
else:
await event.reply(f"通知冷却已设置为 {val} 秒")


__all__ = ["SensitiveMonitorPlugin"]
1 change: 0 additions & 1 deletion plugins/skland/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ def _format_account_result(
if error:
lines.append(f"失败: {error}")
return lines
assert results is not None
if not results:
lines.append("未找到绑定角色")
return lines
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies = [
"ncatbot",
"markdown",
"playwright",
"psutil",
]

[[tool.uv.index]]
Expand Down