@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# 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
|
## beta 1.0.1.1016 - 2026-10-16
|
||||||
|
|
||||||
- Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the
|
- Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
|||||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||||
并通过带视觉确认的 GUI 发送链路回复。
|
并通过带视觉确认的 GUI 发送链路回复。
|
||||||
|
|
||||||
当前版本:`beta 1.0.1.1016`,更新日期:2026-10-16。
|
当前版本:`beta 1.0.2.1016`,更新日期:2026-10-16。
|
||||||
|
|
||||||
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
|
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
|
||||||
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
|
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
|
||||||
@@ -263,7 +263,7 @@ visually confirmed GUI bridge.
|
|||||||
Current version:
|
Current version:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
beta 1.0.1.1016
|
beta 1.0.2.1016
|
||||||
```
|
```
|
||||||
|
|
||||||
Updated: 2026-10-16.
|
Updated: 2026-10-16.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-config"
|
name = "jwebot-config"
|
||||||
version = "1.0.1b1+1016"
|
version = "1.0.2b1+1016"
|
||||||
description = "Profile-based configuration compiler for JWeBot"
|
description = "Profile-based configuration compiler for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ __all__ = [
|
|||||||
"write_default_profile",
|
"write_default_profile",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.1b1+1016"
|
__version__ = "1.0.2b1+1016"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-extension-host"
|
name = "jwebot-extension-host"
|
||||||
version = "1.0.1b1+1016"
|
version = "1.0.2b1+1016"
|
||||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==1.0.1b1+1016",
|
"jwebot-sdk==1.0.2b1+1016",
|
||||||
"jwebot-kernel==1.0.1b1+1016",
|
"jwebot-kernel==1.0.2b1+1016",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ __all__ = [
|
|||||||
"process_request",
|
"process_request",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.1b1+1016"
|
__version__ = "1.0.2b1+1016"
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-kernel"
|
name = "jwebot-kernel"
|
||||||
version = "1.0.1b1+1016"
|
version = "1.0.2b1+1016"
|
||||||
description = "Stable routing and extension authority for JWeBot"
|
description = "Stable routing and extension authority for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-config==1.0.1b1+1016",
|
"jwebot-config==1.0.2b1+1016",
|
||||||
"jwebot-sdk==1.0.1b1+1016",
|
"jwebot-sdk==1.0.2b1+1016",
|
||||||
"jwebot-official-extensions==1.0.1b1+1016",
|
"jwebot-official-extensions==1.0.2b1+1016",
|
||||||
"tomli>=2; python_version < '3.11'",
|
"tomli>=2; python_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -107,4 +107,4 @@ __all__ = [
|
|||||||
"verify_installed_release_signature",
|
"verify_installed_release_signature",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.1b1+1016"
|
__version__ = "1.0.2b1+1016"
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-official-extensions"
|
name = "jwebot-official-extensions"
|
||||||
version = "1.0.1b1+1016"
|
version = "1.0.2b1+1016"
|
||||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==1.0.1b1+1016",
|
"jwebot-sdk==1.0.2b1+1016",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -90,4 +90,4 @@ __all__ = [
|
|||||||
"render_feature_list",
|
"render_feature_list",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.1b1+1016"
|
__version__ = "1.0.2b1+1016"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-sdk"
|
name = "jwebot-sdk"
|
||||||
version = "1.0.1b1+1016"
|
version = "1.0.2b1+1016"
|
||||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ __all__ = [
|
|||||||
"SingleGroupContext",
|
"SingleGroupContext",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.1b1+1016"
|
__version__ = "1.0.2b1+1016"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tool.jwebot]
|
[tool.jwebot]
|
||||||
release = "beta 1.0.1.1016"
|
release = "beta 1.0.2.1016"
|
||||||
production_deployments_enabled_by_default = false
|
production_deployments_enabled_by_default = false
|
||||||
|
|
||||||
[tool.jwebot.workspace]
|
[tool.jwebot.workspace]
|
||||||
|
|||||||
@@ -867,9 +867,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
|||||||
self.assertEqual(version["route"], "local")
|
self.assertEqual(version["route"], "local")
|
||||||
self.assertIn("JWeBot 版本", version["text"])
|
self.assertIn("JWeBot 版本", version["text"])
|
||||||
self.assertIn("kernel", 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("更新时间:2026-10-16", version["text"])
|
||||||
self.assertIn("更新摘要:Limit RapidOCR", version["text"])
|
self.assertIn("更新摘要:Cache OCR-negative", version["text"])
|
||||||
self.assertEqual(debug["route"], "local")
|
self.assertEqual(debug["route"], "local")
|
||||||
self.assertIn("调试埋点", debug["text"])
|
self.assertIn("调试埋点", debug["text"])
|
||||||
self.assertEqual(balance["route"], "local")
|
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_hits, 0)
|
||||||
self.assertEqual(engine.last_cache_misses, 1)
|
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):
|
def test_periodic_refresh_revalidates_cached_bubbles(self):
|
||||||
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
|
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
|
||||||
image = MODULE.Image.new("RGB", (300, 180), "white")
|
image = MODULE.Image.new("RGB", (300, 180), "white")
|
||||||
|
|||||||
+11
-5
@@ -1126,7 +1126,7 @@ class OcrEngine:
|
|||||||
)
|
)
|
||||||
self._message_cache: OrderedDict[
|
self._message_cache: OrderedDict[
|
||||||
str,
|
str,
|
||||||
tuple[str, str, float, float, float],
|
tuple[str, str, float, float, float] | None,
|
||||||
] = OrderedDict()
|
] = OrderedDict()
|
||||||
self._scan_count = 0
|
self._scan_count = 0
|
||||||
self.last_cache_hits = 0
|
self.last_cache_hits = 0
|
||||||
@@ -1202,7 +1202,7 @@ class OcrEngine:
|
|||||||
|
|
||||||
def _cache(self) -> OrderedDict[
|
def _cache(self) -> OrderedDict[
|
||||||
str,
|
str,
|
||||||
tuple[str, str, float, float, float],
|
tuple[str, str, float, float, float] | None,
|
||||||
]:
|
]:
|
||||||
cache = getattr(self, "_message_cache", None)
|
cache = getattr(self, "_message_cache", None)
|
||||||
if cache is None:
|
if cache is None:
|
||||||
@@ -1282,15 +1282,17 @@ class OcrEngine:
|
|||||||
bubble_crop = image.crop(
|
bubble_crop = image.crop(
|
||||||
(bubble.x, bubble.y, bubble.right, bubble.bottom)
|
(bubble.x, bubble.y, bubble.right, bubble.bottom)
|
||||||
)
|
)
|
||||||
key = self._cache_key(sender_crop, bubble_crop)
|
|
||||||
sequence_signature = bubble_sequence_signature(
|
sequence_signature = bubble_sequence_signature(
|
||||||
sender_crop,
|
sender_crop,
|
||||||
bubble_crop,
|
bubble_crop,
|
||||||
)
|
)
|
||||||
cached = cache.get(key)
|
key = sequence_signature
|
||||||
if cached is not None:
|
if key in cache:
|
||||||
|
cached = cache[key]
|
||||||
hits += 1
|
hits += 1
|
||||||
cache.move_to_end(key)
|
cache.move_to_end(key)
|
||||||
|
if cached is None:
|
||||||
|
continue
|
||||||
messages.append(
|
messages.append(
|
||||||
IncomingMessage(
|
IncomingMessage(
|
||||||
sender=cached[0],
|
sender=cached[0],
|
||||||
@@ -1311,6 +1313,10 @@ class OcrEngine:
|
|||||||
visual_signature=sequence_signature,
|
visual_signature=sequence_signature,
|
||||||
)
|
)
|
||||||
if message is None:
|
if message is None:
|
||||||
|
cache[key] = None
|
||||||
|
cache.move_to_end(key)
|
||||||
|
while len(cache) > OCR_MESSAGE_CACHE_SIZE:
|
||||||
|
cache.popitem(last=False)
|
||||||
continue
|
continue
|
||||||
cache[key] = (
|
cache[key] = (
|
||||||
message.sender,
|
message.sender,
|
||||||
|
|||||||
Reference in New Issue
Block a user