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)
wwwroot/llm_vendor/
wwwroot/llm_account/
wwwroot/llm_account_by_vendor/
wwwroot/llm_model/
wwwroot/llm_api_profile/
wwwroot/llm_org_policy/

View File

@ -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",

View File

@ -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')}}"
}
]
}
}

View File

@ -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})

View File

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