Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion server/routers/knowledge_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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实例),则忽略
Expand Down Expand Up @@ -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(...),
Expand Down
90 changes: 90 additions & 0 deletions src/knowledge/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
"""
Expand Down Expand Up @@ -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),
}

# 按创建时间倒序排序文件列表
Expand Down Expand Up @@ -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),
}

# 按创建时间倒序排序文件列表
Expand Down Expand Up @@ -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:
"""
Expand Down
19 changes: 18 additions & 1 deletion src/knowledge/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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获取对应的知识库实例
Expand Down Expand Up @@ -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:
"""
创建数据库
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/knowledge/utils/kb_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

# 保存处理参数到元数据
Expand Down
28 changes: 28 additions & 0 deletions web/src/apis/knowledge_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading