feat(llmage): 添加llmusage历史记录备份和记账失败检索功能
- 新增 llmusage_history 表:定时备份已记账(use_date<today)的历史记录 - 新增 llmusage_accounting_failed 表:记录记账失败详情,支持检索 - 新增 backup_accounted_llmusage() 函数:备份+清理历史数据 - 新增 get_failed_accounting_records() 函数:按条件检索失败记录 - 更新 llm_accoung_failed():同时写入失败表记录 - 新增 failed_accounting.ui 页面和 failed_accounting_list.dspy API - 新增 llmusage CRUD API (create/update/delete) - 新增表索引优化查询性能 - 更新 setup_llmage_perms.sh 添加新端点权限 - 生成生产迁移SQL: scripts/migrate_llmusage_history.sql
This commit is contained in:
parent
e98d9fbce0
commit
07b4893252
@ -7,8 +7,11 @@
|
|||||||
"exclouded": ["id"],
|
"exclouded": ["id"],
|
||||||
"alters": {}
|
"alters": {}
|
||||||
},
|
},
|
||||||
"editexclouded": [
|
"editexclouded": ["id"],
|
||||||
"id"
|
"editable": {
|
||||||
]
|
"new_data_url": "{{entire_url('../api/llmusage_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/llmusage_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/llmusage_delete.dspy')}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
json/llmusage_accounting_failed.json
Normal file
25
json/llmusage_accounting_failed.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"tblname": "llmusage_accounting_failed",
|
||||||
|
"title": "记账失败记录",
|
||||||
|
"params": {
|
||||||
|
"sortby": "failed_time desc",
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id"],
|
||||||
|
"alters": {
|
||||||
|
"handled": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "0", "text": "未处理"},
|
||||||
|
{"value": "1", "text": "已处理"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "llmusageid", "failed_time"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/llmusage_accounting_failed_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/llmusage_accounting_failed_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/llmusage_accounting_failed_delete.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
json/llmusage_history.json
Normal file
17
json/llmusage_history.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"tblname": "llmusage_history",
|
||||||
|
"title": "模型使用历史",
|
||||||
|
"params": {
|
||||||
|
"sortby": "use_time desc",
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id"],
|
||||||
|
"alters": {}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/llmusage_history_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/llmusage_history_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/llmusage_history_delete.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -186,7 +186,7 @@ where a.llmid = b.id
|
|||||||
r.usages = output.get('usage')
|
r.usages = output.get('usage')
|
||||||
if r.usages is None:
|
if r.usages is None:
|
||||||
debug(f'{r.usages=} is None, accoiunting failed')
|
debug(f'{r.usages=} is None, accoiunting failed')
|
||||||
await llm_accoung_failed(r.id)
|
await llm_accoung_failed(r.id, reason='usages is None')
|
||||||
continue
|
continue
|
||||||
d = None
|
d = None
|
||||||
try:
|
try:
|
||||||
@ -195,7 +195,7 @@ where a.llmid = b.id
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception(f'{r.ppid=}, {r.usages=} llm_charging() failed,{e}')
|
exception(f'{r.ppid=}, {r.usages=} llm_charging() failed,{e}')
|
||||||
await llm_accoung_failed(r.id)
|
await llm_accoung_failed(r.id, reason=f'llm_charging failed: {e}')
|
||||||
continue
|
continue
|
||||||
r.amount = d.amount
|
r.amount = d.amount
|
||||||
r.cost = d.cost
|
r.cost = d.cost
|
||||||
@ -209,17 +209,135 @@ where a.llmid = b.id
|
|||||||
lus.append(r)
|
lus.append(r)
|
||||||
return lus
|
return lus
|
||||||
|
|
||||||
async def llm_accoung_failed(luid):
|
async def llm_accoung_failed(luid, reason=None):
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
async with get_sor_context(env, 'llmage') as sor:
|
async with get_sor_context(env, 'llmage') as sor:
|
||||||
await sor.U('llmusage', {
|
await sor.U('llmusage', {
|
||||||
'id': luid,
|
'id': luid,
|
||||||
'accounting_status': 'failed'
|
'accounting_status': 'failed'
|
||||||
})
|
})
|
||||||
|
# Also record in the failed accounting table for tracking
|
||||||
|
recs = await sor.R('llmusage', {'id': luid})
|
||||||
|
if recs:
|
||||||
|
r = recs[0]
|
||||||
|
failed_id = getID()
|
||||||
|
failed_rec = {
|
||||||
|
'id': failed_id,
|
||||||
|
'llmusageid': luid,
|
||||||
|
'llmid': r.llmid,
|
||||||
|
'userid': r.userid,
|
||||||
|
'userorgid': r.userorgid,
|
||||||
|
'use_date': r.use_date,
|
||||||
|
'use_time': r.use_time,
|
||||||
|
'amount': r.amount,
|
||||||
|
'cost': r.cost,
|
||||||
|
'failed_reason': reason or 'accounting failed',
|
||||||
|
'failed_time': env.timestampstr(),
|
||||||
|
'retry_count': 0,
|
||||||
|
'handled': '0'
|
||||||
|
}
|
||||||
|
await sor.C('llmusage_accounting_failed', failed_rec)
|
||||||
|
|
||||||
|
|
||||||
|
async def backup_accounted_llmusage():
|
||||||
|
"""Backup yesterday's accounted records to history table and remove from llmusage."""
|
||||||
|
env = ServerEnv()
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
ts = env.timestampstr()
|
||||||
|
batched = 0
|
||||||
|
async with get_sor_context(env, 'llmage') as sor:
|
||||||
|
# Select yesterday's accounted records
|
||||||
|
sql = """select * from llmusage
|
||||||
|
where accounting_status='accounted'
|
||||||
|
and use_date < ${yesterday}$"""
|
||||||
|
recs = await sor.sqlExe(sql, {'yesterday': yesterday})
|
||||||
|
if not recs:
|
||||||
|
debug(f'backup_accounted_llmusage: no records to backup for use_date < {yesterday}')
|
||||||
|
return 0
|
||||||
|
debug(f'backup_accounted_llmusage: {len(recs)} records to backup')
|
||||||
|
for r in recs:
|
||||||
|
history_rec = {
|
||||||
|
'id': r.id,
|
||||||
|
'llmid': r.llmid,
|
||||||
|
'use_date': r.use_date,
|
||||||
|
'use_time': r.use_time,
|
||||||
|
'userid': r.userid,
|
||||||
|
'usages': r.usages,
|
||||||
|
'ioinfo': r.ioinfo,
|
||||||
|
'transno': r.transno,
|
||||||
|
'responsed_seconds': r.responsed_seconds,
|
||||||
|
'finish_seconds': r.finish_seconds,
|
||||||
|
'status': r.status,
|
||||||
|
'taskid': r.taskid,
|
||||||
|
'amount': r.amount,
|
||||||
|
'cost': r.cost,
|
||||||
|
'userorgid': r.userorgid,
|
||||||
|
'ownerid': r.ownerid,
|
||||||
|
'accounting_status': r.accounting_status,
|
||||||
|
'backup_time': ts
|
||||||
|
}
|
||||||
|
await sor.C('llmusage_history', history_rec)
|
||||||
|
# Delete from main table
|
||||||
|
await sor.D('llmusage', {'id': r.id})
|
||||||
|
batched += 1
|
||||||
|
debug(f'backup_accounted_llmusage: backed up {batched} records')
|
||||||
|
return batched
|
||||||
|
|
||||||
|
|
||||||
|
async def get_failed_accounting_records(filters=None, page=1, page_size=50):
|
||||||
|
"""Search failed accounting records with optional filters.
|
||||||
|
|
||||||
|
filters: dict with optional keys:
|
||||||
|
- userorgid: filter by user organization
|
||||||
|
- llmid: filter by model ID
|
||||||
|
- handled: '0' or '1'
|
||||||
|
- start_date: filter use_date >= start_date
|
||||||
|
- end_date: filter use_date <= end_date
|
||||||
|
"""
|
||||||
|
env = ServerEnv()
|
||||||
|
async with get_sor_context(env, 'llmage') as sor:
|
||||||
|
conditions = []
|
||||||
|
ns = {}
|
||||||
|
if filters:
|
||||||
|
if filters.get('userorgid'):
|
||||||
|
conditions.append("userorgid=${userorgid}$")
|
||||||
|
ns['userorgid'] = filters['userorgid']
|
||||||
|
if filters.get('llmid'):
|
||||||
|
conditions.append("llmid=${llmid}$")
|
||||||
|
ns['llmid'] = filters['llmid']
|
||||||
|
if filters.get('handled') is not None:
|
||||||
|
conditions.append("handled=${handled}$")
|
||||||
|
ns['handled'] = filters['handled']
|
||||||
|
if filters.get('start_date'):
|
||||||
|
conditions.append("use_date>=${start_date}$")
|
||||||
|
ns['start_date'] = filters['start_date']
|
||||||
|
if filters.get('end_date'):
|
||||||
|
conditions.append("use_date<=${end_date}$")
|
||||||
|
ns['end_date'] = filters['end_date']
|
||||||
|
where = ""
|
||||||
|
if conditions:
|
||||||
|
where = "where " + " and ".join(conditions)
|
||||||
|
# Count total
|
||||||
|
count_sql = f"select count(*) as cnt from llmusage_accounting_failed {where}"
|
||||||
|
count_recs = await sor.sqlExe(count_sql, ns)
|
||||||
|
total = count_recs[0].cnt if count_recs else 0
|
||||||
|
# Query with pagination
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
query_sql = f"""select * from llmusage_accounting_failed {where}
|
||||||
|
order by failed_time desc limit {page_size} offset {offset}"""
|
||||||
|
recs = await sor.sqlExe(query_sql, ns)
|
||||||
|
return {
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'records': recs
|
||||||
|
}
|
||||||
|
|
||||||
async def backend_accounting():
|
async def backend_accounting():
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
debug(f'backend accounting started ...')
|
debug(f'backend accounting started ...')
|
||||||
|
backup_counter = 0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
lus = await get_accounting_llmusages()
|
lus = await get_accounting_llmusages()
|
||||||
@ -238,8 +356,16 @@ async def backend_accounting():
|
|||||||
await llm_accounting(lu)
|
await llm_accounting(lu)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception(f'{e}, {lu.id=}')
|
exception(f'{e}, {lu.id=}')
|
||||||
await llm_accoung_failed(lu.id)
|
await llm_accoung_failed(lu.id, reason=str(e))
|
||||||
|
|
||||||
|
# Run backup every 100 iterations (roughly every ~1000 seconds)
|
||||||
|
backup_counter += 1
|
||||||
|
if backup_counter >= 100:
|
||||||
|
backup_counter = 0
|
||||||
|
try:
|
||||||
|
await backup_accounted_llmusage()
|
||||||
|
except Exception as e:
|
||||||
|
exception(f'backup_accounted_llmusage failed: {e}')
|
||||||
|
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,10 @@ from .accounting import (
|
|||||||
llm_charging,
|
llm_charging,
|
||||||
get_accounting_llmusages,
|
get_accounting_llmusages,
|
||||||
backend_accounting,
|
backend_accounting,
|
||||||
llm_accounting
|
llm_accounting,
|
||||||
|
backup_accounted_llmusage,
|
||||||
|
get_failed_accounting_records,
|
||||||
|
llm_accoung_failed
|
||||||
)
|
)
|
||||||
|
|
||||||
from .asyncinference import (
|
from .asyncinference import (
|
||||||
@ -58,6 +61,8 @@ def load_llmage():
|
|||||||
env.keling_token = keling_token
|
env.keling_token = keling_token
|
||||||
env.llm_query_price = llm_query_price
|
env.llm_query_price = llm_query_price
|
||||||
env.get_llms_by_catelog_to_customer = get_llms_by_catelog_to_customer
|
env.get_llms_by_catelog_to_customer = get_llms_by_catelog_to_customer
|
||||||
|
env.backup_accounted_llmusage = backup_accounted_llmusage
|
||||||
|
env.get_failed_accounting_records = get_failed_accounting_records
|
||||||
rf = RegisterFunction()
|
rf = RegisterFunction()
|
||||||
rf.register('jimeng_auth_headers', jimeng_auth_headers)
|
rf.register('jimeng_auth_headers', jimeng_auth_headers)
|
||||||
|
|
||||||
|
|||||||
125
models/llmusage_accounting_failed.json
Normal file
125
models/llmusage_accounting_failed.json
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "llmusage_accounting_failed",
|
||||||
|
"title": "记账失败记录",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "llmusageid",
|
||||||
|
"title": "使用记录id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "llmid",
|
||||||
|
"title": "模型id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userid",
|
||||||
|
"title": "用户id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userorgid",
|
||||||
|
"title": "用户机构id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "use_date",
|
||||||
|
"title": "使用日期",
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "use_time",
|
||||||
|
"title": "使用时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"title": "交易金额",
|
||||||
|
"type": "double",
|
||||||
|
"length": 18,
|
||||||
|
"dec": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cost",
|
||||||
|
"title": "交易成本",
|
||||||
|
"type": "double",
|
||||||
|
"length": 18,
|
||||||
|
"dec": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "failed_reason",
|
||||||
|
"title": "失败原因",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "failed_time",
|
||||||
|
"title": "失败时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "retry_count",
|
||||||
|
"title": "重试次数",
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "handled",
|
||||||
|
"title": "是否已处理",
|
||||||
|
"type": "str",
|
||||||
|
"length": 1,
|
||||||
|
"default": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "handled_time",
|
||||||
|
"title": "处理时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "handled_note",
|
||||||
|
"title": "处理备注",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_laf_use_date",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["use_date"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_laf_userorgid",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["userorgid"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_laf_llmid",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["llmid"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_laf_handled",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["handled"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_laf_failed_time",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["failed_time"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
141
models/llmusage_history.json
Normal file
141
models/llmusage_history.json
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "llmusage_history",
|
||||||
|
"title": "模型使用历史记录",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "llmid",
|
||||||
|
"title": "模型id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "use_date",
|
||||||
|
"title": "使用日期",
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "use_time",
|
||||||
|
"title": "使用时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userid",
|
||||||
|
"title": "用户id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "usages",
|
||||||
|
"title": "使用信息",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ioinfo",
|
||||||
|
"title": "交互内容",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transno",
|
||||||
|
"title": "交易号",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "responsed_seconds",
|
||||||
|
"title": "响应时间",
|
||||||
|
"type": "double",
|
||||||
|
"length": 18,
|
||||||
|
"dec": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "finish_seconds",
|
||||||
|
"title": "结束时间",
|
||||||
|
"type": "double",
|
||||||
|
"length": 18,
|
||||||
|
"dec": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "taskid",
|
||||||
|
"title": "任务号",
|
||||||
|
"type": "str",
|
||||||
|
"length": 256
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"title": "交易金额",
|
||||||
|
"type": "double",
|
||||||
|
"length": 18,
|
||||||
|
"dec": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cost",
|
||||||
|
"title": "交易成本",
|
||||||
|
"type": "double",
|
||||||
|
"length": 18,
|
||||||
|
"dec": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userorgid",
|
||||||
|
"title": "用户机构id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ownerid",
|
||||||
|
"title": "模型机构id",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "accounting_status",
|
||||||
|
"title": "记账状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backup_time",
|
||||||
|
"title": "备份时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_lh_use_date",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["use_date"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_lh_userorgid",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["userorgid"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_lh_llmid",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["llmid"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_lh_backup_time",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["backup_time"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
86
scripts/migrate_llmusage_history.sql
Normal file
86
scripts/migrate_llmusage_history.sql
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- llmage 模块数据库迁移脚本
|
||||||
|
-- 新增: llmusage_history (使用历史表) + llmusage_accounting_failed (记账失败表)
|
||||||
|
-- 日期: 2026-05-24
|
||||||
|
-- 执行前请备份数据库!
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 1. 创建 llmusage_history 表 (已记账记录的历史归档)
|
||||||
|
CREATE TABLE IF NOT EXISTS llmusage_history
|
||||||
|
(
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT 'id',
|
||||||
|
`llmid` VARCHAR(32) COMMENT '模型id',
|
||||||
|
`use_date` date COMMENT '使用日期',
|
||||||
|
`use_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '使用时间',
|
||||||
|
`userid` VARCHAR(32) COMMENT '用户id',
|
||||||
|
`usages` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '使用信息',
|
||||||
|
`ioinfo` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '交互内容',
|
||||||
|
`transno` VARCHAR(32) COMMENT '交易号',
|
||||||
|
`responsed_seconds` double(18,3) COMMENT '响应时间',
|
||||||
|
`finish_seconds` double(18,3) COMMENT '结束时间',
|
||||||
|
`status` VARCHAR(32) COMMENT '状态',
|
||||||
|
`taskid` VARCHAR(256) COMMENT '任务号',
|
||||||
|
`amount` double(18,5) COMMENT '交易金额',
|
||||||
|
`cost` double(18,5) COMMENT '交易成本',
|
||||||
|
`userorgid` VARCHAR(32) COMMENT '用户机构id',
|
||||||
|
`ownerid` VARCHAR(32) COMMENT '模型机构id',
|
||||||
|
`accounting_status` VARCHAR(32) COMMENT '记账状态',
|
||||||
|
`backup_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '备份时间',
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
)
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci
|
||||||
|
engine=innodb
|
||||||
|
COMMENT '模型使用历史记录';
|
||||||
|
|
||||||
|
CREATE INDEX idx_lh_use_date ON llmusage_history(use_date);
|
||||||
|
CREATE INDEX idx_lh_userorgid ON llmusage_history(userorgid);
|
||||||
|
CREATE INDEX idx_lh_llmid ON llmusage_history(llmid);
|
||||||
|
CREATE INDEX idx_lh_backup_time ON llmusage_history(backup_time);
|
||||||
|
|
||||||
|
-- 2. 创建 llmusage_accounting_failed 表 (记账失败记录)
|
||||||
|
CREATE TABLE IF NOT EXISTS llmusage_accounting_failed
|
||||||
|
(
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT 'id',
|
||||||
|
`llmusageid` VARCHAR(32) COMMENT '使用记录id',
|
||||||
|
`llmid` VARCHAR(32) COMMENT '模型id',
|
||||||
|
`userid` VARCHAR(32) COMMENT '用户id',
|
||||||
|
`userorgid` VARCHAR(32) COMMENT '用户机构id',
|
||||||
|
`use_date` date COMMENT '使用日期',
|
||||||
|
`use_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '使用时间',
|
||||||
|
`amount` double(18,5) COMMENT '交易金额',
|
||||||
|
`cost` double(18,5) COMMENT '交易成本',
|
||||||
|
`failed_reason` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '失败原因',
|
||||||
|
`failed_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '失败时间',
|
||||||
|
`retry_count` int COMMENT '重试次数',
|
||||||
|
`handled` VARCHAR(1) DEFAULT '0' COMMENT '是否已处理',
|
||||||
|
`handled_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '处理时间',
|
||||||
|
`handled_note` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '处理备注',
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
)
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci
|
||||||
|
engine=innodb
|
||||||
|
COMMENT '记账失败记录';
|
||||||
|
|
||||||
|
CREATE INDEX idx_laf_use_date ON llmusage_accounting_failed(use_date);
|
||||||
|
CREATE INDEX idx_laf_userorgid ON llmusage_accounting_failed(userorgid);
|
||||||
|
CREATE INDEX idx_laf_llmid ON llmusage_accounting_failed(llmid);
|
||||||
|
CREATE INDEX idx_laf_handled ON llmusage_accounting_failed(handled);
|
||||||
|
CREATE INDEX idx_laf_failed_time ON llmusage_accounting_failed(failed_time);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 验证步骤(执行后运行):
|
||||||
|
-- 1. 确认表创建成功:
|
||||||
|
-- SHOW TABLES LIKE 'llmusage_%';
|
||||||
|
-- 2. 确认索引创建成功:
|
||||||
|
-- SHOW INDEX FROM llmusage_history;
|
||||||
|
-- SHOW INDEX FROM llmusage_accounting_failed;
|
||||||
|
-- 3. 确认表结构正确:
|
||||||
|
-- DESCRIBE llmusage_history;
|
||||||
|
-- DESCRIBE llmusage_accounting_failed;
|
||||||
|
-- ============================================================
|
||||||
|
-- 回滚步骤(如需撤销):
|
||||||
|
-- DROP TABLE IF EXISTS llmusage_history;
|
||||||
|
-- DROP TABLE IF EXISTS llmusage_accounting_failed;
|
||||||
|
-- ============================================================
|
||||||
@ -60,6 +60,42 @@ for p in "${LLM_API_MAP_PATHS[@]}"; do
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " llmage: 记账失败记录权限初始化"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
FAILED_ACCOUNTING_PATHS=(
|
||||||
|
"/llmage/failed_accounting.ui"
|
||||||
|
"/llmage/api/failed_accounting_list.dspy"
|
||||||
|
"/llmage/api/llmusage_accounting_failed_create.dspy"
|
||||||
|
"/llmage/api/llmusage_accounting_failed_update.dspy"
|
||||||
|
"/llmage/api/llmusage_accounting_failed_delete.dspy"
|
||||||
|
)
|
||||||
|
|
||||||
|
for p in "${FAILED_ACCOUNTING_PATHS[@]}"; do
|
||||||
|
for role in "${PERM_ROLES[@]}"; do
|
||||||
|
set_perm "${role}" "${p}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " llmage: llmusage CRUD权限初始化"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
LLMUSAGE_PATHS=(
|
||||||
|
"/llmage/api/llmusage_create.dspy"
|
||||||
|
"/llmage/api/llmusage_update.dspy"
|
||||||
|
"/llmage/api/llmusage_delete.dspy"
|
||||||
|
)
|
||||||
|
|
||||||
|
for p in "${LLMUSAGE_PATHS[@]}"; do
|
||||||
|
for role in "${PERM_ROLES[@]}"; do
|
||||||
|
set_perm "${role}" "${p}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " 权限配置完成,共设置 ${COUNT} 条权限"
|
echo " 权限配置完成,共设置 ${COUNT} 条权限"
|
||||||
|
|||||||
76
wwwroot/api/failed_accounting_list.dspy
Normal file
76
wwwroot/api/failed_accounting_list.dspy
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
result = {'success': False, 'rows': [], 'total': 0, 'page': 1, 'page_size': 50}
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbname = get_module_dbname('llmage')
|
||||||
|
user_orgid = await get_userorgid()
|
||||||
|
|
||||||
|
# Extract filter parameters from params_kw
|
||||||
|
filters = {}
|
||||||
|
if params_kw.get('userorgid'):
|
||||||
|
filters['userorgid'] = params_kw.get('userorgid')
|
||||||
|
if params_kw.get('llmid'):
|
||||||
|
filters['llmid'] = params_kw.get('llmid')
|
||||||
|
if params_kw.get('handled') is not None:
|
||||||
|
filters['handled'] = params_kw.get('handled')
|
||||||
|
if params_kw.get('start_date'):
|
||||||
|
filters['start_date'] = params_kw.get('start_date')
|
||||||
|
if params_kw.get('end_date'):
|
||||||
|
filters['end_date'] = params_kw.get('end_date')
|
||||||
|
|
||||||
|
page = int(params_kw.get('page', 1))
|
||||||
|
page_size = int(params_kw.get('page_size', 50))
|
||||||
|
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
# Build dynamic SQL
|
||||||
|
conditions = []
|
||||||
|
ns = {}
|
||||||
|
|
||||||
|
# Default: show unhandled records
|
||||||
|
if 'handled' not in filters:
|
||||||
|
conditions.append("handled='0'")
|
||||||
|
|
||||||
|
if filters.get('userorgid'):
|
||||||
|
conditions.append("userorgid=${userorgid}$")
|
||||||
|
ns['userorgid'] = filters['userorgid']
|
||||||
|
if filters.get('llmid'):
|
||||||
|
conditions.append("llmid=${llmid}$")
|
||||||
|
ns['llmid'] = filters['llmid']
|
||||||
|
if filters.get('handled') is not None:
|
||||||
|
conditions.append("handled=${handled}$")
|
||||||
|
ns['handled'] = filters['handled']
|
||||||
|
if filters.get('start_date'):
|
||||||
|
conditions.append("use_date>=${start_date}$")
|
||||||
|
ns['start_date'] = filters['start_date']
|
||||||
|
if filters.get('end_date'):
|
||||||
|
conditions.append("use_date<=${end_date}$")
|
||||||
|
ns['end_date'] = filters['end_date']
|
||||||
|
|
||||||
|
where = ""
|
||||||
|
if conditions:
|
||||||
|
where = "where " + " and ".join(conditions)
|
||||||
|
|
||||||
|
# Count total
|
||||||
|
count_sql = f"select count(*) as cnt from llmusage_accounting_failed {where}"
|
||||||
|
count_recs = await sor.sqlExe(count_sql, ns)
|
||||||
|
total = count_recs[0].cnt if count_recs else 0
|
||||||
|
|
||||||
|
# Query with pagination
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
query_sql = f"""select * from llmusage_accounting_failed {where}
|
||||||
|
order by failed_time desc limit {page_size} offset {offset}"""
|
||||||
|
recs = await sor.sqlExe(query_sql, ns)
|
||||||
|
|
||||||
|
result['rows'] = [dict(r) for r in (recs or [])]
|
||||||
|
result['total'] = total
|
||||||
|
result['page'] = page
|
||||||
|
result['page_size'] = page_size
|
||||||
|
result['success'] = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result['error'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
18
wwwroot/api/llmusage_accounting_failed_create.dspy
Normal file
18
wwwroot/api/llmusage_accounting_failed_create.dspy
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/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
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('llmusage_accounting_failed', data)
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = '创建成功'
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
19
wwwroot/api/llmusage_accounting_failed_delete.dspy
Normal file
19
wwwroot/api/llmusage_accounting_failed_delete.dspy
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
|
||||||
|
result = {'success': False, 'message': ''}
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbname = get_module_dbname('llmage')
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
rid = params_kw.get('id')
|
||||||
|
if not rid:
|
||||||
|
result['message'] = '缺少id参数'
|
||||||
|
else:
|
||||||
|
await sor.D('llmusage_accounting_failed', {'id': rid})
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = '删除成功'
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
26
wwwroot/api/llmusage_accounting_failed_update.dspy
Normal file
26
wwwroot/api/llmusage_accounting_failed_update.dspy
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
|
result = {'success': False, 'message': ''}
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbname = get_module_dbname('llmage')
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
data = params_kw
|
||||||
|
rid = data.pop('id', None)
|
||||||
|
if not rid:
|
||||||
|
result['message'] = '缺少id参数'
|
||||||
|
else:
|
||||||
|
data['id'] = rid
|
||||||
|
# Mark as handled - set handled_time
|
||||||
|
if data.get('handled') == '1' and not data.get('handled_time'):
|
||||||
|
env = ServerEnv()
|
||||||
|
data['handled_time'] = env.timestampstr()
|
||||||
|
await sor.U('llmusage_accounting_failed', data)
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = '更新成功'
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
18
wwwroot/api/llmusage_create.dspy
Normal file
18
wwwroot/api/llmusage_create.dspy
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/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
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('llmusage', data)
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = '创建成功'
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
19
wwwroot/api/llmusage_delete.dspy
Normal file
19
wwwroot/api/llmusage_delete.dspy
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
|
||||||
|
result = {'success': False, 'message': ''}
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbname = get_module_dbname('llmage')
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
rid = params_kw.get('id')
|
||||||
|
if not rid:
|
||||||
|
result['message'] = '缺少id参数'
|
||||||
|
else:
|
||||||
|
await sor.D('llmusage', {'id': rid})
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = '删除成功'
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
21
wwwroot/api/llmusage_update.dspy
Normal file
21
wwwroot/api/llmusage_update.dspy
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/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
|
||||||
|
rid = data.pop('id', None)
|
||||||
|
if not rid:
|
||||||
|
result['message'] = '缺少id参数'
|
||||||
|
else:
|
||||||
|
data['id'] = rid
|
||||||
|
await sor.U('llmusage', data)
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = '更新成功'
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = str(e)
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False, default=str)
|
||||||
112
wwwroot/failed_accounting.ui
Normal file
112
wwwroot/failed_accounting.ui
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "16px",
|
||||||
|
"spacing": 12
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Title2",
|
||||||
|
"options": {
|
||||||
|
"text": "记账失败记录",
|
||||||
|
"halign": "left"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"spacing": 12,
|
||||||
|
"alignItems": "flex-end"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"spacing": 4},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "开始日期", "fontSize": "12px"}},
|
||||||
|
{"widgettype": "DatePicker", "id": "start_date", "options": {"width": "150px"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"spacing": 4},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "结束日期", "fontSize": "12px"}},
|
||||||
|
{"widgettype": "DatePicker", "id": "end_date", "options": {"width": "150px"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"spacing": 4},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "处理状态", "fontSize": "12px"}},
|
||||||
|
{
|
||||||
|
"widgettype": "Combobox",
|
||||||
|
"id": "handled_filter",
|
||||||
|
"options": {
|
||||||
|
"width": "120px",
|
||||||
|
"data": [
|
||||||
|
{"value": "", "text": "全部"},
|
||||||
|
{"value": "0", "text": "未处理"},
|
||||||
|
{"value": "1", "text": "已处理"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"spacing": 4},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "", "fontSize": "12px"}},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"id": "search_btn",
|
||||||
|
"options": {
|
||||||
|
"text": "查询",
|
||||||
|
"bgcolor": "#1976d2",
|
||||||
|
"color": "#ffffff",
|
||||||
|
"width": "80px"
|
||||||
|
},
|
||||||
|
"binds": [{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "failed_table",
|
||||||
|
"script": "var sd = this.root.getElementById('start_date'); var ed = this.root.getElementById('end_date'); var hf = this.root.getElementById('handled_filter'); var params = {handled: hf.value}; if(sd.value) params.start_date = sd.value; if(ed.value) params.end_date = ed.value; this.root.getElementById('failed_table').load(params);"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "DataViewer",
|
||||||
|
"id": "failed_table",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/llmage/api/failed_accounting_list.dspy')}}",
|
||||||
|
"title": "失败记录列表",
|
||||||
|
"pageSize": 20,
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "ID", "hidden": true},
|
||||||
|
{"name": "llmusageid", "title": "使用记录ID", "width": "120px"},
|
||||||
|
{"name": "llmid", "title": "模型ID", "width": "120px"},
|
||||||
|
{"name": "userid", "title": "用户ID", "width": "120px"},
|
||||||
|
{"name": "userorgid", "title": "机构ID", "width": "120px"},
|
||||||
|
{"name": "use_date", "title": "使用日期", "width": "110px"},
|
||||||
|
{"name": "use_time", "title": "使用时间", "width": "160px"},
|
||||||
|
{"name": "amount", "title": "金额", "width": "80px"},
|
||||||
|
{"name": "cost", "title": "成本", "width": "80px"},
|
||||||
|
{"name": "failed_reason", "title": "失败原因", "width": "30%"},
|
||||||
|
{"name": "failed_time", "title": "失败时间", "width": "160px"},
|
||||||
|
{"name": "retry_count", "title": "重试次数", "width": "80px"},
|
||||||
|
{"name": "handled", "title": "状态", "width": "80px",
|
||||||
|
"formatter": "function(v){return v==='1'?'已处理':'未处理';}"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -114,6 +114,53 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#1e3a5f",
|
||||||
|
"padding": "24px",
|
||||||
|
"cursor": "pointer",
|
||||||
|
"borderRadius": "8px"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.llmage_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/llmage/failed_accounting.ui')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"40\" height=\"40\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ef5350\" stroke-width=\"2\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>",
|
||||||
|
"width": "40px",
|
||||||
|
"height": "40px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Title4",
|
||||||
|
"options": {
|
||||||
|
"text": "记账失败记录",
|
||||||
|
"color": "#ffffff",
|
||||||
|
"marginTop": "12px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "查看和检索记账失败的记录",
|
||||||
|
"color": "#ef5350",
|
||||||
|
"fontSize": "14px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user