feat: add data_filter and CRUD endpoints for llm table

- Add data_filter with 4 searchable fields (name LIKE, model LIKE, providerid, upappid)
- Add filter_labels for search form display
- Create llm_list.dspy with DBFilter support and LIKE wildcard handling
- Create llm_create.dspy, llm_update.dspy, llm_delete.dspy
- Create get_organizations.dspy and get_upapps.dspy for dropdown options
- Add browserfields alters for providerid and upappid dropdowns
- Add editable URLs for DataViewer CRUD operations
This commit is contained in:
yumoqing 2026-05-26 14:26:38 +08:00
parent 04913dbe42
commit 9aa917bce5
7 changed files with 218 additions and 1 deletions

View File

@ -4,6 +4,21 @@
"params": {
"sortby":"model",
"logined_userorgid": "ownerid",
"data_url": "{{entire_url('../api/llm_list.dspy')}}",
"data_filter": {
"AND": [
{"field": "name", "op": "LIKE", "var": "name_input"},
{"field": "model", "op": "LIKE", "var": "model_input"},
{"field": "providerid", "op": "=", "var": "providerid_input"},
{"field": "upappid", "op": "=", "var": "upappid_input"}
]
},
"filter_labels": {
"name_input": "名称",
"model_input": "识别名",
"providerid_input": "供应商",
"upappid_input": "上位系统"
},
"browserfields": {
"exclouded": ["id", "ownerid"],
"alters": {
@ -11,7 +26,21 @@
"dataurl":"{{entire_url('/pricing/get_all_pricing_programs.dspy')}}",
"textField": "name",
"valueField": "id"
}
},
"providerid": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_organizations.dspy')}}",
"data_field": "organizations",
"textField": "text",
"valueField": "id"
},
"upappid": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_upapps.dspy')}}",
"data_field": "upapps",
"textField": "text",
"valueField": "id"
}
}
},
"toolbar": {
@ -45,6 +74,11 @@
"editexclouded": [
"id", "ownerid"
],
"editable": {
"new_data_url": "{{entire_url('../api/llm_create.dspy')}}",
"update_data_url": "{{entire_url('../api/llm_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/llm_delete.dspy')}}"
},
"subtables":[
{
"field":"llmid",

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'data': {'organizations': []}}
try:
async with get_sor_context(request._run_ns, 'rbac') as sor:
user_orgid = await get_userorgid()
orgs = await sor.R('organization', {'id': user_orgid})
# Get current org and its children
all_orgs = await sor.sqlExe(
"select id, orgname from organization where id = ${id}$ or parentid = ${id}$ order by orgname",
{'id': user_orgid}
)
result['data']['organizations'] = [{'id': r['id'], 'text': r['orgname']} for r in (all_orgs or [])]
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'data': {'upapps': []}}
try:
async with get_sor_context(request._run_ns, 'uapi') as sor:
user_orgid = await get_userorgid()
apps = await sor.sqlExe(
"select id, name from upapp where ownerid = ${ownerid}$ or ownerid is null order by name",
{'ownerid': user_orgid}
)
result['data']['upapps'] = [{'id': r['id'], 'text': r['name']} for r in (apps or [])]
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import json
from appPublic.uniqueID import getID
result = {'success': False, 'message': ''}
try:
dbname = get_module_dbname('llmage')
async with DBPools().sqlorContext(dbname) as sor:
data = params_kw.copy()
data.pop('page', None)
data.pop('rows', None)
data.pop('data_filter', None)
data['id'] = getID()
await sor.C('llm', data)
result['success'] = True
result['message'] = '创建成功'
except Exception as e:
result['message'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'message': ''}
try:
dbname = get_module_dbname('llmage')
async with DBPools().sqlorContext(dbname) as sor:
data = params_kw.copy()
data.pop('page', None)
data.pop('rows', None)
data.pop('data_filter', None)
record_id = data.get('id')
if not record_id:
result['message'] = '缺少id'
else:
await sor.D('llm', {'id': record_id})
result['success'] = True
result['message'] = '删除成功'
except Exception as e:
result['message'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

78
wwwroot/api/llm_list.dspy Normal file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
import json
from sqlor.filter import DBFilter
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('llmage')
page = int(params_kw.get('page', 1))
rows_per_page = int(params_kw.get('rows', 20))
offset = (page - 1) * rows_per_page
# Get data_filter JSON from frontend
filterjson_str = params_kw.get('data_filter')
filterjson = None
if filterjson_str:
try:
filterjson = json.loads(filterjson_str)
except (json.JSONDecodeError, TypeError):
filterjson = None
async with DBPools().sqlorContext(dbname) as sor:
where_clause = ''
filterdic = {}
if filterjson:
# Preprocess LIKE values: add wildcards if user didn't provide them
ns = dict(params_kw)
for key, val in ns.items():
# Check if this var is used with LIKE op in filterjson
if _is_like_var(filterjson, key) and val and '%' not in val:
ns[key] = f'%{val}%'
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
where_clause = f' WHERE {conds}'
filterdic = ns
# Total count
count_sql = f"select count(*) as cnt from llm{where_clause}"
count_rows = await sor.sqlExe(count_sql, filterdic)
total = count_rows[0]['cnt'] if count_rows else 0
# Paginated data
data_sql = f"""
select id, name, model, description, iconid, upappid, providerid,
ownerid, enabled_date, expired_date, min_balance
from llm{where_clause}
order by model
limit ${limit}$ offset ${offset}$
"""
rows = await sor.sqlExe(data_sql, {**filterdic, 'limit': rows_per_page, 'offset': offset})
result['rows'] = [dict(r) for r in (rows or [])]
result['total'] = total
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)
def _is_like_var(filterjson, varname):
"""Check if a var is used with LIKE operator in the filter tree."""
if not filterjson:
return False
for key, val in filterjson.items():
if key.upper() in ('AND', 'OR') and isinstance(val, list):
for item in val:
if _is_like_var(item, varname):
return True
elif key.upper() == 'NOT' and isinstance(val, dict):
if _is_like_var(val, varname):
return True
elif isinstance(val, dict) and val.get('var') == varname:
if val.get('op', '').upper() == 'LIKE':
return True
return False

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'message': ''}
try:
dbname = get_module_dbname('llmage')
async with DBPools().sqlorContext(dbname) as sor:
data = params_kw.copy()
data.pop('page', None)
data.pop('rows', None)
data.pop('data_filter', None)
record_id = data.pop('id', None)
if not record_id:
result['message'] = '缺少id'
else:
await sor.U('llm', data, {'id': record_id})
result['success'] = True
result['message'] = '更新成功'
except Exception as e:
result['message'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)