修复2问题: KB误匹配+返回全文 / Ctrl+C无法停止

1. KB匹配与返回全文
   - _poll中过滤纯时间/日期格式文本(OCR时间戳分割残留)->不入KB搜索
   - knowledge_base.py子串fallback要求query>=4字符,避免"19:15"等短时间戳误匹配
   - auto_reply._get_answer: 非Q&A文档只返回content前500字符,不返回整篇文件
   - 时间戳文本不进入KB + 短query不子串匹配 + 非Q&A截取 = 三层兜底

2. Ctrl+C无法停止
   - ThreadPoolExecutor创建非daemon线程->进程无法退出->必须杀CMD
     改用daemon thread + Event.wait(timeout): daemon线程不阻塞进程退出
   - main.py注册signal.signal(SIGINT) handler: 收到Ctrl+C后stop监控+quit窗口
   - 移除from concurrent.futures导入,替换为import signal
This commit is contained in:
youpaishidifu
2026-07-26 15:30:55 +08:00
parent 6d0974ea8f
commit 7bdc7ad637
16 changed files with 58 additions and 21 deletions
+9 -5
View File
@@ -67,15 +67,19 @@ class AutoReply:
def _get_answer(self, platform: str, msg: str, doc: dict, score: float) -> str or None: def _get_answer(self, platform: str, msg: str, doc: dict, score: float) -> str or None:
"""提取答案 + 防重复检查。 """提取答案 + 防重复检查。
优先返回 doc['answer'](Q&A 段),兜底用整段 content。 优先返回 doc['answer'](Q&A 段),兜底用整段 content。
非 Q&A 文档限制返回内容首 500 字符,避免子串匹配触发后返回整篇文件。
""" """
answer = (doc.get("answer") or doc["content"]).strip() answer = doc.get("answer")
if answer:
answer = answer.strip()
else:
# 非 Q&A 文档 → 取 content 前半段(最多 500 字符),不返回全文
content = doc.get("content", "").strip()
answer = content[:497] + "..." if len(content) > 500 else content
if not answer: if not answer:
return None return None
# 截取过长内容
if len(answer) > 500:
answer = answer[:497] + "..."
# 防重复指纹 # 防重复指纹
fp = hash(f"{platform}:{msg}:{answer[:80]}") fp = hash(f"{platform}:{msg}:{answer[:80]}")
replied_set = self._replied.setdefault(platform, set()) replied_set = self._replied.setdefault(platform, set())
+7 -5
View File
@@ -151,11 +151,13 @@ class KnowledgeBase:
# Fallback: 子串匹配(查询包含在文档标题或内容中) # Fallback: 子串匹配(查询包含在文档标题或内容中)
q = query.strip() q = query.strip()
for doc in self.docs: # 短文本(<4 字符)不做子串匹配,避免"19:15""14:30"等时间戳误匹配 KB 文档
if q in doc["content"] or q in doc["title"]: if len(q) >= 4:
logger = logging.getLogger("msgbot.kb") for doc in self.docs:
logger.debug(f"子串匹配: \"{q}\" → {doc['title']}") if q in doc["content"] or q in doc["title"]:
return [(doc, min_score)] logger = logging.getLogger("msgbot.kb")
logger.debug(f"子串匹配: \"{q}\" → {doc['title']}")
return [(doc, min_score)]
return results return results
def get_doc_count(self) -> int: def get_doc_count(self) -> int:
+12
View File
@@ -3,6 +3,7 @@
import logging import logging
import os import os
import sys import sys
import signal
# 确保在项目目录 # 确保在项目目录
os.chdir(os.path.dirname(os.path.abspath(__file__))) os.chdir(os.path.dirname(os.path.abspath(__file__)))
@@ -42,6 +43,17 @@ def main():
sys.exit(1) sys.exit(1)
app = MsgBotApp() app = MsgBotApp()
# Ctrl+C 信号处理:Windows Tkinter mainloop 不响应 KeyboardInterrupt
# 需 signal handler 强制停止监控 + 退出
def _sigint_handler(sig, frame):
logger.info("收到 Ctrl+C,正在关闭...")
app.monitor.stop()
app.quit()
sys.exit(0)
signal.signal(signal.SIGINT, _sigint_handler)
app.mainloop() app.mainloop()
+30 -11
View File
@@ -16,7 +16,7 @@ import ctypes
import difflib import difflib
import os import os
from datetime import datetime from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout import signal
from PIL import ImageGrab from PIL import ImageGrab
from winrt.windows.media.ocr import OcrEngine from winrt.windows.media.ocr import OcrEngine
from winrt.windows.globalization import Language from winrt.windows.globalization import Language
@@ -212,6 +212,26 @@ class MessageMonitor:
pass pass
return None return None
def _ensure_hwnd_with_timeout(self, platform: str, timeout: float = 12.0) -> Optional[int]:
"""_ensure_hwnd 包装 daemon 线程超时保护。
ThreadPoolExecutor 创建非 daemon 线程 → Ctrl+C 进程不退出。
改用 daemon thread + Event.wait(timeout),daemon 线程不阻塞进程退出。
"""
result = [None]
done = threading.Event()
def worker():
result[0] = self._ensure_hwnd(platform)
done.set()
t = threading.Thread(target=worker, daemon=True)
t.start()
if not done.wait(timeout=timeout):
logger.warning(f"[{platform}] _ensure_hwnd 超时({timeout:.0f}s),跳过本轮")
return None
return result[0]
# ── 强制刷新 UIA ────────────────────────────────────────── # ── 强制刷新 UIA ──────────────────────────────────────────
def _fresh_window(self, platform: str): def _fresh_window(self, platform: str):
@@ -1411,16 +1431,7 @@ class MessageMonitor:
self._poll_count[platform] = counter self._poll_count[platform] = counter
# 1. 确保窗口句柄(带超时保护,防止 UIA COM 调用挂死不返回) # 1. 确保窗口句柄(带超时保护,防止 UIA COM 调用挂死不返回)
hwnd = None hwnd = self._ensure_hwnd_with_timeout(platform, timeout=12.0)
_ex = ThreadPoolExecutor(max_workers=1)
try:
hwnd = _ex.submit(self._ensure_hwnd, platform).result(timeout=12.0)
except FuturesTimeout:
logger.warning(f"[{platform}] _ensure_hwnd 超时(12s),跳过本轮")
except Exception as e:
logger.warning(f"[{platform}] _ensure_hwnd 异常: {e}")
finally:
_ex.shutdown(wait=False)
if not hwnd: if not hwnd:
if counter % 10 == 0: if counter % 10 == 0:
logger.debug(f"[{platform}] 窗口未找到") logger.debug(f"[{platform}] 窗口未找到")
@@ -1499,6 +1510,14 @@ class MessageMonitor:
MAX_NEW_ITEMS = 3 MAX_NEW_ITEMS = 3
new_items = new_items[:MAX_NEW_ITEMS] new_items = new_items[:MAX_NEW_ITEMS]
# 过滤纯时间/日期文本 → 不应进入 KB 匹配(OCR 时间戳分割遗留的噪音片段)
# 这些文本在 KB 子串匹配阶段会命中文件内任何含相同数字的内容
new_items = [
it for it in new_items
if not re.match(r'^\d{1,2}[::]\d{2}(?:[::]\d{2})?$', it.strip())
and not re.match(r'^\d{4}[/-]\d{1,2}', it.strip())
]
# 更新 last_texts 与签名 # 更新 last_texts 与签名
self._last_texts[platform] = list(texts) self._last_texts[platform] = list(texts)
self._last_sig[platform] = sig self._last_sig[platform] = sig
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB