Skip to content

Commit 0e44cdb

Browse files
committed
feat(知识库): 实现文件夹功能支持
添加文件夹创建、移动和删除功能 - 后端添加文件夹相关API和逻辑处理 - 前端实现文件夹树形展示和操作界面 - 支持文件上传到指定文件夹 - 添加拖拽移动文件功能
1 parent 34fbd1a commit 0e44cdb

File tree

9 files changed

+717
-45
lines changed

9 files changed

+717
-45
lines changed

server/routers/knowledge_router.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,17 @@ async def get_document_content(db_id: str, doc_id: str, current_user: User = Dep
394394

395395
@knowledge.delete("/databases/{db_id}/documents/{doc_id}")
396396
async def delete_document(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
397-
"""删除文档"""
397+
"""删除文档或文件夹"""
398398
logger.debug(f"DELETE document {doc_id} info in {db_id}")
399399
try:
400400
file_meta_info = await knowledge_base.get_file_basic_info(db_id, doc_id)
401+
402+
# Check if it is a folder
403+
is_folder = file_meta_info.get("meta", {}).get("is_folder", False)
404+
if is_folder:
405+
await knowledge_base.delete_folder(db_id, doc_id)
406+
return {"message": "文件夹删除成功"}
407+
401408
file_name = file_meta_info.get("meta", {}).get("filename")
402409

403410
# 尝试从MinIO删除文件,如果失败(例如旧知识库没有MinIO实例),则忽略
@@ -1150,6 +1157,38 @@ async def get_sample_questions(db_id: str, current_user: User = Depends(get_admi
11501157
# =============================================================================
11511158

11521159

1160+
@knowledge.post("/databases/{db_id}/folders")
1161+
async def create_folder(
1162+
db_id: str,
1163+
folder_name: str = Body(..., embed=True),
1164+
parent_id: str | None = Body(None, embed=True),
1165+
current_user: User = Depends(get_admin_user)
1166+
):
1167+
"""创建文件夹"""
1168+
try:
1169+
return await knowledge_base.create_folder(db_id, folder_name, parent_id)
1170+
except Exception as e:
1171+
logger.error(f"创建文件夹失败 {e}, {traceback.format_exc()}")
1172+
raise HTTPException(status_code=500, detail=str(e))
1173+
1174+
@knowledge.put("/databases/{db_id}/documents/{doc_id}/move")
1175+
async def move_document(
1176+
db_id: str,
1177+
doc_id: str,
1178+
new_parent_id: str | None = Body(..., embed=True),
1179+
current_user: User = Depends(get_admin_user)
1180+
):
1181+
"""移动文件或文件夹"""
1182+
logger.debug(f"Move document {doc_id} to {new_parent_id} in {db_id}")
1183+
try:
1184+
return await knowledge_base.move_file(db_id, doc_id, new_parent_id)
1185+
except ValueError as e:
1186+
raise HTTPException(status_code=400, detail=str(e))
1187+
except Exception as e:
1188+
logger.error(f"移动文件失败 {e}, {traceback.format_exc()}")
1189+
raise HTTPException(status_code=500, detail=str(e))
1190+
1191+
11531192
@knowledge.post("/files/upload")
11541193
async def upload_file(
11551194
file: UploadFile = File(...),

src/knowledge/base.py

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,25 @@ def delete_database(self, db_id: str) -> dict:
210210

211211
return {"message": "删除成功"}
212212

213+
def create_folder(self, db_id: str, folder_name: str, parent_id: str | None = None) -> dict:
214+
"""Create a folder in the database."""
215+
import uuid
216+
folder_id = f"folder-{uuid.uuid4()}"
217+
218+
self.files_meta[folder_id] = {
219+
"file_id": folder_id,
220+
"filename": folder_name,
221+
"is_folder": True,
222+
"parent_id": parent_id,
223+
"database_id": db_id,
224+
"created_at": utc_isoformat(),
225+
"status": "done",
226+
"path": folder_name,
227+
"file_type": "folder"
228+
}
229+
self._save_metadata()
230+
return self.files_meta[folder_id]
231+
213232
@abstractmethod
214233
async def add_content(self, db_id: str, items: list[str], params: dict | None = None) -> list[dict]:
215234
"""
@@ -300,14 +319,16 @@ def get_database_info(self, db_id: str) -> dict | None:
300319
if file_info.get("database_id") == db_id:
301320
created_at = self._normalize_timestamp(file_info.get("created_at"))
302321
db_files[file_id] = {
303-
"file_id": file_id,
304-
"filename": file_info.get("filename", ""),
305-
"path": file_info.get("path", ""),
306-
"type": file_info.get("file_type", ""),
307-
"status": file_info.get("status", "done"),
308-
"created_at": created_at,
309-
"processing_params": file_info.get("processing_params", None),
310-
}
322+
"file_id": file_id,
323+
"filename": file_info.get("filename", ""),
324+
"path": file_info.get("path", ""),
325+
"type": file_info.get("file_type", ""),
326+
"status": file_info.get("status", "done"),
327+
"created_at": created_at,
328+
"processing_params": file_info.get("processing_params", None),
329+
"is_folder": file_info.get("is_folder", False),
330+
"parent_id": file_info.get("parent_id", None),
331+
}
311332

312333
# 按创建时间倒序排序文件列表
313334
sorted_files = dict(
@@ -350,6 +371,8 @@ def get_databases(self) -> dict:
350371
"type": file_info.get("file_type", ""),
351372
"status": file_info.get("status", "done"),
352373
"created_at": created_at,
374+
"is_folder": file_info.get("is_folder", False),
375+
"parent_id": file_info.get("parent_id", None),
353376
}
354377

355378
# 按创建时间倒序排序文件列表
@@ -439,6 +462,71 @@ def _check_and_fix_processing_status(self, db_id: str) -> None:
439462
except Exception as e:
440463
logger.error(f"Error checking processing status for database {db_id}: {e}")
441464

465+
async def delete_folder(self, db_id: str, folder_id: str) -> None:
466+
"""
467+
Recursively delete a folder and its content.
468+
469+
Args:
470+
db_id: Database ID
471+
folder_id: Folder ID to delete
472+
"""
473+
# Find all children
474+
children = [
475+
fid for fid, meta in self.files_meta.items()
476+
if meta.get("database_id") == db_id and meta.get("parent_id") == folder_id
477+
]
478+
479+
for child_id in children:
480+
child_meta = self.files_meta.get(child_id)
481+
if child_meta and child_meta.get("is_folder"):
482+
await self.delete_folder(db_id, child_id)
483+
else:
484+
await self.delete_file(db_id, child_id)
485+
486+
# Delete the folder itself
487+
# We call delete_file which should handle the actual removal.
488+
# Implementations should ensure they handle folder deletion gracefully (e.g. skip vector deletion)
489+
await self.delete_file(db_id, folder_id)
490+
491+
async def move_file(self, db_id: str, file_id: str, new_parent_id: str | None) -> dict:
492+
"""
493+
Move a file or folder to a new parent folder.
494+
495+
Args:
496+
db_id: Database ID
497+
file_id: File/Folder ID to move
498+
new_parent_id: New parent folder ID (None for root)
499+
500+
Returns:
501+
dict: Updated metadata
502+
"""
503+
if file_id not in self.files_meta:
504+
raise ValueError(f"File {file_id} not found")
505+
506+
meta = self.files_meta[file_id]
507+
if meta.get("database_id") != db_id:
508+
raise ValueError(f"File {file_id} does not belong to database {db_id}")
509+
510+
# Basic cycle detection for folders
511+
if meta.get("is_folder") and new_parent_id:
512+
# Check if new_parent_id is a child of file_id (or is file_id itself)
513+
if new_parent_id == file_id:
514+
raise ValueError("Cannot move a folder into itself")
515+
516+
# Walk up the tree from new_parent_id
517+
current = new_parent_id
518+
while current:
519+
parent_meta = self.files_meta.get(current)
520+
if not parent_meta:
521+
break # Should not happen if integrity is maintained
522+
if current == file_id:
523+
raise ValueError("Cannot move a folder into its own subfolder")
524+
current = parent_meta.get("parent_id")
525+
526+
meta["parent_id"] = new_parent_id
527+
self._save_metadata()
528+
return meta
529+
442530
@abstractmethod
443531
async def delete_file(self, db_id: str, file_id: str) -> None:
444532
"""

src/knowledge/manager.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ def _get_or_create_kb_instance(self, kb_type: str) -> KnowledgeBase:
174174
logger.info(f"Created {kb_type} knowledge base instance")
175175
return kb_instance
176176

177+
async def move_file(self, db_id: str, file_id: str, new_parent_id: str | None) -> dict:
178+
"""
179+
移动文件/文件夹
180+
"""
181+
kb_instance = self._get_kb_for_database(db_id)
182+
return await kb_instance.move_file(db_id, file_id, new_parent_id)
183+
177184
def _get_kb_for_database(self, db_id: str) -> KnowledgeBase:
178185
"""
179186
根据数据库ID获取对应的知识库实例
@@ -220,8 +227,13 @@ def get_databases(self) -> dict:
220227

221228
return {"databases": all_databases}
222229

230+
async def create_folder(self, db_id: str, folder_name: str, parent_id: str = None) -> dict:
231+
"""Create a folder in the database."""
232+
kb_instance = self._get_kb_for_database(db_id)
233+
return kb_instance.create_folder(db_id, folder_name, parent_id)
234+
223235
async def create_database(
224-
self, database_name: str, description: str, kb_type: str, embed_info: dict | None = None, **kwargs
236+
self, database_name: str, description: str, kb_type: str = "lightrag", embed_info: dict | None = None, **kwargs
225237
) -> dict:
226238
"""
227239
创建数据库
@@ -315,6 +327,11 @@ def get_database_info(self, db_id: str) -> dict | None:
315327
except KBNotFoundError:
316328
return None
317329

330+
async def delete_folder(self, db_id: str, folder_id: str) -> None:
331+
"""递归删除文件夹"""
332+
kb_instance = self._get_kb_for_database(db_id)
333+
await kb_instance.delete_folder(db_id, folder_id)
334+
318335
async def delete_file(self, db_id: str, file_id: str) -> None:
319336
"""删除文件"""
320337
kb_instance = self._get_kb_for_database(db_id)

src/knowledge/utils/kb_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ async def prepare_item_metadata(item: str, content_type: str, db_id: str, params
213213
"created_at": utc_isoformat(),
214214
"file_id": file_id,
215215
"content_hash": content_hash,
216+
"parent_id": params.get("parent_id") if params else None,
216217
}
217218

218219
# 保存处理参数到元数据

web/src/apis/knowledge_api.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,34 @@ export const databaseApi = {
7474
// =============================================================================
7575

7676
export const documentApi = {
77+
/**
78+
* 创建文件夹
79+
* @param {string} dbId - 知识库ID
80+
* @param {string} folderName - 文件夹名称
81+
* @param {string} parentId - 父文件夹ID
82+
* @returns {Promise} - 创建结果
83+
*/
84+
createFolder: async (dbId, folderName, parentId = null) => {
85+
return apiAdminPost(`/api/knowledge/databases/${dbId}/folders`, {
86+
folder_name: folderName,
87+
parent_id: parentId
88+
})
89+
},
90+
91+
/**
92+
* 移动文档/文件夹
93+
* @param {string} dbId - 知识库ID
94+
* @param {string} docId - 文档/文件夹ID
95+
* @param {string} newParentId - 新的父文件夹ID
96+
* @returns {Promise} - 移动结果
97+
*/
98+
moveDocument: async (dbId, docId, newParentId) => {
99+
return apiAdminPut(`/api/knowledge/databases/${dbId}/documents/${docId}/move`, {
100+
new_parent_id: newParentId
101+
})
102+
},
103+
104+
77105
/**
78106
* 添加文档到知识库
79107
* @param {string} dbId - 知识库ID

0 commit comments

Comments
 (0)