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
@ -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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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} 条权限"
|
||||
|
||||
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