diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c6224..aea9859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## beta 1.0.2.1016 - 2026-10-16 + +- Cache OCR-negative visible bubbles and reuse stable visual signatures as the + OCR cache key so unreadable or lightly re-rendered bubbles do not trigger + repeated 10+ second RapidOCR passes during active chat windows. + ## beta 1.0.1.1016 - 2026-10-16 - Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the diff --git a/README.md b/README.md index 40040ec..76fb63a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 并通过带视觉确认的 GUI 发送链路回复。 -当前版本:`beta 1.0.1.1016`,更新日期:2026-10-16。 +当前版本:`beta 1.0.2.1016`,更新日期:2026-10-16。 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、 Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major @@ -263,7 +263,7 @@ visually confirmed GUI bridge. Current version: ```text -beta 1.0.1.1016 +beta 1.0.2.1016 ``` Updated: 2026-10-16. diff --git a/VERSION b/VERSION index de79c2e..c650b31 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta 1.0.1.1016 +beta 1.0.2.1016 diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index 75e3820..133b114 100644 --- a/packages/jwebot-config/pyproject.toml +++ b/packages/jwebot-config/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-config" -version = "1.0.1b1+1016" +version = "1.0.2b1+1016" description = "Profile-based configuration compiler for JWeBot" requires-python = ">=3.9" dependencies = [ diff --git a/packages/jwebot-config/src/jwebot_config/__init__.py b/packages/jwebot-config/src/jwebot_config/__init__.py index 15e6b04..d2ced4f 100644 --- a/packages/jwebot-config/src/jwebot_config/__init__.py +++ b/packages/jwebot-config/src/jwebot_config/__init__.py @@ -36,4 +36,4 @@ __all__ = [ "write_default_profile", ] -__version__ = "1.0.1b1+1016" +__version__ = "1.0.2b1+1016" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index 6a5e817..e93d95e 100644 --- a/packages/jwebot-extension-host/pyproject.toml +++ b/packages/jwebot-extension-host/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-extension-host" -version = "1.0.1b1+1016" +version = "1.0.2b1+1016" description = "Single-release, single-invocation process host for JWeBot extensions" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.0.1b1+1016", - "jwebot-kernel==1.0.1b1+1016", + "jwebot-sdk==1.0.2b1+1016", + "jwebot-kernel==1.0.2b1+1016", ] [project.scripts] diff --git a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py index 274207e..5f87c1d 100644 --- a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py +++ b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py @@ -16,4 +16,4 @@ __all__ = [ "process_request", ] -__version__ = "1.0.1b1+1016" +__version__ = "1.0.2b1+1016" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index 38aa0a1..70a50d9 100644 --- a/packages/jwebot-kernel/pyproject.toml +++ b/packages/jwebot-kernel/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-kernel" -version = "1.0.1b1+1016" +version = "1.0.2b1+1016" description = "Stable routing and extension authority for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-config==1.0.1b1+1016", - "jwebot-sdk==1.0.1b1+1016", - "jwebot-official-extensions==1.0.1b1+1016", + "jwebot-config==1.0.2b1+1016", + "jwebot-sdk==1.0.2b1+1016", + "jwebot-official-extensions==1.0.2b1+1016", "tomli>=2; python_version < '3.11'", ] diff --git a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py index fcdc10f..40f83fa 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "verify_installed_release_signature", ] -__version__ = "1.0.1b1+1016" +__version__ = "1.0.2b1+1016" diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index f70b46f..b10353b 100644 --- a/packages/jwebot-official-extensions/pyproject.toml +++ b/packages/jwebot-official-extensions/pyproject.toml @@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-official-extensions" -version = "1.0.1b1+1016" +version = "1.0.2b1+1016" description = "Reviewed local command adapters and broker implementation for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.0.1b1+1016", + "jwebot-sdk==1.0.2b1+1016", ] [tool.setuptools.packages.find] diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py index 93396d9..6d2ede9 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py @@ -90,4 +90,4 @@ __all__ = [ "render_feature_list", ] -__version__ = "1.0.1b1+1016" +__version__ = "1.0.2b1+1016" diff --git a/packages/jwebot-sdk/pyproject.toml b/packages/jwebot-sdk/pyproject.toml index 842f040..8409651 100644 --- a/packages/jwebot-sdk/pyproject.toml +++ b/packages/jwebot-sdk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-sdk" -version = "1.0.1b1+1016" +version = "1.0.2b1+1016" description = "Typed, capability-limited extension API for JWeBot plugins" requires-python = ">=3.9" dependencies = [] diff --git a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py index 3fb968c..6afe6ce 100644 --- a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py +++ b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py @@ -38,4 +38,4 @@ __all__ = [ "SingleGroupContext", ] -__version__ = "1.0.1b1+1016" +__version__ = "1.0.2b1+1016" diff --git a/pyproject.toml b/pyproject.toml index dc547e7..61846b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.jwebot] -release = "beta 1.0.1.1016" +release = "beta 1.0.2.1016" production_deployments_enabled_by_default = false [tool.jwebot.workspace] diff --git a/tests/test_jwebot_edge.py b/tests/test_jwebot_edge.py index 0621576..3be9b0c 100644 --- a/tests/test_jwebot_edge.py +++ b/tests/test_jwebot_edge.py @@ -867,9 +867,9 @@ class LedgerAndApplicationTests(unittest.TestCase): self.assertEqual(version["route"], "local") self.assertIn("JWeBot 版本", version["text"]) self.assertIn("kernel", version["text"]) - self.assertIn("最新版本:beta 1.0.1.1016", version["text"]) + self.assertIn("最新版本:beta 1.0.2.1016", version["text"]) self.assertIn("更新时间:2026-10-16", version["text"]) - self.assertIn("更新摘要:Limit RapidOCR", version["text"]) + self.assertIn("更新摘要:Cache OCR-negative", version["text"]) self.assertEqual(debug["route"], "local") self.assertIn("调试埋点", debug["text"]) self.assertEqual(balance["route"], "local") diff --git a/tests/test_wechat_local_ocr.py b/tests/test_wechat_local_ocr.py index c787fc9..b9bb2b3 100755 --- a/tests/test_wechat_local_ocr.py +++ b/tests/test_wechat_local_ocr.py @@ -689,6 +689,46 @@ class OcrCacheTests(unittest.TestCase): self.assertEqual(engine.last_cache_hits, 0) self.assertEqual(engine.last_cache_misses, 1) + def test_antialias_drift_reuses_visual_signature_cache(self): + engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine) + bubble = MODULE.Bubble(50, 60, 132, 40, 5000) + first = MODULE.Image.new("RGB", (300, 180), (238, 238, 240)) + second = MODULE.Image.new("RGB", (300, 180), (242, 242, 244)) + first.putpixel((70, 75), (20, 20, 20)) + second.putpixel((70, 75), (35, 35, 35)) + + with mock.patch.object( + engine, + "_recognize_bubble", + return_value=self.message(60), + ) as recognize: + engine.recognize_messages(first, [bubble]) + messages, _ = engine.recognize_messages(second, [bubble]) + + self.assertEqual(recognize.call_count, 1) + self.assertEqual(len(messages), 1) + self.assertEqual(engine.last_cache_hits, 1) + self.assertEqual(engine.last_cache_misses, 0) + + def test_unrecognized_bubble_reuses_negative_cache(self): + engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine) + image = MODULE.Image.new("RGB", (300, 180), "white") + bubble = MODULE.Bubble(50, 60, 132, 40, 5000) + + with mock.patch.object( + engine, + "_recognize_bubble", + return_value=None, + ) as recognize: + first_messages, _ = engine.recognize_messages(image, [bubble]) + second_messages, _ = engine.recognize_messages(image, [bubble]) + + self.assertEqual(first_messages, []) + self.assertEqual(second_messages, []) + self.assertEqual(recognize.call_count, 1) + self.assertEqual(engine.last_cache_hits, 1) + self.assertEqual(engine.last_cache_misses, 0) + def test_periodic_refresh_revalidates_cached_bubbles(self): engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine) image = MODULE.Image.new("RGB", (300, 180), "white") diff --git a/wechat_local_ocr.py b/wechat_local_ocr.py index 160af56..5af6763 100755 --- a/wechat_local_ocr.py +++ b/wechat_local_ocr.py @@ -1126,7 +1126,7 @@ class OcrEngine: ) self._message_cache: OrderedDict[ str, - tuple[str, str, float, float, float], + tuple[str, str, float, float, float] | None, ] = OrderedDict() self._scan_count = 0 self.last_cache_hits = 0 @@ -1202,7 +1202,7 @@ class OcrEngine: def _cache(self) -> OrderedDict[ str, - tuple[str, str, float, float, float], + tuple[str, str, float, float, float] | None, ]: cache = getattr(self, "_message_cache", None) if cache is None: @@ -1282,15 +1282,17 @@ class OcrEngine: bubble_crop = image.crop( (bubble.x, bubble.y, bubble.right, bubble.bottom) ) - key = self._cache_key(sender_crop, bubble_crop) sequence_signature = bubble_sequence_signature( sender_crop, bubble_crop, ) - cached = cache.get(key) - if cached is not None: + key = sequence_signature + if key in cache: + cached = cache[key] hits += 1 cache.move_to_end(key) + if cached is None: + continue messages.append( IncomingMessage( sender=cached[0], @@ -1311,6 +1313,10 @@ class OcrEngine: visual_signature=sequence_signature, ) if message is None: + cache[key] = None + cache.move_to_end(key) + while len(cache) > OCR_MESSAGE_CACHE_SIZE: + cache.popitem(last=False) continue cache[key] = ( message.sender,