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:
yumoqing 2026-05-24 13:55:48 +08:00
parent e98d9fbce0
commit 07b4893252
18 changed files with 931 additions and 11 deletions

View File

@ -1,14 +1,17 @@
{
"tblname": "llmusage",
"title":"模型使用",
"title": "模型使用",
"params": {
"sortby":"use_time desc",
"sortby": "use_time desc",
"browserfields": {
"exclouded": ["id"],
"alters": {}
},
"editexclouded": [
"id"
]
"editexclouded": ["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')}}"
}
}
}

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

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

View File

@ -186,7 +186,7 @@ where a.llmid = b.id
r.usages = output.get('usage')
if r.usages is None:
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
d = None
try:
@ -195,7 +195,7 @@ where a.llmid = b.id
except Exception as 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
r.amount = d.amount
r.cost = d.cost
@ -209,17 +209,135 @@ where a.llmid = b.id
lus.append(r)
return lus
async def llm_accoung_failed(luid):
async def llm_accoung_failed(luid, reason=None):
env = ServerEnv()
async with get_sor_context(env, 'llmage') as sor:
await sor.U('llmusage', {
'id': luid,
'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():
env = ServerEnv()
debug(f'backend accounting started ...')
backup_counter = 0
while True:
try:
lus = await get_accounting_llmusages()
@ -238,8 +356,16 @@ async def backend_accounting():
await llm_accounting(lu)
except Exception as e:
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)

View File

@ -27,7 +27,10 @@ from .accounting import (
llm_charging,
get_accounting_llmusages,
backend_accounting,
llm_accounting
llm_accounting,
backup_accounted_llmusage,
get_failed_accounting_records,
llm_accoung_failed
)
from .asyncinference import (
@ -58,6 +61,8 @@ def load_llmage():
env.keling_token = keling_token
env.llm_query_price = llm_query_price
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.register('jimeng_auth_headers', jimeng_auth_headers)

View 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"]
}
]
}

View 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"]
}
]
}

View 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;
-- ============================================================

View File

@ -60,6 +60,42 @@ for p in "${LLM_API_MAP_PATHS[@]}"; do
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 " 权限配置完成,共设置 ${COUNT} 条权限"

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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'?'已处理':'未处理';}"}
]
}
}
]
}

View File

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