From 1c1ef6593199f1d44d33af8f2394135854a9e695 Mon Sep 17 00:00:00 2001 From: ymq Date: Wed, 2 Sep 2026 12:09:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(rag):=20=E6=9C=BA=E6=9E=84rags=E7=9B=AE?= =?UTF-8?q?=E5=BD=95+=E5=AE=B9=E9=87=8F=E6=A3=80=E6=9F=A5=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=8C=96+KB=E7=BA=A7=E8=A7=92=E8=89=B2=E6=9D=83?= =?UTF-8?q?=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 存储: 知识库文件存 {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一律拒绝 --- models/rag_knowledge_bases.json | 16 ++ rag/__pycache__/init.cpython-311.pyc | Bin 0 -> 111446 bytes rag/init.py | 242 ++++++++++++++++-- wwwroot/knowledge_bases_list/add_tag.dspy | 8 + wwwroot/knowledge_bases_list/create_kb.dspy | 14 +- wwwroot/knowledge_bases_list/delete_file.dspy | 9 + wwwroot/knowledge_bases_list/delete_kb.dspy | 9 + .../delete_tree_item.dspy | 14 +- wwwroot/knowledge_bases_list/file_list.dspy | 23 +- wwwroot/knowledge_bases_list/new_kb_form.ui | 20 +- .../knowledge_bases_list/new_tree_item.dspy | 6 + wwwroot/knowledge_bases_list/rename_kb.dspy | 9 + wwwroot/knowledge_bases_list/save_tags.dspy | 9 + .../update_tree_item.dspy | 9 + 14 files changed, 347 insertions(+), 41 deletions(-) create mode 100644 rag/__pycache__/init.cpython-311.pyc diff --git a/models/rag_knowledge_bases.json b/models/rag_knowledge_bases.json index 45171b3..105ef1d 100644 --- a/models/rag_knowledge_bases.json +++ b/models/rag_knowledge_bases.json @@ -84,6 +84,22 @@ "nullable": "yes", "default": "active" }, + { + "name": "maintain_roles", + "title": "维护角色", + "type": "str", + "length": 500, + "nullable": "yes", + "default": "" + }, + { + "name": "search_roles", + "title": "检索角色", + "type": "str", + "length": 500, + "nullable": "yes", + "default": "" + }, { "name": "created_by", "title": "创建人", diff --git a/rag/__pycache__/init.cpython-311.pyc b/rag/__pycache__/init.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ee1b460688aa5a098917fdde17dac7045e30982 GIT binary patch literal 111446 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 literal 0 HcmV?d00001 diff --git a/rag/init.py b/rag/init.py index 977e245..63955c5 100644 --- a/rag/init.py +++ b/rag/init.py @@ -165,15 +165,26 @@ async def search_handler(request, params_kw, *args, **kwargs): async def _resolve_search_kbs(env, userorgid, kb_id): - """Resolve KB IDs: specific or all org KBs""" + """Resolve KB IDs: specific or all org KBs。 + 机构隔离 + 检索角色过滤(search_roles 为空=同机构默认可检)。""" async with get_sor_context(env, 'rag') as sor: if kb_id: recs = await sor.R("rag_knowledge_bases", {"id": kb_id}) - return [r.id for r in recs] - # All org KBs (global + org-specific) - sql = "SELECT id FROM rag_knowledge_bases WHERE org_id IS NULL OR org_id=${org_id}$" - recs = await sor.sqlExe(sql, {"org_id": userorgid}) - return [r.id for r in recs] + cands = list(recs or []) + else: + # All org KBs (global + org-specific) + sql = "SELECT * FROM rag_knowledge_bases WHERE org_id IS NULL OR org_id=${org_id}$" + recs = await sor.sqlExe(sql, {"org_id": userorgid}) + cands = list(recs or []) + user_id = await env.get_user() + roles = await _get_user_roles_for(sor, user_id) + out = [] + for r in cands: + if str(r.org_id or '') not in ('', str(userorgid or '')): + continue # 跨机构 KB 不可检 + if _role_match(roles, getattr(r, 'search_roles', '')): + out.append(r.id) + return out async def _build_search_vector(query, file_data, file_name, env=None, kb_id=''): @@ -278,6 +289,110 @@ def _fmt_bytes(n): return str(round(n/1048576, 1)) + 'MB' +# ────────────────────────── 机构 rags 目录 + 容量参数 + KB 角色权限 ────────────────────────── +# 2026-09-02 需求: +# 1) 产线平台各机构目录下开设 rags/ 目录,rag 模块创建的知识库文件都存放在该目录 +# ({workspace_base}/{org_id}/rags/{kb_id}/,workspace_base 与产线工作空间同参数); +# 2) 容量检查不独立硬查,由 appbase params 参数 rag_check_storage_quota 控制(默认不检查); +# 3) 每个知识库可独立设置维护角色(maintain_roles)/检索角色(search_roles), +# 用户实际操作(上传/删除/改名/检索/下载)按角色做权限控制。 + +_RAGS_BASE_DEFAULT = '/d/pipeline/workspaces' + + +async def _get_param_value(sor, name, default=""): + """读 appbase params 表(params_name/params_value),表/行不存在返回 default。""" + try: + recs = await sor.sqlExe( + "SELECT params_value FROM params WHERE params_name=${n}$ LIMIT 1", {"n": name}) + await sor.sqlExe("COMMIT", {}) + if recs: + v = getattr(recs[0], 'params_value', None) + if v is not None and str(v).strip() != '': + return str(v) + except Exception as e: + debug(f"rag _get_param_value({name}) failed: {e}") + return default + + +async def get_rags_base(sor): + """机构 rags 根目录 = 产线工作空间根(workspace_base 参数)下,与机构目录同级。""" + return await _get_param_value(sor, 'workspace_base', _RAGS_BASE_DEFAULT) + + +def kb_dir_for(base, org_id, kb_id): + """{base}/{org_id}/rags/{kb_id}。org_id 空兜底 '0'。""" + return os.path.join(base, str(org_id or '0'), 'rags', str(kb_id)) + + +def ensure_kb_dir(base, org_id, kb_id): + """确保知识库目录存在(含机构 rags 父目录),返回绝对路径。""" + d = kb_dir_for(base, org_id, kb_id) + os.makedirs(d, exist_ok=True) + return d + + +async def _get_user_roles_for(sor, user_id): + """查用户 RBAC 角色({orgtypeid}.{name} 格式,与产线惯例一致)。""" + roles = ['any', 'logined'] + if not user_id: + return roles + try: + recs = await sor.sqlExe( + "SELECT r.orgtypeid, r.name FROM userrole ur JOIN role r ON ur.roleid=r.id " + "WHERE ur.userid=${u}$", {"u": user_id}) + await sor.sqlExe("COMMIT", {}) + for r in (recs or []): + o = getattr(r, 'orgtypeid', '') or '' + n = getattr(r, 'name', '') or '' + if o and n: + roles.append(f"{o}.{n}") + roles.append(f"{o}.*") + except Exception as e: + debug(f"rag _get_user_roles_for failed: {e}") + return roles + + +def _role_match(user_roles, allowed_csv): + """allowed_csv 为空 → 默认放行(同机构登录用户);'*' → 全部;否则按角色名精确/通配匹配。""" + allowed = [x.strip() for x in str(allowed_csv or '').split(',') if x.strip()] + if not allowed: + return True + if '*' in allowed: + return True + ur = set(user_roles or []) + for a in allowed: + if a in ur: + return True + if a.endswith('.*'): + prefix = a[:-1] + if any(r.startswith(prefix) for r in ur): + return True + return False + + +async def check_kb_perm(env, sor, kb_id, perm): + """KB 级角色权限校验。perm='maintain'|'search'。 + 返回 (ok, kb, msg)。机构隔离:kb 不属于用户机构 → 拒绝。 + 角色为空 → 同机构登录用户默认放行。""" + userorgid = await env.get_userorgid() + user_id = await env.get_user() + recs = await sor.sqlExe( + "SELECT * FROM rag_knowledge_bases WHERE id=${k}$ LIMIT 1", {"k": kb_id}) + await sor.sqlExe("COMMIT", {}) + if not recs: + return False, None, "知识库不存在" + kb = recs[0] + if str(kb.org_id or '') != str(userorgid or ''): + return False, kb, "无权访问其他机构的知识库" + allowed = kb.maintain_roles if perm == 'maintain' else kb.search_roles + roles = await _get_user_roles_for(sor, user_id) + if not _role_match(roles, allowed): + label = '维护' if perm == 'maintain' else '检索' + return False, kb, f"无{label}权限:该知识库限角色 [{allowed}],当前角色不匹配" + return True, kb, "" + + async def doc_upload_handler(request, params_kw, *args, **kwargs): """文件上传 → 保存 → DB记录 → 触发RAG入库""" env = request._run_ns @@ -296,21 +411,37 @@ async def doc_upload_handler(request, params_kw, *args, **kwargs): file_size = len(file_data) - # ---- ORG STORAGE QUOTA CHECK (per-org limit, not global) ---- - quota_limit = 104857600 - used = 0 + # ---- KB 级维护权限校验(机构隔离 + 角色) ---- async with get_sor_context(env, 'rag') as sor: - rec = await sor.sqlExe("SELECT COALESCE(SUM(file_size),0) AS used FROM rag_documents WHERE org_id=${org_id}$", {"org_id": userorgid}) - if rec: used = int(rec[0].used) - lim = await sor.sqlExe("SELECT limit_bytes FROM rag_org_storage_limits WHERE org_id=${org_id}$", {"org_id": userorgid}) - if lim: quota_limit = int(lim[0].limit_bytes) - if used + file_size > quota_limit: - return json.dumps({"error": "storage_quota_exceeded", - "message": "存储配额超限:机构已用 " + _fmt_bytes(used) + ",限额 " + _fmt_bytes(quota_limit) + ",本文件 " + _fmt_bytes(file_size)}, ensure_ascii=False) + ok, kb, msg = await check_kb_perm(env, sor, kb_id, 'maintain') + if not ok: + return json.dumps({"error": "kb_perm_denied", "message": msg}, ensure_ascii=False) - # Save file via FileStorage (layered dir layout, returns web path e.g. /191/193/197/97/xxx.txt) + # ---- 机构存储容量检查(参数化:rag_check_storage_quota 默认不检查) ---- + async with get_sor_context(env, 'rag') as sor: + check_quota = str(await _get_param_value(sor, 'rag_check_storage_quota', '0')).strip().lower() + if check_quota in ('1', 'true', 'yes', 'on'): + quota_limit = 104857600 + used = 0 + async with get_sor_context(env, 'rag') as sor: + rec = await sor.sqlExe("SELECT COALESCE(SUM(file_size),0) AS used FROM rag_documents WHERE org_id=${org_id}$", {"org_id": userorgid}) + if rec: used = int(rec[0].used) + lim = await sor.sqlExe("SELECT limit_bytes FROM rag_org_storage_limits WHERE org_id=${org_id}$", {"org_id": userorgid}) + if lim: quota_limit = int(lim[0].limit_bytes) + if used + file_size > quota_limit: + return json.dumps({"error": "storage_quota_exceeded", + "message": "存储配额超限:机构已用 " + _fmt_bytes(used) + ",限额 " + _fmt_bytes(quota_limit) + ",本文件 " + _fmt_bytes(file_size)}, ensure_ascii=False) + + # ---- 保存到机构 rags 目录:{workspace_base}/{org}/rags/{kb}/ ---- doc_id = uuid.uuid4().hex - web_path = await env.save_file(file_data, file_name) + async with get_sor_context(env, 'rag') as sor: + rags_base = await get_rags_base(sor) + kb_dir = ensure_kb_dir(rags_base, userorgid, kb_id) + safe_name = (file_name or 'upload.bin').replace('/', '_').replace('\\', '_') + disk_name = doc_id[:8] + '_' + safe_name + with open(os.path.join(kb_dir, disk_name), 'wb') as f: + f.write(file_data) + web_path = '/rags/' + str(userorgid or '0') + '/' + str(kb_id) + '/' + disk_name file_type = _detect_file_type(file_name, "application/octet-stream") @@ -379,6 +510,11 @@ async def doc_delete_handler(request, params_kw, *args, **kwargs): return json.dumps({"error": "document not found"}) doc = recs[0] + # KB 级维护权限校验(机构隔离 + 角色) + ok, kb, msg = await check_kb_perm(env, sor, doc.kb_id, 'maintain') + if not ok: + return json.dumps({"error": "kb_perm_denied", "message": msg}, ensure_ascii=False) + # Get chunks to clean VDB chunks = await sor.R("rag_document_chunks", {"doc_id": doc_id}) @@ -409,9 +545,17 @@ async def doc_delete_handler(request, params_kw, *args, **kwargs): "UPDATE rag_knowledge_bases SET doc_count=GREATEST(doc_count-1,0), total_size=GREATEST(total_size-${size}$,0), chunk_count=GREATEST(chunk_count-${n}$,0) WHERE id=${kb_id}$", {"size": doc.file_size, "n": len(chunks), "kb_id": doc.kb_id}) - # Delete file from disk + # Delete file from disk(/rags/{org}/{kb}/{disk} 新布局;旧 /idfile/ 兼容) file_path = doc.file_path - if file_path and file_path.startswith("/idfile/"): + if file_path and file_path.startswith("/rags/"): + parts = [p for p in file_path.split('/') if p] + if len(parts) >= 4: + async with get_sor_context(env, 'rag') as sor: + rags_base = await get_rags_base(sor) + real_path = os.path.join(rags_base, parts[1], 'rags', parts[2], parts[3]) + if os.path.isfile(real_path): + os.remove(real_path) + elif file_path and file_path.startswith("/idfile/"): real_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files", os.path.basename(file_path)) if os.path.exists(real_path): @@ -424,6 +568,52 @@ async def doc_delete_handler(request, params_kw, *args, **kwargs): return json.dumps({"error": str(e)}) +async def file_serve_handler(request, params_kw, *args, **kwargs): + """知识库文件下载/预览:search 角色校验 + 机构隔离,base64 返回(前端 Blob 下载)。 + 堵住 /idfile/ 猜路径越权:rags 目录文件不暴露给 idfile,只能经此端点取。""" + import base64, mimetypes as _mt + env = request._run_ns + try: + doc_id = params_kw.get("doc_id", "") + if not doc_id: + return json.dumps({"error": "doc_id required"}) + async with get_sor_context(env, 'rag') as sor: + recs = await sor.R("rag_documents", {"id": doc_id}) + if not recs: + return json.dumps({"error": "document not found"}) + doc = recs[0] + ok, kb, msg = await check_kb_perm(env, sor, doc.kb_id, 'search') + if not ok: + return json.dumps({"error": "kb_perm_denied", "message": msg}, ensure_ascii=False) + fp = doc.file_path or '' + if fp.startswith('/rags/'): + parts = [p for p in fp.split('/') if p] + if len(parts) < 4: + return json.dumps({"error": "bad file path"}) + rags_base = await get_rags_base(sor) + real_path = os.path.join(rags_base, parts[1], 'rags', parts[2], parts[3]) + elif fp.startswith('/idfile/'): + real_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files", + os.path.basename(fp)) + else: + return json.dumps({"error": "file path not served"}) + if not os.path.isfile(real_path): + return json.dumps({"error": "file missing on disk"}) + with open(real_path, 'rb') as f: + data = f.read() + mime = _mt.guess_type(doc.file_name or '')[0] or 'application/octet-stream' + return json.dumps({ + "status": "SUCCEEDED", + "file_name": doc.file_name, + "mime": mime, + "size": len(data), + "b64": base64.b64encode(data).decode('ascii') + }, ensure_ascii=False) + except Exception as e: + exception(f"file_serve: {e}, {format_exc()}") + return json.dumps({"error": str(e)}) + + def _detect_file_type(name, mime): """Detect file type from name and MIME""" ext = os.path.splitext(name)[1].lower() @@ -678,6 +868,10 @@ async def dir_create_handler(request, params_kw, *args, **kwargs): if not kb_id or not dir_name: return json.dumps({"error": "kb_id and dir_name required"}) async with get_sor_context(env, 'rag') as sor: + # KB 维护角色校验 + ok, kb, msg = await check_kb_perm(env, sor, kb_id, 'maintain') + if not ok: + return json.dumps({"error": "kb_perm_denied", "message": msg}, ensure_ascii=False) dir_id = uuid.uuid4().hex await sor.sqlExe( "INSERT INTO rag_document_chunks (id, doc_id, kb_id, chunk_index, chunk_type, content, description, created_at) " @@ -697,6 +891,13 @@ async def dir_delete_handler(request, params_kw, *args, **kwargs): if not item_id: return json.dumps({"error": "item_id required"}) async with get_sor_context(env, 'rag') as sor: + # KB 维护角色校验(按节点反查 kb_id) + recs = await sor.sqlExe("SELECT kb_id FROM rag_document_chunks WHERE id=${id}$", {"id": item_id}) + await sor.sqlExe("COMMIT", {}) + _kb_id = getattr(recs[0], 'kb_id', '') if recs else '' + ok, kb, msg = await check_kb_perm(env, sor, _kb_id, 'maintain') + if not ok: + return json.dumps({"error": "kb_perm_denied", "message": msg}, ensure_ascii=False) await sor.sqlExe("DELETE FROM rag_document_chunks WHERE id=${id}$ OR doc_id=${id}$", {"id": item_id}) return json.dumps({"status": "SUCCEEDED"}) except Exception as e: @@ -1133,6 +1334,7 @@ def init_rag_module(): rf.register("search", search_handler) rf.register("doc_upload", doc_upload_handler) rf.register("doc_delete", doc_delete_handler) + rf.register("file_serve", file_serve_handler) rf.register("dir_create", dir_create_handler) rf.register("dir_delete", dir_delete_handler) rf.register("dir_list", dir_list_handler) diff --git a/wwwroot/knowledge_bases_list/add_tag.dspy b/wwwroot/knowledge_bases_list/add_tag.dspy index 7507beb..ca862d1 100644 --- a/wwwroot/knowledge_bases_list/add_tag.dspy +++ b/wwwroot/knowledge_bases_list/add_tag.dspy @@ -4,6 +4,14 @@ tag = ns.get('tag', '').strip() kb_id = ns.get('kb_id', '') env = request._run_ns +# KB 维护角色校验(机构隔离 + 角色) +from rag.init import check_kb_perm +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 json.dumps({"status": "error", "error": _msg}, ensure_ascii=False) + if not doc_id or not tag: return json.dumps({"status": "error", "error": "doc_id and tag required"}, ensure_ascii=False) diff --git a/wwwroot/knowledge_bases_list/create_kb.dspy b/wwwroot/knowledge_bases_list/create_kb.dspy index 1444eb4..c742c38 100644 --- a/wwwroot/knowledge_bases_list/create_kb.dspy +++ b/wwwroot/knowledge_bases_list/create_kb.dspy @@ -11,10 +11,18 @@ async with db.sqlorContext(dbname) as sor: emb_type = ns.get("embedding_type", "") or ns.get("embedding_engine", "") if emb_type not in ("bge-m3", "clip-vith14", "qwen3-vl-embedding"): emb_type = "bge-m3" + # 知识库级角色权限:维护角色/检索角色(逗号分隔,空=同机构默认可用) + maintain_roles = ns.get("maintain_roles", "") or "" + search_roles = ns.get("search_roles", "") or "" await sor.sqlExe( - "INSERT INTO rag_knowledge_bases (id, name, description, org_id, embedding_engine, vdb_collection, doc_count, total_size, chunk_count, status, created_at) " - "VALUES (${id}$, ${name}$, ${desc}$, ${org_id}$, ${emb}$, 'rag_collection', 0, 0, 0, 'active', NOW())", - {"id": kb_id, "name": name, "desc": desc, "org_id": userorgid, "emb": emb_type}) + "INSERT INTO rag_knowledge_bases (id, name, description, org_id, embedding_engine, vdb_collection, doc_count, total_size, chunk_count, status, maintain_roles, search_roles, created_at) " + "VALUES (${id}$, ${name}$, ${desc}$, ${org_id}$, ${emb}$, 'rag_collection', 0, 0, 0, 'active', ${mr}$, ${sr}$, NOW())", + {"id": kb_id, "name": name, "desc": desc, "org_id": userorgid, "emb": emb_type, + "mr": maintain_roles, "sr": search_roles}) + # 机构 rags 目录下建知识库子目录:{workspace_base}/{org}/rags/{kb}/ + from rag.init import get_rags_base, ensure_kb_dir + rags_base = await get_rags_base(sor) + ensure_kb_dir(rags_base, userorgid, kb_id) return { "widgettype": "urlwidget", "options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')} diff --git a/wwwroot/knowledge_bases_list/delete_file.dspy b/wwwroot/knowledge_bases_list/delete_file.dspy index 7f23db9..32b6030 100644 --- a/wwwroot/knowledge_bases_list/delete_file.dspy +++ b/wwwroot/knowledge_bases_list/delete_file.dspy @@ -4,6 +4,15 @@ kb_id = ns.get('kb_id', '') folder_id = ns.get('folder_id', '') folder_label = ns.get('folder_label', '全部文件') +# 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 json.dumps({"status": "error", "error": _msg}, ensure_ascii=False) + if not doc_id: return json.dumps({"status": "error", "error": "doc_id required"}, ensure_ascii=False) diff --git a/wwwroot/knowledge_bases_list/delete_kb.dspy b/wwwroot/knowledge_bases_list/delete_kb.dspy index d1a5c3e..c17c534 100644 --- a/wwwroot/knowledge_bases_list/delete_kb.dspy +++ b/wwwroot/knowledge_bases_list/delete_kb.dspy @@ -7,6 +7,15 @@ 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, diff --git a/wwwroot/knowledge_bases_list/delete_tree_item.dspy b/wwwroot/knowledge_bases_list/delete_tree_item.dspy index 42ce8b8..ab1a21c 100644 --- a/wwwroot/knowledge_bases_list/delete_tree_item.dspy +++ b/wwwroot/knowledge_bases_list/delete_tree_item.dspy @@ -2,6 +2,16 @@ ns = params_kw.copy() db = DBPools() dbname = get_module_dbname('rag') async with db.sqlorContext(dbname) as sor: - await sor.sqlExe("DELETE FROM rag_document_chunks WHERE id=${id}$", {"id": ns.get("id","")}) - return {"widgettype": "Message", "options": {"user_data": {"id": ns.get("id","")}}} + _id = ns.get("id", "") + # KB 维护角色校验(按节点反查 kb_id) + _recs = await sor.sqlExe("SELECT kb_id FROM rag_document_chunks WHERE id=${id}$", {"id": _id}) + await sor.sqlExe("COMMIT", {}) + _kb_id = getattr(_recs[0], 'kb_id', '') if _recs else '' + from rag.init import check_kb_perm + env = request._run_ns + _ok, _kb, _msg = await check_kb_perm(env, sor, _kb_id, 'maintain') + if not _ok: + return json.dumps({"status": "error", "error": _msg}, ensure_ascii=False) + await sor.sqlExe("DELETE FROM rag_document_chunks WHERE id=${id}$", {"id": _id}) + return {"widgettype": "Message", "options": {"user_data": {"id": _id}}} return {"error": "failed"} diff --git a/wwwroot/knowledge_bases_list/file_list.dspy b/wwwroot/knowledge_bases_list/file_list.dspy index 574affc..e4c0aef 100644 --- a/wwwroot/knowledge_bases_list/file_list.dspy +++ b/wwwroot/knowledge_bases_list/file_list.dspy @@ -9,6 +9,14 @@ if folder_id == '__root__': db = DBPools() dbname = get_module_dbname('rag') +# KB 检索角色校验(机构隔离 + 角色):无 search 权限不列文件 +from rag.init import check_kb_perm +env = request._run_ns +async with db.sqlorContext(dbname) as sor: + _ok, _kb, _msg = await check_kb_perm(env, sor, kb_id, 'search') +if not _ok: + return {"widgettype": "Text", "options": {"text": "⛔ " + _msg, "cfontsize": 13, "color": "#f56c6c", "halign": "center", "marginTop": "40px"}} + rows = [] try: async with db.sqlorContext(dbname) as sor: @@ -69,6 +77,17 @@ else: file_url = f["file_path"] if not file_url.startswith("/idfile"): file_url = "/idfile" + file_url + # 查看/下载统一走 file_serve.dspy(search 角色校验),不再直连 /idfile/ + serve_url = "/rag/knowledge_bases_list/file_serve.dspy?_webbricks_=1&doc_id=" + f["id"] + view_script = ("fetch(" + json.dumps(serve_url) + ").then(function(r){return r.json()}).then(function(d){" + "if(d.error){alert(d.message||d.error);return}" + "var bin=atob(d.b64);var arr=new Uint8Array(bin.length);for(var i=0;i