@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -36,4 +36,4 @@ __all__ = [
|
||||
"write_default_profile",
|
||||
]
|
||||
|
||||
__version__ = "1.0.1b1+1016"
|
||||
__version__ = "1.0.2b1+1016"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -16,4 +16,4 @@ __all__ = [
|
||||
"process_request",
|
||||
]
|
||||
|
||||
__version__ = "1.0.1b1+1016"
|
||||
__version__ = "1.0.2b1+1016"
|
||||
|
||||
@@ -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'",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "1.0.1b1+1016"
|
||||
__version__ = "1.0.2b1+1016"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -90,4 +90,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "1.0.1b1+1016"
|
||||
__version__ = "1.0.2b1+1016"
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -38,4 +38,4 @@ __all__ = [
|
||||
"SingleGroupContext",
|
||||
]
|
||||
|
||||
__version__ = "1.0.1b1+1016"
|
||||
__version__ = "1.0.2b1+1016"
|
||||
|
||||
+1
-1
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
+11
-5
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user