diff --git a/_keyword_scorer.py b/_keyword_scorer.py index 815fa8f..3a6f656 100644 --- a/_keyword_scorer.py +++ b/_keyword_scorer.py @@ -187,20 +187,28 @@ class KeywordScorer: conn.close() def should_skip_message(self, text: str, platform: str, + ocr_time: Optional[str] = None, threshold: int = 2) -> Tuple[bool, int, List[str]]: """判断是否应该跳过该消息(基于关键词得分 + 时间感知) - ⭐ Task #1558: 新逻辑 - - 得分≤1 → 直接放行(新消息) - - 得分≥2 → 检查最近一次打分时间: - * 最近打分在5分钟内 → 丢弃(同一用户重复) - * 最近打分超过5分钟 → 放行(不同用户的新提问) - 这样避免一刀切≥2分就丢弃,因为不同用户可能提出相同问题。 + 流程: + 1. 得分≤1 → 直接放行(新消息) + 2. 得分≥1 → 比较三个时间源: + - DB落库时间(历史最后一次处理时间) + - OCR识别时间(来自时间戳) + - 系统当前时间 + 3. 时间源选择: + - 若OCR时间与系统时间差 ≤ ±10秒 → 用OCR时间(OCR时间更准) + - 否则 → 用系统时间 + 4. 判断: + - 若当前时间 > DB落库时间 + 5分钟 → 放行(5分钟前已处理过) + - 否则 → 拦截(重复消息) Args: text: 消息文本 platform: 平台标识 - threshold: 得分阈值,≥threshold 则检查时间 + ocr_time: OCR识别到的时间戳(如'22:54:16'),可选 + threshold: 得分阈值 Returns: (是否跳过, 得分, 匹配关键词) @@ -218,47 +226,85 @@ class KeywordScorer: if score <= 1: return False, score, matched - # 得分≥2:检查最近一次同类消息的打分时间 - should_skip = self._should_skip_by_recent_score(platform, score, matched) + # 得分≥1:时间比较逻辑 + should_skip = self._should_skip_by_time(text, platform, score, matched, ocr_time) if should_skip: - logger.info(f"[keyword-scorer] 跳过消息(得分{score}≥{threshold},5分钟内): {text[:40]}... " + logger.info(f"[keyword-scorer] 跳过消息(得分{score},重复): {text[:40]}... " f"匹配关键词: {matched[:5]}") else: - logger.info(f"[keyword-scorer] 放行消息(得分{score}≥{threshold},超过5分钟或无记录): " - f"{text[:40]}... 匹配关键词: {matched[:5]}") + logger.info(f"[keyword-scorer] 放行消息(得分{score},时间超时): {text[:40]}... " + f"匹配关键词: {matched[:5]}") return should_skip, score, matched - def _should_skip_by_recent_score(self, platform: str, score: int, matched: List[str]) -> bool: - """检查最近一次同类消息的打分时间,超过5分钟则放行""" + def _should_skip_by_time(self, text: str, platform: str, score: int, matched: List[str], + ocr_time: Optional[str]) -> bool: + """时间比较逻辑:判断是否拦截重复消息 + + 规则: + 1. 时间源选择: + - 若OCR时间与系统时间差 ≤ ±10秒 → 用OCR时间(OCR时间更准) + - 否则 → 用系统时间 + 2. 判断: + - 当前时间 > DB落库时间 + 5分钟 → 放行 + - 否则 → 拦截 + """ + import hashlib + from datetime import timedelta + + # 获取DB落库时间(同平台、同hash的最后一条记录) + message_hash = hashlib.md5(text.strip().encode('utf-8')).hexdigest() conn = sqlite3.connect(self.db_path, check_same_thread=False) try: - from datetime import timedelta - cutoff_time = (datetime.now() - timedelta(seconds=300)).strftime("%Y-%m-%d %H:%M:%S") - - # 查询最近300秒内得分≥1的消息(不限score阈值,只看时间) + # 查询同hash的最新记录 cursor = conn.execute(""" SELECT local_time, score FROM keyword_scores WHERE platform = ? - AND score >= 1 - AND matched_keywords != '' - AND local_time > ? + AND message_hash = ? ORDER BY id DESC - LIMIT 3 - """, (platform, cutoff_time)) + LIMIT 1 + """, (platform, message_hash)) + row = cursor.fetchone() - recent = cursor.fetchall() - if not recent: - # 无近期记录 → 放行(可能是新用户的相同提问) - logger.debug(f"[keyword-scorer] 无近期记录,放行: score={score}, matched={matched[:3]}") + if not row: + # 无历史同hash记录 → 放行 + logger.debug(f"[keyword-scorer] 无历史同hash记录,放行: hash={message_hash[:8]}") return False - # 有近期记录 → 丢弃(同一用户重复提问) - logger.debug(f"[keyword-scorer] 发现近期记录,跳过: count={len(recent)}, " - f"最近时间={recent[0][0]}, score={recent[0][1]}") - return True + db_time_str = row[0] + db_time = datetime.strptime(db_time_str, "%Y-%m-%d %H:%M:%S") + logger.debug(f"[keyword-scorer] DB落库时间: {db_time_str}, score={row[1]}") + + # 确定当前时间(OCR时间 vs 系统时间) + now = datetime.now() + current_time = now + + if ocr_time: + # 解析OCR时间(格式如'22:54:16') + try: + ocr_dt = datetime.strptime(ocr_time, "%H:%M:%S") + # 同一天 + ocr_dt = ocr_dt.replace(year=now.year, month=now.month, day=now.day) + # 时间差检查 + time_diff = abs((now - ocr_dt).total_seconds()) + if time_diff <= 10: + current_time = ocr_dt + logger.debug(f"[keyword-scorer] 使用OCR时间(差值{time_diff:.0f}秒≤10秒)") + else: + logger.debug(f"[keyword-scorer] OCR时间差值{time_diff:.0f}秒>10秒,使用系统时间") + except ValueError: + logger.debug(f"[keyword-scorer] OCR时间解析失败'{ocr_time}',使用系统时间") + + # 判断:当前时间 > DB落库时间 + 5分钟 → 放行 + cutoff_time = db_time + timedelta(minutes=5) + if current_time > cutoff_time: + logger.debug(f"[keyword-scorer] 当前时间{current_time.strftime('%H:%M:%S')} > DB+5分{cutoff_time.strftime('%H:%M:%S')},放行") + return False + else: + logger.debug(f"[keyword-scorer] 当前时间{current_time.strftime('%H:%M:%S')} <= DB+5分{cutoff_time.strftime('%H:%M:%S')},拦截") + return True finally: conn.close() diff --git a/_ocr_blue_test.py b/_ocr_blue_test.py new file mode 100644 index 0000000..3c03419 --- /dev/null +++ b/_ocr_blue_test.py @@ -0,0 +1,22 @@ +import sys +sys.path.insert(0, '.') +from PIL import Image, ImageOps +from winrt.windows.media.ocr import OcrEngine +from winrt.windows.globalization import Language +from winrt.windows.graphics.imaging import SoftwareBitmap, BitmapPixelFormat + +def ocr_image(path): + im = Image.open(path).convert('RGBA') + sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height) + sb.copy_from_buffer(im.tobytes()) + eng = OcrEngine.try_create_from_language(Language('zh-Hans-CN')) + if not eng: + eng = OcrEngine.try_create_from_user_profile_languages() + res = eng.recognize_async(sb).get() + return res.text + +for p in ['img/raw/qq/_dbg_msg4_blue.png','img/raw/qq/_dbg_msg4_black.png','img/raw/qq/_dbg_msg4_blue6.png']: + try: + print(f'{p}: {ocr_image(p)!r}') + except Exception as e: + print(f'{p}: ERROR {e}') diff --git a/_ocr_crop_test.py b/_ocr_crop_test.py new file mode 100644 index 0000000..5ad1039 --- /dev/null +++ b/_ocr_crop_test.py @@ -0,0 +1,34 @@ +import sys +sys.path.insert(0, '.') +from PIL import Image, ImageOps, ImageEnhance +from winrt.windows.media.ocr import OcrEngine +from winrt.windows.globalization import Language +from winrt.windows.graphics.imaging import SoftwareBitmap, BitmapPixelFormat + +def ocr_image(path): + im = Image.open(path).convert('RGBA') + sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height) + sb.copy_from_buffer(im.tobytes()) + eng = OcrEngine.try_create_from_language(Language('zh-Hans-CN')) + if not eng: + eng = OcrEngine.try_create_from_user_profile_languages() + res = eng.recognize_async(sb).get() + return res.text + +im = Image.open('img/raw/qq/ocr_debug_qq_20260818_162956.261.png').convert('RGB') +# 整条消息区 y=380-740 x=0-739, 温和处理 +for (x0,y0,x1,y1,name,scale) in [ + (0,380,739,740,'bottom_full',2), + (0,540,739,740,'ts_bottom',3), + (0,555,430,665,'ts_msg',4), + (0,665,739,740,'very_bottom',4), +]: + crop = im.crop((x0,y0,x1,y1)) + crop8 = crop.resize((crop.width*scale, crop.height*scale), Image.LANCZOS) + p = f'img/raw/qq/_dbg_{name}.png' + crop8.save(p) + try: + t = ocr_image(p) + print(f'{name}: {t!r}') + except Exception as e: + print(f'{name}: ERROR {e}') \ No newline at end of file diff --git a/monitor.py b/monitor.py index 97fac73..0dba63b 100644 --- a/monitor.py +++ b/monitor.py @@ -533,6 +533,8 @@ class MessageMonitor: self._new_msg_cooldown: float = 3.0 # 同平台 3 秒内不重复处理 # 发送失败消息冷却:msg_hash → 失败时间,冷却期内不再尝试回复(防止卡死) self._failed_send_cooldown: dict = {} # platform → timestamp + # ⭐ 连续失败计数:同一消息连续3次发送失败则跳过本轮(用户可能已接管) + self._failed_consecutive_count: dict = {} # platform → int # 跨平台前台冷却:某平台刚切过前台(回复后),对端平台等待此时间再截图 self._foreground_cooldown_until: dict = {} # platform → float(time.time) @@ -8208,14 +8210,21 @@ class MessageMonitor: if len(ts) <= 3 and len(clean) == 0: continue - # ⭐ Task #1558: 关键词打分去重(替代OCR时间戳判断) - # 核心思路:与历史回复匹配,得分≥1视为已知回复, - # - 若5分钟内已有同类记录 → 跳过(同一用户重复) - # - 若超过5分钟或无记录 → 放行(不同用户的新提问) - # 得分≤1 直接放行(新消息) + # ⭐ Task #1558 新逻辑: 关键词打分前置(所有消息必须先打分) + # 流程: + # 1. 提取OCR时间戳(用于时间比较) + # 2. 调用关键词打分器 → ≤1分无条件放行,≥1分时间比较 + # 3. 时间比较:OCR时间 vs 系统时间(差值≤10秒用OCR,否则用系统时间) + # 4. 判断:当前时间 > DB落库时间+5分钟 → 放行,否则拦截 + ocr_time = None if self._keyword_scorer: + # 从文本中提取最近的时间戳(格式如'22:54:16') + _timestamps = re.findall(r'\d{1,2}:\d{1,2}(?::\d{1,2})?', ts) + if _timestamps: + ocr_time = _timestamps[-1] # 取最后一个时间戳 + should_skip, score, matched_kws = self._keyword_scorer.should_skip_message( - ts, platform, threshold=1 + ts, platform, ocr_time=ocr_time, threshold=1 ) if should_skip: logger.debug(f"[{platform}] 关键词打分跳过(score={score}): {ts[:50]}... " @@ -8225,6 +8234,8 @@ class MessageMonitor: elif score > 0: logger.info(f"[{platform}] 关键词得分(score={score},新消息): {ts[:50]}... " f"匹配: {matched_kws[:5]}") + else: + logger.debug(f"[{platform}] 关键词得分(score={score},新消息): {ts[:50]}") seen.add(ts) new_items.append(ts)