From a3c525e3ee53f3fa2c58dbadce143f33183e6715 Mon Sep 17 00:00:00 2001 From: EterUltimate <1831303476@qq.com> Date: Sat, 29 Aug 2026 22:55:26 +0800 Subject: [PATCH 1/2] fix: only exclude confirmed jargon from candidate statistics The statistics pre-filter exclusion query did not distinguish confirmation state, so unconfirmed candidates (is_jargon IS NULL) were excluded as well and their occurrence count never accumulated toward the inference threshold, leaving collected jargon unexplained. - pass only_confirmed=True in jargon_learning and the V2 batch path - document that _get_existing_jargon_terms returns confirmed terms - add unit tests covering both learning paths Fixes #245 --- .../core_learning/v2_learning_integration.py | 1 + services/learning/jargon_learning.py | 3 +- tests/unit/test_jargon_learning.py | 183 ++++++++++++++++++ 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_jargon_learning.py diff --git a/services/core_learning/v2_learning_integration.py b/services/core_learning/v2_learning_integration.py index 6ef3c155..a04a6c58 100644 --- a/services/core_learning/v2_learning_integration.py +++ b/services/core_learning/v2_learning_integration.py @@ -734,6 +734,7 @@ async def _jargon_batch(group_id: str) -> None: rows = await db.get_recent_jargon_list( chat_id=group_id, limit=1000, + only_confirmed=True, ) exclude_terms = { str(row.get("content") or "").strip() diff --git a/services/learning/jargon_learning.py b/services/learning/jargon_learning.py index a9c6deb4..24ea3b43 100644 --- a/services/learning/jargon_learning.py +++ b/services/learning/jargon_learning.py @@ -164,13 +164,14 @@ async def mine_jargon(self, group_id: str) -> None: ) async def _get_existing_jargon_terms(self, group_id: str) -> Set[str]: - """Return existing DB jargon/candidate terms for statistical pre-filter exclusion.""" + """Return confirmed DB jargon terms for statistical pre-filter exclusion.""" if not self._db_manager or not hasattr(self._db_manager, "get_recent_jargon_list"): return set() try: rows = await self._db_manager.get_recent_jargon_list( chat_id=group_id, limit=1000, + only_confirmed=True, ) except Exception as exc: logger.debug( diff --git a/tests/unit/test_jargon_learning.py b/tests/unit/test_jargon_learning.py new file mode 100644 index 00000000..bbfa6c10 --- /dev/null +++ b/tests/unit/test_jargon_learning.py @@ -0,0 +1,183 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest + +from self_learning_EterU.services.core_learning.v2_learning_integration import ( + V2LearningIntegration, +) +from self_learning_EterU.services.learning.jargon_learning import JargonLearningModule + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_jargon_exclusion_only_loads_confirmed_terms(): + pending = {"content": "待确认", "is_jargon": None} + confirmed = {"content": "已确认", "is_jargon": True} + calls = [] + + async def get_recent_jargon_list(**kwargs): + calls.append(kwargs) + if kwargs.get("only_confirmed") is True: + return [confirmed] + return [pending, confirmed] + + module = JargonLearningModule( + config=SimpleNamespace(enable_jargon_learning=True), + message_collector=None, + jargon_miner_manager=None, + jargon_statistical_filter=None, + db_manager=SimpleNamespace(get_recent_jargon_list=get_recent_jargon_list), + ) + + terms = await module._get_existing_jargon_terms("group-a") + + assert terms == {"已确认"} + assert calls == [ + { + "chat_id": "group-a", + "limit": 1000, + "only_confirmed": True, + } + ] + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_mine_jargon_keeps_pending_statistical_candidates_eligible(): + pending = {"content": "待确认", "is_jargon": None} + confirmed = {"content": "已确认", "is_jargon": True} + query_calls = [] + candidate = { + "term": "待确认", + "context_examples": ["待确认上下文"], + } + + async def get_recent_jargon_list(**kwargs): + query_calls.append(kwargs) + if kwargs.get("only_confirmed") is True: + return [confirmed] + return [pending, confirmed] + + def get_jargon_candidates(group_id, *, top_k, exclude_terms): + assert group_id == "group-a" + assert top_k == 20 + return [] if "待确认" in exclude_terms else [candidate] + + statistical_filter = SimpleNamespace( + get_jargon_candidates=Mock(side_effect=get_jargon_candidates), + ) + miner = SimpleNamespace( + should_trigger=Mock(return_value=True), + run_once=AsyncMock(), + ) + db = SimpleNamespace( + get_recent_raw_messages=AsyncMock( + return_value=[ + {"sender_id": "user-a", "message": "这是一条聊天上下文消息"} + for _ in range(10) + ] + ), + get_recent_jargon_list=get_recent_jargon_list, + ) + module = JargonLearningModule( + config=SimpleNamespace(enable_jargon_learning=True), + message_collector=SimpleNamespace( + get_statistics=AsyncMock(return_value={"raw_messages": 10}) + ), + jargon_miner_manager=SimpleNamespace( + get_or_create_miner=Mock(return_value=miner) + ), + jargon_statistical_filter=statistical_filter, + db_manager=db, + ) + + await module.mine_jargon("group-a") + + assert query_calls == [ + { + "chat_id": "group-a", + "limit": 1000, + "only_confirmed": True, + } + ] + statistical_filter.get_jargon_candidates.assert_called_once_with( + "group-a", + top_k=20, + exclude_terms={"已确认"}, + ) + miner.run_once.assert_awaited_once() + assert miner.run_once.await_args.kwargs["statistical_candidates"] == [candidate] + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_v2_jargon_batch_only_excludes_confirmed_terms(): + pending = {"content": "待确认", "is_jargon": None} + confirmed = {"content": "已确认", "is_jargon": True} + query_calls = [] + + async def get_recent_jargon_list(**kwargs): + query_calls.append(kwargs) + if kwargs.get("only_confirmed") is True: + return [confirmed] + return [pending, confirmed] + + candidate = {"term": "待确认"} + + def get_jargon_candidates(group_id, *, top_k, exclude_terms): + assert group_id == "group-a" + assert top_k == 20 + return [] if "待确认" in exclude_terms else [candidate] + + jargon_filter = SimpleNamespace( + get_jargon_candidates=Mock(side_effect=get_jargon_candidates), + ) + db = SimpleNamespace( + get_recent_jargon_list=get_recent_jargon_list, + save_or_update_jargon=AsyncMock(), + ) + llm = SimpleNamespace( + generate_response=AsyncMock(return_value="测试释义"), + ) + integration = V2LearningIntegration.__new__(V2LearningIntegration) + integration._jargon_filter = jargon_filter + integration._llm = llm + integration._db = db + integration._knowledge_manager = None + integration._memory_manager = None + integration._exemplar_library = None + integration._social_analyzer = None + integration._ingestion_buffer = {} + + from self_learning_EterU.services.quality import TieredLearningTrigger + + integration._trigger = TieredLearningTrigger() + integration._register_trigger_operations() + + assert await integration._trigger.force_tier2("jargon", "group-a") is True + + assert query_calls == [ + { + "chat_id": "group-a", + "limit": 1000, + "only_confirmed": True, + } + ] + jargon_filter.get_jargon_candidates.assert_called_once_with( + "group-a", + top_k=20, + exclude_terms={"已确认"}, + ) + llm.generate_response.assert_awaited_once() + db.save_or_update_jargon.assert_awaited_once_with( + "group-a", + "待确认", + { + "meaning": "测试释义", + "raw_content": "[]", + "is_jargon": True, + "count": 1, + "is_complete": True, + }, + ) From 1d7a8653c9cb63f8a3e44b48ffc5bf2995f67acd Mon Sep 17 00:00:00 2001 From: EterUltimate <1831303476@qq.com> Date: Sat, 29 Aug 2026 22:55:53 +0800 Subject: [PATCH 2/2] chore: bump version to 3.6.4 --- CHANGELOG.md | 15 +++++++++++++++ README.md | 2 +- README_EN.md | 2 +- __init__.py | 2 +- docs/README.md | 2 +- metadata.yaml | 2 +- web_src/package.json | 2 +- 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421c00b8..87164941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ 所有重要更改都将记录在此文件中。 +## [3.6.4] - 2026-08-29 + +### 黑话学习 + +- 修复黑话被收录后长时间无法解释的问题,修复 [#245](https://github.com/NickCharlie/astrbot_plugin_self_learning/issues/245):候选统计的排除查询未区分确认状态,把 `is_jargon IS NULL` 的待确认词条一并排除,导致候选出现次数始终无法累加到推断阈值。现显式传入 `only_confirmed=True`,仅排除已确认黑话;待确认候选继续参与统计,重复出现时正常累加 count,达到 3 次后触发含义推断。 +- V2 学习批处理路径同步应用相同的确认状态过滤,避免启用 V2 学习时出现同样的问题;`_get_existing_jargon_terms()` 的方法说明同步更新为「返回已确认词条」。 + +### 测试 + +- 新增单元测试覆盖:统计查询正确传递 `only_confirmed=True`;旧版学习流程不再排除待确认候选;V2 黑话批处理流程使用相同的确认状态过滤。 + +### 版本 + +- 将插件元数据、运行时包、Dashboard 和文档版本统一提升至 `3.6.4`。 + ## [3.6.3] - 2026-08-28 ### 集成兼容性 diff --git a/README.md b/README.md index 5d77bc4c..1e9893b6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 让 AstrBot 在群聊中持续采集、学习、审查并注入上下文,使 Bot 逐步具备表达风格、群组黑话、社交关系、长期记忆和人格演化能力。 -[![Version](https://img.shields.io/badge/version-3.6.3-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) +[![Version](https://img.shields.io/badge/version-3.6.4-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![AstrBot](https://img.shields.io/badge/AstrBot-%3E%3D4.11.4-orange.svg)](https://github.com/Soulter/AstrBot) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) diff --git a/README_EN.md b/README_EN.md index b49983d5..a898ed1b 100644 --- a/README_EN.md +++ b/README_EN.md @@ -14,7 +14,7 @@
-[![Version](https://img.shields.io/badge/version-3.6.3-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![AstrBot](https://img.shields.io/badge/AstrBot-%3E%3D4.11.4-orange.svg)](https://github.com/Soulter/AstrBot) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) +[![Version](https://img.shields.io/badge/version-3.6.4-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![AstrBot](https://img.shields.io/badge/AstrBot-%3E%3D4.11.4-orange.svg)](https://github.com/Soulter/AstrBot) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) [Features](#what-we-can-do) · [Quick Start](#quick-start) · [Web UI](#visual-management-interface) · [Community](#community) · [Contributing](CONTRIBUTING.md) diff --git a/__init__.py b/__init__.py index 78fffba1..49537b25 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ # AstrBot 自学习插件 -__version__ = "3.6.3" +__version__ = "3.6.4" # Ensure parent namespace packages ("data", "data.plugins") are # durably registered in sys.modules. AstrBot loads plugins via diff --git a/docs/README.md b/docs/README.md index 1d5dba5a..1880af6d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ AstrBot 自主学习插件的实现文档和使用文档。 - 插件名: `astrbot_plugin_self_learning` - 展示名: `self-learning` -- 当前元数据版本: `3.6.3` +- 当前元数据版本: `3.6.4` - 最低 AstrBot 版本: `4.11.4` - 主要入口: `main.py` - 配置入口: `_conf_schema.json`, `config.py` diff --git a/metadata.yaml b/metadata.yaml index a2df19f2..1b0fe065 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -2,7 +2,7 @@ name: "astrbot_plugin_self_learning" author: "NickMo, EterUltimate" display_name: "self-learning" description: "SELF LEARNING 自主学习插件 — 让 AI 聊天机器人自主学习对话风格、理解群组黑话、管理社交关系与好感度、自适应人格演化,像真人一样自然对话。(使用前必须手动备份人格数据)" -version: "3.6.3" +version: "3.6.4" repo: "https://github.com/NickCharlie/astrbot_plugin_self_learning" tags: - "自学习" diff --git a/web_src/package.json b/web_src/package.json index 9b0901f1..2ffa4d37 100644 --- a/web_src/package.json +++ b/web_src/package.json @@ -1,6 +1,6 @@ { "name": "astrbot-self-learning-dashboard", - "version": "3.6.3", + "version": "3.6.4", "description": "Solid.js source for the self-learning plugin dashboard", "type": "module", "scripts": {