feat: 优化文明命名,更加清晰

This commit is contained in:
Wenjie Zhang 2026-05-04 11:36:08 +08:00
parent f9fae5c5df
commit 6c35fd732a
9 changed files with 301 additions and 366 deletions

View File

@ -0,0 +1,117 @@
"""思维导图工具函数。"""
import json
import textwrap
from typing import Any
MINDMAP_SYSTEM_PROMPT = """你是一个专业的知识整理助手。
你的任务是分析用户提供的文件列表生成一个层次分明的思维导图结构
**核心规则每个文件名只能出现一次不允许重复**
要求
1. 思维导图要有清晰的层级结构2-4
2. 根节点是知识库名称
3. 第一层是主要分类技术文档规章制度数据资源等
4. 第二层是子分类
5. **叶子节点必须是具体的文件名称**
6. **每个文件名在整个思维导图中只能出现一次不得重复**
7. 如果一个文件可能属于多个分类只选择最合适的一个分类放置
8. 使用合适的emoji图标增强可读性
9. 返回JSON格式遵循以下结构
```json
{
"content": "知识库名称",
"children": [
{
"content": "🎯 主分类1",
"children": [
{
"content": "子分类1.1",
"children": [
{"content": "文件名1.txt", "children": []},
{"content": "文件名2.pdf", "children": []}
]
}
]
},
{
"content": "💻 主分类2",
"children": [
{"content": "文件名3.docx", "children": []},
{"content": "文件名4.md", "children": []}
]
}
]
}
```
**重要约束**
- 每个文件名在整个JSON中只能出现一次
- 不要按多个维度分类导致文件重复
- 选择最主要最合适的分类维度
- 每个叶子节点的children必须是空数组[]
- 分类名称要简洁明了
- 使用emoji增强视觉效果
"""
def build_database_file_list(files: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
return [
{
"file_id": file_id,
"filename": file_info.get("filename", ""),
"type": file_info.get("type", ""),
"status": file_info.get("status", ""),
"created_at": file_info.get("created_at", ""),
}
for file_id, file_info in files.items()
]
def collect_mindmap_files(all_files: dict[str, dict[str, Any]], file_ids: list[str]) -> list[dict[str, str]]:
return [
{
"filename": all_files[file_id].get("filename", ""),
"type": all_files[file_id].get("type", ""),
}
for file_id in file_ids
if file_id in all_files
]
def build_mindmap_user_message(db_name: str, files_info: list[dict[str, str]], user_prompt: str = "") -> str:
files_text = "\n".join([f"- {file_info['filename']} ({file_info['type']})" for file_info in files_info])
return textwrap.dedent(f"""请为知识库\"{db_name}\"生成思维导图结构。
文件列表{len(files_info)}个文件
{files_text}
{f"用户补充说明:{user_prompt}" if user_prompt else ""}
**重要提醒**
1. 这个知识库共有{len(files_info)}个文件
2. 每个文件名只能在思维导图中出现一次
3. 不要让同一个文件出现在多个分类下
4. 为每个文件选择最合适的唯一分类
请生成合理的思维导图结构""")
def parse_mindmap_content(content: str) -> dict[str, Any]:
if "```json" in content:
json_start = content.find("```json") + 7
json_end = content.find("```", json_start)
content = content[json_start:json_end].strip()
elif "```" in content:
json_start = content.find("```") + 3
json_end = content.find("```", json_start)
content = content[json_start:json_end].strip()
mindmap_data = json.loads(content)
if not isinstance(mindmap_data, dict) or "content" not in mindmap_data:
raise ValueError("思维导图结构不正确")
return mindmap_data

View File

@ -5,15 +5,15 @@ from fastapi import APIRouter
from server.routers.auth_router import auth from server.routers.auth_router import auth
from server.routers.chat_router import chat from server.routers.chat_router import chat
from server.routers.dashboard_router import dashboard from server.routers.dashboard_router import dashboard
from server.routers.department_router import department from server.routers.auth_dept_router import department
from server.routers.mcp_router import mcp from server.routers.mcp_router import mcp
from server.routers.model_provider_router import model_providers from server.routers.model_provider_router import model_providers
from server.routers.skill_router import skills from server.routers.skill_router import skills
from server.routers.subagent_router import subagents_router from server.routers.subagent_router import subagents_router
from server.routers.system_router import system from server.routers.system_router import system
from server.routers.task_router import tasks from server.routers.system_task_router import tasks
from server.routers.tool_router import tools from server.routers.tool_router import tools
from server.routers.apikey_router import apikey_router from server.routers.auth_apikey_router import apikey_router
from server.routers.filesystem_router import filesystem_router from server.routers.filesystem_router import filesystem_router
from server.routers.workspace_router import workspace from server.routers.workspace_router import workspace
@ -42,8 +42,8 @@ router.include_router(workspace) # /api/workspace/* 用户个人工作区
if not _LITE_MODE: if not _LITE_MODE:
from server.routers.graph_router import graph from server.routers.graph_router import graph
from server.routers.knowledge_router import knowledge from server.routers.knowledge_router import knowledge
from server.routers.evaluation_router import evaluation from server.routers.knowledge_eval_router import evaluation
from server.routers.mindmap_router import mindmap from server.routers.knowledge_mindmap_router import mindmap
# 知识库与图谱能力依赖较重LITE 模式下跳过这组接口。 # 知识库与图谱能力依赖较重LITE 模式下跳过这组接口。
router.include_router(knowledge) # /api/knowledge/* 知识库管理与检索 router.include_router(knowledge) # /api/knowledge/* 知识库管理与检索

View File

@ -0,0 +1,178 @@
"""
知识库思维导图路由模块
提供思维导图相关的 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/{db_id}/files")
async def get_database_files(db_id: str, current_user: User = Depends(get_admin_user)):
"""获取指定知识库的所有文件列表。"""
try:
db_info = await knowledge_base.get_database_info(db_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {db_id} 不存在")
file_list = build_database_file_list(db_info.get("files", {}))
return {
"message": "success",
"db_id": db_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(
db_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(db_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {db_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(db_id, {"mindmap": mindmap_data})
logger.info(f"思维导图已保存到知识库: {db_id}")
except Exception as save_error:
logger.error(f"保存思维导图失败: {save_error}")
return {
"message": "success",
"mindmap": mindmap_data,
"db_id": db_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_user_id(current_user.user_id)
db_list = []
for db_info in databases.get("databases", []):
db_id = db_info.get("db_id")
if not db_id:
continue
detail_info = await knowledge_base.get_database_info(db_id)
file_count = len(detail_info.get("files", {})) if detail_info else 0
db_list.append(
{
"db_id": db_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/{db_id}")
async def get_database_mindmap(db_id: str, current_user: User = Depends(get_admin_user)):
"""获取知识库关联的思维导图。"""
try:
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
kb = await KnowledgeBaseRepository().get_by_id(db_id)
if kb is None:
raise HTTPException(status_code=404, detail=f"知识库 {db_id} 不存在")
return {
"message": "success",
"mindmap": kb.mindmap,
"db_id": db_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)}")

View File

@ -1,361 +0,0 @@
"""
思维导图路由模块
提供思维导图相关的API接口包括
- 获取知识库文件列表
- AI生成思维导图
- 保存和加载思维导图配置
"""
import json
import traceback
import textwrap
from fastapi import APIRouter, Body, Depends, HTTPException
from yuxi.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user
from yuxi import config, knowledge_base
from yuxi.models import select_model
from yuxi.utils import logger
mindmap = APIRouter(prefix="/mindmap", tags=["mindmap"])
# =============================================================================
# === 获取知识库文件列表 ===
# =============================================================================
MINDMAP_SYSTEM_PROMPT = """你是一个专业的知识整理助手。
你的任务是分析用户提供的文件列表生成一个层次分明的思维导图结构
**核心规则每个文件名只能出现一次不允许重复**
要求
1. 思维导图要有清晰的层级结构2-4
2. 根节点是知识库名称
3. 第一层是主要分类技术文档规章制度数据资源等
4. 第二层是子分类
5. **叶子节点必须是具体的文件名称**
6. **每个文件名在整个思维导图中只能出现一次不得重复**
7. 如果一个文件可能属于多个分类只选择最合适的一个分类放置
8. 使用合适的emoji图标增强可读性
9. 返回JSON格式遵循以下结构
```json
{
"content": "知识库名称",
"children": [
{
"content": "🎯 主分类1",
"children": [
{
"content": "子分类1.1",
"children": [
{"content": "文件名1.txt", "children": []},
{"content": "文件名2.pdf", "children": []}
]
}
]
},
{
"content": "💻 主分类2",
"children": [
{"content": "文件名3.docx", "children": []},
{"content": "文件名4.md", "children": []}
]
}
]
}
```
**重要约束**
- 每个文件名在整个JSON中只能出现一次
- 不要按多个维度分类导致文件重复
- 选择最主要最合适的分类维度
- 每个叶子节点的children必须是空数组[]
- 分类名称要简洁明了
- 使用emoji增强视觉效果
"""
@mindmap.get("/databases/{db_id}/files")
async def get_database_files(db_id: str, current_user: User = Depends(get_admin_user)):
"""
获取指定知识库的所有文件列表
Args:
db_id: 知识库ID
Returns:
文件列表信息
"""
try:
# 获取知识库详细信息
db_info = await knowledge_base.get_database_info(db_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {db_id} 不存在")
# 提取文件信息
files = db_info.get("files", {})
# 转换为列表格式
file_list = []
for file_id, file_info in files.items():
file_list.append(
{
"file_id": file_id,
"filename": file_info.get("filename", ""),
"type": file_info.get("type", ""),
"status": file_info.get("status", ""),
"created_at": file_info.get("created_at", ""),
}
)
return {
"message": "success",
"db_id": db_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)}")
# =============================================================================
# === AI生成思维导图 ===
# =============================================================================
@mindmap.post("/generate")
async def generate_mindmap(
db_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分析知识库文件生成思维导图结构
Args:
db_id: 知识库ID
file_ids: 选择的文件ID列表为空则使用所有文件
user_prompt: 用户自定义提示词
Returns:
Markmap格式的思维导图数据
"""
try:
# 获取知识库信息
db_info = await knowledge_base.get_database_info(db_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {db_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="知识库中没有文件")
# 限制文件数量不超过100个如果超过则选择前100个
if len(file_ids) > 20:
original_count = len(file_ids)
file_ids = file_ids[:20]
logger.info(f"文件数量超过限制,已从{original_count}个文件中选择前20个文件生成思维导图")
# 收集文件信息
files_info = []
for file_id in file_ids:
if file_id in all_files:
file_info = all_files[file_id]
files_info.append(
{
"filename": file_info.get("filename", ""),
"type": file_info.get("type", ""),
}
)
if not files_info:
raise HTTPException(status_code=400, detail="选择的文件不存在")
# 构建AI提示词
system_prompt = MINDMAP_SYSTEM_PROMPT
# 构建用户消息
files_text = "\n".join([f"- {f['filename']} ({f['type']})" for f in files_info])
user_message = textwrap.dedent(f"""请为知识库"{db_name}"生成思维导图结构。
文件列表{len(files_info)}个文件
{files_text}
{f"用户补充说明:{user_prompt}" if user_prompt else ""}
**重要提醒**
1. 这个知识库共有{len(files_info)}个文件
2. 每个文件名只能在思维导图中出现一次
3. 不要让同一个文件出现在多个分类下
4. 为每个文件选择最合适的唯一分类
请生成合理的思维导图结构""")
# 调用AI生成
logger.info(f"开始生成思维导图,知识库: {db_name}, 文件数量: {len(files_info)}")
# 选择模型并调用
model = select_model(model_spec=config.default_model)
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}]
response = await model.call(messages, stream=False)
# 解析AI返回的JSON
try:
# 提取JSON内容
content = response.content if hasattr(response, "content") else str(response)
# 尝试从markdown代码块中提取JSON
if "```json" in content:
json_start = content.find("```json") + 7
json_end = content.find("```", json_start)
content = content[json_start:json_end].strip()
elif "```" in content:
json_start = content.find("```") + 3
json_end = content.find("```", json_start)
content = content[json_start:json_end].strip()
mindmap_data = json.loads(content)
# 验证结构
if not isinstance(mindmap_data, dict) or "content" not in mindmap_data:
raise ValueError("思维导图结构不正确")
logger.info("思维导图生成成功")
# 保存思维导图到知识库元数据
try:
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
await KnowledgeBaseRepository().update(db_id, {"mindmap": mindmap_data})
logger.info(f"思维导图已保存到知识库: {db_id}")
except Exception as save_error:
logger.error(f"保存思维导图失败: {save_error}")
# 不影响返回结果,只记录错误
return {
"message": "success",
"mindmap": mindmap_data,
"db_id": db_id,
"db_name": db_name,
"file_count": len(files_info),
"original_file_count": original_count if "original_count" in locals() else len(files_info),
"truncated": len(files_info) < (original_count if "original_count" in locals() else len(files_info)),
}
except json.JSONDecodeError as e:
logger.error(f"AI返回的JSON解析失败: {e}, 原始内容: {content}")
raise HTTPException(status_code=500, detail=f"AI返回格式错误: {str(e)}")
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)):
"""
获取所有知识库的概览信息用于思维导图界面选择根据用户权限过滤
Returns:
知识库列表
"""
try:
databases = await knowledge_base.get_databases_by_user_id(current_user.user_id)
# databases["databases"] 是一个列表,每个元素已经包含了基本信息
db_list_raw = databases.get("databases", [])
db_list = []
for db_info in db_list_raw:
db_id = db_info.get("db_id")
if not db_id:
continue
# 获取详细信息以获取文件数量
detail_info = await knowledge_base.get_database_info(db_id)
file_count = len(detail_info.get("files", {})) if detail_info else 0
db_list.append(
{
"db_id": db_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/{db_id}")
async def get_database_mindmap(db_id: str, current_user: User = Depends(get_admin_user)):
"""
获取知识库关联的思维导图
Args:
db_id: 知识库ID
Returns:
思维导图数据
"""
try:
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
kb_repo = KnowledgeBaseRepository()
kb = await kb_repo.get_by_id(db_id)
if kb is None:
raise HTTPException(status_code=404, detail=f"知识库 {db_id} 不存在")
return {
"message": "success",
"mindmap": kb.mindmap,
"db_id": db_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)}")

View File

@ -37,6 +37,7 @@
### 0.6.2 开发记录 ### 0.6.2 开发记录
<!-- 0.6.2 的内容请放在这里 --> <!-- 0.6.2 的内容请放在这里 -->
- 调整知识库思维导图后端结构:将思维导图路由文件重命名为知识库语义更明确的 router并把文件列表整理、提示词构建、AI JSON 解析等纯逻辑下沉到知识库 utils。
- 新增个人工作区预览与管理:提供独立于对话 thread 的用户级 workspace API并增加“工作区”页面用于浏览个人 workspace 文件、预览 Markdown/文本/代码/图片/PDF支持新建文件夹、上传文件、下载文件、删除文件/文件夹和多选删除;工作区预览支持 Markdown/TXT 在右侧预览框内切换编辑并保存,其他格式和非工作区预览默认只读;知识库与团队空间入口先展示到占位层级;默认创建 `agents/AGENTS.md`,并在 Agent 执行时将其内容追加到系统提示词。 - 新增个人工作区预览与管理:提供独立于对话 thread 的用户级 workspace API并增加“工作区”页面用于浏览个人 workspace 文件、预览 Markdown/文本/代码/图片/PDF支持新建文件夹、上传文件、下载文件、删除文件/文件夹和多选删除;工作区预览支持 Markdown/TXT 在右侧预览框内切换编辑并保存,其他格式和非工作区预览默认只读;知识库与团队空间入口先展示到占位层级;默认创建 `agents/AGENTS.md`,并在 Agent 执行时将其内容追加到系统提示词。
- 加固 JWT 鉴权安全:移除历史默认密钥回退,初始化脚本支持生成并持久化 `JWT_SECRET_KEY``YUXI_INSTANCE_ID`,签发和验证令牌时校验 `iss/aud`,并在鉴权阶段拒绝已删除或登录锁定用户继续使用旧令牌访问系统。 - 加固 JWT 鉴权安全:移除历史默认密钥回退,初始化脚本支持生成并持久化 `JWT_SECRET_KEY``YUXI_INSTANCE_ID`,签发和验证令牌时校验 `iss/aud`,并在鉴权阶段拒绝已删除或登录锁定用户继续使用旧令牌访问系统。
- 扩展管理界面交互逻辑重构:将 MCP / Subagents / Skills 三个标签页从「左侧边栏 + 右侧详情面板」布局重构为「卡片式网格布局 + 路由跳转二级页面」布局,工具标签页改为卡片网格布局 + 弹窗详情(保持弹窗内容不变)。新增共享组件 `ExtensionCard`、`ExtensionCardGrid`、`ExtensionToolbar`、`ExtensionDetailLayout`,详情页(`McpDetailView`、`SubagentDetailView`、`SkillDetailView`)使用居中宽度限制,路由规划为 `/extensions/mcp/:name`、`/extensions/subagent/:name`、`/extensions/skill/:slug`。 - 扩展管理界面交互逻辑重构:将 MCP / Subagents / Skills 三个标签页从「左侧边栏 + 右侧详情面板」布局重构为「卡片式网格布局 + 路由跳转二级页面」布局,工具标签页改为卡片网格布局 + 弹窗详情(保持弹窗内容不变)。新增共享组件 `ExtensionCard`、`ExtensionCardGrid`、`ExtensionToolbar`、`ExtensionDetailLayout`,详情页(`McpDetailView`、`SubagentDetailView`、`SkillDetailView`)使用居中宽度限制,路由规划为 `/extensions/mcp/:name`、`/extensions/subagent/:name`、`/extensions/skill/:slug`。