feat: add recover_usages button to accounting failed page
- Add recover_usages.dspy: reads ioinfo files, extracts usage from last output, writes back to llmusage.usages field - Add toolbar button in llmusage_accounting_failed/index.ui - Register new path in load_path.py RBAC config - Force-add dspy (parent dir in .gitignore for CRUD auto-gen)
This commit is contained in:
parent
2789f191d4
commit
0d2b39ddd7
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,5 +5,6 @@ wwwroot/llm_api_map/
|
||||
wwwroot/llmcatelog_list/
|
||||
wwwroot/llmusage/
|
||||
wwwroot/llmusage_accounting_failed/
|
||||
!wwwroot/llmusage_accounting_failed/recover_usages.dspy
|
||||
wwwroot/llmusage_history/
|
||||
build/
|
||||
|
||||
26
scripts/fix_m3_pricing.sql
Normal file
26
scripts/fix_m3_pricing.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- ============================================================
|
||||
-- 修复 MiniMax-M3 定价重复条目
|
||||
-- 问题:步骤11b的CONCAT重复执行导致M3条目重复
|
||||
-- 解决:删除没有prompt_tokens filter的旧M3条目(前3条)
|
||||
-- ============================================================
|
||||
|
||||
UPDATE `pricing_program_timing`
|
||||
SET `pricing_data` = REPLACE(`pricing_data`,
|
||||
'- price_factors: prompt_tokens
|
||||
unit_prices: 2.1
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
- price_factors: completion_tokens
|
||||
unit_prices: 8.4
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
- price_factors: cached_tokens
|
||||
unit_prices: 0.42
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
|
||||
', '')
|
||||
WHERE `ppid` = '5jmzupARABxkDFwUraFiQ' AND `enabled_date` = '2026-04-12';
|
||||
@ -153,6 +153,7 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}/llmusage_accounting_failed/add_llmusage_accounting_failed.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/delete_llmusage_accounting_failed.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/get_llmusage_accounting_failed.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/recover_usages.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/update_llmusage_accounting_failed.dspy",
|
||||
|
||||
# CRUD 子目录 — llmusage_history/
|
||||
|
||||
@ -65,6 +65,48 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"css": "card", "padding": "4px 8px", "cheight": 3
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"id": "btn_recover_usages",
|
||||
"options": {
|
||||
"label": "从IO文件恢复Usages",
|
||||
"css": "primary"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urldata",
|
||||
"target": "msg_area",
|
||||
"options": {
|
||||
"url": "{{entire_url('./recover_usages.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
},
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "method",
|
||||
"target": "llmusage_accounting_failed_tbl",
|
||||
"method": "render"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "msg_area",
|
||||
"options": {
|
||||
"width": "100%", "css": "filler"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "llmusage_accounting_failed_tbl",
|
||||
"widgettype": "Tabular",
|
||||
|
||||
126
wwwroot/llmusage_accounting_failed/recover_usages.dspy
Normal file
126
wwwroot/llmusage_accounting_failed/recover_usages.dspy
Normal file
@ -0,0 +1,126 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
limit = int(ns.get('limit') or 100)
|
||||
single_id = ns.get('id') or None
|
||||
|
||||
from ahserver.filestorage import FileStorage
|
||||
import os
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('llmage')
|
||||
|
||||
details = []
|
||||
recovered = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
try:
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
if single_id:
|
||||
sql = """select a.id, a.llmid, a.ioinfo, a.status, a.use_date,
|
||||
b.model
|
||||
from llmusage a
|
||||
left join llm b on a.llmid = b.id
|
||||
where a.id = ${id}$"""
|
||||
params = {'id': single_id}
|
||||
else:
|
||||
sql = """select a.id, a.llmid, a.ioinfo, a.status, a.use_date,
|
||||
b.model
|
||||
from llmusage a
|
||||
left join llm b on a.llmid = b.id
|
||||
where a.usages is null
|
||||
and a.status = 'SUCCEEDED'
|
||||
order by a.use_date desc"""
|
||||
params = {'page': 1, 'rows': limit}
|
||||
|
||||
recs = await sor.sqlExe(sql, params)
|
||||
if isinstance(recs, dict):
|
||||
rows = recs.get('rows', [])
|
||||
else:
|
||||
rows = recs if recs else []
|
||||
|
||||
if not rows:
|
||||
return {
|
||||
"widgettype": "Message",
|
||||
"options": {
|
||||
"title": "恢复Usages",
|
||||
"cwidth": 20,
|
||||
"cheight": 5,
|
||||
"timeout": 5,
|
||||
"message": "没有找到需要恢复的记录"
|
||||
}
|
||||
}
|
||||
|
||||
fs = FileStorage()
|
||||
|
||||
for r in rows:
|
||||
rid = r.id if hasattr(r, 'id') else r.get('id', '')
|
||||
model = r.model if hasattr(r, 'model') else r.get('model', '')
|
||||
ioinfo = r.ioinfo if hasattr(r, 'ioinfo') else r.get('ioinfo', None)
|
||||
|
||||
if not ioinfo:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
real_path = fs.realPath(ioinfo)
|
||||
if not os.path.isfile(real_path):
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
with open(real_path, 'r', encoding='utf-8') as f:
|
||||
io_data = json.load(f)
|
||||
|
||||
outputs = io_data.get('output', [])
|
||||
if not outputs:
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
# 从最后一条output中获取usage
|
||||
usage = None
|
||||
for out in reversed(outputs):
|
||||
if isinstance(out, dict) and out.get('usage'):
|
||||
usage = out['usage']
|
||||
break
|
||||
|
||||
if not usage:
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
usages_str = json.dumps(usage, ensure_ascii=False)
|
||||
await sor.U('llmusage', {
|
||||
'id': rid,
|
||||
'usages': usages_str
|
||||
})
|
||||
recovered += 1
|
||||
|
||||
except Exception as e:
|
||||
debug(f'recover_usages error for {rid}: {e}')
|
||||
failed += 1
|
||||
|
||||
except Exception as e:
|
||||
exception(f'recover_usages error: {e}')
|
||||
return {
|
||||
"widgettype": "Error",
|
||||
"options": {
|
||||
"title": "恢复Usages失败",
|
||||
"cwidth": 20,
|
||||
"cheight": 5,
|
||||
"timeout": 5,
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
total = recovered + failed + skipped
|
||||
msg = f"处理 {total} 条: 恢复成功 {recovered}, 失败 {failed}, 跳过 {skipped}"
|
||||
|
||||
return {
|
||||
"widgettype": "Message",
|
||||
"options": {
|
||||
"title": "恢复Usages完成",
|
||||
"cwidth": 24,
|
||||
"cheight": 5,
|
||||
"timeout": 6,
|
||||
"message": msg
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user