Просмотр исходного кода

feat: add batch delete functionality and increase page size to 200

guantao 3 часов назад
Родитель
Сommit
aa739a7268
1 измененных файлов с 146 добавлено и 16 удалено
  1. 146 16
      knowhub/server.py

+ 146 - 16
knowhub/server.py

@@ -15,7 +15,7 @@ import time
 import uuid
 from contextlib import asynccontextmanager
 from datetime import datetime, timezone
-from typing import Optional
+from typing import Optional, List
 from pathlib import Path
 from cryptography.hazmat.primitives.ciphers.aead import AESGCM
 
@@ -978,6 +978,52 @@ async def patch_knowledge(knowledge_id: str, patch: KnowledgePatchIn):
         raise HTTPException(status_code=500, detail=str(e))
 
 
+@app.delete("/api/knowledge/{knowledge_id}")
+def delete_knowledge(knowledge_id: str):
+    """删除单条知识"""
+    try:
+        # 检查知识是否存在
+        existing = milvus_store.get_by_id(knowledge_id)
+        if not existing:
+            raise HTTPException(status_code=404, detail=f"Knowledge not found: {knowledge_id}")
+
+        # 从 Milvus 删除
+        milvus_store.collection.delete(expr=f'id == "{knowledge_id}"')
+        print(f"[Delete Knowledge] 已删除知识: {knowledge_id}")
+
+        return {"status": "ok", "knowledge_id": knowledge_id}
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        print(f"[Delete Knowledge] 错误: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.post("/api/knowledge/batch_delete")
+def batch_delete_knowledge(knowledge_ids: List[str]):
+    """批量删除知识"""
+    try:
+        if not knowledge_ids:
+            raise HTTPException(status_code=400, detail="knowledge_ids cannot be empty")
+
+        # 构建删除表达式
+        ids_str = '", "'.join(knowledge_ids)
+        expr = f'id in ["{ids_str}"]'
+
+        # 批量删除
+        milvus_store.collection.delete(expr=expr)
+        print(f"[Batch Delete] 已删除 {len(knowledge_ids)} 条知识")
+
+        return {"status": "ok", "deleted_count": len(knowledge_ids)}
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        print(f"[Batch Delete] 错误: {e}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+
 @app.post("/api/knowledge/batch_update")
 async def batch_update_knowledge(batch: KnowledgeBatchUpdateIn):
     """批量反馈知识有效性"""
@@ -1411,9 +1457,17 @@ def frontend():
     <div class="container mx-auto px-4 py-8 max-w-7xl">
         <div class="flex justify-between items-center mb-8">
             <h1 class="text-3xl font-bold text-gray-800">KnowHub 全局知识库</h1>
-            <button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
-                + 新增知识
-            </button>
+            <div class="flex gap-3">
+                <button onclick="toggleSelectAll()" id="selectAllBtn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg">
+                    全选
+                </button>
+                <button onclick="batchDelete()" id="batchDeleteBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled>
+                    删除选中 (<span id="selectedCount">0</span>)
+                </button>
+                <button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
+                    + 新增知识
+                </button>
+            </div>
         </div>
 
         <!-- 搜索栏 -->
@@ -1530,10 +1584,11 @@ def frontend():
         let allKnowledge = [];
         let availableTags = [];
         let currentPage = 1;
-        let pageSize = 20;
+        let pageSize = 200;  // 每页显示200条
         let totalPages = 1;
         let totalCount = 0;
         let isSearchMode = false;  // 标记是否在搜索模式
+        let selectedIds = new Set();  // 选中的知识ID集合
 
         async function loadTags() {
             const res = await fetch('/api/knowledge/meta/tags');
@@ -1709,26 +1764,101 @@ def frontend():
                     }
                 }
                 const eval_data = k.eval || {};
+                const isChecked = selectedIds.has(k.id);
 
                 return `
-                <div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition cursor-pointer" onclick="openEditModal('${k.id}')">
-                    <div class="flex justify-between items-start mb-2">
-                        <div class="flex gap-2 flex-wrap">
-                            ${types.map(t => `<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">${t}</span>`).join('')}
-                        </div>
-                        <span class="text-sm text-gray-500">${eval_data.score || 3}/5</span>
+                <div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition relative">
+                    <div class="absolute top-4 left-4">
+                        <input type="checkbox" class="knowledge-checkbox w-5 h-5 cursor-pointer"
+                               data-id="${k.id}" ${isChecked ? 'checked' : ''}
+                               onclick="event.stopPropagation(); toggleSelect('${k.id}')">
                     </div>
-                    <h3 class="text-lg font-semibold text-gray-800 mb-2">${escapeHtml(k.task)}</h3>
-                    <p class="text-sm text-gray-600 mb-2">${escapeHtml(k.content.substring(0, 150))}${k.content.length > 150 ? '...' : ''}</p>
-                    <div class="flex justify-between text-xs text-gray-500">
-                        <span>Owner: ${k.owner || 'N/A'}</span>
-                        <span>${new Date(k.created_at).toLocaleDateString()}</span>
+                    <div class="ml-10 cursor-pointer" onclick="openEditModal('${k.id}')">
+                        <div class="flex justify-between items-start mb-2">
+                            <div class="flex gap-2 flex-wrap">
+                                ${types.map(t => `<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">${t}</span>`).join('')}
+                            </div>
+                            <span class="text-sm text-gray-500">${eval_data.score || 3}/5</span>
+                        </div>
+                        <h3 class="text-lg font-semibold text-gray-800 mb-2">${escapeHtml(k.task)}</h3>
+                        <p class="text-sm text-gray-600 mb-2">${escapeHtml(k.content.substring(0, 150))}${k.content.length > 150 ? '...' : ''}</p>
+                        <div class="flex justify-between text-xs text-gray-500">
+                            <span>Owner: ${k.owner || 'N/A'}</span>
+                            <span>${new Date(k.created_at).toLocaleDateString()}</span>
+                        </div>
                     </div>
                 </div>
             `;
             }).join('');
         }
 
+        function toggleSelect(id) {
+            if (selectedIds.has(id)) {
+                selectedIds.delete(id);
+            } else {
+                selectedIds.add(id);
+            }
+            updateBatchDeleteButton();
+        }
+
+        function toggleSelectAll() {
+            if (selectedIds.size === allKnowledge.length) {
+                // 全部取消选中
+                selectedIds.clear();
+            } else {
+                // 全部选中
+                selectedIds.clear();
+                allKnowledge.forEach(k => selectedIds.add(k.id));
+            }
+            renderKnowledge(allKnowledge);
+            updateBatchDeleteButton();
+        }
+
+        function updateBatchDeleteButton() {
+            const count = selectedIds.size;
+            document.getElementById('selectedCount').textContent = count;
+            document.getElementById('batchDeleteBtn').disabled = count === 0;
+            document.getElementById('selectAllBtn').textContent =
+                selectedIds.size === allKnowledge.length ? '取消全选' : '全选';
+        }
+
+        async function batchDelete() {
+            if (selectedIds.size === 0) return;
+
+            if (!confirm(`确定要删除选中的 ${selectedIds.size} 条知识吗?此操作不可恢复!`)) {
+                return;
+            }
+
+            try {
+                const ids = Array.from(selectedIds);
+                const res = await fetch('/api/knowledge/batch_delete', {
+                    method: 'POST',
+                    headers: {'Content-Type': 'application/json'},
+                    body: JSON.stringify(ids)
+                });
+
+                if (!res.ok) {
+                    throw new Error(`删除失败: ${res.status}`);
+                }
+
+                const data = await res.json();
+                alert(`成功删除 ${data.deleted_count} 条知识`);
+
+                // 清空选择并重新加载
+                selectedIds.clear();
+                updateBatchDeleteButton();
+
+                if (isSearchMode) {
+                    clearSearch();
+                } else {
+                    loadKnowledge(currentPage);
+                }
+            } catch (error) {
+                console.error('批量删除错误:', error);
+                alert('删除失败: ' + error.message);
+            }
+        }
+
         function openAddModal() {
             document.getElementById('modalTitle').textContent = '新增知识';
             document.getElementById('knowledgeForm').reset();