diff --git a/server/routers/knowledge_router.py b/server/routers/knowledge_router.py index 3c8973d3..2b06a966 100644 --- a/server/routers/knowledge_router.py +++ b/server/routers/knowledge_router.py @@ -659,6 +659,55 @@ async def get_document_content(db_id: str, doc_id: str, current_user: User = Dep return {"message": "Failed to get file content", "status": "failed"} +@knowledge.delete("/databases/{db_id}/documents/batch") +async def batch_delete_documents(db_id: str, file_ids: list[str] = Body(...), current_user: User = Depends(get_admin_user)): + """批量删除文档或文件夹""" + logger.debug(f"BATCH DELETE documents {file_ids} in {db_id}") + await _ensure_database_not_dify(db_id, "批量文档删除") + + deleted_count = 0 + failed_items = [] + + for doc_id in file_ids: + 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) + deleted_count += 1 + continue + + file_name = file_meta_info.get("meta", {}).get("filename") + + # 尝试从MinIO删除文件,如果失败(例如旧知识库没有MinIO实例),则忽略 + try: + minio_client = get_minio_client() + await minio_client.adelete_file("ref-" + db_id.replace("_", "-"), file_name) + logger.debug(f"成功从MinIO删除文件: {file_name}") + except Exception as minio_error: + logger.warning(f"从MinIO删除文件失败(可能是旧知识库): {minio_error}") + + # 无论MinIO删除是否成功,都继续从知识库删除 + await knowledge_base.delete_file(db_id, doc_id) + deleted_count += 1 + except Exception as e: + logger.error(f"批量删除过程中删除文档 {doc_id} 失败: {e}, {traceback.format_exc()}") + failed_items.append({"doc_id": doc_id, "error": str(e)}) + + if failed_items: + if deleted_count == 0: + raise HTTPException(status_code=400, detail=f"批量删除失败: 所有 {len(failed_items)} 个文件均未删除。") + return { + "message": f"部分删除成功: 已删除 {deleted_count} 个文件,失败 {len(failed_items)} 个", + "deleted_count": deleted_count, + "failed_items": failed_items + } + + return {"message": f"批量删除成功: 已删除 {deleted_count} 个文件", "deleted_count": deleted_count} + + @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)): """删除文档或文件夹""" diff --git a/web/src/apis/knowledge_api.js b/web/src/apis/knowledge_api.js index 7e78794f..f835e2a4 100644 --- a/web/src/apis/knowledge_api.js +++ b/web/src/apis/knowledge_api.js @@ -145,6 +145,22 @@ export const documentApi = { return apiAdminDelete(`/api/knowledge/databases/${dbId}/documents/${docId}`) }, + /** + * 批量删除文档 + * @param {string} dbId - 知识库ID + * @param {Array} fileIds - 文件ID列表 + * @returns {Promise} - 批量删除结果 + */ + batchDeleteDocuments: async (dbId, fileIds) => { + return apiRequest(`/api/knowledge/databases/${dbId}/documents/batch`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(fileIds) + }, true, 'json') + }, + /** * 下载文档 * @param {string} dbId - 知识库ID diff --git a/web/src/stores/database.js b/web/src/stores/database.js index bc359b66..c6dbe44d 100644 --- a/web/src/stores/database.js +++ b/web/src/stores/database.js @@ -208,24 +208,36 @@ export const useDatabaseStore = defineStore('database', () => { state.batchDeleting = true let successCount = 0 let failureCount = 0 - let progressMessage = message.loading(`正在删除文件 0/${validFileIds.length}`, 0) + let processedCount = 0 + const totalCount = validFileIds.length + const progressKey = `batch-delete-${Date.now()}` + message.loading({ content: `正在删除文件 0/${totalCount}`, key: progressKey, duration: 0 }) try { - for (let i = 0; i < validFileIds.length; i++) { - const fileId = validFileIds[i] + const CHUNK_SIZE = 50 + for (let i = 0; i < totalCount; i += CHUNK_SIZE) { + const chunk = validFileIds.slice(i, i + CHUNK_SIZE) + try { - await deleteFile(fileId) - successCount++ - } catch (error) { - console.error(`删除文件 ${fileId} 失败:`, error) - failureCount++ - } - progressMessage?.() - if (i + 1 < validFileIds.length) { - progressMessage = message.loading(`正在删除文件 ${i + 1}/${validFileIds.length}`, 0) + const res = await documentApi.batchDeleteDocuments(databaseId.value, chunk) + successCount += res.deleted_count || 0 + if (res.failed_items) { + failureCount += res.failed_items.length + } + } catch (err) { + console.error(`删除批次 ${i / CHUNK_SIZE + 1} 失败:`, err) + failureCount += chunk.length + } finally { + processedCount += chunk.length + message.loading({ + content: `正在删除文件 ${processedCount}/${totalCount}`, + key: progressKey, + duration: 0 + }) } } - progressMessage?.() + + message.destroy(progressKey) if (successCount > 0 && failureCount === 0) { message.success(`成功删除 ${successCount} 个文件`) } else if (successCount > 0 && failureCount > 0) { @@ -233,12 +245,13 @@ export const useDatabaseStore = defineStore('database', () => { } else if (failureCount > 0) { message.error(`${failureCount} 个文件删除失败`) } + selectedRowKeys.value = [] await getDatabaseInfo(undefined, true) // Skip query params for batch deletion } catch (error) { - progressMessage?.() + message.destroy(progressKey) console.error('批量删除出错:', error) - message.error('批量删除过程中发生错误') + message.error(error.message || '批量删除过程中发生错误') } finally { state.batchDeleting = false }