强化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:
youpaishidifu
2026-08-10 10:21:03 +08:00
parent a11741fa69
commit d8734021bb
2 changed files with 180 additions and 8 deletions
+127 -1
View File
@@ -19,6 +19,33 @@ from auto_reply import AutoReply
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_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")
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.grid(row=2, column=0, padx=12, pady=(4, 12), sticky="nsew")
@@ -302,6 +374,55 @@ class MsgBotApp(ctk.CTk):
else:
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):
@@ -336,7 +457,12 @@ class MsgBotApp(ctk.CTk):
# 详细调试日志:记录发送给KB搜索的完整消息
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}"]
if results:
+52 -6
View File
@@ -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*')
_leading_junk = re.compile(r'^[\s,,.。::;;!!??]+')
_at_user_pattern = re.compile(r'^@([A-Za-z0-9_\u4e00-\u9fff]{2,30})')
# ⭐ 主循环:每轮按优先级尝试剥离一层,直到无可剥离
changed = True
@@ -4440,6 +4441,7 @@ class MessageMonitor:
# 3) @提及格式: @username 或 @昵称 —— QQ的@是UI元素,不是消息内容
if ts.startswith('@'):
matched = False
# 先尝试已知昵称
for nick in nicknames:
if ts[1:].startswith(nick):
ts = ts[1 + len(nick):].lstrip()
@@ -4447,11 +4449,24 @@ class MessageMonitor:
changed = True
break
if not matched:
# @后不是已知昵称,尝试剥离@+ASCII用户名(QQ的@提及格式)
m_unknown = re.match(r'^@([A-Za-z0-9_]{2,30}|[\u4e00-\u9fff]{2,10})', ts)
if m_unknown:
ts = ts[m_unknown.end():].lstrip()
# 用正则匹配任意用户名(中英文混合,2-30字符)
m_at = _at_user_pattern.match(ts)
if m_at:
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:
# 无法识别用户名,只剥离@符号
ts = ts[1:].lstrip()
changed = True
if changed:
@@ -4464,8 +4479,39 @@ class MessageMonitor:
changed = True
break
# 清理残留的标点/空格
# ⭐ 群聊多消息拆分:OCR多条消息拼接时,[头衔]用户名 或 裸昵称 作为分隔符出现在文本中间
# 如 "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}")
return ts.strip()