feat(llm): 供应商账号改为供应商CRUD子表(llm_account_by_vendor)——弹窗进入只显示/处理选定供应商账号;账号归属服务端守卫(vendor_id不可改/跨供应商禁删);删独立账号列表页(index.ui卡片同步移除)

This commit is contained in:
yumoqing 2026-09-06 21:51:05 +08:00
parent c16bdb4a28
commit 34ef256fe6
5 changed files with 36 additions and 36 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ dist/
# CRUD generated directories (auto-generated by xls2ui at build time, NOT in repo) # CRUD generated directories (auto-generated by xls2ui at build time, NOT in repo)
wwwroot/llm_vendor/ wwwroot/llm_vendor/
wwwroot/llm_account/ wwwroot/llm_account/
wwwroot/llm_account_by_vendor/
wwwroot/llm_model/ wwwroot/llm_model/
wwwroot/llm_api_profile/ wwwroot/llm_api_profile/
wwwroot/llm_org_policy/ wwwroot/llm_org_policy/

View File

@ -1,5 +1,6 @@
{ {
"tblname": "llm_account", "tblname": "llm_account",
"alias": "llm_account_by_vendor",
"title": "供应商账号", "title": "供应商账号",
"params": { "params": {
"sortby": "name", "sortby": "name",
@ -7,15 +8,10 @@
"browserfields": { "browserfields": {
"exclouded": [ "exclouded": [
"id", "id",
"api_key" "api_key",
"vendor_id"
], ],
"alters": { "alters": {
"vendor_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_llm_vendor_options.dspy')}}",
"valueField": "value",
"textField": "text"
},
"status": { "status": {
"uitype": "code", "uitype": "code",
"dataurl": "{{entire_url('../api/get_llm_status_options.dspy')}}", "dataurl": "{{entire_url('../api/get_llm_status_options.dspy')}}",
@ -34,6 +30,7 @@
"editexclouded": [ "editexclouded": [
"id", "id",
"org_id", "org_id",
"vendor_id",
"balance", "balance",
"total_recharge", "total_recharge",
"created_at", "created_at",

View File

@ -30,6 +30,14 @@
"new_data_url": "{{entire_url('../api/add_llm_vendor.dspy')}}", "new_data_url": "{{entire_url('../api/add_llm_vendor.dspy')}}",
"update_data_url": "{{entire_url('../api/update_llm_vendor.dspy')}}", "update_data_url": "{{entire_url('../api/update_llm_vendor.dspy')}}",
"delete_data_url": "{{entire_url('../api/delete_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')}}"
}
]
} }
} }

View File

@ -222,10 +222,18 @@ async def create_llm_account(params_kw):
async def update_llm_account(params_kw): async def update_llm_account(params_kw):
"""账号更新:api_key 非空才重新加密(空=不改)。""" """账号更新:api_key 非空才重新加密(空=不改)。
vendor_id 归属不可改:账号在哪个供应商下创建就固定在哪个供应商下,
防止子表(按供应商过滤)入口下篡改表单把账号挪到别的供应商。
"""
result = {'success': False, 'message': ''} result = {'success': False, 'message': ''}
try: try:
data = _clean(params_kw) 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: if data.get('api_key') and data['api_key'] != MASK:
data['api_key'] = encrypt_api_key(data['api_key']) data['api_key'] = encrypt_api_key(data['api_key'])
else: else:
@ -243,17 +251,28 @@ async def update_llm_account(params_kw):
async def delete_llm_account(params_kw): async def delete_llm_account(params_kw):
"""账号删除:被模型引用时禁止;带 vendor_id 时校验归属(子表范围隔离)。"""
result = {'success': False, 'message': ''} result = {'success': False, 'message': ''}
try: try:
aid = (params_kw or {}).get('id', '') pk = params_kw or {}
aid = pk.get('id', '')
if not aid: if not aid:
raise ValueError('缺少 id') raise ValueError('缺少 id')
db, dbname = _get_sor() db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe( 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}) "SELECT COUNT(*) AS c FROM llm_model WHERE account_id=${a}$", {"a": aid})
await sor.sqlExe("COMMIT", {}) 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: if cnt > 0:
raise ValueError('该账号被 %d 个模型设为默认账号,请先解除后再删除' % cnt) raise ValueError('该账号被 %d 个模型设为默认账号,请先解除后再删除' % cnt)
await sor.sqlExe("DELETE FROM llm_account WHERE id=${a}$", {"a": aid}) await sor.sqlExe("DELETE FROM llm_account WHERE id=${a}$", {"a": aid})

View File

@ -82,31 +82,6 @@
{"widgettype": "Text", "options": {"text": "供应商协议 + 全球端点目录(国内/国际,配置一次全账号共用)", "cfontsize": 1.2}} {"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", "widgettype": "VBox",
"options": { "options": {