diff --git a/knowledge_base.py b/knowledge_base.py index 5dc31c8..f7a61bc 100644 --- a/knowledge_base.py +++ b/knowledge_base.py @@ -2,6 +2,7 @@ import os import re +import logging from pathlib import Path from typing import List, Tuple, Optional @@ -73,9 +74,9 @@ class KnowledgeBase: if not self.is_ready or not query.strip(): return [] - # 短文本(< 4 个字或 <= 2 个有效词)不做搜索,避免噪音 + # 短文本:至少 2 个字且至少有 1 个有效词才搜索 tokens = [t for t in jieba.cut(query.strip()) if len(t) >= 2] - if len(query.strip()) < 4 or len(tokens) <= 1: + if len(query.strip()) < 2 or len(tokens) == 0: return [] query_vec = self._vectorizer.transform([query]) @@ -88,6 +89,16 @@ class KnowledgeBase: if score < min_score: continue results.append((self.docs[idx], score)) + if results: + return results + + # Fallback: 子串匹配(查询包含在文档标题或内容中) + q = query.strip() + for doc in self.docs: + if q in doc["content"] or q in doc["title"]: + logger = logging.getLogger("msgbot.kb") + logger.debug(f"子串匹配: \"{q}\" → {doc['title']}") + return [(doc, min_score)] return results def get_doc_count(self) -> int: diff --git a/monitor.py b/monitor.py index b3e38f9..e06df91 100644 --- a/monitor.py +++ b/monitor.py @@ -280,7 +280,7 @@ class MessageMonitor: pass def _read_win32_texts(self, platform: str) -> list: - """Win32 API 直接读窗口文本(绕过 UIA 缓存)。""" + """Win32 API 读窗口文本,递归枚举(depth 不限)。""" hwnd = self._cached_hwnd.get(platform) if not hwnd: return [] @@ -289,7 +289,7 @@ class MessageMonitor: def enum_callback(child_hwnd, _): try: text = win32gui.GetWindowText(child_hwnd) - if text and len(text.strip()) >= 4: + if text and len(text.strip()) >= 2: texts.append(text.strip()) except Exception: pass @@ -297,7 +297,7 @@ class MessageMonitor: try: main_text = win32gui.GetWindowText(hwnd) - if main_text and len(main_text.strip()) >= 4: + if main_text and len(main_text.strip()) >= 2: texts.append(main_text.strip()) win32gui.EnumChildWindows(hwnd, enum_callback, None) except Exception: @@ -308,7 +308,19 @@ class MessageMonitor: """剪贴板法:聚焦窗口→Ctrl+A→Ctrl+C→读剪贴板(适用 Electron 应用)。 稳妥可靠但会切焦点+清用户剪贴板(自动恢复原内容)。 + 自动重试 1 次若首次结果为空。 """ + texts = self._read_via_clipboard_once(platform) + if texts: + return texts + # 重试一次:再次 SetForegroundWindow + SendKeys + logger.debug(f"[{platform}] 剪贴板首次空,重试") + time.sleep(0.5) + texts = self._read_via_clipboard_once(platform) + return texts + + def _read_via_clipboard_once(self, platform: str) -> list: + """剪贴板法单次执行。""" hwnd = self._cached_hwnd.get(platform) if not hwnd or not win32gui.IsWindow(hwnd): return [] @@ -359,8 +371,6 @@ class MessageMonitor: return texts - # ── 自动回复相关 ────────────────────────────────────────── - def get_raw_clipboard(self, platform: str) -> str: """获取最近一次剪贴板读取的原始文本。""" return self._raw_clipboard.get(platform, "") @@ -512,8 +522,9 @@ class MessageMonitor: def _read_messages(self, window, platform: str = "") -> list: """每轮重新定位 ListControl。""" if platform == "qq": - # QQ NT/TIM Electron: UIA 不可遍历,唯一可靠是 SetForegroundWindow + SendKeys(Ctrl+A/C) - # 降频到 20s 一次,减少焦点切换次数;切回原前台由 sendkeys 后自动恢复 + # QQ NT/TIM Electron: UIA 不可遍历,剪贴板法+Win32回退 + # 先 SendMessage 点击消息区确保焦点位置 + self._click_message_area(platform) texts = self._read_via_clipboard(platform) if texts: return texts