refactor: 整合知识导图接口
将思维导图生成、查询和文件列表接口收敛到知识库 API,并移除独立 mindmap router 与前端模块。
This commit is contained in:
parent
b08ddc118f
commit
7b1b326523
@ -4,6 +4,11 @@ import json
|
||||
import textwrap
|
||||
from typing import Any
|
||||
|
||||
from yuxi import config, knowledge_base
|
||||
from yuxi.models import select_model
|
||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||
from yuxi.utils import logger
|
||||
|
||||
MINDMAP_SYSTEM_PROMPT = """你是一个专业的知识整理助手。
|
||||
|
||||
你的任务是分析用户提供的文件列表,生成一个层次分明的思维导图结构。
|
||||
@ -58,6 +63,18 @@ MINDMAP_SYSTEM_PROMPT = """你是一个专业的知识整理助手。
|
||||
"""
|
||||
|
||||
|
||||
class MindmapNotFoundError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class MindmapValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class MindmapGenerationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def build_database_file_list(files: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
@ -114,3 +131,115 @@ def parse_mindmap_content(content: str) -> dict[str, Any]:
|
||||
if not isinstance(mindmap_data, dict) or "content" not in mindmap_data:
|
||||
raise ValueError("思维导图结构不正确")
|
||||
return mindmap_data
|
||||
|
||||
|
||||
async def get_mindmap_database_files(kb_id: str) -> dict[str, Any]:
|
||||
db_info = await knowledge_base.get_database_info(kb_id)
|
||||
if not db_info:
|
||||
raise MindmapNotFoundError(f"知识库 {kb_id} 不存在")
|
||||
|
||||
file_list = build_database_file_list(db_info.get("files", {}))
|
||||
return {
|
||||
"message": "success",
|
||||
"kb_id": kb_id,
|
||||
"slug": kb_id,
|
||||
"db_name": db_info.get("name", ""),
|
||||
"files": file_list,
|
||||
"total": len(file_list),
|
||||
}
|
||||
|
||||
|
||||
async def generate_database_mindmap(
|
||||
kb_id: str, file_ids: list[str] | None = None, user_prompt: str = ""
|
||||
) -> dict[str, Any]:
|
||||
db_info = await knowledge_base.get_database_info(kb_id)
|
||||
if not db_info:
|
||||
raise MindmapNotFoundError(f"知识库 {kb_id} 不存在")
|
||||
|
||||
db_name = db_info.get("name", "知识库")
|
||||
all_files = db_info.get("files", {})
|
||||
selected_file_ids = list(file_ids or all_files.keys())
|
||||
if not selected_file_ids:
|
||||
raise MindmapValidationError("知识库中没有文件")
|
||||
|
||||
original_count = len(selected_file_ids)
|
||||
if len(selected_file_ids) > 20:
|
||||
selected_file_ids = selected_file_ids[:20]
|
||||
logger.info(f"文件数量超过限制,已从{original_count}个文件中选择前20个文件生成思维导图")
|
||||
|
||||
files_info = collect_mindmap_files(all_files, selected_file_ids)
|
||||
if not files_info:
|
||||
raise MindmapValidationError("选择的文件不存在")
|
||||
|
||||
logger.info(f"开始生成思维导图,知识库: {db_name}, 文件数量: {len(files_info)}")
|
||||
|
||||
model = select_model(model_spec=config.default_model)
|
||||
messages = [
|
||||
{"role": "system", "content": MINDMAP_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": build_mindmap_user_message(db_name, files_info, user_prompt)},
|
||||
]
|
||||
response = await model.call(messages, stream=False)
|
||||
content = response.content if hasattr(response, "content") else str(response)
|
||||
|
||||
try:
|
||||
mindmap_data = parse_mindmap_content(content)
|
||||
except ValueError as e:
|
||||
logger.error(f"AI返回的JSON解析失败: {e}, 原始内容: {content}")
|
||||
raise MindmapGenerationError(f"AI返回格式错误: {str(e)}") from e
|
||||
|
||||
logger.info("思维导图生成成功")
|
||||
|
||||
try:
|
||||
await KnowledgeBaseRepository().update(kb_id, {"mindmap": mindmap_data})
|
||||
logger.info(f"思维导图已保存到知识库: {kb_id}")
|
||||
except Exception as save_error:
|
||||
logger.error(f"保存思维导图失败: {save_error}")
|
||||
|
||||
return {
|
||||
"message": "success",
|
||||
"mindmap": mindmap_data,
|
||||
"kb_id": kb_id,
|
||||
"slug": kb_id,
|
||||
"db_name": db_name,
|
||||
"file_count": len(files_info),
|
||||
"original_file_count": original_count,
|
||||
"truncated": len(files_info) < original_count,
|
||||
}
|
||||
|
||||
|
||||
async def get_mindmap_databases_overview(uid: str) -> dict[str, Any]:
|
||||
databases = await knowledge_base.get_databases_by_uid(uid)
|
||||
db_list = []
|
||||
for db_info in databases.get("databases", []):
|
||||
kb_id = db_info.get("kb_id") or db_info.get("slug")
|
||||
if not kb_id:
|
||||
continue
|
||||
|
||||
detail_info = await knowledge_base.get_database_info(kb_id)
|
||||
file_count = len(detail_info.get("files", {})) if detail_info else 0
|
||||
db_list.append(
|
||||
{
|
||||
"kb_id": kb_id,
|
||||
"slug": kb_id,
|
||||
"name": db_info.get("name", ""),
|
||||
"description": db_info.get("description", ""),
|
||||
"kb_type": db_info.get("kb_type", ""),
|
||||
"file_count": file_count,
|
||||
}
|
||||
)
|
||||
|
||||
return {"message": "success", "databases": db_list, "total": len(db_list)}
|
||||
|
||||
|
||||
async def get_database_mindmap_data(kb_id: str) -> dict[str, Any]:
|
||||
kb = await KnowledgeBaseRepository().get_by_kb_id(kb_id)
|
||||
if kb is None:
|
||||
raise MindmapNotFoundError(f"知识库 {kb_id} 不存在")
|
||||
|
||||
return {
|
||||
"message": "success",
|
||||
"mindmap": kb.mindmap,
|
||||
"kb_id": kb_id,
|
||||
"slug": kb_id,
|
||||
"db_name": kb.name,
|
||||
}
|
||||
|
||||
@ -47,10 +47,8 @@ if not _LITE_MODE:
|
||||
from server.routers.graph_router import graph
|
||||
from server.routers.knowledge_router import knowledge
|
||||
from server.routers.knowledge_eval_router import evaluation
|
||||
from server.routers.knowledge_mindmap_router import mindmap
|
||||
|
||||
# 知识库与图谱能力依赖较重,LITE 模式下跳过这组接口。
|
||||
router.include_router(knowledge) # /api/knowledge/* 知识库管理与检索
|
||||
router.include_router(evaluation) # /api/evaluation/* 知识库评估
|
||||
router.include_router(mindmap) # /api/mindmap/* 思维导图生成与查询
|
||||
router.include_router(graph) # /api/graph/* 图谱查询与管理
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
"""
|
||||
知识库思维导图路由模块。
|
||||
|
||||
提供思维导图相关的 API 接口,包括:
|
||||
- 获取知识库文件列表
|
||||
- AI 生成思维导图
|
||||
- 保存和加载思维导图配置
|
||||
"""
|
||||
|
||||
import traceback
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
|
||||
from server.utils.auth_middleware import get_admin_user
|
||||
from yuxi import config, knowledge_base
|
||||
from yuxi.knowledge.utils.mindmap_utils import (
|
||||
MINDMAP_SYSTEM_PROMPT,
|
||||
build_database_file_list,
|
||||
build_mindmap_user_message,
|
||||
collect_mindmap_files,
|
||||
parse_mindmap_content,
|
||||
)
|
||||
from yuxi.models import select_model
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils import logger
|
||||
|
||||
mindmap = APIRouter(prefix="/mindmap", tags=["mindmap"])
|
||||
|
||||
|
||||
@mindmap.get("/databases/{kb_id}/files")
|
||||
async def get_database_files(kb_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取指定知识库的所有文件列表。"""
|
||||
try:
|
||||
db_info = await knowledge_base.get_database_info(kb_id)
|
||||
if not db_info:
|
||||
raise HTTPException(status_code=404, detail=f"知识库 {kb_id} 不存在")
|
||||
|
||||
file_list = build_database_file_list(db_info.get("files", {}))
|
||||
return {
|
||||
"message": "success",
|
||||
"kb_id": kb_id,
|
||||
"db_name": db_info.get("name", ""),
|
||||
"files": file_list,
|
||||
"total": len(file_list),
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取知识库文件列表失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"获取文件列表失败: {str(e)}")
|
||||
|
||||
|
||||
@mindmap.post("/generate")
|
||||
async def generate_mindmap(
|
||||
kb_id: str = Body(..., description="知识库ID"),
|
||||
file_ids: list[str] = Body(default=[], description="选择的文件ID列表"),
|
||||
user_prompt: str = Body(default="", description="用户自定义提示词"),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
"""使用 AI 分析知识库文件,生成思维导图结构。"""
|
||||
try:
|
||||
db_info = await knowledge_base.get_database_info(kb_id)
|
||||
if not db_info:
|
||||
raise HTTPException(status_code=404, detail=f"知识库 {kb_id} 不存在")
|
||||
|
||||
db_name = db_info.get("name", "知识库")
|
||||
all_files = db_info.get("files", {})
|
||||
|
||||
if not file_ids:
|
||||
file_ids = list(all_files.keys())
|
||||
if not file_ids:
|
||||
raise HTTPException(status_code=400, detail="知识库中没有文件")
|
||||
|
||||
original_count = len(file_ids)
|
||||
if len(file_ids) > 20:
|
||||
file_ids = file_ids[:20]
|
||||
logger.info(f"文件数量超过限制,已从{original_count}个文件中选择前20个文件生成思维导图")
|
||||
|
||||
files_info = collect_mindmap_files(all_files, file_ids)
|
||||
if not files_info:
|
||||
raise HTTPException(status_code=400, detail="选择的文件不存在")
|
||||
|
||||
logger.info(f"开始生成思维导图,知识库: {db_name}, 文件数量: {len(files_info)}")
|
||||
|
||||
model = select_model(model_spec=config.default_model)
|
||||
messages = [
|
||||
{"role": "system", "content": MINDMAP_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": build_mindmap_user_message(db_name, files_info, user_prompt)},
|
||||
]
|
||||
response = await model.call(messages, stream=False)
|
||||
content = response.content if hasattr(response, "content") else str(response)
|
||||
|
||||
try:
|
||||
mindmap_data = parse_mindmap_content(content)
|
||||
except ValueError as e:
|
||||
logger.error(f"AI返回的JSON解析失败: {e}, 原始内容: {content}")
|
||||
raise HTTPException(status_code=500, detail=f"AI返回格式错误: {str(e)}")
|
||||
|
||||
logger.info("思维导图生成成功")
|
||||
|
||||
try:
|
||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||
|
||||
await KnowledgeBaseRepository().update(kb_id, {"mindmap": mindmap_data})
|
||||
logger.info(f"思维导图已保存到知识库: {kb_id}")
|
||||
except Exception as save_error:
|
||||
logger.error(f"保存思维导图失败: {save_error}")
|
||||
|
||||
return {
|
||||
"message": "success",
|
||||
"mindmap": mindmap_data,
|
||||
"kb_id": kb_id,
|
||||
"db_name": db_name,
|
||||
"file_count": len(files_info),
|
||||
"original_file_count": original_count,
|
||||
"truncated": len(files_info) < original_count,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"生成思维导图失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"生成思维导图失败: {str(e)}")
|
||||
|
||||
|
||||
@mindmap.get("/databases")
|
||||
async def get_databases_overview(current_user: User = Depends(get_admin_user)):
|
||||
"""获取所有知识库的概览信息,用于思维导图界面选择。"""
|
||||
try:
|
||||
databases = await knowledge_base.get_databases_by_uid(current_user.uid)
|
||||
db_list = []
|
||||
for db_info in databases.get("databases", []):
|
||||
kb_id = db_info.get("kb_id")
|
||||
if not kb_id:
|
||||
continue
|
||||
|
||||
detail_info = await knowledge_base.get_database_info(kb_id)
|
||||
file_count = len(detail_info.get("files", {})) if detail_info else 0
|
||||
db_list.append(
|
||||
{
|
||||
"kb_id": kb_id,
|
||||
"name": db_info.get("name", ""),
|
||||
"description": db_info.get("description", ""),
|
||||
"kb_type": db_info.get("kb_type", ""),
|
||||
"file_count": file_count,
|
||||
}
|
||||
)
|
||||
|
||||
return {"message": "success", "databases": db_list, "total": len(db_list)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取知识库列表失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"获取知识库列表失败: {str(e)}")
|
||||
|
||||
|
||||
@mindmap.get("/database/{kb_id}")
|
||||
async def get_database_mindmap(kb_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取知识库关联的思维导图。"""
|
||||
try:
|
||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||
|
||||
kb = await KnowledgeBaseRepository().get_by_kb_id(kb_id)
|
||||
if kb is None:
|
||||
raise HTTPException(status_code=404, detail=f"知识库 {kb_id} 不存在")
|
||||
|
||||
return {
|
||||
"message": "success",
|
||||
"mindmap": kb.mindmap,
|
||||
"kb_id": kb_id,
|
||||
"db_name": kb.name,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取知识库思维导图失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"获取思维导图失败: {str(e)}")
|
||||
@ -20,6 +20,15 @@ from yuxi.knowledge.graphs.milvus_graph_service import GRAPH_TASK_TYPE, MilvusGr
|
||||
from yuxi.plugins.parser import Parser, SUPPORTED_FILE_EXTENSIONS, is_supported_file_extension
|
||||
from yuxi.knowledge.utils import calculate_content_hash
|
||||
from yuxi.knowledge.utils.kb_utils import is_minio_url, parse_minio_url
|
||||
from yuxi.knowledge.utils.mindmap_utils import (
|
||||
MindmapGenerationError,
|
||||
MindmapNotFoundError,
|
||||
MindmapValidationError,
|
||||
generate_database_mindmap,
|
||||
get_database_mindmap_data,
|
||||
get_mindmap_database_files,
|
||||
get_mindmap_databases_overview,
|
||||
)
|
||||
from yuxi.services.model_cache import model_cache
|
||||
from yuxi.services.workspace_service import MAX_WORKSPACE_UPLOAD_SIZE_BYTES, resolve_workspace_file_path
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
@ -229,6 +238,58 @@ async def get_accessible_databases(current_user: User = Depends(get_required_use
|
||||
return {"message": f"获取可访问知识库列表失败: {str(e)}", "databases": []}
|
||||
|
||||
|
||||
def _raise_mindmap_http_exception(error: Exception, operation: str) -> None:
|
||||
if isinstance(error, MindmapNotFoundError):
|
||||
raise HTTPException(status_code=404, detail=str(error))
|
||||
if isinstance(error, MindmapValidationError):
|
||||
raise HTTPException(status_code=400, detail=str(error))
|
||||
if isinstance(error, MindmapGenerationError):
|
||||
raise HTTPException(status_code=500, detail=str(error))
|
||||
logger.error(f"{operation}失败: {error}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"{operation}失败: {str(error)}")
|
||||
|
||||
|
||||
@knowledge.get("/mindmap/databases")
|
||||
async def get_mindmap_databases(current_user: User = Depends(get_admin_user)):
|
||||
"""获取所有知识库的概览信息,用于思维导图界面选择。"""
|
||||
try:
|
||||
return await get_mindmap_databases_overview(current_user.uid)
|
||||
except Exception as e:
|
||||
_raise_mindmap_http_exception(e, "获取知识库列表")
|
||||
|
||||
|
||||
@knowledge.get("/databases/{kb_id}/mindmap/files")
|
||||
async def get_database_mindmap_files(kb_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取指定知识库的所有文件列表。"""
|
||||
try:
|
||||
return await get_mindmap_database_files(kb_id)
|
||||
except Exception as e:
|
||||
_raise_mindmap_http_exception(e, "获取文件列表")
|
||||
|
||||
|
||||
@knowledge.post("/databases/{kb_id}/mindmap/generate")
|
||||
async def generate_mindmap(
|
||||
kb_id: str,
|
||||
file_ids: list[str] | None = Body(default=None, description="选择的文件ID列表"),
|
||||
user_prompt: str = Body(default="", description="用户自定义提示词"),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
"""使用 AI 分析知识库文件,生成思维导图结构。"""
|
||||
try:
|
||||
return await generate_database_mindmap(kb_id, file_ids, user_prompt)
|
||||
except Exception as e:
|
||||
_raise_mindmap_http_exception(e, "生成思维导图")
|
||||
|
||||
|
||||
@knowledge.get("/databases/{kb_id}/mindmap")
|
||||
async def get_database_mindmap(kb_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取知识库关联的思维导图。"""
|
||||
try:
|
||||
return await get_database_mindmap_data(kb_id)
|
||||
except Exception as e:
|
||||
_raise_mindmap_http_exception(e, "获取知识库思维导图")
|
||||
|
||||
|
||||
@knowledge.get("/databases/{kb_id}")
|
||||
async def get_database_info(kb_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取知识库详细信息"""
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Integration tests for knowledge router and mindmap router endpoints.
|
||||
Integration tests for knowledge router endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -384,13 +384,13 @@ async def test_dify_query_params_and_documents_readonly(test_client, admin_heade
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === Mindmap Router Tests ===
|
||||
# === Mindmap Tests ===
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_get_databases_overview(test_client, admin_headers, knowledge_database):
|
||||
"""测试获取所有知识库概览"""
|
||||
response = await test_client.get("/api/mindmap/databases", headers=admin_headers)
|
||||
response = await test_client.get("/api/knowledge/mindmap/databases", headers=admin_headers)
|
||||
assert response.status_code == 200, response.text
|
||||
payload = response.json()
|
||||
assert payload["message"] == "success"
|
||||
@ -405,7 +405,7 @@ async def test_get_databases_overview(test_client, admin_headers, knowledge_data
|
||||
async def test_get_database_files(test_client, admin_headers, knowledge_database):
|
||||
"""测试获取知识库文件列表"""
|
||||
slug = knowledge_database["slug"]
|
||||
response = await test_client.get(f"/api/mindmap/databases/{slug}/files", headers=admin_headers)
|
||||
response = await test_client.get(f"/api/knowledge/databases/{slug}/mindmap/files", headers=admin_headers)
|
||||
assert response.status_code == 200, response.text
|
||||
payload = response.json()
|
||||
assert payload["message"] == "success"
|
||||
@ -417,7 +417,7 @@ async def test_get_database_files(test_client, admin_headers, knowledge_database
|
||||
|
||||
async def test_get_database_files_not_found(test_client, admin_headers):
|
||||
"""测试获取不存在的知识库文件列表"""
|
||||
response = await test_client.get("/api/mindmap/databases/nonexistent_slug/files", headers=admin_headers)
|
||||
response = await test_client.get("/api/knowledge/databases/nonexistent_slug/mindmap/files", headers=admin_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@ -425,8 +425,8 @@ async def test_generate_mindmap_empty_files(test_client, admin_headers, knowledg
|
||||
"""测试空文件列表生成思维导图"""
|
||||
slug = knowledge_database["slug"]
|
||||
response = await test_client.post(
|
||||
"/api/mindmap/generate",
|
||||
json={"slug": slug, "file_ids": [], "user_prompt": ""},
|
||||
f"/api/knowledge/databases/{slug}/mindmap/generate",
|
||||
json={"file_ids": [], "user_prompt": ""},
|
||||
headers=admin_headers,
|
||||
)
|
||||
# 空文件应该返回400错误
|
||||
@ -437,7 +437,7 @@ async def test_generate_mindmap_empty_files(test_client, admin_headers, knowledg
|
||||
async def test_get_database_mindmap_not_exists(test_client, admin_headers, knowledge_database):
|
||||
"""测试获取不存在的思维导图"""
|
||||
slug = knowledge_database["slug"]
|
||||
response = await test_client.get(f"/api/mindmap/database/{slug}", headers=admin_headers)
|
||||
response = await test_client.get(f"/api/knowledge/databases/{slug}/mindmap", headers=admin_headers)
|
||||
assert response.status_code == 200, response.text
|
||||
payload = response.json()
|
||||
assert payload["slug"] == slug
|
||||
@ -455,8 +455,8 @@ async def test_generate_and_get_mindmap(test_client, admin_headers, knowledge_da
|
||||
|
||||
# 空文件场景 - 预期返回400错误
|
||||
generate_response = await test_client.post(
|
||||
"/api/mindmap/generate",
|
||||
json={"slug": slug, "file_ids": [], "user_prompt": ""},
|
||||
f"/api/knowledge/databases/{slug}/mindmap/generate",
|
||||
json={"file_ids": [], "user_prompt": ""},
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert generate_response.status_code == 400
|
||||
@ -728,15 +728,17 @@ async def test_mindmap_permissions(test_client, standard_user, knowledge_databas
|
||||
slug = knowledge_database["slug"]
|
||||
|
||||
# 普通用户应该无法访问
|
||||
forbidden_list = await test_client.get("/api/mindmap/databases", headers=standard_user["headers"])
|
||||
forbidden_list = await test_client.get("/api/knowledge/mindmap/databases", headers=standard_user["headers"])
|
||||
_assert_forbidden_response(forbidden_list)
|
||||
|
||||
forbidden_files = await test_client.get(f"/api/mindmap/databases/{slug}/files", headers=standard_user["headers"])
|
||||
forbidden_files = await test_client.get(
|
||||
f"/api/knowledge/databases/{slug}/mindmap/files", headers=standard_user["headers"]
|
||||
)
|
||||
_assert_forbidden_response(forbidden_files)
|
||||
|
||||
forbidden_generate = await test_client.post(
|
||||
"/api/mindmap/generate",
|
||||
json={"slug": slug, "file_ids": []},
|
||||
f"/api/knowledge/databases/{slug}/mindmap/generate",
|
||||
json={"file_ids": []},
|
||||
headers=standard_user["headers"],
|
||||
)
|
||||
_assert_forbidden_response(forbidden_generate)
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
- 重梳理知识库评估存储:评估数据集、题目、评估运行和逐题结果统一入库,JSONL 仅作为导入/导出格式;后端和前端 API 统一使用 dataset/run 语义。
|
||||
- 扩展知识库上传来源:添加“从工作区上传”模式,后端将当前用户工作区文件预处理上传到 MinIO,前端沿用现有 `addDocuments` 入库链路提交 MinIO URL、内容哈希和文件大小。
|
||||
- 重构知识库详情页布局:`DatabaseInfo` 改为顶部详情 header + 左侧功能 tab 侧边栏 + 右侧内容区,Milvus 默认进入文件管理,并将检索测试、知识图谱、知识导图、检索配置、RAG 评估和评估基准统一纳入侧边栏导航;只读连接器保留检索测试与检索配置。
|
||||
- 整合知识导图接口:移除独立 mindmap router 与前端 API 模块,思维导图生成、查询和文件列表接口统一收敛到知识库 API 下。
|
||||
- 收敛独立模型配置模块运行时:运行时 chat / embedding / rerank 均统一从 provider 模块与模型缓存读取 `provider_id:model_id`;旧版静态模型配置、v1 slash spec、旧模型列表接口和 Ollama 适配已移除。
|
||||
- 调整智能体配置归属与字段权限:`AgentConfig` 从部门共享改为按 `uid` 隔离,所有登录用户可管理自己的配置;`BaseContext` 支持字段级 `auth` 元数据,后端按用户角色过滤可见与可保存的配置项。
|
||||
- 新增用户级沙盒环境变量:增加 `agent_envs` 表与 `/api/user/agent-env` 接口,设置面板支持当前用户维护 Agent 沙盒环境变量;创建新沙盒时与全局 `sandbox.env` 合并注入,用户变量优先。
|
||||
|
||||
@ -9,7 +9,6 @@ export * from './knowledge_api' // 知识库管理API
|
||||
export * from './graph_api' // 图谱API
|
||||
export * from './agent_api' // 智能体API
|
||||
export * from './tasker' // 任务管理API
|
||||
export * from './mindmap_api' // 思维导图API
|
||||
export * from './department_api' // 部门管理API
|
||||
export * from './mcp_api' // MCP API
|
||||
export * from './skill_api' // Skills API
|
||||
|
||||
@ -229,6 +229,31 @@ export const graphBuildApi = {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// === 思维导图分组 ===
|
||||
// =============================================================================
|
||||
|
||||
export const mindmapApi = {
|
||||
getDatabases: async () => {
|
||||
return apiAdminGet('/api/knowledge/mindmap/databases')
|
||||
},
|
||||
|
||||
getDatabaseFiles: async (kbId) => {
|
||||
return apiAdminGet(`/api/knowledge/databases/${kbId}/mindmap/files`)
|
||||
},
|
||||
|
||||
generateMindmap: async (kbId, fileIds = [], userPrompt = '') => {
|
||||
return apiAdminPost(`/api/knowledge/databases/${kbId}/mindmap/generate`, {
|
||||
file_ids: fileIds,
|
||||
user_prompt: userPrompt
|
||||
})
|
||||
},
|
||||
|
||||
getByDatabase: async (kbId) => {
|
||||
return apiAdminGet(`/api/knowledge/databases/${kbId}/mindmap`)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// === 查询分组 ===
|
||||
// =============================================================================
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
import { apiAdminGet, apiAdminPost } from './base'
|
||||
|
||||
/**
|
||||
* 思维导图API模块
|
||||
* 提供思维导图相关的接口功能
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// === 知识库管理 ===
|
||||
// =============================================================================
|
||||
|
||||
export const mindmapApi = {
|
||||
/**
|
||||
* 获取所有知识库概览(用于选择)
|
||||
* @returns {Promise} - 知识库列表
|
||||
*/
|
||||
getDatabases: async () => {
|
||||
return apiAdminGet('/api/mindmap/databases')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指定知识库的文件列表
|
||||
* @param {string} kbId - 知识库ID
|
||||
* @returns {Promise} - 文件列表
|
||||
*/
|
||||
getDatabaseFiles: async (kbId) => {
|
||||
return apiAdminGet(`/api/mindmap/databases/${kbId}/files`)
|
||||
},
|
||||
|
||||
/**
|
||||
* AI生成思维导图
|
||||
* @param {string} kbId - 知识库ID
|
||||
* @param {Array<string>} fileIds - 选择的文件ID列表(为空则使用所有文件)
|
||||
* @param {string} userPrompt - 用户自定义提示词
|
||||
* @returns {Promise} - 思维导图数据
|
||||
*/
|
||||
generateMindmap: async (kbId, fileIds = [], userPrompt = '') => {
|
||||
return apiAdminPost('/api/mindmap/generate', {
|
||||
kb_id: kbId,
|
||||
file_ids: fileIds,
|
||||
user_prompt: userPrompt
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取知识库的思维导图
|
||||
* @param {string} kbId - 知识库ID
|
||||
* @returns {Promise} - 思维导图数据
|
||||
*/
|
||||
getByDatabase: async (kbId) => {
|
||||
return apiAdminGet(`/api/mindmap/database/${kbId}`)
|
||||
}
|
||||
}
|
||||
@ -15,32 +15,44 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="!mindmapData" class="empty-state">
|
||||
<Network :size="32" />
|
||||
<p>暂无思维导图</p>
|
||||
<a-button type="primary" size="small" @click="generateMindmap">
|
||||
<template #icon><Sparkles :size="14" /></template>
|
||||
生成思维导图
|
||||
</a-button>
|
||||
<div class="empty-icon">
|
||||
<MapIcon :size="24" />
|
||||
</div>
|
||||
<p class="empty-title">暂无思维导图</p>
|
||||
<p class="empty-description">从当前知识库内容生成结构化导图。</p>
|
||||
<button
|
||||
type="button"
|
||||
class="lucide-icon-btn mindmap-primary-action"
|
||||
@click="generateMindmap"
|
||||
>
|
||||
<Sparkles :size="14" />
|
||||
<span>生成思维导图</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 思维导图显示 -->
|
||||
<div v-else class="mindmap-container">
|
||||
<div class="mindmap-toolbar">
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
<button
|
||||
type="button"
|
||||
class="lucide-icon-btn mindmap-toolbar-btn"
|
||||
:disabled="generating"
|
||||
@click="refreshMindmap"
|
||||
:loading="generating"
|
||||
title="重新生成"
|
||||
>
|
||||
<template #icon><RefreshCw :size="14" /></template>
|
||||
<RefreshCw :size="14" :class="{ spin: generating }" />
|
||||
<span class="toolbar-text">重新生成</span>
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="fitView" title="适应视图">
|
||||
<template #icon><Maximize2 :size="14" /></template>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="lucide-icon-btn mindmap-toolbar-btn"
|
||||
@click="fitView"
|
||||
title="适应视图"
|
||||
>
|
||||
<Maximize2 :size="14" />
|
||||
<span class="toolbar-text">适应视图</span>
|
||||
</a-button>
|
||||
</button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="mindmap-svg-container">
|
||||
@ -54,8 +66,8 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { RefreshCw, Network, Sparkles, Maximize2 } from 'lucide-vue-next'
|
||||
import { mindmapApi } from '@/apis/mindmap_api'
|
||||
import { RefreshCw, Map as MapIcon, Sparkles, Maximize2 } from 'lucide-vue-next'
|
||||
import { mindmapApi } from '@/apis/knowledge_api'
|
||||
import { Markmap } from 'markmap-view'
|
||||
import { Transformer } from 'markmap-lib'
|
||||
|
||||
@ -90,13 +102,20 @@ const loadMindmap = async () => {
|
||||
loading.value = true
|
||||
const response = await mindmapApi.getByDatabase(props.kbId)
|
||||
|
||||
if (response.mindmap) {
|
||||
mindmapData.value = response.mindmap
|
||||
const mindmap = response.mindmap || null
|
||||
mindmapData.value = mindmap
|
||||
|
||||
if (markmapInstance) {
|
||||
markmapInstance.destroy()
|
||||
markmapInstance = null
|
||||
}
|
||||
|
||||
if (mindmap) {
|
||||
await nextTick()
|
||||
|
||||
// 延迟渲染,确保DOM完全更新
|
||||
setTimeout(() => {
|
||||
renderMindmap(response.mindmap)
|
||||
renderMindmap(mindmap)
|
||||
}, 100)
|
||||
}
|
||||
} catch (error) {
|
||||
@ -302,9 +321,11 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--gray-0);
|
||||
border-top: 1px solid var(--border-color);
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
@ -323,20 +344,71 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
color: var(--text-secondary);
|
||||
gap: 10px;
|
||||
padding: 28px;
|
||||
color: var(--gray-500);
|
||||
font-size: 13px;
|
||||
|
||||
svg {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 12px;
|
||||
background: var(--main-30);
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin-top: 2px;
|
||||
color: var(--gray-900);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
max-width: 280px;
|
||||
color: var(--gray-500);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mindmap-primary-action {
|
||||
min-height: 32px;
|
||||
margin-top: 4px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: var(--main-600);
|
||||
color: var(--main-0);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: var(--main-700);
|
||||
color: var(--main-0);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--main-200);
|
||||
}
|
||||
}
|
||||
|
||||
.mindmap-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -346,9 +418,9 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.mindmap-toolbar {
|
||||
padding: 8px 16px;
|
||||
padding: 8px 12px;
|
||||
background: var(--gray-0);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@ -357,14 +429,53 @@ onUnmounted(() => {
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-btn-text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.mindmap-toolbar-btn {
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--gray-600);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: var(--gray-50);
|
||||
color: var(--main-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--main-100);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:disabled:hover {
|
||||
background: transparent;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user