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
6 changes: 3 additions & 3 deletions _conf_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -669,12 +669,12 @@
"Integration_Settings": {
"description": "功能融合",
"type": "object",
"hint": "与专门插件协同:记忆交给 LivingMemory,回复决策和生成交给 Group Chat Plus。本插件保留学习、审查、黑话、表达方式和上下文注入能力。",
"hint": "与专门插件协同:记忆交给 LivingMemory,回复决策和生成交给 Group Chat Plus。本插件保留学习、审查、黑话、表达方式和上下文注入能力。若同时使用 LightRAG hybrid/mix 查询与 LivingMemory 委托,可能显著增加 LLM 调用与 token 消耗。",
"items": {
"delegate_memory_to_livingmemory": {
"description": "记忆委托给 LivingMemory",
"type": "bool",
"hint": "开启后,检测到 LivingMemory 插件已加载时,本插件不再写入或注入本地长期记忆,避免双重记忆系统互相污染。",
"hint": "开启后,检测到 LivingMemory 插件已加载时,本插件不再写入或注入本地长期记忆,避免双重记忆系统互相污染。若 V2 知识引擎使用 LightRAG 且查询模式为 hybrid/mix,建议评估调用量与 token 成本。",
"default": true
},
"livingmemory_plugin_name": {
Expand Down Expand Up @@ -755,7 +755,7 @@
"lightrag_query_mode": {
"description": "LightRAG 查询模式",
"type": "string",
"hint": "LightRAG检索模式。local=仅实体邻域检索(低延迟),hybrid=实体邻域+全局社区聚合(高质量但慢约4-5秒),naive=纯向量检索,global=仅全局社区,mix=混合模式",
"hint": "LightRAG检索模式。local=仅实体邻域检索(低延迟),hybrid=实体邻域+全局社区聚合(高质量但慢约4-5秒),naive=纯向量检索,global=仅全局社区,mix=混合模式。若同时委托 LivingMemory,hybrid/mix 会叠加记忆检索与融合上下文,可能显著增加 LLM 调用与 token 消耗,优先建议 local/naive。",
"default": "local"
},
"memory_engine": {
Expand Down
55 changes: 55 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
DEFAULT_DB_TYPE = "postgresql"
SUPPORTED_DB_TYPES = {"sqlite", "mysql", "postgresql"}
POSTGRESQL_DB_TYPE_ALIASES = {"postgres", "pg", "pgsql"}
HIGH_COST_LIGHTRAG_QUERY_MODES = {"hybrid", "mix"}
LIGHTRAG_LIVINGMEMORY_COST_WARNING = (
"当前配置选择 LightRAG 的 hybrid/mix 查询,并允许记忆委托给 LivingMemory;"
"当 LivingMemory 插件已加载时,会叠加 LightRAG 全局/混合检索与 LivingMemory 记忆检索,"
"可能显著增加 LLM 调用与 token 消耗。建议优先改为 local/naive,或只保留一种记忆/检索策略。"
)


def normalize_db_type(db_type: Any) -> Optional[str]:
Expand All @@ -31,6 +37,53 @@ def normalize_db_type(db_type: Any) -> Optional[str]:
return value


def _read_config_value(config_like: Any, key: str, default: Any = None) -> Any:
if isinstance(config_like, dict):
if key in config_like:
return config_like.get(key, default)
for group_key in ("V2_Architecture_Settings", "Integration_Settings"):
group = config_like.get(group_key)
if isinstance(group, dict) and key in group:
return group.get(key, default)
return default
return getattr(config_like, key, default)


def _read_config_bool(config_like: Any, key: str, default: bool = False) -> bool:
value = _read_config_value(config_like, key, default)
if isinstance(value, bool):
return value
if value is None:
return default
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)


def is_lightrag_livingmemory_high_cost_config(config_like: Any) -> bool:
"""Return True for the known high-cost LightRAG + LivingMemory combination."""
knowledge_engine = str(
_read_config_value(config_like, "knowledge_engine", "legacy") or "legacy"
).strip().lower()
query_mode = str(
_read_config_value(config_like, "lightrag_query_mode", "local") or "local"
).strip().lower()
delegate_memory = _read_config_bool(config_like, "delegate_memory_to_livingmemory")

return (
knowledge_engine == "lightrag"
and query_mode in HIGH_COST_LIGHTRAG_QUERY_MODES
and delegate_memory
)


def get_config_cost_warnings(config_like: Any) -> List[str]:
"""Return non-blocking warnings for expensive cross-feature config combinations."""
if is_lightrag_livingmemory_high_cost_config(config_like):
return [LIGHTRAG_LIVINGMEMORY_COST_WARNING]
return []


def normalize_identifier_list(value: Any, *, full_learning_markers: bool = False) -> List[str]:
"""Normalize user/group identifier lists from AstrBot settings."""
if value is None:
Expand Down Expand Up @@ -660,6 +713,8 @@ def validate_config(self) -> List[str]:
errors.append("PostgreSQL schema 不能为空")

# 提示性警告而非错误
errors.extend(f" {warning}" for warning in get_config_cost_warnings(self))

provider_warnings = []
if not self.filter_provider_id:
provider_warnings.append("未配置筛选模型提供商ID,将尝试自动配置或使用备选模型")
Expand Down
5 changes: 5 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ PostgreSQL 支持 `postgresql_schema`,非 `public` 时会自动创建 schema
| `lightrag_query_mode` | `local` | LightRAG 查询模式 |
| `memory_engine` | `legacy` | `legacy` 或 `mem0` |

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

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

`embedding_provider_id` 只显示 Embedding Provider,`rerank_provider_id` 只显示 Reranker Provider。聊天模型不会混入这两个下拉框。
Expand Down
6 changes: 6 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ Self Learning 负责学习、审查、黑话、表达方式和 LLM 上下文注

委托只在目标插件已加载、激活且存在 `star_cls` 时生效。未检测到目标插件时,本插件自动回退到本地能力。

成本提示: 如果 V2 架构同时选择 `knowledge_engine="lightrag"`、
`lightrag_query_mode="hybrid"`/`"mix"`,并开启 LivingMemory 记忆委托,
一次上下文增强可能同时触发 LightRAG 全局/混合检索与 LivingMemory 记忆检索。
设置页和功能融合页会显示非阻塞提醒。需要控制成本时,优先将 LightRAG 查询模式改为
`local`/`naive`,或只保留一种记忆/检索策略。

## 运行路径

### 记忆委托
Expand Down
16 changes: 16 additions & 0 deletions pages/dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,20 @@
}, 3200);
}

