From 548fc6013e679dca8eb71b7d1770d258ddef5489 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 4 Aug 2026 15:36:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20per-org=20storage=20quota=20enforcement?= =?UTF-8?q?=20=E2=80=94=20org=5Fstorage=5Flimits=20table,=20upload=20rejec?= =?UTF-8?q?tion,=20UI=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- json/org_storage_limits.json | 17 ++++++++ models/org_storage_limits.json | 17 ++++++++ rag/__pycache__/init.cpython-310.pyc | Bin 31363 -> 32142 bytes rag/init.py | 21 ++++++++- wwwroot/knowledge_bases_list/detail.ui | 2 +- .../knowledge_bases_list/storage_stats.dspy | 40 +++++++++--------- wwwroot/knowledge_bases_list/upload_file.dspy | 19 ++++++++- 7 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 json/org_storage_limits.json create mode 100644 models/org_storage_limits.json diff --git a/json/org_storage_limits.json b/json/org_storage_limits.json new file mode 100644 index 0000000..f40d581 --- /dev/null +++ b/json/org_storage_limits.json @@ -0,0 +1,17 @@ +{ + "tblname": "org_storage_limits", + "params": { + "title": "机构存储限额", + "browserfields": [ + {"field": "org_id", "title": "机构ID", "width": "35%"}, + {"field": "limit_bytes", "title": "限额(字节)", "width": "35%"}, + {"field": "updated_at", "title": "更新时间", "width": "30%"} + ], + "editfields": [ + {"field": "org_id", "uitype": "Text", "required": true}, + {"field": "limit_bytes", "uitype": "Text", "required": true, "default": "104857600"} + ], + "searchfields": ["org_id"], + "sort": "updated_at desc" + } +} diff --git a/models/org_storage_limits.json b/models/org_storage_limits.json new file mode 100644 index 0000000..f40d581 --- /dev/null +++ b/models/org_storage_limits.json @@ -0,0 +1,17 @@ +{ + "tblname": "org_storage_limits", + "params": { + "title": "机构存储限额", + "browserfields": [ + {"field": "org_id", "title": "机构ID", "width": "35%"}, + {"field": "limit_bytes", "title": "限额(字节)", "width": "35%"}, + {"field": "updated_at", "title": "更新时间", "width": "30%"} + ], + "editfields": [ + {"field": "org_id", "uitype": "Text", "required": true}, + {"field": "limit_bytes", "uitype": "Text", "required": true, "default": "104857600"} + ], + "searchfields": ["org_id"], + "sort": "updated_at desc" + } +} diff --git a/rag/__pycache__/init.cpython-310.pyc b/rag/__pycache__/init.cpython-310.pyc index f8b6738c94f37548858d77b4b50ab7b2869a9a9a..3d783108cac6a8247a6171dec72f76c25d0221e0 100644 GIT binary patch delta 8347 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 diff --git a/rag/init.py b/rag/init.py index 24624f4..1b94675 100644 --- a/rag/init.py +++ b/rag/init.py @@ -285,6 +285,12 @@ async def _enrich_search_results(env, hits): return hits +def _fmt_bytes(n): + if n < 1024: return str(n) + 'B' + if n < 1048576: return str(round(n/1024, 1)) + 'KB' + return str(round(n/1048576, 1)) + 'MB' + + async def doc_upload_handler(request, params_kw, *args, **kwargs): """文件上传 → 保存 → DB记录 → 触发RAG入库""" env = request._run_ns @@ -301,6 +307,20 @@ async def doc_upload_handler(request, params_kw, *args, **kwargs): if not file_data: return json.dumps({"error": "no file data"}) + file_size = len(file_data) + + # ---- ORG STORAGE QUOTA CHECK (per-org limit, not global) ---- + 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 documents WHERE org_id=${org_id}$", {"org_id": userorgid}) + if rec: used = int(rec[0].used) + lim = await sor.sqlExe("SELECT limit_bytes FROM 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) + # Save file doc_id = uuid.uuid4().hex[:16] ext = os.path.splitext(file_name)[1] or ".bin" @@ -311,7 +331,6 @@ async def doc_upload_handler(request, params_kw, *args, **kwargs): with open(file_path, "wb") as f: f.write(file_data) - file_size = len(file_data) file_type = _detect_file_type(file_name, "application/octet-stream") # Create document record diff --git a/wwwroot/knowledge_bases_list/detail.ui b/wwwroot/knowledge_bases_list/detail.ui index e025145..d03bccd 100644 --- a/wwwroot/knowledge_bases_list/detail.ui +++ b/wwwroot/knowledge_bases_list/detail.ui @@ -219,7 +219,7 @@ "event": "changed", "actiontype": "script", "target": "file_drop_zone", - "script": "var kb_id=this.opts.kb_id||'';var folder=window._rag_folder||'';if(folder==='__root__')folder='';var files=this.value;if(!files)return;if(!Array.isArray(files))files=[files];var st=document.getElementById('upload_status');var done=0;var total=files.length;function refreshList(){var fp=bricks.getWidgetById('file_list_panel',bricks.app.root);fp.clear_widgets();var url=location.origin+'/rag/knowledge_bases_list/file_list.dspy?_webbricks_=1&kb_id='+kb_id+(folder?'&id='+folder:'');bricks.widgetBuild({widgettype:'urlwidget',options:{url:url,css:'filler'}},fp).then(function(w){if(w)fp.add_widget(w)})};for(var i=0;i=total)refreshList()};x.onerror=function(){done++;if(st)st.innerText='上传失败';if(done>=total)refreshList()};x.send(f)})(files[i])}" + "script": "var kb_id=this.opts.kb_id||'';var folder=window._rag_folder||'';if(folder==='__root__')folder='';var files=this.value;if(!files)return;if(!Array.isArray(files))files=[files];var st=document.getElementById('upload_status');var done=0;var total=files.length;function refreshList(){var fp=bricks.getWidgetById('file_list_panel',bricks.app.root);fp.clear_widgets();var url=location.origin+'/rag/knowledge_bases_list/file_list.dspy?_webbricks_=1&kb_id='+kb_id+(folder?'&id='+folder:'');bricks.widgetBuild({widgettype:'urlwidget',options:{url:url,css:'filler'}},fp).then(function(w){if(w)fp.add_widget(w)})};for(var i=0;i=total)refreshList()};x.onerror=function(){done++;if(st){st.innerText='上传失败';st.style.color='#f56c6c'}if(done>=total)refreshList()};x.send(f)})(files[i])}" }, { "wid": "dir_tree", diff --git a/wwwroot/knowledge_bases_list/storage_stats.dspy b/wwwroot/knowledge_bases_list/storage_stats.dspy index 93310af..d05e6d9 100644 --- a/wwwroot/knowledge_bases_list/storage_stats.dspy +++ b/wwwroot/knowledge_bases_list/storage_stats.dspy @@ -1,42 +1,40 @@ ns = params_kw.copy() env = request._run_ns +userorgid = await env.get_userorgid() +# Per-org metering: documents.file_size by org_id (auto-decreases on delete) +used = 0 +limit = 104857600 +docs = 0 async with get_sor_context(env, 'rag') as sor: - kb_id = ns.get('kb_id', '') - if kb_id: - recs = await sor.sqlExe("SELECT doc_count, total_size, chunk_count FROM knowledge_bases WHERE id=${id}$", {"id": kb_id}) - else: - recs = await sor.sqlExe("SELECT COALESCE(SUM(doc_count),0) AS doc_count, COALESCE(SUM(total_size),0) AS total_size, COALESCE(SUM(chunk_count),0) AS chunk_count FROM knowledge_bases", {}) - - if recs: - r = recs[0] - docs = int(r.doc_count or 0) - size = int(r.total_size or 0) - chunks = int(r.chunk_count or 0) - else: - docs = 0 - size = 0 - chunks = 0 + rec = await sor.sqlExe( + "SELECT COALESCE(SUM(file_size),0) AS used, COUNT(*) AS docs FROM documents WHERE org_id=${org_id}$", + {"org_id": userorgid}) + if rec: + used = int(rec[0].used) + docs = int(rec[0].docs) + lim = await sor.sqlExe("SELECT limit_bytes FROM org_storage_limits WHERE org_id=${org_id}$", {"org_id": userorgid}) + if lim: + limit = int(lim[0].limit_bytes) def fmt(sz): if sz < 1024: return str(sz) + 'B' elif sz < 1024*1024: return str(round(sz/1024,1)) + 'KB' else: return str(round(sz/(1024*1024),1)) + 'MB' -pct = min(round(size / (100*1024*1024) * 100, 1), 100) if size > 0 else 0 -if pct > 100: - pct = 100 +pct = min(round(used / max(limit, 1) * 100, 1), 100) if used > 0 else 0 +bar_color = "#f56c6c" if pct >= 90 else ("#e6a23c" if pct >= 70 else "#4a90d9") return json.dumps({ "widgettype": "VBox", "options": {"width": "100%", "bgcolor": "#f8f9fa", "padding": "16px", "css": "card", "spacing": "4px"}, "subwidgets": [ {"widgettype": "HBox", "options": {"width": "100%", "justifyContent": "space-between"}, "subwidgets": [ - {"widgettype": "Text", "options": {"text": "💾 存储容量", "cfontsize": 14, "fontWeight": "bold", "color": "#333"}}, - {"widgettype": "Text", "options": {"text": f"已用 {fmt(size)} / 总计 100MB | {docs} 文档 {chunks} 块", "cfontsize": 12, "color": "#888"}} + {"widgettype": "Text", "options": {"text": "💾 存储容量(本机构)", "cfontsize": 14, "fontWeight": "bold", "color": "#333"}}, + {"widgettype": "Text", "options": {"text": "已用 " + fmt(used) + " / 限额 " + fmt(limit) + " | " + str(docs) + " 文档", "cfontsize": 12, "color": "#888"}} ]}, {"widgettype": "VBox", "options": {"width": "100%", "cheight": 0.5, "bgcolor": "#e0e0e0"}, "subwidgets": [ - {"widgettype": "VBox", "options": {"cwidth": pct, "cheight": 0.5, "bgcolor": "#4a90d9"}} + {"widgettype": "VBox", "options": {"cwidth": pct, "cheight": 0.5, "bgcolor": bar_color}} ]} ] }, ensure_ascii=False, default=str) diff --git a/wwwroot/knowledge_bases_list/upload_file.dspy b/wwwroot/knowledge_bases_list/upload_file.dspy index dbde791..da29501 100644 --- a/wwwroot/knowledge_bases_list/upload_file.dspy +++ b/wwwroot/knowledge_bases_list/upload_file.dspy @@ -12,13 +12,30 @@ if not file_data: env = request._run_ns userorgid = await env.get_userorgid() +file_size = len(file_data) + +# ---- ORG STORAGE QUOTA CHECK (per-org limit, not global) ---- +def fmt_bytes(n): + if n < 1024: return str(n) + 'B' + if n < 1048576: return str(round(n/1024, 1)) + 'KB' + return str(round(n/1048576, 1)) + 'MB' + +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 documents WHERE org_id=${org_id}$", {"org_id": userorgid}) + if rec: used = int(rec[0].used) + lim = await sor.sqlExe("SELECT limit_bytes FROM 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({"status": "error", "error": "storage_quota_exceeded", + "message": "存储配额超限:机构已用 " + fmt_bytes(used) + ",限额 " + fmt_bytes(quota_limit) + ",本文件 " + fmt_bytes(file_size)}, ensure_ascii=False) # Save file via FileStorage (returns web path e.g. /idfile/191/193/197/97/xxx.txt) web_path = await env.save_file(file_data, file_name) real_path = env.realpath(web_path) doc_id = str(uuid()).replace('-', '')[:16] -file_size = len(file_data) ext = '.' + file_name.rsplit('.', 1)[1] if '.' in file_name else '.bin' ext_l = ext.lower()