refactor: 将 A_Memorix 重构为主线长期记忆子系统并重建管理界面

- 将 A_Memorix 从旧 submodule / 插件形态迁入主线源码,主体落到 src/A_memorix
- 调整主程序接入方式,使 A_Memorix 作为源码内长期记忆子系统运行
- 回收父项目插件体系中针对 A_Memorix 的特判,减少对 plugin 通用层的侵入
- 将长期记忆配置、运行时、自检、导入、调优等能力收口到 memory 路由与主线服务层
- 重做长期记忆控制台与图谱页面,按 MaiBot 现有 dashboard 风格接入
- 补充实体关系图与证据视图双视图能力,支持查看节点、关系、段落及其证据链路
- 新增长期记忆配置编辑器与 memory-api,支持主线内配置管理
- 补齐删除管理能力:删除预览、混合删除、来源批量删除、删除操作恢复
- 优化删除预览与删除操作详情的前端展示,支持分页、检索,并以实体名/关系内容/段落摘要替代单纯 hash 展示
- 修复图谱与控制台相关前端问题,包括证据视图切换、查询触发时机、删除弹层空值保护等
- 新增或更新 A_Memorix 相关测试、WebUI 路由测试、前端 vitest 测试与辅助验证脚本
- 移除旧 plugins/A_memorix、.gitmodules 及相关历史维护文档
This commit is contained in:
A-Dawn
2026-04-03 08:08:24 +08:00
parent bf5eb45709
commit 15d436b3a1
136 changed files with 52533 additions and 629 deletions

View File

@@ -13,6 +13,7 @@ from .support import (
coerce_types,
find_plugin_instance,
find_plugin_path_by_id,
get_plugin_config_path,
normalize_dotted_keys,
require_plugin_token,
resolve_plugin_file_path,
@@ -23,6 +24,20 @@ logger = get_logger("webui.plugin_routes")
router = APIRouter()
def _to_builtin_data(obj: Any) -> Any:
if hasattr(obj, "unwrap"):
try:
obj = obj.unwrap()
except Exception:
pass
if isinstance(obj, dict):
return {str(key): _to_builtin_data(value) for key, value in obj.items()}
if isinstance(obj, list):
return [_to_builtin_data(value) for value in obj]
return obj
def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> Dict[str, Any]:
schema: Dict[str, Any] = {
"plugin_id": plugin_id,
@@ -142,7 +157,7 @@ async def get_plugin_config_schema(plugin_id: str, maibot_session: Optional[str]
logger.warning(f"读取 config_schema.json 失败,回退到自动推断: {e}")
current_config: Any = {}
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
if config_path.exists():
with open(config_path, "r", encoding="utf-8") as file_obj:
current_config = tomlkit.load(file_obj)
@@ -165,7 +180,7 @@ async def get_plugin_config_raw(plugin_id: str, maibot_session: Optional[str] =
if plugin_path is None:
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
if not config_path.exists():
return {"success": True, "config": "", "message": "配置文件不存在"}
@@ -192,7 +207,7 @@ async def update_plugin_config_raw(
if plugin_path is None:
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
try:
tomlkit.loads(request.config)
except Exception as e:
@@ -202,6 +217,7 @@ async def update_plugin_config_raw(
if backup_path is not None:
logger.info(f"已备份配置文件: {backup_path}")
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as file_obj:
file_obj.write(request.config)
@@ -224,13 +240,13 @@ async def get_plugin_config(plugin_id: str, maibot_session: Optional[str] = Cook
if plugin_path is None:
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
if not config_path.exists():
return {"success": True, "config": {}, "message": "配置文件不存在"}
with open(config_path, "r", encoding="utf-8") as file_obj:
config = tomlkit.load(file_obj)
return {"success": True, "config": dict(config)}
return {"success": True, "config": _to_builtin_data(config)}
except HTTPException:
raise
except Exception as e:
@@ -259,11 +275,12 @@ async def update_plugin_config(
if plugin_path is None:
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
backup_path = backup_file(config_path, "backup")
if backup_path is not None:
logger.info(f"已备份配置文件: {backup_path}")
config_path.parent.mkdir(parents=True, exist_ok=True)
save_toml_with_format(config_data, str(config_path))
logger.info(f"已更新插件配置: {plugin_id}")
return {"success": True, "message": "配置已保存", "note": "配置更改将自动热更新到对应插件"}
@@ -284,7 +301,7 @@ async def reset_plugin_config(plugin_id: str, maibot_session: Optional[str] = Co
if plugin_path is None:
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
if not config_path.exists():
return {"success": True, "message": "配置文件不存在,无需重置"}
@@ -308,7 +325,7 @@ async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(N
if plugin_path is None:
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
config_path = get_plugin_config_path(plugin_id, plugin_path)
config = tomlkit.document()
if config_path.exists():
with open(config_path, "r", encoding="utf-8") as file_obj:
@@ -321,6 +338,7 @@ async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(N
current_enabled = bool(plugin_config.get("enabled", True))
new_enabled = not current_enabled
plugin_config["enabled"] = new_enabled
config_path.parent.mkdir(parents=True, exist_ok=True)
save_toml_with_format(config, str(config_path))
status = "启用" if new_enabled else "禁用"

View File

@@ -195,6 +195,9 @@ def get_plugins_dir() -> Path:
plugins_dir.mkdir(exist_ok=True)
return plugins_dir
def get_plugin_config_path(plugin_id: str, plugin_path: Path) -> Path:
return resolve_plugin_file_path(plugin_path, "config.toml")
def get_plugin_candidate_paths(plugin_id: str) -> Tuple[Path, Path]:
plugins_dir = get_plugins_dir()