|
@@ -15,7 +15,7 @@ import time
|
|
|
import uuid
|
|
import uuid
|
|
|
from contextlib import asynccontextmanager
|
|
from contextlib import asynccontextmanager
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
-from typing import Optional
|
|
|
|
|
|
|
+from typing import Optional, List
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
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))
|
|
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")
|
|
@app.post("/api/knowledge/batch_update")
|
|
|
async def batch_update_knowledge(batch: KnowledgeBatchUpdateIn):
|
|
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="container mx-auto px-4 py-8 max-w-7xl">
|
|
|
<div class="flex justify-between items-center mb-8">
|
|
<div class="flex justify-between items-center mb-8">
|
|
|
<h1 class="text-3xl font-bold text-gray-800">KnowHub 全局知识库</h1>
|
|
<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>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 搜索栏 -->
|
|
<!-- 搜索栏 -->
|
|
@@ -1530,10 +1584,11 @@ def frontend():
|
|
|
let allKnowledge = [];
|
|
let allKnowledge = [];
|
|
|
let availableTags = [];
|
|
let availableTags = [];
|
|
|
let currentPage = 1;
|
|
let currentPage = 1;
|
|
|
- let pageSize = 20;
|
|
|
|
|
|
|
+ let pageSize = 200; // 每页显示200条
|
|
|
let totalPages = 1;
|
|
let totalPages = 1;
|
|
|
let totalCount = 0;
|
|
let totalCount = 0;
|
|
|
let isSearchMode = false; // 标记是否在搜索模式
|
|
let isSearchMode = false; // 标记是否在搜索模式
|
|
|
|
|
+ let selectedIds = new Set(); // 选中的知识ID集合
|
|
|
|
|
|
|
|
async function loadTags() {
|
|
async function loadTags() {
|
|
|
const res = await fetch('/api/knowledge/meta/tags');
|
|
const res = await fetch('/api/knowledge/meta/tags');
|
|
@@ -1709,26 +1764,101 @@ def frontend():
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
const eval_data = k.eval || {};
|
|
const eval_data = k.eval || {};
|
|
|
|
|
+ const isChecked = selectedIds.has(k.id);
|
|
|
|
|
|
|
|
return `
|
|
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>
|
|
</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>
|
|
|
</div>
|
|
</div>
|
|
|
`;
|
|
`;
|
|
|
}).join('');
|
|
}).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() {
|
|
function openAddModal() {
|
|
|
document.getElementById('modalTitle').textContent = '新增知识';
|
|
document.getElementById('modalTitle').textContent = '新增知识';
|
|
|
document.getElementById('knowledgeForm').reset();
|
|
document.getElementById('knowledgeForm').reset();
|