强化QQ昵称剥离+群聊消息拆分+KB搜索去噪+GUI昵称管理
1. 群聊多消息拆分:_strip_qq_nickname新增中间段分割逻辑 - 找到最后一个[头衔]用户名或裸昵称分隔符,截取最新消息 - 解决群聊多条消息打包导致关键词被压住的问题 2. KB搜索去噪:gui.py新增_clean_query_for_kb - 移除用户名/头衔/@提及/时间戳等噪音 - _search_and_log调用前自动清理查询文本 3. GUI昵称管理: - 昵称列表展示+手动输入添加 - 删除选中昵称功能 - @提及新用户自动入库(monitor.py)
This commit is contained in:
@@ -19,6 +19,33 @@ from auto_reply import AutoReply
|
|||||||
|
|
||||||
logger = logging.getLogger("msgbot.gui")
|
logger = logging.getLogger("msgbot.gui")
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_query_for_kb(msg: str) -> str:
|
||||||
|
"""清理查询文本中的用户名/头衔/时间戳等噪音,提升KB搜索精度。"""
|
||||||
|
if not msg:
|
||||||
|
return msg
|
||||||
|
t = msg.strip()
|
||||||
|
# 1. 移除QQ群头衔格式: [头衔]用户名 / 【头衔】用户名
|
||||||
|
t = re.sub(r'[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_\u4e00-\u9fff]{2,30})\s*[,,]?\s*', '', t)
|
||||||
|
# 2. 移除@提及: @username
|
||||||
|
t = re.sub(r'@[A-Za-z0-9_\u4e00-\u9fff]{2,30}\s*', '', t)
|
||||||
|
# 3. 移除时间戳: HH:MM:SS / HH:MM / 日期格式
|
||||||
|
t = re.sub(r'\d{1,2}[::]\d{2}(?:[::]\d{2})?\s*', '', t)
|
||||||
|
t = re.sub(r'\d{4}[//\-]\d{1,2}[//\-]\d{1,2}\s*', '', t)
|
||||||
|
# 4. 移除已知昵称列表
|
||||||
|
try:
|
||||||
|
cfg = load_config()
|
||||||
|
nicknames = cfg.get('qq_nicknames', [])
|
||||||
|
for nick in nicknames:
|
||||||
|
if nick in t:
|
||||||
|
t = t.replace(nick, '')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 5. 清理残留标点/空格
|
||||||
|
t = re.sub(r'[,,、\s]+', ' ', t).strip(',,、 \t')
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
ctk.set_appearance_mode("System")
|
ctk.set_appearance_mode("System")
|
||||||
ctk.set_default_color_theme("green")
|
ctk.set_default_color_theme("green")
|
||||||
|
|
||||||
@@ -194,6 +221,51 @@ class MsgBotApp(ctk.CTk):
|
|||||||
)
|
)
|
||||||
self._ar_threshold_label.grid(row=0, column=3, padx=(6, 0), pady=0, sticky="w")
|
self._ar_threshold_label.grid(row=0, column=3, padx=(6, 0), pady=0, sticky="w")
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
# ── QQ昵称管理 ──
|
||||||
|
nick_label = ctk.CTkLabel(settings, text="👥 QQ昵称:", font=("Microsoft YaHei", 12))
|
||||||
|
nick_label.grid(row=row, column=0, padx=(8, 4), pady=6, sticky="nw")
|
||||||
|
|
||||||
|
nick_frame = ctk.CTkFrame(settings, fg_color="transparent")
|
||||||
|
nick_frame.grid(row=row, column=1, columnspan=2, padx=4, pady=6, sticky="ew")
|
||||||
|
nick_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# 昵称列表显示
|
||||||
|
self._nick_list_var = ctk.StringVar(value="")
|
||||||
|
self._nick_entry = ctk.CTkEntry(
|
||||||
|
nick_frame, placeholder_text="输入新昵称后回车添加"
|
||||||
|
)
|
||||||
|
self._nick_entry.grid(row=0, column=0, padx=(0, 4), pady=2, sticky="ew")
|
||||||
|
self._nick_entry.bind("<Return>", self._add_nickname)
|
||||||
|
|
||||||
|
self._add_nick_btn = ctk.CTkButton(
|
||||||
|
nick_frame, text="添加", width=56, command=self._add_nickname
|
||||||
|
)
|
||||||
|
self._add_nick_btn.grid(row=0, column=1, padx=(0, 4), pady=2, sticky="e")
|
||||||
|
|
||||||
|
# 昵称列表
|
||||||
|
self._nick_listbox = ctk.CTkTextbox(nick_frame, height=60, font=("Microsoft YaHei", 11))
|
||||||
|
self._nick_listbox.grid(row=1, column=0, columnspan=2, padx=0, pady=4, sticky="ew")
|
||||||
|
|
||||||
|
# 操作按钮行
|
||||||
|
nick_btn_frame = ctk.CTkFrame(nick_frame, fg_color="transparent")
|
||||||
|
nick_btn_frame.grid(row=2, column=0, columnspan=2, pady=2, sticky="ew")
|
||||||
|
|
||||||
|
self._del_nick_btn = ctk.CTkButton(
|
||||||
|
nick_btn_frame, text="删除选中", width=72,
|
||||||
|
command=self._delete_selected_nickname
|
||||||
|
)
|
||||||
|
self._del_nick_btn.pack(side="left", padx=(0, 4))
|
||||||
|
|
||||||
|
self._refresh_nick_btn = ctk.CTkButton(
|
||||||
|
nick_btn_frame, text="刷新", width=56,
|
||||||
|
command=self._refresh_nick_list
|
||||||
|
)
|
||||||
|
self._refresh_nick_btn.pack(side="left")
|
||||||
|
|
||||||
|
# 初始加载昵称列表
|
||||||
|
self._refresh_nick_list()
|
||||||
|
|
||||||
# ── 日志区域 ──
|
# ── 日志区域 ──
|
||||||
log_frame = ctk.CTkFrame(self)
|
log_frame = ctk.CTkFrame(self)
|
||||||
log_frame.grid(row=2, column=0, padx=12, pady=(4, 12), sticky="nsew")
|
log_frame.grid(row=2, column=0, padx=12, pady=(4, 12), sticky="nsew")
|
||||||
@@ -302,6 +374,55 @@ class MsgBotApp(ctk.CTk):
|
|||||||
else:
|
else:
|
||||||
self._kb_status.configure(text="❌ 未加载", text_color="#e74c3c")
|
self._kb_status.configure(text="❌ 未加载", text_color="#e74c3c")
|
||||||
|
|
||||||
|
# ── 昵称管理 ──
|
||||||
|
|
||||||
|
def _refresh_nick_list(self):
|
||||||
|
"""刷新昵称列表显示"""
|
||||||
|
self.cfg = load_config()
|
||||||
|
nicks = self.cfg.get("qq_nicknames", [])
|
||||||
|
self._nick_listbox.configure(state="normal")
|
||||||
|
self._nick_listbox.delete("1.0", "end")
|
||||||
|
if nicks:
|
||||||
|
for n in nicks:
|
||||||
|
self._nick_listbox.insert("end", n + "\n")
|
||||||
|
else:
|
||||||
|
self._nick_listbox.insert("end", "(空 — 通过@提及或手动添加)")
|
||||||
|
self._nick_listbox.configure(state="disabled")
|
||||||
|
|
||||||
|
def _add_nickname(self, event=None):
|
||||||
|
"""添加新昵称到列表"""
|
||||||
|
nick = self._nick_entry.get().strip()
|
||||||
|
if not nick:
|
||||||
|
return
|
||||||
|
self.cfg = load_config()
|
||||||
|
nicks = self.cfg.setdefault("qq_nicknames", [])
|
||||||
|
if nick in nicks:
|
||||||
|
self._log(f"⚠ 昵称 '{nick}' 已存在")
|
||||||
|
return
|
||||||
|
nicks.append(nick)
|
||||||
|
save_config(self.cfg)
|
||||||
|
self._nick_entry.delete(0, "end")
|
||||||
|
self._refresh_nick_list()
|
||||||
|
self._log(f"✅ 已添加昵称: {nick}")
|
||||||
|
|
||||||
|
def _delete_selected_nickname(self):
|
||||||
|
"""删除光标所在行的昵称"""
|
||||||
|
try:
|
||||||
|
cursor_pos = self._nick_listbox.index("insert")
|
||||||
|
line_text = self._nick_listbox.get(cursor_pos + " linestart",
|
||||||
|
cursor_pos + " lineend").strip()
|
||||||
|
if not line_text or line_text.startswith("("):
|
||||||
|
return
|
||||||
|
self.cfg = load_config()
|
||||||
|
nicks = self.cfg.get("qq_nicknames", [])
|
||||||
|
if line_text in nicks:
|
||||||
|
nicks.remove(line_text)
|
||||||
|
save_config(self.cfg)
|
||||||
|
self._refresh_nick_list()
|
||||||
|
self._log(f"🗑 已删除昵称: {line_text}")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠ 删除失败: {e}")
|
||||||
|
|
||||||
# ── 消息处理 ──
|
# ── 消息处理 ──
|
||||||
|
|
||||||
def _on_new_message(self, platform: str, msg: str):
|
def _on_new_message(self, platform: str, msg: str):
|
||||||
@@ -336,7 +457,12 @@ class MsgBotApp(ctk.CTk):
|
|||||||
# 详细调试日志:记录发送给KB搜索的完整消息
|
# 详细调试日志:记录发送给KB搜索的完整消息
|
||||||
logger.debug(f"[{platform}] KB搜索输入: msg={msg!r} (长度={len(msg)})")
|
logger.debug(f"[{platform}] KB搜索输入: msg={msg!r} (长度={len(msg)})")
|
||||||
|
|
||||||
results = self.kb.search(msg, top_k=3)
|
# 清理查询文本:移除用户名/头衔/时间戳等噪音,提升搜索精度
|
||||||
|
cleaned_msg = _clean_query_for_kb(msg)
|
||||||
|
if cleaned_msg != msg:
|
||||||
|
logger.debug(f"[{platform}] KB清理后: cleaned={cleaned_msg!r}")
|
||||||
|
|
||||||
|
results = self.kb.search(cleaned_msg, top_k=3)
|
||||||
lines = [f"[{platform.upper()}] {msg}"]
|
lines = [f"[{platform.upper()}] {msg}"]
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
|
|||||||
+53
-7
@@ -4404,9 +4404,10 @@ class MessageMonitor:
|
|||||||
"游戏专用",
|
"游戏专用",
|
||||||
]
|
]
|
||||||
|
|
||||||
qq_title_pattern = re.compile(r'^[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_]{2,30}|[\u4e00-\u9fff]{2,10})[,,]?\s*')
|
qq_title_pattern = re.compile(r'^[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_\u4e00-\u9fff]{2,30})[,,]?\s*')
|
||||||
_rank_pattern = re.compile(r'^[((][^))]+[))]\s*')
|
_rank_pattern = re.compile(r'^[((][^))]+[))]\s*')
|
||||||
_leading_junk = re.compile(r'^[\s,,.。::;;!!??]+')
|
_leading_junk = re.compile(r'^[\s,,.。::;;!!??]+')
|
||||||
|
_at_user_pattern = re.compile(r'^@([A-Za-z0-9_\u4e00-\u9fff]{2,30})')
|
||||||
|
|
||||||
# ⭐ 主循环:每轮按优先级尝试剥离一层,直到无可剥离
|
# ⭐ 主循环:每轮按优先级尝试剥离一层,直到无可剥离
|
||||||
changed = True
|
changed = True
|
||||||
@@ -4440,6 +4441,7 @@ class MessageMonitor:
|
|||||||
# 3) @提及格式: @username 或 @昵称 —— QQ的@是UI元素,不是消息内容
|
# 3) @提及格式: @username 或 @昵称 —— QQ的@是UI元素,不是消息内容
|
||||||
if ts.startswith('@'):
|
if ts.startswith('@'):
|
||||||
matched = False
|
matched = False
|
||||||
|
# 先尝试已知昵称
|
||||||
for nick in nicknames:
|
for nick in nicknames:
|
||||||
if ts[1:].startswith(nick):
|
if ts[1:].startswith(nick):
|
||||||
ts = ts[1 + len(nick):].lstrip()
|
ts = ts[1 + len(nick):].lstrip()
|
||||||
@@ -4447,11 +4449,24 @@ class MessageMonitor:
|
|||||||
changed = True
|
changed = True
|
||||||
break
|
break
|
||||||
if not matched:
|
if not matched:
|
||||||
# @后不是已知昵称,尝试剥离@+ASCII用户名(QQ的@提及格式)
|
# 用正则匹配任意用户名(中英文混合,2-30字符)
|
||||||
m_unknown = re.match(r'^@([A-Za-z0-9_]{2,30}|[\u4e00-\u9fff]{2,10})', ts)
|
m_at = _at_user_pattern.match(ts)
|
||||||
if m_unknown:
|
if m_at:
|
||||||
ts = ts[m_unknown.end():].lstrip()
|
username = m_at.group(1)
|
||||||
|
ts = ts[m_at.end():].lstrip()
|
||||||
|
# 自动入库:如果昵称不在列表中,添加并保存配置
|
||||||
|
if username not in nicknames:
|
||||||
|
nicknames.append(username)
|
||||||
|
try:
|
||||||
|
from config import load_config, save_config
|
||||||
|
cfg = load_config()
|
||||||
|
cfg.setdefault('qq_nicknames', []).append(username)
|
||||||
|
save_config(cfg)
|
||||||
|
logger.info(f"[qq] 自动入库@提及昵称: {username}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[qq] 自动入库昵称失败: {e}")
|
||||||
else:
|
else:
|
||||||
|
# 无法识别用户名,只剥离@符号
|
||||||
ts = ts[1:].lstrip()
|
ts = ts[1:].lstrip()
|
||||||
changed = True
|
changed = True
|
||||||
if changed:
|
if changed:
|
||||||
@@ -4464,8 +4479,39 @@ class MessageMonitor:
|
|||||||
changed = True
|
changed = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# 清理残留的标点/空格
|
# ⭐ 群聊多消息拆分:OCR多条消息拼接时,[头衔]用户名 或 裸昵称 作为分隔符出现在文本中间
|
||||||
ts = _leading_junk.sub('', ts)
|
# 如 "C4炸弹起来准备测试了,[专业皮肤定制] C4炸弹,[专业皮肤定制] C4炸弹,youpaishidifu关于我们"
|
||||||
|
# 策略:找到所有QQ昵称分隔符位置,取最后一个分隔符之后的文本(最新用户消息)
|
||||||
|
|
||||||
|
# 模式1: [头衔]用户名 后跟逗号/冒号(QQ群头衔格式)
|
||||||
|
_mid_qq_title = re.compile(r'[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_\u4e00-\u9fff]{2,30})\s*[,,::]')
|
||||||
|
# 模式2: 已知裸昵称 后跟逗号/冒号(无头衔格式)
|
||||||
|
_mid_bare_nick = None
|
||||||
|
if nicknames:
|
||||||
|
# 构造已知昵称的转义正则
|
||||||
|
_escaped = [re.escape(n) for n in nicknames]
|
||||||
|
_mid_bare_nick = re.compile(r'(?:' + '|'.join(_escaped) + r')\s*[,,::]')
|
||||||
|
|
||||||
|
# 找到最后一个分隔符位置,截取其后的文本(最新消息)
|
||||||
|
last_sep_end = -1
|
||||||
|
|
||||||
|
# 查找模式1(QQ头衔格式)的最后一个匹配
|
||||||
|
for m in _mid_qq_title.finditer(ts):
|
||||||
|
last_sep_end = m.end()
|
||||||
|
|
||||||
|
# 查找模式2(裸昵称)的最后一个匹配(需排除开头的昵称)
|
||||||
|
if _mid_bare_nick:
|
||||||
|
for m in _mid_bare_nick.finditer(ts):
|
||||||
|
# 跳过开头的昵称(已由上面的主循环处理)
|
||||||
|
if m.start() > 0:
|
||||||
|
last_sep_end = m.end()
|
||||||
|
|
||||||
|
if last_sep_end > 0:
|
||||||
|
ts = ts[last_sep_end:].lstrip()
|
||||||
|
# 清理残留的前导标点/空格
|
||||||
|
ts = _leading_junk.sub('', ts)
|
||||||
|
logger.info(f"[qq] 群聊拆分: 取最后分隔符后的文本,长度={len(ts)}")
|
||||||
|
|
||||||
logger.debug(f"[qq] _strip_qq_nickname输出: {ts.strip()[:80]!r}")
|
logger.debug(f"[qq] _strip_qq_nickname输出: {ts.strip()[:80]!r}")
|
||||||
return ts.strip()
|
return ts.strip()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user