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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

所有重要更改都将记录在此文件中。

## [4.2.2] - 2026-09-18

### 命令消息直接放行(issue #254)

- 新增 `enable_command_pass_through` 开关(默认开启):以系统级命令前缀(`/` `!` `#` `.` 等)开头的命令消息会跳过 LLM Hook 上下文注入直接处理,避免功能命令(如其他插件的管理/工具命令)的响应被上下文拉取拉长。
- 命令识别逻辑抽为 `CommandFilter.is_command_text`,与消息收集路径的命令过滤保持同一前缀约定。

### LightRAG LLM 响应缓存治理(issue #253)

- 修复 `kv_store_llm_response_cache.json` 无上限增长(长期运行单群可达 500MB+,冷加载 5~7 秒并触发 LLM Hook 批量超时):插件构造 LightRAG 时显式关闭 `enable_llm_cache` 与 `enable_llm_cache_for_entity_extract`,可通过新增配置 `lightrag_enable_llm_cache`(默认关闭)开启。
- 缓存关闭时自动清理历史残留:知识管理器启动时与每个群实例创建前会删除旧的 `kv_store_llm_response_cache.json`(该文件为纯缓存,删除后 JsonKVStorage 以空缓存加载,不影响图谱与向量数据)。
- 新增管理员命令 `/clean_rag_cache`:手动清理全部(或指定)群的 LLM 响应缓存,热实例走 LightRAG `aclear_cache` API,冷群直接移除缓存文件,并报告释放空间。

### 审查修复

- `/clean_rag_cache` 的群号参数按安全格式校验(仅接受字母/数字/下划线/连字符/冒号,拒绝路径穿越);缓存文件删除前校验目标路径始终位于 LightRAG 数据目录内。
- 缓存清理与群实例初始化共用同一把 per-group 锁并在锁内重新判定冷/热,消除并发初始化时"刚清理的缓存被新实例复活"的竞态。

### 版本

- 版本号由 4.2.1 提升至 **4.2.2**。

## [4.2.1] - 2026-09-12

### LivingMemory 2.7 适配
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-4.2.1-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning)
[![Version](https://img.shields.io/badge/version-4.2.2-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-4.2.1-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-4.2.2-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__ = "4.2.1"
__version__ = "4.2.2"

# Ensure parent namespace packages ("data", "data.plugins") are
# durably registered in sys.modules. AstrBot loads plugins via
Expand Down
12 changes: 12 additions & 0 deletions _conf_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"hint": "开启后每次回复前会并行拉取社交、记忆、黑话、few-shot 等上下文;默认关闭以避免高频模型调用",
"default": false
},
"enable_command_pass_through": {
"description": "命令消息直接放行",
"type": "bool",
"hint": "开启后,以系统级命令前缀(/ ! # . 等)开头的命令消息会跳过 LLM Hook 上下文注入直接处理,避免功能命令响应被拉长",
"default": true
},
"use_sqlalchemy": {
"description": "强制使用 SQLAlchemy ORM",
"type": "bool",
Expand Down Expand Up @@ -977,6 +983,12 @@
"hint": "LightRAG检索模式。local=仅实体邻域检索(低延迟),hybrid=实体邻域+全局社区聚合(高质量但慢约4-5秒),naive=纯向量检索,global=仅全局社区,mix=混合模式。若同时委托 LivingMemory,hybrid/mix 会叠加记忆检索与融合上下文,可能显著增加 LLM 调用与 token 消耗,优先建议 local/naive。",
"default": "local"
},
"lightrag_enable_llm_cache": {
"description": "LightRAG 缓存 LLM 响应",
"type": "bool",
"hint": "开启后 LightRAG 会缓存实体抽取与查询的 LLM 响应(kv_store_llm_response_cache.json)。该文件无上限且会拖慢冷加载;自然语言聊天查询重复率低,默认关闭。开启后可在群内用 /clean_rag_cache 清理。",
"default": false
},
"memory_engine": {
"description": "记忆引擎",
"type": "string",
Expand Down
8 changes: 8 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class PluginConfig(BaseModel):
# v2 Architecture: Knowledge engine
knowledge_engine: str = "legacy" # "lightrag" | "legacy"
lightrag_query_mode: str = "local" # "naive" | "local" | "global" | "hybrid" | "mix"
lightrag_enable_llm_cache: bool = False # LightRAG 缓存 LLM 响应;默认关闭避免 kv_store_llm_response_cache.json 无上限增长

# v2 Architecture: Memory engine
memory_engine: str = "legacy" # "mem0" | "legacy"
Expand Down Expand Up @@ -242,6 +243,7 @@ class PluginConfig(BaseModel):
service_stop_timeout: int = 5 # 单个服务停止超时
enable_llm_hooks: bool = False # 启用 LLM Hook 上下文注入,默认关闭以避免高频调用
llm_hook_context_timeout: float = 3.0 # LLM Hook 单个上下文源超时(秒)
enable_command_pass_through: bool = True # 命令消息直接放行:跳过 LLM Hook 上下文注入,保证命令响应速度

# PersonaUpdater配置
persona_merge_strategy: str = "smart" # 人格合并策略: "replace", "append", "prepend", "smart"
Expand Down Expand Up @@ -485,6 +487,9 @@ def create_from_config(cls, config: dict, data_dir: Optional[str] = None) -> 'Pl
),
knowledge_engine=v2_settings.get('knowledge_engine', 'legacy'),
lightrag_query_mode=v2_settings.get('lightrag_query_mode', 'local'),
lightrag_enable_llm_cache=v2_settings.get(
'lightrag_enable_llm_cache', False
),
memory_engine=v2_settings.get('memory_engine', 'legacy'),

# 功能融合设置
Expand Down Expand Up @@ -602,6 +607,9 @@ def create_from_config(cls, config: dict, data_dir: Optional[str] = None) -> 'Pl
service_stop_timeout=runtime_internal_settings.get('service_stop_timeout', 5),
enable_llm_hooks=runtime_internal_settings.get('enable_llm_hooks', False),
llm_hook_context_timeout=float(runtime_internal_settings.get('llm_hook_context_timeout', 3.0)),
enable_command_pass_through=runtime_internal_settings.get(
'enable_command_pass_through', True
),
llm_hook_injection_target=runtime_internal_settings.get(
'llm_hook_injection_target',
CACHE_FRIENDLY_LLM_HOOK_TARGET,
Expand Down
1 change: 1 addition & 0 deletions core/plugin_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def bootstrap(
db_manager=p.db_manager,
llm_adapter=p.llm_adapter,
remember_service=p.remember_service,
v2_integration=getattr(p, "v2_integration", None),
)
p._command_filter = CommandFilter()

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`
- 当前元数据版本: `4.2.1`
- 当前元数据版本: `4.2.2`
- 最低 AstrBot 版本: `4.11.4`
- 主要入口: `main.py`
- 配置入口: `_conf_schema.json`, `config.py`
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,20 @@ PostgreSQL 支持 `postgresql_schema`,非 `public` 时会自动创建 schema
| `rerank_min_candidates` | `3` | 候选数低于该值跳过 rerank |
| `knowledge_engine` | `legacy` | `legacy` 或 `lightrag` |
| `lightrag_query_mode` | `local` | LightRAG 查询模式 |
| `lightrag_enable_llm_cache` | `false` | LightRAG 缓存 LLM 响应(实体抽取与查询)。缓存文件无上限且拖慢冷加载,聊天场景重复率低,默认关闭;开启后可用 `/clean_rag_cache` 清理 |
| `memory_engine` | `legacy` | `legacy` 或 `mem0` |

成本提示: 当 `knowledge_engine="lightrag"` 且 `lightrag_query_mode` 为
`hybrid` 或 `mix` 时,如果同时允许 `delegate_memory_to_livingmemory`,
LivingMemory 已加载后会叠加 LightRAG 全局/混合检索与记忆检索,可能明显增加
LLM 调用和 token 消耗。优先建议使用 `local`/`naive`,或只保留一种记忆/检索策略。

`lightrag_enable_llm_cache` 关闭时(默认),插件会在启动与各群实例创建时自动
删除残留的 `kv_store_llm_response_cache.json`(纯缓存数据,不影响图谱/向量);
也可随时用管理员命令 `/clean_rag_cache` 手动清理。若开启缓存导致该文件增长过
大引发冷加载变慢与 LLM Hook 超时,可关闭该开关后重启,或适当调大
`llm_hook_context_timeout` 作为缓解。

只有 `knowledge_engine != "legacy"` 或 `memory_engine != "legacy"` 时才创建 `V2LearningIntegration`。

`embedding_provider_id` 只显示 Embedding Provider,`rerank_provider_id` 只显示 Reranker Provider。聊天模型不会混入这两个下拉框。
Expand Down
10 changes: 10 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,16 @@ async def remember_command(self, event: AstrMessageEvent):
async for result in self._command_handlers.remember(event):
yield result

@filter.command("clean_rag_cache")
@filter.permission_type(filter.PermissionType.ADMIN)
async def clean_rag_cache_command(self, event: AstrMessageEvent):
"""清理 LightRAG LLM 响应缓存"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.clean_rag_cache(event):
yield result

@filter.command("affection_status")
@filter.permission_type(filter.PermissionType.ADMIN)
async def affection_status_command(self, event: AstrMessageEvent):
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: "4.2.1"
version: "4.2.2"
repo: "https://github.com/NickCharlie/astrbot_plugin_self_learning"
tags:
- "自学习"
Expand Down
11 changes: 10 additions & 1 deletion services/commands/command_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class CommandFilter:
"affection_status",
"set_mood",
"remember",
"clean_rag_cache",
]

def is_astrbot_command(self, event: Any) -> bool:
Expand All @@ -29,8 +30,16 @@ def is_astrbot_command(self, event: Any) -> bool:
if self.is_plugin_command(message_text):
return True

return self.is_command_text(message_text)

@staticmethod
def is_command_text(message_text: Any) -> bool:
"""判断纯文本是否为命令格式(系统级命令前缀 + 命令词)"""
if not message_text:
return False

command_prefixes = ["/", "!", "#", "."]
stripped_text = message_text.strip()
stripped_text = str(message_text).strip()
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
if stripped_text and stripped_text[0] in command_prefixes:
if len(stripped_text) > 1 and stripped_text[1].isalpha():
return True
Expand Down
52 changes: 52 additions & 0 deletions services/commands/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(
db_manager: Any,
llm_adapter: Any,
remember_service: Any = None,
v2_integration: Any = None,
):
self._config = plugin_config
self._service_factory = service_factory
Expand All @@ -34,6 +35,7 @@ def __init__(
self._db_manager = db_manager
self._llm_adapter = llm_adapter
self._remember_service = remember_service
self._v2_integration = v2_integration
self._force_learning_in_progress: set = set()

# learning_status
Expand Down Expand Up @@ -290,6 +292,56 @@ async def remember(self, event: Any) -> AsyncGenerator:
logger.error(f"remember 命令处理失败: {e}", exc_info=True)
yield event.plain_result(f"remember 失败:{str(e)}")

# clean_rag_cache

async def clean_rag_cache(self, event: Any) -> AsyncGenerator:
"""清理 LightRAG LLM 响应缓存(issue #253 维护入口)

用法:``/clean_rag_cache`` 清理全部群;``/clean_rag_cache 12345`` 或
空格/逗号分隔的多个群号只清理指定群。
"""
try:
knowledge_manager = getattr(
self._v2_integration, "_knowledge_manager", None
)
if not hasattr(knowledge_manager, "clear_llm_response_cache"):
yield event.plain_result(
"当前知识引擎不是 lightrag,无需清理 LightRAG 缓存"
)
return

payload = self._extract_command_payload(event, "clean_rag_cache")
group_ids = [
group_id
for group_id in payload.replace(",", " ").split()
if group_id
]

yield event.plain_result("正在清理 LightRAG LLM 响应缓存...")
result = await knowledge_manager.clear_llm_response_cache(
group_ids=group_ids or None
)

freed_mb = result.get("freed_bytes", 0) / 1024 / 1024
cleared = result.get("cleared", []) or []
errors = result.get("errors", []) or []
if group_ids:
lines = [
f"清理完成:指定 {len(group_ids)} 个群,"
f"涉及 {len(cleared)} 个群,释放 {freed_mb:.1f} MB"
]
else:
lines = [f"清理完成:{len(cleared)} 个群,释放 {freed_mb:.1f} MB"]
if cleared:
lines.append("涉及群组: " + ", ".join(str(g) for g in cleared))
for error in errors:
lines.append(f"失败: {error}")
yield event.plain_result("\n".join(lines))

except Exception as e:
logger.error(f"clean_rag_cache 命令处理失败: {e}", exc_info=True)
yield event.plain_result(f"清理 LightRAG 缓存失败:{str(e)}")

# affection_status

async def affection_status(self, event: Any) -> AsyncGenerator:
Expand Down
18 changes: 18 additions & 0 deletions services/hooks/llm_hook_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
from ...utils.persona_selection import get_event_persona_scope
except ImportError:
from utils.persona_selection import get_event_persona_scope
try:
from ...services.commands.command_filter import CommandFilter
except ImportError:
from services.commands.command_filter import CommandFilter

try:
from astrbot.core.agent.message import TextPart
Expand Down Expand Up @@ -80,6 +84,7 @@ def __init__(
self._db_manager = db_manager
self._feature_delegation = feature_delegation
self._shadow_mode_service = shadow_mode_service
self._command_filter = CommandFilter()
if self._shadow_mode_service is None and db_manager is not None:
try:
from ..shadow_mode import ShadowModeService
Expand All @@ -105,6 +110,19 @@ async def handle(self, event: AstrMessageEvent, req: Any) -> None:
logger.debug("[LLM Hook] 总开关未启用,跳过上下文注入")
return

# 命令/系统级唤醒词消息直接放行:命令回复追求即时响应,
# 上下文注入对命令处理没有价值,反而会占用最长 3s 的预算。
if getattr(self._config, "enable_command_pass_through", True):
message_text = (
getattr(event, "message_str", None) or event.get_message_str()
)
if self._command_filter.is_command_text(message_text):
logger.debug(
f"[LLM Hook] 命令消息直接放行,跳过上下文注入: "
f"{str(message_text)[:80]}"
)
return

if not self._diversity_manager:
logger.debug("[LLM Hook] diversity_manager未初始化,跳过多样性注入")
return
Expand Down
Loading
Loading