|
|
@@ -308,6 +308,18 @@ class KnowledgeBatchUpdateIn(BaseModel):
|
|
|
feedback_list: list[dict]
|
|
|
|
|
|
|
|
|
+
|
|
|
+class KnowledgeVerifyIn(BaseModel):
|
|
|
+ action: str # "approve" | "reject"
|
|
|
+ verified_by: str = "user"
|
|
|
+
|
|
|
+
|
|
|
+class KnowledgeBatchVerifyIn(BaseModel):
|
|
|
+ knowledge_ids: List[str]
|
|
|
+ action: str # "approve"
|
|
|
+ verified_by: str
|
|
|
+
|
|
|
+
|
|
|
class KnowledgeSearchResponse(BaseModel):
|
|
|
results: list[dict]
|
|
|
count: int
|
|
|
@@ -542,12 +554,14 @@ class KnowledgeProcessor:
|
|
|
final_decision = "rejected"
|
|
|
|
|
|
if final_decision == "rejected":
|
|
|
- milvus_store.update(kid, {"status": "rejected", "updated_at": now})
|
|
|
+ # 记录 rejected 知识的关系(便于溯源为什么被拒绝)
|
|
|
+ rejected_relationships = []
|
|
|
for rel in relations:
|
|
|
- if rel.get("type") in ("duplicate", "subset"):
|
|
|
- old_id = rel.get("old_id")
|
|
|
- if not old_id:
|
|
|
- continue
|
|
|
+ old_id = rel.get("old_id")
|
|
|
+ rel_type = rel.get("type", "none")
|
|
|
+ if old_id and rel_type != "none":
|
|
|
+ rejected_relationships.append({"type": rel_type, "target": old_id})
|
|
|
+ if rel_type in ("duplicate", "subset") and old_id:
|
|
|
try:
|
|
|
old = milvus_store.get_by_id(old_id)
|
|
|
if not old:
|
|
|
@@ -558,13 +572,14 @@ class KnowledgeProcessor:
|
|
|
helpful_history.append({
|
|
|
"source": "dedup",
|
|
|
"related_id": kid,
|
|
|
- "relation_type": rel["type"],
|
|
|
+ "relation_type": rel_type,
|
|
|
"timestamp": now
|
|
|
})
|
|
|
eval_data["helpful_history"] = helpful_history
|
|
|
milvus_store.update(old_id, {"eval": eval_data, "updated_at": now})
|
|
|
except Exception as e:
|
|
|
print(f"[Apply Decision] 更新旧知识 {old_id} helpful 失败: {e}")
|
|
|
+ milvus_store.update(kid, {"status": "rejected", "relationships": json.dumps(rejected_relationships), "updated_at": now})
|
|
|
else:
|
|
|
new_relationships = []
|
|
|
for rel in relations:
|
|
|
@@ -1408,6 +1423,77 @@ def batch_delete_knowledge(knowledge_ids: List[str] = Body(...)):
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
+@app.post("/api/knowledge/batch_verify")
|
|
|
+async def batch_verify_knowledge(batch: KnowledgeBatchVerifyIn):
|
|
|
+ """批量验证通过(approved → checked)"""
|
|
|
+ if not batch.knowledge_ids:
|
|
|
+ return {"status": "ok", "updated": 0}
|
|
|
+
|
|
|
+ try:
|
|
|
+ now_iso = datetime.now(timezone.utc).isoformat()
|
|
|
+ updated_count = 0
|
|
|
+
|
|
|
+ for kid in batch.knowledge_ids:
|
|
|
+ existing = milvus_store.get_by_id(kid)
|
|
|
+ if not existing:
|
|
|
+ continue
|
|
|
+
|
|
|
+ eval_data = existing.get("eval") or {}
|
|
|
+ eval_data["verification"] = {
|
|
|
+ "status": "checked",
|
|
|
+ "verified_by": batch.verified_by,
|
|
|
+ "verified_at": now_iso,
|
|
|
+ "note": None,
|
|
|
+ "issue_type": None,
|
|
|
+ "issue_action": None,
|
|
|
+ }
|
|
|
+ milvus_store.update(kid, {"eval": eval_data, "status": "checked", "updated_at": int(time.time())})
|
|
|
+ updated_count += 1
|
|
|
+
|
|
|
+ return {"status": "ok", "updated": updated_count}
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"[Batch Verify] 错误: {e}")
|
|
|
+ raise HTTPException(status_code=500, detail=str(e))
|
|
|
+
|
|
|
+
|
|
|
+@app.post("/api/knowledge/{knowledge_id}/verify")
|
|
|
+async def verify_knowledge(knowledge_id: str, verify: KnowledgeVerifyIn):
|
|
|
+ """知识验证:approve 切换 approved↔checked,reject 设为 rejected"""
|
|
|
+ try:
|
|
|
+ existing = milvus_store.get_by_id(knowledge_id)
|
|
|
+ if not existing:
|
|
|
+ raise HTTPException(status_code=404, detail=f"Knowledge not found: {knowledge_id}")
|
|
|
+
|
|
|
+ current_status = existing.get("status", "approved")
|
|
|
+
|
|
|
+ if verify.action == "approve":
|
|
|
+ # checked → approved(取消验证),其他 → checked
|
|
|
+ new_status = "approved" if current_status == "checked" else "checked"
|
|
|
+ milvus_store.update(knowledge_id, {
|
|
|
+ "status": new_status,
|
|
|
+ "updated_at": int(time.time())
|
|
|
+ })
|
|
|
+ return {"status": "ok", "new_status": new_status,
|
|
|
+ "message": "已取消验证" if new_status == "approved" else "验证通过"}
|
|
|
+
|
|
|
+ elif verify.action == "reject":
|
|
|
+ milvus_store.update(knowledge_id, {
|
|
|
+ "status": "rejected",
|
|
|
+ "updated_at": int(time.time())
|
|
|
+ })
|
|
|
+ return {"status": "ok", "new_status": "rejected", "message": "已拒绝"}
|
|
|
+
|
|
|
+ else:
|
|
|
+ raise HTTPException(status_code=400, detail=f"Unknown action: {verify.action}")
|
|
|
+
|
|
|
+ except HTTPException:
|
|
|
+ raise
|
|
|
+ except Exception as e:
|
|
|
+ print(f"[Verify Knowledge] 错误: {e}")
|
|
|
+ raise HTTPException(status_code=500, detail=str(e))
|
|
|
+
|
|
|
+
|
|
|
@app.post("/api/knowledge/batch_update")
|
|
|
async def batch_update_knowledge(batch: KnowledgeBatchUpdateIn):
|
|
|
"""批量反馈知识有效性"""
|
|
|
@@ -1853,6 +1939,9 @@ def frontend():
|
|
|
<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="batchVerify()" id="batchVerifyBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
|
+ ✓ 批量验证通过 (<span id="verifyCount">0</span>)
|
|
|
+ </button>
|
|
|
<button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
|
|
|
+ 新增知识
|
|
|
</button>
|
|
|
@@ -2206,6 +2295,16 @@ def frontend():
|
|
|
<span>${new Date(k.created_at).toLocaleDateString()}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
|
|
|
+ <button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'approve', this)"
|
|
|
+ class="${k.status === 'checked' ? 'bg-gray-300 hover:bg-gray-400 text-gray-700' : 'bg-green-400 hover:bg-green-500 text-white'} text-xs px-3 py-1 rounded transition-colors">
|
|
|
+ ${k.status === 'checked' ? '取消验证' : '✓ 验证通过'}
|
|
|
+ </button>
|
|
|
+ <button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'reject', this)"
|
|
|
+ class="bg-red-400 hover:bg-red-500 text-white text-xs px-3 py-1 rounded transition-colors">
|
|
|
+ ✗ 拒绝
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
@@ -2236,7 +2335,9 @@ def frontend():
|
|
|
function updateBatchDeleteButton() {
|
|
|
const count = selectedIds.size;
|
|
|
document.getElementById('selectedCount').textContent = count;
|
|
|
+ document.getElementById('verifyCount').textContent = count;
|
|
|
document.getElementById('batchDeleteBtn').disabled = count === 0;
|
|
|
+ document.getElementById('batchVerifyBtn').disabled = count === 0;
|
|
|
document.getElementById('selectAllBtn').textContent =
|
|
|
selectedIds.size === allKnowledge.length ? '取消全选' : '全选';
|
|
|
}
|
|
|
@@ -2287,8 +2388,15 @@ def frontend():
|
|
|
}
|
|
|
|
|
|
async function openEditModal(id) {
|
|
|
- const k = allKnowledge.find(item => item.id === id);
|
|
|
- if (!k) return;
|
|
|
+ let k = allKnowledge.find(item => item.id === id);
|
|
|
+ if (!k) {
|
|
|
+ // 当前列表中找不到(可能是 rejected/其他状态),通过 API 单独获取
|
|
|
+ try {
|
|
|
+ const res = await fetch('/api/knowledge/' + encodeURIComponent(id));
|
|
|
+ if (!res.ok) { alert('知识未找到: ' + id); return; }
|
|
|
+ k = await res.json();
|
|
|
+ } catch (e) { alert('获取知识失败: ' + e.message); return; }
|
|
|
+ }
|
|
|
|
|
|
document.getElementById('modalTitle').textContent = '编辑知识';
|
|
|
document.getElementById('editId').value = k.id;
|
|
|
@@ -2308,8 +2416,13 @@ def frontend():
|
|
|
el.checked = types.includes(el.value);
|
|
|
});
|
|
|
|
|
|
- // 填充 relationships
|
|
|
- const rels = Array.isArray(k.relationships) ? k.relationships : [];
|
|
|
+ // 填充 relationships(可能是 JSON 字符串或数组)
|
|
|
+ let rels = [];
|
|
|
+ if (Array.isArray(k.relationships)) {
|
|
|
+ rels = k.relationships;
|
|
|
+ } else if (typeof k.relationships === 'string' && k.relationships.startsWith('[')) {
|
|
|
+ try { rels = JSON.parse(k.relationships); } catch(e) {}
|
|
|
+ }
|
|
|
const section = document.getElementById('relationshipsSection');
|
|
|
if (rels.length > 0) {
|
|
|
const typeColor = {
|
|
|
@@ -2387,6 +2500,61 @@ def frontend():
|
|
|
await loadKnowledge();
|
|
|
});
|
|
|
|
|
|
+ async function verifyKnowledge(id, action, btn) {
|
|
|
+ if (btn) {
|
|
|
+ btn.disabled = true;
|
|
|
+ btn._origText = btn.textContent;
|
|
|
+ btn.textContent = '处理中...';
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await fetch('/api/knowledge/' + id + '/verify', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {'Content-Type': 'application/json'},
|
|
|
+ body: JSON.stringify({ action })
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error('请求失败: ' + res.status);
|
|
|
+ if (isSearchMode) {
|
|
|
+ clearSearch();
|
|
|
+ } else {
|
|
|
+ loadKnowledge(currentPage);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('验证错误:', error);
|
|
|
+ alert('操作失败: ' + error.message);
|
|
|
+ if (btn) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = btn._origText;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function batchVerify() {
|
|
|
+ if (selectedIds.size === 0) return;
|
|
|
+ if (!confirm(`确定要批量验证通过选中的 ${selectedIds.size} 条知识吗?`)) return;
|
|
|
+ const btn = document.getElementById('batchVerifyBtn');
|
|
|
+ if (btn) { btn.disabled = true; btn.textContent = `处理中...`; }
|
|
|
+ try {
|
|
|
+ const ids = Array.from(selectedIds);
|
|
|
+ const res = await fetch('/api/knowledge/batch_verify', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {'Content-Type': 'application/json'},
|
|
|
+ body: JSON.stringify({ knowledge_ids: ids, action: 'approve', verified_by: 'user' })
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error('请求失败: ' + res.status);
|
|
|
+ selectedIds.clear();
|
|
|
+ updateBatchDeleteButton();
|
|
|
+ if (isSearchMode) {
|
|
|
+ clearSearch();
|
|
|
+ } else {
|
|
|
+ loadKnowledge(currentPage);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('批量验证错误:', error);
|
|
|
+ alert('验证失败: ' + error.message);
|
|
|
+ if (btn) { btn.disabled = false; updateBatchDeleteButton(); }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
function escapeHtml(text) {
|
|
|
const div = document.createElement('div');
|
|
|
div.textContent = text;
|