function warningListHtml(warnings) {
const items = Array.isArray(warnings)
? warnings
.map((message) => String(message).trim())
.filter(Boolean)
: [];
return items.map((message) => `
<div class="settings-warning">
<strong>成本提醒</strong>
<span>${escapeHtml(message)}</span>
</div>
`).join("");
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

function showErrors(errors) {
const panel = $("error-panel");
if (!panel) return;
Expand Down Expand Up @@ -947,6 +961,7 @@

function renderIntegrations(data) {
setHtml("integration-cards", (data.dashboards || []).map(integrationCardHtml).join("") || empty("暂无融合状态"));
setHtml("integration-warnings", warningListHtml(data.warnings || []));
const settings = data.settings || {};
setHtml("integration-settings", Object.entries(settings).map(([key, value]) => `
<div class="table-row">
Expand Down Expand Up @@ -1072,6 +1087,7 @@

function renderSettings(data) {
const schema = data.schema || {};
setHtml("settings-warnings", warningListHtml(schema.warnings || data.warnings || []));
const groups = schema.groups || [];
if (!state.settingsGroup && groups.length) state.settingsGroup = groups[0].key;
setHtml("settings-groups", groups.map((group) => `
Expand Down
2 changes: 2 additions & 0 deletions pages/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ <h3>MaiBot 学习数据迁移</h3>
</div>
<div class="panel">
<h3>融合设置</h3>
<div class="settings-warning-list" id="integration-warnings"></div>
<div class="compact-table" id="integration-settings"></div>
</div>
</section>
Expand All @@ -438,6 +439,7 @@ <h3>设置</h3>
<button class="ghost-button" type="button" data-refresh-page="settings">刷新</button>
</div>
</div>
<div class="settings-warning-list" id="settings-warnings"></div>
<div class="settings-grid">
<div class="panel settings-sidebar" id="settings-groups"></div>
<div class="panel">
Expand Down
35 changes: 35 additions & 0 deletions pages/dashboard/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,41 @@ button.is-busy {
margin-bottom: 14px;
}

.settings-warning-list {
display: grid;
gap: 8px;
margin: 0 0 14px;
}

.settings-warning {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 10px;
align-items: start;
padding: 10px 12px;
border: 1px solid rgba(217, 119, 6, 0.28);
border-radius: 8px;
color: #92400e;
background: rgba(217, 119, 6, 0.10);
}

.settings-warning strong {
white-space: nowrap;
font-size: 12px;
}

.settings-warning span {
min-width: 0;
overflow-wrap: anywhere;
font-size: 12px;
line-height: 1.55;
}

html[data-theme="dark"] .settings-warning {
color: #fbbf24;
background: rgba(217, 119, 6, 0.16);
}

.panel {
padding: 16px;
}
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_config_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ async def test_config_schema_route_returns_groups(client):
assert response.status_code == 200
data = await response.get_json()
assert "groups" in data
assert "warnings" in data
assert any(group["key"] == "Database_Settings" for group in data["groups"])
assert any(
field["key"] == "relevance_threshold"
Expand Down
16 changes: 16 additions & 0 deletions tests/integration/test_webui_static_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,22 @@ def test_dashboard_exposes_companion_plugin_api_hub():
assert "GET /api/data/overview" in service


def test_dashboard_exposes_config_cost_warnings():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")
index = (PLUGIN_ROOT / "pages" / "dashboard" / "index.html").read_text(encoding="utf-8")
app = (PLUGIN_ROOT / "pages" / "dashboard" / "app.js").read_text(encoding="utf-8")
service = (PLUGIN_ROOT / "webui" / "services" / "config_service.py").read_text(encoding="utf-8")

assert "configWarnings" in text
assert "settings-warning-list" in text
assert "成本提醒" in text
assert "settings-warnings" in index
assert "integration-warnings" in index
assert "warningListHtml" in app
assert "schema.warnings" in app
Comment on lines +276 to +288

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider asserting the presence of the new warning containers (settings-warnings / integration-warnings) to make the dashboard wiring more robust.

Adding assertions for these DOM containers in dashboard.html would ensure the warning slots themselves remain intact, not just the JS symbols. This helps detect template changes that remove or rename the containers while leaving the JS references in place.

assert "get_config_cost_warnings" in service


def test_dashboard_exposes_tiered_dependency_install_controls():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

Expand Down
55 changes: 54 additions & 1 deletion tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
import pytest
from unittest.mock import patch, MagicMock

from config import DEFAULT_DATA_DIR, DEFAULT_DB_TYPE, PluginConfig, normalize_db_type
from config import (
DEFAULT_DATA_DIR,
DEFAULT_DB_TYPE,
PluginConfig,
get_config_cost_warnings,
is_lightrag_livingmemory_high_cost_config,
normalize_db_type,
)


@pytest.mark.unit
Expand Down Expand Up @@ -461,6 +468,52 @@ def test_partial_providers_configured(self):
blocking_errors = [e for e in errors if not e.startswith(" ")]
assert len(blocking_errors) == 0

def test_lightrag_hybrid_livingmemory_combo_warns_without_blocking(self):
"""LightRAG hybrid plus LivingMemory delegation should surface cost warnings."""
config = PluginConfig(
filter_provider_id="provider_1",
knowledge_engine="lightrag",
lightrag_query_mode="hybrid",
delegate_memory_to_livingmemory=True,
)

warnings = get_config_cost_warnings(config)
errors = config.validate_config()
blocking_errors = [e for e in errors if not e.startswith(" ")]

assert is_lightrag_livingmemory_high_cost_config(config) is True
assert warnings
assert "LivingMemory" in warnings[0]
assert "token" in warnings[0]
assert any("LivingMemory" in error and error.startswith(" ") for error in errors)
assert blocking_errors == []

def test_lightrag_local_livingmemory_combo_does_not_warn(self):
"""Low-latency LightRAG local mode should not raise the high-cost warning."""
config = PluginConfig(
knowledge_engine="lightrag",
lightrag_query_mode="local",
delegate_memory_to_livingmemory=True,
)

assert is_lightrag_livingmemory_high_cost_config(config) is False
assert get_config_cost_warnings(config) == []

def test_lightrag_hybrid_string_false_delegation_does_not_warn(self):
"""Raw grouped config values should parse string false as disabled."""
raw_config = {
"V2_Architecture_Settings": {
"knowledge_engine": "lightrag",
"lightrag_query_mode": "hybrid",
},
"Integration_Settings": {
"delegate_memory_to_livingmemory": "false",
},
}

assert is_lightrag_livingmemory_high_cost_config(raw_config) is False
assert get_config_cost_warnings(raw_config) == []

@pytest.mark.parametrize(
"raw_db_type",
["postgres", "pg", "pgsql", "postgresql", "", None],
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/test_config_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,24 @@ async def test_get_config_schema_includes_full_settings(self, tmp_path):
assert schema["provider_options_by_type"]["embedding"][0]["value"] == "embed-a"
assert schema["provider_options_by_type"]["rerank"][0]["value"] == "rerank-a"

@pytest.mark.asyncio
async def test_config_schema_exposes_lightrag_livingmemory_cost_warning(self, tmp_path):
container = build_container(tmp_path)
container.plugin_config.knowledge_engine = "lightrag"
container.plugin_config.lightrag_query_mode = "hybrid"
container.plugin_config.delegate_memory_to_livingmemory = True

schema = await ConfigService(container).get_config_schema()
groups = {group["key"]: group for group in schema["groups"]}
v2_fields = {field["key"]: field for field in groups["V2_Architecture_Settings"]["fields"]}
integration_fields = {field["key"]: field for field in groups["Integration_Settings"]["fields"]}

assert schema["warnings"]
assert "LivingMemory" in schema["warnings"][0]
assert "token" in schema["warnings"][0]
assert "LivingMemory" in v2_fields["lightrag_query_mode"]["hint"]
assert "LightRAG" in integration_fields["delegate_memory_to_livingmemory"]["hint"]

@pytest.mark.asyncio
async def test_provider_schema_uses_astrbot_provider_config_classification(self, tmp_path):
plugin_config = PluginConfig.create_default()
Expand Down Expand Up @@ -491,6 +509,29 @@ async def test_update_config_persists_and_syncs_paths(self, tmp_path):
assert saved["relevance_threshold"] == 0.75
assert saved["log_level"] == "debug"

@pytest.mark.asyncio
async def test_update_config_returns_cost_warning_for_high_cost_combo(self, tmp_path):
container = build_container(tmp_path)
service = ConfigService(container)

success, message, updated = await service.update_config(
{
"V2_Architecture_Settings": {
"knowledge_engine": "lightrag",
"lightrag_query_mode": "hybrid",
},
"Integration_Settings": {
"delegate_memory_to_livingmemory": True,
},
}
)

assert success is True
assert updated["knowledge_engine"] == "lightrag"
assert updated["lightrag_query_mode"] == "hybrid"
assert "LivingMemory" in message
assert "token" in message

@pytest.mark.asyncio
async def test_update_config_syncs_webui_changes_to_plugin_page_config_and_runtime(self, tmp_path):
container = build_container(tmp_path)
Expand Down
Loading
Loading