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": {