From 6c35fd732a722548b8a77648b1ffa58d9d741ddb Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Mon, 4 May 2026 11:36:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=96=87=E6=98=8E?= =?UTF-8?q?=E5=91=BD=E5=90=8D=EF=BC=8C=E6=9B=B4=E5=8A=A0=E6=B8=85=E6=99=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yuxi/knowledge/utils/mindmap_utils.py | 117 ++++++ backend/server/routers/__init__.py | 10 +- ...apikey_router.py => auth_apikey_router.py} | 0 ...partment_router.py => auth_dept_router.py} | 0 ...ion_router.py => knowledge_eval_router.py} | 0 .../routers/knowledge_mindmap_router.py | 178 +++++++++ backend/server/routers/mindmap_router.py | 361 ------------------ .../{task_router.py => system_task_router.py} | 0 docs/develop-guides/roadmap.md | 1 + 9 files changed, 301 insertions(+), 366 deletions(-) create mode 100644 backend/package/yuxi/knowledge/utils/mindmap_utils.py rename backend/server/routers/{apikey_router.py => auth_apikey_router.py} (100%) rename backend/server/routers/{department_router.py => auth_dept_router.py} (100%) rename backend/server/routers/{evaluation_router.py => knowledge_eval_router.py} (100%) create mode 100644 backend/server/routers/knowledge_mindmap_router.py delete mode 100644 backend/server/routers/mindmap_router.py rename backend/server/routers/{task_router.py => system_task_router.py} (100%) diff --git a/backend/package/yuxi/knowledge/utils/mindmap_utils.py b/backend/package/yuxi/knowledge/utils/mindmap_utils.py new file mode 100644 index 00000000..beee0ca3 --- /dev/null +++ b/backend/package/yuxi/knowledge/utils/mindmap_utils.py @@ -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 diff --git a/backend/server/routers/__init__.py b/backend/server/routers/__init__.py index 0413eea3..fefc6ce8 100644 --- a/backend/server/routers/__init__.py +++ b/backend/server/routers/__init__.py @@ -5,15 +5,15 @@ from fastapi import APIRouter from server.routers.auth_router import auth from server.routers.chat_router import chat 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.model_provider_router import model_providers from server.routers.skill_router import skills from server.routers.subagent_router import subagents_router 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.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.workspace_router import workspace @@ -42,8 +42,8 @@ router.include_router(workspace) # /api/workspace/* 用户个人工作区 if not _LITE_MODE: from server.routers.graph_router import graph from server.routers.knowledge_router import knowledge - from server.routers.evaluation_router import evaluation - from server.routers.mindmap_router import mindmap + from server.routers.knowledge_eval_router import evaluation + from server.routers.knowledge_mindmap_router import mindmap # 知识库与图谱能力依赖较重,LITE 模式下跳过这组接口。 router.include_router(knowledge) # /api/knowledge/* 知识库管理与检索 diff --git a/backend/server/routers/apikey_router.py b/backend/server/routers/auth_apikey_router.py similarity index 100% rename from backend/server/routers/apikey_router.py rename to backend/server/routers/auth_apikey_router.py diff --git a/backend/server/routers/department_router.py b/backend/server/routers/auth_dept_router.py similarity index 100% rename from backend/server/routers/department_router.py rename to backend/server/routers/auth_dept_router.py diff --git a/backend/server/routers/evaluation_router.py b/backend/server/routers/knowledge_eval_router.py similarity index 100% rename from backend/server/routers/evaluation_router.py rename to backend/server/routers/knowledge_eval_router.py diff --git a/backend/server/routers/knowledge_mindmap_router.py b/backend/server/routers/knowledge_mindmap_router.py new file mode 100644 index 00000000..ef85d8cc --- /dev/null +++ b/backend/server/routers/knowledge_mindmap_router.py @@ -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)}") diff --git a/backend/server/routers/mindmap_router.py b/backend/server/routers/mindmap_router.py deleted file mode 100644 index 57f2d2c5..00000000 --- a/backend/server/routers/mindmap_router.py +++ /dev/null @@ -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)}") diff --git a/backend/server/routers/task_router.py b/backend/server/routers/system_task_router.py similarity index 100% rename from backend/server/routers/task_router.py rename to backend/server/routers/system_task_router.py diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index 2fe4e591..e329fc05 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -37,6 +37,7 @@ ### 0.6.2 开发记录 +- 调整知识库思维导图后端结构:将思维导图路由文件重命名为知识库语义更明确的 router,并把文件列表整理、提示词构建、AI JSON 解析等纯逻辑下沉到知识库 utils。 - 新增个人工作区预览与管理:提供独立于对话 thread 的用户级 workspace API,并增加“工作区”页面,用于浏览个人 workspace 文件、预览 Markdown/文本/代码/图片/PDF;支持新建文件夹、上传文件、下载文件、删除文件/文件夹和多选删除;工作区预览支持 Markdown/TXT 在右侧预览框内切换编辑并保存,其他格式和非工作区预览默认只读;知识库与团队空间入口先展示到占位层级;默认创建 `agents/AGENTS.md`,并在 Agent 执行时将其内容追加到系统提示词。 - 加固 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`。