diff --git a/server/routers/knowledge_router.py b/server/routers/knowledge_router.py
index 79ae3605..bfa65056 100644
--- a/server/routers/knowledge_router.py
+++ b/server/routers/knowledge_router.py
@@ -394,10 +394,17 @@ async def get_document_content(db_id: str, doc_id: str, current_user: User = Dep
@knowledge.delete("/databases/{db_id}/documents/{doc_id}")
async def delete_document(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
- """删除文档"""
+ """删除文档或文件夹"""
logger.debug(f"DELETE document {doc_id} info in {db_id}")
try:
file_meta_info = await knowledge_base.get_file_basic_info(db_id, doc_id)
+
+ # Check if it is a folder
+ is_folder = file_meta_info.get("meta", {}).get("is_folder", False)
+ if is_folder:
+ await knowledge_base.delete_folder(db_id, doc_id)
+ return {"message": "文件夹删除成功"}
+
file_name = file_meta_info.get("meta", {}).get("filename")
# 尝试从MinIO删除文件,如果失败(例如旧知识库没有MinIO实例),则忽略
@@ -1150,6 +1157,39 @@ async def get_sample_questions(db_id: str, current_user: User = Depends(get_admi
# =============================================================================
+@knowledge.post("/databases/{db_id}/folders")
+async def create_folder(
+ db_id: str,
+ folder_name: str = Body(..., embed=True),
+ parent_id: str | None = Body(None, embed=True),
+ current_user: User = Depends(get_admin_user),
+):
+ """创建文件夹"""
+ try:
+ return await knowledge_base.create_folder(db_id, folder_name, parent_id)
+ except Exception as e:
+ logger.error(f"创建文件夹失败 {e}, {traceback.format_exc()}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@knowledge.put("/databases/{db_id}/documents/{doc_id}/move")
+async def move_document(
+ db_id: str,
+ doc_id: str,
+ new_parent_id: str | None = Body(..., embed=True),
+ current_user: User = Depends(get_admin_user),
+):
+ """移动文件或文件夹"""
+ logger.debug(f"Move document {doc_id} to {new_parent_id} in {db_id}")
+ try:
+ return await knowledge_base.move_file(db_id, doc_id, new_parent_id)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ logger.error(f"移动文件失败 {e}, {traceback.format_exc()}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
@knowledge.post("/files/upload")
async def upload_file(
file: UploadFile = File(...),
diff --git a/src/knowledge/base.py b/src/knowledge/base.py
index 0c950ca7..7d03ffa2 100644
--- a/src/knowledge/base.py
+++ b/src/knowledge/base.py
@@ -210,6 +210,26 @@ def delete_database(self, db_id: str) -> dict:
return {"message": "删除成功"}
+ def create_folder(self, db_id: str, folder_name: str, parent_id: str | None = None) -> dict:
+ """Create a folder in the database."""
+ import uuid
+
+ folder_id = f"folder-{uuid.uuid4()}"
+
+ self.files_meta[folder_id] = {
+ "file_id": folder_id,
+ "filename": folder_name,
+ "is_folder": True,
+ "parent_id": parent_id,
+ "database_id": db_id,
+ "created_at": utc_isoformat(),
+ "status": "done",
+ "path": folder_name,
+ "file_type": "folder",
+ }
+ self._save_metadata()
+ return self.files_meta[folder_id]
+
@abstractmethod
async def add_content(self, db_id: str, items: list[str], params: dict | None = None) -> list[dict]:
"""
@@ -307,6 +327,8 @@ def get_database_info(self, db_id: str) -> dict | None:
"status": file_info.get("status", "done"),
"created_at": created_at,
"processing_params": file_info.get("processing_params", None),
+ "is_folder": file_info.get("is_folder", False),
+ "parent_id": file_info.get("parent_id", None),
}
# 按创建时间倒序排序文件列表
@@ -350,6 +372,8 @@ def get_databases(self) -> dict:
"type": file_info.get("file_type", ""),
"status": file_info.get("status", "done"),
"created_at": created_at,
+ "is_folder": file_info.get("is_folder", False),
+ "parent_id": file_info.get("parent_id", None),
}
# 按创建时间倒序排序文件列表
@@ -439,6 +463,72 @@ def _check_and_fix_processing_status(self, db_id: str) -> None:
except Exception as e:
logger.error(f"Error checking processing status for database {db_id}: {e}")
+ async def delete_folder(self, db_id: str, folder_id: str) -> None:
+ """
+ Recursively delete a folder and its content.
+
+ Args:
+ db_id: Database ID
+ folder_id: Folder ID to delete
+ """
+ # Find all children
+ children = [
+ fid
+ for fid, meta in self.files_meta.items()
+ if meta.get("database_id") == db_id and meta.get("parent_id") == folder_id
+ ]
+
+ for child_id in children:
+ child_meta = self.files_meta.get(child_id)
+ if child_meta and child_meta.get("is_folder"):
+ await self.delete_folder(db_id, child_id)
+ else:
+ await self.delete_file(db_id, child_id)
+
+ # Delete the folder itself
+ # We call delete_file which should handle the actual removal.
+ # Implementations should ensure they handle folder deletion gracefully (e.g. skip vector deletion)
+ await self.delete_file(db_id, folder_id)
+
+ async def move_file(self, db_id: str, file_id: str, new_parent_id: str | None) -> dict:
+ """
+ Move a file or folder to a new parent folder.
+
+ Args:
+ db_id: Database ID
+ file_id: File/Folder ID to move
+ new_parent_id: New parent folder ID (None for root)
+
+ Returns:
+ dict: Updated metadata
+ """
+ if file_id not in self.files_meta:
+ raise ValueError(f"File {file_id} not found")
+
+ meta = self.files_meta[file_id]
+ if meta.get("database_id") != db_id:
+ raise ValueError(f"File {file_id} does not belong to database {db_id}")
+
+ # Basic cycle detection for folders
+ if meta.get("is_folder") and new_parent_id:
+ # Check if new_parent_id is a child of file_id (or is file_id itself)
+ if new_parent_id == file_id:
+ raise ValueError("Cannot move a folder into itself")
+
+ # Walk up the tree from new_parent_id
+ current = new_parent_id
+ while current:
+ parent_meta = self.files_meta.get(current)
+ if not parent_meta:
+ break # Should not happen if integrity is maintained
+ if current == file_id:
+ raise ValueError("Cannot move a folder into its own subfolder")
+ current = parent_meta.get("parent_id")
+
+ meta["parent_id"] = new_parent_id
+ self._save_metadata()
+ return meta
+
@abstractmethod
async def delete_file(self, db_id: str, file_id: str) -> None:
"""
diff --git a/src/knowledge/manager.py b/src/knowledge/manager.py
index f19ed6c0..a409b57d 100644
--- a/src/knowledge/manager.py
+++ b/src/knowledge/manager.py
@@ -174,6 +174,13 @@ def _get_or_create_kb_instance(self, kb_type: str) -> KnowledgeBase:
logger.info(f"Created {kb_type} knowledge base instance")
return kb_instance
+ async def move_file(self, db_id: str, file_id: str, new_parent_id: str | None) -> dict:
+ """
+ 移动文件/文件夹
+ """
+ kb_instance = self._get_kb_for_database(db_id)
+ return await kb_instance.move_file(db_id, file_id, new_parent_id)
+
def _get_kb_for_database(self, db_id: str) -> KnowledgeBase:
"""
根据数据库ID获取对应的知识库实例
@@ -220,8 +227,13 @@ def get_databases(self) -> dict:
return {"databases": all_databases}
+ async def create_folder(self, db_id: str, folder_name: str, parent_id: str = None) -> dict:
+ """Create a folder in the database."""
+ kb_instance = self._get_kb_for_database(db_id)
+ return kb_instance.create_folder(db_id, folder_name, parent_id)
+
async def create_database(
- self, database_name: str, description: str, kb_type: str, embed_info: dict | None = None, **kwargs
+ self, database_name: str, description: str, kb_type: str = "lightrag", embed_info: dict | None = None, **kwargs
) -> dict:
"""
创建数据库
@@ -315,6 +327,11 @@ def get_database_info(self, db_id: str) -> dict | None:
except KBNotFoundError:
return None
+ async def delete_folder(self, db_id: str, folder_id: str) -> None:
+ """递归删除文件夹"""
+ kb_instance = self._get_kb_for_database(db_id)
+ await kb_instance.delete_folder(db_id, folder_id)
+
async def delete_file(self, db_id: str, file_id: str) -> None:
"""删除文件"""
kb_instance = self._get_kb_for_database(db_id)
diff --git a/src/knowledge/utils/kb_utils.py b/src/knowledge/utils/kb_utils.py
index 569ff62d..c4cd7935 100644
--- a/src/knowledge/utils/kb_utils.py
+++ b/src/knowledge/utils/kb_utils.py
@@ -213,6 +213,7 @@ async def prepare_item_metadata(item: str, content_type: str, db_id: str, params
"created_at": utc_isoformat(),
"file_id": file_id,
"content_hash": content_hash,
+ "parent_id": params.get("parent_id") if params else None,
}
# 保存处理参数到元数据
diff --git a/web/src/apis/knowledge_api.js b/web/src/apis/knowledge_api.js
index aa3fa638..df074946 100644
--- a/web/src/apis/knowledge_api.js
+++ b/web/src/apis/knowledge_api.js
@@ -74,6 +74,34 @@ export const databaseApi = {
// =============================================================================
export const documentApi = {
+ /**
+ * 创建文件夹
+ * @param {string} dbId - 知识库ID
+ * @param {string} folderName - 文件夹名称
+ * @param {string} parentId - 父文件夹ID
+ * @returns {Promise} - 创建结果
+ */
+ createFolder: async (dbId, folderName, parentId = null) => {
+ return apiAdminPost(`/api/knowledge/databases/${dbId}/folders`, {
+ folder_name: folderName,
+ parent_id: parentId
+ })
+ },
+
+ /**
+ * 移动文档/文件夹
+ * @param {string} dbId - 知识库ID
+ * @param {string} docId - 文档/文件夹ID
+ * @param {string} newParentId - 新的父文件夹ID
+ * @returns {Promise} - 移动结果
+ */
+ moveDocument: async (dbId, docId, newParentId) => {
+ return apiAdminPut(`/api/knowledge/databases/${dbId}/documents/${docId}/move`, {
+ new_parent_id: newParentId
+ })
+ },
+
+
/**
* 添加文档到知识库
* @param {string} dbId - 知识库ID
diff --git a/web/src/components/FileTable.vue b/web/src/components/FileTable.vue
index f2621f9c..e1e8043c 100644
--- a/web/src/components/FileTable.vue
+++ b/web/src/components/FileTable.vue
@@ -8,6 +8,13 @@
:loading="refreshing"
:icon="h(PlusOutlined)"
>添加文件
+