修复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
@@ -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())
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |