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

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

## [3.0.3] - 2026-05-23

### 版本

- 将插件发布版本号提升至 `3.0.3`。

## [3.0.1] - 2026-05-23

### WebUI

- 将监控板拆分为模块首页和独立 hash 页面,主页面只保留模块缩略入口。
- 保留现有 WebUI API 与轻量运行时,新增模块入口和页面结构回归测试。

## [3.0.0] - 2026-05-22

### 版本
Expand Down
6 changes: 4 additions & 2 deletions 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.0.0-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning)
[![Version](https://img.shields.io/badge/version-3.0.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/)
Expand Down Expand Up @@ -357,9 +357,11 @@ WebUI 提供以下入口:

## 推荐搭配

[LivingMemory 长期记忆插件](https://github.com/lxfight-s-Astrbot-Plugins/astrbot_plugin_livingmemory)

[群聊增强插件 Group Chat Plus](https://github.com/Him666233/astrbot_plugin_group_chat_plus)

本插件负责学习、审查、记忆和人格优化;群聊增强插件负责回复决策和读空气。组合使用时,Bot 既能积累上下文,也能更稳地决定何时回复
本插件负责学习、审查、黑话、表达方式和上下文注入;LivingMemory 负责长期记忆、召回和反思;Group Chat Plus 负责回复决策、读空气和回复生成。三者组合时,本插件会在检测到目标插件已加载后自动跳过本地重叠能力,避免重复记忆或重复回复

---

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.0.0-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.0.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/)

[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.0.0"
__version__ = "3.0.3"

# Ensure parent namespace packages ("data", "data.plugins") are
# durably registered in sys.modules. AstrBot loads plugins via
Expand Down
49 changes: 47 additions & 2 deletions _conf_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,49 @@
}
}
},
"Integration_Settings": {
"description": "功能融合",
"type": "object",
"hint": "与专门插件协同:记忆交给 LivingMemory,回复决策和生成交给 Group Chat Plus。本插件保留学习、审查、黑话、表达方式和上下文注入能力。",
"items": {
"delegate_memory_to_livingmemory": {
"description": "记忆委托给 LivingMemory",
"type": "bool",
"hint": "开启后,检测到 LivingMemory 插件已加载时,本插件不再写入或注入本地长期记忆,避免双重记忆系统互相污染。",
"default": true
},
"livingmemory_plugin_name": {
"description": "LivingMemory 插件名",
"type": "string",
"hint": "用于检测 LivingMemory 插件是否已加载。通常保持默认值即可。",
"default": "LivingMemory"
},
"disable_local_memory_when_delegated": {
"description": "委托时禁用本地记忆",
"type": "bool",
"hint": "开启后,只有检测到 LivingMemory 时才禁用本插件本地长期记忆;LivingMemory 未加载时自动保留本地降级能力。",
"default": true
},
"delegate_reply_to_group_chat_plus": {
"description": "回复交给 Group Chat Plus",
"type": "bool",
"hint": "开启后,检测到 Group Chat Plus 插件已加载时,本插件不再接管回复决策和回复生成,只负责学习上下文增强。",
"default": true
},
"group_chat_plus_plugin_name": {
"description": "Group Chat Plus 插件名",
"type": "string",
"hint": "用于检测 Group Chat Plus 插件是否已加载。通常保持默认值即可。",
"default": "astrbot_plugin_group_chat_plus"
},
"disable_local_reply_when_delegated": {
"description": "委托时禁用本地回复",
"type": "bool",
"hint": "开启后,只有检测到 Group Chat Plus 时才禁用本插件本地回复器;目标插件未加载时保留兼容降级。",
"default": true
}
}
},
"V2_Architecture_Settings": {
"description": "v2架构升级配置",
"type": "object",
Expand All @@ -668,13 +711,15 @@
"description": "Embedding 提供商 ID",
"type": "string",
"hint": "填写Embedding提供商的完整ID(如 'openai/text-embedding-3-large')。需要先在AstrBot的Provider管理中创建Embedding类型的提供商,然后将其ID填写到此处。格式通常为 '来源名/模型名'",
"default": ""
"default": "",
"_provider_type": "embedding"
},
"rerank_provider_id": {
"description": "Reranker 提供商 ID",
"type": "string",
"hint": "填写Reranker提供商的完整ID(如 'openai/qwen3-rerank')。需要先在AstrBot的Provider管理中创建Reranker类型的提供商,然后将其ID填写到此处。格式通常为 '来源名/模型名'",
"default": ""
"default": "",
"_provider_type": "rerank"
},
"rerank_top_k": {
"description": "重排序保留结果数",
Expand Down
17 changes: 17 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ class PluginConfig(BaseModel):
# v2 Architecture: Memory engine
memory_engine: str = "legacy" # "mem0" | "legacy"

# 功能融合:将重叠能力委托给专门插件
delegate_memory_to_livingmemory: bool = True # 将长期记忆交给 LivingMemory
livingmemory_plugin_name: str = "LivingMemory" # LivingMemory 插件名
disable_local_memory_when_delegated: bool = True # 检测到 LivingMemory 时禁用本地长期记忆写入/注入
delegate_reply_to_group_chat_plus: bool = True # 将回复决策和生成交给 Group Chat Plus
group_chat_plus_plugin_name: str = "astrbot_plugin_group_chat_plus" # Group Chat Plus 插件名
disable_local_reply_when_delegated: bool = True # 检测到 Group Chat Plus 时禁用本地回复器

# 当前人格设置
current_persona_name: str = "default"

Expand Down Expand Up @@ -302,6 +310,7 @@ def create_from_config(cls, config: dict, data_dir: Optional[str] = None) -> 'Pl
repository_settings = config.get('Repository_Settings', {}) # 新增:Repository配置
goal_driven_chat_settings = config.get('Goal_Driven_Chat_Settings', {}) # 新增:目标驱动对话设置
v2_settings = config.get('V2_Architecture_Settings', {}) # v2架构升级设置
integration_settings = config.get('Integration_Settings', {}) # 功能融合设置

# 添加调试日志:显示目标驱动对话配置数据
logger.info(f" [配置加载] Goal_Driven_Chat_Settings原始数据: {goal_driven_chat_settings}")
Expand Down Expand Up @@ -335,6 +344,14 @@ def create_from_config(cls, config: dict, data_dir: Optional[str] = None) -> 'Pl
lightrag_query_mode=v2_settings.get('lightrag_query_mode', 'local'),
memory_engine=v2_settings.get('memory_engine', 'legacy'),

# 功能融合设置
delegate_memory_to_livingmemory=integration_settings.get('delegate_memory_to_livingmemory', True),
livingmemory_plugin_name=integration_settings.get('livingmemory_plugin_name', 'LivingMemory'),
disable_local_memory_when_delegated=integration_settings.get('disable_local_memory_when_delegated', True),
delegate_reply_to_group_chat_plus=integration_settings.get('delegate_reply_to_group_chat_plus', True),
group_chat_plus_plugin_name=integration_settings.get('group_chat_plus_plugin_name', 'astrbot_plugin_group_chat_plus'),
disable_local_reply_when_delegated=integration_settings.get('disable_local_reply_when_delegated', True),

learning_interval_hours=learning_params.get('learning_interval_hours', 6),
min_messages_for_learning=learning_params.get('min_messages_for_learning', 50),
max_messages_per_batch=learning_params.get('max_messages_per_batch', 200),
Expand Down
10 changes: 8 additions & 2 deletions core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,10 @@ def get_service_registry(self) -> ServiceRegistry:
"""获取服务注册表"""
return self._registry

async def initialize_all_services(self) -> bool:
async def initialize_all_services(
self,
skip_intelligent_responder: bool = False,
) -> bool:
"""初始化所有服务"""
self._logger.info("开始初始化所有服务")

Expand All @@ -519,7 +522,10 @@ async def initialize_all_services(self) -> bool:

# 社交上下文注入器由 ComponentFactory 创建(plugin_lifecycle.py)

self.create_intelligent_responder() # 重新启用智能回复器
if skip_intelligent_responder:
self._logger.info("已跳过本地智能回复器初始化,回复由外部插件接管")
else:
self.create_intelligent_responder()
self.create_persona_manager()
self.create_multidimensional_analyzer()
self.create_progressive_learning()
Expand Down
159 changes: 159 additions & 0 deletions core/feature_delegation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Runtime feature delegation between self-learning and companion plugins."""

from __future__ import annotations

from typing import Any, Iterable, Optional

from astrbot.api import logger


class FeatureDelegation:
"""Detect external companion plugins and decide which local features to skip.

The self-learning plugin remains responsible for learning, review, jargon,
style examples and prompt enrichment. Long-term memory and reply ownership can
be delegated to specialized plugins when they are loaded in AstrBot.
"""

LIVING_MEMORY_ALIASES = ("LivingMemory", "astrbot_plugin_livingmemory")
GROUP_CHAT_PLUS_ALIASES = (
"astrbot_plugin_group_chat_plus",
"Group Chat Plus",
"ChatPlus",
)

def __init__(self, config: Any, context: Any) -> None:
self._config = config
self._context = context
self._last_status: tuple[bool, bool] | None = None

def memory_plugin(self) -> Optional[Any]:
aliases = (
getattr(self._config, "livingmemory_plugin_name", None),
*self.LIVING_MEMORY_ALIASES,
)
return self._find_active_star(aliases)

def reply_plugin(self) -> Optional[Any]:
aliases = (
getattr(self._config, "group_chat_plus_plugin_name", None),
*self.GROUP_CHAT_PLUS_ALIASES,
)
return self._find_active_star(aliases)

def should_delegate_memory(self) -> bool:
if not getattr(self._config, "delegate_memory_to_livingmemory", True):
return False
if not getattr(self._config, "disable_local_memory_when_delegated", True):
return False
return self.memory_plugin() is not None

def should_delegate_reply(self) -> bool:
if not getattr(self._config, "delegate_reply_to_group_chat_plus", True):
return False
if not getattr(self._config, "disable_local_reply_when_delegated", True):
return False
return self.reply_plugin() is not None

def status(self) -> dict[str, Any]:
memory_plugin = self.memory_plugin()
reply_plugin = self.reply_plugin()
return {
"memory_delegated": self.should_delegate_memory(),
"memory_plugin": self._star_label(memory_plugin),
"reply_delegated": self.should_delegate_reply(),
"reply_plugin": self._star_label(reply_plugin),
}

def log_status(self) -> None:
status = self.status()
current = (status["memory_delegated"], status["reply_delegated"])
if current == self._last_status:
return
self._last_status = current

if status["memory_delegated"]:
logger.info(
"[功能融合] 记忆能力已委托给 "
f"{status['memory_plugin']},本插件跳过本地长期记忆写入/注入"
)
else:
logger.info("[功能融合] 未检测到可用 LivingMemory,保留本插件本地记忆能力")

if status["reply_delegated"]:
logger.info(
"[功能融合] 回复决策和回复生成已委托给 "
f"{status['reply_plugin']},本插件仅注入学习上下文"
)
else:
logger.info("[功能融合] 未检测到可用 Group Chat Plus,保留本插件本地回复兼容能力")

def _find_active_star(self, aliases: Iterable[Any]) -> Optional[Any]:
raw_aliases = [
str(alias).strip()
for alias in aliases
if str(alias or "").strip()
]
wanted = {alias.lower() for alias in raw_aliases}
if not wanted or not self._context:
return None

getter = getattr(self._context, "get_registered_star", None)
if callable(getter):
for alias in raw_aliases:
try:
star = getter(alias)
except Exception:
star = None
if self._is_active_star(star):
return star

all_stars_getter = getattr(self._context, "get_all_stars", None)
if not callable(all_stars_getter):
return None

try:
stars = all_stars_getter() or []
except Exception:
return None

for star in stars:
if not self._is_active_star(star):
continue
candidates = {
getattr(star, "name", None),
getattr(star, "display_name", None),
getattr(star, "root_dir_name", None),
getattr(star, "module_path", None),
}
module_path = getattr(star, "module_path", None)
if isinstance(module_path, str):
parts = [part for part in module_path.split(".") if part]
candidates.update(parts)
normalized = {
str(candidate).strip().lower()
for candidate in candidates
if str(candidate or "").strip()
}
if normalized & wanted:
return star
return None

@staticmethod
def _is_active_star(star: Any) -> bool:
if not star:
return False
if getattr(star, "activated", True) is False:
return False
return getattr(star, "star_cls", None) is not None

@staticmethod
def _star_label(star: Any) -> Optional[str]:
if not star:
return None
return (
getattr(star, "display_name", None)
or getattr(star, "name", None)
or getattr(star, "root_dir_name", None)
or getattr(star, "module_path", None)
)
Loading
Loading