From 34ef256fe6eb7b38816c6ad8f74d4117e8d3a16a Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 6 Sep 2026 21:51:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(llm):=20=E4=BE=9B=E5=BA=94=E5=95=86?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=94=B9=E4=B8=BA=E4=BE=9B=E5=BA=94=E5=95=86?= =?UTF-8?q?CRUD=E5=AD=90=E8=A1=A8(llm=5Faccount=5Fby=5Fvendor)=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=BC=B9=E7=AA=97=E8=BF=9B=E5=85=A5=E5=8F=AA=E6=98=BE?= =?UTF-8?q?=E7=A4=BA/=E5=A4=84=E7=90=86=E9=80=89=E5=AE=9A=E4=BE=9B?= =?UTF-8?q?=E5=BA=94=E5=95=86=E8=B4=A6=E5=8F=B7;=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=AE=88=E5=8D=AB?= =?UTF-8?q?(vendor=5Fid=E4=B8=8D=E5=8F=AF=E6=94=B9/=E8=B7=A8=E4=BE=9B?= =?UTF-8?q?=E5=BA=94=E5=95=86=E7=A6=81=E5=88=A0);=E5=88=A0=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E8=B4=A6=E5=8F=B7=E5=88=97=E8=A1=A8=E9=A1=B5(index.ui?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=90=8C=E6=AD=A5=E7=A7=BB=E9=99=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + ...t_list.json => llm_account_by_vendor.json} | 11 +++----- json/llm_vendor_list.json | 10 +++++++- pipeline_llm/init.py | 25 ++++++++++++++++--- wwwroot/index.ui | 25 ------------------- 5 files changed, 36 insertions(+), 36 deletions(-) rename json/{llm_account_list.json => llm_account_by_vendor.json} (84%) diff --git a/.gitignore b/.gitignore index 3c7cc48..bd740be 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist/ # CRUD generated directories (auto-generated by xls2ui at build time, NOT in repo) wwwroot/llm_vendor/ wwwroot/llm_account/ +wwwroot/llm_account_by_vendor/ wwwroot/llm_model/ wwwroot/llm_api_profile/ wwwroot/llm_org_policy/ diff --git a/json/llm_account_list.json b/json/llm_account_by_vendor.json similarity index 84% rename from json/llm_account_list.json rename to json/llm_account_by_vendor.json index e6d4052..e59ccb1 100644 --- a/json/llm_account_list.json +++ b/json/llm_account_by_vendor.json @@ -1,5 +1,6 @@ { "tblname": "llm_account", + "alias": "llm_account_by_vendor", "title": "供应商账号", "params": { "sortby": "name", @@ -7,15 +8,10 @@ "browserfields": { "exclouded": [ "id", - "api_key" + "api_key", + "vendor_id" ], "alters": { - "vendor_id": { - "uitype": "code", - "dataurl": "{{entire_url('../api/get_llm_vendor_options.dspy')}}", - "valueField": "value", - "textField": "text" - }, "status": { "uitype": "code", "dataurl": "{{entire_url('../api/get_llm_status_options.dspy')}}", @@ -34,6 +30,7 @@ "editexclouded": [ "id", "org_id", + "vendor_id", "balance", "total_recharge", "created_at", diff --git a/json/llm_vendor_list.json b/json/llm_vendor_list.json index 1166f53..21774be 100644 --- a/json/llm_vendor_list.json +++ b/json/llm_vendor_list.json @@ -30,6 +30,14 @@ "new_data_url": "{{entire_url('../api/add_llm_vendor.dspy')}}", "update_data_url": "{{entire_url('../api/update_llm_vendor.dspy')}}", "delete_data_url": "{{entire_url('../api/delete_llm_vendor.dspy')}}" - } + }, + "subtables": [ + { + "field": "vendor_id", + "title": "供应商账号", + "subtable": "llm_account_by_vendor", + "url": "{{entire_url('/pipeline-llm/llm_account_by_vendor')}}" + } + ] } } diff --git a/pipeline_llm/init.py b/pipeline_llm/init.py index a1e01fe..b3d11fb 100644 --- a/pipeline_llm/init.py +++ b/pipeline_llm/init.py @@ -222,10 +222,18 @@ async def create_llm_account(params_kw): async def update_llm_account(params_kw): - """账号更新:api_key 非空才重新加密(空=不改)。""" + """账号更新:api_key 非空才重新加密(空=不改)。 + + vendor_id 归属不可改:账号在哪个供应商下创建就固定在哪个供应商下, + 防止子表(按供应商过滤)入口下篡改表单把账号挪到别的供应商。 + """ result = {'success': False, 'message': ''} try: data = _clean(params_kw) + aid = data.get('id', '') + if not aid: + raise ValueError('缺少 id') + data.pop('vendor_id', None) if data.get('api_key') and data['api_key'] != MASK: data['api_key'] = encrypt_api_key(data['api_key']) else: @@ -243,17 +251,28 @@ async def update_llm_account(params_kw): async def delete_llm_account(params_kw): + """账号删除:被模型引用时禁止;带 vendor_id 时校验归属(子表范围隔离)。""" result = {'success': False, 'message': ''} try: - aid = (params_kw or {}).get('id', '') + pk = params_kw or {} + aid = pk.get('id', '') if not aid: raise ValueError('缺少 id') db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: recs = await sor.sqlExe( + "SELECT vendor_id FROM llm_account WHERE id=${a}$", {"a": aid}) + await sor.sqlExe("COMMIT", {}) + if not recs: + raise ValueError('账号不存在') + # 子表入口会带 vendor_id:校验账号确实属于该供应商,防跨供应商误删/越权删 + expect_vid = pk.get('vendor_id', '') + if expect_vid and getattr(recs[0], 'vendor_id', '') != expect_vid: + raise ValueError('账号不属于该供应商,禁止删除') + cnt_recs = await sor.sqlExe( "SELECT COUNT(*) AS c FROM llm_model WHERE account_id=${a}$", {"a": aid}) await sor.sqlExe("COMMIT", {}) - cnt = int(getattr(recs[0], 'c', 0)) if recs else 0 + cnt = int(getattr(cnt_recs[0], 'c', 0)) if cnt_recs else 0 if cnt > 0: raise ValueError('该账号被 %d 个模型设为默认账号,请先解除后再删除' % cnt) await sor.sqlExe("DELETE FROM llm_account WHERE id=${a}$", {"a": aid}) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index bc1c5fd..2085e27 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -82,31 +82,6 @@ {"widgettype": "Text", "options": {"text": "供应商协议 + 全球端点目录(国内/国际,配置一次全账号共用)", "cfontsize": 1.2}} ] }, - { - "widgettype": "VBox", - "options": { - "css": "card", - "cwidth": 25, - "padding": "16px", - "cursor": "pointer" - }, - "binds": [ - { - "wid": "self", - "event": "click", - "actiontype": "urlwidget", - "target": "app.llm_content", - "options": { - "url": "{{entire_url('/pipeline-llm/llm_account')}}" - }, - "mode": "replace" - } - ], - "subwidgets": [ - {"widgettype": "Title4", "options": {"text": "供应商账号(钱包)", "marginBottom": "8px"}}, - {"widgettype": "Text", "options": {"text": "多账号独立充值记账、选用端点;轮转突破供应商限流", "cfontsize": 1.2}} - ] - }, { "widgettype": "VBox", "options": {