feat: llm_api_map CRUD management and ownerid-based data isolation
- Update json/llm.json subtable from llm_catelog_rel to llm_api_map - Rewrite json/llm_api_map.json as standard CRUD format (tblname+params) - Add models/llm_api_map.json table definition (summary/fields/indexes/codes) - Add independent management UI (llm_api_map_manage.ui) - Add CRUD DSPY APIs (list/create/delete/options) with ownerid filtering - All operations verify l.ownerid for data isolation - Add uapi_options.dspy for API selection dropdown
This commit is contained in:
parent
70e8fd791f
commit
1060cac2de
@ -48,8 +48,8 @@
|
||||
"subtables":[
|
||||
{
|
||||
"field":"llmid",
|
||||
"title":"模型类型",
|
||||
"subtable":"llm_catelog_rel"
|
||||
"title":"能力映射",
|
||||
"subtable":"llm_api_map"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,17 +1,27 @@
|
||||
{
|
||||
"summary": [{"name": "llm_api_map", "title": "模型能力映射表", "primary": ["id"], "catelog": "relation"}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "llmid", "title": "模型ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "llmcatelogid", "title": "类目ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "apiname", "title": "API接口名", "type": "str", "length": 100, "nullable": "no"},
|
||||
{"name": "query_apiname", "title": "结果查询API名", "type": "str", "length": 100},
|
||||
{"name": "query_period", "title": "查询间隔(秒)", "type": "int"},
|
||||
{"name": "ppid", "title": "定价项目ID", "type": "str", "length": 32}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_llm_api_llm", "idxtype": "index", "idxfields": ["llmid"]},
|
||||
{"name": "idx_llm_api_catelog", "idxtype": "unique", "idxfields": ["llmid", "llmcatelogid"]}
|
||||
],
|
||||
"codes": []
|
||||
"tblname": "llm_api_map",
|
||||
"title": "模型能力映射",
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "llmid"],
|
||||
"alters": {
|
||||
"llmcatelogid": {
|
||||
"dataurl": "{{entire_url('../api/llm_catelog_options.dspy')}}",
|
||||
"textField": "name",
|
||||
"valueField": "id"
|
||||
},
|
||||
"apiname": {
|
||||
"dataurl": "{{entire_url('../api/uapi_options.dspy')}}",
|
||||
"textField": "name",
|
||||
"valueField": "name"
|
||||
},
|
||||
"ppid": {
|
||||
"dataurl": "{{entire_url('/pricing/get_all_pricing_programs.dspy')}}",
|
||||
"textField": "name",
|
||||
"valueField": "id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": ["id", "llmid"]
|
||||
}
|
||||
}
|
||||
|
||||
99
models/llm_api_map.json
Normal file
99
models/llm_api_map.json
Normal file
@ -0,0 +1,99 @@
|
||||
{
|
||||
"table_name": "llm_api_map",
|
||||
"summary": [
|
||||
{
|
||||
"name": "llm_api_map",
|
||||
"title": "模型API映射表",
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "varchar(32)",
|
||||
"not_null": true,
|
||||
"comment": "主键ID"
|
||||
},
|
||||
{
|
||||
"name": "llmid",
|
||||
"type": "varchar(32)",
|
||||
"not_null": true,
|
||||
"comment": "模型ID,关联llm表"
|
||||
},
|
||||
{
|
||||
"name": "llmcatelogid",
|
||||
"type": "varchar(32)",
|
||||
"not_null": true,
|
||||
"comment": "模型分类ID,关联llmcatelog表"
|
||||
},
|
||||
{
|
||||
"name": "apiname",
|
||||
"type": "varchar(100)",
|
||||
"not_null": true,
|
||||
"comment": "推理接口名称,关联uapi表name字段"
|
||||
},
|
||||
{
|
||||
"name": "query_apiname",
|
||||
"type": "varchar(100)",
|
||||
"comment": "异步任务结果查询接口名称,可逗号分隔多个"
|
||||
},
|
||||
{
|
||||
"name": "query_period",
|
||||
"type": "bigint",
|
||||
"default": 30,
|
||||
"comment": "异步任务查询轮询间隔(秒),默认30"
|
||||
},
|
||||
{
|
||||
"name": "ppid",
|
||||
"type": "varchar(32)",
|
||||
"comment": "计费程序ID,关联pricing_program表"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_api_map_llmid",
|
||||
"type": "normal",
|
||||
"idxfields": ["llmid"],
|
||||
"idxtype": "index"
|
||||
},
|
||||
{
|
||||
"name": "idx_api_map_catelog",
|
||||
"type": "normal",
|
||||
"idxfields": ["llmcatelogid"],
|
||||
"idxtype": "index"
|
||||
},
|
||||
{
|
||||
"name": "idx_api_map_apiname",
|
||||
"type": "normal",
|
||||
"idxfields": ["apiname"],
|
||||
"idxtype": "index"
|
||||
},
|
||||
{
|
||||
"name": "uk_llmid_apiname",
|
||||
"type": "unique",
|
||||
"idxfields": ["llmid", "apiname"],
|
||||
"idxtype": "unique"
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "llmid",
|
||||
"table": "llm",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
},
|
||||
{
|
||||
"field": "llmcatelogid",
|
||||
"table": "llmcatelog",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
},
|
||||
{
|
||||
"field": "ppid",
|
||||
"table": "pricing_program",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
}
|
||||
]
|
||||
}
|
||||
63
wwwroot/api/llm_api_map_create.dspy
Normal file
63
wwwroot/api/llm_api_map_create.dspy
Normal file
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
llmid = params_kw.get('llmid', '')
|
||||
catelogid = params_kw.get('llmcatelogid', '')
|
||||
apiname = params_kw.get('apiname', '')
|
||||
|
||||
if not llmid or not catelogid or not apiname:
|
||||
result['options'] = {'title': 'Error', 'message': '模型、分类和API接口为必填项', 'type': 'error'}
|
||||
else:
|
||||
user_orgid = await get_userorgid()
|
||||
from appPublic.uniqueID import getID
|
||||
new_id = getID()
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# 验证模型属于当前用户的机构
|
||||
check_llm = await sor.sqlExe(
|
||||
"select id from llm where id = ${llmid}$ and ownerid = ${ownerid}$",
|
||||
{'llmid': llmid, 'ownerid': user_orgid}
|
||||
)
|
||||
if not check_llm:
|
||||
result['options'] = {'title': 'Error', 'message': '无权操作该模型', 'type': 'error'}
|
||||
else:
|
||||
# 检查是否已存在
|
||||
check_sql = """select id from llm_api_map
|
||||
where llmid = ${llmid}$ and apiname = ${apiname}$"""
|
||||
exists = await sor.sqlExe(check_sql, {'llmid': llmid, 'apiname': apiname})
|
||||
|
||||
if exists:
|
||||
result['options'] = {'title': '提示', 'message': '该模型的此API映射已存在', 'type': 'warning'}
|
||||
else:
|
||||
data = {
|
||||
'id': new_id,
|
||||
'llmid': llmid,
|
||||
'llmcatelogid': catelogid,
|
||||
'apiname': apiname
|
||||
}
|
||||
query_apiname = params_kw.get('query_apiname', '').strip()
|
||||
if query_apiname:
|
||||
data['query_apiname'] = query_apiname
|
||||
query_period = params_kw.get('query_period', '').strip()
|
||||
if query_period:
|
||||
try:
|
||||
data['query_period'] = int(query_period)
|
||||
except ValueError:
|
||||
data['query_period'] = 30
|
||||
else:
|
||||
data['query_period'] = 30
|
||||
ppid = params_kw.get('ppid', '').strip()
|
||||
if ppid:
|
||||
data['ppid'] = ppid
|
||||
|
||||
await sor.C('llm_api_map', data)
|
||||
result['options'] = {'title': 'Success', 'message': '添加成功', 'type': 'success'}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'添加失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
31
wwwroot/api/llm_api_map_delete.dspy
Normal file
31
wwwroot/api/llm_api_map_delete.dspy
Normal file
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
map_id = params_kw.get('id', '')
|
||||
|
||||
if not map_id:
|
||||
result['options'] = {'title': 'Error', 'message': '缺少ID参数', 'type': 'error'}
|
||||
else:
|
||||
user_orgid = await get_userorgid()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# 验证映射记录对应的模型属于当前用户的机构
|
||||
check = await sor.sqlExe(
|
||||
"""select m.id from llm_api_map m
|
||||
join llm l on m.llmid = l.id
|
||||
where m.id = ${id}$ and l.ownerid = ${ownerid}$""",
|
||||
{'id': map_id, 'ownerid': user_orgid}
|
||||
)
|
||||
if not check:
|
||||
result['options'] = {'title': 'Error', 'message': '无权删除该映射', 'type': 'error'}
|
||||
else:
|
||||
await sor.sqlExe("delete from llm_api_map where id = ${id}$", {'id': map_id})
|
||||
result['options'] = {'title': 'Success', 'message': '删除成功', 'type': 'success'}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
32
wwwroot/api/llm_api_map_list.dspy
Normal file
32
wwwroot/api/llm_api_map_list.dspy
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
user_orgid = await get_userorgid()
|
||||
|
||||
sql = """
|
||||
select m.id, m.llmid, l.name as llm_name,
|
||||
m.llmcatelogid, c.name as catelog_name,
|
||||
m.apiname, m.query_apiname, m.query_period,
|
||||
m.ppid, p.name as ppid_name
|
||||
from llm_api_map m
|
||||
join llm l on m.llmid = l.id
|
||||
join llmcatelog c on m.llmcatelogid = c.id
|
||||
left join pricing_program p on m.ppid = p.id
|
||||
where l.ownerid = ${ownerid}$
|
||||
order by l.name, c.name, m.apiname
|
||||
"""
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe(sql, {'ownerid': user_orgid})
|
||||
result['rows'] = [dict(r) for r in (rows or [])]
|
||||
result['total'] = len(result['rows'])
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
38
wwwroot/api/llm_api_map_options.dspy
Normal file
38
wwwroot/api/llm_api_map_options.dspy
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'data': {'llms': [], 'catelogs': [], 'apis': [], 'ppids': []}}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
user_orgid = await get_userorgid()
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# Active LLMs for user's organization
|
||||
today = await get_business_date(sor)
|
||||
llms_sql = """select id, name from llm
|
||||
where enabled_date <= ${today}$
|
||||
and expired_date > ${today}$
|
||||
and ownerid = ${ownerid}$
|
||||
order by name"""
|
||||
llms = await sor.sqlExe(llms_sql, {'today': today, 'ownerid': user_orgid})
|
||||
result['data']['llms'] = [{'id': r['id'], 'text': r['name']} for r in (llms or [])]
|
||||
|
||||
# All catalogs
|
||||
catelogs = await sor.sqlExe("select id, name from llmcatelog order by name", {})
|
||||
result['data']['catelogs'] = [{'id': r['id'], 'text': r['name']} for r in (catelogs or [])]
|
||||
|
||||
# uapi list (for apiname selection)
|
||||
apis = await sor.sqlExe("select name, path from uapi order by name", {})
|
||||
result['data']['apis'] = [{'id': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (apis or [])]
|
||||
|
||||
# Pricing programs
|
||||
ppids = await sor.sqlExe("select id, name from pricing_program order by name", {})
|
||||
result['data']['ppids'] = [{'id': r['id'], 'text': r['name']} for r in (ppids or [])]
|
||||
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
17
wwwroot/api/uapi_options.dspy
Normal file
17
wwwroot/api/uapi_options.dspy
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'data': []}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe("select name, path from uapi order by name", {})
|
||||
result['data'] = [{'id': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (rows or [])]
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
181
wwwroot/llm_api_map_manage.ui
Normal file
181
wwwroot/llm_api_map_manage.ui
Normal file
@ -0,0 +1,181 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"spacing": 16
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Title2",
|
||||
"options": {
|
||||
"text": "模型能力映射管理",
|
||||
"halign": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "calc(100% - 40px)",
|
||||
"margin": "0 20px",
|
||||
"padding": "16px",
|
||||
"bgcolor": "#f5f5f5",
|
||||
"spacing": 12
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "添加新映射",
|
||||
"fontSize": "16px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "add_form",
|
||||
"options": {
|
||||
"layout": "horizontal",
|
||||
"cols": 3,
|
||||
"fields": [
|
||||
{
|
||||
"name": "llmid",
|
||||
"label": "选择模型",
|
||||
"uitype": "select",
|
||||
"dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
|
||||
"data_field": "llms",
|
||||
"placeholder": "请选择模型"
|
||||
},
|
||||
{
|
||||
"name": "llmcatelogid",
|
||||
"label": "选择分类",
|
||||
"uitype": "select",
|
||||
"dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
|
||||
"data_field": "catelogs",
|
||||
"placeholder": "请选择分类"
|
||||
},
|
||||
{
|
||||
"name": "apiname",
|
||||
"label": "API接口",
|
||||
"uitype": "select",
|
||||
"dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
|
||||
"data_field": "apis",
|
||||
"placeholder": "请选择API接口"
|
||||
},
|
||||
{
|
||||
"name": "query_apiname",
|
||||
"label": "查询API",
|
||||
"uitype": "text",
|
||||
"placeholder": "异步查询API名,多个用逗号分隔"
|
||||
},
|
||||
{
|
||||
"name": "query_period",
|
||||
"label": "查询间隔(秒)",
|
||||
"uitype": "number",
|
||||
"placeholder": "默认30"
|
||||
},
|
||||
{
|
||||
"name": "ppid",
|
||||
"label": "计费项目",
|
||||
"uitype": "select",
|
||||
"dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
|
||||
"data_field": "ppids",
|
||||
"placeholder": "请选择计费项目"
|
||||
}
|
||||
],
|
||||
"buttons": [
|
||||
{
|
||||
"name": "add_btn",
|
||||
"label": "添加映射",
|
||||
"variant": "primary"
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "add_btn",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {"archor": "cc", "width": "30%", "height": "20%"},
|
||||
"options": {
|
||||
"url": "{{entire_url('./api/llm_api_map_create.dspy')}}",
|
||||
"params": {
|
||||
"llmid": "$[add_form.llmid]$",
|
||||
"llmcatelogid": "$[add_form.llmcatelogid]$",
|
||||
"apiname": "$[add_form.apiname]$",
|
||||
"query_apiname": "$[add_form.query_apiname]$",
|
||||
"query_period": "$[add_form.query_period]$",
|
||||
"ppid": "$[add_form.ppid]$"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "calc(100% - 40px)",
|
||||
"margin": "0 20px",
|
||||
"spacing": 12
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "当前映射列表",
|
||||
"fontSize": "16px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Tabular",
|
||||
"id": "map_table",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "400px",
|
||||
"data_url": "{{entire_url('./api/llm_api_map_list.dspy')}}",
|
||||
"data_method": "GET",
|
||||
"page_rows": 20,
|
||||
"row_options": {
|
||||
"fields": [
|
||||
{"name": "llm_name", "title": "模型名称", "width": 180},
|
||||
{"name": "catelog_name", "title": "分类", "width": 120},
|
||||
{"name": "apiname", "title": "API接口", "width": 150},
|
||||
{"name": "query_apiname", "title": "查询API", "width": 180},
|
||||
{"name": "query_period", "title": "间隔(秒)", "width": 80},
|
||||
{"name": "ppid_name", "title": "计费项目", "width": 150},
|
||||
{
|
||||
"name": "actions",
|
||||
"title": "操作",
|
||||
"width": 100,
|
||||
"uitype": "button",
|
||||
"data": [
|
||||
{"text": "删除", "event": "delete_map"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "delete_map",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {"archor": "cc", "width": "30%", "height": "20%"},
|
||||
"options": {
|
||||
"url": "{{entire_url('./api/llm_api_map_delete.dspy')}}",
|
||||
"params": {
|
||||
"id": "$[event.params.id]$"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user