ymq 1c1ef65931 feat(rag): 机构rags目录+容量检查参数化+KB级角色权限
- 存储: 知识库文件存 {workspace_base}/{org}/rags/{kb}/ (机构目录下开设rags目录)
- 容量: rag_check_storage_quota 参数控制是否检查机构上限(默认不检查)
- 权限: rag_knowledge_bases 加 maintain_roles/search_roles; 空=同机构默认可用
  维护入口(upload/delete/rename/tree/tags)校验maintain; 检索入口(search/file_list/file_serve)校验search
- file_serve.dspy: 下载/预览走校验端点, 堵住/idfile/猜路径越权
- 机构隔离: 跨机构KB一律拒绝
2026-09-02 12:09:16 +08:00

115 lines
4.5 KiB
Plaintext

import os
ns = params_kw.copy()
kb_id = ns.get('kb_id', '')
if not kb_id:
return {"widgettype": "urlwidget",
"options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')}}
# KB 维护角色校验(机构隔离 + 角色)
from rag.init import check_kb_perm
env = request._run_ns
db = DBPools()
async with db.sqlorContext(get_module_dbname('rag')) as sor:
_ok, _kb, _msg = await check_kb_perm(env, sor, kb_id, 'maintain')
if not _ok:
return {"widgettype": "Text", "options": {"text": "⛔ " + _msg, "cfontsize": 13, "color": "#f56c6c", "halign": "center"}}
env = request._run_ns
userorgid = await env.get_userorgid()
deleted = {"documents": 0, "chunks": 0, "tags": 0, "media_tags": 0,
"files": 0, "vdb": 0, "graph": 0, "entities": 0}
async with get_sor_context(env, 'rag') as sor:
kb = await sor.sqlExe(
"SELECT id, name FROM rag_knowledge_bases WHERE id=${id}$ AND (org_id=${org_id}$ OR org_id IS NULL)",
{"id": kb_id, "org_id": userorgid})
if not kb:
return {"widgettype": "urlwidget",
"options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')}}
docs = await sor.sqlExe(
"SELECT id, file_path FROM rag_documents WHERE kb_id=${kb_id}$",
{"kb_id": kb_id})
doc_ids = [d.id for d in docs]
# 1. VDB cleanup
if doc_ids:
chunks = await sor.sqlExe(
"SELECT vector_id FROM rag_document_chunks WHERE doc_id IN (${ids}$) AND vector_id IS NOT NULL AND vector_id != ''",
{"ids": doc_ids})
vector_ids = [c.vector_id for c in chunks]
if vector_ids:
try:
client = StreamHttpClient()
await client.request('POST',
'https://vectordb.opencomputing.net:10443/v1/delete',
json={"colname": kb_id, "ids": vector_ids})
deleted["vdb"] = len(vector_ids)
except Exception as e:
info(f'delete_kb vdb error: {e}')
# 2. Graph DB cleanup
try:
client2 = StreamHttpClient()
await client2.request('POST',
'http://localhost:9092/api/graph/delete',
json={"graph": kb_id})
deleted["graph"] = 1
except Exception as e:
info(f'delete_kb graph error: {e}')
# 3. Entities & relations
er = await sor.sqlExe(
"SELECT COUNT(*) AS cnt FROM rag_entity_relations WHERE kb_id=${kb_id}$",
{"kb_id": kb_id})
await sor.sqlExe("DELETE FROM rag_entity_relations WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
deleted["entities"] += er[0].cnt if er else 0
ent = await sor.sqlExe(
"SELECT COUNT(*) AS cnt FROM rag_entities WHERE kb_id=${kb_id}$",
{"kb_id": kb_id})
await sor.sqlExe("DELETE FROM rag_entities WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
deleted["entities"] += ent[0].cnt if ent else 0
# 4. Chunks
ch = await sor.sqlExe(
"SELECT COUNT(*) AS cnt FROM rag_document_chunks WHERE doc_id IN (${ids}$)",
{"ids": doc_ids})
await sor.sqlExe("DELETE FROM rag_document_chunks WHERE doc_id IN (${ids}$)", {"ids": doc_ids})
deleted["chunks"] = ch[0].cnt if ch else 0
# Media tags
mt = await sor.sqlExe("SELECT COUNT(*) AS cnt FROM rag_media_tags WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
await sor.sqlExe("DELETE FROM rag_media_tags WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
deleted["media_tags"] = mt[0].cnt if mt else 0
# Tags
t = await sor.sqlExe("SELECT COUNT(*) AS cnt FROM rag_tags WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
await sor.sqlExe("DELETE FROM rag_tags WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
deleted["tags"] = t[0].cnt if t else 0
# Documents
await sor.sqlExe("DELETE FROM rag_documents WHERE kb_id=${kb_id}$", {"kb_id": kb_id})
deleted["documents"] = len(doc_ids)
# KB itself
await sor.sqlExe("DELETE FROM rag_knowledge_bases WHERE id=${id}$", {"id": kb_id})
# Physical files — paths are like /44/126/174/61/file.mp4
for d in docs:
fp = d.file_path or ''
if fp and fp.startswith("/"):
try:
real_path = env.realpath(fp)
if os.path.exists(real_path):
os.remove(real_path)
deleted["files"] += 1
except Exception:
pass
info(f'delete_kb {kb_id}: {deleted}')
return {"widgettype": "urlwidget",
"options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')}}