feat(kb): add rename and delete knowledge base
- kb_list.dspy: add per-card rename (popup form) and delete (confirm+cascade) buttons - rename_kb_form.dspy: popup form with name + description fields - rename_kb.dspy: UPDATE knowledge_bases handler - delete_kb.dspy: cascade delete (docs, chunks, media_tags, tags, files) handler
This commit is contained in:
parent
1dd59f7caa
commit
8f1f75c947
76
wwwroot/knowledge_bases_list/delete_kb.dspy
Normal file
76
wwwroot/knowledge_bases_list/delete_kb.dspy
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
ns = params_kw.copy()
|
||||||
|
kb_id = ns.get('kb_id', '')
|
||||||
|
|
||||||
|
if not kb_id:
|
||||||
|
return json.dumps({"status": "error", "error": "kb_id required"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
env = request._run_ns
|
||||||
|
userorgid = await env.get_userorgid()
|
||||||
|
deleted = {"documents": 0, "chunks": 0, "tags": 0, "media_tags": 0, "files": 0}
|
||||||
|
|
||||||
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
|
# Check owner
|
||||||
|
kb = await sor.sqlExe(
|
||||||
|
"SELECT id, name FROM knowledge_bases WHERE id=${id}$ AND org_id=${org_id}$",
|
||||||
|
{"id": kb_id, "org_id": userorgid})
|
||||||
|
if not kb:
|
||||||
|
return json.dumps({"status": "error", "error": "knowledge base not found or access denied"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Get all doc IDs and file paths for physical cleanup
|
||||||
|
docs = await sor.sqlExe(
|
||||||
|
"SELECT id, file_path FROM documents WHERE kb_id=${kb_id}$",
|
||||||
|
{"kb_id": kb_id})
|
||||||
|
doc_ids = [d.id for d in docs]
|
||||||
|
|
||||||
|
if doc_ids:
|
||||||
|
# Delete chunks
|
||||||
|
await sor.sqlExe(
|
||||||
|
"DELETE FROM document_chunks WHERE doc_id IN (${ids}$)",
|
||||||
|
{"ids": doc_ids})
|
||||||
|
deleted["chunks"] = len(doc_ids)
|
||||||
|
|
||||||
|
# Delete media_tags
|
||||||
|
mt = await sor.sqlExe(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM media_tags WHERE kb_id=${kb_id}$",
|
||||||
|
{"kb_id": kb_id})
|
||||||
|
await sor.sqlExe(
|
||||||
|
"DELETE FROM media_tags WHERE kb_id=${kb_id}$",
|
||||||
|
{"kb_id": kb_id})
|
||||||
|
deleted["media_tags"] = mt[0].cnt if mt else 0
|
||||||
|
|
||||||
|
# Delete tags
|
||||||
|
t = await sor.sqlExe(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM tags WHERE kb_id=${kb_id}$",
|
||||||
|
{"kb_id": kb_id})
|
||||||
|
await sor.sqlExe(
|
||||||
|
"DELETE FROM tags WHERE kb_id=${kb_id}$",
|
||||||
|
{"kb_id": kb_id})
|
||||||
|
deleted["tags"] = t[0].cnt if t else 0
|
||||||
|
|
||||||
|
# Delete documents
|
||||||
|
await sor.sqlExe(
|
||||||
|
"DELETE FROM documents WHERE kb_id=${kb_id}$",
|
||||||
|
{"kb_id": kb_id})
|
||||||
|
deleted["documents"] = len(doc_ids)
|
||||||
|
|
||||||
|
# Delete the KB itself
|
||||||
|
await sor.sqlExe(
|
||||||
|
"DELETE FROM knowledge_bases WHERE id=${id}$",
|
||||||
|
{"id": kb_id})
|
||||||
|
deleted["kb_name"] = str(kb[0].name)
|
||||||
|
|
||||||
|
# Delete physical files
|
||||||
|
for d in docs:
|
||||||
|
fp = d.file_path or ''
|
||||||
|
if fp and fp.startswith("/idfile/"):
|
||||||
|
try:
|
||||||
|
real_path = env.realpath(fp)
|
||||||
|
if os.path.exists(real_path):
|
||||||
|
os.remove(real_path)
|
||||||
|
deleted["files"] += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return json.dumps({"status": "SUCCEEDED", "kb_name": str(kb[0].name),
|
||||||
|
"documents": deleted["documents"], "files_removed": deleted["files"],
|
||||||
|
"tags": deleted["tags"]}, ensure_ascii=False)
|
||||||
@ -3,8 +3,7 @@ cards = []
|
|||||||
async with get_sor_context(env, 'rag') as sor:
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
recs = await sor.R("knowledge_bases", {})
|
recs = await sor.R("knowledge_bases", {})
|
||||||
for r in recs:
|
for r in recs:
|
||||||
doc_recs = await sor.R("documents", {"kb_id": r.id})
|
doc_count = r.doc_count or 0
|
||||||
doc_count = len(doc_recs)
|
|
||||||
total_size = r.total_size or 0
|
total_size = r.total_size or 0
|
||||||
if total_size >= 1073741824:
|
if total_size >= 1073741824:
|
||||||
size_str = "%.1fGB" % (total_size / 1073741824)
|
size_str = "%.1fGB" % (total_size / 1073741824)
|
||||||
@ -15,19 +14,58 @@ async with get_sor_context(env, 'rag') as sor:
|
|||||||
else:
|
else:
|
||||||
size_str = str(total_size) + "B"
|
size_str = str(total_size) + "B"
|
||||||
engine = r.embedding_engine or "CLIP"
|
engine = r.embedding_engine or "CLIP"
|
||||||
|
idstr = str(r.id)
|
||||||
|
namestr = r.name or ''
|
||||||
cards.append({
|
cards.append({
|
||||||
"widgettype": "VBox",
|
"widgettype": "VBox",
|
||||||
"options": {"cwidth": 16, "cheight": 12, "bgcolor": "#f0f7ff", "padding": "16px", "css": "card clickable", "border": "1px solid #d0e4f7"},
|
"options": {"cwidth": 16, "cheight": 12, "bgcolor": "#f0f7ff", "padding": "12px",
|
||||||
|
"css": "card", "border": "1px solid #d0e4f7"},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Text", "options": {"text": "📚 " + str(r.name), "cfontsize": 16, "fontWeight": "bold"}},
|
# Name row (clickable to enter KB)
|
||||||
{"widgettype": "Text", "options": {"text": str(engine) + " · 1024维", "cfontsize": 12, "color": "#888"}},
|
{"widgettype": "Text", "options": {"text": "\U0001f4da " + namestr,
|
||||||
{"widgettype": "HBox", "options": {"spacing": "12px"}, "subwidgets": [
|
"cfontsize": 16, "fontWeight": "bold", "css": "clickable"},
|
||||||
{"widgettype": "Text", "options": {"text": "📄 " + str(doc_count) + "文档", "cfontsize": 12, "color": "#666"}},
|
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||||
{"widgettype": "Text", "options": {"text": "💾 " + size_str, "cfontsize": 12, "color": "#666"}}]}
|
"target": "app.rag_main_content", "mode": "replace",
|
||||||
],
|
"options": {"url": "/rag/knowledge_bases_list/detail.ui",
|
||||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.rag_main_content", "mode": "replace",
|
"params": {"kb_id": idstr, "kb_name": namestr}}}]},
|
||||||
"options": {"url": "/rag/knowledge_bases_list/detail.ui", "params": {"kb_id": str(r.id), "kb_name": str(r.name)}}}]
|
# Meta info
|
||||||
|
{"widgettype": "Text", "options": {"text": str(engine) + " \u00b7 1024\u7ef4",
|
||||||
|
"cfontsize": 12, "color": "#888", "padding": "2px 0 6px 0"}},
|
||||||
|
{"widgettype": "HBox", "options": {"spacing": "12px", "padding": "0 0 8px 0"}, "subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "\U0001f4c4 " + str(doc_count) + "\u6587\u6863",
|
||||||
|
"cfontsize": 12, "color": "#666"}},
|
||||||
|
{"widgettype": "Text", "options": {"text": "\U0001f4be " + size_str,
|
||||||
|
"cfontsize": 12, "color": "#666"}}
|
||||||
|
]},
|
||||||
|
# Action buttons row
|
||||||
|
{"widgettype": "HBox", "options": {"justifyContent": "flex-end", "spacing": "6px",
|
||||||
|
"padding": "4px 0 0 0", "borderTop": "1px solid #e8f0fe"}, "subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "\u270f\ufe0f \u91cd\u547d\u540d",
|
||||||
|
"cfontsize": 12, "color": "#4a90d9", "css": "clickable",
|
||||||
|
"padding": "2px 8px", "borderRadius": "4px", "bgcolor": "#e3f0fb"},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||||
|
"target": "PopupWindow",
|
||||||
|
"popup_options": {"title": "\u91cd\u547d\u540d\u300c" + namestr + "\u300d",
|
||||||
|
"width": "30%", "height": "auto",
|
||||||
|
"movable": True, "resizable": True, "modal": True},
|
||||||
|
"options": {"url": "/rag/knowledge_bases_list/rename_kb_form.dspy?kb_id=" + idstr}}]},
|
||||||
|
{"widgettype": "Text", "options": {"text": "\U0001f5d1\ufe0f \u5220\u9664",
|
||||||
|
"cfontsize": 12, "color": "#e74c3c", "css": "clickable",
|
||||||
|
"padding": "2px 8px", "borderRadius": "4px", "bgcolor": "#fee"},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||||||
|
"script": "if(!confirm('\u786e\u8ba4\u5220\u9664\u77e5\u8bc6\u5e93\u300c" + namestr + "\u300d\uff1f\n\n\u8be5\u64cd\u4f5c\u4f1a\u5220\u9664\u77e5\u8bc6\u5e93\u5185\u6240\u6709\u6587\u4ef6\u3001\u6807\u7b7e\u548c\u6570\u636e\uff0c\u4e0d\u53ef\u6062\u590d\uff01'))return;\n"
|
||||||
|
"var u='/rag/knowledge_bases_list/delete_kb.dspy?kb_id=" + idstr + "';\n"
|
||||||
|
"fetch(u).then(function(r){return r.json()}).then(function(d){\n"
|
||||||
|
"if(d.status==='error'){alert('\u5220\u9664\u5931\u8d25\uff1a'+d.error);return;}\n"
|
||||||
|
"window.location.href='/rag/knowledge_bases_list/index.ui';\n"
|
||||||
|
"}).catch(function(){alert('\u7f51\u7edc\u9519\u8bef')});"}]}
|
||||||
|
]}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
if not cards:
|
if not cards:
|
||||||
cards.append({"widgettype": "Text", "options": {"text": "暂无知识库", "color": "#aaa", "cfontsize": 14, "halign": "center"}})
|
cards.append({"widgettype": "Text", "options": {
|
||||||
return json.dumps({"widgettype": "HBox", "options": {"width": "100%", "spacing": "12px", "wrap": True}, "subwidgets": cards}, ensure_ascii=False, default=str)
|
"text": "\u6682\u65e0\u77e5\u8bc6\u5e93", "cfontsize": 14, "color": "#aaa", "halign": "center"}})
|
||||||
|
|
||||||
|
return json.dumps({"widgettype": "HBox", "options": {"width": "100%", "spacing": "12px", "wrap": True},
|
||||||
|
"subwidgets": cards}, ensure_ascii=False, default=str)
|
||||||
|
|||||||
20
wwwroot/knowledge_bases_list/rename_kb.dspy
Normal file
20
wwwroot/knowledge_bases_list/rename_kb.dspy
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
ns = params_kw.copy()
|
||||||
|
kb_id = ns.get('kb_id', '')
|
||||||
|
name = ns.get('name', '').strip()
|
||||||
|
desc = ns.get('description', '')
|
||||||
|
|
||||||
|
if not kb_id or not name:
|
||||||
|
return json.dumps({"status": "error", "error": "kb_id and name required"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
env = request._run_ns
|
||||||
|
userorgid = await env.get_userorgid()
|
||||||
|
|
||||||
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
|
await sor.sqlExe(
|
||||||
|
"UPDATE knowledge_bases SET name=${name}$, description=${desc}$, updated_at=NOW() "
|
||||||
|
"WHERE id=${id}$ AND org_id=${org_id}$",
|
||||||
|
{"name": name, "desc": desc, "id": kb_id, "org_id": userorgid})
|
||||||
|
|
||||||
|
# Reload KB list page
|
||||||
|
return {"widgettype": "urlwidget",
|
||||||
|
"options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')}}
|
||||||
51
wwwroot/knowledge_bases_list/rename_kb_form.dspy
Normal file
51
wwwroot/knowledge_bases_list/rename_kb_form.dspy
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
ns = params_kw.copy()
|
||||||
|
kb_id = ns.get('kb_id', '')
|
||||||
|
name = ns.get('name', '')
|
||||||
|
desc = ns.get('description', '')
|
||||||
|
|
||||||
|
if not kb_id:
|
||||||
|
return json.dumps({"status": "error", "error": "kb_id required"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
env = request._run_ns
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT name, description FROM knowledge_bases WHERE id=${id}$",
|
||||||
|
{"id": kb_id})
|
||||||
|
if recs:
|
||||||
|
name = recs[0].name or ''
|
||||||
|
desc = recs[0].description or ''
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"padding": "16px", "spacing": "12px", "width": "100%"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "重命名知识库", "cfontsize": 16, "fontWeight": "bold",
|
||||||
|
"color": "#333", "padding": "0 0 8px 0"}},
|
||||||
|
{
|
||||||
|
"widgettype": "Form",
|
||||||
|
"id": "rename_form",
|
||||||
|
"options": {
|
||||||
|
"cols": 1,
|
||||||
|
"fields": [
|
||||||
|
{"name": "name", "label": "名称", "uitype": "str", "required": True,
|
||||||
|
"defaultValue": name},
|
||||||
|
{"name": "description", "label": "描述", "uitype": "text",
|
||||||
|
"defaultValue": desc},
|
||||||
|
{"name": "kb_id", "uitype": "hide", "value": kb_id}
|
||||||
|
],
|
||||||
|
"submit_label": "保存"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"binds": [{
|
||||||
|
"wid": "rename_form",
|
||||||
|
"event": "submit",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.rag_main_content",
|
||||||
|
"mode": "replace",
|
||||||
|
"options": {"url": "{{entire_url('./rename_kb.dspy')}}"}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
return result
|
||||||
Loading…
x
Reference in New Issue
Block a user