Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 集成兼容性
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<br>

[![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)

Expand Down
2 changes: 1 addition & 1 deletion __init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- "自学习"
Expand Down
1 change: 1 addition & 0 deletions services/core_learning/v2_learning_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion services/learning/jargon_learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
183 changes: 183 additions & 0 deletions tests/unit/test_jargon_learning.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
2 changes: 1 addition & 1 deletion web_src/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading